optimiser SEO et images, corriger age minimum inscription
This commit is contained in:
12
backend/.gitignore
vendored
12
backend/.gitignore
vendored
@@ -4,3 +4,15 @@ node_modules/
|
|||||||
coverage/
|
coverage/
|
||||||
dist/
|
dist/
|
||||||
test-results/
|
test-results/
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
@@ -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?
|
||||||
@@ -5,7 +5,43 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="description" content="Participez au jeu-concours Thé Tip Top : saisissez le code de votre ticket et découvrez immédiatement votre lot." />
|
<meta name="description" content="Participez au jeu-concours Thé Tip Top : saisissez le code de votre ticket et découvrez immédiatement votre lot." />
|
||||||
|
<link rel="canonical" href="https://www.dsp5-archi-o24a-4-5-g4.fr/accueil" />
|
||||||
<title>Jeu concours - Thé Tip Top</title>
|
<title>Jeu concours - Thé Tip Top</title>
|
||||||
|
|
||||||
|
<!-- Open Graph : valeurs par défaut, mises à jour par page via le hook usePageMeta -->
|
||||||
|
<meta property="og:site_name" content="Thé Tip Top" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:locale" content="fr_FR" />
|
||||||
|
<meta property="og:title" content="Jeu concours - Thé Tip Top" />
|
||||||
|
<meta property="og:description" content="Participez au jeu-concours Thé Tip Top : saisissez le code de votre ticket et découvrez immédiatement votre lot." />
|
||||||
|
<meta property="og:url" content="https://www.dsp5-archi-o24a-4-5-g4.fr/accueil" />
|
||||||
|
<meta property="og:image" content="https://www.dsp5-archi-o24a-4-5-g4.fr/og-image.webp" />
|
||||||
|
|
||||||
|
<!-- Twitter Card -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content="Jeu concours - Thé Tip Top" />
|
||||||
|
<meta name="twitter:description" content="Participez au jeu-concours Thé Tip Top : saisissez le code de votre ticket et découvrez immédiatement votre lot." />
|
||||||
|
<meta name="twitter:image" content="https://www.dsp5-archi-o24a-4-5-g4.fr/og-image.webp" />
|
||||||
|
|
||||||
|
<!-- Schema.org : identité du site uniquement, aucune adresse (non communiquée) -->
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@graph": [
|
||||||
|
{
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "Thé Tip Top",
|
||||||
|
"url": "https://www.dsp5-archi-o24a-4-5-g4.fr/",
|
||||||
|
"email": "thetiptop.gp4@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "WebSite",
|
||||||
|
"name": "Thé Tip Top",
|
||||||
|
"url": "https://www.dsp5-archi-o24a-4-5-g4.fr/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
BIN
frontend/public/og-image.webp
Normal file
BIN
frontend/public/og-image.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 231 KiB |
@@ -3,6 +3,7 @@ User-agent: *
|
|||||||
# Pages publiques autorisées
|
# Pages publiques autorisées
|
||||||
Allow: /
|
Allow: /
|
||||||
Allow: /accueil
|
Allow: /accueil
|
||||||
|
Allow: /notre-concept
|
||||||
Allow: /regles-jeu
|
Allow: /regles-jeu
|
||||||
Allow: /contact
|
Allow: /contact
|
||||||
Allow: /mentions-legales
|
Allow: /mentions-legales
|
||||||
|
|||||||
@@ -194,12 +194,6 @@ export default function Footer() {
|
|||||||
<a href="mailto:thetiptop.gp4@gmail.com" className="inline-block mt-3 font-medium break-all hover:text-teal-300">
|
<a href="mailto:thetiptop.gp4@gmail.com" className="inline-block mt-3 font-medium break-all hover:text-teal-300">
|
||||||
thetiptop.gp4@gmail.com
|
thetiptop.gp4@gmail.com
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<p className="text-teal-100 text-sm mt-2">
|
|
||||||
18 rue Léon Frot
|
|
||||||
<br />
|
|
||||||
75011 Paris, France
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,29 +1,52 @@
|
|||||||
import { useEffect } from 'react'
|
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 <head> (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 <title>, 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(() => {
|
useEffect(() => {
|
||||||
const previousTitle = document.title
|
const previousTitle = document.title
|
||||||
if (title) {
|
if (title) document.title = title
|
||||||
document.title = title
|
|
||||||
}
|
|
||||||
|
|
||||||
const metaEl = document.querySelector('meta[name="description"]')
|
const url = `${SITE_URL}${pathname}`
|
||||||
const previousDescription = metaEl?.getAttribute('content') ?? null
|
const restores = [
|
||||||
if (description && metaEl) {
|
setAttr('meta[name="description"]', 'content', description),
|
||||||
metaEl.setAttribute('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 () => {
|
return () => {
|
||||||
document.title = previousTitle
|
document.title = previousTitle
|
||||||
if (metaEl && previousDescription !== null) {
|
restores.forEach((restore) => restore())
|
||||||
metaEl.setAttribute('content', previousDescription)
|
|
||||||
}
|
}
|
||||||
}
|
}, [title, description, image, pathname])
|
||||||
}, [title, description])
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
|
import usePageMeta from '../hooks/usePageMeta'
|
||||||
|
|
||||||
export default function Accessibilite() {
|
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 (
|
return (
|
||||||
<main className="bg-gray-50 min-h-screen px-4 sm:px-6 py-10 sm:py-16">
|
<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">
|
<div className="max-w-5xl mx-auto bg-white rounded-2xl shadow p-5 sm:p-8 md:p-10">
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ import { Link } from 'react-router-dom'
|
|||||||
import usePageMeta from '../hooks/usePageMeta'
|
import usePageMeta from '../hooks/usePageMeta'
|
||||||
import bannerImg from '../assets/img/banner-accueil.webp'
|
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 = [
|
const gains = [
|
||||||
{ label: 'Infuseur à thé', top: '26%', right: '26%', delay: 700, variant: 'a' },
|
{ 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' },
|
{ 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' },
|
{ 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) {
|
function handleCardTilt(e) {
|
||||||
const el = e.currentTarget
|
const el = e.currentTarget
|
||||||
const rect = el.getBoundingClientRect()
|
const rect = el.getBoundingClientRect()
|
||||||
@@ -28,11 +22,6 @@ function resetCardTilt(e) {
|
|||||||
e.currentTarget.style.transform = ''
|
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 = ['🍃', '🍵', '🫖', '☕', '🥄', '✨']
|
const TEA_EMOJI = ['🍃', '🍵', '🫖', '☕', '🥄', '✨']
|
||||||
|
|
||||||
function createTeaParticles(count) {
|
function createTeaParticles(count) {
|
||||||
@@ -100,12 +89,10 @@ const faqJsonLd = {
|
|||||||
|
|
||||||
export default function Accueil() {
|
export default function Accueil() {
|
||||||
usePageMeta({
|
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.",
|
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), [])
|
const teaParticles = useMemo(() => createTeaParticles(30), [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -113,25 +100,14 @@ export default function Accueil() {
|
|||||||
|
|
||||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }} />
|
<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">
|
<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]">
|
<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
|
<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"
|
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"
|
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">
|
<div className="pointer-events-none absolute inset-0 overflow-hidden z-0" aria-hidden="true">
|
||||||
{teaParticles.map((p) => (
|
{teaParticles.map((p) => (
|
||||||
<span
|
<span
|
||||||
@@ -152,7 +128,6 @@ export default function Accueil() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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">
|
<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">
|
<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
|
Du 02 au 31 octobre 2026
|
||||||
@@ -163,10 +138,6 @@ export default function Accueil() {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</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 -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">
|
<div className="relative aspect-[16/10] sm:aspect-[21/9] overflow-hidden lg:hidden">
|
||||||
<img
|
<img
|
||||||
@@ -184,27 +155,14 @@ export default function Accueil() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<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{' '}
|
À l'occasion de l'ouverture de notre{' '}
|
||||||
<span className="text-amber-300">10<sup>e</sup> boutique</span> à Nice : chaque code
|
<span className="text-amber-300">10<sup>e</sup> boutique</span> à Nice : chaque code
|
||||||
distribué correspond à un lot réel, attribué à l'avance.
|
distribué correspond à un lot réel, attribué à l'avance.
|
||||||
</p>
|
</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">
|
<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="shrink-0 lg:absolute lg:right-[1%] lg:top-[45%] animate-banner-badge [animation-delay:500ms]">
|
||||||
<div className="animate-card-float-a">
|
<div className="animate-card-float-a">
|
||||||
<div
|
<div
|
||||||
@@ -220,14 +178,6 @@ export default function Accueil() {
|
|||||||
</div>
|
</div>
|
||||||
</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) => (
|
{gains.map((gain) => (
|
||||||
<div
|
<div
|
||||||
key={gain.label}
|
key={gain.label}
|
||||||
@@ -247,7 +197,6 @@ export default function Accueil() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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">
|
<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
|
<Link
|
||||||
to="/saisir-code-unique"
|
to="/saisir-code-unique"
|
||||||
@@ -265,7 +214,6 @@ export default function Accueil() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Comment participer */}
|
|
||||||
<section className="max-w-5xl mx-auto px-4 pt-6 sm:px-6 pb-12 sm:pb-20">
|
<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">
|
<h2 className="text-2xl sm:text-3xl font-bold text-[#006B6B] mb-6 sm:mb-8 text-center">
|
||||||
Comment participer ?
|
Comment participer ?
|
||||||
@@ -284,7 +232,6 @@ export default function Accueil() {
|
|||||||
</ol>
|
</ol>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Questions fréquentes */}
|
|
||||||
<section className="max-w-3xl mx-auto px-4 sm:px-6 pb-16 sm:pb-24">
|
<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">
|
<h2 className="text-2xl sm:text-3xl font-bold text-[#006B6B] mb-6 sm:mb-8 text-center">
|
||||||
Questions fréquentes
|
Questions fréquentes
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
import usePageMeta from '../hooks/usePageMeta'
|
||||||
|
|
||||||
export default function Cgu() {
|
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 (
|
return (
|
||||||
<main className="bg-gray-50 min-h-screen px-4 sm:px-6 py-10 sm:py-16">
|
<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">
|
<div className="max-w-5xl mx-auto bg-white rounded-2xl shadow p-5 sm:p-8 md:p-10">
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
|
import usePageMeta from '../hooks/usePageMeta'
|
||||||
|
|
||||||
export default function Confidentialite() {
|
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 (
|
return (
|
||||||
<main className="bg-[#F0F0F0] min-h-screen px-4 sm:px-6 py-10 sm:py-16">
|
<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">
|
<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>
|
</h2>
|
||||||
<p className="text-gray-700 leading-relaxed text-sm sm:text-base">
|
<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
|
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>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,14 @@ import { useState } from 'react'
|
|||||||
import { Link, useNavigate, useLocation } from 'react-router-dom'
|
import { Link, useNavigate, useLocation } from 'react-router-dom'
|
||||||
import { loginApi } from '../services/auth'
|
import { loginApi } from '../services/auth'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
|
import usePageMeta from '../hooks/usePageMeta'
|
||||||
|
|
||||||
export default function Connexion() {
|
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 navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const { login } = useAuth()
|
const { login } = useAuth()
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import imgContact from '../assets/img/img-contact.webp'
|
import imgContact from '../assets/img/img-contact.webp'
|
||||||
|
import usePageMeta from '../hooks/usePageMeta'
|
||||||
|
|
||||||
export default function Contact() {
|
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 (
|
return (
|
||||||
<main className="bg-[#F0F0F0] px-4 sm:px-6 py-10 sm:py-16">
|
<main className="bg-[#F0F0F0] px-4 sm:px-6 py-10 sm:py-16">
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-6xl mx-auto">
|
||||||
@@ -19,19 +25,6 @@ export default function Contact() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="space-y-5 text-gray-700">
|
<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>
|
<div>
|
||||||
<h2 className="font-semibold text-[#006B6B] mb-1">
|
<h2 className="font-semibold text-[#006B6B] mb-1">
|
||||||
Email
|
Email
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useRef } from 'react'
|
|||||||
import { Link, useNavigate } from 'react-router-dom'
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
import { registerApi, verifyCodeApi } from '../services/auth'
|
import { registerApi, verifyCodeApi } from '../services/auth'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
|
import usePageMeta from '../hooks/usePageMeta'
|
||||||
|
|
||||||
// Une naissance ne peut pas être dans le futur : borne haute du sélecteur.
|
// Une naissance ne peut pas être dans le futur : borne haute du sélecteur.
|
||||||
const AUJOURD_HUI = new Date().toISOString().split('T')[0]
|
const AUJOURD_HUI = new Date().toISOString().split('T')[0]
|
||||||
@@ -23,6 +24,11 @@ function hasAtLeast18Years(birthdate) {
|
|||||||
const GENRES = ['Femme', 'Homme', 'Autre']
|
const GENRES = ['Femme', 'Homme', 'Autre']
|
||||||
|
|
||||||
export default function Inscription() {
|
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 navigate = useNavigate()
|
||||||
const { login } = useAuth()
|
const { login } = useAuth()
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
|
import usePageMeta from '../hooks/usePageMeta'
|
||||||
|
|
||||||
export default function MentionsLegales() {
|
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 (
|
return (
|
||||||
<main className="bg-gray-50 min-h-screen px-4 sm:px-6 py-10 sm:py-16">
|
<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">
|
<div className="max-w-5xl mx-auto bg-white rounded-2xl shadow p-5 sm:p-8 md:p-10">
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
|
import usePageMeta from '../hooks/usePageMeta'
|
||||||
|
|
||||||
export default function ReglesJeu() {
|
export default function ReglesJeu() {
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
const isLoggedIn = !!user
|
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 (
|
return (
|
||||||
<div className="bg-gray-50 min-h-screen">
|
<div className="bg-gray-50 min-h-screen">
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { useAuth} from '../context/AuthContext'
|
import { useAuth} from '../context/AuthContext'
|
||||||
|
import usePageMeta from '../hooks/usePageMeta'
|
||||||
|
|
||||||
const sections = [
|
const sections = [
|
||||||
{
|
{
|
||||||
@@ -45,6 +46,11 @@ const sections = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export default function Sitemap() {
|
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 { user } = useAuth()
|
||||||
const isAdmin = user?.role === 'ADMIN'
|
const isAdmin = user?.role === 'ADMIN'
|
||||||
const visibleSections = sections.filter(
|
const visibleSections = sections.filter(
|
||||||
|
|||||||
Reference in New Issue
Block a user