418 lines
16 KiB
Markdown
418 lines
16 KiB
Markdown
# 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 <CvWorkspace />;
|
||
}
|
||
```
|
||
|
||
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 `<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-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 <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
|
||
|
||
```bash
|
||
# 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 | |