Refonte front : flux inscription/connexion, saisie code et historique gains
- Redesign Connexion, Inscription avec modal succès et compte à rebours - Page SaisirCodeUnique connectée au backend avec overlay Félicitation - Page Perdu pour les codes invalides/déjà utilisés - MesGains redesigné en timeline avec appel API et bouton saisir nouveau code - Profile redesigné avec onglets et données depuis localStorage - Service game.js ajouté (participateApi, myGainsApi) - Ajout des designs de référence dans frontend/Design/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
BIN
frontend/Design/Code gagnang.png
Normal file
BIN
frontend/Design/Code gagnang.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
frontend/Design/Inscription reussit.png
Normal file
BIN
frontend/Design/Inscription reussit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
frontend/Design/Saisir_code.png
Normal file
BIN
frontend/Design/Saisir_code.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
frontend/Design/historique des gain .png
Normal file
BIN
frontend/Design/historique des gain .png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
BIN
frontend/Design/profil .png
Normal file
BIN
frontend/Design/profil .png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -1,12 +1,13 @@
|
|||||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
||||||
import Connexion from './pages/Connexion'
|
import Connexion from './pages/Connexion'
|
||||||
import Inscription from './pages/Inscription'
|
import Inscription from './pages/Inscription'
|
||||||
import Participation from './pages/Participation'
|
//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'
|
||||||
import Profile from './pages/Profile'
|
import Profile from './pages/Profile'
|
||||||
import SaisirCodeUnique from './pages/SaisirCodeUnique.jsx'
|
import SaisirCodeUnique from './pages/SaisirCodeUnique.jsx'
|
||||||
|
import Perdu from './pages/Perdu'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
@@ -15,12 +16,13 @@ export default function App() {
|
|||||||
<Route path="/" element={<Navigate to="/connexion" replace />} />
|
<Route path="/" element={<Navigate to="/connexion" replace />} />
|
||||||
<Route path="/connexion" element={<Connexion />} />
|
<Route path="/connexion" element={<Connexion />} />
|
||||||
<Route path="/inscription" element={<Inscription />} />
|
<Route path="/inscription" element={<Inscription />} />
|
||||||
<Route path="/participation" element={<Participation />} />
|
{/* <Route path="/participation" element={<Participation />} /> */}
|
||||||
<Route path="/mes-gains" element={<MesGains />} />
|
<Route path="/mes-gains" element={<MesGains />} />
|
||||||
<Route path="/employe" element={<Employe />} />
|
<Route path="/employe" element={<Employe />} />
|
||||||
<Route path="/admin" element={<Admin />} />
|
<Route path="/admin" element={<Admin />} />
|
||||||
<Route path="/profile" element={<Profile />} />
|
<Route path="/profile" element={<Profile />} />
|
||||||
<Route path="/saisir-code-unique" element={<SaisirCodeUnique />} />
|
<Route path="/saisir-code-unique" element={<SaisirCodeUnique />} />
|
||||||
|
<Route path="/perdu" element={<Perdu />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default function Connexion() {
|
|||||||
localStorage.setItem('user', JSON.stringify(user))
|
localStorage.setItem('user', JSON.stringify(user))
|
||||||
if (user.role === 'ADMIN') navigate('/admin')
|
if (user.role === 'ADMIN') navigate('/admin')
|
||||||
else if (user.role === 'EMPLOYEE') navigate('/employe')
|
else if (user.role === 'EMPLOYEE') navigate('/employe')
|
||||||
else navigate('/participation')
|
else navigate('/mes-gains')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Link, useNavigate } from 'react-router-dom'
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
import { registerApi } from '../services/auth'
|
import { registerApi } from '../services/auth'
|
||||||
|
|
||||||
export default function Inscription() {
|
export default function Inscription() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({ email: '', lastName: '', firstName: '', password: '' })
|
||||||
email: '',
|
|
||||||
lastName: '',
|
|
||||||
firstName: '',
|
|
||||||
password: '',
|
|
||||||
})
|
|
||||||
const [showPassword, setShowPassword] = useState(false)
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [successName, setSuccessName] = useState(null)
|
||||||
|
const [countdown, setCountdown] = useState(5)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (successName === null) return
|
||||||
|
if (countdown === 0) {
|
||||||
|
navigate('/saisir-code-unique')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const t = setTimeout(() => setCountdown((c) => c - 1), 1000)
|
||||||
|
return () => clearTimeout(t)
|
||||||
|
}, [successName, countdown, navigate])
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
const { name, value } = e.target
|
const { name, value } = e.target
|
||||||
@@ -30,7 +37,7 @@ export default function Inscription() {
|
|||||||
lastname: form.lastName,
|
lastname: form.lastName,
|
||||||
password: form.password,
|
password: form.password,
|
||||||
})
|
})
|
||||||
navigate('/connexion', { state: { registered: true } })
|
setSuccessName(form.firstName)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -53,7 +60,6 @@ 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">
|
<label 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>
|
||||||
@@ -120,9 +126,7 @@ export default function Inscription() {
|
|||||||
aria-label="Afficher/masquer le mot de passe"
|
aria-label="Afficher/masquer le mot de passe"
|
||||||
>
|
>
|
||||||
<div className="w-6 h-6 rounded-full border-2 border-gray-400 flex items-center justify-center">
|
<div className="w-6 h-6 rounded-full border-2 border-gray-400 flex items-center justify-center">
|
||||||
{showPassword && (
|
{showPassword && <div className="w-1.5 h-1.5 rounded-full bg-gray-400" />}
|
||||||
<div className="w-1.5 h-1.5 rounded-full bg-gray-400" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,32 +148,42 @@ export default function Inscription() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<button
|
<button type="button" className="flex items-center justify-center gap-2 border border-gray-200 rounded-lg py-3 text-sm text-gray-700 hover:bg-gray-50 transition-colors">
|
||||||
type="button"
|
<div className="w-5 h-5 rounded-full border border-gray-400 flex items-center justify-center text-[10px] font-bold text-gray-500">G</div>
|
||||||
className="flex items-center justify-center gap-2 border border-gray-200 rounded-lg py-3 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="w-5 h-5 rounded-full border border-gray-400 flex items-center justify-center text-[10px] font-bold text-gray-500">
|
|
||||||
G
|
|
||||||
</div>
|
|
||||||
Google
|
Google
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button" className="flex items-center justify-center gap-2 border border-gray-200 rounded-lg py-3 text-sm text-gray-700 hover:bg-gray-50 transition-colors">
|
||||||
type="button"
|
<div className="w-5 h-5 rounded-full border border-gray-400 flex items-center justify-center text-[10px] font-bold text-gray-500">f</div>
|
||||||
className="flex items-center justify-center gap-2 border border-gray-200 rounded-lg py-3 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="w-5 h-5 rounded-full border border-gray-400 flex items-center justify-center text-[10px] font-bold text-gray-500">
|
|
||||||
f
|
|
||||||
</div>
|
|
||||||
Facebook
|
Facebook
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-center text-sm text-gray-500 mt-8">
|
<p className="text-center text-sm text-gray-500 mt-8">
|
||||||
Vous avez déjà un compte ?{' '}
|
Vous avez déjà un compte ?{' '}
|
||||||
<Link to="/connexion" className="text-gray-700 font-medium hover:underline">
|
<Link to="/connexion" className="text-gray-700 font-medium hover:underline">Se connecter</Link>
|
||||||
Se connecter
|
|
||||||
</Link>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{/* Modal succès */}
|
||||||
|
{successName !== null && (
|
||||||
|
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 px-6">
|
||||||
|
<div className="bg-white rounded-2xl p-8 w-full max-w-sm text-center shadow-xl">
|
||||||
|
<div className="flex justify-center mb-5">
|
||||||
|
<div className="w-16 h-16 bg-green-500 rounded-full flex items-center justify-center">
|
||||||
|
<svg className="w-9 h-9 text-white" fill="none" stroke="currentColor" strokeWidth={3} viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
||||||
|
Bienvenue {successName} !!
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-500 text-sm leading-relaxed">
|
||||||
|
Votre compte a été créé avec succès. Vous serez redirigé vers l'étape suivante dans{' '}
|
||||||
|
<span className="font-semibold text-[#006B6B]">{countdown}</span> secondes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,108 @@
|
|||||||
const MOCK_GAINS = [
|
import { useState, useEffect } from 'react'
|
||||||
{ id: 1, code: 'TTT-00123', lot: 'Infusion découverte (30g)', status: 'PENDING' },
|
import { Link } from 'react-router-dom'
|
||||||
{ id: 2, code: 'TTT-00456', lot: 'Thé signature (100g)', status: 'DELIVERED' },
|
import { myGainsApi } from '../services/game'
|
||||||
]
|
|
||||||
|
|
||||||
const statusLabel = {
|
function TabNav({ active }) {
|
||||||
PENDING: { label: 'À récupérer', color: 'bg-yellow-100 text-yellow-800' },
|
return (
|
||||||
DELIVERED: { label: 'Récupéré', color: 'bg-green-100 text-green-800' },
|
<div className="flex border-b border-gray-200 mb-6">
|
||||||
CANCELLED: { label: 'Annulé', color: 'bg-red-100 text-red-800' },
|
<Link
|
||||||
|
to="/mes-gains"
|
||||||
|
className={`flex-1 flex items-center justify-center gap-2 py-3 text-sm font-medium transition-colors ${
|
||||||
|
active === 'gains'
|
||||||
|
? 'text-[#006B6B] border-b-2 border-[#006B6B]'
|
||||||
|
: 'text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M20 12V22H4V12M22 7H2v5h20V7zM12 22V7M12 7H7.5a2.5 2.5 0 010-5C11 2 12 7 12 7zM12 7h4.5a2.5 2.5 0 000-5C13 2 12 7 12 7z" />
|
||||||
|
</svg>
|
||||||
|
Mes gains
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/profile"
|
||||||
|
className={`flex-1 flex items-center justify-center gap-2 py-3 text-sm font-medium transition-colors ${
|
||||||
|
active === 'profile'
|
||||||
|
? 'text-[#006B6B] border-b-2 border-[#006B6B]'
|
||||||
|
: 'text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||||
|
</svg>
|
||||||
|
Profile
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateStr) {
|
||||||
|
return new Date(dateStr).toLocaleDateString('fr-FR', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MesGains() {
|
export default function MesGains() {
|
||||||
return (
|
const [gains, setGains] = useState([])
|
||||||
<div className="min-h-screen bg-green-50 p-6">
|
const [loading, setLoading] = useState(true)
|
||||||
<div className="max-w-2xl mx-auto">
|
const [error, setError] = useState('')
|
||||||
<h1 className="text-3xl font-bold text-green-800 mb-2">Mes gains</h1>
|
|
||||||
<p className="text-gray-500 mb-6">Historique de vos participations</p>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
useEffect(() => {
|
||||||
{MOCK_GAINS.map((gain) => (
|
myGainsApi()
|
||||||
<div key={gain.id} className="bg-white rounded-xl shadow p-5 flex items-center justify-between">
|
.then((data) => setGains(data.gains))
|
||||||
<div>
|
.catch((err) => setError(err.message))
|
||||||
<p className="font-semibold text-gray-800">{gain.lot}</p>
|
.finally(() => setLoading(false))
|
||||||
<p className="text-sm text-gray-400 font-mono mt-1">{gain.code}</p>
|
}, [])
|
||||||
</div>
|
|
||||||
<span className={`text-xs font-semibold px-3 py-1 rounded-full ${statusLabel[gain.status].color}`}>
|
return (
|
||||||
{statusLabel[gain.status].label}
|
<div className="min-h-screen bg-white max-w-sm mx-auto px-6 pt-6">
|
||||||
</span>
|
<TabNav active="gains" />
|
||||||
</div>
|
|
||||||
))}
|
<Link
|
||||||
|
to="/saisir-code-unique"
|
||||||
|
className="flex items-center justify-center gap-2 w-full bg-[#006B6B] hover:bg-[#005959] text-white font-medium py-3 rounded-lg transition-colors mb-6 text-sm"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
Saisir un nouveau code
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<p className="text-center text-gray-400 text-sm mt-8">Chargement...</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="px-4 py-3 rounded-lg bg-red-50 border border-red-200 text-red-600 text-sm">
|
||||||
|
{error}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && !error && gains.length === 0 && (
|
||||||
|
<p className="text-center text-gray-400 text-sm mt-8">Aucun gain pour le moment.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Timeline */}
|
||||||
|
<div className="relative">
|
||||||
|
{gains.map((gain, index) => (
|
||||||
|
<div key={gain.participationId} className="flex gap-4">
|
||||||
|
{/* Ligne verticale + cercle */}
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="w-3 h-3 mt-1 rounded-full border-2 border-gray-400 flex-shrink-0" />
|
||||||
|
{index < gains.length - 1 && (
|
||||||
|
<div className="w-px flex-1 bg-gray-200 my-1" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contenu */}
|
||||||
|
<div className={`flex-1 pb-6 ${index === 0 ? 'bg-gray-50 rounded-lg px-3 pt-1 -ml-1' : ''}`}>
|
||||||
|
<p className="font-semibold text-gray-800 text-sm">{gain.lot}</p>
|
||||||
|
<p className="text-gray-500 text-xs mt-1 leading-relaxed">{gain.description}</p>
|
||||||
|
<p className="text-[#006B6B] text-xs mt-2">Gain obtenu le {formatDate(gain.wonAt)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
37
frontend/src/pages/Perdu.jsx
Normal file
37
frontend/src/pages/Perdu.jsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
export default function Perdu() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
|
const message = location.state?.message
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center px-8 max-w-sm mx-auto text-center">
|
||||||
|
|
||||||
|
<div className="w-20 h-20 bg-gray-200 rounded-full flex items-center justify-center mb-6">
|
||||||
|
<svg className="w-10 h-10 text-gray-400" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="text-3xl font-bold italic font-serif text-[#006B6B] mb-2">
|
||||||
|
Réessayer la prochaine fois
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="text-gray-500 text-sm mb-2">
|
||||||
|
{message || 'Ce code est invalide ou a déjà été utilisé.'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-gray-400 text-xs mb-8">
|
||||||
|
Vérifiez le code présent sur votre ticket et réessayez.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/saisir-code-unique')}
|
||||||
|
className="w-full bg-[#006B6B] hover:bg-[#005959] text-white font-medium py-3 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Réessayer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,94 +1,89 @@
|
|||||||
import { useState } from 'react'
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
|
function TabNav({ active }) {
|
||||||
|
return (
|
||||||
|
<div className="flex border-b border-gray-200 mb-6">
|
||||||
|
<Link
|
||||||
|
to="/mes-gains"
|
||||||
|
className={`flex-1 flex items-center justify-center gap-2 py-3 text-sm font-medium transition-colors ${
|
||||||
|
active === 'gains'
|
||||||
|
? 'text-[#006B6B] border-b-2 border-[#006B6B]'
|
||||||
|
: 'text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M20 12V22H4V12M22 7H2v5h20V7zM12 22V7M12 7H7.5a2.5 2.5 0 010-5C11 2 12 7 12 7zM12 7h4.5a2.5 2.5 0 000-5C13 2 12 7 12 7z" />
|
||||||
|
</svg>
|
||||||
|
Mes gains
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/profile"
|
||||||
|
className={`flex-1 flex items-center justify-center gap-2 py-3 text-sm font-medium transition-colors ${
|
||||||
|
active === 'profile'
|
||||||
|
? 'text-[#006B6B] border-b-2 border-[#006B6B]'
|
||||||
|
: 'text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||||
|
</svg>
|
||||||
|
Profile
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function Profile() {
|
export default function Profile() {
|
||||||
const [form, setForm] = useState({
|
const user = JSON.parse(localStorage.getItem('user') || '{}')
|
||||||
firstName: '',
|
|
||||||
lastName: '',
|
|
||||||
email: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const initials = `${(user.firstname || '?')[0]}${(user.lastname || '?')[0]}`.toUpperCase()
|
||||||
const { name, value } = e.target
|
|
||||||
setForm({ ...form, [name]: value })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
// TODO: appeler backend pour update profil
|
// TODO: connecter au backend pour update profil
|
||||||
alert('Profil mis à jour')
|
alert('Modification du profil à venir')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initiales pour l'avatar
|
return (
|
||||||
const initials = 'MD'
|
<div className="min-h-screen bg-white max-w-sm mx-auto px-6 pt-6">
|
||||||
|
<TabNav active="profile" />
|
||||||
|
|
||||||
return (
|
{/* Avatar */}
|
||||||
<div className="min-h-screen bg-green-50 flex items-center justify-center p-4">
|
<div className="mb-6">
|
||||||
<div className="bg-white rounded-2xl shadow-lg w-full max-w-md p-8">
|
<div className="w-14 h-14 rounded-full bg-[#006B6B] flex items-center justify-center text-white text-lg font-bold">
|
||||||
<h1 className="text-2xl font-bold text-gray-800 mb-6">Mon Profil</h1>
|
{initials}
|
||||||
{/* Avatar */}
|
|
||||||
<div className="flex flex-col mb-6">
|
|
||||||
<div className="w-20 h-20 rounded-full bg-green-700 flex items-center justify-center text-white text-2xl font-bold uppercase">
|
|
||||||
{initials}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Formulaire */}
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-5">
|
|
||||||
|
|
||||||
{/* Email */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Adresse email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
name="email"
|
|
||||||
value={form.email}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="mathieu.dupont@example.com"
|
|
||||||
className="w-full border border-gray-300 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-green-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Nom */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Nom
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="lastName"
|
|
||||||
value={form.lastName}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="Dupont"
|
|
||||||
className="w-full border border-gray-300 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-green-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Prénom */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Prénom
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="firstName"
|
|
||||||
value={form.firstName}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="Mathieu"
|
|
||||||
className="w-full border border-gray-300 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-green-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bouton de soumission */}
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="w-full bg-green-700 hover:bg-green-800 text-white font-semibold py-2.5 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
Modifier mon profil
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
</div>
|
||||||
}
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-800 mb-1">Adresse email</label>
|
||||||
|
<div className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm text-gray-700 bg-gray-50">
|
||||||
|
{user.email || '—'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-800 mb-1">Nom</label>
|
||||||
|
<div className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm text-gray-700 bg-gray-50">
|
||||||
|
{user.lastname || '—'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-800 mb-1">Prénom</label>
|
||||||
|
<div className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm text-gray-700 bg-gray-50">
|
||||||
|
{user.firstname || '—'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-[#006B6B] hover:bg-[#005959] text-white font-medium py-3 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Modifier mon profil
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,65 +1,88 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { participateApi } from '../services/game'
|
||||||
|
|
||||||
export default function SaisirCodeUnique() {
|
export default function SaisirCodeUnique() {
|
||||||
const [form, setForm] = useState({
|
const navigate = useNavigate()
|
||||||
codeUnique: '',
|
const [code, setCode] = useState('')
|
||||||
})
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [gain, setGain] = useState(null)
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleSubmit = async (e) => {
|
||||||
const { name, value } = e.target
|
e.preventDefault()
|
||||||
setForm({ ...form, [name]: value.toUpperCase() })
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const data = await participateApi(code.trim().toUpperCase())
|
||||||
|
setGain(data.gain)
|
||||||
|
} catch (err) {
|
||||||
|
navigate('/perdu', { state: { message: err.message } })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
return (
|
||||||
e.preventDefault()
|
<div className="min-h-screen bg-gray-50 flex flex-col justify-center px-8 py-12 max-w-sm mx-auto">
|
||||||
|
|
||||||
const code = form.codeUnique.trim()
|
<div className="mb-8">
|
||||||
|
<h1 className="text-2xl font-bold italic font-serif text-[#006B6B]">Saisir mon code gagnant</h1>
|
||||||
if (!code) {
|
<p className="text-gray-500 mt-2 text-sm">Saisissez le code présent sur votre ticket</p>
|
||||||
alert('Veuillez saisir un code')
|
</div>
|
||||||
return
|
|
||||||
}
|
|
||||||
if (code.length !== 10) {
|
|
||||||
alert('Le code doit contenir exactement 10 caractères')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: appeler backend pour valider le code unique
|
|
||||||
alert('Code validé')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div className="min-h-screen bg-green-50 flex items-center justify-center p-4">
|
<input
|
||||||
<div className="bg-white rounded-2xl shadow-lg w-full max-w-md p-8">
|
type="text"
|
||||||
<h1 className="text-3xl font-bold text-green-800 mb-2">Saisir mon code gagnant</h1>
|
value={code}
|
||||||
<p className="text-gray-600 mb-6">Saisissez le code présent sur votre ticket</p>
|
onChange={(e) => setCode(e.target.value.toUpperCase())}
|
||||||
|
placeholder="Votre code"
|
||||||
|
maxLength={10}
|
||||||
|
required
|
||||||
|
className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm text-gray-700 placeholder-gray-300 bg-white focus:outline-none focus:ring-1 focus:ring-[#006B6B] focus:border-[#006B6B]"
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Formulaire */}
|
<button
|
||||||
<form onSubmit={handleSubmit} className="space-y-5">
|
type="submit"
|
||||||
|
disabled={loading || code.length !== 10}
|
||||||
|
className="w-full bg-[#00786F] hover:bg-[#1A6B65] disabled:opacity-60 text-white font-medium py-3 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
{loading ? 'Validation...' : 'Valider'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
{/* Code unique */}
|
{/* Overlay Félicitation */}
|
||||||
<div>
|
{gain && (
|
||||||
<input
|
<div className="fixed inset-0 bg-black/60 flex flex-col items-center justify-between z-50 px-6 py-12">
|
||||||
type="text"
|
<div className="w-full max-w-sm">
|
||||||
name="codeUnique"
|
<h2 className="text-4xl font-bold italic font-serif text-white mb-1">Félicitation !</h2>
|
||||||
value={form.codeUnique}
|
<p className="text-white/80 text-sm">Vous avez obtenu une récompense</p>
|
||||||
onChange={handleChange}
|
</div>
|
||||||
placeholder="Votre code"
|
|
||||||
maxLength={10}
|
<div className="flex-1 flex items-center justify-center">
|
||||||
className="w-full border border-gray-300 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-green-500"
|
{/* Code saisi */}
|
||||||
/>
|
<div className="bg-white/10 rounded-lg px-6 py-3 mb-6 text-white font-mono tracking-widest text-lg">
|
||||||
</div>
|
{code}
|
||||||
|
|
||||||
{/* Bouton de soumission */}
|
|
||||||
<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
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Icône décorative */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<svg className="w-24 h-24 text-yellow-200" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M12 2l2.4 7.4H22l-6.2 4.5 2.4 7.4L12 17l-6.2 4.3 2.4-7.4L2 9.4h7.6z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full max-w-sm">
|
||||||
|
<p className="text-white text-center font-semibold mb-1">{gain.name}</p>
|
||||||
|
<p className="text-white/70 text-center text-sm mb-6">{gain.description}</p>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/mes-gains')}
|
||||||
|
className="w-full bg-[#006B6B] hover:bg-[#005959] text-white font-medium py-3 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Découvrir mon lot
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)}
|
||||||
}
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
24
frontend/src/services/game.js
Normal file
24
frontend/src/services/game.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export async function myGainsApi() {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const res = await fetch('/api/game/my-gains', {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (!res.ok) throw new Error(data.message || 'Erreur lors de la récupération des gains.')
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function participateApi(code) {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const res = await fetch('/api/game/participate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ code }),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (!res.ok) throw new Error(data.message || 'Erreur lors de la participation.')
|
||||||
|
return data
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user