130 lines
5.4 KiB
JavaScript
130 lines
5.4 KiB
JavaScript
import { useState, useEffect, useCallback } from 'react'
|
|
import { myGainsApi } from '../services/game'
|
|
import UserNav from '../components/UserNav'
|
|
import { useHandleAuthError } from '../hooks/useHandleAuthError'
|
|
|
|
function formatDate(dateStr) {
|
|
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 handleAuthError = useHandleAuthError()
|
|
const [gains, setGains] = useState([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState('')
|
|
const [refreshing, setRefreshing] = useState(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-gray-50 flex flex-col px-8 py-12 max-w-lg mx-auto">
|
|
<UserNav active="gains" />
|
|
|
|
<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">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{!loading && !error && gains.length === 0 && (
|
|
<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) => {
|
|
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>
|
|
</div>
|
|
)
|
|
}
|