refactor: Remove all mock data and use real API data for elections and voting
This commit is contained in:
parent
a73c713b9c
commit
c367dbaf43
@ -5,69 +5,58 @@ import Link from "next/link"
|
|||||||
import { useParams } from "next/navigation"
|
import { useParams } from "next/navigation"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { ArrowLeft, Clock, Users, CheckCircle2, AlertCircle } from "lucide-react"
|
import { ArrowLeft, Clock, Users, CheckCircle2, AlertCircle, Loader2 } from "lucide-react"
|
||||||
import { VotingInterface } from "@/components/voting-interface"
|
import { VotingInterface } from "@/components/voting-interface"
|
||||||
|
|
||||||
|
interface Candidate {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
order: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Election {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
start_date: string
|
||||||
|
end_date: string
|
||||||
|
is_active: boolean
|
||||||
|
results_published: boolean
|
||||||
|
candidates: Candidate[]
|
||||||
|
}
|
||||||
|
|
||||||
export default function VoteDetailPage() {
|
export default function VoteDetailPage() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const voteId = params.id as string
|
const voteId = params.id as string
|
||||||
const [election, setElection] = useState<any>(null)
|
const [election, setElection] = useState<Election | null>(null)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [hasVoted, setHasVoted] = useState(false)
|
const [hasVoted, setHasVoted] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
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 progressive" },
|
|
||||||
{ id: 2, name: "Bob Martin", description: "Candidate centriste" },
|
|
||||||
{ id: 3, name: "Claire Laurent", description: "Candidate conservatrice" },
|
|
||||||
],
|
|
||||||
votes: 4521,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchElection = async () => {
|
const fetchElection = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
// First try to fetch from API
|
|
||||||
const token = localStorage.getItem("access_token")
|
const token = localStorage.getItem("access_token")
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/elections/${voteId}`, {
|
const response = await fetch(`/api/elections/${voteId}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.ok) {
|
if (!response.ok) {
|
||||||
const data = await response.json()
|
throw new Error("Élection non trouvée")
|
||||||
setElection(data)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Fall back to mock data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use mock data if API fails
|
const data = await response.json()
|
||||||
const mockData = mockElections[voteId]
|
setElection(data)
|
||||||
if (mockData) {
|
|
||||||
setElection(mockData)
|
|
||||||
} else {
|
|
||||||
setError("Élection non trouvée")
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError("Erreur lors du chargement de l'élection")
|
const message = err instanceof Error ? err.message : "Erreur lors du chargement"
|
||||||
|
setError(message)
|
||||||
|
setElection(null)
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
@ -89,8 +78,9 @@ export default function VoteDetailPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6 flex gap-4 items-center justify-center py-12">
|
||||||
<p className="text-muted-foreground">Chargement...</p>
|
<Loader2 className="w-5 h-5 animate-spin text-accent" />
|
||||||
|
<p className="text-muted-foreground">Chargement de l'élection...</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@ -110,7 +100,7 @@ export default function VoteDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
<Card className="border-red-500 bg-red-50 dark:bg-red-950">
|
<Card className="border-red-500 bg-red-50 dark:bg-red-950">
|
||||||
<CardContent className="pt-6 flex gap-4">
|
<CardContent className="pt-6 flex gap-4">
|
||||||
<AlertCircle className="w-5 h-5 text-red-500 flex-shrink-0" />
|
<AlertCircle className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-red-900 dark:text-red-100">Erreur</h3>
|
<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">
|
<p className="text-sm text-red-800 dark:text-red-200 mt-1">
|
||||||
@ -137,7 +127,9 @@ export default function VoteDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h1 className="text-3xl font-bold">{election.name}</h1>
|
<h1 className="text-3xl font-bold">{election.name}</h1>
|
||||||
|
{election.description && (
|
||||||
<p className="text-muted-foreground">{election.description}</p>
|
<p className="text-muted-foreground">{election.description}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -158,30 +150,34 @@ export default function VoteDetailPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
||||||
<CheckCircle2 className="w-4 h-4" />
|
<Clock className="w-4 h-4" />
|
||||||
Votes
|
Date de fin
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-3xl font-bold">{election.votes?.toLocaleString() || 0}</p>
|
<p className="text-sm font-bold">
|
||||||
|
{new Date(election.end_date).toLocaleDateString("fr-FR")}
|
||||||
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
||||||
<Clock className="w-4 h-4" />
|
<CheckCircle2 className="w-4 h-4" />
|
||||||
Statut
|
Statut
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-lg font-bold text-accent">{election.status}</p>
|
<p className="text-lg font-bold text-accent">
|
||||||
|
{election.is_active ? "En cours" : "Terminée"}
|
||||||
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Voting Interface */}
|
{/* Voting Interface */}
|
||||||
{!hasVoted ? (
|
{!hasVoted && election.is_active ? (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Voter</CardTitle>
|
<CardTitle>Voter</CardTitle>
|
||||||
@ -199,21 +195,33 @@ export default function VoteDetailPage() {
|
|||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : hasVoted ? (
|
||||||
<Card className="border-green-500 bg-green-50 dark:bg-green-950">
|
<Card className="border-green-500 bg-green-50 dark:bg-green-950">
|
||||||
<CardContent className="pt-6 flex gap-4">
|
<CardContent className="pt-6 flex gap-4">
|
||||||
<CheckCircle2 className="w-5 h-5 text-green-500 flex-shrink-0 mt-0.5" />
|
<CheckCircle2 className="w-5 h-5 text-green-500 flex-shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-green-900 dark:text-green-100">Vote enregistré</h3>
|
<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">
|
<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.
|
Votre vote a été enregistré dans la blockchain et chiffré de manière sécurisée.
|
||||||
</p>
|
</p>
|
||||||
<Link href="/dashboard/votes/history" className="text-sm font-medium text-green-700 dark:text-green-300 hover:underline mt-2 block">
|
<Link href="/dashboard/blockchain" className="text-sm font-medium text-green-700 dark:text-green-300 hover:underline mt-2 block">
|
||||||
Voir l'historique de vos votes →
|
Voir la blockchain →
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Card className="border-yellow-500 bg-yellow-50 dark:bg-yellow-950">
|
||||||
|
<CardContent className="pt-6 flex gap-4">
|
||||||
|
<AlertCircle className="w-5 h-5 text-yellow-500 flex-shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-yellow-900 dark:text-yellow-100">Élection terminée</h3>
|
||||||
|
<p className="text-sm text-yellow-800 dark:text-yellow-200 mt-1">
|
||||||
|
Cette élection est terminée. Les résultats sont disponibles.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Candidates List */}
|
{/* Candidates List */}
|
||||||
@ -224,7 +232,7 @@ export default function VoteDetailPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{election.candidates.map((candidate: any) => (
|
{election.candidates.map((candidate) => (
|
||||||
<div
|
<div
|
||||||
key={candidate.id}
|
key={candidate.id}
|
||||||
className="p-3 rounded-lg border border-border hover:border-accent/50 transition-colors"
|
className="p-3 rounded-lg border border-border hover:border-accent/50 transition-colors"
|
||||||
|
|||||||
@ -1,24 +1,119 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { ChevronRight } from "lucide-react"
|
import { ChevronRight, AlertCircle, Loader2 } from "lucide-react"
|
||||||
|
|
||||||
|
interface Election {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
start_date: string
|
||||||
|
end_date: string
|
||||||
|
is_active: boolean
|
||||||
|
candidates: Array<{ id: number; name: string }>
|
||||||
|
}
|
||||||
|
|
||||||
export default function ActiveVotesPage() {
|
export default function ActiveVotesPage() {
|
||||||
const activeVotes = [
|
const [elections, setElections] = useState<Election[]>([])
|
||||||
{
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
id: 1,
|
const [error, setError] = useState<string | null>(null)
|
||||||
title: "Election Présidentielle 2025",
|
|
||||||
description: "Première manche - Scrutin dimanche",
|
useEffect(() => {
|
||||||
category: "Nationale",
|
const fetchElections = async () => {
|
||||||
status: "En cours",
|
try {
|
||||||
progress: 65,
|
setIsLoading(true)
|
||||||
endDate: "6 Nov 2025 à 20:00",
|
setError(null)
|
||||||
candidates: 12,
|
const token = localStorage.getItem("access_token")
|
||||||
votes: 4521,
|
|
||||||
|
const response = await fetch("/api/elections/active", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
]
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Impossible de charger les élections actives")
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
setElections(data || [])
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : "Erreur lors du chargement"
|
||||||
|
setError(message)
|
||||||
|
setElections([])
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchElections()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">Votes Actifs</h1>
|
||||||
|
<p className="text-muted-foreground mt-2">
|
||||||
|
Participez aux élections et scrutins en cours
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6 flex gap-4 items-center justify-center py-12">
|
||||||
|
<Loader2 className="w-5 h-5 animate-spin text-accent" />
|
||||||
|
<p className="text-muted-foreground">Chargement des élections actives...</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">Votes Actifs</h1>
|
||||||
|
<p className="text-muted-foreground mt-2">
|
||||||
|
Participez aux élections et scrutins en cours
|
||||||
|
</p>
|
||||||
|
</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 mt-0.5" />
|
||||||
|
<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}</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elections.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">Votes Actifs</h1>
|
||||||
|
<p className="text-muted-foreground mt-2">
|
||||||
|
Participez aux élections et scrutins en cours
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6 text-center py-12">
|
||||||
|
<div className="text-5xl mb-4">📭</div>
|
||||||
|
<h3 className="font-semibold text-lg">Aucune élection active</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mt-2">
|
||||||
|
Il n'y a actuellement aucune élection en cours. Revenez bientôt.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
@ -33,66 +128,49 @@ export default function ActiveVotesPage() {
|
|||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
<Button variant="default" size="sm">
|
<Button variant="default" size="sm">
|
||||||
Tous ({activeVotes.length})
|
Tous ({elections.length})
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
Nationales (1)
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Votes List */}
|
{/* Elections List */}
|
||||||
<div className="grid gap-6">
|
<div className="grid gap-6">
|
||||||
{activeVotes.map((vote) => (
|
{elections.map((election) => (
|
||||||
<Card key={vote.id} className="overflow-hidden hover:border-accent transition-colors">
|
<Card key={election.id} className="overflow-hidden hover:border-accent transition-colors">
|
||||||
<CardHeader className="pb-0">
|
<CardHeader className="pb-0">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Title and Category */}
|
{/* Title */}
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<CardTitle className="text-xl">{vote.title}</CardTitle>
|
<CardTitle className="text-xl">{election.name}</CardTitle>
|
||||||
<CardDescription className="mt-2">{vote.description}</CardDescription>
|
{election.description && (
|
||||||
|
<CardDescription className="mt-2">{election.description}</CardDescription>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="px-3 py-1 rounded-full bg-accent/10 text-accent text-xs font-medium whitespace-nowrap">
|
<span className="px-3 py-1 rounded-full bg-accent/10 text-accent text-xs font-medium whitespace-nowrap">
|
||||||
{vote.category}
|
En cours
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats Row */}
|
{/* Stats Row */}
|
||||||
<div className="grid grid-cols-3 gap-4 py-4 border-y border-border">
|
<div className="grid grid-cols-2 gap-4 py-4 border-y border-border">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-muted-foreground">Candidats</p>
|
<p className="text-xs text-muted-foreground">Candidats</p>
|
||||||
<p className="text-lg font-bold">{vote.candidates}</p>
|
<p className="text-lg font-bold">{election.candidates?.length || 0}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-muted-foreground">Votes</p>
|
<p className="text-xs text-muted-foreground">Date de fin</p>
|
||||||
<p className="text-lg font-bold">{vote.votes.toLocaleString()}</p>
|
<p className="text-sm font-bold">
|
||||||
</div>
|
{new Date(election.end_date).toLocaleDateString("fr-FR")}
|
||||||
<div>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">Participation</p>
|
|
||||||
<p className="text-lg font-bold">{vote.progress}%</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress Bar */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-muted-foreground">Participation</span>
|
|
||||||
<span className="font-medium">{vote.progress}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full h-2 bg-muted rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full bg-gradient-to-r from-accent to-accent/60 transition-all"
|
|
||||||
style={{ width: `${vote.progress}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="flex items-center justify-between pt-2">
|
<div className="flex items-center justify-between pt-2">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Ferme le {vote.endDate}
|
Ferme le {new Date(election.end_date).toLocaleString("fr-FR")}
|
||||||
</span>
|
</span>
|
||||||
<Link href={`/dashboard/votes/active/${vote.id}`}>
|
<Link href={`/dashboard/votes/active/${election.id}`}>
|
||||||
<Button>
|
<Button>
|
||||||
Participer
|
Participer
|
||||||
<ChevronRight className="w-4 h-4 ml-2" />
|
<ChevronRight className="w-4 h-4 ml-2" />
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user