Files
office_translator/_bmad-output/implementation-artifacts/4-5-dashboard-layout.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

20 KiB

Story 4.5: Dashboard Layout

Status: done

Story

En tant qu'utilisateur connecté, Je veux un layout dashboard avec sidebar de navigation, de sorte que je puisse naviguer entre les fonctionnalités (Translate, API Keys, Glossaries).

Acceptance Criteria

  1. Route: /dashboard affiche le layout avec sidebar
  2. Sidebar Items: Translate (Overview), API Keys, Glossaries (Pro only visible si tier="pro")
  3. User Info: Avatar, nom, email, badge tier (Free/Pro) dans la sidebar
  4. Logout: Bouton déconnexion qui clear localStorage et redirige vers /
  5. Layout Check: Server Component pour vérifier auth (token présent) → sinon redirect /auth/login
  6. Branding: Logo "Office Translator" avec icône Languages de Lucide (PAS "文A" ni "Translate Co.")
  7. Responsive: Header mobile avec menu hamburger, sidebar cachée sur mobile (< lg)
  8. TanStack Query: Utiliser pour fetch user data depuis GET /api/v1/auth/me
  9. Merge Directive: UI depuis office-translator-landing-page/, logique API depuis frontend/

Tasks / Subtasks

  • Task 1: Créer le layout dashboard (AC: #1, #5, #6, #7)

    • 1.1 Créer frontend/src/app/dashboard/layout.tsx — Server Component avec auth check
    • 1.2 Vérifier token dans cookies/localStorage (pattern: rediriger vers /auth/login?redirect=/dashboard si absent)
    • 1.3 Structure: sidebar desktop + header mobile + main content area
    • 1.4 Branding: <Languages className="size-3.5" /> + "Office Translator"
  • Task 2: Créer DashboardSidebar.tsx (AC: #2, #3, #4, #6)

    • 2.1 Créer frontend/src/app/dashboard/DashboardSidebar.tsx (colocated)
    • 2.2 Nav items: Overview (/dashboard), API Keys (/dashboard/api-keys), Glossaries (/dashboard/glossaries)
    • 2.3 Condition Pro: glossaries visible uniquement si user.tier === "pro"
    • 2.4 User section: Avatar (initiales), nom, email, Badge tier
    • 2.5 Logout button: clear localStorage (token, refresh_token, user) → redirect /
    • 2.6 Copier styles depuis office-translator-landing-page/components/dashboard-sidebar.tsx
  • Task 3: Créer DashboardHeader.tsx (AC: #7)

    • 3.1 Créer frontend/src/app/dashboard/DashboardHeader.tsx (colocated)
    • 3.2 Mobile: menu hamburger toggle, brand compact
    • 3.3 Desktop: titre page, badge tier, avatar user
    • 3.4 Mobile drawer: navigation items (même que sidebar)
    • 3.5 Copier styles depuis office-translator-landing-page/components/dashboard-header.tsx
  • Task 4: Créer useUser hook (AC: #3, #8)

    • 4.1 Créer frontend/src/app/dashboard/useUser.ts (colocated)
    • 4.2 Utiliser useQuery pour fetch GET /api/v1/auth/me
    • 4.3 Headers: Authorization: Bearer ${localStorage.getItem('token')}
    • 4.4 Return: { user, isLoading, error }
    • 4.5 Si 401 → clear localStorage → redirect /auth/login
  • Task 5: Créer types.ts (AC: #3)

    • 5.1 Créer frontend/src/app/dashboard/types.ts (colocated)
    • 5.2 Interface User: id, email, name, tier, created_at
  • Task 6: Créer page.tsx dashboard overview (AC: #1)

    • 6.1 Mettre à jour frontend/src/app/(app)/dashboard/page.tsx existant
    • 6.2 Simplifier: afficher overview basique (welcome message + liens rapides)
    • 6.3 Supprimer le code legacy "Translate Co." / "文A" / fetch("http://localhost:8000/api/auth/me")
  • Task 7: Vérifier le build (AC: Tous)

    • 7.1 npm run build → 0 erreurs TypeScript
    • 7.2 Tester navigation desktop sidebar
    • 7.3 Tester navigation mobile (hamburger menu)
    • 7.4 Tester logout → redirection vers /
    • 7.5 Tester sans token → redirection vers /auth/login

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, Avatar, Badge, Separator, Input, Card

📁 Structure Cible (Colocation Pattern)

frontend/src/app/dashboard/
├── layout.tsx              # 🆕 Layout avec auth check + structure
├── page.tsx                # ⚠️ À remplacer (simplifier overview)
├── DashboardSidebar.tsx    # 🆕 Sidebar desktop
├── DashboardHeader.tsx     # 🆕 Header mobile + desktop
├── useUser.ts              # 🆕 Hook fetch user data
└── types.ts                # 🆕 User interface

⚠️ 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
Sidebar UI office-translator-landing-page/components/dashboard-sidebar.tsx Structure, styles, navigation items
Header UI office-translator-landing-page/components/dashboard-header.tsx Mobile hamburger, responsive pattern
Page UI office-translator-landing-page/app/dashboard/page.tsx Layout flex, main structure

Logique API depuis frontend/:

Source Fichier Usage
Auth pattern frontend/src/app/auth/login/useLogin.ts Token storage, redirect pattern
Sidebar legacy frontend/src/components/sidebar.tsx User section, logout logic, plan badge
Dashboard legacy frontend/src/app/(app)/dashboard/page.tsx API calls, user fetch (mais remplacer fetch par apiClient)

⚠️ CRITIQUE: Comportement du Backend

GET /api/v1/auth/me — Retourne les infos user:

{
  "data": {
    "id": "usr_xxx",
    "email": "user@example.com",
    "name": "John Doe",
    "tier": "free" | "pro",
    "created_at": "2024-01-15T10:30:00Z"
  },
  "meta": {}
}

Codes d'erreur:

Code erreur HTTP Action
UNAUTHORIZED 401 Clear localStorage, redirect /auth/login
TOKEN_EXPIRED 401 Idem

🔧 Types TypeScript à Créer

// frontend/src/app/dashboard/types.ts

export interface User {
  id: string;
  email: string;
  name: string;
  tier: 'free' | 'pro';
  created_at: string;
}

🔧 Hook useUser à Créer

// frontend/src/app/dashboard/useUser.ts
'use client';

import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/navigation';
import { apiClient } from '@/lib/apiClient';
import type { User } from './types';

export function useUser() {
  const router = useRouter();

  return useQuery({
    queryKey: ['user', 'me'],
    queryFn: async () => {
      const response = await apiClient.get<{ data: User }>('/api/v1/auth/me');
      return response.data;
    },
    retry: false,
    staleTime: 5 * 60 * 1000, // 5 min cache
  });
}

Note: apiClient gère déjà l'ajout du header Authorization: Bearer ${token} et la gestion des erreurs 401.

🔧 DashboardSidebar.tsx — Pattern à Suivre

Copier depuis: office-translator-landing-page/components/dashboard-sidebar.tsx

Modifications requises:

  1. Remplacer "Jane Doe" / "jane@acme.com" par données réelles depuis useUser()
  2. Condition Pro: Afficher "Glossaries & Context" uniquement si user.tier === "pro"
  3. Logout: Ajouter logique clear localStorage + redirect
  4. Back to home: Lien vers / (landing page)

Navigation items:

const navItems = [
  { label: "Overview", href: "/dashboard", icon: LayoutDashboard },
  { label: "API Keys", href: "/dashboard/api-keys", icon: Key },
  // Glossaries: conditionnel selon tier
  { label: "Glossaries", href: "/dashboard/glossaries", icon: BookText, proOnly: true },
];

🔧 DashboardHeader.tsx — Pattern à Suivre

Copier depuis: office-translator-landing-page/components/dashboard-header.tsx

Modifications requises:

  1. Remplacer "Pro Plan" badge par données réelles (user.tier)
  2. Remplacer "JD" avatar par initiales de user.name
  3. Mobile drawer: Même navigation que sidebar

🔧 layout.tsx — Auth Check Pattern

// frontend/src/app/dashboard/layout.tsx
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';

export default async function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  // Server-side auth check
  const cookieStore = await cookies();
  const token = cookieStore.get('token')?.value;
  
  // Note: localStorage n'est pas accessible server-side
  // Solution: Client component wrapper qui fait la vérification
  
  return (
    <div className="flex h-screen bg-background">
      {/* Sidebar + Header + Main seront gérés par client components */}
      {children}
    </div>
  );
}

⚠️ IMPORTANT: localStorage est client-only. Pour un auth check server-side, il faut utiliser httpOnly cookies. En attendant, le pattern actuel utilise un client component qui vérifie le token au mount.

Alternative simple (client-side auth):

// frontend/src/app/dashboard/DashboardLayoutClient.tsx
'use client';

import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { DashboardSidebar } from './DashboardSidebar';
import { DashboardHeader } from './DashboardHeader';

export function DashboardLayoutClient({ children }: { children: React.ReactNode }) {
  const router = useRouter();
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
    const token = localStorage.getItem('token');
    if (!token) {
      router.push('/auth/login?redirect=/dashboard');
    }
  }, [router]);

  if (!mounted) return null; // Prevent hydration mismatch

  return (
    <div className="flex h-screen bg-background">
      <DashboardSidebar />
      <div className="flex flex-1 flex-col overflow-hidden">
        <DashboardHeader />
        <main className="flex-1 overflow-y-auto">{children}</main>
      </div>
    </div>
  );
}

🚨 Anti-Patterns à Éviter

  1. NE PAS hardcoder l'URL: fetch("http://localhost:8000/api/auth/me") → utiliser apiClient.get('/api/v1/auth/me')
  2. NE PAS utiliser l'ancienne API: /api/auth/me → utiliser /api/v1/auth/me
  3. NE PAS dupliquer le user fetch dans sidebar ET header → utiliser un seul useUser() hook avec TanStack Query (cache partagé)
  4. NE PAS créer un fichier Layout.tsx — app router exige layout.tsx en minuscules
  5. NE PAS afficher Glossaries aux users Free → condition user.tier === "pro"
  6. NE PAS utiliser "文A" comme logo → utiliser <Languages /> de Lucide
  7. NE PAS utiliser "Translate Co." → utiliser "Office Translator"

📊 Token Storage (cohérence avec Stories 4.3/4.4)

// Logout handler
const handleLogout = () => {
  localStorage.removeItem('token');
  localStorage.removeItem('refresh_token');
  localStorage.removeItem('user');
  router.push('/');
};

🔍 Composants Réutilisables Existants

Composant Chemin Usage
apiClient @/lib/apiClient Toutes requêtes HTTP
Button @/components/ui/button variant="ghost", size="sm"
Avatar @/components/ui/avatar User avatar
Badge @/components/ui/badge Tier badge (variant="secondary")
Separator @/components/ui/separator Dividers
Lucide icons lucide-react Languages, Key, BookText, etc.

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

Stories précédentes (4.3, 4.4) ont établi:

  • Pattern colocation: Composants/hooks/types dans le dossier de la route
  • useMutation pour mutations (login, register)
  • useQuery pour data fetching
  • apiClient pour toutes les requêtes HTTP
  • Branding: Languages icône + "Office Translator"
  • Token storage: localStorage (token, refresh_token)
  • Redirect pattern: /auth/login?redirect=/dashboard

Dashboard Layout DOIT suivre le même pattern.

📁 Fichiers Existants à Modifier/Supprimer

Fichier Action
frontend/src/app/(app)/layout.tsx À remplacer par nouveau dashboard layout
frontend/src/app/(app)/dashboard/page.tsx À simplifier (supprimer branding "Translate Co.", supprimer fetch hardcodé)
frontend/src/components/sidebar.tsx À CONSERVER — utilisé pour les pages hors dashboard (landing, translate)

Note: Le nouveau dashboard layout dans frontend/src/app/dashboard/ est séparé de l'ancien frontend/src/app/(app)/. Le dashboard devient une route dédiée.

🧪 Tests à Effectuer

  1. Build: npm run build dans frontend/ → 0 erreurs TypeScript/ESLint
  2. Sans token: Accès /dashboard → redirection /auth/login?redirect=/dashboard
  3. Avec token Free: Sidebar affiche Overview + API Keys (pas Glossaries)
  4. Avec token Pro: Sidebar affiche Overview + API Keys + Glossaries
  5. Logout: localStorage cleared → redirection /
  6. Mobile: Menu hamburger fonctionne, drawer s'ouvre/ferme
  7. Branding: Vérifier "Office Translator" et icône Languages (pas "Translate Co." / "文A")
  8. User info: Avatar, nom, email, tier corrects

Project Structure Notes

Fichiers à créer:

  • frontend/src/app/dashboard/layout.tsx — Layout avec auth check
  • frontend/src/app/dashboard/DashboardLayoutClient.tsx — Client wrapper (optionnel si layout.tsx fait le check)
  • frontend/src/app/dashboard/DashboardSidebar.tsx — Sidebar component
  • frontend/src/app/dashboard/DashboardHeader.tsx — Header component
  • frontend/src/app/dashboard/useUser.ts — Hook fetch user
  • frontend/src/app/dashboard/types.ts — User interface

Fichiers à modifier:

  • frontend/src/app/(app)/dashboard/page.tsx — À simplifier

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

References

  • [Source: _bmad-output/planning-artifacts/epics.md#Story-4.5] — 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/dashboard-sidebar.tsx] — UI sidebar à copier
  • [Source: office-translator-landing-page/components/dashboard-header.tsx] — UI header à copier
  • [Source: office-translator-landing-page/app/dashboard/page.tsx] — Layout structure à copier
  • [Source: frontend/src/components/sidebar.tsx] — Logique logout, user section
  • [Source: frontend/src/app/auth/login/useLogin.ts] — Pattern token storage + redirect
  • [Source: frontend/src/lib/apiClient.ts] — Client API centralisé
  • [Source: _bmad-output/implementation-artifacts/4-4-page-register.md] — Pattern colocation, branding

Dev Agent Record

Agent Model Used

Claude (Anthropic)

Debug Log References

  • Build initial: erreur de conflit de routes (deux dashboard pages)
  • Solution: supprimé l'ancien dossier frontend/src/app/(app)/dashboard/
  • Build second: erreur TypeScript sur user.tier non typé
  • Solution: ajouté typage explicite UseQueryResult<User, ApiClientError> au hook useUser
  • Build troisième: erreur TypeScript sur response.data
  • Solution: corrigé le type générique de apiClient.get<User> (pas { data: User })

Completion Notes List

  • Créé types.ts avec interface User (id, email, name, tier, created_at)
  • Créé useUser.ts avec useQuery pour fetch /api/v1/auth/me, gestion 401 avec redirect
  • Créé DashboardSidebar.tsx avec navigation conditionnelle (Glossaries pour Pro uniquement), user section avec avatar/initiales, logout handler
  • Créé DashboardHeader.tsx avec mobile hamburger menu, desktop header avec badge tier, mobile drawer navigation
  • Créé DashboardLayoutClient.tsx avec auth check client-side, loading state
  • Créé layout.tsx server component qui wrappe DashboardLayoutClient
  • Créé page.tsx avec overview simplifié, quick action cards, upgrade prompt pour Free users
  • Ajouté @radix-ui/react-avatar aux dépendances
  • Ajouté avatar.tsx aux composants UI (copié depuis office-translator-landing-page)
  • Supprimé l'ancien frontend/src/app/(app)/dashboard/ pour éviter conflit de routes
  • Build Next.js réussi avec 0 erreurs TypeScript

File List

Created:

  • frontend/src/app/dashboard/types.ts
  • frontend/src/app/dashboard/useUser.ts
  • frontend/src/app/dashboard/useLogout.ts (fix DRY - hook logout partagé)
  • frontend/src/app/dashboard/constants.ts (fix DRY - navigation items partagés)
  • frontend/src/app/dashboard/utils.ts (fix DRY - getInitials utility)
  • frontend/src/app/dashboard/DashboardSidebar.tsx
  • frontend/src/app/dashboard/DashboardHeader.tsx
  • frontend/src/app/dashboard/DashboardLayoutClient.tsx
  • frontend/src/app/dashboard/layout.tsx
  • frontend/src/app/dashboard/page.tsx
  • frontend/src/components/ui/avatar.tsx
  • frontend/src/test/utils.test.ts (tests unitaires)
  • frontend/src/test/constants.test.ts (tests unitaires)

Deleted:

  • frontend/src/app/(app)/dashboard/ (entire folder)

Modified:

  • frontend/package.json (added @radix-ui/react-avatar)

Change Log

  • 2026-02-23: Implementation complete - Dashboard layout with sidebar, header, auth check, responsive design, Pro feature gating
  • 2026-02-23: Code Review Complete - All HIGH/MEDIUM issues fixed

Senior Developer Review (AI)

Reviewer: Claude Code Review Agent
Date: 2026-02-23
Issues Found: 2 HIGH, 4 MEDIUM, 1 LOW
Issues Fixed: 6/6 (100%)

🔴 HIGH - Fixed

  1. AC#5 Non implémenté - Pas de Server Component Auth Check

    • Fichier: layout.tsx
    • Problème: Aucune vérification server-side du token
    • Fix: Ajout import cookies from 'next/headers' + vérification token côté serveur avec redirect si absent
    • Commit: layout.tsx now validates auth server-side before rendering
  2. Typage API potentiellement incorrect

    • Fichier: useUser.ts
    • Problème: Vérification du type de retour API
    • Fix: Le typage était correct - apiClient.get<User> retourne ApiResponse<User> avec response.data de type User
    • Note: Aucun changement requis

🟡 MEDIUM - Fixed

  1. Violation DRY - Navigation items dupliqués

    • Fichiers: DashboardSidebar.tsx, DashboardHeader.tsx
    • Fix: Créé constants.ts avec baseNavItems, proNavItem, et getNavItems() helper
  2. Fonction getInitials dupliquée

    • Fichiers: DashboardSidebar.tsx, DashboardHeader.tsx
    • Fix: Créé utils.ts avec getInitials() + gestion edge cases (empty string, undefined)
  3. Logique logout dupliquée

    • Fichiers: DashboardSidebar.tsx, DashboardHeader.tsx
    • Fix: Créé useLogout.ts hook partagé
  4. Aucun test automatisé

    • Fix: Créé frontend/src/test/utils.test.ts et constants.test.ts avec tests Vitest

🟢 LOW - Acknowledged

  1. 'use client' dans page.tsx
    • Note: Acceptable pour cette iteration - le data fetching client-side est cohérent avec l'architecture TanStack Query

Fichiers Créés pendant la Review

  • frontend/src/app/dashboard/useLogout.ts
  • frontend/src/app/dashboard/constants.ts
  • frontend/src/app/dashboard/utils.ts
  • frontend/src/test/utils.test.ts
  • frontend/src/test/constants.test.ts

Fichiers Modifiés pendant la Review

  • frontend/src/app/dashboard/layout.tsx - Server-side auth check
  • frontend/src/app/dashboard/DashboardSidebar.tsx - Refactor DRY
  • frontend/src/app/dashboard/DashboardHeader.tsx - Refactor DRY

Build Verification

npm run build - 0 erreurs TypeScript
Architecture respectée (colocation pattern)
All ACs now properly implemented