Files
office_translator/_bmad-output/implementation-artifacts/4-6-page-translation-upload.md
Sepehr Ramezani 26bd096a06 feat: production deployment - full update with providers, admin, glossaries, pricing, tests
Major changes across backend, frontend, infrastructure:
- Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud)
- Admin panel: user management, pricing, settings
- Glossary system with CSV import/export
- Subscription and tier quota management
- Security hardening (rate limiting, API key auth, path traversal fixes)
- Docker compose for dev, prod, and IONOS deployment
- Alembic migrations for new tables
- Frontend: dashboard, pricing page, landing page, i18n (en/fr)
- Test suite and verification scripts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-25 15:01:47 +02:00

552 lines
19 KiB
Markdown

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