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

206 lines
8.5 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 { 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, AlertCircle, CheckCircle } from "lucide-react"
import { useAuth } from "@/lib/auth-context"
import { registerSchema, type RegisterFormData } from "@/lib/validation"
export default function RegisterPage() {
const router = useRouter()
const { register: registerUser, isLoading } = useAuth()
const [apiError, setApiError] = useState("")
const [success, setSuccess] = useState(false)
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<RegisterFormData>({
resolver: zodResolver(registerSchema),
})
const onSubmit = async (data: RegisterFormData) => {
setApiError("")
setSuccess(false)
try {
await registerUser(data.email, data.password, data.firstName, data.lastName)
setSuccess(true)
setTimeout(() => {
router.push("/dashboard")
}, 500)
} catch (err) {
setApiError("Une erreur s'est produite lors de l'inscription")
}
}
return (
<div className="min-h-screen grid grid-cols-1 md:grid-cols-2">
{/* Left 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">Rejoignez-nous</h2>
<p className="text-muted-foreground leading-relaxed">
Créez un compte pour participer à des élections sécurisées et transparentes
</p>
</div>
<div className="space-y-4 pt-4">
<div className="flex items-center gap-4 text-left">
<CheckCircle className="w-6 h-6 text-accent flex-shrink-0" />
<p className="font-semibold text-foreground">Inscription gratuite</p>
</div>
<div className="flex items-center gap-4 text-left">
<CheckCircle className="w-6 h-6 text-accent flex-shrink-0" />
<p className="font-semibold text-foreground">Sécurité maximale</p>
</div>
<div className="flex items-center gap-4 text-left">
<CheckCircle className="w-6 h-6 text-accent flex-shrink-0" />
<p className="font-semibold text-foreground">Aucune données</p>
</div>
</div>
</div>
</div>
{/* Right 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">S'inscrire</h1>
<p className="text-muted-foreground">Créez votre compte E-Voting</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</p>
<p className="text-sm text-destructive/80">{apiError}</p>
</div>
</div>
)}
{success && (
<div className="mb-4 p-4 rounded-md bg-accent/10 border border-accent/50 flex gap-3">
<CheckCircle className="w-5 h-5 text-accent flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-accent">Succès</p>
<p className="text-sm text-accent/80">Votre compte a été créé avec succès</p>
</div>
</div>
)}
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="firstName">Prénom</Label>
<Input
id="firstName"
placeholder="Jean"
{...register("firstName")}
disabled={isLoading || isSubmitting}
className={errors.firstName ? "border-destructive" : ""}
/>
{errors.firstName && (
<p className="text-sm text-destructive">{errors.firstName.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="lastName">Nom</Label>
<Input
id="lastName"
placeholder="Dupont"
{...register("lastName")}
disabled={isLoading || isSubmitting}
className={errors.lastName ? "border-destructive" : ""}
/>
{errors.lastName && (
<p className="text-sm text-destructive">{errors.lastName.message}</p>
)}
</div>
</div>
<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="space-y-2">
<Label htmlFor="passwordConfirm">Confirmer le mot de passe</Label>
<div className="relative">
<Lock className="absolute left-3 top-3 w-5 h-5 text-muted-foreground" />
<Input
id="passwordConfirm"
type="password"
placeholder="••••••••"
{...register("passwordConfirm")}
disabled={isLoading || isSubmitting}
className={`pl-10 ${errors.passwordConfirm ? "border-destructive" : ""}`}
/>
</div>
{errors.passwordConfirm && (
<p className="text-sm text-destructive">{errors.passwordConfirm.message}</p>
)}
</div>
<Button type="submit" className="w-full" disabled={isLoading || isSubmitting}>
{isLoading || isSubmitting ? "Inscription en cours..." : "S'inscrire"}
</Button>
</form>
<p className="text-center text-sm text-muted-foreground mt-6">
Déjà un compte?{" "}
<Link href="/auth/login" className="text-accent font-medium hover:underline">
Se connecter
</Link>
</p>
</CardContent>
</Card>
</div>
</div>
</div>
)
}