feat: Add vote detail page for individual elections (/dashboard/votes/active/[id])
This commit is contained in:
parent
f83bd796dd
commit
2b8adc1e30
@ -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 été 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>
|
||||
)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user