Alexis Bruneteau 546785ef67 feat: Integrate backend API with frontend - Authentication & Elections
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>
2025-11-06 17:15:34 +01:00

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>
)
}