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

121 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Menu, LogOut, User as UserIcon } from "lucide-react"
import { useState } from "react"
import { useAuth } from "@/lib/auth-context"
import { ProtectedRoute } from "@/components/protected-route"
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const router = useRouter()
const { user, logout } = useAuth()
const [sidebarOpen, setSidebarOpen] = useState(false)
const handleLogout = () => {
logout()
router.push("/auth/login")
}
const navItems = [
{ href: "/dashboard", label: "Tableau de Bord", icon: "📊" },
{ href: "/dashboard/votes/active", label: "Votes Actifs", icon: "🗳️" },
{ href: "/dashboard/votes/upcoming", label: "Votes à Venir", icon: "📅" },
{ href: "/dashboard/votes/history", label: "Historique", icon: "📜" },
{ href: "/dashboard/votes/archives", label: "Archives", icon: "🗂️" },
{ href: "/dashboard/profile", label: "Profil", icon: "👤" },
]
return (
<div className="min-h-screen bg-background flex">
{/* Sidebar */}
<aside
className={`fixed inset-y-0 left-0 z-40 w-64 bg-card border-r border-border transition-transform duration-300 ${
sidebarOpen ? "translate-x-0" : "-translate-x-full"
} lg:static lg:translate-x-0`}
>
<div className="flex flex-col h-full">
{/* Logo */}
<div className="flex items-center gap-2 p-6 border-b border-border">
<span className="text-2xl">🗳</span>
<span className="font-bold text-lg text-accent">E-Voting</span>
</div>
{/* Navigation */}
<nav className="flex-1 p-4 space-y-2">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-muted transition-colors text-foreground hover:text-accent"
onClick={() => setSidebarOpen(false)}
>
<span className="text-xl">{item.icon}</span>
<span className="text-sm font-medium">{item.label}</span>
</Link>
))}
</nav>
{/* Footer */}
<div className="p-4 border-t border-border space-y-2">
<Link href="/dashboard/profile">
<Button variant="ghost" className="w-full justify-start gap-2">
<UserIcon className="w-4 h-4" />
Mon Profil
</Button>
</Link>
<Button
onClick={handleLogout}
variant="ghost"
className="w-full justify-start gap-2 text-destructive hover:text-destructive"
>
<LogOut className="w-4 h-4" />
Déconnexion
</Button>
</div>
</div>
</aside>
{/* Main Content */}
<div className="flex-1 flex flex-col">
{/* Top Bar */}
<header className="sticky top-0 z-30 border-b border-border bg-card/50 backdrop-blur-sm">
<div className="flex items-center justify-between px-6 py-4">
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
className="lg:hidden p-2 hover:bg-muted rounded-lg"
>
<Menu className="w-5 h-5" />
</button>
<div className="ml-auto flex items-center gap-4">
{user && (
<span className="text-sm text-muted-foreground">
Bienvenue, {user.first_name} {user.last_name}
</span>
)}
</div>
</div>
</header>
{/* Content Area */}
<main className="flex-1 overflow-auto p-6 max-w-7xl w-full mx-auto">
<ProtectedRoute>{children}</ProtectedRoute>
</main>
</div>
{/* Mobile Overlay */}
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/50 z-30 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
</div>
)
}