mise a jour
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user