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