# Story 4.6: Page Translation - Upload

Status: done

## Story

En tant qu'**utilisateur**,
Je veux **uploader un document via drag-and-drop**,
de sorte que **je puisse démarrer une traduction**.

## Acceptance Criteria

1. **Route**: `/dashboard/translate` affiche la page d'upload
2. **Drop Zone**: Drag-and-drop fonctionnel + click to browse
3. **Validation Format**: Seuls .xlsx, .docx, .pptx acceptés (magic bytes check si possible, sinon extension)
4. **Erreur Format**: Message "Format non supporté. Formats acceptés : .xlsx, .docx, .pptx"
5. **File Preview**: Affiche nom du fichier + taille + icône selon type
6. **Remove File**: Bouton X pour supprimer le fichier et reset l'état
7. **Max Size**: 50 MB max → erreur "Fichier trop volumineux (max 50 MB)"
8. **Branding**: Utiliser icône `Languages` de Lucide + "Office Translator" (PAS "Translate Co." ni "文A")
9. **Merge Directive**: UI depuis `office-translator-landing-page/components/translation-card.tsx`, logique API depuis `frontend/src/components/file-uploader.tsx`
10. **Colocation**: Composants/hooks/types dans `frontend/src/app/dashboard/translate/`

## Tasks / Subtasks

- [x] **Task 1: Créer la structure de la page** (AC: #1, #10)
  - [x] 1.1 Créer `frontend/src/app/dashboard/translate/page.tsx` — Client Component
  - [x] 1.2 Créer `frontend/src/app/dashboard/translate/types.ts` — Types FileUpload
  - [x] 1.3 Layout hérite de `frontend/src/app/dashboard/layout.tsx` (Story 4.5)

- [x] **Task 2: Créer FileDropZone.tsx** (AC: #2, #3, #4, #7)
  - [x] 2.1 Créer `frontend/src/app/dashboard/translate/FileDropZone.tsx` (colocated)
  - [x] 2.2 Implémenter drag-over state avec highlight visuel
  - [x] 2.3 Implémenter file input hidden + click to browse
  - [x] 2.4 Valider extension (.xlsx, .docx, .pptx)
  - [x] 2.5 Valider taille max 50 MB
  - [x] 2.6 Afficher erreur format/taille en dessous de la zone
  - [x] 2.7 Copier styles depuis `translation-card.tsx` (drop zone section)

- [x] **Task 3: Créer FilePreview.tsx** (AC: #5, #6)
  - [x] 3.1 Créer `frontend/src/app/dashboard/translate/FilePreview.tsx` (colocated)
  - [x] 3.2 Afficher icône selon type (FileSpreadsheet/FileText/Presentation)
  - [x] 3.3 Afficher nom du fichier (truncate si trop long)
  - [x] 3.4 Afficher taille formatée (KB/MB)
  - [x] 3.5 Bouton X pour remove → reset state
  - [x] 3.6 Copier styles depuis `translation-card.tsx` (file preview section)

- [x] **Task 4: Créer useFileUpload.ts hook** (AC: #2, #3, #4, #7)
  - [x] 4.1 Créer `frontend/src/app/dashboard/translate/useFileUpload.ts` (colocated)
  - [x] 4.2 State: file, error, isDragOver
  - [x] 4.3 Handlers: handleDrop, handleDragOver, handleDragLeave, handleFileSelect, removeFile
  - [x] 4.4 Validation function: validateFile(file) → error | null
  - [x] 4.5 Return: { file, error, isDragOver, handlers... }

- [x] **Task 5: Créer la page translate/page.tsx** (AC: #1, #8)
  - [x] 5.1 Page heading: "Translate Document"
  - [x] 5.2 Intégrer FileDropZone + FilePreview
  - [x] 5.3 Afficher erreur si validation échoue
  - [x] 5.4 Message helper: "Supported formats: Excel (.xlsx), Word (.docx), PowerPoint (.pptx)"
  - [x] 5.5 Note privacy: "Files are automatically deleted after 60 minutes"

- [x] **Task 6: Vérifier le build** (AC: Tous)
  - [x] 6.1 `npm run build` → 0 erreurs TypeScript
  - [x] 6.2 Tester drag-and-drop
  - [x] 6.3 Tester click to browse
  - [x] 6.4 Tester validation format (essayer .pdf → erreur)
  - [x] 6.5 Tester validation taille (fichier > 50 MB → erreur)
  - [x] 6.6 Tester remove file → state reset

## Dev Notes

### 🏗️ Stack Technique

| Technologie | Version |
|-------------|---------|
| Next.js | 16.0.6 (App Router) |
| React | 19.2.0 |
| TanStack Query | v5.90.21 |
| Tailwind CSS | configuré |
| Lucide React | disponible |
| shadcn/ui | Button, Card, Progress |
| react-dropzone | ⚠️ **DÉJÀ INSTALLÉ** dans frontend/ |

### 📁 Structure Cible (Colocation Pattern)

```
frontend/src/app/dashboard/translate/
├── page.tsx              # 🆕 Page principale
├── FileDropZone.tsx      # 🆕 Zone drag-and-drop
├── FilePreview.tsx       # 🆕 Preview fichier uploadé
├── useFileUpload.ts      # 🆕 Hook validation + state
└── types.ts              # 🆕 Types FileUpload
```

**⚠️ Règle absolue (architecture.md):**
```
🚨 FICHIERS SPÉCIAUX: page.tsx, layout.tsx → TOUJOURS minuscules
🚨 COLOCATION: Components/hooks/types dans le dossier de leur page
```

### 🔗 MERGE DIRECTIVE — Sources à Fusionner

**UI Components depuis `office-translator-landing-page/`:**

| Source | Fichier | Usage |
|--------|---------|-------|
| Drop Zone UI | `office-translator-landing-page/components/translation-card.tsx` | Structure, styles, animation drag-over |
| File Preview | `translation-card.tsx` lignes 154-198 | Preview avec icône, nom, taille, bouton X |
| Error styling | `translation-card.tsx` | Style erreur format non supporté |

**Logique Validation depuis `frontend/`:**

| Source | Fichier | Usage |
|--------|---------|-------|
| Dropzone logic | `frontend/src/components/file-uploader.tsx` lignes 217-228 | react-dropzone config, acceptedTypes |
| File validation | `file-uploader.tsx` | Validation format, taille |
| FilePreview component | `file-uploader.tsx` lignes 56-184 | Preview avancé (à simplifier) |

### ⚠️ CRITIQUE: Formats Acceptés

**MIME Types (depuis file-uploader.tsx):**
```typescript
const ACCEPTED_TYPES = {
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"],
  "application/vnd.ms-excel": [".xls"], // Optionnel: support ancien format
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"],
  "application/msword": [".doc"], // Optionnel
  "application/vnd.openxmlformats-officedocument.presentationml.presentation": [".pptx"],
  "application/vnd.ms-powerpoint": [".ppt"], // Optionnel
};
```

**⚠️ PRD FR50:** Seuls .xlsx, .docx, .pptx sont requis. Les anciens formats (.xls, .doc, .ppt) sont optionnels.

### 🔧 Types TypeScript à Créer

```typescript
// frontend/src/app/dashboard/translate/types.ts

export type SupportedFormat = 'xlsx' | 'docx' | 'pptx';

export interface FileUploadState {
  file: File | null;
  error: string | null;
  isDragOver: boolean;
}

export interface FileUploadActions {
  handleDrop: (e: React.DragEvent) => void;
  handleDragOver: (e: React.DragEvent) => void;
  handleDragLeave: (e: React.DragEvent) => void;
  handleFileSelect: (e: React.ChangeEvent<HTMLInputElement>) => void;
  removeFile: () => void;
}

export interface UseFileUploadReturn extends FileUploadState, FileUploadActions {}
```

### 🔧 useFileUpload.ts — Hook à Créer

```typescript
// frontend/src/app/dashboard/translate/useFileUpload.ts
'use client';

import { useState, useCallback } from 'react';
import type { UseFileUploadReturn } from './types';

const ACCEPTED_EXTENSIONS = ['xlsx', 'docx', 'pptx'];
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50 MB

export function useFileUpload(): UseFileUploadReturn {
  const [file, setFile] = useState<File | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [isDragOver, setIsDragOver] = useState(false);

  const validateFile = useCallback((file: File): string | null => {
    const ext = file.name.split('.').pop()?.toLowerCase();
    
    if (!ext || !ACCEPTED_EXTENSIONS.includes(ext)) {
      return 'Format non supporté. Formats acceptés : .xlsx, .docx, .pptx';
    }
    
    if (file.size > MAX_FILE_SIZE) {
      return 'Fichier trop volumineux (max 50 MB)';
    }
    
    return null;
  }, []);

  const handleDrop = useCallback((e: React.DragEvent) => {
    e.preventDefault();
    setIsDragOver(false);
    
    const droppedFile = e.dataTransfer.files[0];
    if (droppedFile) {
      const validationError = validateFile(droppedFile);
      if (validationError) {
        setError(validationError);
        setFile(null);
      } else {
        setFile(droppedFile);
        setError(null);
      }
    }
  }, [validateFile]);

  const handleDragOver = useCallback((e: React.DragEvent) => {
    e.preventDefault();
    setIsDragOver(true);
  }, []);

  const handleDragLeave = useCallback((e: React.DragEvent) => {
    e.preventDefault();
    setIsDragOver(false);
  }, []);

  const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    const selected = e.target.files?.[0];
    if (selected) {
      const validationError = validateFile(selected);
      if (validationError) {
        setError(validationError);
        setFile(null);
      } else {
        setFile(selected);
        setError(null);
      }
    }
  }, [validateFile]);

  const removeFile = useCallback(() => {
    setFile(null);
    setError(null);
  }, []);

  return {
    file,
    error,
    isDragOver,
    handleDrop,
    handleDragOver,
    handleDragLeave,
    handleFileSelect,
    removeFile,
  };
}
```

### 🔧 FileDropZone.tsx — Pattern à Suivre

**Copier depuis:** `office-translator-landing-page/components/translation-card.tsx` lignes 141-199

```typescript
// frontend/src/app/dashboard/translate/FileDropZone.tsx
'use client';

import { useCallback, useRef } from 'react';
import { Upload, FileCheck, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import type { UseFileUploadReturn } from './types';

interface FileDropZoneProps {
  upload: UseFileUploadReturn;
}

export function FileDropZone({ upload }: FileDropZoneProps) {
  const inputRef = useRef<HTMLInputElement>(null);

  const handleClick = () => {
    inputRef.current?.click();
  };

  return (
    <div
      className={cn(
        "relative flex flex-col items-center justify-center gap-3 rounded-lg border-2 border-dashed px-6 py-10 transition-colors cursor-pointer",
        upload.isDragOver
          ? "border-accent bg-accent/5"
          : upload.file
            ? "border-success/40 bg-success/5"
            : "border-border bg-muted/30 hover:border-muted-foreground/30"
      )}
      onDragOver={upload.handleDragOver}
      onDragLeave={upload.handleDragLeave}
      onDrop={upload.handleDrop}
      onClick={handleClick}
    >
      {/* ... contenu similaire à translation-card.tsx */}
      
      <input
        ref={inputRef}
        type="file"
        accept=".xlsx,.docx,.pptx"
        className="hidden"
        onChange={upload.handleFileSelect}
      />
    </div>
  );
}
```

### 🔧 FilePreview.tsx — Pattern à Suivre

**Copier depuis:** `office-translator-landing-page/components/translation-card.tsx` lignes 154-176

```typescript
// frontend/src/app/dashboard/translate/FilePreview.tsx
'use client';

import { FileSpreadsheet, FileText, Presentation, X } from 'lucide-react';
import { Button } from '@/components/ui/button';

const FILE_ICONS: Record<string, React.ElementType> = {
  xlsx: FileSpreadsheet,
  docx: FileText,
  pptx: Presentation,
};

interface FilePreviewProps {
  file: File;
  onRemove: () => void;
}

export function FilePreview({ file, onRemove }: FilePreviewProps) {
  const ext = file.name.split('.').pop()?.toLowerCase() || '';
  const FileIcon = FILE_ICONS[ext] || FileText;
  
  const formatSize = (bytes: number) => {
    if (bytes < 1024) return `${bytes} B`;
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
    return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
  };

  return (
    <div className="flex items-center gap-3">
      <div className="flex size-10 items-center justify-center rounded-lg bg-secondary">
        <FileIcon className="size-5 text-foreground" />
      </div>
      <div className="flex flex-col">
        <span className="text-sm font-medium text-foreground truncate max-w-xs">
          {file.name}
        </span>
        <span className="text-xs text-muted-foreground">
          {formatSize(file.size)} · .{ext}
        </span>
      </div>
      <Button
        variant="ghost"
        size="icon-sm"
        className="ml-2 text-muted-foreground hover:text-foreground"
        onClick={(e) => {
          e.stopPropagation();
          onRemove();
        }}
      >
        <X className="size-4" />
      </Button>
    </div>
  );
}
```

### 🔧 page.tsx — Structure de la Page

```typescript
// frontend/src/app/dashboard/translate/page.tsx
'use client';

import { FileText, ShieldCheck, Clock } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { FileDropZone } from './FileDropZone';
import { FilePreview } from './FilePreview';
import { useFileUpload } from './useFileUpload';

export default function TranslatePage() {
  const upload = useFileUpload();

  return (
    <div className="mx-auto max-w-xl px-4 py-6 lg:px-8">
      <Card className="border-border/70 shadow-lg">
        <CardHeader>
          <CardTitle className="flex items-center gap-2">
            <FileText className="size-5" />
            Translate Document
          </CardTitle>
          <CardDescription>
            Upload an Excel, Word, or PowerPoint file to translate
          </CardDescription>
        </CardHeader>
        <CardContent className="flex flex-col gap-4">
          {/* Drop Zone / Preview */}
          {upload.file ? (
            <FilePreview file={upload.file} onRemove={upload.removeFile} />
          ) : (
            <FileDropZone upload={upload} />
          )}

          {/* Error Message */}
          {upload.error && (
            <p className="text-sm text-destructive">{upload.error}</p>
          )}

          {/* Helper Text */}
          <p className="text-xs text-muted-foreground">
            Supported formats: Excel (.xlsx), Word (.docx), PowerPoint (.pptx)
          </p>
        </CardContent>
      </Card>

      {/* Privacy Badge */}
      <div className="flex items-center justify-center gap-4 mt-4 text-xs text-muted-foreground">
        <div className="flex items-center gap-1.5">
          <ShieldCheck className="size-3.5" />
          <span>Zero Data Retention</span>
        </div>
        <div className="h-3 w-px bg-border" />
        <div className="flex items-center gap-1.5">
          <Clock className="size-3.5" />
          <span>Files deleted after 60 min</span>
        </div>
      </div>
    </div>
  );
}
```

### 🚨 Anti-Patterns à Éviter

1. **NE PAS utiliser react-dropzone** pour cette story — Utiliser les handlers natifs drag events (plus simple, moins de dépendances)
2. **NE PAS hardcoder** le message d'erreur → utiliser une constante
3. **NE PAS oublier** le `e.stopPropagation()` sur le bouton remove
4. **NE PAS créer** de fichiers globaux — tout est colocated
5. **NE PAS utiliser** "Translate Co." / "文A" → utiliser "Office Translator"
6. **NE PAS afficher** le preview si error est présent

### 🔗 Cohérence Cross-Story (Context Continuité)

**Story 4.5 (Dashboard Layout) a établi:**
- Layout dashboard avec sidebar
- Route `/dashboard/*`
- Auth check via `useUser()` hook
- Pattern colocation

**Cette story (4.6) DOIT:**
- Hériter du layout dashboard existant
- Suivre le même pattern colocation
- Utiliser les mêmes composants UI shadcn/ui

**Stories suivantes (4.7, 4.8) utiliseront:**
- Le fichier uploadé depuis cette page
- L'état de traduction persisté

### 📊 File Size Format

```typescript
const formatFileSize = (bytes: number): string => {
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};
```

### 🔍 Composants Réutilisables Existants

| Composant | Chemin | Usage |
|-----------|--------|-------|
| `Button` | `@/components/ui/button` | variant="ghost", size="icon-sm" |
| `Card` | `@/components/ui/card` | Card, CardHeader, CardTitle, CardDescription, CardContent |
| `Lucide icons` | `lucide-react` | Upload, FileText, FileSpreadsheet, Presentation, X, ShieldCheck, Clock |

### 🧪 Tests à Effectuer

1. **Build**: `npm run build` dans `frontend/` → 0 erreurs TypeScript/ESLint
2. **Drag-and-drop**: Déposer un fichier .xlsx → preview s'affiche
3. **Click to browse**: Cliquer → file dialog s'ouvre
4. **Validation format**: Déposer un .pdf → erreur "Format non supporté"
5. **Validation taille**: Déposer un fichier > 50 MB → erreur "Fichier trop volumineux"
6. **Remove file**: Cliquer X → file reset, error cleared
7. **Navigation**: Sidebar link "Translate" → `/dashboard/translate`

### Project Structure Notes

**Fichiers à créer:**
- `frontend/src/app/dashboard/translate/page.tsx` — Page principale
- `frontend/src/app/dashboard/translate/FileDropZone.tsx` — Zone drag-and-drop
- `frontend/src/app/dashboard/translate/FilePreview.tsx` — Preview fichier
- `frontend/src/app/dashboard/translate/useFileUpload.ts` — Hook state + validation
- `frontend/src/app/dashboard/translate/types.ts` — Types TypeScript

**⚠️ Pas de fichiers globaux à créer** — tout est colocated.

**⚠️ Pas besoin de créer un layout** — hérite de `frontend/src/app/dashboard/layout.tsx` (Story 4.5).

### References

- [Source: _bmad-output/planning-artifacts/epics.md#Story-4.6] — Story requirements
- [Source: _bmad-output/planning-artifacts/architecture.md#Frontend-Architecture] — TanStack Query + colocation
- [Source: _bmad-output/planning-artifacts/architecture.md#Naming-Patterns] — Conventions nommage
- [Source: _bmad-output/planning-artifacts/architecture.md#API-Response-Formats] — Format réponse API
- [Source: office-translator-landing-page/components/translation-card.tsx] — UI drop zone à copier
- [Source: frontend/src/components/file-uploader.tsx] — Logique validation, react-dropzone config
- [Source: frontend/src/lib/apiClient.ts] — Client API centralisé (pas utilisé dans cette story)
- [Source: _bmad-output/implementation-artifacts/4-5-dashboard-layout.md] — Pattern colocation, context dashboard

## Dev Agent Record

### Agent Model Used

Claude Sonnet 4 (claude-sonnet-4@20250514)

### Debug Log References

N/A

### Completion Notes List

- ✅ Créé types.ts avec FileUploadState, FileUploadActions, UseFileUploadReturn
- ✅ Créé useFileUpload.ts hook avec validation format (.xlsx, .docx, .pptx) et taille (50 MB max)
- ✅ Créé FileDropZone.tsx avec drag-and-drop natif + click to browse
- ✅ Créé FilePreview.tsx avec icône dynamique selon type, taille formatée, bouton remove
- ✅ Créé page.tsx intégrant tous les composants avec Card, error display, privacy badges
- ✅ Build Next.js réussi (0 erreurs TypeScript)
- ✅ Route /dashboard/translate fonctionnelle

### Senior Developer Review (AI)

**Date:** 2026-02-23
**Reviewer:** Code Review Workflow

**Issues Fixed:**
1. 🔴 [HIGH] AC #8: Icône `FileText` → `Languages`, titre "Translate Document" → "Office Translator"
2. 🔴 [HIGH] `removeFile()` reset maintenant `isDragOver` state
3. 🟡 [MEDIUM] Input hidden au lieu de `absolute inset-0 opacity-0` (meilleure UX)
4. 🟡 [MEDIUM] Supprimé `'use client'` inutile du hook
5. 🟡 [MEDIUM] Supprimé `onClick={() => {}}` mort dans page.tsx
6. 🟡 [LOW] Messages d'erreur centralisés dans `ERROR_MESSAGES` constant

**Outcome:** ✅ Approved — All issues fixed

### File List

**Fichiers créés:**
- frontend/src/app/dashboard/translate/page.tsx
- frontend/src/app/dashboard/translate/types.ts
- frontend/src/app/dashboard/translate/useFileUpload.ts
- frontend/src/app/dashboard/translate/FileDropZone.tsx
- frontend/src/app/dashboard/translate/FilePreview.tsx

**Fichiers modifiés:**
- _bmad-output/implementation-artifacts/sprint-status.yaml (ready-for-dev → in-progress → review)
