mise a jour

This commit is contained in:
Emma781227
2026-07-01 22:06:22 +02:00
parent efd10d6754
commit 68a9650223
22 changed files with 719 additions and 167 deletions

View File

@@ -1,32 +1,76 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useCallback } from 'react'
import { myGainsApi } from '../services/game'
import UserNav from '../components/UserNav'
import { useHandleAuthError } from '../hooks/useHandleAuthError'
function formatDate(dateStr) {
return new Date(dateStr).toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
const date = new Date(dateStr)
const dateLabel = date.toLocaleDateString('fr-FR', { day: '2-digit', month: 'long', year: 'numeric' })
const timeLabel = date.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })
return `${dateLabel} à ${timeLabel}`
}
function timeAgo(dateStr) {
const diff = Date.now() - new Date(dateStr).getTime()
const minutes = Math.floor(diff / 60000)
const hours = Math.floor(diff / 3600000)
const days = Math.floor(diff / 86400000)
if (minutes < 1) return "À l'instant"
if (minutes < 60) return `Il y a ${minutes} min`
if (hours < 24) return `Il y a ${hours}h`
if (days < 7) return `Il y a ${days}j`
return null
}
const CLAIM_STATUS = {
PENDING: { label: 'En attente de remise', classes: 'bg-yellow-100 text-yellow-700' },
DELIVERED: { label: 'Lot remis', classes: 'bg-green-100 text-green-700' },
CANCELLED: { label: 'Expiré', classes: 'bg-gray-100 text-gray-500' },
}
export default function MesGains() {
const [gains, setGains] = useState([])
const handleAuthError = useHandleAuthError()
const [gains, setGains] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [error, setError] = useState('')
const [refreshing, setRefreshing] = useState(false)
useEffect(() => {
myGainsApi()
.then((data) => setGains(data.gains))
.catch((err) => setError(err.message))
.finally(() => setLoading(false))
}, [])
const fetchGains = useCallback(async (isRefresh = false) => {
if (isRefresh) setRefreshing(true)
else setLoading(true)
setError('')
try {
const data = await myGainsApi()
setGains(data.gains)
} catch (err) {
if (!handleAuthError(err)) setError(err.message)
} finally {
setLoading(false)
setRefreshing(false)
}
}, [handleAuthError])
useEffect(() => { fetchGains() }, [fetchGains])
return (
<div className="min-h-screen bg-white max-w-sm mx-auto px-6 pt-6">
<UserNav active="gains" />
{loading && <p className="text-center text-gray-400 text-sm mt-8">Chargement...</p>}
<div className="flex items-center justify-between mb-4">
<h2 className="text-sm font-semibold text-gray-700">Historique de mes gains</h2>
<button
onClick={() => fetchGains(true)}
disabled={refreshing}
className="text-xs text-[#006B6B] hover:underline disabled:opacity-50 flex items-center gap-1"
>
<svg className={`w-3.5 h-3.5 ${refreshing ? 'animate-spin' : ''}`} fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582M20 20v-5h-.581M4.582 9A8 8 0 0120 15M19.418 15A8 8 0 014 9" />
</svg>
{refreshing ? 'Actualisation…' : 'Actualiser'}
</button>
</div>
{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">
@@ -35,23 +79,50 @@ export default function MesGains() {
)}
{!loading && !error && gains.length === 0 && (
<p className="text-center text-gray-400 text-sm mt-8">Aucun gain pour le moment.</p>
<div className="text-center mt-12">
<p className="text-gray-400 text-sm">Aucun gain pour le moment.</p>
<p className="text-gray-400 text-xs mt-1">Saisissez un code pour participer !</p>
</div>
)}
<div className="relative">
{gains.map((gain, index) => (
<div key={gain.participationId} className="flex gap-4">
<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" />}
{gains.map((gain, index) => {
const statusCfg = CLAIM_STATUS[gain.claimStatus] ?? CLAIM_STATUS.PENDING
return (
<div key={gain.participationId} className="flex gap-4">
<div className="flex flex-col items-center">
<div className={`w-3 h-3 mt-1.5 rounded-full flex-shrink-0 border-2 ${
gain.claimStatus === 'DELIVERED' ? 'border-[#006B6B] bg-[#006B6B]' : 'border-gray-300'
}`} />
{index < gains.length - 1 && <div className="w-px flex-1 bg-gray-200 my-1" />}
</div>
<div className={`flex-1 pb-6 ${index === 0 ? 'bg-gray-50 rounded-lg px-3 pt-2 -ml-1' : 'pt-1'}`}>
<div className="flex items-start justify-between gap-2 flex-wrap">
<p className="font-semibold text-gray-800 text-sm">{gain.lot}</p>
<span className={`text-[10px] font-semibold px-2 py-0.5 rounded-full whitespace-nowrap ${statusCfg.classes}`}>
{statusCfg.label}
</span>
</div>
<p className="text-gray-500 text-xs mt-1 leading-relaxed">{gain.description}</p>
{gain.value && (
<p className="text-gray-400 text-xs mt-0.5">Valeur : {gain.value} €</p>
)}
<div className="flex items-center gap-2 mt-2 flex-wrap">
<p className="text-[#006B6B] text-xs">Gagné le {formatDate(gain.wonAt)}</p>
{timeAgo(gain.wonAt) && (
<span className="text-[10px] font-medium text-white bg-[#006B6B]/70 px-1.5 py-0.5 rounded-full">
{timeAgo(gain.wonAt)}
</span>
)}
</div>
<p className="text-gray-400 text-[10px] mt-1">
Code utilisé :{' '}
<span className="font-mono tracking-widest text-gray-600">{gain.ticketCode}</span>
</p>
</div>
</div>
<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>
)