# 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<LoginResponse>()`
  - [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<LoginResponse>(
        '/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 (
    <Card variant="elevated" className="w-full max-w-md mx-auto">
      <CardHeader className="text-center pb-6">
        {/* Logo - Office Translator */}
        <Link href="/" className="inline-flex items-center gap-3 mb-6 group">
          <div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary text-primary-foreground shadow-lg">
            <Languages className="h-6 w-6" />
          </div>
          <span className="text-2xl font-semibold text-foreground">
            Office Translator
          </span>
        </Link>

        <CardTitle className="text-2xl font-bold">
          Welcome back
        </CardTitle>
        <CardDescription>
          Sign in to continue translating
        </CardDescription>
      </CardHeader>

      <CardContent className="space-y-6">
        {/* Error Message */}
        {loginMutation.isError && (
          <div className="rounded-lg bg-destructive/10 border border-destructive/30 p-4">
            <p className="text-destructive text-sm">
              {loginMutation.error?.message || 'Login failed'}
            </p>
          </div>
        )}

        <form onSubmit={handleSubmit} className="space-y-4">
          {/* Email Field */}
          <div className="space-y-2">
            <Label htmlFor="email">Email</Label>
            <div className="relative">
              <Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
              <Input
                id="email"
                type="email"
                placeholder="you@example.com"
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                className="pl-10"
                required
              />
            </div>
          </div>

          {/* Password Field */}
          <div className="space-y-2">
            <div className="flex justify-between items-center">
              <Label htmlFor="password">Password</Label>
              <Link href="/auth/forgot-password" className="text-sm text-primary hover:underline">
                Forgot password?
              </Link>
            </div>
            <div className="relative">
              <Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
              <Input
                id="password"
                type={showPassword ? 'text' : 'password'}
                placeholder="••••••••"
                value={password}
                onChange={(e) => setPassword(e.target.value)}
                className="pl-10 pr-10"
                required
              />
              <button
                type="button"
                onClick={() => setShowPassword(!showPassword)}
                className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
              >
                {showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
              </button>
            </div>
          </div>

          {/* Submit Button */}
          <Button
            type="submit"
            disabled={loginMutation.isPending || !email || !password}
            className="w-full"
          >
            {loginMutation.isPending ? (
              <>
                <Loader2 className="mr-2 h-4 w-4 animate-spin" />
                Signing in...
              </>
            ) : (
              <>
                Sign In
                <ArrowRight className="ml-2 h-4 w-4" />
              </>
            )}
          </Button>
        </form>

        {/* Sign up link */}
        <p className="text-center text-sm text-muted-foreground">
          Don&apos;t have an account?{' '}
          <Link href="/auth/register" className="text-primary hover:underline">
            Sign up for free
          </Link>
        </p>
      </CardContent>
    </Card>
  );
}
```

### 📁 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
