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 / 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 (
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() {
- {/* 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. */}
- {/* Halo doux derrière l'illustration (uniquement pertinent à partir de lg, où
- l'illustration passe en fond de la composition) */}
- {/* 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. */}
{teaParticles.map((p) => (
- {/* Titre */}
Du 02 au 31 octobre 2026
@@ -163,10 +138,6 @@ export default function Accueil() {
- {/* 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. */}
- {/* 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. */}
À l'occasion de l'ouverture de notre{' '}
10e boutique à Nice : chaque code
distribué correspond à un lot réel, attribué à l'avance.
- {/* 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. */}
- {/* 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. */}
- {/* 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) => (
- {/* CTA, remonté par rapport au bord bas de la bannière */}
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 (
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 (
@@ -19,7 +26,7 @@ export default function Confidentialite() {
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.
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 (
@@ -19,19 +25,6 @@ export default function Contact() {
-
-
- Adresse
-
-
- ThéTipTop
-
- 18 rue Léon Frot
-
- 75011 Paris, France
-
-
-
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 (
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 (
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(