Ajouter la page concept, améliorer la bannière, ajouter pages contact et concept au menu et ajouter la condition d'inscription pour plus de 18 ans
25
frontend/.gitignore
vendored
@@ -1,25 +0,0 @@
|
|||||||
# 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?
|
|
||||||
.DS_Store
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<url><loc>https://www.dsp5-archi-o24a-4-5-g4.fr/accueil</loc></url>
|
<url><loc>https://www.dsp5-archi-o24a-4-5-g4.fr/accueil</loc></url>
|
||||||
|
<url><loc>https://www.dsp5-archi-o24a-4-5-g4.fr/notre-concept</loc></url>
|
||||||
<url><loc>https://www.dsp5-archi-o24a-4-5-g4.fr/regles-jeu</loc></url>
|
<url><loc>https://www.dsp5-archi-o24a-4-5-g4.fr/regles-jeu</loc></url>
|
||||||
<url><loc>https://www.dsp5-archi-o24a-4-5-g4.fr/contact</loc></url>
|
<url><loc>https://www.dsp5-archi-o24a-4-5-g4.fr/contact</loc></url>
|
||||||
<url><loc>https://www.dsp5-archi-o24a-4-5-g4.fr/mentions-legales</loc></url>
|
<url><loc>https://www.dsp5-archi-o24a-4-5-g4.fr/mentions-legales</loc></url>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Inscription from './pages/Inscription'
|
|||||||
import AuthCallback from './pages/AuthCallback'
|
import AuthCallback from './pages/AuthCallback'
|
||||||
import DefinirMotDePasse from './pages/DefinirMotDePasse'
|
import DefinirMotDePasse from './pages/DefinirMotDePasse'
|
||||||
import Accueil from './pages/Accueil'
|
import Accueil from './pages/Accueil'
|
||||||
|
import NotreConcept from './pages/NotreConcept'
|
||||||
import ReglesJeu from './pages/ReglesJeu'
|
import ReglesJeu from './pages/ReglesJeu'
|
||||||
import MesGains from './pages/MesGains'
|
import MesGains from './pages/MesGains'
|
||||||
import Employe from './pages/Employe'
|
import Employe from './pages/Employe'
|
||||||
@@ -36,6 +37,7 @@ export default function App() {
|
|||||||
|
|
||||||
{/* Pages publiques */}
|
{/* Pages publiques */}
|
||||||
<Route path="/accueil" element={<Layout><Accueil /></Layout>} />
|
<Route path="/accueil" element={<Layout><Accueil /></Layout>} />
|
||||||
|
<Route path="/notre-concept" element={<Layout><NotreConcept /></Layout>} />
|
||||||
<Route path="/regles-jeu" element={<Layout><ReglesJeu /></Layout>} />
|
<Route path="/regles-jeu" element={<Layout><ReglesJeu /></Layout>} />
|
||||||
<Route path="/mentions-legales" element={<Layout><MentionsLegales /></Layout>} />
|
<Route path="/mentions-legales" element={<Layout><MentionsLegales /></Layout>} />
|
||||||
<Route path="/confidentialite" element={<Layout><Confidentialite /></Layout>} />
|
<Route path="/confidentialite" element={<Layout><Confidentialite /></Layout>} />
|
||||||
|
|||||||
BIN
frontend/src/assets/img/banner-accueil.webp
Normal file
|
After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 231 KiB |
|
Before Width: | Height: | Size: 7.2 MiB |
BIN
frontend/src/assets/img/img-contact.webp
Normal file
|
After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 18 KiB |
BIN
frontend/src/assets/img/logo.webp
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
@@ -58,6 +58,16 @@ export default function Footer() {
|
|||||||
</FooterLink>
|
</FooterLink>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<FooterLink
|
||||||
|
to="/notre-concept"
|
||||||
|
pathname={pathname}
|
||||||
|
className="hover:text-white transition"
|
||||||
|
>
|
||||||
|
Notre concept
|
||||||
|
</FooterLink>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<FooterLink
|
<FooterLink
|
||||||
to="/regles-jeu"
|
to="/regles-jeu"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||||
import logo from '../assets/img/logo.png'
|
import logo from '../assets/img/logo.webp'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
@@ -19,7 +19,9 @@ export default function Header() {
|
|||||||
|
|
||||||
const navLinks = [
|
const navLinks = [
|
||||||
{ to: '/accueil', label: 'Accueil' },
|
{ to: '/accueil', label: 'Accueil' },
|
||||||
|
{ to: '/notre-concept', label: 'Notre concept' },
|
||||||
{ to: '/regles-jeu', label: 'Règles du jeu' },
|
{ to: '/regles-jeu', label: 'Règles du jeu' },
|
||||||
|
{ to: '/contact', label: 'Contact' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const initials = user
|
const initials = user
|
||||||
|
|||||||
@@ -1 +1,54 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* Animation d'entrée de la bannière d'accueil : l'illustration glisse
|
||||||
|
légèrement depuis la droite en grossissant jusqu'à sa taille finale, et
|
||||||
|
les cartes de gains apparaissent juste après avec un léger décalage
|
||||||
|
(delay défini au cas par cas via [animation-delay:...]). */
|
||||||
|
@keyframes banner-image-enter {
|
||||||
|
0% { opacity: 0; transform: translateX(70px) scale(0.9); }
|
||||||
|
100% { opacity: 1; transform: translateX(0) scale(1); }
|
||||||
|
}
|
||||||
|
@keyframes banner-badge-enter {
|
||||||
|
0% { opacity: 0; transform: translateY(16px) scale(0.9); }
|
||||||
|
100% { opacity: 1; transform: translateY(0) scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flottement continu des cartes de la bannière, une fois qu'elles sont
|
||||||
|
apparues (deux variantes légèrement différentes pour éviter un
|
||||||
|
mouvement trop synchronisé entre les deux cartes). Appliqué sur un
|
||||||
|
conteneur séparé de celui qui gère l'inclinaison au survol (tilt), pour
|
||||||
|
que les deux animations du `transform` ne se marchent pas dessus. */
|
||||||
|
@keyframes card-float-a {
|
||||||
|
0%, 100% { transform: translateY(0) rotate(0deg); }
|
||||||
|
50% { transform: translateY(-10px) rotate(1.5deg); }
|
||||||
|
}
|
||||||
|
@keyframes card-float-b {
|
||||||
|
0%, 100% { transform: translateY(0) rotate(0deg); }
|
||||||
|
50% { transform: translateY(-7px) rotate(-1.5deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dérive douce des particules de thé générées en JS (position, taille,
|
||||||
|
durée et délai tirés au sort côté composant, amplitude reçue ici via la
|
||||||
|
variable CSS --tea-drift posée en style inline par particule). */
|
||||||
|
@keyframes tea-drift {
|
||||||
|
0%, 100% { transform: translateY(0) rotate(0deg); }
|
||||||
|
50% { transform: translateY(calc(-1 * var(--tea-drift, 12px))) rotate(8deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-banner-image { animation: banner-image-enter 900ms cubic-bezier(0.22, 1, 0.36, 1) both; }
|
||||||
|
.animate-banner-badge { animation: banner-badge-enter 600ms cubic-bezier(0.22, 1, 0.36, 1) both; }
|
||||||
|
.animate-card-float-a { animation: card-float-a 4.5s ease-in-out infinite; }
|
||||||
|
.animate-card-float-b { animation: card-float-b 5.5s ease-in-out infinite; }
|
||||||
|
.animate-tea-drift { animation: tea-drift 6s ease-in-out infinite; }
|
||||||
|
|
||||||
|
/* Respecte les préférences d'accessibilité : pas d'animation pour les
|
||||||
|
personnes qui ont désactivé les mouvements dans leur OS. */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.animate-banner-image,
|
||||||
|
.animate-banner-badge,
|
||||||
|
.animate-card-float-a,
|
||||||
|
.animate-card-float-b,
|
||||||
|
.animate-tea-drift {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,53 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import usePageMeta from '../hooks/usePageMeta'
|
import usePageMeta from '../hooks/usePageMeta'
|
||||||
import bannerImg from '../assets/img/banner-the-tip-top.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 = [
|
||||||
|
{ 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 thé signature', top: '48%', right: '30%', delay: 1000, variant: 'a' },
|
||||||
|
{ label: 'Coffret découverte 39 €', top: '78%', right: '2%', delay: 1150, variant: 'b' },
|
||||||
|
{ 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()
|
||||||
|
const px = (e.clientX - rect.left) / rect.width - 0.5
|
||||||
|
const py = (e.clientY - rect.top) / rect.height - 0.5
|
||||||
|
el.style.transform = `perspective(700px) rotateX(${(-py * 12).toFixed(2)}deg) rotateY(${(px * 12).toFixed(2)}deg) scale(1.04)`
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
return Array.from({ length: count }, (_, i) => ({
|
||||||
|
id: i,
|
||||||
|
emoji: TEA_EMOJI[i % TEA_EMOJI.length],
|
||||||
|
left: Math.round(Math.random() * 100),
|
||||||
|
top: Math.round(Math.random() * 100),
|
||||||
|
size: Math.round(20 + Math.random() * 26),
|
||||||
|
duration: +(4 + Math.random() * 4).toFixed(1),
|
||||||
|
delay: +(Math.random() * 4).toFixed(1),
|
||||||
|
opacity: +(0.18 + Math.random() * 0.22).toFixed(2),
|
||||||
|
drift: Math.round(12 + Math.random() * 20),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
const faqs = [
|
const faqs = [
|
||||||
{
|
{
|
||||||
@@ -57,41 +104,160 @@ export default function Accueil() {
|
|||||||
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), [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-50 min-h-screen">
|
<div className="bg-gray-50 min-h-screen">
|
||||||
|
|
||||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }} />
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }} />
|
||||||
|
|
||||||
{/* Bannière */}
|
{/* Bannière : fond teal plein cadre, illustration très grande et animée à l'entrée,
|
||||||
<section className="w-full">
|
textes et cartes de gains répartis en plusieurs blocs pour occuper toute la zone
|
||||||
<img
|
plutôt qu'une seule colonne dense. La composition « poster » en absolute (position:
|
||||||
src={bannerImg}
|
static → absolute) ne s'active qu'à partir de lg (1024px) : en dessous — mobile ET
|
||||||
alt="Jeu-concours 100 % Gagnant Thé Tip Top, du 2 au 31 octobre 2026 : tentez de gagner un an de thé d'une valeur de 360 € pour l'ouverture de notre 10e boutique à Nice."
|
tablette —, un écran trop étroit ferait chevaucher le titre et l'illustration de façon
|
||||||
width={2048}
|
illisible (constaté sur iPad portrait), donc tout reste en empilement normal, dans
|
||||||
height={1152}
|
l'ordre du DOM, jusqu'à avoir vraiment la place. */}
|
||||||
className="w-full h-full object-cover object-center"
|
<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]">
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Hero */}
|
{/* Halo doux derrière l'illustration (uniquement pertinent à partir de lg, où
|
||||||
<section className="max-w-7xl mx-auto px-4 sm:px-6 py-12 sm:py-16 md:py-24">
|
l'illustration passe en fond de la composition) */}
|
||||||
<div className="text-center max-w-3xl mx-auto">
|
<div
|
||||||
<h1 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-[#006B6B] mb-4 sm:mb-6 leading-tight">
|
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"
|
||||||
Bienvenue au jeu-concours Thé Tip Top Nice
|
aria-hidden="true"
|
||||||
</h1>
|
/>
|
||||||
<p className="text-base sm:text-lg md:text-xl text-gray-600 mb-8 sm:mb-10">
|
|
||||||
Exclusivement réservé à la boutique Thé Tip Top de Nice. Créez votre compte, saisissez le code de votre ticket et découvrez immédiatement votre lot.
|
{/* 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
|
||||||
|
key={p.id}
|
||||||
|
className="absolute animate-tea-drift select-none"
|
||||||
|
style={{
|
||||||
|
left: `${p.left}%`,
|
||||||
|
top: `${p.top}%`,
|
||||||
|
fontSize: `${p.size}px`,
|
||||||
|
opacity: p.opacity,
|
||||||
|
animationDuration: `${p.duration}s`,
|
||||||
|
animationDelay: `${p.delay}s`,
|
||||||
|
'--tea-drift': `${p.drift}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{p.emoji}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</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
|
||||||
|
</p>
|
||||||
|
<h1 className="text-3xl sm:text-5xl lg:text-6xl font-extrabold text-white leading-[1.05]">
|
||||||
|
Jeu-concours{' '}
|
||||||
|
<span className="text-amber-300">100 % Gagnant</span>
|
||||||
|
</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
|
||||||
|
src={bannerImg}
|
||||||
|
alt="Jeu-concours 100 % Gagnant Thé Tip Top, du 2 au 31 octobre 2026 : tentez de gagner un an de thé d'une valeur de 360 € pour l'ouverture de notre 10e boutique à Nice."
|
||||||
|
className="absolute inset-y-0 right-0 w-[210%] sm:w-[130%] max-w-none h-full object-cover object-right animate-banner-image"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
src={bannerImg}
|
||||||
|
alt="Jeu-concours 100 % Gagnant Thé Tip Top, du 2 au 31 octobre 2026 : tentez de gagner un an de thé d'une valeur de 360 € pour l'ouverture de notre 10e boutique à Nice."
|
||||||
|
width={2048}
|
||||||
|
height={1152}
|
||||||
|
className="hidden lg:block lg:absolute lg:right-[-2%] lg:top-1/2 lg:-translate-y-1/2 lg:h-[145%] xl:h-[160%] lg:w-auto lg:max-w-none object-contain drop-shadow-2xl animate-banner-image"
|
||||||
|
/>
|
||||||
|
</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>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row justify-center gap-3 sm:gap-4">
|
|
||||||
|
{/* 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
|
||||||
|
onMouseMove={handleCardTilt}
|
||||||
|
onMouseLeave={resetCardTilt}
|
||||||
|
className="bg-white rounded-2xl shadow-xl px-5 sm:px-7 py-4 sm:py-5 text-center transition-transform duration-200 ease-out will-change-transform cursor-default"
|
||||||
|
>
|
||||||
|
<p className="text-3xl sm:text-4xl lg:text-5xl font-extrabold text-gray-900 leading-none">
|
||||||
|
360 <span className="text-[#006B6B]">€</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-xs sm:text-sm font-semibold text-gray-800 whitespace-nowrap mt-1.5">de thé à gagner</p>
|
||||||
|
</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) => (
|
||||||
|
<div
|
||||||
|
key={gain.label}
|
||||||
|
className="shrink-0 lg:absolute animate-banner-badge"
|
||||||
|
style={{ top: gain.top, right: gain.right, animationDelay: `${gain.delay}ms` }}
|
||||||
|
>
|
||||||
|
<div className={gain.variant === 'a' ? 'animate-card-float-a' : 'animate-card-float-b'}>
|
||||||
|
<div
|
||||||
|
onMouseMove={handleCardTilt}
|
||||||
|
onMouseLeave={resetCardTilt}
|
||||||
|
className="bg-white rounded-2xl shadow-xl px-3.5 py-2.5 text-left transition-transform duration-200 ease-out will-change-transform cursor-default"
|
||||||
|
>
|
||||||
|
<span className="text-xs sm:text-sm font-semibold text-gray-800 whitespace-nowrap">{gain.label}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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">
|
||||||
<Link
|
<Link
|
||||||
to="/saisir-code-unique"
|
to="/saisir-code-unique"
|
||||||
className="bg-[#006B6B] hover:bg-[#005555] text-white font-semibold px-6 sm:px-8 py-3 sm:py-4 rounded-xl transition-colors text-sm sm:text-base"
|
className="bg-white hover:bg-gray-100 text-[#006B6B] font-semibold px-6 sm:px-8 py-3 sm:py-4 rounded-xl transition-colors text-sm sm:text-base text-center"
|
||||||
>
|
>
|
||||||
Tester mon code
|
Tester mon code
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/regles-jeu"
|
to="/regles-jeu"
|
||||||
className="border border-[#006B6B] text-[#006B6B] hover:bg-[#005555] hover:text-white font-semibold px-6 sm:px-8 py-3 sm:py-4 rounded-xl transition-colors text-sm sm:text-base"
|
className="border-2 border-white text-white hover:bg-white hover:text-[#006B6B] font-semibold px-6 sm:px-8 py-3 sm:py-4 rounded-xl transition-colors text-sm sm:text-base text-center"
|
||||||
>
|
>
|
||||||
Découvrir les règles
|
Découvrir les règles
|
||||||
</Link>
|
</Link>
|
||||||
@@ -100,7 +266,7 @@ export default function Accueil() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Comment participer */}
|
{/* Comment participer */}
|
||||||
<section className="max-w-5xl mx-auto px-4 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 ?
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
export default function Cgu() {
|
export default function Cgu() {
|
||||||
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">
|
||||||
@@ -226,11 +228,16 @@ export default function Cgu() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-gray-800 mb-1">6.2 – Suppression du compte</h3>
|
<h3 className="font-semibold text-gray-800 mb-1">6.2 – Suppression du compte</h3>
|
||||||
<p>
|
<p>
|
||||||
L'utilisateur peut demander la suppression de son compte à tout moment en
|
La suppression du compte n'est pas disponible en libre-service. L'utilisateur peut
|
||||||
contactant directement l'équipe via thetiptop.gp4@gmail.com. La suppression entraîne
|
la demander à tout moment en contactant directement l'équipe via
|
||||||
la perte définitive des participations et gains non encore réclamés. Les données
|
thetiptop.gp4@gmail.com, notamment depuis la rubrique dédiée de son{' '}
|
||||||
personnelles sont supprimées conformément aux dispositions de la Politique de
|
<Link to="/profile#suppression-compte" className="text-[#006B6B] hover:underline font-medium">
|
||||||
Confidentialité, sous réserve des obligations légales de conservation.
|
espace personnel
|
||||||
|
</Link>
|
||||||
|
. La suppression entraîne la perte définitive des participations et gains non
|
||||||
|
encore réclamés. Les données personnelles sont supprimées conformément aux
|
||||||
|
dispositions de la Politique de Confidentialité, sous réserve des obligations
|
||||||
|
légales de conservation.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import imgContact from '../assets/img/img-contact.jpg'
|
import imgContact from '../assets/img/img-contact.webp'
|
||||||
|
|
||||||
export default function Contact() {
|
export default function Contact() {
|
||||||
return (
|
return (
|
||||||
@@ -58,8 +58,14 @@ export default function Contact() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Image */}
|
{/* Image : hauteur fixe en empilement mobile (h-64/h-80), puis à partir de md, où
|
||||||
<section className="rounded-2xl shadow overflow-hidden min-h-80">
|
les deux blocs deviennent des colonnes de grille, h-full + min-h-0 pour que
|
||||||
|
l'image s'étire exactement à la hauteur de la colonne texte (déterminée par son
|
||||||
|
propre contenu) au lieu d'imposer la sienne via son ratio d'aspect — min-h-0
|
||||||
|
annule le min-height: auto par défaut des enfants de grille qui, sinon,
|
||||||
|
empêcherait l'image de se réduire en dessous de sa hauteur intrinsèque.
|
||||||
|
object-cover recadre l'excédent. */}
|
||||||
|
<section className="rounded-2xl shadow overflow-hidden h-64 sm:h-80 md:h-full md:min-h-0">
|
||||||
<img
|
<img
|
||||||
src={imgContact}
|
src={imgContact}
|
||||||
alt="Une personne savourant un thé"
|
alt="Une personne savourant un thé"
|
||||||
|
|||||||
@@ -6,6 +6,18 @@ import { useAuth } from '../context/AuthContext'
|
|||||||
// 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]
|
||||||
|
|
||||||
|
// Le jeu est réservé aux personnes majeures.
|
||||||
|
function hasAtLeast18Years(birthdate) {
|
||||||
|
const birth = new Date(birthdate)
|
||||||
|
const today = new Date()
|
||||||
|
let age = today.getFullYear() - birth.getFullYear()
|
||||||
|
const monthDiff = today.getMonth() - birth.getMonth()
|
||||||
|
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
|
||||||
|
age--
|
||||||
|
}
|
||||||
|
return age >= 18
|
||||||
|
}
|
||||||
|
|
||||||
// Valeurs alignées sur le tableau de bord admin, qui compte exactement
|
// Valeurs alignées sur le tableau de bord admin, qui compte exactement
|
||||||
// "Homme" et "Femme" ; toute autre valeur retombe dans la catégorie "autre".
|
// "Homme" et "Femme" ; toute autre valeur retombe dans la catégorie "autre".
|
||||||
const GENRES = ['Femme', 'Homme', 'Autre']
|
const GENRES = ['Femme', 'Homme', 'Autre']
|
||||||
@@ -51,6 +63,12 @@ export default function Inscription() {
|
|||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setError('')
|
setError('')
|
||||||
|
|
||||||
|
if (!hasAtLeast18Years(form.birthdate)) {
|
||||||
|
setError('Vous devez avoir au moins 18 ans pour vous inscrire.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
await registerApi({
|
await registerApi({
|
||||||
@@ -203,9 +221,10 @@ export default function Inscription() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="reg-birthdate" className="block text-sm font-medium text-gray-800 mb-1">Date de naissance</label>
|
<label htmlFor="reg-birthdate" className="block text-sm font-medium text-gray-800 mb-1">Date de naissance <span className="text-gray-800">*</span></label>
|
||||||
<input type="date" id="reg-birthdate" name="birthdate" value={form.birthdate} onChange={handleChange} max={AUJOURD_HUI}
|
<input type="date" id="reg-birthdate" name="birthdate" value={form.birthdate} onChange={handleChange} required max={AUJOURD_HUI}
|
||||||
className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm text-gray-700 focus:outline-none focus:ring-1 focus:ring-[#006B6B] focus:border-[#006B6B]" />
|
className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm text-gray-700 focus:outline-none focus:ring-1 focus:ring-[#006B6B] focus:border-[#006B6B]" />
|
||||||
|
<p className="text-xs text-gray-500 mt-1">Le jeu-concours est réservé aux personnes majeures.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default function MesGains() {
|
|||||||
}, [handleAuthError])
|
}, [handleAuthError])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex flex-col px-8 py-12 max-w-lg mx-auto">
|
<div className="min-h-screen bg-gray-50 flex flex-col px-8 py-12 max-w-xl mx-auto">
|
||||||
<UserNav active="gains" />
|
<UserNav active="gains" />
|
||||||
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
|||||||
145
frontend/src/pages/NotreConcept.jsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import usePageMeta from '../hooks/usePageMeta'
|
||||||
|
import bannerImg from '../assets/img/banner-the-tip-top.webp'
|
||||||
|
|
||||||
|
export default function NotreConcept() {
|
||||||
|
usePageMeta({
|
||||||
|
title: 'Notre concept : un jeu-concours sans perdant | Thé Tip Top',
|
||||||
|
description: "Pourquoi un jeu 100 % gagnant ? Découvrez le concept, la répartition des lots et les garanties de loyauté du jeu-concours Thé Tip Top.",
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-50 min-h-screen">
|
||||||
|
|
||||||
|
{/* Hero */}
|
||||||
|
<section className="max-w-7xl mx-auto px-4 sm:px-6 py-12 sm:py-16 md:py-20">
|
||||||
|
<div className="text-center max-w-3xl mx-auto">
|
||||||
|
<h1 className="text-3xl sm:text-4xl md:text-5xl font-bold text-[#006B6B] mb-4 sm:mb-6">
|
||||||
|
Le concept 100 % Gagnant : un jeu-concours sans perdant
|
||||||
|
</h1>
|
||||||
|
<p className="text-base sm:text-xl text-gray-600 mb-8 sm:mb-10">
|
||||||
|
La plupart des jeux-concours font gagner une personne et laissent les autres repartir
|
||||||
|
les mains vides. Nous avons choisi l'inverse : chaque code distribué correspond à
|
||||||
|
un lot réel, attribué à l'avance. Personne ne joue pour rien.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row justify-center gap-3 sm:gap-4">
|
||||||
|
<Link
|
||||||
|
to="/accueil"
|
||||||
|
className="bg-[#006B6B] hover:bg-[#005555] text-white font-semibold px-6 sm:px-8 py-3 rounded-xl transition-colors text-sm sm:text-base"
|
||||||
|
>
|
||||||
|
Voir le jeu-concours
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/regles-jeu"
|
||||||
|
className="border border-[#006B6B] text-[#006B6B] hover:bg-[#006B6B] hover:text-white font-semibold px-6 sm:px-8 py-3 rounded-xl transition-colors text-sm sm:text-base"
|
||||||
|
>
|
||||||
|
Consulter le règlement
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Visuel principal */}
|
||||||
|
<section className="max-w-5xl mx-auto px-4 sm:px-6 pb-12 sm:pb-16">
|
||||||
|
<div className="rounded-2xl shadow overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={bannerImg}
|
||||||
|
alt="Infuseur à thé Thé Tip Top, lot garanti du jeu-concours 100 % Gagnant"
|
||||||
|
className="w-full h-full object-cover object-center"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Pourquoi un jeu sans perdant */}
|
||||||
|
<section className="max-w-5xl mx-auto px-4 sm:px-6 pb-12 sm:pb-16">
|
||||||
|
<div className="bg-white rounded-2xl shadow p-6 sm:p-10">
|
||||||
|
<h2 className="text-2xl sm:text-3xl font-bold text-[#006B6B] mb-4 sm:mb-6">
|
||||||
|
Pourquoi un jeu sans perdant
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 text-sm sm:text-base leading-relaxed">
|
||||||
|
Un jeu classique récompense la chance. Celui-ci récompense la visite. Nous ouvrons une
|
||||||
|
nouvelle adresse et nous voulions que chaque personne qui pousse la porte reparte avec
|
||||||
|
quelque chose à goûter, pas avec un ticket de loterie. C'est aussi la raison pour
|
||||||
|
laquelle le lot se retire sur place : le jeu est un prétexte à une rencontre, pas à
|
||||||
|
une transaction de plus.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Comment la loyauté du jeu est garantie */}
|
||||||
|
<section className="max-w-5xl mx-auto px-4 sm:px-6 pb-12 sm:pb-16">
|
||||||
|
<div className="bg-white rounded-2xl shadow p-6 sm:p-10">
|
||||||
|
<h2 className="text-2xl sm:text-3xl font-bold text-[#006B6B] mb-4 sm:mb-6">
|
||||||
|
Comment la loyauté du jeu est garantie
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 text-sm sm:text-base leading-relaxed">
|
||||||
|
L'intégralité des codes et leur lot associé sont générés avant l'ouverture du jeu, puis
|
||||||
|
déposés auprès de Maître Arnaud Rick, commissaire de justice. Aucun lot ne peut être
|
||||||
|
modifié en cours d'opération. Le règlement complet est{' '}
|
||||||
|
<a
|
||||||
|
href="/reglement-jeu-concours-100-gagnant.pdf"
|
||||||
|
className="text-[#006B6B] font-semibold underline hover:no-underline"
|
||||||
|
>
|
||||||
|
consultable et téléchargeable
|
||||||
|
</a>{' '}
|
||||||
|
à tout moment. Le tirage au sort du lot principal est effectué sous le contrôle du même
|
||||||
|
commissaire de justice, et le nombre de participations n'augmente pas les chances de le
|
||||||
|
remporter.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* La répartition des lots */}
|
||||||
|
<section className="max-w-5xl mx-auto px-4 sm:px-6 pb-12 sm:pb-16">
|
||||||
|
<div className="bg-white rounded-2xl shadow p-6 sm:p-10">
|
||||||
|
<h2 className="text-2xl sm:text-3xl font-bold text-[#006B6B] mb-4 sm:mb-6">
|
||||||
|
La répartition des lots, annoncée à l'avance
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 text-sm sm:text-base leading-relaxed">
|
||||||
|
Six codes sur dix donnent un infuseur, deux sur dix une boîte de 100 g, un sur dix une
|
||||||
|
boîte d'un mélange signature, et le solde un coffret découverte. Cette répartition est
|
||||||
|
publiée dès le premier jour et ne varie pas. Vous savez donc, avant même de jouer, ce
|
||||||
|
que vous pouvez obtenir et dans quelle proportion.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Un jeu conçu pour ne rien gaspiller */}
|
||||||
|
<section className="max-w-5xl mx-auto px-4 sm:px-6 pb-12 sm:pb-20">
|
||||||
|
<div className="bg-white rounded-2xl shadow p-6 sm:p-10">
|
||||||
|
<h2 className="text-2xl sm:text-3xl font-bold text-[#006B6B] mb-4 sm:mb-6">
|
||||||
|
Un jeu conçu pour ne rien gaspiller
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 text-sm sm:text-base leading-relaxed">
|
||||||
|
Le lot le plus distribué est un infuseur réutilisable, choisi pour remplacer durablement
|
||||||
|
les sachets jetables plutôt que pour faire nombre. La participation est entièrement
|
||||||
|
numérique, sans bulletin papier, et nos supports imprimés se limitent au strict
|
||||||
|
nécessaire d'un point de vente.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<section className="max-w-5xl mx-auto px-4 sm:px-6 pb-12 sm:pb-20">
|
||||||
|
<div className="bg-[#006B6B] rounded-2xl p-6 sm:p-10 text-center text-white">
|
||||||
|
<h2 className="text-2xl sm:text-3xl font-bold mb-3 sm:mb-4">
|
||||||
|
Une question sur le concept du jeu ?
|
||||||
|
</h2>
|
||||||
|
<p className="mb-6 sm:mb-8 text-base sm:text-lg">
|
||||||
|
Notre équipe reste à votre écoute pour tout ce qui concerne le jeu-concours et vos
|
||||||
|
lots.
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Link
|
||||||
|
to="/contact"
|
||||||
|
className="bg-white text-[#006B6B] px-6 sm:px-8 py-3 rounded-xl font-semibold hover:bg-[#006B6B] hover:text-white border border-white text-sm sm:text-base"
|
||||||
|
>
|
||||||
|
Contactez-nous
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,13 +1,25 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import UserNav from '../components/UserNav'
|
import UserNav from '../components/UserNav'
|
||||||
import { changePasswordApi, deleteAccountApi, meApi, forgotPasswordApi } from '../services/auth'
|
import { changePasswordApi, meApi, forgotPasswordApi } from '../services/auth'
|
||||||
import { useHandleAuthError } from '../hooks/useHandleAuthError'
|
import { useHandleAuthError } from '../hooks/useHandleAuthError'
|
||||||
|
|
||||||
export default function Profile() {
|
export default function Profile() {
|
||||||
const user = JSON.parse(localStorage.getItem('user') || '{}')
|
const user = JSON.parse(localStorage.getItem('user') || '{}')
|
||||||
const initials = `${(user.firstname || '?')[0]}${(user.lastname || '?')[0]}`.toUpperCase()
|
const initials = `${(user.firstname || '?')[0]}${(user.lastname || '?')[0]}`.toUpperCase()
|
||||||
const handleAuthError = useHandleAuthError()
|
const handleAuthError = useHandleAuthError()
|
||||||
|
const { hash } = useLocation()
|
||||||
|
|
||||||
|
const deleteSectionRef = useRef(null)
|
||||||
|
|
||||||
|
// Permet à un lien externe (ex. les CGU) de renvoyer directement sur cette
|
||||||
|
// section via /profile#suppression-compte, plutôt que d'obliger l'utilisateur
|
||||||
|
// à chercher l'information au milieu de la page.
|
||||||
|
useEffect(() => {
|
||||||
|
if (hash === '#suppression-compte') {
|
||||||
|
deleteSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
|
}
|
||||||
|
}, [hash])
|
||||||
|
|
||||||
const [pwForm, setPwForm] = useState({ currentPassword: '', newPassword: '', confirmPassword: '' })
|
const [pwForm, setPwForm] = useState({ currentPassword: '', newPassword: '', confirmPassword: '' })
|
||||||
const [showPw, setShowPw] = useState({ current: false, new: false, confirm: false })
|
const [showPw, setShowPw] = useState({ current: false, new: false, confirm: false })
|
||||||
@@ -82,7 +94,7 @@ export default function Profile() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex flex-col px-8 py-12 max-w-lg mx-auto">
|
<div className="min-h-screen bg-gray-50 flex flex-col px-8 py-12 max-w-xl mx-auto">
|
||||||
<UserNav active="profile" />
|
<UserNav active="profile" />
|
||||||
|
|
||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
@@ -206,6 +218,30 @@ export default function Profile() {
|
|||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Séparateur */}
|
||||||
|
<div className="border-t border-gray-100 my-6" />
|
||||||
|
|
||||||
|
{/* Section suppression du compte */}
|
||||||
|
<div ref={deleteSectionRef} id="suppression-compte">
|
||||||
|
<h2 className="text-sm font-semibold text-gray-800 mb-4">Supprimer mon compte</h2>
|
||||||
|
|
||||||
|
<div className="px-4 py-4 rounded-lg bg-gray-50 border border-gray-200">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Pour des raisons de sécurité, la suppression de compte n'est pas disponible en
|
||||||
|
libre-service. Envoyez-nous un email depuis votre adresse enregistrée
|
||||||
|
({user.email || '—'}) et notre équipe supprimera votre compte, vos participations et
|
||||||
|
vos gains non retirés.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="mailto:thetiptop.gp4@gmail.com?subject=Suppression%20de%20mon%20compte"
|
||||||
|
className="mt-3 inline-flex items-center justify-center bg-[#006B6B] hover:bg-[#005959] text-white font-medium px-4 py-2.5 rounded-lg transition-colors text-sm"
|
||||||
|
>
|
||||||
|
Demander la suppression par email
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export default function SaisirCodeUnique() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex flex-col px-8 py-12 max-w-lg mx-auto">
|
<div className="min-h-screen bg-gray-50 flex flex-col px-8 py-12 max-w-xl mx-auto">
|
||||||
<UserNav active="code" />
|
<UserNav active="code" />
|
||||||
|
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const sections = [
|
|||||||
title: 'Navigation principale',
|
title: 'Navigation principale',
|
||||||
links: [
|
links: [
|
||||||
{ label: 'Accueil', to: '/accueil', desc: "Page d'accueil du jeu-concours" },
|
{ label: 'Accueil', to: '/accueil', desc: "Page d'accueil du jeu-concours" },
|
||||||
|
{ label: 'Notre concept', to: '/notre-concept', desc: 'Pourquoi un jeu-concours sans perdant' },
|
||||||
{ label: 'Règles du jeu', to: '/regles-jeu', desc: 'Comment participer et que gagner' },
|
{ label: 'Règles du jeu', to: '/regles-jeu', desc: 'Comment participer et que gagner' },
|
||||||
{ label: 'Contact', to: '/contact', desc: 'Nous contacter' },
|
{ label: 'Contact', to: '/contact', desc: 'Nous contacter' },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -43,7 +43,13 @@ export async function registerApi({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (!res.ok) throw apiError(data.message || "Erreur lors de l'inscription.", res.status)
|
if (!res.ok) {
|
||||||
|
// Le backend renvoie un message générique ("Données invalides") avec le
|
||||||
|
// détail par champ dans `errors` : on préfère afficher ce détail quand il
|
||||||
|
// existe (ex. l'âge minimum requis pour la date de naissance).
|
||||||
|
const fieldMessage = Object.values(data.errors || {})[0]?.[0]
|
||||||
|
throw apiError(fieldMessage || data.message || "Erreur lors de l'inscription.", res.status)
|
||||||
|
}
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||