Core Integration: - Create API client with TypeScript types for all endpoints - Implement authentication context provider for user state management - Add protected route component for dashboard access control - Connect login/register pages to backend authentication endpoints - Implement user session persistence with localStorage tokens Authentication: - Login page now connects to /api/auth/login endpoint - Register page connects to /api/auth/register with validation - Password strength requirements (min 8 chars) - Form validation and error handling - Automatic redirect to dashboard on successful auth - Logout functionality with session cleanup Protected Routes: - Dashboard pages require authentication - Non-authenticated users redirected to login - Loading spinner during auth verification - User name displayed in dashboard header - Proper session management Election/Vote APIs: - Dashboard fetches active elections from /api/elections/active - Display real election data with candidates count - Handle loading and error states - Skeleton loaders for better UX Type Safety: - Full TypeScript interfaces for all API responses - Proper error handling with try-catch blocks - API response types: AuthToken, VoterProfile, Election, Candidate, Vote, VoteHistory Environment: - API URL configurable via NEXT_PUBLIC_API_URL env variable - Default to http://localhost:8000 for local development 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
177 lines
5.5 KiB
TypeScript
177 lines
5.5 KiB
TypeScript
"use client"
|
|
|
|
import Link from "next/link"
|
|
import { useState, useEffect } from "react"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { BarChart3, CheckCircle, Clock, Archive } from "lucide-react"
|
|
import { electionsApi, Election } from "@/lib/api"
|
|
|
|
export default function DashboardPage() {
|
|
const [activeVotes, setActiveVotes] = useState<Election[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
try {
|
|
setLoading(true)
|
|
const response = await electionsApi.getActive()
|
|
if (response.data) {
|
|
setActiveVotes(response.data)
|
|
} else if (response.error) {
|
|
setError(response.error)
|
|
}
|
|
} catch (err) {
|
|
setError("Erreur lors du chargement des données")
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
fetchData()
|
|
}, [])
|
|
|
|
// Mock data for stats
|
|
const stats = [
|
|
{
|
|
title: "Votes Actifs",
|
|
value: "3",
|
|
icon: CheckCircle,
|
|
color: "text-accent",
|
|
href: "/dashboard/votes/active",
|
|
},
|
|
{
|
|
title: "À Venir",
|
|
value: "5",
|
|
icon: Clock,
|
|
color: "text-blue-500",
|
|
href: "/dashboard/votes/upcoming",
|
|
},
|
|
{
|
|
title: "Votes Passés",
|
|
value: "12",
|
|
icon: BarChart3,
|
|
color: "text-green-500",
|
|
href: "/dashboard/votes/history",
|
|
},
|
|
{
|
|
title: "Archives",
|
|
value: "8",
|
|
icon: Archive,
|
|
color: "text-gray-500",
|
|
href: "/dashboard/votes/archives",
|
|
},
|
|
]
|
|
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
{/* Header */}
|
|
<div>
|
|
<h1 className="text-3xl font-bold">Tableau de Bord</h1>
|
|
<p className="text-muted-foreground mt-2">
|
|
Gérez et participez à vos élections en toute sécurité
|
|
</p>
|
|
</div>
|
|
|
|
{/* Stats Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
{stats.map((stat) => {
|
|
const Icon = stat.icon
|
|
return (
|
|
<Link key={stat.href} href={stat.href}>
|
|
<Card className="hover:border-accent transition-colors cursor-pointer h-full">
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardDescription className="text-xs">{stat.title}</CardDescription>
|
|
<CardTitle className="text-2xl mt-2">{stat.value}</CardTitle>
|
|
</div>
|
|
<Icon className={`w-8 h-8 ${stat.color}`} />
|
|
</div>
|
|
</CardHeader>
|
|
</Card>
|
|
</Link>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Active Votes Section */}
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-2xl font-bold">Votes Actifs</h2>
|
|
<Link href="/dashboard/votes/active">
|
|
<Button variant="outline">Voir tous</Button>
|
|
</Link>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="p-4 rounded-lg bg-destructive/10 border border-destructive/50">
|
|
<p className="text-sm text-destructive">{error}</p>
|
|
</div>
|
|
)}
|
|
|
|
{loading && (
|
|
<div className="flex justify-center py-8">
|
|
<div className="w-6 h-6 border-3 border-muted border-t-accent rounded-full animate-spin" />
|
|
</div>
|
|
)}
|
|
|
|
{!loading && activeVotes.length === 0 && (
|
|
<div className="text-center py-8">
|
|
<p className="text-muted-foreground">Aucun vote actif en ce moment</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid gap-6">
|
|
{activeVotes.slice(0, 3).map((vote) => (
|
|
<Link key={vote.id} href={`/dashboard/votes/active/${vote.id}`}>
|
|
<Card className="hover:border-accent transition-colors cursor-pointer">
|
|
<CardHeader>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<CardTitle className="text-lg">{vote.name}</CardTitle>
|
|
<CardDescription className="mt-1">{vote.description}</CardDescription>
|
|
</div>
|
|
|
|
{/* Footer Info */}
|
|
<div className="flex items-center justify-between pt-2 border-t border-border">
|
|
<span className="text-xs text-muted-foreground">
|
|
Candidates: {vote.candidates?.length || 0}
|
|
</span>
|
|
<Button size="sm">Détails</Button>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
</Card>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Actions */}
|
|
<div className="bg-card border border-border rounded-lg p-6 space-y-4">
|
|
<h3 className="font-bold text-lg">Actions Rapides</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<Link href="/dashboard/votes/active">
|
|
<Button variant="outline" className="w-full">
|
|
Voir mes votes actifs
|
|
</Button>
|
|
</Link>
|
|
<Link href="/dashboard/votes/history">
|
|
<Button variant="outline" className="w-full">
|
|
Historique de votes
|
|
</Button>
|
|
</Link>
|
|
<Link href="/dashboard/profile">
|
|
<Button variant="outline" className="w-full">
|
|
Gérer mon profil
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|