Alexis Bruneteau b1756f1320 feat: Add form validation with Zod and React Hook Form
Form Validation:
- Create comprehensive Zod validation schemas for all forms
- Login form: email, password validation
- Register form: first name, last name, email, password strength requirements
- Profile update form: all user fields with optional phone/address
- Password change form: current password, new password confirmation
- Vote submission form: election ID and candidate selection

Password Strength:
- Minimum 8 characters
- At least one uppercase letter
- At least one digit
- At least one special character (!@#$%^&*)

React Hook Form Integration:
- Update login page with useForm and field-level error display
- Update register page with form validation and error messages
- Show validation errors inline with red borders
- Disable form submission while loading or submitting
- Better user feedback with detailed error messages

Type Safety:
- Zod schemas with TypeScript inference
- Type-safe form data types
- Proper error handling and validation

Build Status:
- All pages compile successfully
- Zero TypeScript errors
- Bundle size includes Zod (~40 kB) and React Hook Form
- Login/Register pages: 145 kB First Load JS (includes new validation libraries)
- Shared bundle remains ~102 kB

Setup:
- npm install zod react-hook-form @hookform/resolvers
- Ready for production with form validation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 17:20:16 +01:00

178 lines
6.9 KiB
TypeScript

"use client"
import Link from "next/link"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Mail, Lock, LogIn, AlertCircle } from "lucide-react"
import { useAuth } from "@/lib/auth-context"
import { loginSchema, type LoginFormData } from "@/lib/validation"
export default function LoginPage() {
const router = useRouter()
const { login, isLoading } = useAuth()
const [apiError, setApiError] = useState("")
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
})
const onSubmit = async (data: LoginFormData) => {
setApiError("")
try {
await login(data.email, data.password)
router.push("/dashboard")
} catch (err) {
setApiError("Email ou mot de passe incorrect")
}
}
return (
<div className="min-h-screen grid grid-cols-1 md:grid-cols-2">
{/* Left side - Form */}
<div className="flex items-center justify-center p-4 md:p-8 bg-background">
<div className="w-full max-w-sm space-y-8">
<div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">Se Connecter</h1>
<p className="text-muted-foreground">Accédez à votre tableau de bord</p>
</div>
<Card>
<CardContent className="pt-6">
{apiError && (
<div className="mb-4 p-4 rounded-md bg-destructive/10 border border-destructive/50 flex gap-3">
<AlertCircle className="w-5 h-5 text-destructive flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-destructive">Erreur de connexion</p>
<p className="text-sm text-destructive/80">{apiError}</p>
</div>
</div>
)}
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<div className="relative">
<Mail className="absolute left-3 top-3 w-5 h-5 text-muted-foreground" />
<Input
id="email"
type="email"
placeholder="votre@email.com"
{...register("email")}
disabled={isLoading || isSubmitting}
className={`pl-10 ${errors.email ? "border-destructive" : ""}`}
/>
</div>
{errors.email && (
<p className="text-sm text-destructive">{errors.email.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="password">Mot de passe</Label>
<div className="relative">
<Lock className="absolute left-3 top-3 w-5 h-5 text-muted-foreground" />
<Input
id="password"
type="password"
placeholder="••••••••"
{...register("password")}
disabled={isLoading || isSubmitting}
className={`pl-10 ${errors.password ? "border-destructive" : ""}`}
/>
</div>
{errors.password && (
<p className="text-sm text-destructive">{errors.password.message}</p>
)}
</div>
<div className="text-right">
<Link href="#" className="text-sm text-accent hover:underline">
Mot de passe oublié ?
</Link>
</div>
<Button
type="submit"
className="w-full"
disabled={isLoading || isSubmitting}
>
{isLoading || isSubmitting ? (
"Connexion en cours..."
) : (
<>
<LogIn className="w-4 h-4 mr-2" />
Se Connecter
</>
)}
</Button>
</form>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-border"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-background text-muted-foreground">ou</span>
</div>
</div>
<p className="text-center text-sm text-muted-foreground">
Pas encore de compte?{" "}
<Link href="/auth/register" className="text-accent font-medium hover:underline">
S'inscrire
</Link>
</p>
</CardContent>
</Card>
</div>
</div>
{/* Right side - Illustration */}
<div className="hidden md:flex items-center justify-center p-8 bg-gradient-to-br from-card to-background">
<div className="text-center space-y-8 max-w-sm">
<div className="text-7xl">🗳️</div>
<div className="space-y-4">
<h2 className="text-3xl font-bold">Bienvenue</h2>
<p className="text-muted-foreground leading-relaxed">
Votez en toute confiance sur notre plateforme sécurisée par cryptographie post-quantique
</p>
</div>
<div className="space-y-4 pt-4">
<div className="flex items-center gap-4 text-left">
<span className="text-2xl">🔒</span>
<div>
<p className="font-semibold text-foreground">Cryptographie Post-Quantique</p>
<p className="text-sm text-muted-foreground">Sécurité certifiée NIST</p>
</div>
</div>
<div className="flex items-center gap-4 text-left">
<span className="text-2xl">📊</span>
<div>
<p className="font-semibold text-foreground">Résultats Transparents</p>
<p className="text-sm text-muted-foreground">Traçabilité complète</p>
</div>
</div>
<div className="flex items-center gap-4 text-left">
<span className="text-2xl">⚡</span>
<div>
<p className="font-semibold text-foreground">Accès Instantané</p>
<p className="text-sm text-muted-foreground">De n'importe quel appareil</p>
</div>
</div>
</div>
</div>
</div>
</div>
)
}