feat: Add vote detail page for individual elections (/dashboard/votes/active/[id])

This commit is contained in:
Alexis Bruneteau 2025-11-07 02:47:06 +01:00
parent f83bd796dd
commit 2b8adc1e30

View File

@ -0,0 +1,288 @@
"use client"
import { useState, useEffect } from "react"
import Link from "next/link"
import { useParams } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { ArrowLeft, Clock, Users, CheckCircle2, AlertCircle } from "lucide-react"
import { VotingInterface } from "@/components/voting-interface"
export default function VoteDetailPage() {
const params = useParams()
const voteId = params.id as string
const [election, setElection] = useState<any>(null)
const [isLoading, setIsLoading] = useState(true)
const [hasVoted, setHasVoted] = useState(false)
const [error, setError] = useState<string | null>(null)
// Mock elections data - matches the active/page.tsx
const mockElections: Record<string, any> = {
"1": {
id: 1,
name: "Election Présidentielle 2025",
description: "Première manche - Scrutin dimanche",
category: "Nationale",
status: "En cours",
progress: 65,
endDate: "2025-11-06T20:00:00",
candidates: [
{ id: 1, name: "Alice Dupont", description: "Candidate A" },
{ id: 2, name: "Bob Martin", description: "Candidate B" },
{ id: 3, name: "Claire Laurent", description: "Candidate C" },
],
votes: 4521,
},
"2": {
id: 2,
name: "Référendum : Réforme Constitutionnelle",
description: "Consultez la population sur la nouvelle constitution",
category: "Nationale",
status: "En cours",
progress: 45,
endDate: "2025-11-08T18:00:00",
candidates: [
{ id: 1, name: "Oui", description: "Pour la réforme" },
{ id: 2, name: "Non", description: "Contre la réforme" },
],
votes: 2341,
},
"3": {
id: 3,
name: "Election Municipale - Île-de-France",
description: "Élection locale régionale pour les positions municipales",
category: "Locale",
status: "En cours",
progress: 78,
endDate: "2025-11-10T17:00:00",
candidates: [
{ id: 1, name: "Liste A", description: "Équipe A" },
{ id: 2, name: "Liste B", description: "Équipe B" },
{ id: 3, name: "Liste C", description: "Équipe C" },
],
votes: 1234,
},
"4": {
id: 4,
name: "Conseil Départemental",
description: "Élection des conseillers départementaux",
category: "Régionale",
status: "En cours",
progress: 52,
endDate: "2025-11-12T19:00:00",
candidates: [
{ id: 1, name: "Conseiller A", description: "Candidat 1" },
{ id: 2, name: "Conseiller B", description: "Candidat 2" },
{ id: 3, name: "Conseiller C", description: "Candidat 3" },
],
votes: 987,
},
}
useEffect(() => {
const fetchElection = async () => {
try {
setIsLoading(true)
setError(null)
// First try to fetch from API
const token = localStorage.getItem("access_token")
try {
const response = await fetch(`/api/elections/${voteId}`, {
headers: {
Authorization: `Bearer ${token}`,
},
})
if (response.ok) {
const data = await response.json()
setElection(data)
return
}
} catch (err) {
// Fall back to mock data
}
// Use mock data if API fails
const mockData = mockElections[voteId]
if (mockData) {
setElection(mockData)
} else {
setError("Élection non trouvée")
}
} catch (err) {
setError("Erreur lors du chargement de l'élection")
} finally {
setIsLoading(false)
}
}
fetchElection()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [voteId])
if (isLoading) {
return (
<div className="space-y-8">
<div className="flex items-center gap-4 mb-4">
<Link href="/dashboard/votes/active">
<Button variant="ghost" size="sm">
<ArrowLeft className="w-4 h-4 mr-2" />
Retour
</Button>
</Link>
</div>
<Card>
<CardContent className="pt-6">
<p className="text-muted-foreground">Chargement...</p>
</CardContent>
</Card>
</div>
)
}
if (error || !election) {
return (
<div className="space-y-8">
<div className="flex items-center gap-4 mb-4">
<Link href="/dashboard/votes/active">
<Button variant="ghost" size="sm">
<ArrowLeft className="w-4 h-4 mr-2" />
Retour
</Button>
</Link>
</div>
<Card className="border-red-500 bg-red-50 dark:bg-red-950">
<CardContent className="pt-6 flex gap-4">
<AlertCircle className="w-5 h-5 text-red-500 flex-shrink-0" />
<div>
<h3 className="font-semibold text-red-900 dark:text-red-100">Erreur</h3>
<p className="text-sm text-red-800 dark:text-red-200 mt-1">
{error || "Élection non trouvée"}
</p>
</div>
</CardContent>
</Card>
</div>
)
}
return (
<div className="space-y-8">
{/* Header */}
<div>
<div className="flex items-center gap-4 mb-4">
<Link href="/dashboard/votes/active">
<Button variant="ghost" size="sm">
<ArrowLeft className="w-4 h-4 mr-2" />
Retour aux votes actifs
</Button>
</Link>
</div>
<div className="space-y-2">
<h1 className="text-3xl font-bold">{election.name}</h1>
<p className="text-muted-foreground">{election.description}</p>
</div>
</div>
{/* Election Info */}
<div className="grid gap-4 md:grid-cols-3">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<Users className="w-4 h-4" />
Candidats
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold">{election.candidates?.length || 0}</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<CheckCircle2 className="w-4 h-4" />
Votes
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold">{election.votes?.toLocaleString() || 0}</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<Clock className="w-4 h-4" />
Statut
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-lg font-bold text-accent">{election.status}</p>
</CardContent>
</Card>
</div>
{/* Voting Interface */}
{!hasVoted ? (
<Card>
<CardHeader>
<CardTitle>Voter</CardTitle>
<CardDescription>
Sélectionnez votre choix et confirmez votre vote
</CardDescription>
</CardHeader>
<CardContent>
<VotingInterface
electionId={election.id}
candidates={election.candidates || []}
onVoteSubmitted={() => {
setHasVoted(true)
}}
/>
</CardContent>
</Card>
) : (
<Card className="border-green-500 bg-green-50 dark:bg-green-950">
<CardContent className="pt-6 flex gap-4">
<CheckCircle2 className="w-5 h-5 text-green-500 flex-shrink-0 mt-0.5" />
<div>
<h3 className="font-semibold text-green-900 dark:text-green-100">Vote enregistré</h3>
<p className="text-sm text-green-800 dark:text-green-200 mt-1">
Votre vote a é enregistré et chiffré de manière sécurisée.
</p>
<Link href="/dashboard/votes/history" className="text-sm font-medium text-green-700 dark:text-green-300 hover:underline mt-2 block">
Voir l'historique de vos votes
</Link>
</div>
</CardContent>
</Card>
)}
{/* Candidates List */}
{election.candidates && election.candidates.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Candidats</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{election.candidates.map((candidate: any) => (
<div
key={candidate.id}
className="p-3 rounded-lg border border-border hover:border-accent/50 transition-colors"
>
<h4 className="font-medium">{candidate.name}</h4>
{candidate.description && (
<p className="text-sm text-muted-foreground">{candidate.description}</p>
)}
</div>
))}
</div>
</CardContent>
</Card>
)}
</div>
)
}