# Story 4.3: Page Login Status: done ## Story En tant qu'**utilisateur**, Je veux **me connecter via un formulaire clair et moderne**, de sorte que **je puisse accéder à mon dashboard**. ## Acceptance Criteria 1. **Navigation**: Page accessible via `/auth/login` ou `/login` 2. **Formulaire**: Email + mot de passe avec validation en temps réel 3. **API Integration**: Utilise `apiClient` pour appeler `POST /api/v1/auth/login` 4. **Success**: Si connexion réussie → redirection vers `/dashboard` (ou URL dans `?redirect=`) 5. **Error Handling**: Si échec → toast d'erreur avec message de l'API (`{error, message}`) 6. **Token Storage**: Stocke `access_token` et `refresh_token` dans localStorage (note: httpOnly cookies nécessite backend) 7. **Branding**: Logo et nom "Office Translator" (pas "Translate Co.") 8. **TanStack Query**: Utilise `useMutation` pour la requête de login 9. **UX**: Loading state, validation visuelle, toggle password visibility ## Tasks / Subtasks - [x] **Task 1: Refactor LoginForm avec apiClient et TanStack Query** (AC: #3, #5, #8) - [x] 1.1 Extraire `LoginForm.tsx` en composant colocated dans `app/auth/login/` - [x] 1.2 Remplacer `fetch()` par `apiClient.post()` - [x] 1.3 Utiliser `useMutation` de TanStack Query pour le login - [x] 1.4 Gérer les erreurs avec `ApiClientError` et afficher toast - [x] **Task 2: Créer le hook useLogin** (AC: #3, #4, #5, #6, #8) - [x] 2.1 Créer `useLogin.ts` dans le même dossier (colocation) - [x] 2.2 Encapsuler la logique de mutation avec `useMutation` - [x] 2.3 Stocker tokens dans localStorage après succès - [x] 2.4 Rediriger vers `/dashboard` ou `redirect` param - [x] **Task 3: Créer les types TypeScript** (AC: #3) - [x] 3.1 Créer `types.ts` dans `app/auth/login/` - [x] 3.2 Définir `LoginRequest`, `LoginResponse`, `User` - [x] **Task 4: Mettre à jour le branding** (AC: #7) - [x] 4.1 Remplacer "Translate Co." par "Office Translator" - [x] 4.2 Remplacer logo "文A" par icône `Languages` de Lucide - [x] 4.3 Aligner le design avec la landing page (Story 4.2) - [x] **Task 5: Intégrer le toast pour les erreurs** (AC: #5) - [x] 5.1 Utiliser le système de toast existant (`notification.tsx` ou créer `useToast`) - [x] 5.2 Afficher `error.message` de l'API en cas d'échec - [x] **Task 6: Tester l'intégration complète** (AC: Tous) - [x] 6.1 Vérifier build: `npm run build` dans frontend/ - [x] 6.2 Tester login avec backend (credentials valides/invalides) - [x] 6.3 Vérifier redirection vers /dashboard - [x] 6.4 Vérifier persistance des tokens ## Dev Notes ### 🏗️ Architecture - Stack Frontend **Technologies:** - Next.js: 16.0.6 (App Router) - React: 19.2.0 - TanStack Query: v5.90.21 - Tailwind CSS: configuré - Lucide React: icônes **⚠️ Règles Critiques App Router:** ``` 🚨 FICHIERS SPÉCIAUX: page.tsx, layout.tsx → TOUJOURS minuscules → JAMAIS Page.tsx, Layout.tsx (le framework retourne 404) 🚨 COLOCATION: Components/hooks/types dans le dossier de leur page ``` ### 📁 Structure Actuelle vs Cible **Actuel (à refactorer):** ``` frontend/src/app/auth/login/ └── page.tsx # 386 lignes avec tout inline ``` **Cible (colocation pattern):** ``` frontend/src/app/(auth)/login/ ├── page.tsx # Page wrapper avec Suspense ├── LoginForm.tsx # Composant formulaire (colocated) ├── useLogin.ts # Hook mutation (colocated) └── types.ts # Types LoginRequest, LoginResponse (colocated) ``` ### 🔧 Backend API Endpoint **Endpoint:** `POST /api/v1/auth/login` **Request:** ```json { "email": "user@example.com", "password": "password123" } ``` **Response (succès):** ```json { "data": { "access_token": "eyJ...", "refresh_token": "eyJ...", "user": { "id": "uuid", "email": "user@example.com", "tier": "free" } }, "meta": {} } ``` **Response (erreur):** ```json { "error": "INVALID_CREDENTIALS", "message": "Email ou mot de passe incorrect", "details": {} } ``` ### 🔧 Types TypeScript à Créer ```typescript // app/(auth)/login/types.ts export interface LoginRequest { email: string; password: string; } export interface User { id: string; email: string; tier: 'free' | 'pro'; } export interface LoginResponse { access_token: string; refresh_token: string; user: User; } ``` ### 🔧 Hook useLogin à Créer ```typescript // app/(auth)/login/useLogin.ts 'use client'; import { useMutation } from '@tanstack/react-query'; import { useRouter, useSearchParams } from 'next/navigation'; import { apiClient } from '@/lib/apiClient'; import type { LoginRequest, LoginResponse } from './types'; export function useLogin() { const router = useRouter(); const searchParams = useSearchParams(); const redirect = searchParams.get('redirect') || '/dashboard'; return useMutation({ mutationFn: async (credentials: LoginRequest) => { const response = await apiClient.post( '/api/v1/auth/login', credentials ); return response.data; }, onSuccess: (data) => { // Stocker tokens dans localStorage localStorage.setItem('token', data.access_token); localStorage.setItem('refresh_token', data.refresh_token); localStorage.setItem('user', JSON.stringify(data.user)); // Rediriger router.push(redirect); }, }); } ``` ### 🔧 LoginForm.tsx Refactorisé ```typescript // app/(auth)/login/LoginForm.tsx 'use client'; import { useState } from 'react'; import Link from 'next/link'; import { Eye, EyeOff, Mail, Lock, ArrowRight, Loader2, Languages } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { useLogin } from './useLogin'; import { cn } from '@/lib/utils'; export function LoginForm() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); const loginMutation = useLogin(); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); loginMutation.mutate({ email, password }); }; return ( {/* Logo - Office Translator */}
Office Translator Welcome back Sign in to continue translating
{/* Error Message */} {loginMutation.isError && (

{loginMutation.error?.message || 'Login failed'}

)}
{/* Email Field */}
setEmail(e.target.value)} className="pl-10" required />
{/* Password Field */}
Forgot password?
setPassword(e.target.value)} className="pl-10 pr-10" required />
{/* Submit Button */}
{/* Sign up link */}

Don't have an account?{' '} Sign up for free

); } ``` ### 📁 Fichier Actuel à Modifier Le fichier actuel `frontend/src/app/auth/login/page.tsx` (386 lignes) doit être: 1. Déplacé vers `frontend/src/app/(auth)/login/page.tsx` (route group) 2. Simplifié pour utiliser les composants colocated 3. Le composant LoginForm extrait dans son propre fichier ### 🚨 Anti-Patterns à Éviter 1. **NE PAS hardcoder l'URL API** - Utiliser `apiClient` qui lit `NEXT_PUBLIC_API_URL` 2. **NE PAS utiliser fetch() direct** - Utiliser `apiClient.post()` pour la gestion automatique des erreurs 3. **NE PAS utiliser useEffect pour le login** - Utiliser `useMutation` de TanStack Query 4. **NE PAS utiliser "Translate Co."** - Branding doit être "Office Translator" 5. **NE PAS utiliser l'icône "文A"** - Utiliser `Languages` de Lucide (comme sur la landing page) 6. **NE PAS oublier le Suspense** - `useSearchParams()` nécessite Suspense boundary ### 📊 Token Storage Note **Actuel:** localStorage ```typescript localStorage.setItem('token', data.access_token); localStorage.setItem('refresh_token', data.refresh_token); ``` **Future (sécurisé):** httpOnly cookies - Nécessite modification backend: set-cookie dans response - Non prévu dans cette story (backend déjà utilise localStorage pattern) - L'apiClient actuel lit depuis localStorage ### 🔍 Composants UI Disponibles Depuis `frontend/src/components/ui/`: - `button.tsx` - Bouton shadcn/ui - `card.tsx` - Card avec variants (elevated, outline) - `input.tsx` - Input shadcn/ui - `label.tsx` - Label shadcn/ui - `notification.tsx` - Pour les toasts ### 🧪 Tests à Effectuer 1. **Build sans erreurs**: `npm run build` dans frontend/ 2. **Login succès**: Credentials valides → redirection /dashboard 3. **Login échec**: Credentials invalides → message d'erreur affiché 4. **Token storage**: Vérifier localStorage après login 5. **Redirect param**: `/auth/login?redirect=/dashboard/api-keys` → redirige vers /dashboard/api-keys 6. **Branding**: Vérifier "Office Translator" et icône Languages ### Project Structure Notes - **Modification:** `src/app/auth/login/page.tsx` → Simplifier et utiliser composants colocated - **Optionnel:** Déplacer vers `src/app/(auth)/login/` pour route group cohérent - **Création:** `src/app/(auth)/login/LoginForm.tsx` - Composant formulaire - **Création:** `src/app/(auth)/login/useLogin.ts` - Hook mutation - **Création:** `src/app/(auth)/login/types.ts` - Types TypeScript **Note:** Le pattern colocation recommande de placer les composants, hooks et types spécifiques à la page dans le même dossier que `page.tsx`. ### References - [Source: _bmad-output/planning-artifacts/epics.md#Story-4.3] - Story requirements - [Source: _bmad-output/planning-artifacts/architecture.md#Frontend-Architecture] - TanStack Query decision - [Source: _bmad-output/planning-artifacts/architecture.md#API-Response-Formats] - Format API - [Source: _bmad-output/planning-artifacts/architecture.md#Naming-Patterns] - Conventions naming - [Source: _bmad-output/planning-artifacts/architecture.md#Organisation-Frontend] - Structure colocation - [Source: _bmad-output/implementation-artifacts/4-1-setup-tanstack-query-api-client.md] - apiClient setup - [Source: _bmad-output/implementation-artifacts/4-2-landing-page.md] - Landing page (branding, styling) - [Source: frontend/src/app/auth/login/page.tsx] - Page actuelle à refactorer - [Source: frontend/src/lib/apiClient.ts] - Client API centralisé - [Source: frontend/src/components/layout/site-header.tsx] - Branding Office Translator + icône Languages ## Dev Agent Record ### Agent Model Used Claude 3.5 Sonnet (claude-3-5-sonnet) ### Debug Log References Aucun problème rencontré lors de l'implémentation. ### Completion Notes List - ✅ Créé `types.ts` avec LoginRequest, LoginResponse, User interfaces - ✅ Créé `useLogin.ts` hook avec useMutation de TanStack Query - ✅ Créé `LoginForm.tsx` composant avec branding Office Translator - ✅ Refactoré `page.tsx` pour utiliser les composants colocalisés - ✅ Remplacé fetch() direct par apiClient.post() - ✅ Branding mis à jour: "Office Translator" + icône Languages - ✅ Build Next.js réussi sans erreurs - ✅ 11 tests existants passent - ✅ Lint OK sur les nouveaux fichiers - ✅ [Review] Ajouté NotificationProvider dans layout.tsx - ✅ [Review] LoginForm utilise useNotification() pour les erreurs (toast) - ✅ [Review] useLogin typé avec ApiClientError pour type safety - ✅ [Review] Ajouté route `/login` comme alias vers `/auth/login` ### File List **Nouveaux fichiers:** - `frontend/src/app/auth/login/types.ts` - Types TypeScript pour login - `frontend/src/app/auth/login/useLogin.ts` - Hook useMutation pour login - `frontend/src/app/auth/login/LoginForm.tsx` - Composant formulaire login - `frontend/src/app/login/page.tsx` - Route alias `/login` → `/auth/login` **Fichiers modifiés:** - `frontend/src/app/auth/login/page.tsx` - Refactoré pour utiliser composants colocalisés (386 → 41 lignes) - `frontend/src/app/layout.tsx` - Ajouté NotificationProvider pour toasts ## Change Log - 2026-02-23: Implémentation complète de la Story 4.3 - Page Login avec colocation pattern - 2026-02-23: [Code Review] Fix 6 issues: NotificationProvider, toast system, error typing, /login alias