# 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 6 zones + chaque expérience ; compose header/colonnes.
2. `CvHeader.tsx` — accroche (zone 1).
3. `CvLeftColumn.tsx` — 5 × `CvSection` (État civil, Formations, Compétences, 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 |