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:
@@ -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 % 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 <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>
|
||||
|
||||
Reference in New Issue
Block a user