diff --git a/src/app/(dashboard)/admin/faq/page.tsx b/src/app/(dashboard)/admin/faq/page.tsx new file mode 100644 index 00000000..63fe5808 --- /dev/null +++ b/src/app/(dashboard)/admin/faq/page.tsx @@ -0,0 +1,87 @@ +'use client'; + +import { Button, Title } from '@/components/UI'; +import { useAppDispatch, useAppSelector } from '@/lib/hooks'; +import { Faq } from '@/types'; +import { useEffect, useMemo, useState } from 'react'; +import styles from './style.module.scss'; +import { fetchFaqs, deleteFaqApi } from '@/modules/admin'; +import FaqModal from '@/components/dashboard/FaqModal'; + +const AdminFaqPage = () => { + const dispatch = useAppDispatch(); + const faqs = useAppSelector((state) => state.admin.faqs); + const [selectedFaq, setSelectedFaq] = useState(null); + const [createNewFAQ, setCreateNewFAQ] = useState(false); + + // Charger les FAQ au montage + useEffect(() => { + dispatch(fetchFaqs()); + }, [dispatch]); + + // Regrouper par catégorie + const groupedFaqs = useMemo(() => { + if (!faqs) return {}; + return faqs.reduce>((acc, faq) => { + if (!acc[faq.category]) acc[faq.category] = []; + acc[faq.category].push(faq); + return acc; + }, {}); + }, [faqs]); + + const handleDelete = (id: string) => { + dispatch(deleteFaqApi(id)); + }; + + return ( +
+
+ + FAQ + + +
+ +
+ {Object.keys(groupedFaqs).length > 0 ? ( + Object.keys(groupedFaqs).map((category) => ( +
+

{category}

+ {groupedFaqs[category].map((faq) => ( +
+

+ {faq.display ? '✅' : '🚫'}{' '} + {faq.question} +

+

{faq.answer}

+
+ + +
+
+ ))} +
+ )) + ) : ( +

Aucune FAQ disponible

+ )} +
+ + {(selectedFaq !== null || createNewFAQ) && ( + { + setSelectedFaq(null); + setCreateNewFAQ(false); + }} + /> + )} +
+ ); +}; + +export default AdminFaqPage; diff --git a/src/app/(dashboard)/admin/faq/style.module.scss b/src/app/(dashboard)/admin/faq/style.module.scss new file mode 100644 index 00000000..498fdbf6 --- /dev/null +++ b/src/app/(dashboard)/admin/faq/style.module.scss @@ -0,0 +1,61 @@ +.faq { + padding: 3rem 5rem; + + .titleContainer { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + + > * { + max-width: 50%; + margin-bottom: 0; + + > * { + margin-left: 1rem; + } + } + } + + .squareContainer { + display: flex; + flex-direction: column; + gap: 2rem; + } + + .categoryBlock { + background-color: #2d1b4e; + color: white; + padding: 2rem; + border-radius: 10px; + + .categoryTitle { + font-size: 1.4rem; + font-weight: 600; + margin-bottom: 1.5rem; + } + + .faqItem { + background: rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 1rem 1.5rem; + margin-bottom: 1rem; + + .question { + font-weight: 600; + } + + .answer { + margin-top: 0.5rem; + font-size: 0.95rem; + opacity: 0.9; + } + + .actions { + margin-top: 0.75rem; + display: flex; + gap: 0.5rem; + } + } + } +} diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index 784bce06..e3871f97 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -51,6 +51,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { menu.push({ title: 'Boutique', href: '/admin/shop' }); menu.push({ title: 'Partenaires', href: '/admin/partners' }); menu.push({ title: 'Mails', href: '/admin/mails' }); + menu.push({ title: 'Faq', href: '/admin/faq' }); menu.push({ title: 'Paramètres', href: '/admin/settings' }); } if (permissions.includes(Permission.orga)) { diff --git a/src/app/help/page.tsx b/src/app/help/page.tsx index b848735c..e289fb80 100644 --- a/src/app/help/page.tsx +++ b/src/app/help/page.tsx @@ -1,198 +1,53 @@ 'use client'; import styles from './style.module.scss'; import { ReactNode, useEffect, useState } from 'react'; - +import { useDispatch, useSelector } from 'react-redux'; import { Title, Input, Textarea, Button, Select, Collapse } from '@/components/UI'; import { sendMessage } from '@/utils/contact'; import { usePathname } from 'next/navigation'; import Link from 'next/link'; import { useAppSelector } from '@/lib/hooks'; import { uploadsUrl } from '@/utils/environment'; +import { fetchFaqs } from '@/modules/admin'; +import { RootState } from '@/lib/store'; interface Question { question: string; - answer: ReactNode; + answer: string | React.ReactNode; } - -interface Faq { - [key: string]: Question[]; +interface FaqByCategory { + [category: string]: Question[]; } -const faq: Faq = { - Général: [ - { - question: 'Quand commencent les tournois ?', - answer: ( - <> - Les tournois commencent le samedi à 10h mais les participants des tournois devront être présents le samedi à - 9h pour effectuer un check-in. - - ), - }, - { - question: "Quel est l'âge minimum pour participer aux tournois ?", - answer: ( - <> - Pour participer aux tournois, il faut avoir au minimum 16 ans lors de l'évènement. Il faudra - que tu présentes l'autorisation parentale - - {' '} - disponible ici - - , - ainsi qu'une photocopie de la pièce d'identité de ton responsable légal et de la tienne - {' '} - avant d'accéder à l'UTT Arena. - - ), - }, - { - question: 'Puis-je streamer pendant les tournois ?', - answer: ( - <> - Seulement si tu as reçu un mail t'en donnant l'autorisation suite à ta demande. Un formulaire de demande de - streaming est{' '} - - disponible ici - - . L'équipe de l'UTT Arena se réserve le droit d'accepter ou refuser ta demande, et toute personne faisant du - streaming sans autorisation validée par l'équipe se verra sanctionnée. L'équipe de l'UTT Arena communiquera - prochainement sur les conditions de streaming. - - ), - }, - { - question: 'Où puis-je trouver des joueurs pour monter une équipe ?', - answer: ( - <> - Sur le discord de l'UTT Arena tu trouveras sûrement d'autres joueurs qui cherchent une équipe.{' '} - Tu peux rejoindre notre Discord ici. - - ), - }, - { - question: "Qui contacter si j'ai des questions avant ou pendant l'UTT Arena ?", - answer: ( - <> - N'hésite pas à demander aux responsables de ton tournoi si tu as une quelconque question ! Tu peux les - identifier en taguant @Staff tournoi [nom du tournoi] sur le Discord de l'UTT Arena. - - ), - }, - { - question: 'Où puis-je trouver les règlements des tournois (formats, règles, ...) ?', - answer: ( - <> - Tu pourras télécharger le règlement du tournoi qui t'intéresse dans l'onglet du tournoi concerné. Il est - important de le lire attentivement ! - - ), - }, - { - question: "Puis-je assister à l'UTT Arena en tant que spectateur ?", - answer: ( - <> - Cette année, les spectateurs voulant assister à l'UTT Arena devront acheter leur place à la billetterie de - l'UTT Arena. Venez profiter de l'ambiance de la scène, du Stand Console, avec diverses activités autour du jeu - vidéo, et visiter les stands de nos partenaires. Vous pouvez prendre votre place en vous inscrivant sur ce - site.
- Si tu es mineur et que tu souhaites participer à l'événement, il faudra que tu présentes l'autorisation - parentale disponible ici, ainsi - qu'une photocopie de la pièce d'identité de ton responsable légal et de la tienne avant d'accéder à l'UTT - Arena. - - ), - }, - ], - Inscription: [ - { - question: 'Comment savoir si mon équipe est inscrite ?', - answer: ( - <> - Il faut que l'équipe soit complète et que tous les joueurs de l'équipe aient payé leur place. - L'équipe est ensuite verrouillée, le statut dans l'onglet "équipe" devient vert et ton équipe est inscrite. -
- Attention : kicker un joueur de l'équipe, quitter ou dissoudre l'équipe la déverrouillera et - vous fera perdre votre place. - - ), - }, - { - question: 'Combien coûte la participation à un tournoi ?', - answer: ( - <> - - - ), - }, - { - question: "Dans combien de tournois puis-je m'inscrire ?", - answer: "Les tournois se jouant en simultané, tu ne peux t'inscrire qu'à un seul tournoi.", - }, - ], - Paiement: [ - { - question: 'Puis-je payer en espèces ?', - answer: - "Payer en espèce sur place n'est possible que pour les places spectateurs. Si vous êtes un joueur, vous devez impérativement payer en ligne via la billetterie.", - }, - { - question: 'Puis-je payer par PayPal ?', - answer: 'Non, sur le site seul le paiement par carte bancaire est disponible.', - }, - { - question: 'Puis-je payer pour toute mon équipe ?', - answer: - "Oui, cette année il est possible de payer pour d'autres joueurs. Mais il faut qu'ils aient d'abord créé leur compte sur le site de l'UTT Arena et qu'ils aient rejoint ton équipe.", - }, - { - question: "J'ai payé ma place, puis-je encore changer de tournoi ?", - answer: ( - <> - Oui, tu peux changer librement de tournoi à condition que le tournoi que tu veux rejoindre soit au même prix - que la place que tu as déjà payée. Si ce n'est pas le cas, contacte-nous ! - - ), - }, - ], - // 'Tournoi Super Smash Bros Ultimate': [ - // { - // question: 'Dois-je apporter ma console ?', - // answer: ( - // <> - // Si tu as coché la case "Réduction si tu amènes ta propre Nintendo Switch" à l'inscription au - // tournoi, tu dois en effet apporter ta Nintendo Switch, son dock, le jeu SSBU avec tous les - // personnages, DLCs inclus et un câble HDMI, et tu bénéficies d'une réduction{' '} - // de 3€ sur le prix de ton billet. Cette option est disponible pour les 30 premiers seulement. - //
- // Même sans cocher cette case, tu peux apporter ta console pour jouer en freeplay. - //
- //
- // - // Si tu as indiqué que tu apportais ta console et que ce n'est pas le cas, un supplément de 6€ te sera facturé - // sur place. - // - // - // ), - // }, - // { - // question: 'Puis-je apporter mon PC ?', - // answer: "Non, car tu n'auras pas de place pour installer ton setup.", - // }, - // { - // question: 'Dois-je apporter mes manettes ?', - // answer: 'Oui. Tu dois apporter tes manettes de Switch ou ta manette de GameCube sans oublier ton adaptateur.', - // }, - // ], -} as Faq; +export const useFaq = () => { + const dispatch = useDispatch(); + const faqsFromStore = useSelector((state: RootState) => state.admin.faqs || []); + + // ⚡ On garde le même nom `faq` pour ton front + const [faq, setFaq] = useState({}); + + useEffect(() => { + dispatch(fetchFaqs()); // fetch API + }, [dispatch]); + + useEffect(() => { + const grouped: FaqByCategory = {}; + + faqsFromStore + .filter((f) => f.display) // uniquement celles affichables + .forEach((f) => { + if (!grouped[f.category]) grouped[f.category] = []; + grouped[f.category].push({ + question: f.question, + answer: f.answer, + }); + }); + + setFaq(grouped); + }, [faqsFromStore]); + + return faq; +}; const Help = () => { const [name, setName] = useState(''); @@ -206,6 +61,8 @@ const Help = () => { const tournaments = useAppSelector((state) => state.tournament.tournaments); + const faq = useFaq(); + const options = tournaments ? [...tournaments!.map((tournament) => 'Tournoi ' + tournament.name), 'Problème sur le site', 'Autre'] // Transform the array to match the requested type of Select component @@ -214,7 +71,6 @@ const Help = () => { value, })) : []; - useEffect(() => { // Scroll to the element if the hash is present in the url const hash = window.location.hash; @@ -277,7 +133,11 @@ const Help = () => { .normalize('NFD') .replace(/[\u0300-\u036f]/g, '')}-${index}` }> -
{question.answer}
+ {typeof question.answer === 'string' ? ( +
+ ) : ( +
{question.answer}
+ )} ))}
diff --git a/src/components/dashboard/FaqModal.tsx b/src/components/dashboard/FaqModal.tsx new file mode 100644 index 00000000..ad9a2c9b --- /dev/null +++ b/src/components/dashboard/FaqModal.tsx @@ -0,0 +1,92 @@ +'use client'; + +import { useState } from 'react'; +import { Modal, Button, Checkbox, Input, Textarea } from '@/components/UI'; +import { useAppDispatch } from '@/lib/hooks'; +import { Faq } from '@/types'; +import { createFaqApi, updateFaqApi, deleteFaqApi } from '@/modules/admin'; + +interface FaqModalProps { + faq: Faq | null; + onClose?: () => void; +} + +const FaqModal = ({ faq, onClose }: FaqModalProps) => { + const dispatch = useAppDispatch(); + + // Les différents champs + const [category, setCategory] = useState(faq?.category ?? ''); + const [question, setQuestion] = useState(faq?.question ?? ''); + const [answer, setAnswer] = useState(faq?.answer ?? ''); + const [display, setDisplay] = useState(faq?.display ?? true); + + // ID existant pour édition + const id = faq?.id ?? null; + + const handleSave = () => { + if (!question.trim() || !answer.trim()) return; + + const payload = { category, question, answer, display }; + + if (faq == null) { + // Création + dispatch(createFaqApi(payload)); + } else { + // Edition + dispatch(updateFaqApi(id!, payload)); + } + + if (onClose) onClose(); + }; + + const handleDelete = () => { + if (id) { + dispatch(deleteFaqApi(id)); + if (onClose) onClose(); + } + }; + + return ( + {}} + buttons={ + <> + {faq && ( + + )} + + + } + containerClassName="faq-modal"> + <> + + +