feat: modern UI redesign with SVG icons and improved styling
- Update client dashboard with modern cards, SVG statistics icons, and prize icons - Add roulette animation with colored prize icons during ticket draw - Redesign history page with 4 statistics cards and SVG icons - Add "Rejetés" filter button in history page - Update profile page with modern card styling - Redesign header with clickable user name/email button - Add Facebook login button with green border styling - Update game page with roulette animation and prize display - Add prize values to constants (15€, 25€, 39€, 69€) - Replace all emoji icons with professional SVG icons - Apply consistent color scheme: green (#1a4d2e, #2d5a3d) and orange (#f59e0b) - Improve button styles and hover effects across all pages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f822077f51
commit
70f61fca88
|
|
@ -78,178 +78,214 @@ export default function ClientPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="py-8">
|
||||
{/* Welcome Section */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">
|
||||
Bonjour {user?.firstName} ! 👋
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
Bienvenue dans votre espace client
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
<div className="container mx-auto px-4">
|
||||
{/* Welcome Section */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">
|
||||
Bonjour {user?.firstName} ! 👋
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
Bienvenue dans votre espace client
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Action */}
|
||||
<div className="mb-8">
|
||||
<Card className="bg-gradient-to-r from-primary-500 to-primary-600 text-white">
|
||||
<CardContent className="py-8">
|
||||
{/* Quick Action */}
|
||||
<div className="mb-8">
|
||||
<div className="bg-gradient-to-r from-[#1a4d2e] to-[#2d5a3d] text-white rounded-xl shadow-md p-8">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-2">
|
||||
Vous avez un nouveau ticket ?
|
||||
</h2>
|
||||
<p className="text-primary-50">
|
||||
<p className="text-green-50">
|
||||
Entrez votre code et découvrez votre gain instantanément
|
||||
</p>
|
||||
</div>
|
||||
<Link href={ROUTES.GAME}>
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-white text-black hover:bg-gray-50 border-2 border-black"
|
||||
>
|
||||
<button className="bg-[#f59e0b] hover:bg-[#d97706] text-white font-bold px-8 py-4 rounded-lg transition-all hover:shadow-xl whitespace-nowrap">
|
||||
Jouer maintenant 🎮
|
||||
</Button>
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Statistics Cards */}
|
||||
<div className="grid md:grid-cols-3 gap-6 mb-8">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
{/* Statistics Cards */}
|
||||
<div className="grid md:grid-cols-3 gap-6 mb-8">
|
||||
<div className="bg-white rounded-xl shadow-md p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">
|
||||
<p className="text-sm font-medium text-gray-600 mb-2">
|
||||
Total Participations
|
||||
</p>
|
||||
<p className="text-3xl font-bold text-gray-900 mt-2">
|
||||
<p className="text-4xl font-bold text-gray-900">
|
||||
{stats.total}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-4xl">🎫</div>
|
||||
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="bg-white rounded-xl shadow-md p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">
|
||||
<p className="text-sm font-medium text-gray-600 mb-2">
|
||||
Gains réclamés
|
||||
</p>
|
||||
<p className="text-3xl font-bold text-green-600 mt-2">
|
||||
<p className="text-4xl font-bold text-green-600">
|
||||
{stats.claimed}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-4xl">✅</div>
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="bg-white rounded-xl shadow-md p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">
|
||||
<p className="text-sm font-medium text-gray-600 mb-2">
|
||||
En attente
|
||||
</p>
|
||||
<p className="text-3xl font-bold text-yellow-600 mt-2">
|
||||
<p className="text-4xl font-bold text-yellow-600">
|
||||
{stats.pending}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-4xl">⏳</div>
|
||||
<div className="w-16 h-16 bg-yellow-100 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Recent Tickets */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>Mes derniers tickets</CardTitle>
|
||||
<Link href={ROUTES.HISTORY}>
|
||||
<Button variant="outline" size="sm">
|
||||
Voir tout l'historique
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{tickets.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-6xl mb-4">🎲</div>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Vous n'avez pas encore participé au jeu
|
||||
</p>
|
||||
<Link href={ROUTES.GAME}>
|
||||
<Button className="bg-white text-black hover:bg-gray-50 border-2 border-black">Jouer maintenant</Button>
|
||||
</div>
|
||||
|
||||
{/* Recent Tickets */}
|
||||
<div className="bg-white rounded-xl shadow-md overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold text-gray-900">Mes derniers tickets</h2>
|
||||
<Link href={ROUTES.HISTORY}>
|
||||
<button className="text-[#1a4d2e] hover:text-[#f59e0b] font-semibold text-sm transition-colors">
|
||||
Voir tout l'historique
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Code Ticket
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Gain
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Statut
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Date
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{tickets.slice(0, 5).map((ticket) => {
|
||||
const prizeConfig = ticket.prize
|
||||
? PRIZE_CONFIG[ticket.prize.type as keyof typeof PRIZE_CONFIG]
|
||||
: null;
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{tickets.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-6xl mb-4">🎲</div>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Vous n'avez pas encore participé au jeu
|
||||
</p>
|
||||
<Link href={ROUTES.GAME}>
|
||||
<button className="bg-[#f59e0b] hover:bg-[#d97706] text-white font-bold px-6 py-3 rounded-lg transition-all">
|
||||
Jouer maintenant
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200">
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
||||
Code Ticket
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
||||
Gain
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
||||
Statut
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
||||
Date
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{tickets.slice(0, 5).map((ticket) => {
|
||||
const prizeConfig = ticket.prize
|
||||
? PRIZE_CONFIG[ticket.prize.type as keyof typeof PRIZE_CONFIG]
|
||||
: null;
|
||||
|
||||
return (
|
||||
<tr key={ticket.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className="font-mono text-sm font-medium text-gray-900">
|
||||
{ticket.code}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
{prizeConfig && (
|
||||
<>
|
||||
<span className="text-2xl mr-2">
|
||||
{prizeConfig.icon}
|
||||
</span>
|
||||
<span className="text-sm text-gray-900">
|
||||
{prizeConfig.name}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getStatusBadge(ticket.status)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{ticket.playedAt ? new Date(ticket.playedAt).toLocaleDateString("fr-FR") : "-"}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
return (
|
||||
<tr key={ticket.id} className="hover:bg-gray-50 transition-colors">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className="font-mono text-sm font-semibold text-gray-900">
|
||||
{ticket.code}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{prizeConfig && (
|
||||
<>
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${prizeConfig.color}`}>
|
||||
{ticket.prize?.type === 'INFUSEUR' && (
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20 3H4v10c0 2.21 1.79 4 4 4h6c2.21 0 4-1.79 4-4v-3h2c1.11 0 2-.9 2-2V5c0-1.11-.89-2-2-2zm0 5h-2V5h2v3zM4 19h16v2H4z"/>
|
||||
</svg>
|
||||
)}
|
||||
{ticket.prize?.type === 'THE_SIGNATURE' && (
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20 3H4v10c0 2.21 1.79 4 4 4h6c2.21 0 4-1.79 4-4v-3h2c1.11 0 2-.9 2-2V5c0-1.11-.89-2-2-2zm0 5h-2V5h2v3z"/>
|
||||
</svg>
|
||||
)}
|
||||
{ticket.prize?.type === 'COFFRET_DECOUVERTE' && (
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20 6h-2.18c.11-.31.18-.65.18-1 0-1.66-1.34-3-3-3-1.05 0-1.96.54-2.5 1.35l-.5.67-.5-.68C10.96 2.54 10.05 2 9 2 7.34 2 6 3.34 6 5c0 .35.07.69.18 1H4c-1.11 0-1.99.89-1.99 2L2 19c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-5-2c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zM9 4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm11 15H4v-2h16v2zm0-5H4V8h5.08L7 10.83 8.62 12 11 8.76l1-1.36 1 1.36L15.38 12 17 10.83 14.92 8H20v6z"/>
|
||||
</svg>
|
||||
)}
|
||||
{ticket.prize?.type === 'COFFRET_PRESTIGE' && (
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2L4 5v6.09c0 5.05 3.41 9.76 8 10.91 4.59-1.15 8-5.86 8-10.91V5l-8-3zm6 9.09c0 4-2.55 7.7-6 8.83-3.45-1.13-6-4.82-6-8.83v-4.7l6-2.25 6 2.25v4.7zM8 10.5l1.5 1.5L15 6.5 13.5 5z"/>
|
||||
</svg>
|
||||
)}
|
||||
{ticket.prize?.type === 'THE_GRATUIT' && (
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20 3H4v10c0 2.21 1.79 4 4 4h6c2.21 0 4-1.79 4-4v-3h2c1.11 0 2-.9 2-2V5c0-1.11-.89-2-2-2zm0 5h-2V5h2v3zM4 19h16v2H4z"/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{prizeConfig.name}
|
||||
</p>
|
||||
{ticket.prize?.value && ticket.prize.value > 0 && (
|
||||
<p className="text-xs text-gray-500">
|
||||
{ticket.prize.value}€
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getStatusBadge(ticket.status)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
|
||||
{ticket.playedAt ? new Date(ticket.playedAt).toLocaleDateString("fr-FR") : "-"}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,68 +103,77 @@ export default function HistoriquePage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-2 flex items-center gap-3">
|
||||
<Calendar className="w-10 h-10 text-primary-600" />
|
||||
Historique de mes participations
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
Consultez l'historique complet de vos participations et gains
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-2 flex items-center gap-3">
|
||||
<Calendar className="w-10 h-10 text-[#1a4d2e]" />
|
||||
Historique de mes participations
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
Consultez l'historique complet de vos participations et gains
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-4 gap-6 mb-8">
|
||||
<Card className="bg-gradient-to-br from-blue-50 to-blue-100">
|
||||
<CardContent className="pt-6">
|
||||
<div className="grid md:grid-cols-4 gap-6 mb-8">
|
||||
<div className="bg-white rounded-xl shadow-md p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Total</p>
|
||||
<p className="text-3xl font-bold text-blue-600 mt-2">{stats.total}</p>
|
||||
<p className="text-sm font-medium text-gray-600 mb-2">Total</p>
|
||||
<p className="text-4xl font-bold text-blue-600">{stats.total}</p>
|
||||
</div>
|
||||
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-4xl">📊</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-green-50 to-green-100">
|
||||
<CardContent className="pt-6">
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-md p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Réclamés</p>
|
||||
<p className="text-3xl font-bold text-green-600 mt-2">{stats.claimed}</p>
|
||||
<p className="text-sm font-medium text-gray-600 mb-2">Réclamés</p>
|
||||
<p className="text-4xl font-bold text-green-600">{stats.claimed}</p>
|
||||
</div>
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-4xl">✅</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="bg-gradient-to-br from-yellow-50 to-yellow-100">
|
||||
<CardContent className="pt-6">
|
||||
<div className="bg-white rounded-xl shadow-md p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">En attente</p>
|
||||
<p className="text-3xl font-bold text-yellow-600 mt-2">{stats.pending}</p>
|
||||
<p className="text-sm font-medium text-gray-600 mb-2">En attente</p>
|
||||
<p className="text-4xl font-bold text-yellow-600">{stats.pending}</p>
|
||||
</div>
|
||||
<div className="w-16 h-16 bg-yellow-100 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-4xl">⏳</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="bg-gradient-to-br from-red-50 to-red-100">
|
||||
<CardContent className="pt-6">
|
||||
<div className="bg-white rounded-xl shadow-md p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Rejetés</p>
|
||||
<p className="text-3xl font-bold text-red-600 mt-2">{stats.rejected}</p>
|
||||
<p className="text-sm font-medium text-gray-600 mb-2">Rejetés</p>
|
||||
<p className="text-4xl font-bold text-red-600">{stats.rejected}</p>
|
||||
</div>
|
||||
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-4xl">❌</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="mb-6">
|
||||
<CardContent className="pt-6">
|
||||
<div className="bg-white rounded-xl shadow-md p-6 mb-6">
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
|
|
@ -174,7 +183,7 @@ export default function HistoriquePage() {
|
|||
placeholder="Rechercher par code ticket..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#1a4d2e] focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -182,9 +191,9 @@ export default function HistoriquePage() {
|
|||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setFilter('ALL')}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
className={`px-4 py-2 rounded-lg font-semibold transition-all ${
|
||||
filter === 'ALL'
|
||||
? 'bg-primary-600 text-white'
|
||||
? 'bg-[#1a4d2e] text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
|
|
@ -192,7 +201,7 @@ export default function HistoriquePage() {
|
|||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('CLAIMED')}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
className={`px-4 py-2 rounded-lg font-semibold transition-all ${
|
||||
filter === 'CLAIMED'
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
|
|
@ -202,7 +211,7 @@ export default function HistoriquePage() {
|
|||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('PENDING')}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
className={`px-4 py-2 rounded-lg font-semibold transition-all ${
|
||||
filter === 'PENDING'
|
||||
? 'bg-yellow-600 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
|
|
@ -210,118 +219,152 @@ export default function HistoriquePage() {
|
|||
>
|
||||
En attente ({stats.pending})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('REJECTED')}
|
||||
className={`px-4 py-2 rounded-lg font-semibold transition-all ${
|
||||
filter === 'REJECTED'
|
||||
? 'bg-red-600 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
Rejetés ({stats.rejected})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Tous mes tickets ({filteredTickets.length})</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{filteredTickets.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-6xl mb-4">🎲</div>
|
||||
<p className="text-gray-600 mb-4">
|
||||
{searchQuery || filter !== 'ALL'
|
||||
? 'Aucun ticket trouvé avec ces filtres'
|
||||
: 'Vous n\'avez pas encore participé au jeu'}
|
||||
</p>
|
||||
{!searchQuery && filter === 'ALL' && (
|
||||
<Button onClick={() => router.push(ROUTES.GAME)} className="bg-white text-black hover:bg-gray-50 border-2 border-black">
|
||||
Jouer maintenant
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Code Ticket
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Gain
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Statut
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Date de participation
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Date de réclamation
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{filteredTickets.map((ticket) => {
|
||||
const prizeConfig = ticket.prize
|
||||
? PRIZE_CONFIG[ticket.prize.type as keyof typeof PRIZE_CONFIG]
|
||||
: null;
|
||||
<div className="bg-white rounded-xl shadow-md overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h2 className="text-xl font-bold text-gray-900">Tous mes tickets ({filteredTickets.length})</h2>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{filteredTickets.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-6xl mb-4">🎲</div>
|
||||
<p className="text-gray-600 mb-4">
|
||||
{searchQuery || filter !== 'ALL'
|
||||
? 'Aucun ticket trouvé avec ces filtres'
|
||||
: 'Vous n\'avez pas encore participé au jeu'}
|
||||
</p>
|
||||
{!searchQuery && filter === 'ALL' && (
|
||||
<button
|
||||
onClick={() => router.push(ROUTES.GAME)}
|
||||
className="bg-[#f59e0b] hover:bg-[#d97706] text-white font-bold px-6 py-3 rounded-lg transition-all"
|
||||
>
|
||||
Jouer maintenant
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200">
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
||||
Code Ticket
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
||||
Gain
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
||||
Statut
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
||||
Date de participation
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
||||
Date de réclamation
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{filteredTickets.map((ticket) => {
|
||||
const prizeConfig = ticket.prize
|
||||
? PRIZE_CONFIG[ticket.prize.type as keyof typeof PRIZE_CONFIG]
|
||||
: null;
|
||||
|
||||
return (
|
||||
<tr key={ticket.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className="font-mono text-sm font-medium text-gray-900">
|
||||
{ticket.code}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
{prizeConfig && (
|
||||
<>
|
||||
<span className="text-2xl mr-2">
|
||||
{prizeConfig.icon}
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{prizeConfig.name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{ticket.prize?.value}€
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getStatusBadge(ticket.status)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{ticket.playedAt
|
||||
? new Date(ticket.playedAt).toLocaleDateString("fr-FR", {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
: "-"}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{ticket.claimedAt
|
||||
? new Date(ticket.claimedAt).toLocaleDateString("fr-FR", {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
: "-"}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
return (
|
||||
<tr key={ticket.id} className="hover:bg-gray-50 transition-colors">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className="font-mono text-sm font-semibold text-gray-900">
|
||||
{ticket.code}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{prizeConfig && (
|
||||
<>
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${prizeConfig.color}`}>
|
||||
{ticket.prize?.type === 'INFUSEUR' && (
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20 3H4v10c0 2.21 1.79 4 4 4h6c2.21 0 4-1.79 4-4v-3h2c1.11 0 2-.9 2-2V5c0-1.11-.89-2-2-2zm0 5h-2V5h2v3zM4 19h16v2H4z"/>
|
||||
</svg>
|
||||
)}
|
||||
{ticket.prize?.type === 'THE_SIGNATURE' && (
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20 3H4v10c0 2.21 1.79 4 4 4h6c2.21 0 4-1.79 4-4v-3h2c1.11 0 2-.9 2-2V5c0-1.11-.89-2-2-2zm0 5h-2V5h2v3z"/>
|
||||
</svg>
|
||||
)}
|
||||
{ticket.prize?.type === 'COFFRET_DECOUVERTE' && (
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20 6h-2.18c.11-.31.18-.65.18-1 0-1.66-1.34-3-3-3-1.05 0-1.96.54-2.5 1.35l-.5.67-.5-.68C10.96 2.54 10.05 2 9 2 7.34 2 6 3.34 6 5c0 .35.07.69.18 1H4c-1.11 0-1.99.89-1.99 2L2 19c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-5-2c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zM9 4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm11 15H4v-2h16v2zm0-5H4V8h5.08L7 10.83 8.62 12 11 8.76l1-1.36 1 1.36L15.38 12 17 10.83 14.92 8H20v6z"/>
|
||||
</svg>
|
||||
)}
|
||||
{ticket.prize?.type === 'COFFRET_PRESTIGE' && (
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2L4 5v6.09c0 5.05 3.41 9.76 8 10.91 4.59-1.15 8-5.86 8-10.91V5l-8-3zm6 9.09c0 4-2.55 7.7-6 8.83-3.45-1.13-6-4.82-6-8.83v-4.7l6-2.25 6 2.25v4.7zM8 10.5l1.5 1.5L15 6.5 13.5 5z"/>
|
||||
</svg>
|
||||
)}
|
||||
{ticket.prize?.type === 'THE_GRATUIT' && (
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20 3H4v10c0 2.21 1.79 4 4 4h6c2.21 0 4-1.79 4-4v-3h2c1.11 0 2-.9 2-2V5c0-1.11-.89-2-2-2zm0 5h-2V5h2v3zM4 19h16v2H4z"/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{prizeConfig.name}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getStatusBadge(ticket.status)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
|
||||
{ticket.playedAt
|
||||
? new Date(ticket.playedAt).toLocaleDateString("fr-FR", {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
: "-"}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
|
||||
{ticket.claimedAt
|
||||
? new Date(ticket.claimedAt).toLocaleDateString("fr-FR", {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
: "-"}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"use client";
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
|
|
@ -15,13 +15,24 @@ import { useRouter } from "next/navigation";
|
|||
import { ROUTES } from "@/utils/constants";
|
||||
import Link from "next/link";
|
||||
|
||||
const PRIZES = [
|
||||
{ type: 'INFUSEUR', name: 'Infuseur à thé', color: 'bg-blue-100 text-blue-800' },
|
||||
{ type: 'THE_SIGNATURE', name: 'Thé signature 100g', color: 'bg-green-100 text-green-800' },
|
||||
{ type: 'COFFRET_DECOUVERTE', name: 'Coffret découverte 39€', color: 'bg-purple-100 text-purple-800' },
|
||||
{ type: 'COFFRET_PRESTIGE', name: 'Coffret prestige 69€', color: 'bg-amber-100 text-amber-800' },
|
||||
{ type: 'THE_GRATUIT', name: 'Thé gratuit en magasin', color: 'bg-pink-100 text-pink-800' },
|
||||
];
|
||||
|
||||
export default function JeuxPage() {
|
||||
const { user, isAuthenticated } = useAuth();
|
||||
const { play, isPlaying } = useGame();
|
||||
const router = useRouter();
|
||||
const [showResultModal, setShowResultModal] = useState(false);
|
||||
const [showRouletteModal, setShowRouletteModal] = useState(false);
|
||||
const [gameResult, setGameResult] = useState<PlayGameResponse | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||
const [currentPrizeIndex, setCurrentPrizeIndex] = useState(0);
|
||||
const [isSpinning, setIsSpinning] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
|
|
@ -32,6 +43,17 @@ export default function JeuxPage() {
|
|||
resolver: zodResolver(ticketCodeSchema),
|
||||
});
|
||||
|
||||
// Animation de la roulette
|
||||
useEffect(() => {
|
||||
if (isSpinning) {
|
||||
const interval = setInterval(() => {
|
||||
setCurrentPrizeIndex((prev) => (prev + 1) % PRIZES.length);
|
||||
}, 100);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [isSpinning]);
|
||||
|
||||
const onSubmit = async (data: TicketCodeFormData) => {
|
||||
// Réinitialiser le message d'erreur
|
||||
setErrorMessage("");
|
||||
|
|
@ -42,14 +64,37 @@ export default function JeuxPage() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Afficher la modal de roulette et démarrer l'animation
|
||||
setShowRouletteModal(true);
|
||||
setIsSpinning(true);
|
||||
setCurrentPrizeIndex(0);
|
||||
|
||||
const result = await play(data.ticketCode);
|
||||
|
||||
if (result) {
|
||||
setGameResult(result);
|
||||
setShowResultModal(true);
|
||||
setErrorMessage("");
|
||||
reset();
|
||||
// Trouver l'index du prix gagné
|
||||
const winningIndex = PRIZES.findIndex(p => p.type === result.prize?.type);
|
||||
|
||||
// Continuer à tourner pendant 3 secondes
|
||||
setTimeout(() => {
|
||||
setIsSpinning(false);
|
||||
if (winningIndex !== -1) {
|
||||
setCurrentPrizeIndex(winningIndex);
|
||||
}
|
||||
|
||||
// Afficher le résultat après 2 secondes (pour montrer le gain)
|
||||
setTimeout(() => {
|
||||
setShowRouletteModal(false);
|
||||
setGameResult(result);
|
||||
setShowResultModal(true);
|
||||
setErrorMessage("");
|
||||
reset();
|
||||
}, 2000);
|
||||
}, 3000);
|
||||
} else {
|
||||
// En cas d'erreur, afficher un message personnalisé
|
||||
// En cas d'erreur, fermer la roulette et afficher l'erreur
|
||||
setIsSpinning(false);
|
||||
setShowRouletteModal(false);
|
||||
setErrorMessage("Ce code a déjà été utilisé ou est invalide. Si vous avez déjà utilisé ce code, consultez vos tickets dans 'Mes lots'.");
|
||||
}
|
||||
};
|
||||
|
|
@ -64,110 +109,189 @@ export default function JeuxPage() {
|
|||
: null;
|
||||
|
||||
return (
|
||||
<div className="py-8">
|
||||
{/* Formulaire Section */}
|
||||
<section className="mb-16">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<Card className="shadow-xl">
|
||||
<CardHeader className="bg-gradient-to-r from-primary-50 to-green-50">
|
||||
<CardTitle className="text-center text-2xl md:text-3xl text-primary-800">
|
||||
🎁 Jouez maintenant !
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-8">
|
||||
<div className="mb-6 text-center">
|
||||
{isAuthenticated ? (
|
||||
<p className="text-gray-700">
|
||||
Bonjour <span className="font-bold text-primary-600">{user?.firstName}</span>,
|
||||
entrez le code de 10 caractères présent sur votre ticket de caisse
|
||||
</p>
|
||||
) : (
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4">
|
||||
<p className="text-yellow-800 text-sm">
|
||||
💡 Vous devez être connecté pour valider votre code.
|
||||
<Link href={ROUTES.LOGIN} className="font-semibold underline ml-1">
|
||||
Connectez-vous
|
||||
</Link>
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
<div className="container mx-auto px-4">
|
||||
{/* Formulaire Section */}
|
||||
<section className="mb-16">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="bg-white rounded-xl shadow-md overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-[#1a4d2e] to-[#2d5a3d] px-6 py-6">
|
||||
<h1 className="text-center text-3xl md:text-4xl font-bold text-white">
|
||||
🎁 Jouez maintenant !
|
||||
</h1>
|
||||
</div>
|
||||
<div className="p-8">
|
||||
<div className="mb-6 text-center">
|
||||
{isAuthenticated ? (
|
||||
<p className="text-gray-700 text-lg">
|
||||
Bonjour <span className="font-bold text-[#1a4d2e]">{user?.firstName}</span>,
|
||||
entrez le code de 10 caractères présent sur votre ticket de caisse
|
||||
</p>
|
||||
) : (
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4">
|
||||
<p className="text-yellow-800 text-sm">
|
||||
💡 Vous devez être connecté pour valider votre code.
|
||||
<Link href={ROUTES.LOGIN} className="font-semibold underline ml-1 hover:text-yellow-900">
|
||||
Connectez-vous
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="ticketCode" className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Code du ticket
|
||||
</label>
|
||||
<input
|
||||
id="ticketCode"
|
||||
type="text"
|
||||
placeholder="TTP2025ABC"
|
||||
{...register("ticketCode")}
|
||||
className="w-full px-6 py-4 text-center text-2xl font-mono font-bold uppercase border-2 border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#1a4d2e] focus:border-transparent tracking-widest"
|
||||
maxLength={10}
|
||||
/>
|
||||
{errors.ticketCode && (
|
||||
<p className="mt-2 text-sm text-red-600">{errors.ticketCode.message}</p>
|
||||
)}
|
||||
{errorMessage && (
|
||||
<div className="mt-3 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<p className="text-sm text-red-800 font-medium mb-2">
|
||||
❌ {errorMessage}
|
||||
</p>
|
||||
<Link
|
||||
href={ROUTES.HISTORY}
|
||||
className="text-sm text-red-600 hover:text-red-800 underline font-medium"
|
||||
>
|
||||
→ Voir vos tickets déjà utilisés
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<p className="mt-2 text-sm text-gray-500 text-center">
|
||||
Format: TTP2025ABC (10 caractères)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPlaying}
|
||||
className="bg-[#f59e0b] hover:bg-[#d97706] disabled:bg-gray-400 text-white font-bold px-12 py-4 text-lg rounded-lg transition-all shadow-lg hover:shadow-xl"
|
||||
>
|
||||
{isPlaying ? "Vérification en cours..." : "🎲 Tenter ma chance !"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{!isAuthenticated && (
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-sm text-gray-600 mb-3">
|
||||
Pas encore de compte ?
|
||||
</p>
|
||||
<Link href={ROUTES.REGISTER}>
|
||||
<button className="border-2 border-[#1a4d2e] text-[#1a4d2e] hover:bg-[#1a4d2e] hover:text-white font-bold px-6 py-2 rounded-lg transition-all">
|
||||
Créer un compte gratuitement
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAuthenticated && (
|
||||
<div className="mt-6 p-4 bg-blue-50 border-l-4 border-blue-500 rounded-lg">
|
||||
<p className="text-sm text-blue-800 font-semibold mb-2">
|
||||
💡 Bon à savoir :
|
||||
</p>
|
||||
<ul className="text-sm text-blue-700 space-y-1 list-disc list-inside">
|
||||
<li>Chaque code ne peut être utilisé qu'une seule fois</li>
|
||||
<li>Consultez vos tickets sur la page <Link href={ROUTES.HISTORY} className="underline font-medium hover:text-blue-900">Mes gains</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="ticketCode" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Code du ticket
|
||||
</label>
|
||||
<input
|
||||
id="ticketCode"
|
||||
type="text"
|
||||
placeholder="TTP2025ABC"
|
||||
{...register("ticketCode")}
|
||||
className="w-full px-6 py-4 text-center text-2xl font-mono font-bold uppercase border-2 border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent tracking-widest"
|
||||
maxLength={10}
|
||||
/>
|
||||
{errors.ticketCode && (
|
||||
<p className="mt-2 text-sm text-red-600">{errors.ticketCode.message}</p>
|
||||
)}
|
||||
{errorMessage && (
|
||||
<div className="mt-3 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<p className="text-sm text-red-800 font-medium mb-2">
|
||||
❌ {errorMessage}
|
||||
</p>
|
||||
<Link
|
||||
href={ROUTES.MY_LOTS}
|
||||
className="text-sm text-red-600 hover:text-red-800 underline font-medium"
|
||||
{/* Roulette Modal */}
|
||||
<Modal
|
||||
isOpen={showRouletteModal}
|
||||
onClose={() => {}}
|
||||
title="🎰 Tirage en cours..."
|
||||
size="md"
|
||||
>
|
||||
<div className="py-12">
|
||||
<div className="flex flex-col items-center gap-8">
|
||||
{/* Roulette Display */}
|
||||
<div className="relative w-full max-w-sm">
|
||||
<div className="flex flex-col gap-3">
|
||||
{PRIZES.map((prize, index) => {
|
||||
const getPrizeIcon = (type: string) => {
|
||||
switch(type) {
|
||||
case 'INFUSEUR':
|
||||
return (
|
||||
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20 3H4v10c0 2.21 1.79 4 4 4h6c2.21 0 4-1.79 4-4v-3h2c1.11 0 2-.9 2-2V5c0-1.11-.89-2-2-2zm0 5h-2V5h2v3zM4 19h16v2H4z"/>
|
||||
</svg>
|
||||
);
|
||||
case 'THE_SIGNATURE':
|
||||
return (
|
||||
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20 3H4v10c0 2.21 1.79 4 4 4h6c2.21 0 4-1.79 4-4v-3h2c1.11 0 2-.9 2-2V5c0-1.11-.89-2-2-2zm0 5h-2V5h2v3z"/>
|
||||
</svg>
|
||||
);
|
||||
case 'COFFRET_DECOUVERTE':
|
||||
return (
|
||||
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20 6h-2.18c.11-.31.18-.65.18-1 0-1.66-1.34-3-3-3-1.05 0-1.96.54-2.5 1.35l-.5.67-.5-.68C10.96 2.54 10.05 2 9 2 7.34 2 6 3.34 6 5c0 .35.07.69.18 1H4c-1.11 0-1.99.89-1.99 2L2 19c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-5-2c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zM9 4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm11 15H4v-2h16v2zm0-5H4V8h5.08L7 10.83 8.62 12 11 8.76l1-1.36 1 1.36L15.38 12 17 10.83 14.92 8H20v6z"/>
|
||||
</svg>
|
||||
);
|
||||
case 'COFFRET_PRESTIGE':
|
||||
return (
|
||||
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2L4 5v6.09c0 5.05 3.41 9.76 8 10.91 4.59-1.15 8-5.86 8-10.91V5l-8-3zm6 9.09c0 4-2.55 7.7-6 8.83-3.45-1.13-6-4.82-6-8.83v-4.7l6-2.25 6 2.25v4.7zM8 10.5l1.5 1.5L15 6.5 13.5 5z"/>
|
||||
</svg>
|
||||
);
|
||||
case 'THE_GRATUIT':
|
||||
return (
|
||||
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20 3H4v10c0 2.21 1.79 4 4 4h6c2.21 0 4-1.79 4-4v-3h2c1.11 0 2-.9 2-2V5c0-1.11-.89-2-2-2zm0 5h-2V5h2v3zM4 19h16v2H4z"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex items-center gap-4 p-4 rounded-xl transition-all duration-200 ${
|
||||
currentPrizeIndex === index
|
||||
? `${prize.color} scale-110 shadow-xl border-4 border-current`
|
||||
: 'bg-gray-100 opacity-50 scale-95'
|
||||
}`}
|
||||
>
|
||||
→ Voir vos tickets déjà utilisés
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<p className="mt-2 text-sm text-gray-500 text-center">
|
||||
Format: TTP2025ABC (10 caractères)
|
||||
</p>
|
||||
<div className={`w-12 h-12 rounded-full flex items-center justify-center ${prize.color}`}>
|
||||
{getPrizeIcon(prize.type)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className={`font-bold ${currentPrizeIndex === index ? 'text-lg' : 'text-sm'}`}>
|
||||
{prize.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={isPlaying}
|
||||
disabled={isPlaying}
|
||||
size="lg"
|
||||
className="px-12 py-4 text-lg"
|
||||
>
|
||||
{isPlaying ? "Vérification en cours..." : "🎲 Tenter ma chance !"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{!isAuthenticated && (
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-sm text-gray-600 mb-3">
|
||||
Pas encore de compte ?
|
||||
</p>
|
||||
<Link href={ROUTES.REGISTER}>
|
||||
<Button variant="outline" size="sm">
|
||||
Créer un compte gratuitement
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAuthenticated && (
|
||||
<div className="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<p className="text-sm text-blue-800 mb-2">
|
||||
💡 <strong>Bon à savoir :</strong>
|
||||
</p>
|
||||
<ul className="text-sm text-blue-700 space-y-1 list-disc list-inside">
|
||||
<li>Chaque code ne peut être utilisé qu'une seule fois</li>
|
||||
<li>Consultez vos tickets sur la page <Link href={ROUTES.MY_LOTS} className="underline font-medium">Mes lots</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
{/* Loading Animation */}
|
||||
<div className="flex items-center gap-2 text-gray-600">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-[#1a4d2e]"></div>
|
||||
<span className="font-medium">Tirage en cours...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Result Modal */}
|
||||
<Modal
|
||||
|
|
@ -178,8 +302,36 @@ export default function JeuxPage() {
|
|||
>
|
||||
{gameResult && prizeConfig && (
|
||||
<div className="text-center py-6">
|
||||
<div className="text-6xl mb-4">{prizeConfig.icon}</div>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className={`w-24 h-24 rounded-full flex items-center justify-center ${prizeConfig.color}`}>
|
||||
{gameResult.prize?.type === 'INFUSEUR' && (
|
||||
<svg className="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20 3H4v10c0 2.21 1.79 4 4 4h6c2.21 0 4-1.79 4-4v-3h2c1.11 0 2-.9 2-2V5c0-1.11-.89-2-2-2zm0 5h-2V5h2v3zM4 19h16v2H4z"/>
|
||||
</svg>
|
||||
)}
|
||||
{gameResult.prize?.type === 'THE_SIGNATURE' && (
|
||||
<svg className="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20 3H4v10c0 2.21 1.79 4 4 4h6c2.21 0 4-1.79 4-4v-3h2c1.11 0 2-.9 2-2V5c0-1.11-.89-2-2-2zm0 5h-2V5h2v3z"/>
|
||||
</svg>
|
||||
)}
|
||||
{gameResult.prize?.type === 'COFFRET_DECOUVERTE' && (
|
||||
<svg className="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20 6h-2.18c.11-.31.18-.65.18-1 0-1.66-1.34-3-3-3-1.05 0-1.96.54-2.5 1.35l-.5.67-.5-.68C10.96 2.54 10.05 2 9 2 7.34 2 6 3.34 6 5c0 .35.07.69.18 1H4c-1.11 0-1.99.89-1.99 2L2 19c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-5-2c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zM9 4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm11 15H4v-2h16v2zm0-5H4V8h5.08L7 10.83 8.62 12 11 8.76l1-1.36 1 1.36L15.38 12 17 10.83 14.92 8H20v6z"/>
|
||||
</svg>
|
||||
)}
|
||||
{gameResult.prize?.type === 'COFFRET_PRESTIGE' && (
|
||||
<svg className="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2L4 5v6.09c0 5.05 3.41 9.76 8 10.91 4.59-1.15 8-5.86 8-10.91V5l-8-3zm6 9.09c0 4-2.55 7.7-6 8.83-3.45-1.13-6-4.82-6-8.83v-4.7l6-2.25 6 2.25v4.7zM8 10.5l1.5 1.5L15 6.5 13.5 5z"/>
|
||||
</svg>
|
||||
)}
|
||||
{gameResult.prize?.type === 'THE_GRATUIT' && (
|
||||
<svg className="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20 3H4v10c0 2.21 1.79 4 4 4h6c2.21 0 4-1.79 4-4v-3h2c1.11 0 2-.9 2-2V5c0-1.11-.89-2-2-2zm0 5h-2V5h2v3zM4 19h16v2H4z"/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
Félicitations ! 🎉
|
||||
</h3>
|
||||
<p className="text-lg text-gray-700 mb-4">
|
||||
|
|
@ -194,16 +346,23 @@ export default function JeuxPage() {
|
|||
{gameResult.message || "Présentez-vous en magasin pour récupérer votre lot !"}
|
||||
</p>
|
||||
<div className="flex gap-3 justify-center">
|
||||
<Button onClick={closeModal} variant="outline">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="border-2 border-gray-300 hover:bg-gray-50 text-gray-700 font-bold px-6 py-3 rounded-lg transition-all"
|
||||
>
|
||||
Fermer
|
||||
</Button>
|
||||
<Button onClick={() => router.push(ROUTES.MY_LOTS)}>
|
||||
Voir mes lots
|
||||
</Button>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push(ROUTES.HISTORY)}
|
||||
className="bg-[#1a4d2e] hover:bg-[#2d5a3d] text-white font-bold px-6 py-3 rounded-lg transition-all"
|
||||
>
|
||||
Voir mes gains
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,7 @@ import { useAuth } from "@/contexts/AuthContext";
|
|||
import { loginSchema, LoginFormData } from "@/lib/validations";
|
||||
import { Input } from "@/components/ui/Input";
|
||||
import Button from "@/components/Button";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { ROUTES } from "@/utils/constants";
|
||||
import { GoogleLoginButton } from "@/components/GoogleLoginButton";
|
||||
import { initFacebookSDK, loginWithFacebook } from "@/lib/facebook-sdk";
|
||||
|
|
@ -21,12 +19,12 @@ export default function LoginPage() {
|
|||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isFacebookLoading, setIsFacebookLoading] = useState(false);
|
||||
const [isFacebookSDKLoaded, setIsFacebookSDKLoaded] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const hasGoogleAuth = !!process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID;
|
||||
const hasFacebookAuth = !!process.env.NEXT_PUBLIC_FACEBOOK_APP_ID;
|
||||
|
||||
useEffect(() => {
|
||||
// Initialiser le SDK Facebook au chargement de la page
|
||||
initFacebookSDK()
|
||||
.then(() => {
|
||||
setIsFacebookSDKLoaded(true);
|
||||
|
|
@ -77,112 +75,153 @@ export default function LoginPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-[calc(100vh-4rem)] flex items-center justify-center py-12 px-4">
|
||||
<Card className="w-full max-w-md p-8">
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4">
|
||||
<div className="w-full max-w-md">
|
||||
|
||||
{/* Title */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="flex justify-center mb-6">
|
||||
<Image
|
||||
src="/logos/logo.svg"
|
||||
alt="Thé Tip Top"
|
||||
width={120}
|
||||
height={120}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Connexion</h1>
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">Connexion</h1>
|
||||
<p className="text-gray-600">
|
||||
Connectez-vous pour participer au jeu-concours
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
label="Email"
|
||||
placeholder="votre.email@example.com"
|
||||
error={errors.email?.message}
|
||||
{...register("email")}
|
||||
required
|
||||
/>
|
||||
{/* Main Card */}
|
||||
<div className="bg-white rounded-xl shadow-md overflow-hidden">
|
||||
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
label="Mot de passe"
|
||||
placeholder="••••••••"
|
||||
error={errors.password?.message}
|
||||
{...register("password")}
|
||||
required
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-gray-200">
|
||||
<button className="flex-1 py-4 px-6 text-center font-semibold text-gray-900 bg-white border-b-2 border-[#1a4d2e]">
|
||||
Connexion
|
||||
</button>
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="text-blue-600 hover:text-blue-700 hover:underline"
|
||||
href={ROUTES.REGISTER}
|
||||
className="flex-1 py-4 px-6 text-center font-semibold text-gray-500 bg-gray-50 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
Mot de passe oublié ?
|
||||
Inscription
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
fullWidth
|
||||
size="lg"
|
||||
>
|
||||
Se connecter
|
||||
</Button>
|
||||
</form>
|
||||
{/* Form Container */}
|
||||
<div className="p-8">
|
||||
|
||||
{(hasGoogleAuth || hasFacebookAuth) && (
|
||||
<div className="mt-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
{/* Social Login Buttons */}
|
||||
{(hasGoogleAuth || hasFacebookAuth) && (
|
||||
<div className="space-y-3 mb-6">
|
||||
{hasGoogleAuth && (
|
||||
<GoogleLoginButton fullWidth />
|
||||
)}
|
||||
|
||||
{hasFacebookAuth && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleFacebookLogin}
|
||||
disabled={isFacebookLoading || !isFacebookSDKLoaded}
|
||||
className="w-full flex items-center justify-center gap-3 px-4 py-3 border-2 border-primary-600 rounded-lg hover:bg-primary-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="#1877F2" viewBox="0 0 24 24">
|
||||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
|
||||
</svg>
|
||||
<span className="font-medium text-primary-600">
|
||||
{isFacebookLoading ? "Connexion..." : "Continuer avec Facebook"}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 bg-white text-gray-500">
|
||||
Ou continuer avec
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Divider */}
|
||||
{(hasGoogleAuth || hasFacebookAuth) && (
|
||||
<div className="relative mb-6">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-4 bg-white text-gray-500">
|
||||
Ou avec votre email
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`mt-6 grid ${hasGoogleAuth && hasFacebookAuth ? 'grid-cols-2' : 'grid-cols-1'} gap-3`}>
|
||||
{hasGoogleAuth && (
|
||||
<GoogleLoginButton fullWidth />
|
||||
)}
|
||||
{/* Login Form */}
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
|
||||
|
||||
{hasFacebookAuth && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleFacebookLogin}
|
||||
disabled={isFacebookLoading || !isFacebookSDKLoaded}
|
||||
isLoading={isFacebookLoading}
|
||||
fullWidth
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Email <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="votre@email.com"
|
||||
{...register("email")}
|
||||
className={`w-full px-4 py-3 border ${errors.email ? 'border-red-500' : 'border-gray-300'} rounded-lg focus:outline-none focus:ring-2 focus:ring-[#1a4d2e] focus:border-transparent`}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="mt-1 text-sm text-red-500">{errors.email.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Mot de passe <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="••••••••"
|
||||
{...register("password")}
|
||||
className={`w-full px-4 py-3 border ${errors.password ? 'border-red-500' : 'border-gray-300'} rounded-lg focus:outline-none focus:ring-2 focus:ring-[#1a4d2e] focus:border-transparent pr-12`}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
{showPassword ? (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{errors.password && (
|
||||
<p className="mt-1 text-sm text-red-500">{errors.password.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full bg-[#1a4d2e] hover:bg-[#2d5a3d] disabled:bg-gray-400 text-white font-bold px-8 py-4 rounded-lg transition-all"
|
||||
>
|
||||
{isSubmitting ? "Connexion..." : "Se connecter"}
|
||||
</button>
|
||||
|
||||
{/* Forgot Password */}
|
||||
<div className="text-center">
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="text-sm text-[#1a4d2e] hover:text-[#f59e0b] hover:underline transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5 mr-2" fill="#1877F2" viewBox="0 0 24 24">
|
||||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
|
||||
</svg>
|
||||
Facebook
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
Mot de passe oublié ?
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p className="mt-8 text-center text-sm text-gray-600">
|
||||
Vous n'avez pas de compte ?{" "}
|
||||
<Link
|
||||
href={ROUTES.REGISTER}
|
||||
className="font-medium text-blue-600 hover:text-blue-700 hover:underline"
|
||||
>
|
||||
Créer un compte
|
||||
</Link>
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,124 +95,128 @@ export default function ProfilePage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="py-8 max-w-4xl mx-auto">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-8">Mon profil</h1>
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
<div className="container mx-auto px-4 max-w-4xl">
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-8">Mon profil</h1>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{/* Profile Info Card */}
|
||||
<div className="md:col-span-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Informations personnelles</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!isEditing ? (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-500">
|
||||
Prénom
|
||||
</label>
|
||||
<p className="text-lg text-gray-900">{user.firstName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-500">
|
||||
Nom
|
||||
</label>
|
||||
<p className="text-lg text-gray-900">{user.lastName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-500">
|
||||
Email
|
||||
</label>
|
||||
<p className="text-lg text-gray-900">{user.email}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-500">
|
||||
Téléphone
|
||||
</label>
|
||||
<p className="text-lg text-gray-900">
|
||||
{user.phone || "Non renseigné"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-500">
|
||||
Rôle
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<Badge variant={getRoleBadgeVariant(user.role)}>
|
||||
{getRoleLabel(user.role)}
|
||||
</Badge>
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{/* Profile Info Card */}
|
||||
<div className="md:col-span-2">
|
||||
<div className="bg-white rounded-xl shadow-md overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h2 className="text-xl font-bold text-gray-900">Informations personnelles</h2>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{!isEditing ? (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-500">
|
||||
Prénom
|
||||
</label>
|
||||
<p className="text-lg text-gray-900">{user.firstName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-500">
|
||||
Nom
|
||||
</label>
|
||||
<p className="text-lg text-gray-900">{user.lastName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-500">
|
||||
Email
|
||||
</label>
|
||||
<p className="text-lg text-gray-900">{user.email}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-500">
|
||||
Téléphone
|
||||
</label>
|
||||
<p className="text-lg text-gray-900">
|
||||
{user.phone || "Non renseigné"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-500">
|
||||
Rôle
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<Badge variant={getRoleBadgeVariant(user.role)}>
|
||||
{getRoleLabel(user.role)}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<button
|
||||
onClick={() => setIsEditing(true)}
|
||||
className="bg-[#1a4d2e] hover:bg-[#2d5a3d] text-white font-bold px-6 py-3 rounded-lg transition-all"
|
||||
>
|
||||
Modifier mes informations
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<Button onClick={() => setIsEditing(true)}>
|
||||
Modifier mes informations
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<Input
|
||||
id="firstName"
|
||||
type="text"
|
||||
label="Prénom"
|
||||
error={errors.firstName?.message}
|
||||
{...register("firstName")}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
id="lastName"
|
||||
type="text"
|
||||
label="Nom"
|
||||
error={errors.lastName?.message}
|
||||
{...register("lastName")}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
label="Email"
|
||||
value={user.email}
|
||||
disabled
|
||||
helperText="L'email ne peut pas être modifié"
|
||||
/>
|
||||
<Input
|
||||
id="phone"
|
||||
type="tel"
|
||||
label="Téléphone"
|
||||
error={errors.phone?.message}
|
||||
{...register("phone")}
|
||||
/>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Enregistrer
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleCancel}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Annuler
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<Input
|
||||
id="firstName"
|
||||
type="text"
|
||||
label="Prénom"
|
||||
error={errors.firstName?.message}
|
||||
{...register("firstName")}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
id="lastName"
|
||||
type="text"
|
||||
label="Nom"
|
||||
error={errors.lastName?.message}
|
||||
{...register("lastName")}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
label="Email"
|
||||
value={user.email}
|
||||
disabled
|
||||
helperText="L'email ne peut pas être modifié"
|
||||
/>
|
||||
<Input
|
||||
id="phone"
|
||||
type="tel"
|
||||
label="Téléphone"
|
||||
error={errors.phone?.message}
|
||||
{...register("phone")}
|
||||
/>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="bg-[#1a4d2e] hover:bg-[#2d5a3d] disabled:bg-gray-400 text-white font-bold px-6 py-3 rounded-lg transition-all"
|
||||
>
|
||||
{isSubmitting ? "Enregistrement..." : "Enregistrer"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
disabled={isSubmitting}
|
||||
className="border-2 border-gray-300 hover:bg-gray-50 text-gray-700 font-bold px-6 py-3 rounded-lg transition-all"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Account Status Card */}
|
||||
<div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Statut du compte</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="bg-white rounded-xl shadow-md overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h2 className="text-xl font-bold text-gray-900">Statut du compte</h2>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-500">
|
||||
Email vérifié
|
||||
|
|
@ -241,55 +245,52 @@ export default function ProfilePage() {
|
|||
{user.updatedAt ? formatDate(user.updatedAt) : 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions Card */}
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Actions rapides</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="bg-white rounded-xl shadow-md overflow-hidden mt-6">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h2 className="text-xl font-bold text-gray-900">Actions rapides</h2>
|
||||
</div>
|
||||
<div className="p-6 space-y-2">
|
||||
{user.role === "CLIENT" && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
fullWidth
|
||||
<button
|
||||
onClick={() => router.push(ROUTES.GAME)}
|
||||
className="w-full bg-[#f59e0b] hover:bg-[#d97706] text-white font-bold px-6 py-3 rounded-lg transition-all"
|
||||
>
|
||||
Jouer
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
fullWidth
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push(ROUTES.HISTORY)}
|
||||
className="w-full border-2 border-[#1a4d2e] text-[#1a4d2e] hover:bg-[#1a4d2e] hover:text-white font-bold px-6 py-3 rounded-lg transition-all"
|
||||
>
|
||||
Historique
|
||||
</Button>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{user.role === "EMPLOYEE" && (
|
||||
<Button
|
||||
variant="outline"
|
||||
fullWidth
|
||||
<button
|
||||
onClick={() => router.push(ROUTES.EMPLOYEE_DASHBOARD)}
|
||||
className="w-full border-2 border-[#1a4d2e] text-[#1a4d2e] hover:bg-[#1a4d2e] hover:text-white font-bold px-6 py-3 rounded-lg transition-all"
|
||||
>
|
||||
Tableau de bord
|
||||
</Button>
|
||||
</button>
|
||||
)}
|
||||
{user.role === "ADMIN" && (
|
||||
<Button
|
||||
variant="outline"
|
||||
fullWidth
|
||||
<button
|
||||
onClick={() => router.push(ROUTES.ADMIN_DASHBOARD)}
|
||||
className="w-full border-2 border-[#1a4d2e] text-[#1a4d2e] hover:bg-[#1a4d2e] hover:text-white font-bold px-6 py-3 rounded-lg transition-all"
|
||||
>
|
||||
Administration
|
||||
</Button>
|
||||
</button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,8 +49,9 @@ export default function Header() {
|
|||
<div className="container mx-auto px-4">
|
||||
<div className="flex items-center justify-between h-18">
|
||||
{/* Logo */}
|
||||
<Link href={ROUTES.HOME} className="group">
|
||||
<Logo size="md" showText={true} className="group-hover:scale-105 transition-transform" />
|
||||
<Link href={ROUTES.HOME} className="group flex items-center gap-3">
|
||||
<Logo size="md" showText={false} className="group-hover:scale-105 transition-transform" />
|
||||
<span className="text-2xl font-bold text-[#f59e0b]">Thé Tip Top</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
|
|
@ -62,13 +63,13 @@ export default function Header() {
|
|||
Accueil
|
||||
</Link>
|
||||
<Link
|
||||
href={ROUTES.GAME}
|
||||
href={ROUTES.LOTS}
|
||||
className="text-white hover:text-[#f59e0b] font-medium transition-colors"
|
||||
>
|
||||
Loto à gagner
|
||||
</Link>
|
||||
<Link
|
||||
href={ROUTES.LOTS}
|
||||
href="/rules"
|
||||
className="text-white hover:text-[#f59e0b] font-medium transition-colors"
|
||||
>
|
||||
Règlement
|
||||
|
|
@ -80,7 +81,7 @@ export default function Header() {
|
|||
FAQ
|
||||
</Link>
|
||||
<Link
|
||||
href="/about"
|
||||
href="/gagnants"
|
||||
className="text-white hover:text-[#f59e0b] font-medium transition-colors"
|
||||
>
|
||||
Gagnants
|
||||
|
|
@ -146,21 +147,30 @@ export default function Header() {
|
|||
<div className="hidden md:flex items-center gap-3">
|
||||
{isAuthenticated && (
|
||||
<>
|
||||
<Link href={getDashboardRoute()}>
|
||||
<button className="flex flex-col items-center bg-white text-[#1a4d2e] hover:bg-[#f59e0b] hover:text-white font-semibold px-4 py-2 rounded-lg transition-all">
|
||||
<span className="text-sm">{user?.firstName} {user?.lastName}</span>
|
||||
<span className="text-xs font-normal opacity-80">{user?.email}</span>
|
||||
</button>
|
||||
</Link>
|
||||
<Link href={ROUTES.PROFILE}>
|
||||
<Button variant="outline" size="sm">
|
||||
👤 {user?.firstName}
|
||||
</Button>
|
||||
<button className="flex items-center gap-2 bg-white text-[#1a4d2e] hover:bg-[#f59e0b] hover:text-white font-semibold px-4 py-2 rounded-lg transition-all">
|
||||
Profil
|
||||
</button>
|
||||
</Link>
|
||||
{user?.role === 'CLIENT' && (
|
||||
<Link href={ROUTES.HISTORY}>
|
||||
<Button variant="outline" size="sm">
|
||||
🏆 Mes gains
|
||||
</Button>
|
||||
<button className="flex items-center gap-2 bg-white text-[#1a4d2e] hover:bg-[#f59e0b] hover:text-white font-semibold px-4 py-2 rounded-lg transition-all">
|
||||
Mes gains
|
||||
</button>
|
||||
</Link>
|
||||
)}
|
||||
<Button variant="outline" size="sm" onClick={logout}>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="flex items-center gap-2 bg-white text-[#1a4d2e] hover:bg-red-600 hover:text-white font-semibold px-4 py-2 rounded-lg transition-all"
|
||||
>
|
||||
Déconnexion
|
||||
</Button>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -297,36 +307,43 @@ export default function Header() {
|
|||
)}
|
||||
|
||||
{isAuthenticated && (
|
||||
<div className="border-t border-gray-200 pt-3 mt-3 space-y-2">
|
||||
<div className="border-t border-white/20 pt-3 mt-3 space-y-2">
|
||||
<Link
|
||||
href={getDashboardRoute()}
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
<button className="w-full flex flex-col items-center bg-white text-[#1a4d2e] hover:bg-[#f59e0b] hover:text-white font-semibold px-4 py-3 rounded-lg transition-all">
|
||||
<span className="text-sm">{user?.firstName} {user?.lastName}</span>
|
||||
<span className="text-xs font-normal opacity-80">{user?.email}</span>
|
||||
</button>
|
||||
</Link>
|
||||
<Link
|
||||
href={ROUTES.PROFILE}
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
<Button variant="outline" size="sm" fullWidth>
|
||||
👤 {user?.firstName}
|
||||
</Button>
|
||||
<button className="w-full flex items-center justify-center gap-2 bg-white text-[#1a4d2e] hover:bg-[#f59e0b] hover:text-white font-semibold px-4 py-3 rounded-lg transition-all">
|
||||
Profil
|
||||
</button>
|
||||
</Link>
|
||||
{user?.role === 'CLIENT' && (
|
||||
<Link
|
||||
href={ROUTES.HISTORY}
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
<Button variant="outline" size="sm" fullWidth>
|
||||
🏆 Mes gains
|
||||
</Button>
|
||||
<button className="w-full flex items-center justify-center gap-2 bg-white text-[#1a4d2e] hover:bg-[#f59e0b] hover:text-white font-semibold px-4 py-3 rounded-lg transition-all">
|
||||
Mes gains
|
||||
</button>
|
||||
</Link>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
fullWidth
|
||||
<button
|
||||
onClick={() => {
|
||||
logout();
|
||||
setIsMobileMenuOpen(false);
|
||||
}}
|
||||
className="w-full flex items-center justify-center gap-2 bg-white text-[#1a4d2e] hover:bg-red-600 hover:text-white font-semibold px-4 py-3 rounded-lg transition-all"
|
||||
>
|
||||
Déconnexion
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -53,30 +53,35 @@ export const PRIZE_CONFIG = {
|
|||
description: 'Un infuseur à thé de qualité',
|
||||
color: 'bg-blue-100 text-blue-800',
|
||||
icon: '🫖',
|
||||
value: 15,
|
||||
},
|
||||
THE_SIGNATURE: {
|
||||
name: 'Thé signature 100g',
|
||||
description: 'Notre thé signature premium 100g',
|
||||
color: 'bg-green-100 text-green-800',
|
||||
icon: '🍵',
|
||||
value: 25,
|
||||
},
|
||||
COFFRET_DECOUVERTE: {
|
||||
name: 'Coffret découverte 39€',
|
||||
description: 'Un coffret découverte de nos meilleurs thés',
|
||||
color: 'bg-purple-100 text-purple-800',
|
||||
icon: '🎁',
|
||||
value: 39,
|
||||
},
|
||||
COFFRET_PRESTIGE: {
|
||||
name: 'Coffret prestige 69€',
|
||||
description: 'Un coffret prestige d\'exception',
|
||||
color: 'bg-amber-100 text-amber-800',
|
||||
icon: '🏆',
|
||||
value: 69,
|
||||
},
|
||||
THE_GRATUIT: {
|
||||
name: 'Thé gratuit en magasin',
|
||||
description: 'Un thé gratuit de votre choix en magasin',
|
||||
color: 'bg-pink-100 text-pink-800',
|
||||
icon: '☕',
|
||||
value: 0,
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user