changement

This commit is contained in:
Emma781227
2026-07-23 13:45:11 +02:00
parent b6506435d5
commit 7f816c165b
13 changed files with 208 additions and 96 deletions

View File

@@ -1,9 +1,10 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="fr">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<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." />
<title>Jeu concours - Thé Tip Top</title> <title>Jeu concours - Thé Tip Top</title>
</head> </head>
<body> <body>

View File

@@ -7,7 +7,6 @@ import Connexion from './pages/Connexion'
import Inscription from './pages/Inscription' import Inscription from './pages/Inscription'
import Accueil from './pages/Accueil' import Accueil from './pages/Accueil'
import ReglesJeu from './pages/ReglesJeu' import ReglesJeu from './pages/ReglesJeu'
import Participation from './pages/Participation'
import MesGains from './pages/MesGains' import MesGains from './pages/MesGains'
import Employe from './pages/Employe' import Employe from './pages/Employe'
import Admin from './pages/Admin' import Admin from './pages/Admin'
@@ -32,7 +31,6 @@ export default function App() {
{/* Pages publiques */} {/* Pages publiques */}
<Route path="/accueil" element={<Layout><Accueil /></Layout>} /> <Route path="/accueil" element={<Layout><Accueil /></Layout>} />
<Route path="/regles-jeu" element={<Layout><ReglesJeu /></Layout>} /> <Route path="/regles-jeu" element={<Layout><ReglesJeu /></Layout>} />
<Route path="/participation" element={<Layout><Participation /></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>} />
<Route path="/contact" element={<Layout><Contact /></Layout>} /> <Route path="/contact" element={<Layout><Contact /></Layout>} />

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { getStatsApi, getUsersApi, drawWinnerApi, getWinnerApi } from '../services/admin' import { getStatsApi, getUsersApi, drawWinnerApi, getWinnerApi, getEmailingContactsApi } from '../services/admin'
import { searchTicketApi, deliverClaimApi } from '../services/employee' import { searchTicketApi, deliverClaimApi } from '../services/employee'
import { useHandleAuthError } from '../hooks/useHandleAuthError' import { useHandleAuthError } from '../hooks/useHandleAuthError'
@@ -40,6 +40,11 @@ export default function Admin() {
const [ticketDeliverErr, setTicketDeliverErr] = useState('') const [ticketDeliverErr, setTicketDeliverErr] = useState('')
const [ticketSuccess, setTicketSuccess] = useState(false) const [ticketSuccess, setTicketSuccess] = useState(false)
// Contacts emailing
const [emailingContacts, setEmailingContacts] = useState(null)
const [emailingLoading, setEmailingLoading] = useState(false)
const [emailingError, setEmailingError] = useState('')
useEffect(() => { useEffect(() => {
Promise.all([getStatsApi(), getUsersApi(), getWinnerApi()]) Promise.all([getStatsApi(), getUsersApi(), getWinnerApi()])
.then(([s, u, w]) => { .then(([s, u, w]) => {
@@ -67,6 +72,37 @@ export default function Admin() {
} }
} }
const handleLoadEmailingContacts = async () => {
setEmailingLoading(true)
setEmailingError('')
try {
const data = await getEmailingContactsApi()
setEmailingContacts(data.contacts)
} catch (e) {
if (!handleAuthError(e)) setEmailingError(e.message)
} finally {
setEmailingLoading(false)
}
}
const handleExportEmailingCsv = () => {
if (!emailingContacts?.length) return
const header = ['Prénom', 'Nom', 'Email', 'Inscrit le']
const rows = emailingContacts.map((c) => [
c.firstname, c.lastname, c.email, new Date(c.createdAt).toLocaleDateString('fr-FR'),
])
const csv = [header, ...rows]
.map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(';'))
.join('\n')
const blob = new Blob([`${csv}`], { type: 'text/csv;charset=utf-8;' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `contacts-emailing-${new Date().toISOString().slice(0, 10)}.csv`
link.click()
URL.revokeObjectURL(url)
}
const handleTicketSearch = async (e) => { const handleTicketSearch = async (e) => {
e.preventDefault() e.preventDefault()
setTicketResult(null) setTicketResult(null)
@@ -127,6 +163,25 @@ export default function Admin() {
<h1 className="text-xl sm:text-2xl font-bold text-gray-800">Tableau de bord Admin</h1> <h1 className="text-xl sm:text-2xl font-bold text-gray-800">Tableau de bord Admin</h1>
{/* Fenêtre du jeu-concours */}
{stats?.gameWindow?.configured && (
<div className={`rounded-xl shadow p-4 sm:p-6 ${stats.gameWindow.isOpen ? 'bg-white' : 'bg-red-50 border border-red-200'}`}>
<div className="flex items-center justify-between flex-wrap gap-2">
<div>
<p className="text-sm font-semibold text-gray-800">
Jeu-concours {stats.gameWindow.isOpen ? 'en cours' : 'terminé'}
</p>
<p className="text-xs text-gray-500 mt-1">
Début : {formatDate(stats.gameWindow.startDate)} — Fin des participations et réclamations : {formatDate(stats.gameWindow.claimDeadline)}
</p>
</div>
<span className={`text-xs font-semibold px-2.5 py-1 rounded-full ${stats.gameWindow.isOpen ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
{stats.gameWindow.isOpen ? 'Ouvert' : 'Fermé'}
</span>
</div>
</div>
)}
{/* Stats principales */} {/* Stats principales */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4">
<StatCard label="Participants" value={stats?.users} /> <StatCard label="Participants" value={stats?.users} />
@@ -142,6 +197,39 @@ export default function Admin() {
<StatCard label="Tickets restants" value={stats?.ticketsRemaining} /> <StatCard label="Tickets restants" value={stats?.ticketsRemaining} />
</div> </div>
{/* Stats démographiques */}
{stats?.genderStats && stats?.ageStats && (
<div className="grid sm:grid-cols-2 gap-3 sm:gap-4">
<div className="bg-white rounded-xl shadow p-4 sm:p-6">
<h2 className="text-base sm:text-lg font-bold text-gray-800 mb-4">Genre des participants</h2>
<div className="space-y-2">
{[
{ label: 'Femmes', value: stats.genderStats.women },
{ label: 'Hommes', value: stats.genderStats.men },
{ label: 'Autre / non renseigné', value: stats.genderStats.other },
].map((row) => (
<div key={row.label} className="flex items-center justify-between text-sm">
<span className="text-gray-500">{row.label}</span>
<span className="font-semibold text-gray-800">{row.value}</span>
</div>
))}
</div>
</div>
<div className="bg-white rounded-xl shadow p-4 sm:p-6">
<h2 className="text-base sm:text-lg font-bold text-gray-800 mb-4">Âge des participants</h2>
<div className="space-y-2">
{['-18', '18-24', '25-34', '35-44', '45-54', '55-64', '65+', 'unknown'].map((bracket) => (
<div key={bracket} className="flex items-center justify-between text-sm">
<span className="text-gray-500">{bracket === 'unknown' ? 'Non renseigné' : `${bracket} ans`}</span>
<span className="font-semibold text-gray-800">{stats.ageStats[bracket] ?? 0}</span>
</div>
))}
</div>
</div>
</div>
)}
{/* Répartition par lot */} {/* Répartition par lot */}
{stats?.lotsStats?.length > 0 && ( {stats?.lotsStats?.length > 0 && (
<div className="bg-white rounded-xl shadow p-4 sm:p-6"> <div className="bg-white rounded-xl shadow p-4 sm:p-6">
@@ -171,13 +259,80 @@ export default function Admin() {
</div> </div>
)} )}
{/* ── Contacts emailing ── */}
<div className="bg-white rounded-xl shadow p-4 sm:p-6">
<div className="flex items-center justify-between mb-4 flex-wrap gap-2">
<h2 className="text-base sm:text-lg font-bold text-gray-800">Contacts emailing</h2>
<div className="flex gap-2">
<button
onClick={handleLoadEmailingContacts}
disabled={emailingLoading}
className="bg-[#006B6B] hover:bg-[#005959] disabled:opacity-50 text-white font-semibold px-4 py-2 rounded-lg transition-colors text-xs sm:text-sm"
>
{emailingLoading ? 'Chargement…' : emailingContacts ? 'Actualiser' : 'Charger les contacts'}
</button>
{emailingContacts?.length > 0 && (
<button
onClick={handleExportEmailingCsv}
className="border border-gray-200 text-gray-600 hover:bg-gray-50 font-semibold px-4 py-2 rounded-lg transition-colors text-xs sm:text-sm"
>
Exporter en CSV
</button>
)}
</div>
</div>
{emailingError && <p className="text-red-500 text-sm mb-3">{emailingError}</p>}
<p className="text-sm text-gray-500 mb-3">
Clients ayant accepté d'être contactés à des fins marketing.
{emailingContacts && <> — <span className="font-semibold text-gray-800">{emailingContacts.length}</span> contact(s)</>}
</p>
{emailingContacts && (
<div className="overflow-x-auto -mx-4 sm:mx-0">
<table className="w-full text-sm min-w-[400px]">
<thead>
<tr className="text-left text-gray-500 border-b text-xs sm:text-sm">
<th className="pb-2 px-4 sm:px-0">Nom</th>
<th className="pb-2 px-4 sm:px-0">Email</th>
<th className="pb-2 px-4 sm:px-0 text-right">Inscrit le</th>
</tr>
</thead>
<tbody>
{emailingContacts.length === 0 && (
<tr>
<td colSpan={3} className="py-6 text-center text-gray-400 text-sm">
Aucun contact n'a accepté l'emailing pour le moment
</td>
</tr>
)}
{emailingContacts.map((contact) => (
<tr key={contact.email} className="border-b last:border-0">
<td className="py-3 px-4 sm:px-0 font-medium text-gray-800 whitespace-nowrap text-xs sm:text-sm">
{contact.firstname} {contact.lastname}
</td>
<td className="py-3 px-4 sm:px-0 text-gray-500 text-xs">{contact.email}</td>
<td className="py-3 px-4 sm:px-0 text-right text-gray-500 text-xs">
{new Date(contact.createdAt).toLocaleDateString('fr-FR')}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* ── Remise de lot ── */} {/* ── Remise de lot ── */}
<div className="bg-white rounded-xl shadow p-4 sm:p-6"> <div className="bg-white rounded-xl shadow p-4 sm:p-6">
<h2 className="text-base sm:text-lg font-bold text-gray-800 mb-4">Remise de lot</h2> <h2 className="text-base sm:text-lg font-bold text-gray-800 mb-4">Remise de lot</h2>
<form onSubmit={handleTicketSearch} className="flex gap-2 mb-4"> <form onSubmit={handleTicketSearch} className="flex gap-2 mb-4">
<label htmlFor="admin-ticket-code" className="sr-only">Code ticket</label>
<input <input
type="text" type="text"
id="admin-ticket-code"
value={ticketCode} value={ticketCode}
onChange={(e) => setTicketCode(e.target.value.toUpperCase())} onChange={(e) => setTicketCode(e.target.value.toUpperCase())}
placeholder="Code ticket (10 caractères)" placeholder="Code ticket (10 caractères)"

View File

@@ -55,11 +55,12 @@ export default function Connexion() {
<form onSubmit={handleSubmit} className="space-y-5"> <form onSubmit={handleSubmit} className="space-y-5">
<div> <div>
<label className="block text-sm font-medium text-gray-800 mb-1"> <label htmlFor="email" className="block text-sm font-medium text-gray-800 mb-1">
Adresse email <span className="text-gray-800">*</span> Adresse email <span className="text-gray-800">*</span>
</label> </label>
<input <input
type="email" type="email"
id="email"
name="email" name="email"
value={form.email} value={form.email}
onChange={handleChange} onChange={handleChange}
@@ -70,12 +71,13 @@ export default function Connexion() {
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-800 mb-1"> <label htmlFor="password" className="block text-sm font-medium text-gray-800 mb-1">
Mot de passe <span className="text-gray-800">*</span> Mot de passe <span className="text-gray-800">*</span>
</label> </label>
<div className="relative"> <div className="relative">
<input <input
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
id="password"
name="password" name="password"
value={form.password} value={form.password}
onChange={handleChange} onChange={handleChange}

View File

@@ -35,32 +35,36 @@ export default function Contact() {
</h2> </h2>
<form className="space-y-4 sm:space-y-5"> <form className="space-y-4 sm:space-y-5">
<div> <div>
<label className="block text-gray-700 mb-1.5 text-sm sm:text-base">Nom</label> <label htmlFor="contact-name" className="block text-gray-700 mb-1.5 text-sm sm:text-base">Nom</label>
<input <input
type="text" type="text"
id="contact-name"
placeholder="Votre nom" placeholder="Votre nom"
className="w-full border border-gray-300 rounded-xl px-4 py-2.5 sm:py-3 text-sm sm:text-base focus:outline-none focus:ring-2 focus:ring-green-600" className="w-full border border-gray-300 rounded-xl px-4 py-2.5 sm:py-3 text-sm sm:text-base focus:outline-none focus:ring-2 focus:ring-green-600"
/> />
</div> </div>
<div> <div>
<label className="block text-gray-700 mb-1.5 text-sm sm:text-base">Email</label> <label htmlFor="contact-email" className="block text-gray-700 mb-1.5 text-sm sm:text-base">Email</label>
<input <input
type="email" type="email"
id="contact-email"
placeholder="Votre email" placeholder="Votre email"
className="w-full border border-gray-300 rounded-xl px-4 py-2.5 sm:py-3 text-sm sm:text-base focus:outline-none focus:ring-2 focus:ring-green-600" className="w-full border border-gray-300 rounded-xl px-4 py-2.5 sm:py-3 text-sm sm:text-base focus:outline-none focus:ring-2 focus:ring-green-600"
/> />
</div> </div>
<div> <div>
<label className="block text-gray-700 mb-1.5 text-sm sm:text-base">Sujet</label> <label htmlFor="contact-subject" className="block text-gray-700 mb-1.5 text-sm sm:text-base">Sujet</label>
<input <input
type="text" type="text"
id="contact-subject"
placeholder="Sujet de votre demande" placeholder="Sujet de votre demande"
className="w-full border border-gray-300 rounded-xl px-4 py-2.5 sm:py-3 text-sm sm:text-base focus:outline-none focus:ring-2 focus:ring-green-600" className="w-full border border-gray-300 rounded-xl px-4 py-2.5 sm:py-3 text-sm sm:text-base focus:outline-none focus:ring-2 focus:ring-green-600"
/> />
</div> </div>
<div> <div>
<label className="block text-gray-700 mb-1.5 text-sm sm:text-base">Message</label> <label htmlFor="contact-message" className="block text-gray-700 mb-1.5 text-sm sm:text-base">Message</label>
<textarea <textarea
id="contact-message"
rows="4" rows="4"
placeholder="Votre message..." placeholder="Votre message..."
className="w-full border border-gray-300 rounded-xl px-4 py-2.5 sm:py-3 text-sm sm:text-base focus:outline-none focus:ring-2 focus:ring-green-600 resize-none" className="w-full border border-gray-300 rounded-xl px-4 py-2.5 sm:py-3 text-sm sm:text-base focus:outline-none focus:ring-2 focus:ring-green-600 resize-none"

View File

@@ -85,8 +85,10 @@ export default function Employe() {
{/* Formulaire de recherche */} {/* Formulaire de recherche */}
<form onSubmit={handleSearch} className="flex gap-2 mb-6"> <form onSubmit={handleSearch} className="flex gap-2 mb-6">
<label htmlFor="employee-ticket-code" className="sr-only">Code ticket</label>
<input <input
type="text" type="text"
id="employee-ticket-code"
value={code} value={code}
onChange={(e) => setCode(e.target.value.toUpperCase())} onChange={(e) => setCode(e.target.value.toUpperCase())}
required required

View File

@@ -114,6 +114,7 @@ export default function Inscription() {
value={d} value={d}
onChange={(e) => handleDigit(i, e.target.value)} onChange={(e) => handleDigit(i, e.target.value)}
onKeyDown={(e) => handleDigitKeyDown(i, e)} onKeyDown={(e) => handleDigitKeyDown(i, e)}
aria-label={`Chiffre ${i + 1} du code de vérification`}
className="w-14 h-14 text-center text-2xl font-bold border-2 border-gray-200 rounded-xl focus:outline-none focus:border-[#006B6B] focus:ring-1 focus:ring-[#006B6B] transition-colors" className="w-14 h-14 text-center text-2xl font-bold border-2 border-gray-200 rounded-xl focus:outline-none focus:border-[#006B6B] focus:ring-1 focus:ring-[#006B6B] transition-colors"
/> />
))} ))}
@@ -158,27 +159,27 @@ export default function Inscription() {
<form onSubmit={handleSubmit} className="space-y-5"> <form onSubmit={handleSubmit} className="space-y-5">
<div> <div>
<label className="block text-sm font-medium text-gray-800 mb-1">Adresse email <span className="text-gray-800">*</span></label> <label htmlFor="reg-email" className="block text-sm font-medium text-gray-800 mb-1">Adresse email <span className="text-gray-800">*</span></label>
<input type="email" name="email" value={form.email} onChange={handleChange} required placeholder="Votre email" <input type="email" id="reg-email" name="email" value={form.email} onChange={handleChange} required placeholder="Votre email"
className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm text-gray-700 placeholder-gray-300 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 placeholder-gray-300 focus:outline-none focus:ring-1 focus:ring-[#006B6B] focus:border-[#006B6B]" />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-800 mb-1">Nom <span className="text-gray-800">*</span></label> <label htmlFor="reg-lastName" className="block text-sm font-medium text-gray-800 mb-1">Nom <span className="text-gray-800">*</span></label>
<input type="text" name="lastName" value={form.lastName} onChange={handleChange} required placeholder="Votre nom" <input type="text" id="reg-lastName" name="lastName" value={form.lastName} onChange={handleChange} required placeholder="Votre nom"
className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm text-gray-700 placeholder-gray-300 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 placeholder-gray-300 focus:outline-none focus:ring-1 focus:ring-[#006B6B] focus:border-[#006B6B]" />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-800 mb-1">Prénom <span className="text-gray-800">*</span></label> <label htmlFor="reg-firstName" className="block text-sm font-medium text-gray-800 mb-1">Prénom <span className="text-gray-800">*</span></label>
<input type="text" name="firstName" value={form.firstName} onChange={handleChange} required placeholder="Votre prénom" <input type="text" id="reg-firstName" name="firstName" value={form.firstName} onChange={handleChange} required placeholder="Votre prénom"
className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm text-gray-700 placeholder-gray-300 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 placeholder-gray-300 focus:outline-none focus:ring-1 focus:ring-[#006B6B] focus:border-[#006B6B]" />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-800 mb-1">Mot de passe <span className="text-gray-800">*</span></label> <label htmlFor="reg-password" className="block text-sm font-medium text-gray-800 mb-1">Mot de passe <span className="text-gray-800">*</span></label>
<div className="relative"> <div className="relative">
<input type={showPassword ? 'text' : 'password'} name="password" value={form.password} onChange={handleChange} required placeholder="Votre mot de passe" <input type={showPassword ? 'text' : 'password'} id="reg-password" name="password" value={form.password} onChange={handleChange} required placeholder="Votre mot de passe"
className="w-full border border-gray-200 rounded-lg px-4 py-3 pr-12 text-sm text-gray-700 placeholder-gray-300 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 pr-12 text-sm text-gray-700 placeholder-gray-300 focus:outline-none focus:ring-1 focus:ring-[#006B6B] focus:border-[#006B6B]" />
<button type="button" onClick={() => setShowPassword(!showPassword)} <button type="button" onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600" aria-label="Afficher/masquer"> className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600" aria-label="Afficher/masquer">

View File

@@ -1,61 +0,0 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'
export default function Participation() {
const [code, setCode] = useState('')
const [result, setResult] = useState(null)
const handleSubmit = (e) => {
e.preventDefault()
// TODO: connecter au backend
setResult({
lot: 'Infusion découverte (30g)',
description: 'Un assortiment de nos meilleures infusions',
})
}
return (
<div className="min-h-screen bg-green-50 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl shadow-lg w-full max-w-md p-8">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-green-800">Thé Tip Top</h1>
<p className="text-gray-500 mt-1">Participer au jeu-concours</p>
</div>
<form onSubmit={handleSubmit} className="space-y-5">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Code ticket
</label>
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
required
placeholder="Ex: TTT-XXXXX"
className="w-full border border-gray-300 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-green-500 font-mono tracking-widest"
/>
</div>
<button
type="submit"
className="w-full bg-green-700 hover:bg-green-800 text-white font-semibold py-2.5 rounded-lg transition-colors"
>
Valider mon ticket
</button>
</form>
{result && (
<div className="mt-6 bg-green-100 border border-green-300 rounded-xl p-5 text-center">
<p className="text-green-800 font-bold text-lg">Félicitations !</p>
<p className="text-green-700 font-medium mt-1">{result.lot}</p>
<p className="text-gray-600 text-sm mt-1">{result.description}</p>
<Link to="/mes-gains" className="inline-block mt-4 text-sm text-green-700 font-medium hover:underline">
Voir tous mes gains →
</Link>
</div>
)}
</div>
</div>
)
}

View File

@@ -121,10 +121,11 @@ export default function Profile() {
{ name: 'confirmPassword', label: 'Confirmer le nouveau mot de passe', key: 'confirm' }, { name: 'confirmPassword', label: 'Confirmer le nouveau mot de passe', key: 'confirm' },
].map(({ name, label, key }) => ( ].map(({ name, label, key }) => (
<div key={name}> <div key={name}>
<label className="block text-sm font-medium text-gray-800 mb-1">{label}</label> <label htmlFor={name} className="block text-sm font-medium text-gray-800 mb-1">{label}</label>
<div className="relative"> <div className="relative">
<input <input
type={showPw[key] ? 'text' : 'password'} type={showPw[key] ? 'text' : 'password'}
id={name}
name={name} name={name}
value={pwForm[name]} value={pwForm[name]}
onChange={handlePwChange} onChange={handlePwChange}

View File

@@ -56,8 +56,10 @@ export default function SaisirCodeUnique() {
</div> </div>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<label htmlFor="ticket-code" className="sr-only">Code de votre ticket</label>
<input <input
type="text" type="text"
id="ticket-code"
value={code} value={code}
onChange={(e) => setCode(e.target.value.toUpperCase())} onChange={(e) => setCode(e.target.value.toUpperCase())}
placeholder="Votre code" placeholder="Votre code"

View File

@@ -32,6 +32,13 @@ export async function drawWinnerApi() {
return data return data
} }
export async function getEmailingContactsApi() {
const res = await fetch('/api/admin/emailing', { headers: authHeaders() })
const data = await res.json()
if (!res.ok) throw apiError(data.message || 'Erreur contacts emailing.', res.status)
return data
}
export async function getWinnerApi() { export async function getWinnerApi() {
const res = await fetch('/api/admin/draw', { headers: authHeaders() }) const res = await fetch('/api/admin/draw', { headers: authHeaders() })
if (res.status === 404) return null if (res.status === 404) return null

View File

@@ -19,8 +19,8 @@ describe('Connexion', () => {
renderWithProviders(<Connexion />, { route: '/connexion' }) renderWithProviders(<Connexion />, { route: '/connexion' })
expect(screen.getByRole('heading', { name: /se connecter/i })).toBeInTheDocument() expect(screen.getByRole('heading', { name: /se connecter/i })).toBeInTheDocument()
expect(screen.getByPlaceholderText(/votre email/i)).toBeInTheDocument() expect(screen.getByLabelText(/adresse email/i)).toBeInTheDocument()
expect(screen.getByPlaceholderText(/votre mot de passe/i)).toBeInTheDocument() expect(screen.getByLabelText(/^mot de passe/i)).toBeInTheDocument()
expect(screen.getByRole('button', { name: /se connecter/i })).toBeInTheDocument() expect(screen.getByRole('button', { name: /se connecter/i })).toBeInTheDocument()
}) })
@@ -37,8 +37,8 @@ describe('Connexion', () => {
renderWithProviders(<Connexion />, { route: '/connexion' }) renderWithProviders(<Connexion />, { route: '/connexion' })
await userEvent.type(screen.getByPlaceholderText(/votre email/i), 'test@example.com') await userEvent.type(screen.getByLabelText(/adresse email/i), 'test@example.com')
await userEvent.type(screen.getByPlaceholderText(/votre mot de passe/i), 'mauvais-mdp') await userEvent.type(screen.getByLabelText(/^mot de passe/i), 'mauvais-mdp')
await userEvent.click(screen.getByRole('button', { name: /se connecter/i })) await userEvent.click(screen.getByRole('button', { name: /se connecter/i }))
expect(await screen.findByText('Email ou mot de passe incorrect.')).toBeInTheDocument() expect(await screen.findByText('Email ou mot de passe incorrect.')).toBeInTheDocument()
@@ -56,8 +56,8 @@ describe('Connexion', () => {
routes: { '/mes-gains': <p>Page Mes Gains</p> }, routes: { '/mes-gains': <p>Page Mes Gains</p> },
}) })
await userEvent.type(screen.getByPlaceholderText(/votre email/i), 'client@example.com') await userEvent.type(screen.getByLabelText(/adresse email/i), 'client@example.com')
await userEvent.type(screen.getByPlaceholderText(/votre mot de passe/i), 'MotDePasse123!') await userEvent.type(screen.getByLabelText(/^mot de passe/i), 'MotDePasse123!')
await userEvent.click(screen.getByRole('button', { name: /se connecter/i })) await userEvent.click(screen.getByRole('button', { name: /se connecter/i }))
expect(await screen.findByText('Page Mes Gains')).toBeInTheDocument() expect(await screen.findByText('Page Mes Gains')).toBeInTheDocument()
@@ -75,8 +75,8 @@ describe('Connexion', () => {
routes: { '/admin': <p>Page Admin</p> }, routes: { '/admin': <p>Page Admin</p> },
}) })
await userEvent.type(screen.getByPlaceholderText(/votre email/i), 'admin@example.com') await userEvent.type(screen.getByLabelText(/adresse email/i), 'admin@example.com')
await userEvent.type(screen.getByPlaceholderText(/votre mot de passe/i), 'MotDePasse123!') await userEvent.type(screen.getByLabelText(/^mot de passe/i), 'MotDePasse123!')
await userEvent.click(screen.getByRole('button', { name: /se connecter/i })) await userEvent.click(screen.getByRole('button', { name: /se connecter/i }))
expect(await screen.findByText('Page Admin')).toBeInTheDocument() expect(await screen.findByText('Page Admin')).toBeInTheDocument()

View File

@@ -12,10 +12,10 @@ vi.mock('../../services/auth', () => ({
// Remplit et soumet le formulaire d'inscription (étape 1). // Remplit et soumet le formulaire d'inscription (étape 1).
async function remplirEtSoumettre() { async function remplirEtSoumettre() {
await userEvent.type(screen.getByPlaceholderText(/votre email/i), 'jean@example.com') await userEvent.type(screen.getByLabelText(/adresse email/i), 'jean@example.com')
await userEvent.type(screen.getByPlaceholderText(/votre nom/i), 'Dupont') await userEvent.type(screen.getByLabelText(/^nom/i), 'Dupont')
await userEvent.type(screen.getByPlaceholderText(/votre prénom/i), 'Jean') await userEvent.type(screen.getByLabelText(/prénom/i), 'Jean')
await userEvent.type(screen.getByPlaceholderText(/votre mot de passe/i), 'MotDePasse123!') await userEvent.type(screen.getByLabelText(/^mot de passe/i), 'MotDePasse123!')
await userEvent.click(screen.getByRole('checkbox')) await userEvent.click(screen.getByRole('checkbox'))
await userEvent.click(screen.getByRole('button', { name: /s'inscrire/i })) await userEvent.click(screen.getByRole('button', { name: /s'inscrire/i }))
} }
@@ -37,10 +37,10 @@ describe('Inscription', () => {
renderWithProviders(<Inscription />, { route: '/inscription' }) renderWithProviders(<Inscription />, { route: '/inscription' })
expect(screen.getByRole('heading', { name: /s'inscrire/i })).toBeInTheDocument() expect(screen.getByRole('heading', { name: /s'inscrire/i })).toBeInTheDocument()
expect(screen.getByPlaceholderText(/votre email/i)).toBeInTheDocument() expect(screen.getByLabelText(/adresse email/i)).toBeInTheDocument()
expect(screen.getByPlaceholderText(/votre nom/i)).toBeInTheDocument() expect(screen.getByLabelText(/^nom/i)).toBeInTheDocument()
expect(screen.getByPlaceholderText(/votre prénom/i)).toBeInTheDocument() expect(screen.getByLabelText(/prénom/i)).toBeInTheDocument()
expect(screen.getByPlaceholderText(/votre mot de passe/i)).toBeInTheDocument() expect(screen.getByLabelText(/^mot de passe/i)).toBeInTheDocument()
}) })
it("désactive le bouton tant que les conditions ne sont pas acceptées", async () => { it("désactive le bouton tant que les conditions ne sont pas acceptées", async () => {