16 KiB
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
- Vérifier Node :
node -v(≥ 20 attendu). - Activer pnpm :
(ou installer globalement avec
corepack enable pnpm -vnpm install -g pnpmsi corepack indisponible.) - 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/ :
pnpm create next-app@latest . \
--typescript \
--tailwind \
--eslint \
--app \
--src-dir=false \
--import-alias "@/*" \
--turbopack \
--use-pnpm
Contrôles :
pnpm devpuis ouvrirhttp://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.mdprésent → déplacerGOAL.mdtemporairement hors du dossier racine si le scaffolder refuse, puis le remettre).
Étape 2 — Nettoyage du scaffold
- Vider
app/page.tsx(remplacer par un composant squeletteCvWorkspaceminimal). - Supprimer les assets de démo de
public/(vercel.svg,next.svg, etc.) sauf ce qui est utile. - Nettoyer
app/globals.cssdu style de démo : ne conserver que@import "tailwindcss";et les tokens/print CSS ajoutés plus tard. - Vérifier
pnpm dev→ page vide sans erreur.
Étape 3 — Configuration TypeScript strict
Dans tsconfig.json :
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true,
"paths": { "@/*": ["./*"] }
}
}
Ajouter un script de vérification dans package.json :
"typecheck": "tsc --noEmit"
Contrôle : pnpm typecheck → aucune erreur.
Étape 4 — Installation des dépendances
pnpm add marked dompurify lucide-react
pnpm add @serwist/next # PWA (plugin + runtime)
DevDependencies :
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
createLocalStorageAdapteravec 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 :
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 :
dompurifydépend dewindow. Ne l'utiliser que dans des Client Components (bilan : jamais importé dansapp/layout.tsxou 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 :
CvInputPanel.tsx— colonne gauche : gèreactiveTab(useState local), titre « Contenu du CV », composeCvTabs+MarkdownTextarea+ExperienceManager. Reçoitdata, les callbacks deuseCVDataen props.CvTabs.tsx— rend viaCV_TABS/CV_TAB_ORDER(module 03) : boutonsnumero + label, état actif, aria.MarkdownTextarea.tsx—label+Textareacontrôlé + lien « Aide Markdown ».ExperienceManager.tsx— formulaire (titre + contenu), bouton +, listeExperienceItem, gestion asynchrone des modes (closed/new/edit).ExperienceItem.tsx— lignen. 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/ :
CvPreview.tsx— wrapper feuille (dimensions210mm/297mm, ombre, classe d'impression) ; appellemarkdownToHtmldepuislib/markdownpour les 6 zones + chaque expérience ; compose header/colonnes.CvHeader.tsx— accroche (zone 1).CvLeftColumn.tsx— 5 ×CvSection(État civil, Formations, Compétences, Soft skills, Centres d'intérêt).CvSection.tsx— titre stylé +dangerouslySetInnerHTML(HTML assaini) ; masquée si vide.CvRightColumn.tsx— titre « Expériences professionnelles » + listeCvExperiencePreview+ message vide alternatif (print:hidden).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 àCvInputPaneletdataàCvPreview; - compose
CvToolbarau-dessus, grillegrid-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 :
import { CvWorkspace } from "@/components/cv/CvWorkspace";
export default function HomePage() {
return <CvWorkspace />;
}
Contrôle : pnpm dev → éditeur temps réel fonctionnel, persistance au rechargement.
Étape 12 — Impression PDF
Dans app/globals.css, ajouter :
@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 module06-, 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 :
next.config.ts(inclutoutput: "standalone"requis par le Dockerfile) :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);- Créer
app/sw.ts:import { defaultCache } from "@serwist/next/worker";+precacheAndRoute/registerRoutesur/,/documentation,/mentions-legales, icônes, manifest. - Créer
public/manifest.webmanifest(name, short_name, start_url/, displaystandalone, theme_color, icons 192/512). - 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). app/layout.tsx: ajouter<link rel="manifest" href="/manifest.webmanifest">, 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-labelmanquants, - Ajout du bouton « Réinitialiser le CV » (optionnel, appelle
resetCVaprè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 dossierdocs/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
Prérequis : Docker ou Docker Desktop installé, compte Docker Hub créé (docker login une seule fois).
17.1 Créer .dockerignore
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
# ── É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
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
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
# 1. Se connecter au registre
docker login
# 2. Taguer (remplacer <mon-user-dockerhub> par votre identifiant)
docker tag cv-maker:latest <mon-user-dockerhub>/cv-maker:latest
docker tag cv-maker:latest <mon-user-dockerhub>/cv-maker:1.0.0
# 3. Pousser
docker push <mon-user-dockerhub>/cv-maker:latest
docker push <mon-user-dockerhub>/cv-maker:1.0.0
Contrôle : l'image apparaît sur la page Docker Hub du compte (https://hub.docker.com/r/<mon-user-dockerhub>/cv-maker).
17.6 Déploiement depuis Docker Hub
# Sur une autre machine
docker pull <mon-user-dockerhub>/cv-maker:latest
docker run -d -p 3000:3000 --name cv-maker <mon-user-dockerhub>/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 |