diff --git a/backend/.gitignore b/backend/.gitignore index ae19cfb..d651b47 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -4,3 +4,15 @@ node_modules/ coverage/ dist/ test-results/ +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/index.html b/frontend/index.html index ee46fd3..42051f7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,7 +5,43 @@ + Jeu concours - Thé Tip Top + + + + + + + + + + + + + + + + + +
diff --git a/frontend/public/og-image.webp b/frontend/public/og-image.webp new file mode 100644 index 0000000..37a7063 Binary files /dev/null and b/frontend/public/og-image.webp differ diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt index 89fb783..97232b8 100644 --- a/frontend/public/robots.txt +++ b/frontend/public/robots.txt @@ -3,6 +3,7 @@ User-agent: * # Pages publiques autorisées Allow: / Allow: /accueil +Allow: /notre-concept Allow: /regles-jeu Allow: /contact Allow: /mentions-legales diff --git a/frontend/src/components/Footer.jsx b/frontend/src/components/Footer.jsx index 0282c51..f376285 100644 --- a/frontend/src/components/Footer.jsx +++ b/frontend/src/components/Footer.jsx @@ -194,12 +194,6 @@ export default function Footer() { thetiptop.gp4@gmail.com - -

- 18 rue Léon Frot -
- 75011 Paris, France -

diff --git a/frontend/src/hooks/usePageMeta.js b/frontend/src/hooks/usePageMeta.js index 5ec3898..18a0730 100644 --- a/frontend/src/hooks/usePageMeta.js +++ b/frontend/src/hooks/usePageMeta.js @@ -1,29 +1,52 @@ import { useEffect } from 'react' +import { useLocation } from 'react-router-dom' + +// Domaine de production, utilisé pour construire des URLs absolues +// (canonical, og:url) à partir du chemin de la route courante. +const SITE_URL = 'https://www.dsp5-archi-o24a-4-5-g4.fr' + +// Change la valeur d'un attribut sur le premier élément trouvé par le +// sélecteur, et retourne de quoi la restaurer plus tard. N'écrit rien si +// l'élément est absent du (les balises de base vivent dans +// index.html ; ce hook ne fait que les mettre à jour, jamais les créer). +function setAttr(selector, attr, value) { + if (value == null) return null + const el = document.querySelector(selector) + if (!el) return null + const previous = el.getAttribute(attr) + el.setAttribute(attr, value) + return () => el.setAttribute(attr, previous ?? '') +} + +// Permet à une page de définir son propre , sa meta description, son +// URL canonique et ses balises Open Graph/Twitter le temps qu'elle est +// affichée. La SPA n'a qu'un seul index.html : sans ce hook, toutes les +// routes partagent les mêmes balises. Les valeurs précédentes sont +// restaurées au démontage, donc quitter la page revient à l'état d'avant +// (celui d'index.html, ou celui d'une page parente si elles s'enchaînent). +export default function usePageMeta({ title, description, image } = {}) { + const { pathname } = useLocation() -// Permet à une page de définir son propre <title>/<meta description> le -// temps qu'elle est affichée. La SPA n'a qu'un seul index.html : sans ce -// hook, toutes les routes partagent le même title/meta. Les valeurs -// précédentes sont restaurées au démontage, donc quitter la page revient à -// l'état d'avant (celui d'index.html, ou celui d'une page parente si elles -// s'enchaînent). -export default function usePageMeta({ title, description } = {}) { useEffect(() => { const previousTitle = document.title - if (title) { - document.title = title - } + if (title) document.title = title - const metaEl = document.querySelector('meta[name="description"]') - const previousDescription = metaEl?.getAttribute('content') ?? null - if (description && metaEl) { - metaEl.setAttribute('content', description) - } + const url = `${SITE_URL}${pathname}` + const restores = [ + setAttr('meta[name="description"]', 'content', description), + setAttr('link[rel="canonical"]', 'href', url), + setAttr('meta[property="og:title"]', 'content', title), + setAttr('meta[property="og:description"]', 'content', description), + setAttr('meta[property="og:url"]', 'content', url), + setAttr('meta[property="og:image"]', 'content', image), + setAttr('meta[name="twitter:title"]', 'content', title), + setAttr('meta[name="twitter:description"]', 'content', description), + setAttr('meta[name="twitter:image"]', 'content', image), + ].filter(Boolean) return () => { document.title = previousTitle - if (metaEl && previousDescription !== null) { - metaEl.setAttribute('content', previousDescription) - } + restores.forEach((restore) => restore()) } - }, [title, description]) + }, [title, description, image, pathname]) } diff --git a/frontend/src/pages/Accessibilite.jsx b/frontend/src/pages/Accessibilite.jsx index 645ee13..13ad379 100644 --- a/frontend/src/pages/Accessibilite.jsx +++ b/frontend/src/pages/Accessibilite.jsx @@ -1,4 +1,11 @@ +import usePageMeta from '../hooks/usePageMeta' + export default function Accessibilite() { + usePageMeta({ + title: 'Déclaration d’accessibilité | Thé Tip Top', + description: "État de conformité RGAA du site Thé Tip Top et procédure pour signaler un problème d'accessibilité rencontré sur le site.", + }) + return ( <main className="bg-gray-50 min-h-screen px-4 sm:px-6 py-10 sm:py-16"> <div className="max-w-5xl mx-auto bg-white rounded-2xl shadow p-5 sm:p-8 md:p-10"> diff --git a/frontend/src/pages/Accueil.jsx b/frontend/src/pages/Accueil.jsx index 4282489..c60e30e 100644 --- a/frontend/src/pages/Accueil.jsx +++ b/frontend/src/pages/Accueil.jsx @@ -3,9 +3,6 @@ import { Link } from 'react-router-dom' import usePageMeta from '../hooks/usePageMeta' import bannerImg from '../assets/img/banner-accueil.webp' -// Tous les lots possibles hors grand lot (déjà mis en avant dans sa propre -// carte). Chaque lot devient sa propre carte flottante, étalée en quinconce -// le long de la partie droite de l'illustration plutôt qu'empilée en liste. const gains = [ { label: 'Infuseur à thé', top: '26%', right: '26%', delay: 700, variant: 'a' }, { label: 'Boîte 100 g détox ou infusion', top: '22%', right: '1%', delay: 850, variant: 'b' }, @@ -14,9 +11,6 @@ const gains = [ { label: 'Coffret découverte 69 €', top: '84%', right: '24%', delay: 1300, variant: 'a' }, ] -// Légère inclinaison 3D des cartes de la bannière au passage de la souris, -// façon « tilt card ». Fonctions déclarées hors du composant : elles ne -// dépendent d'aucun state, inutile de les recréer à chaque rendu. function handleCardTilt(e) { const el = e.currentTarget const rect = el.getBoundingClientRect() @@ -28,11 +22,6 @@ function resetCardTilt(e) { e.currentTarget.style.transform = '' } -// Petites animations de thé générées en JS : position, taille, durée et -// délai tirés au sort une seule fois (useMemo) pour parsemer toute la -// bannière de feuilles/tasses/théières/cuillères qui dérivent doucement, -// sans jamais se superposer deux fois de la même façon. Taille et opacité -// relevées pour qu'elles restent bien visibles sur le fond teal. const TEA_EMOJI = ['🍃', '🍵', '🫖', '☕', '🥄', '✨'] function createTeaParticles(count) { @@ -100,12 +89,10 @@ const faqJsonLd = { export default function Accueil() { usePageMeta({ - title: 'Jeu-concours 100 % Gagnant | Thé Tip Top — Thés bio et faits main', + title: 'Jeu-concours 100 % Gagnant du 2 au 31 octobre | Thé Tip Top', description: "Du 2 au 31 octobre, tout ticket de plus de 49 € de notre boutique de Nice porte un code 100 % gagnant. Saisissez le vôtre et repartez avec votre lot.", }) - // Tirées au sort une seule fois au montage : la bannière ne doit pas - // réordonner ses particules de thé à chaque re-render. const teaParticles = useMemo(() => createTeaParticles(30), []) return ( @@ -113,25 +100,14 @@ export default function Accueil() { <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }} /> - {/* Bannière : fond teal plein cadre, illustration très grande et animée à l'entrée, - textes et cartes de gains répartis en plusieurs blocs pour occuper toute la zone - plutôt qu'une seule colonne dense. La composition « poster » en absolute (position: - static → absolute) ne s'active qu'à partir de lg (1024px) : en dessous — mobile ET - tablette —, un écran trop étroit ferait chevaucher le titre et l'illustration de façon - illisible (constaté sur iPad portrait), donc tout reste en empilement normal, dans - l'ordre du DOM, jusqu'à avoir vraiment la place. */} <section className="relative bg-[#006B6B] overflow-hidden"> <div className="max-w-7xl mx-auto px-4 sm:px-6 relative py-10 lg:py-0 lg:h-[680px] xl:h-[780px]"> - {/* Halo doux derrière l'illustration (uniquement pertinent à partir de lg, où - l'illustration passe en fond de la composition) */} <div className="hidden lg:block absolute right-[-10%] top-1/2 -translate-y-1/2 w-[75%] h-[75%] rounded-full bg-white/10 blur-3xl" aria-hidden="true" /> - {/* Particules de thé générées en JS (position/taille/durée tirées au sort dans - teaParticles), parsemées sur toute la bannière derrière le contenu. */} <div className="pointer-events-none absolute inset-0 overflow-hidden z-0" aria-hidden="true"> {teaParticles.map((p) => ( <span @@ -152,7 +128,6 @@ export default function Accueil() { ))} </div> - {/* Titre */} <div className="static lg:absolute lg:left-0 lg:top-10 max-w-full lg:max-w-md mb-6 lg:mb-0"> <p className="inline-block text-sm sm:text-base font-bold uppercase tracking-widest text-amber-300 border border-white/70 rounded-lg px-3.5 py-2 mb-3"> Du 02 au 31 octobre 2026 @@ -163,10 +138,6 @@ export default function Accueil() { </h1> </div> - {/* Illustration : très grande à partir de lg, débordant du cadre en haut/bas (rogné - proprement par le overflow-hidden de la section) et glissant à l'entrée. En - dessous de lg, recadrée (object-cover) sur le personnage pour ne pas gaspiller - l'espace transparent intégré au visuel d'origine. */} <div className="relative -mx-4 sm:mx-0 lg:mx-0 mb-6 lg:mb-0 lg:contents"> <div className="relative aspect-[16/10] sm:aspect-[21/9] overflow-hidden lg:hidden"> <img @@ -184,27 +155,14 @@ export default function Accueil() { /> </div> - {/* Sous-titre : mis en évidence dans une carte « verre dépoli » distincte du fond - teal uni (bordure d'accent + fond translucide flouté), pour qu'il se détache - clairement plutôt que de se fondre dans le reste du texte blanc de la bannière. */} <p className="static lg:absolute lg:left-0 lg:top-1/2 lg:-translate-y-1/2 max-w-full lg:max-w-md text-lg sm:text-2xl lg:text-3xl font-semibold text-white leading-snug [text-wrap:balance] mb-6 lg:mb-0 bg-white/10 backdrop-blur-sm border-l-4 border-amber-300 rounded-2xl px-4 py-3 sm:px-6 sm:py-4 shadow-lg"> À l'occasion de l'ouverture de notre{' '} <span className="text-amber-300">10<sup>e</sup> boutique</span> à Nice : chaque code distribué correspond à un lot réel, attribué à l'avance. </p> - {/* Cartes de gains : le grand lot (agrandi) + tous les autres lots possibles, en - cartes flottantes autour de l'illustration à partir de lg uniquement — masquées - sur mobile/tablette, où elles surchargeaient l'écran en plus du reste du contenu. - Chaque carte flotte doucement en continu (animate-card-float-*) et s'incline - légèrement au passage de la souris (onMouseMove → handleCardTilt), deux animations - portées par des conteneurs séparés pour ne pas se marcher dessus. */} <div className="hidden lg:contents"> - {/* Grand lot : décalé plus bas et plus à droite que les autres cartes du haut - (elle touchait « Boîte détox ou infusion »), dans le grand espace libre entre - cette carte et « Coffret découverte 39 € », pour amorcer un arc de cercle autour - de l'illustration plutôt qu'un empilement dans le même coin. */} <div className="shrink-0 lg:absolute lg:right-[1%] lg:top-[45%] animate-banner-badge [animation-delay:500ms]"> <div className="animate-card-float-a"> <div @@ -220,14 +178,6 @@ export default function Accueil() { </div> </div> - {/* Les autres lots : une carte par lot, éclatées en quinconce (position en % - définie dans le tableau `gains`) plutôt qu'empilées dans une seule colonne. - top/right en style inline : ils ne s'appliquent qu'à partir de lg (seul - breakpoint où la carte passe en absolute), sans effet en dessous. - whitespace-nowrap plutôt qu'un max-w fixe : une carte plus étroite que sa - ligne de texte forçait un retour à la ligne qui laissait du blanc (ex. « Boîte - 100 g détox ou » / « infusion »). En nowrap, chaque carte se limite exactement - à la largeur de son texte, sur une seule ligne, sans espace perdu. */} {gains.map((gain) => ( <div key={gain.label} @@ -247,7 +197,6 @@ export default function Accueil() { ))} </div> - {/* CTA, remonté par rapport au bord bas de la bannière */} <div className="static lg:absolute lg:left-0 lg:bottom-24 xl:bottom-28 flex flex-col sm:flex-row gap-3 sm:gap-4"> <Link to="/saisir-code-unique" @@ -265,7 +214,6 @@ export default function Accueil() { </div> </section> - {/* Comment participer */} <section className="max-w-5xl mx-auto px-4 pt-6 sm:px-6 pb-12 sm:pb-20"> <h2 className="text-2xl sm:text-3xl font-bold text-[#006B6B] mb-6 sm:mb-8 text-center"> Comment participer ? @@ -284,7 +232,6 @@ export default function Accueil() { </ol> </section> - {/* Questions fréquentes */} <section className="max-w-3xl mx-auto px-4 sm:px-6 pb-16 sm:pb-24"> <h2 className="text-2xl sm:text-3xl font-bold text-[#006B6B] mb-6 sm:mb-8 text-center"> Questions fréquentes diff --git a/frontend/src/pages/Cgu.jsx b/frontend/src/pages/Cgu.jsx index 4506291..5831143 100644 --- a/frontend/src/pages/Cgu.jsx +++ b/frontend/src/pages/Cgu.jsx @@ -1,6 +1,12 @@ import { Link } from 'react-router-dom' +import usePageMeta from '../hooks/usePageMeta' export default function Cgu() { + usePageMeta({ + title: 'Conditions générales d’utilisation | Thé Tip Top', + description: "CGU du site Thé Tip Top : conditions d'accès au jeu-concours, inscription, participation et gestion de votre compte.", + }) + return ( <main className="bg-gray-50 min-h-screen px-4 sm:px-6 py-10 sm:py-16"> <div className="max-w-5xl mx-auto bg-white rounded-2xl shadow p-5 sm:p-8 md:p-10"> diff --git a/frontend/src/pages/Confidentialite.jsx b/frontend/src/pages/Confidentialite.jsx index 3f2da31..b14522f 100644 --- a/frontend/src/pages/Confidentialite.jsx +++ b/frontend/src/pages/Confidentialite.jsx @@ -1,4 +1,11 @@ +import usePageMeta from '../hooks/usePageMeta' + export default function Confidentialite() { + usePageMeta({ + title: 'Politique de confidentialité | Thé Tip Top', + description: "Comment Thé Tip Top collecte et protège vos données personnelles dans le cadre du jeu-concours, et comment exercer vos droits RGPD.", + }) + return ( <main className="bg-[#F0F0F0] min-h-screen px-4 sm:px-6 py-10 sm:py-16"> <div className="max-w-5xl mx-auto bg-white rounded-2xl shadow p-5 sm:p-8 md:p-10"> @@ -19,7 +26,7 @@ export default function Confidentialite() { </h2> <p className="text-gray-700 leading-relaxed text-sm sm:text-base"> Le responsable du traitement des données collectées via le site du jeu-concours - « 100% Gagnant » est la société Thé Tip Top, située au 18 rue Léon Frot, 75011 Paris. + « 100% Gagnant » est la société Thé Tip Top. </p> </section> diff --git a/frontend/src/pages/Connexion.jsx b/frontend/src/pages/Connexion.jsx index 74d9e6c..fc7adbd 100644 --- a/frontend/src/pages/Connexion.jsx +++ b/frontend/src/pages/Connexion.jsx @@ -2,8 +2,14 @@ import { useState } from 'react' import { Link, useNavigate, useLocation } from 'react-router-dom' import { loginApi } from '../services/auth' import { useAuth } from '../context/AuthContext' +import usePageMeta from '../hooks/usePageMeta' export default function Connexion() { + usePageMeta({ + title: 'Connexion | Thé Tip Top', + description: "Connectez-vous à votre espace participant Thé Tip Top pour saisir votre code et suivre vos gains au jeu-concours 100 % Gagnant.", + }) + const navigate = useNavigate() const location = useLocation() const { login } = useAuth() diff --git a/frontend/src/pages/Contact.jsx b/frontend/src/pages/Contact.jsx index 84f7aec..a56f87a 100644 --- a/frontend/src/pages/Contact.jsx +++ b/frontend/src/pages/Contact.jsx @@ -1,6 +1,12 @@ import imgContact from '../assets/img/img-contact.webp' +import usePageMeta from '../hooks/usePageMeta' export default function Contact() { + usePageMeta({ + title: 'Contact | Thé Tip Top', + description: "Une question sur le jeu-concours, votre participation ou votre lot ? Contactez l'équipe Thé Tip Top par email, téléphone ou via le formulaire en ligne.", + }) + return ( <main className="bg-[#F0F0F0] px-4 sm:px-6 py-10 sm:py-16"> <div className="max-w-6xl mx-auto"> @@ -19,19 +25,6 @@ export default function Contact() { </p> <div className="space-y-5 text-gray-700"> - <div> - <h2 className="font-semibold text-[#006B6B] mb-1"> - Adresse - </h2> - <p className="text-sm sm:text-base"> - ThéTipTop - <br /> - 18 rue Léon Frot - <br /> - 75011 Paris, France - </p> - </div> - <div> <h2 className="font-semibold text-[#006B6B] mb-1"> Email diff --git a/frontend/src/pages/Inscription.jsx b/frontend/src/pages/Inscription.jsx index 4b2c54f..70b76b6 100644 --- a/frontend/src/pages/Inscription.jsx +++ b/frontend/src/pages/Inscription.jsx @@ -2,6 +2,7 @@ import { useState, useRef } from 'react' import { Link, useNavigate } from 'react-router-dom' import { registerApi, verifyCodeApi } from '../services/auth' import { useAuth } from '../context/AuthContext' +import usePageMeta from '../hooks/usePageMeta' // Une naissance ne peut pas être dans le futur : borne haute du sélecteur. const AUJOURD_HUI = new Date().toISOString().split('T')[0] @@ -23,6 +24,11 @@ function hasAtLeast18Years(birthdate) { const GENRES = ['Femme', 'Homme', 'Autre'] export default function Inscription() { + usePageMeta({ + title: 'Créer un compte | Thé Tip Top', + description: "Inscrivez-vous en quelques instants pour participer au jeu-concours 100 % Gagnant Thé Tip Top et saisir le code de votre ticket de caisse.", + }) + const navigate = useNavigate() const { login } = useAuth() diff --git a/frontend/src/pages/MentionsLegales.jsx b/frontend/src/pages/MentionsLegales.jsx index 304ddb1..388468b 100644 --- a/frontend/src/pages/MentionsLegales.jsx +++ b/frontend/src/pages/MentionsLegales.jsx @@ -1,4 +1,11 @@ +import usePageMeta from '../hooks/usePageMeta' + export default function MentionsLegales() { + usePageMeta({ + title: 'Mentions légales | Thé Tip Top', + description: "Mentions légales du site Thé Tip Top : éditeur, hébergeur, propriété intellectuelle et informations relatives au jeu-concours 100 % Gagnant.", + }) + return ( <main className="bg-gray-50 min-h-screen px-4 sm:px-6 py-10 sm:py-16"> <div className="max-w-5xl mx-auto bg-white rounded-2xl shadow p-5 sm:p-8 md:p-10"> diff --git a/frontend/src/pages/ReglesJeu.jsx b/frontend/src/pages/ReglesJeu.jsx index e7ef61c..4a84bab 100644 --- a/frontend/src/pages/ReglesJeu.jsx +++ b/frontend/src/pages/ReglesJeu.jsx @@ -1,10 +1,16 @@ import { Link } from 'react-router-dom' import { useAuth } from '../context/AuthContext' +import usePageMeta from '../hooks/usePageMeta' export default function ReglesJeu() { const { user } = useAuth() const isLoggedIn = !!user + usePageMeta({ + title: 'Règles du jeu-concours 100 % Gagnant | Thé Tip Top', + description: "Comment participer au jeu-concours Thé Tip Top : obtenir un code, créer un compte, découvrir votre lot et tenter de remporter le grand gagnant.", + }) + return ( <div className="bg-gray-50 min-h-screen"> diff --git a/frontend/src/pages/Sitemap.jsx b/frontend/src/pages/Sitemap.jsx index 49966dc..e794cc1 100644 --- a/frontend/src/pages/Sitemap.jsx +++ b/frontend/src/pages/Sitemap.jsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom' import { useAuth} from '../context/AuthContext' +import usePageMeta from '../hooks/usePageMeta' const sections = [ { @@ -45,6 +46,11 @@ const sections = [ ] export default function Sitemap() { + usePageMeta({ + title: 'Plan du site | Thé Tip Top', + description: "Retrouvez toutes les pages du site Thé Tip Top : jeu-concours, compte participant, informations légales et espaces professionnels.", + }) + const { user } = useAuth() const isAdmin = user?.role === 'ADMIN' const visibleSections = sections.filter(