Files
THE-TIP-TOP/frontend/src/pages/Accueil.jsx
Emma781227 2201446ee6 changement
2026-09-14 22:30:40 +02:00

274 lines
13 KiB
JavaScript

import { useMemo } from 'react'
import { Link } from 'react-router-dom'
import usePageMeta from '../hooks/usePageMeta'
import useGameDates from '../hooks/useGameDates'
import { formatDay, formatRange } from '../utils/gameDates'
import bannerImg from '../assets/img/banner-accueil.webp'
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' },
]
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 = ''
}
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),
}))
}
// Les dates viennent du tableau de bord admin (voir useGameDates).
const buildFaqs = ({ start, lastPlayDay, lastClaimDay }) => [
{
question: 'Comment obtenir un code ?',
answer: `Achetez pour plus de 49 € dans notre boutique Thé Tip Top de Nice, du ${formatRange(start, lastPlayDay)} : un code unique à 10 caractères figure sur votre ticket de caisse ou votre facture.`,
},
{
question: 'Quels sont les lots à gagner ?',
answer: (
<>
Tous les tickets sont gagnants ! Selon votre ticket, vous pouvez remporter :
<ul className="list-disc list-inside space-y-1 mt-2">
<li>Un infuseur à thé</li>
<li>Une boîte de 100 g de thé détox ou d'infusion</li>
<li>Une boîte de 100 g de thé signature</li>
<li>Un coffret découverte d'une valeur de 39 €</li>
<li>Un coffret découverte d'une valeur de 69 €</li>
</ul>
Un tirage au sort désigne en plus un grand gagnant qui remporte une année de thé d'une valeur de 360 €.
</>
),
plainAnswer: "Tous les tickets sont gagnants ! Selon votre ticket, vous pouvez remporter : un infuseur à thé, une boîte de 100 g de thé détox ou d'infusion, une boîte de 100 g de thé signature, un coffret découverte d'une valeur de 39 €, ou un coffret découverte d'une valeur de 69 €. Un tirage au sort désigne en plus un grand gagnant qui remporte une année de thé d'une valeur de 360 €.",
},
{
question: 'Jusqu\'à quand puis-je jouer ?',
answer: `La saisie des codes est ouverte du ${formatRange(start, lastPlayDay)} inclus. Les lots sont ensuite à retirer jusqu'au ${formatDay(lastClaimDay)}.`,
},
{
question: 'Que faire si mon code n\'est pas reconnu ?',
answer: "Vérifiez que le code a été saisi sans espace ni erreur. Si le problème persiste, contactez-nous depuis la page Contact.",
},
{
question: 'Où retirer mon lot ?',
answer: `Dans notre boutique de Nice, sur présentation de votre compte utilisateur validé et de la confirmation de gain affichée dans votre espace « Mes gains », jusqu'au ${formatDay(lastClaimDay)}.`,
},
]
const buildFaqJsonLd = (faqs) => ({
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqs.map((faq) => ({
'@type': 'Question',
name: faq.question,
acceptedAnswer: {
'@type': 'Answer',
text: faq.plainAnswer || faq.answer,
},
})),
})
export default function Accueil() {
const gameDates = useGameDates()
const period = formatRange(gameDates.start, gameDates.lastPlayDay)
const faqs = useMemo(() => buildFaqs(gameDates), [gameDates])
const faqJsonLd = useMemo(() => buildFaqJsonLd(faqs), [faqs])
const bannerAlt = `Jeu-concours 100 % Gagnant Thé Tip Top, du ${period} : tentez de gagner un an de thé d'une valeur de 360 € pour l'ouverture de notre 10e boutique à Nice.`
usePageMeta({
title: `Jeu-concours 100 % Gagnant du ${formatRange(gameDates.start, gameDates.lastPlayDay, { withYear: false })} | Thé Tip Top`,
description: `Du ${formatRange(gameDates.start, gameDates.lastPlayDay, { withYear: false })}, 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.`,
})
const teaParticles = useMemo(() => createTeaParticles(30), [])
return (
<div className="bg-gray-50 min-h-screen">
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }} />
<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="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"
/>
<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>
<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 {formatRange(gameDates.start, gameDates.lastPlayDay, { padDay: true })}
</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>
<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={bannerAlt}
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={bannerAlt}
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>
<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="hidden lg:contents">
<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>
{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>
<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-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-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>
</div>
</div>
</section>
<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>
<ol className="grid grid-cols-1 sm:grid-cols-3 gap-4 list-none">
{[
{ num: '1', text: "Achat de 49 € dans notre boutique de Nice, code sur le ticket" },
{ num: '2', text: 'Créez votre compte et saisissez le code' },
{ num: '3', text: 'Découvrez et retirez votre lot en boutique' },
].map((step) => (
<li key={step.num} className="bg-white rounded-2xl shadow p-4 sm:p-6 text-center">
<div className="text-2xl sm:text-3xl font-bold text-[#006B6B] mb-2 sm:mb-3">{step.num}</div>
<p className="text-sm sm:text-base text-gray-700">{step.text}</p>
</li>
))}
</ol>
</section>
<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">
Questions fréquentes
</h2>
<div className="space-y-3">
{faqs.map((faq) => (
<details key={faq.question} className="group bg-white rounded-2xl shadow p-4 sm:p-6">
<summary className="flex items-center justify-between gap-3 cursor-pointer list-none font-semibold text-gray-800 text-sm sm:text-base">
{faq.question}
<svg
className="w-4 h-4 text-[#006B6B] flex-shrink-0 transition-transform group-open:rotate-180"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</summary>
<div className="text-gray-600 text-sm sm:text-base mt-3 leading-relaxed">
{faq.answer}
</div>
</details>
))}
</div>
</section>
</div>
)
}