From 464eb27f1e3ca1bed9cdda712b5aebce106fe93c Mon Sep 17 00:00:00 2001 From: Hermann_Kitio Date: Sat, 18 Apr 2026 02:13:08 +0300 Subject: [PATCH] =?UTF-8?q?feat(auth):=20LoginPage=20+=20RegisterPage=20(S?= =?UTF-8?q?print=201=20=C3=A9tape=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/auth/pages/LoginPage.tsx | 135 +++++++++++++++ src/features/auth/pages/RegisterPage.tsx | 201 +++++++++++++++++++++++ 2 files changed, 336 insertions(+) create mode 100644 src/features/auth/pages/LoginPage.tsx create mode 100644 src/features/auth/pages/RegisterPage.tsx diff --git a/src/features/auth/pages/LoginPage.tsx b/src/features/auth/pages/LoginPage.tsx new file mode 100644 index 0000000..246212e --- /dev/null +++ b/src/features/auth/pages/LoginPage.tsx @@ -0,0 +1,135 @@ +/** + * Page de connexion. + * + * Formulaire email + mot de passe, appel de `signIn` (Supabase via auth-client). + * Si la session devient active (arrivée déjà connecté OU succès de signIn), + * `useAuth` propage l'état et le useEffect redirige vers /dashboard. On évite + * ainsi un double `navigate` concurrent depuis le handler de submit. + */ + +import { useEffect, useState, type FormEvent } from 'react' +import { Link, useNavigate } from 'react-router-dom' +import { Loader2 } from 'lucide-react' + +import { Button } from '@/shared/components/ui/button' +import { Input } from '@/shared/components/ui/input' +import { Label } from '@/shared/components/ui/label' +import { signIn } from '@/shared/lib/auth-client' +import { useAuth } from '../hooks/useAuth' + +function mapSignInError(message: string | undefined): string { + if (!message) return 'Connexion impossible. Réessayez dans quelques instants.' + if (message === 'Invalid login credentials') { + return 'Email ou mot de passe incorrect.' + } + if (message.includes('Email not confirmed')) { + return 'Email non confirmé. Vérifiez votre boîte mail.' + } + return 'Connexion impossible. Réessayez dans quelques instants.' +} + +export function LoginPage() { + const navigate = useNavigate() + const { isAuthenticated, isLoading } = useAuth() + + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + if (!isLoading && isAuthenticated) { + navigate('/dashboard', { replace: true }) + } + }, [isLoading, isAuthenticated, navigate]) + + if (isLoading || isAuthenticated) { + return ( +
+
+ ) + } + + async function handleSubmit(e: FormEvent) { + e.preventDefault() + setError(null) + setIsSubmitting(true) + try { + const { error: signInError } = await signIn(email, password) + if (signInError) { + setError(mapSignInError(signInError.message)) + } + } finally { + setIsSubmitting(false) + } + } + + return ( +
+
+

Se connecter

+

Accédez à votre espace Expria.

+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setEmail(e.target.value)} + disabled={isSubmitting} + /> +
+ +
+ + setPassword(e.target.value)} + disabled={isSubmitting} + /> +
+ + +
+ +

+ Pas encore de compte ?{' '} + + Créer un compte + +

+
+
+ ) +} + +export default LoginPage diff --git a/src/features/auth/pages/RegisterPage.tsx b/src/features/auth/pages/RegisterPage.tsx new file mode 100644 index 0000000..e78a08a --- /dev/null +++ b/src/features/auth/pages/RegisterPage.tsx @@ -0,0 +1,201 @@ +/** + * Page d'inscription. + * + * Formulaire email + mot de passe + confirmation, validation Zod côté client, + * appel de `signUp` (Supabase via auth-client). Supabase envoie un email de + * confirmation par défaut : on n'a donc pas de session active après succès. + * On affiche un message de confirmation invitant à vérifier la boîte mail, + * puis à revenir sur /login. + */ + +import { useState, type FormEvent } from 'react' +import { Link } from 'react-router-dom' +import { Loader2 } from 'lucide-react' +import { z } from 'zod' + +import { Button } from '@/shared/components/ui/button' +import { Input } from '@/shared/components/ui/input' +import { Label } from '@/shared/components/ui/label' +import { signUp } from '@/shared/lib/auth-client' + +const registerSchema = z + .object({ + email: z.string().email('Email invalide'), + password: z.string().min(8, 'Le mot de passe doit contenir au moins 8 caractères'), + confirmPassword: z.string(), + }) + .refine((data) => data.password === data.confirmPassword, { + path: ['confirmPassword'], + message: 'Les mots de passe ne correspondent pas', + }) + +type FieldErrors = Partial> + +function mapSignUpError(message: string | undefined): string { + if (!message) return 'Inscription impossible. Réessayez dans quelques instants.' + if (message.toLowerCase().includes('already registered')) { + return 'Un compte existe déjà avec cet email.' + } + if (/password/i.test(message)) { + return 'Mot de passe refusé par le serveur. Choisissez un mot de passe plus robuste.' + } + return 'Inscription impossible. Réessayez dans quelques instants.' +} + +export function RegisterPage() { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [fieldErrors, setFieldErrors] = useState({}) + const [formError, setFormError] = useState(null) + const [successMessage, setSuccessMessage] = useState(null) + const [isSubmitting, setIsSubmitting] = useState(false) + + async function handleSubmit(e: FormEvent) { + e.preventDefault() + setFieldErrors({}) + setFormError(null) + + const parsed = registerSchema.safeParse({ email, password, confirmPassword }) + if (!parsed.success) { + const flat = parsed.error.flatten().fieldErrors + setFieldErrors({ + email: flat.email?.[0], + password: flat.password?.[0], + confirmPassword: flat.confirmPassword?.[0], + }) + return + } + + setIsSubmitting(true) + try { + const { error: signUpError } = await signUp(parsed.data.email, parsed.data.password) + if (signUpError) { + setFormError(mapSignUpError(signUpError.message)) + return + } + setSuccessMessage( + 'Compte créé. Vérifiez votre email pour confirmer votre inscription, puis connectez-vous.', + ) + } finally { + setIsSubmitting(false) + } + } + + return ( +
+
+

Créer un compte

+

Commencez votre préparation TCF Canada.

+ + {successMessage ? ( + <> +
+ {successMessage} +
+

+ + Retour à la connexion + +

+ + ) : ( + <> + {formError && ( +
+ {formError} +
+ )} + +
+
+ + setEmail(e.target.value)} + disabled={isSubmitting} + aria-invalid={!!fieldErrors.email} + aria-describedby={fieldErrors.email ? 'email-error' : undefined} + /> + {fieldErrors.email && ( +

+ {fieldErrors.email} +

+ )} +
+ +
+ + setPassword(e.target.value)} + disabled={isSubmitting} + aria-invalid={!!fieldErrors.password} + aria-describedby={fieldErrors.password ? 'password-error' : undefined} + /> + {fieldErrors.password && ( +

+ {fieldErrors.password} +

+ )} +
+ +
+ + setConfirmPassword(e.target.value)} + disabled={isSubmitting} + aria-invalid={!!fieldErrors.confirmPassword} + aria-describedby={ + fieldErrors.confirmPassword ? 'confirmPassword-error' : undefined + } + /> + {fieldErrors.confirmPassword && ( +

+ {fieldErrors.confirmPassword} +

+ )} +
+ + +
+ +

+ Déjà un compte ?{' '} + + Se connecter + +

+ + )} +
+
+ ) +} + +export default RegisterPage