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

19 KiB

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

  • Task 1: Créer la structure de la page (AC: #1, #10)

    • 1.1 Créer frontend/src/app/dashboard/translate/page.tsx — Client Component
    • 1.2 Créer frontend/src/app/dashboard/translate/types.ts — Types FileUpload
    • 1.3 Layout hérite de frontend/src/app/dashboard/layout.tsx (Story 4.5)
  • Task 2: Créer FileDropZone.tsx (AC: #2, #3, #4, #7)

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

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

    • 4.1 Créer frontend/src/app/dashboard/translate/useFileUpload.ts (colocated)
    • 4.2 State: file, error, isDragOver
    • 4.3 Handlers: handleDrop, handleDragOver, handleDragLeave, handleFileSelect, removeFile
    • 4.4 Validation function: validateFile(file) → error | null
    • 4.5 Return: { file, error, isDragOver, handlers... }
  • Task 5: Créer la page translate/page.tsx (AC: #1, #8)

    • 5.1 Page heading: "Translate Document"
    • 5.2 Intégrer FileDropZone + FilePreview
    • 5.3 Afficher erreur si validation échoue
    • 5.4 Message helper: "Supported formats: Excel (.xlsx), Word (.docx), PowerPoint (.pptx)"
    • 5.5 Note privacy: "Files are automatically deleted after 60 minutes"
  • Task 6: Vérifier le build (AC: Tous)

    • 6.1 npm run build → 0 erreurs TypeScript
    • 6.2 Tester drag-and-drop
    • 6.3 Tester click to browse
    • 6.4 Tester validation format (essayer .pdf → erreur)
    • 6.5 Tester validation taille (fichier > 50 MB → erreur)
    • 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):

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

// 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

// 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

// 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

// 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

// 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

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 FileTextLanguages, 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)