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:
@@ -1,34 +1,108 @@
|
||||
const MOCK_GAINS = [
|
||||
{ id: 1, code: 'TTT-00123', lot: 'Infusion découverte (30g)', status: 'PENDING' },
|
||||
{ id: 2, code: 'TTT-00456', lot: 'Thé signature (100g)', status: 'DELIVERED' },
|
||||
]
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { myGainsApi } from '../services/game'
|
||||
|
||||
const statusLabel = {
|
||||
PENDING: { label: 'À récupérer', color: 'bg-yellow-100 text-yellow-800' },
|
||||
DELIVERED: { label: 'Récupéré', color: 'bg-green-100 text-green-800' },
|
||||
CANCELLED: { label: 'Annulé', color: 'bg-red-100 text-red-800' },
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
return new Date(dateStr).toLocaleDateString('fr-FR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
export default function MesGains() {
|
||||
return (
|
||||
<div className="min-h-screen bg-green-50 p-6">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<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>
|
||||
const [gains, setGains] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
<div className="space-y-4">
|
||||
{MOCK_GAINS.map((gain) => (
|
||||
<div key={gain.id} className="bg-white rounded-xl shadow p-5 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-semibold text-gray-800">{gain.lot}</p>
|
||||
<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}`}>
|
||||
{statusLabel[gain.status].label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
useEffect(() => {
|
||||
myGainsApi()
|
||||
.then((data) => setGains(data.gains))
|
||||
.catch((err) => setError(err.message))
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white max-w-sm mx-auto px-6 pt-6">
|
||||
<TabNav active="gains" />
|
||||
|
||||
<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>
|
||||
)}
|
||||
|
||||
{!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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user