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

This commit is contained in:
Alexis SANOGO
2026-08-29 01:44:20 +02:00
parent fc9bcbf8da
commit a1ab2df975
22 changed files with 495 additions and 66 deletions

View File

@@ -1,6 +1,53 @@
import { useMemo } from 'react'
import { Link } from 'react-router-dom'
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 = [
{
@@ -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.",
})
// 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 (
<div className="bg-gray-50 min-h-screen">
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }} />
{/* Bannière */}
<section className="w-full">
<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="w-full h-full object-cover object-center"
/>
</section>
{/* 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]">
{/* Hero */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 py-12 sm:py-16 md:py-24">
<div className="text-center max-w-3xl mx-auto">
<h1 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-[#006B6B] mb-4 sm:mb-6 leading-tight">
Bienvenue au jeu-concours Thé Tip Top Nice
</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.
{/* 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
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&nbsp;% 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>
<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&nbsp;<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
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
</Link>
<Link
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
</Link>
@@ -100,7 +266,7 @@ export default function Accueil() {
</section>
{/* 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">
Comment participer ?
</h2>