# 05 — Plan de réalisation pas-à-pas > Feuille de route d'implémentation. Chaque étape produit un état du projet **vérifiable** (commande de lancement + contrôle visuel/fonctionnel). Les commandes utilisent **pnpm**. ## Étape 0 — Prérequis et vérification environnement 1. Vérifier Node : `node -v` (≥ 20 attendu). 2. Activer pnpm : ```bash corepack enable pnpm -v ``` (ou installer globalement avec `npm install -g pnpm` si corepack indisponible.) 3. Créer le répertoire de projet s'il n'existe pas. ## Étape 1 — Scaffold du projet Next.js (App Router + TypeScript) Depuis la racine `cv-maker/` : ```bash pnpm create next-app@latest . \ --typescript \ --tailwind \ --eslint \ --app \ --src-dir=false \ --import-alias "@/*" \ --turbopack \ --use-pnpm ``` Contrôles : - `pnpm dev` puis ouvrir `http://localhost:3000` (page d'accueil Next.js) ; - fichiers générés : `app/`, `components/` (facultatif), `lib/` (absent par défaut), `public/`, `next.config.ts`, `tsconfig.json`. > Si le dossier contient déjà des fichiers, le scaffolder demande. Ici il est vide (seul `GOAL.md` présent → déplacer `GOAL.md` temporairement hors du dossier racine si le scaffolder refuse, puis le remettre). ## Étape 2 — Nettoyage du scaffold 1. Vider `app/page.tsx` (remplacer par un composant squelette `CvWorkspace` minimal). 2. Supprimer les assets de démo de `public/` (`vercel.svg`, `next.svg`, etc.) sauf ce qui est utile. 3. Nettoyer `app/globals.css` du style de démo : ne conserver que `@import "tailwindcss";` et les tokens/print CSS ajoutés plus tard. 4. Vérifier `pnpm dev` → page vide sans erreur. ## Étape 3 — Configuration TypeScript strict Dans `tsconfig.json` : ```json { "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, "forceConsistentCasingInFileNames": true, "paths": { "@/*": ["./*"] } } } ``` Ajouter un script de vérification dans `package.json` : ```json "typecheck": "tsc --noEmit" ``` Contrôle : `pnpm typecheck` → aucune erreur. ## Étape 4 — Installation des dépendances ```bash pnpm add marked dompurify lucide-react pnpm add @serwist/next # PWA (plugin + runtime) ``` DevDependencies : ```bash pnpm add -D @types/dompurify ``` Contrôle : `pnpm typecheck` et `pnpm lint` restent verts après édition du package.json si besoin. ## Étape 5 — Types du domaine Créer `lib/types/cv.ts` avec le contenu du module `03-` (contrats `CVTabId`, `CV_TAB_ORDER`, `CV_TABS`, `Experience`, `CVCareerData`, `createDefaultCVData`). Contrôle : `pnpm typecheck` sans erreur. ## Étape 6 — Couche de stockage Créer `lib/storage.ts` : - constante `STORAGE_KEY = "cv-maker:v1"` ; - interface `CVStorageAdapter` (`load`, `save`, `clear`) ; - implémentation `createLocalStorageAdapter` avec try/catch (JSON invalide → `null`, quota → levée d'erreur remontée). Créer `lib/hooks/useLocalStorage.ts` : hook générique (`value`, `setValue`, `status`) — écriture dans `useEffect`, initialisation paresseuse et garde `typeof window`. Créer `lib/hooks/useCVData.ts` : façade `setText`, `addExperience`, `updateExperience`, `removeExperience`, `resetCV` (comportements du module `03-` §6). Contrôle : `pnpm typecheck` vert. ## Étape 7 — Rendu Markdown sécurisé Créer `lib/markdown.ts` : ```ts import { marked } from "marked"; import DOMPurify from "dompurify"; marked.setOptions({ gfm: true, breaks: true }); export function markdownToHtml(source: string): string { const rawHtml = marked.parse(source, { async: false }) as string; return DOMPurify.sanitize(rawHtml); } ``` > **DOMPurify côté client uniquement** : `dompurify` dépend de `window`. Ne l'utiliser que dans des Client Components (bilan : jamais importé dans `app/layout.tsx` ou pages statiques). Contrôle : `pnpm typecheck` vert ; petit test manuel dans un `useEffect` de la page principale. ## Étape 8 — Primitives UI réutilisables Créer sous `components/ui/` : | Fichier | Rôle | | --- | --- | | `Button.tsx` | variantes `primary` / `ghost` / `danger` / `icon`, support `variant` + `size` | | `IconButton.tsx` | bouton icône avec `aria-label` obligatoire en prop | | `Textarea.tsx` | textarea contrôlé stylé (mono, `resize-y`) | | `Tabs.tsx` | primitive d'onglets accessible (gère `role`/`aria-selected`, callbacks) | Contrôle : intégration temporaire dans la page, `pnpm dev` + `pnpm typecheck`. ## Étape 9 — Composants du panneau gauche Créer `components/cv/` dans cet ordre : 1. `CvInputPanel.tsx` — colonne gauche : gère `activeTab` (useState local), titre « Contenu du CV », compose `CvTabs` + `MarkdownTextarea` + `ExperienceManager`. Reçoit `data`, les callbacks de `useCVData` en props. 2. `CvTabs.tsx` — rend via `CV_TABS`/`CV_TAB_ORDER` (module 03) : boutons `numero + label`, état actif, aria. 3. `MarkdownTextarea.tsx` — `label` + `Textarea` contrôlé + lien « Aide Markdown ». 4. `ExperienceManager.tsx` — formulaire (titre + contenu), bouton +, liste `ExperienceItem`, gestion asynchrone des modes (`closed/new/edit`). 5. `ExperienceItem.tsx` — ligne `n. titre`, boutons Modifier / Supprimer, confirmation inline. Contrôle visuel : saisir du texte dans chaque onglet, ajouter/modifier/supprimer des expériences (état React OK avant même la preview). ## Étape 10 — Composants de la preview A4 Créer dans `components/cv/` : 1. `CvPreview.tsx` — wrapper feuille (dimensions `210mm`/`297mm`, ombre, classe d'impression) ; appelle `markdownToHtml` depuis `lib/markdown` pour les 5 zones + chaque expérience ; compose header/colonnes. 2. `CvHeader.tsx` — accroche (zone 1). 3. `CvLeftColumn.tsx` — 4 × `CvSection` (État civil, Formations, Soft skills, Centres d'intérêt). 4. `CvSection.tsx` — titre stylé + `dangerouslySetInnerHTML` (HTML assaini) ; masquée si vide. 5. `CvRightColumn.tsx` — titre « Expériences professionnelles » + liste `CvExperiencePreview` + message vide alternatif (`print:hidden`). 6. `CvExperiencePreview.tsx` — titre en gras + HTML assaini du contenu. Contrôle : remplir le panneau gauche → vérifier le rendu temps réel dans la feuille (structure 1/3–2/3, header). ## Étape 11 — Assemblage de la page principale Créer `components/cv/CvWorkspace.tsx` : - `"use client"` ; - appelle `useCVData()` ; - « remonte l'état » : passe `data` + callbacks à `CvInputPanel` et `data` à `CvPreview` ; - compose `CvToolbar` au-dessus, grille `grid-cols-[1fr_2fr]` (desktop), stacking < 1024 px. Créer `components/cv/CvToolbar.tsx` : logo + bouton « Imprimer / PDF » (`window.print()`), boutons « Importer JSON » puis « Exporter JSON » (import via fichier validé, téléchargement via `Blob`), bouton « Réinitialiser », lien `/documentation`. Créer `components/ui/Footer.tsx` (lié dans le layout, `print-hidden`) : nom de l'app + lien **Mentions légales**. Remplacer le contenu de `app/page.tsx` : ```tsx import { CvWorkspace } from "@/components/cv/CvWorkspace"; export default function HomePage() { return ; } ``` Contrôle : `pnpm dev` → éditeur temps réel fonctionnel, persistance au rechargement. ## Étape 12 — Impression PDF Dans `app/globals.css`, ajouter : ```css @page { size: A4; margin: 0; } @media print { body { background: #ffffff; } .print-hidden { display: none !important; } .cv-sheet { box-shadow: none; border: 0; margin: 0; width: auto; height: auto; /* hauteur max nécessaire si le contenu dépasse une page */ } } ``` Classes utilitaires : panneau gauche et toolbar marqués `print-hidden` (ou `print:hidden` de Tailwind via `print:` variant si configuré). Contrôle : bouton « Imprimer / PDF » → aperçu ne contenant que la feuille A4 ; « Enregistrer en PDF » → fichier propre. ## Étape 13 — Pages Documentation et Mentions légales - `app/documentation/page.tsx` : **Server Component** statique. Contenu : prise en main (tabs, expériences, impression, sauvegarde locale) + aide sur la syntaxe Markdown supportée (titres, gras, italique, listes, liens, code) illustrée d'exemples. - `app/mentions-legales/page.tsx` : **Server Component** statique. Contenu conforme au module `06-`, sans paragraphe contact. Ajouter un `Link` vers `/documentation` (via `CvToolbar`) et vers `/mentions-legales` (via le `Footer`), plus un en-tête de retour « ← Retour à l'éditeur » sur chaque page. Contrôle : navigation depuis la toolbar (Documentation) et le footer (Mentions légales), `pnpm build` → routes générées statiques (`○` dans la sortie de build). ## Étape 14 — PWA (manifest + service worker) Avec `@serwist/next` : 1. `next.config.ts` (inclut `output: "standalone"` requis par le Dockerfile) : ```ts import withSerwistInit from "@serwist/next"; const nextConfig = { output: "standalone" }; const withSerwist = withSerwistInit({ swSrc: "app/sw.ts", swDest: "public/sw.js", disable: process.env.NODE_ENV === "development", }); export default withSerwist(nextConfig); ``` 2. Créer `app/sw.ts` : `import { defaultCache } from "@serwist/next/worker";` + `precacheAndRoute`/`registerRoute` sur `/`, `/documentation`, `/mentions-legales`, icônes, manifest. 3. Créer `public/manifest.webmanifest` (name, short_name, start_url `/`, display `standalone`, theme_color, icons 192/512). 4. Créer les icônes PNG dans `public/icons/` (192, 512) — génération locale (outil image, ou conversion SVG d'une icône proche du logo). 5. `app/layout.tsx` : ajouter ``, theme-color, description, icône apple-touch si souhaité. Contrôle : `pnpm build` sans erreur ; en production (`pnpm start`), onglet Lighthouse/PWA → service worker actif, installable. ## Étape 15 — Réglages finaux / polish - Espacements de la grille (gap), tailles de police du CV, comportement « feuille qui s'allonge », - Sauvegarde locale silencieuse (pastille de statut non retenue — inutile vu l'enregistrement automatique), - États de focus, `aria-label` manquants, - Ajout du bouton « Réinitialiser le CV » (optionnel, appelle `resetCV` après confirmation), - Vérification des limites : contenu vide, expérience sans titre (titre par défaut « Expérience »), - Mise à jour de `README.md` (documentation technique) et du dossier `docs/conception`. ## Étape 16 — Recette finale selon le GOAL.md Exécuter la checklist complète du module `07-plan-tests.md`. Corriger toute anomalie, re-exécuter `pnpm lint` et `pnpm typecheck` (zéro erreur). ## Étape 17 — Dockerisation et publication sur Docker Hub > Spécification complète : [`08-deploiement-docker.md`](./08-deploiement-docker.md) **Prérequis** : Docker ou Docker Desktop installé, compte Docker Hub créé (`docker login` une seule fois). ### 17.1 Créer `.dockerignore` ```gitignore node_modules .next out .git .gitignore docs *.md Dockerfile docker-compose.yml .dockerignore *.log .env* .DS_Store ``` Contrôle : le contexte de build (`docker build .`) ne contient plus que les fichiers de source (app/, components/, lib/, public/, next.config.ts, tsconfig.json, package.json, pnpm-lock.yaml). ### 17.2 Créer `Dockerfile` ```dockerfile # ── Étape de dépendances ──────────────────────────────────────── FROM node:20-alpine AS deps RUN corepack enable WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile # ── Étape de build ───────────────────────────────────────────── FROM node:20-alpine AS builder RUN corepack enable WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN pnpm build # ── Étape d'exécution ────────────────────────────────────────── FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production ENV HOSTNAME=0.0.0.0 ENV PORT=3000 COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/public ./public EXPOSE 3000 CMD ["node", "server.js"] ``` Contrôle : aucune erreur au build. ### 17.3 Créer `docker-compose.yml` ```yaml services: cv-maker: build: context: . dockerfile: Dockerfile image: cv-maker:local container_name: cv-maker restart: unless-stopped ports: - "3000:3000" environment: - HOSTNAME=0.0.0.0 - PORT=3000 healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:3000"] interval: 30s timeout: 5s retries: 3 start_period: 10s ``` Contrôle : `docker compose up -d` → conteneur healthy, application accessible sur `http://localhost:3000`. ### 17.4 Build de l'image et test local ```bash docker build -t cv-maker:latest . docker run --rm -p 3000:3000 cv-maker:latest # → ouvrir http://localhost:3000 et tester l'éditeur ``` Vérifications spécifiques conteneur : - Le service worker (`sw.js`) est chargé (PWA fonctionne). - La sauvegarde locale est fonctionnelle (localStorage côté navigateur). - `docker images cv-maker` : taille < 200 Mo. ### 17.5 Publication sur Docker Hub ```bash # 1. Se connecter au registre docker login # 2. Taguer (remplacer par votre identifiant) docker tag cv-maker:latest /cv-maker:latest docker tag cv-maker:latest /cv-maker:1.0.0 # 3. Pousser docker push /cv-maker:latest docker push /cv-maker:1.0.0 ``` Contrôle : l'image apparaît sur la page Docker Hub du compte (`https://hub.docker.com/r//cv-maker`). ### 17.6 Déploiement depuis Docker Hub ```bash # Sur une autre machine docker pull /cv-maker:latest docker run -d -p 3000:3000 --name cv-maker /cv-maker:latest # ou via compose (échanger image: dans docker-compose.yml) docker compose up -d ``` ## Récapitulatif des fichiers à créer/modifier ``` créés app/page.tsx (réécrit) app/documentation/page.tsx app/mentions-legales/page.tsx app/sw.ts app/layout.tsx (modifié : manifest, meta) app/globals.css (modifié : tokens + @media print) lib/types/cv.ts lib/markdown.ts lib/storage.ts lib/hooks/useLocalStorage.ts lib/hooks/useCVData.ts components/ui/{Button,IconButton,Textarea,Tabs}.tsx components/cv/{CvWorkspace,CvToolbar,CvInputPanel,CvTabs, MarkdownTextarea,ExperienceManager,ExperienceItem, CvPreview,CvHeader,CvLeftColumn,CvSection, CvRightColumn,CvExperiencePreview}.tsx public/manifest.webmanifest public/icons/icon-192.png, public/icons/icon-512.png Dockerfile (multi-stage : deps/builder/runner) docker-compose.yml (service cv-maker, healthcheck) .dockerignore next.config.ts (modifié : withSerwist + output: "standalone") tsconfig.json (modifié : strict) package.json (modifié : scripts + deps) modifié README.md (documentation technique) ``` ## Jalons de contrôle qualité | Jalon | État | Contrôle | | --- | --- | --- | | J1 (fin étape 4) | Deps installées | `pnpm typecheck`, `pnpm lint` | | J2 (fin étape 11) | MVP fonctionnel | édition temps réel + persistance | | J3 (fin étape 12) | PDF ok | impression → PDF propre | | J4 (fin étape 13) | Pages statiques | routes OK, build ○ | | J5 (fin étape 14) | PWA | installable, offline | | J6 (fin étape 16) | Recette GOAL.md | 100 % critères passés | | J7 (fin étape 17) | Docker + DockerHub | image < 200 Mo, `docker compose up`, image sur Docker Hub |