chore init

This commit is contained in:
devcodetools committed 2026-09-16 21:34:09 +02:00
1 parent 1b7406b1d3
commit a0d312a884
60 files changed
+5046 -125

No files matched your search

+145
View File
@@ -0,0 +1,145 @@
# 01 — Analyse fonctionnelle
## 1. Contexte et objectif
CV-Maker est une application web **sans backend** permettant à un utilisateur de créer son CV rapidement, en saisissant le contenu en **Markdown** dans des zones de saisie et en visualisant le résultat en **temps réel** sur une feuille au **format A4**.
- **Utilisateur cible** : une personne unique, sur poste de travail (desktop).
- **Aucune donnée n'est envoyée à un serveur** : tout est stocké dans le navigateur (localStorage).
- **Notation (ECF CDA)** : le projet sert de support pédagogique et doit démontrer : maîtrise de Next.js/React, composants réutilisables, gestion d'état, persistance locale, sécurité de rendu, capacité de documentation.
## 2. Périmètre fonctionnel
### 2.1 Fonctions principales (issue du GOAL.md)
| Réf. | Fonction | Description |
| --- | --- | --- |
| F1 | Éditeur temps réel | Saisie Markdown dans le panneau gauche → rendu HTML temps réel dans le panneau droit |
| F2 | Preview A4 | Panneau droit matérialisé en feuille A4 (210 × 297 mm), impeccable à l'impression |
| F3 | Persistance | Sauvegarde automatique dans localStorage à chaque frappe ; données restaurées au rechargement |
| F4 | Impression PDF | Bouton d'impression → export PDF via la boîte de dialogue du navigateur |
| F5 | Gestion des expériences | Ajout / modification / suppression d'expériences professionnelles |
| F6 | Page documentation | Page d'aide expliquant l'utilisation et la syntaxe Markdown |
| F7 | Page Mentions légales | Page d'information « mentions légales / vie privée » |
| F8 | PWA | Application web installable hors-ligne (nice to have côté objectif, prévue au cahier des charges) |
### 2.2 Responsive / device
- **Desktop first** : l'expérience est optimisée pour un écran large.
- **Tablette et mobile : nice to have** ; minimum acceptable = interface utilisable sans casse majeure. Pas de refonte dédiée.
## 3. Maquette fonctionnelle de l'écran principal `/`
### 3.1 Agencement général
```
┌──────────────────────────────────────────────────────────────┐
│ Barre d'application (logo, titre, boutons : imprimer, doc) │
├───────────────────────────────┬──────────────────────────────┤
│ Panneau GAUCHE │ Panneau DROIT │
│ 1/3 de la largeur │ 2/3 de la largeur │
│ │ │
│ [Tabs 1 ▸ 2 ▸ 3 ▸ 4 ▸ 5] │ ┌────────────────────────┐ │
│ │ │ Feuille A4 │ │
│ [MarkdownTextarea : │ │ ┌────────────────────┐ │ │
│ zone active] │ │ │ Header accroche │ │ │
│ │ │ └────────────────────┘ │ │
│ ── SÉPARATEUR ── │ │ ┌───────┬────────────┐ │ │
│ [Zone EXPÉRIENCES] │ │ │ 1/3 │ 2/3 │ │ │
│ [+ Ajouter] │ │ │ Etat │ Expériences│ │ │
│ • 1. Titre exp [Modif] [×] │ │ │ civil │ profession.│ │ │
│ • 2. Titre exp [Modif] [×] │ │ │ Forma-│ ... │ │ │
│ │ │ │ tions │ │ │ │
│ │ │ │ Soft │ │ │ │
│ │ │ │ skills│ │ │ │
│ │ │ │ Centr.│ │ │ │
│ │ │ │ intérêt│ │ │ │
│ │ │ └───────┴────────────┘ │ │
│ │ └────────────────────────┘ │
└───────────────────────────────┴──────────────────────────────┘
```
### 3.2 Panneau latéral gauche — saisie
#### Structure
- **Zones 1 à 5** gérées par un **système d'onglets (tabs)** numérotés de 1 à 5. Un seul onglet actif à la fois ; chaque onglet affiche une `textarea` Markdown dédiée.
| N° onglet | Zone | Champ(s) du CV associé(s) | Préfixe de navigation |
| --- | --- | --- | --- |
| 1 | Message d'accroche | En-tête du CV (sous le nom/poste) | |
| 2 | État civil + contacts | Nom, prénom, adresse, e-mail, téléphone, **site web**, **lien GitHub** | |
| 3 | Formations | Liste des formations / diplômes | |
| 4 | Soft skills | Compétences comportementales | |
| 5 | Centres d'intérêt | Loisirs, activités | |
- **Zone « Expériences »** : fonctionne **hors du système d'onglets**. Elle est toujours visible sous la zone d'onglets. Elle sert à saisir **toutes les expériences professionnelles**, une par une.
#### Interactions zone expériences
1. **Bouton `+`** : ajoute une nouvelle expérience.
2. Une **liste numérotée** s'affiche sous la zone de saisie : chaque entrée affiche le titre de l'expérience avec deux actions :
- **`Modifier`** (bouton édition) : recharge le contenu de l'expérience dans la zone de saisie pour modification ;
- **`×`** (bouton suppression) : supprime l'expérience (avec confirmation).
3. Toute nouvelle expérience s'ajoute **en bas** de la section « Expériences professionnelles » du CV : l'ordre d'affichage suit **l'ordre de saisie** (la 1ʳᵉ expérience ajoutée reste en haut, numérotée 1).
4. La liste est numérotée dans l'ordre d'affichage (1, 2, 3… correspondant à la position dans le CV).
### 3.3 Panneau latéral droit — preview A4
- Une seule feuille blanche au **format A4** (210 × 297 mm), avec une marge définie, représentant 1 page de CV.
- **Header** (toute la largeur de la feuille) : rendu de l'**onglet 1 — message d'accroche**.
- **Colonne gauche (1/3 de la largeur utile)** : rendus des sections correspondant aux **onglets 2 à 5** :
- État civil (zone 2) : contacts, site web, GitHub ;
- Formations (zone 3) ;
- Soft skills (zone 4) ;
- Centres d'intérêt (zone 5).
- **Colonne droite (2/3 de la largeur utile)** : rendu des **expériences professionnelles**, sous le titre fixe **« Expériences professionnelles »**.
- Le rendu est **temps réel** : toute frappe dans une zone de saisie met à jour la zone cible correspondante instantanément.
### 3.4 Barre d'application (en-tête de l'application)
Éléments minimaux :
- Nom applicatif « CV-Maker » / logo ;
- Bouton **Imprimer / PDF** (déclenche `window.print()`) ;
- Boutons **Importer JSON** puis **Exporter JSON** (partage de CV entre appareils) ;
- Bouton **Réinitialiser** (après confirmation) ;
- Lien vers la page **Documentation** ;
- Pied de page : lien vers la page **Mentions légales** ;
- Sauvegarde automatique silencieuse (aucun indicateur de statut — inutile vu l'enregistrement à chaque frappe).
## 4. Pages
| Route | Rôle |
| --- | --- |
| `/` | Éditeur principal (2 colonnes) |
| `/documentation` | Aide : prise en main + rappel syntaxe Markdown supportée |
| `/mentions-legales` | Informations vie privée / traitement des données (localOnly) |
## 5. Contraintes transverses
- **Desktop first** : design optimisé ≥ 1024 px ; grille responsive en repli simple (colonnes empilées) sous 768 px.
- **No backend** : aucune API, aucun appel réseau. Les pages `/documentation` et `/mentions-legales` sont statiques.
- **Rendu Markdown sécurisé** : HTML produit par `marked` puis **assaini par DOMPurify** avant injection.
- **Performance de rendu** : rendu Markdown synchrone (contenus de taille « CV »), tolérable en temps réel.
- **Persistance robuste** : écriture localStorage à chaque frappe de la zone active, lecture au chargement, gestion des erreurs (quota, navigateur bloqué, JSON corrompu).
## 6. Règles de gestion (résumé formalisé)
| Règle | Description |
| --- | --- |
| RG1 | Toute modification d'un champ (textarea ou liste expériences) est persistée immédiatement dans localStorage. |
| RG2 | Au chargement, si une sauvegarde existe, l'état de l'application est restauré (toutes les zones). |
| RG3 | Si aucune sauvegarde ou sauvegarde corrompue/non compatible → données par défaut (zones vides), sauvegarde initiale créée. |
| RG4 | Un seul onglet actif ; la navigation entre onglets ne modifie pas les contenus des autres onglets. |
| RG5 | L'ajout d'une expérience place cette expérience en tête de la preview « Expériences professionnelles ». |
| RG6 | Chaque expérience possède un identifiant unique stable (UUID) — jamais l'index de liste. |
| RG7 | L'impression n'interrompt pas la persistance ; le rendu imprimé = rendu de la feuille A4 uniquement (les panneaux d'édition sont masqués en impression). |
| RG8 | Le HTML saisi (via Markdown brut) est neutralisé s'il contient du JavaScript/sans sécurisation → DOMPurify obligatoire avant `dangerouslySetInnerHTML`. |
## 7. Hors périmètre (v1)
- Multi-utilisateurs / comptes / cloud ;
- Édition collaborative ;
- Drag & drop de réorganisation des expériences (prévu comme évolution) ;
- Export multi-formats autres que PDF (Docx, JSON…) ;
- Tests automatisés (prévus en v2, cf. `07-plan-tests.md` pour la recette manuelle).
@@ -0,0 +1,216 @@
# 02 — Architecture technique
## 1. Stack technique
| Couche | Choix | Version cible | Justification |
| --- | --- | --- | --- |
| Framework | **Next.js** (App Router) | 15.x (React 19) | Standard actuel ; SSR/SSG natif, metadata, conventions de structure |
| Langage | **TypeScript** | strict mode | Typage des modèles de données, cohérence du contrat d'état, qualité CDA |
| Styles | **Tailwind CSS** | 4.x | Utility-first, tokens design, `@media print` piloté par classes |
| Markdown | **marked** | 12.x+ | Rendu Markdown → HTML, léger et synchrone, configurable |
| Assainissement | **dompurify** | 3.x | Neutralise tout contenu HTML non sûr avant injection (anti-XSS) |
| Persistance | **localStorage** (natif) | — | Aucun backend, données 100 % locales |
| PWA | **@serwist/next** (fork maintenu de `next-pwa`) | 9.x/10.x | Register + precache des pages statiques, manifest, offline |
| Icônes / UX | **lucide-react** | ≥ 0.4 | Icônes légères, cohérentes, arborescentes |
| Gestion de paquets | **pnpm** | ≥ 9 | Hooks de workspace, rapidité, espace disque (cf. choix pnpm) |
> **Rendu Markdown côté client** : les zones interactives étant des Client Components, `marked` et `dompurify` sont importés uniquement dans la couche `lib/markdown.ts` consommée par les composants client. Le bundle initial des pages statiques ne les embarque volontairement pas.
## 2. Architecture en couches
```
┌────────────────────────────────────────────────────────────┐
│ app/ (Next.js) │
│ layout.tsx · globals.css │
│ page.tsx → CvWorkspace (page principale) │
│ documentation/page.tsx → page statique │
│ mentions-legales/page.tsx → page statique │
├────────────────────────────────────────────────────────────┤
│ components/ (React, réutilisables) │
│ ui/ → primitives génériques (Button, Tabs, ...) │
│ cv/ → composants métier du CV │
├────────────────────────────────────────────────────────────┤
│ lib/ (logique pure, testable) │
│ types/cv.ts → types & constantes du domaine │
│ markdown.ts → md → html sanitized │
│ storage.ts → API localStorage + migration │
│ hooks/ → useLocalStorage, useCVData │
├────────────────────────────────────────────────────────────┤
│ public/ → manifest.webmanifest, icônes, … │
└────────────────────────────────────────────────────────────┘
```
### Règles de couches
1. `components/ui/` n'importe **jamais** `lib/storage` ni les types CV (il est générique).
2. `components/cv/` orchestre la logique (via hooks) et compose les primitives `ui/`.
3. `lib/` ne dépend **jamais** des composants ; il est pur (aucun JSX).
4. La persistance (`useLocalStorage`) est isolée dans `lib/hooks/` pour être remplaçable (autre backend plus tard) sans toucher les composants.
5. Les pages `/documentation` et `/mentions-legales` sont des **Server Components** (statiques, aucune interactivité).
## 3. Répartition client / serveur
| Élément | Côté | Raison |
| --- | --- | --- |
| `CvWorkspace`, panneaux, textareas, preview | **Client** (`"use client"`) | Interactivité temps réel, localStorage, `window` |
| `/documentation`, `/mentions-legales` | **Serveur** (SSG) | Contenu statique, zéro JS nécessaire |
| `layout.tsx`, metadata, manifest | **Serveur** | SEO/meta, PWA |
## 4. Flux de données (vue temps réel)
```
Frappe utilisateur
│
▼
CvInputPanel (textarea, onChange)
│ met à jour l'état global
▼
useCVData (useState + useLocalStorage 🔁)
│ ┌───────────────────┐
├──────── persist ▶ localStorage │ hydration au boot│
│ └───────────────────┘
▼
MarkdownTextarea / ExperienceManager mettent à jour `data`
│
▼
CvPreview (pure : props données)
│ rendu md → html via lib/markdown (marked + DOMPurify)
▼
Feuille A4 (sections positionnées, @media print)
```
- **Temps réel** : chaque frappe provoque une réécriture de l'état React → re-rendu du preview. Pas de debounce bloquant (les textes de CV sont courts) ; un `onChange` direct suffit.
- **Persistance** : synchronisée dans le même cycle (effet sur l'état) ; voir `03-modele-donnees.md`.
## 5. Détail des dossiers Next.js (App Router)
### 5.1 `app/`
| Fichier | Rôle |
| --- | --- |
| `app/layout.tsx` | Layout racine : `<html lang="fr">`, metadata (titre, description), icône, manifest PWA |
| `app/page.tsx` | Page principale : rend `CvWorkspace` |
| `app/documentation/page.tsx` | Contenu Markdown statique (aide d'utilisation) |
| `app/mentions-legales/page.tsx` | Mentions légales / vie privée |
| `components/ui/Footer.tsx` | Pied de page (lien Mentions légales, masqué à l'impression) |
| `app/globals.css` | Tokens Tailwind, styles de base, **styles d'impression @media print** |
### 5.2 `components/`
```
components/
├── ui/
│ ├── Button.tsx → variantes (primary, ghost, danger, icon)
│ ├── Tabs.tsx → primitive d'onglets (accessible, contenu piloté)
│ ├── Textarea.tsx → textarea générique stylée
│ └── IconButton.tsx → bouton icône (Modifier, Supprimer)
└── cv/
├── CvWorkspace.tsx → layout 2 colonnes + barre d'app
├── CvToolbar.tsx → barre d'application (print, export/import JSON, liens)
├── CvInputPanel.tsx → panneau gauche
│ ├── CvTabs.tsx → tabs 1..5 (labels + numéros)
│ ├── MarkdownTextarea.tsx → {label, value, onChange} (textarea md)
│ └── ExperienceManager.tsx
│ └── ExperienceItem.tsx → ligne « n. titre [Modifier] [×] »
└── CvPreview.tsx → panneau droit (feuille A4)
├── CvHeader.tsx → accroche (zone 1)
├── CvLeftColumn.tsx → 1/3 (zones 2-5)
│ └── CvSection.tsx → section générique {titre, html}
└── CvRightColumn.tsx → 2/3 (Expériences professionnelles)
└── CvExperiencePreview.tsx
```
## 6. Gestion d'état
- **État global minimal** : pas de Redux/Zustand nécessaire. Un seul état racine porté par `useCVData` (voir `03-`) et transmis par **props** (Composition). Les textareas sont **contrôlés** par cet état.
- État partagé entre `CvInputPanel` et `CvPreview` via **remontée d'état** (`CvWorkspace`).
- `ExperienceManager` gère son état local de navigation (expérience en cours d'édition, « mode ajout »).
## 7. Impression PDF
- Bouton « Imprimer / PDF » → `window.print()`.
- Feuille A4 visée via `@media print` dans `globals.css` :
- masquage du panneau gauche et de la barre d'outils (`.print:hidden`) ;
- la feuille A4 occupe la page entière : `@page { size: A4; margin: 0 }` ;
- `print-color-adjust: exact` pour conserver les couleurs d'accentuations éventuelles.
- Le PDF est produit par le navigateur (option « Enregistrer en PDF »).
## 8. PWA
- Package : `@serwist/next` (plugin Next.js officiel pour PWA).
- Config `next.config.ts` : `withSerwist({ swSrc, register: true })`.
- `sw.ts` : précache des routes `/`, `/documentation`, `/mentions-legales` + assets (icônes, manifest).
- `public/manifest.webmanifest` : `name`, `short_name`, `start_url: "/"`, `display: "standalone"`, `theme_color`, `icons` (192 et 512 px, PNG).
- Icônes générées dans `public/icons/` (192, 512, masque d'application 1024) — voir `05-`.
## 9. Scripts (package.json)
| Script | Commande |
| --- | --- |
| `dev` | `pnpm dev` — serveur de développement |
| `build` | `pnpm build` — build de production |
| `start` | `pnpm start` — démarrage prod local |
| `lint` | `pnpm lint` — ESLint (Next.js) |
| `typecheck` | `tsc --noEmit` — validation des types |
## 10. Choix techniques justifiés
- **pnpm** : rapide, économise l'espace disque (hardlinks), verrouillage strict des dépendances ; documenté de bout en bout dans `05-`.
- **TypeScript strict** : démontre la maîtrise du typage, évite les bugs d'état (union des zones, types des expériences).
- **marked + dompurify** : le binôme standard pour « Markdown sûr côté navigateur ». `marked` ne sanitiase pas nativement → DOMPurify est le garde-fou (voir `06-`).
- **tailwind `print:hidden` / `print:`** : contrôle CSS pur de l'impression PDF sans librairie tierce.
- **@serwist/next** : successeur maintenu de `next-pwa`, typé, pensé pour l'App Router.
## 11. Déploiement conteneurisé
### Pourquoi Docker ?
- **Reproductibilité** : l'image de production embarque la même version de Node, de pnpm, et les mêmes dépendances compilées.
- **Portabilité** : `docker run` fonctionne sur n'importe quel hôte doté du démon Docker (Linux, macOS, Windows, cloud).
- **Distribution via Docker Hub** : l'image est tagguée et poussée sur un registre public → n'importe qui peut la tirer et l'exécuter (`docker pull`).
### Architecture de l'image (multi-stage)
```
node:20-alpine
│
├─ deps → pnpm install --frozen-lockfile (cache, rapide)
├─ builder → pnpm build (sortie standalone Next.js)
└─ runner → node server.js (image finale ≈ 100-150 Mo)
```
- Le champ `output: "standalone"` dans `next.config.ts` génère un `server.js` autonome dans `.next/standalone` → **aucun node_modules complet** embarqué dans l'image finale.
- Base `node:20-alpine` : image runtime minimale, compatible glibc/musl (ce projet n'utilise pas de dépendances natives).
### Fichiers associés
| Fichier | Rôle |
| --- | --- |
| `Dockerfile` | 3 stages (`deps` → `builder` → `runner`), `EXPOSE 3000`, `HOSTNAME=0.0.0.0` |
| `.dockerignore` | Exclut `node_modules/`, `.next/`, `docs/`, `*.md`, `.git/`, `.env*` |
| `docker-compose.yml` | Service `cv-maker`, build, ports `3000:3000`, healthcheck `wget` |
### Note sur `output: "standalone"`
Ajouter dans `next.config.ts` (côte à côte avec `withSerwist`) :
```ts
export default withSerwist(nextConfig, { output: "standalone" });
```
ou bien :
```ts
const nextConfig = { output: "standalone" };
export default withSerwist(nextConfig);
```
L'ensemble `withSerwist` + `standalone` est compatible : le service worker (`sw.js`) est généré dans `public/` lors du build et copié dans l'image finale avec le reste du dossier `public/`.
### Variables d'environnement au runtime
| Variable | Valeur | Rôle |
| --- | --- | --- |
| `NODE_ENV` | `production` | Désactive les warnings React, active les optimisations Next.js |
| `HOSTNAME` | `0.0.0.0` | Écoute sur toutes les interfaces (sinon `localhost` uniquement → conteneur inaccessible) |
| `PORT` | `3000` | Port du serveur Next.js (mappé via `-p 3000:3000`) |
+181
View File
@@ -0,0 +1,181 @@
# 03 — Modèle de données
## 1. Principes
- **Aucun serveur** : le « modèle de données » = structure de l'état applicatif + son image persistée dans **localStorage**.
- **Une seule clé de stockage** versionnée : `cv-maker:v1`.
- **Toutes les zones sont du Markdown brut** (chaînes de caractères). Le rendu HTML est calculé à la volée, jamais stocké.
- Les expériences sont une **liste d'objets** (avec identifiant stable), contrairement aux 5 zones simples.
## 2. Contracts TypeScript
`lib/types/cv.ts` :
```ts
/** Identifiant des 5 zones de saisie éditées par onglets. */
export type CVTabId =
| "accroche"
| "etatCivil"
| "formations"
| "softSkills"
| "centresInteret";
/** Ordre d'affichage des onglets (1 → 5). */
export const CV_TAB_ORDER: CVTabId[] = [
"accroche",
"etatCivil",
"formations",
"softSkills",
"centresInteret",
];
/** Métadonnées d'affichage d'un onglet. */
export interface CVTabMeta {
id: CVTabId;
numero: 1 | 2 | 3 | 4 | 5; // numéro affiché sur l'onglet
label: string; // libellé court de l'onglet
}
export const CV_TABS: Record<CVTabId, CVTabMeta> = {
accroche: { id: "accroche", numero: 1, label: "Accroche" },
etatCivil: { id: "etatCivil", numero: 2, label: "État civil" },
formations: { id: "formations", numero: 3, label: "Formations" },
softSkills: { id: "softSkills", numero: 4, label: "Soft skills" },
centresInteret: { id: "centresInteret", numero: 5, label: "Centres d'intérêt" },
};
/** Une expérience professionnelle. */
export interface Experience {
id: string; // crypto.randomUUID() — identifiant stable, jamais l'index
titre: string; // titre affiché dans la liste du panneau gauche + section CV
contenu: string; // corps de l'expérience en Markdown
createdAt: number; // timestamp (ms) de création
updatedAt: number; // timestamp (ms) de dernière modification
}
/** État complet du CV (et contenu de la clé localStorage). */
export interface CVCareerData {
version: 1; // version du schéma → déclenche les migrations
accroche: string; // zone 1
etatCivil: string; // zone 2
formations: string; // zone 3
softSkills: string; // zone 4
centresInteret: string; // zone 5
experiences: Experience[]; // liste, ordre = ordre d'affichage dans le CV
}
/** Données restituées après hydratation locale. */
export type CVStorage = CVCareerData; // alias sémantique pour le contrat localStorage
```
### Champs liés à l'onglet 2 (état civil + contacts + site + GitHub)
Le GOAL.md précise que la zone 2 contient « état civil + contacts + site web + github ». Ces informations restent **un seul bloc Markdown** (`etatCivil`) rendu dans la section « État civil » — pas de structuration fine en sous-champs : l'utilisateur formate librement (ex. `**Jean Dupont**` / `jean.dupont@mail.fr` / `github.com/jean`).
## 3. Données par défaut
```ts
export function createDefaultCVData(): CVCareerData {
return {
version: 1,
accroche: "",
etatCivil: "",
formations: "",
softSkills: "",
centresInteret: "",
experiences: [],
};
}
```
## 4. Couche de stockage `lib/storage.ts`
Rôles :
- lecture / écriture de la clé `cv-maker:v1` ;
- **parsing sécurisé** (try/catch, validation minimale de forme) ;
- point d'extension pour une future **migration de version** ;
- nom de clé centralisé (constante `STORAGE_KEY`).
```ts
export const STORAGE_KEY = "cv-maker:v1";
export interface CVStorageAdapter {
load(): CVCareerData | null;
save(data: CVCareerData): void;
clear(): void;
}
export function createLocalStorageAdapter(): CVStorageAdapter; // impl. try/catch
```
Détails d'implémentation attendus :
| Opération | Comportement |
| --- | --- |
| `load()` | `localStorage.getItem(STORAGE_KEY)` → `JSON.parse` → validation de forme (`version === 1`, `experiences` est un tableau) → merge avec les valeurs par défaut pour les champs manquants. Renvoie `null` si rien / invalide. |
| `save()` | `JSON.stringify(data)` → `setItem`. Erreur (quota dépassé, mode privé) capturée et journalisée (`console.warn`) par le hook, sans affichage bloquant. |
| `clear()` | `removeItem(STORAGE_KEY)` — utilisé par un éventuel bouton « Réinitialiser ». |
## 5. Hook `useLocalStorage` (`lib/hooks/useLocalStorage.ts`)
Hook générique de synchronisation état React ↔ localStorage :
```ts
export function useLocalStorage<T>(
storage: StorageLike<T>,
initialValue: T,
enable?: boolean,
): [value: T, setValue: (v: T | ((prev: T) => T)) => void];
```
- **Hydratation sûre** : l'état démarre sur `initialValue` (identique au SSR) ; le `load()` est décalé dans un effet (`setTimeout 0`) après montage pour éviter toute divergence serveur/client.
- **Écriture automatique** : `useEffect` sur `value` → `save(value)` à chaque changement (délai 60 ms).
- **Sans indicateur de statut** : l'enregistrement est silencieux ; les erreurs de chargement/sauvegarde sont uniquement journalisées (`console.warn`).
- Peut être **désactivé** (3ᵉ paramètre `enable = false`) si nécessaire.
## 6. Hook métier `useCVData` (`lib/hooks/useCVData.ts`)
Regroupe toute la logique d'édition du CV, façade unique pour les composants :
```ts
export interface UseCVDataReturn {
data: CVCareerData;
setText(tab: CVTabId, value: string): void; // édition d'une zone 1..5
addExperience(titre: string, contenu: string): void; // ajoute en fin de liste
updateExperience(id: string, titre: string, contenu: string): void;
removeExperience(id: string): void; // suppression + confirmation côté UI
resetCV(): void; // restauration des valeurs par défaut
importCV(value: CVCareerData): void; // remplace les données (import JSON)
}
```
Règles d'implémentation :
| Méthode | Comportement |
| --- | --- |
| `setText(tab, value)` | immuable : crée une nouvelle copie `{ ...prev, [tab]: value }` ; met à jour les timestamps des expériences concernées si besoin (aucun pour les zones simples). |
| `addExperience(titre, contenu)` | crée `{ id: crypto.randomUUID(), titre, contenu, createdAt: Date.now(), updatedAt: same }` et **push** (fin de liste = le nouvel ajout suit le dernier saisi). |
| `updateExperience(id, …)` | remplace titre/contenu, réécrit `updatedAt`, conserve `id`, `createdAt` et la **position** dans la liste. |
| `removeExperience(id)` | filtre sur `id` (pas l'index). |
### Ordre de saisie (ordre d'affichage)
L'affichage respecte **l'ordre de saisie** : la 1ʳᵉ expérience ajoutée est affichée en haut (numérotée 1), la suivante en dessous, etc. Pas de tri par date (`createdAt`/`updatedAt` ne servent pas à ordonner l'affichage). Modifier ou supprimer une entrée ne change pas l'ordre des autres. La numérotation de la liste du panneau gauche (1, 2, 3…) reflète cet ordre et correspond au haut → bas de la section « Expériences professionnelles » du CV.
> Évolution prévue : ajouter `periodeDebut`/`periodeFin` + tri antichronologique ou drag & drop. Les champs `createdAt`/`updatedAt` sont déjà prévus pour supporter ce tri.
## 7. Sérialisation et intégrité
| Point | Règle |
| --- | --- |
| Taille | Contenus de CV : quelques Ko ; bien sous la limite localStorage (5–10 Mo selon navigateur). |
| Reload | L'hydratation se fait au `mount` (SSR désactivé pour la partie interactive) : pas de flash de rendu erroné. |
| Version | Si `version` de la sauvegarde ≠ `1` → migration `migrateData` (no-op pour v1) ou repli sur les valeurs par défaut. |
| Corruption | JSON invalide → ignore la sauvegarde et démarre avec `createDefaultCVData()`, sans crash. |
## 8. Anti-patterns à éviter
- ❌ Stoker le HTML rendu dans localStorage (obsolete dès que la version de `marked` change) → on stocke **toujours le Markdown source**.
- ❌ Utiliser l'index du tableau comme identifiant (cas d'une suppression → mauvaise référence dans React `key` et dans `updateExperience`).
- ❌ Écrire dans localStorage pendant le rendu initial (dépendances d'effet non maîtrisées) → écriture uniquement dans `useEffect` déclenché par un changement d'état.
+168
View File
@@ -0,0 +1,168 @@
# 04 — Spécifications UI et composants
## 1. Design tokens (Tailwind CSS 4)
Fichier source : `app/globals.css` (via `@import "tailwindcss"` + `@theme`).
| Token | Valeur | Usage |
| --- | --- | --- |
| `--color-surface` | blanc (`#ffffff`) | feuille A4 |
| `--color-paper` | `#f1f5f9` gris très clair | fond de l'application hors feuille |
| `--color-ink` | `#0f172a` | texte principal |
| `--color-muted` | `#64748b` | texte secondaire |
| `--color-accent` | `#2563eb` | actions, focus, onglet actif |
| `--color-danger` | `#dc2626` | boutons « × » suppression |
| `--color-border` | `#e2e8f0` | bordures et séparateurs |
| `--radius-*` | `0.5rem` (md), `0.375rem` (sm) | cohérence des angles |
| `--font-sans` | système + `Inter` en fallback | lisibilité écran et impression |
Typographie du CV (feuille A4) :
| Élément | Suggestion |
| --- | --- |
| Nom / accroche (header) | `text-base font-semibold` à `text-xl font-bold` |
| Titres de section | `text-[0.75rem] uppercase tracking-wide text-muted font-bold` |
| Corps | `text-[0.8rem] leading-snug text-ink` |
### Palette neutre recommandée
Le CV imprimé doit rester sobre : **encre sombre** sur blanc, onglet actif en accent, **aucun fond de couleur sur la feuille A4** (économie d'encre et lisibilité).
## 2. Barre d'application (`CvToolbar`)
| Élément | Contenu |
| --- | --- |
| Gauche | Logo (icône `FileText`) + « CV-Maker » |
| Droite | Bouton **Imprimer / PDF**, bouton **Importer JSON**, bouton **Exporter JSON**, bouton **Réinitialiser**, lien **Documentation** |
Rendre les liens actifs en `font-semibold` et inactifs en `text-muted` (page courante) — navigation classique App Router (`usePathname`).
## 2bis. Pied de page (`Footer`)
- **Masqué à l'impression** (`print-hidden`), bordure supérieure.
- Contenu : nom de l'application + lien **Mentions légales** vers `/mentions-legales` (remplace l'ancien lien RGPD de la barre).
## 3. Panneau latéral gauche
### 3.1 `CvTabs` — onglets 1 à 5
- Basé sur la primitive `ui/Tabs` : `role="tablist"`, boutons `role="tab"`, `aria-selected`, `role="tabpanel"`.
- Affichage : barre de 5 boutons horizontaux, libellé = `numéro + label` (`1 · Accroche`, `2 · État civil`, …).
- Onglet actif : fond `accent`, texte blanc ; inactifs : bordure `border` + texte `muted`.
- **Contenu piloté par le parent** (`activeTab` dans `CvInputPanel`) : un seul `MarkdownTextarea` monté pour l'onglet actif. Le texte de l'onglet actif est le `value` contrôlé.
### 3.2 `MarkdownTextarea`
| Élément | Spécification |
| --- | --- |
| Props | `label: string`, `value: string`, `onChange(text: string): void`, `placeholder?: string` |
| Comportement | `textarea` contrôlé, `onChange` → mise à jour immédiate de l'état + persistance |
| UX | `min-h` variable (accroche + courte), `resize-y`, monospace en saisie (`font-mono`) avec bascule visuelle explicite « le texte est en Markdown » |
| Note Markdown | petite légende « Syntaxe Markdown supportée » avec lien vers `/documentation` |
### 3.3 `ExperienceManager`
Composition :
- **En-tête** : titre « Expériences » + bouton **+** (`ui/Button`, variante `primary`) → prépare une nouvelle expérience (zones titre/contenu vidées, focus du formulaire).
- **Formulaire** : deux champs — `titre` (input texte) + `contenu` (textarea Markdown, même composant que les zones 1-5). Boutons : **Enregistrer** et **Annuler** (annulation quand modification).
- **Liste numérotée** : sous le formulaire.
États internes suivants :
```ts
type ExperienceEditor =
| { mode: "closed" } // rien en édition
| { mode: "new" } // ajout en cours
| { mode: "edit"; id: Experience["id"] }; // modification en cours
```
### 3.4 Ligne d'expérience (`ExperienceItem`)
Format visuel : `1. Titre de l'expérience [Modifier] [×]`
| Action | Clé | Comportement |
| --- | --- | --- |
| Modifier | bouton icône `Pencil` + libellé | passe en mode `edit`, recharge titre/contenu dans le formulaire |
| Supprimer | bouton `×` (`IconButton`, variante danger) | **confirmation** (petit état interne « Confirmer la suppression ? [Oui/Non] » ou `window.confirm`) puis `removeExperience(id)` |
> Composants `ui/Button`, `ui/IconButton`, `ui/Textarea`, `ui/Tabs` sont les primitives réutilisables (cf. le volet « composants réutilisables » du cahier des charges).
## 4. Panneau latéral droit — feuille A4 (`CvPreview`)
### 4.1 Conteneur de feuille
```tsx
<div className="cv-sheet"> {/* wrapper lecture imprimée */}
<header>CvHeader</header>
<div className="cv-sheet-body">
<aside>CvLeftColumn</aside> {/* 1/3 */}
<main>CvRightColumn</main> {/* 2/3 */}
</div>
</div>
```
Dimensions écran :
| Propriété | Valeur |
| --- | --- |
| Largeur | `210mm` (with `max-w-full` pour rester contenu) |
| Hauteur | `297mm` (`min-h`, la feuille peut être plus longue si contenu dépassant — dépassement scrollable) |
| Ombre / bordure | `shadow-lg border border-border` (écran) / **supprimées** à l'impression |
| Padding | `16mm` (marges type CV) |
| Fond | blanc `surface` |
Ratio 1/3–2/3 : `.cv-sheet-body { display:grid; grid-template-columns: 1fr 2fr; gap: 12mm; }`.
### 4.2 `CvHeader` (accroche — zone 1)
- Toute la largeur de la feuille.
- Rendu Markdown de `data.accroche` (contenu typiquement : nom + poste + accroche).
- `border-bottom` de séparation avec le corps de feuille.
### 4.3 `CvLeftColumn` (sections 2 à 5, 1/3)
Section générique `CvSection` :
```tsx
<CvSection title="État civil" html={htmlEtatCivil} />
<CvSection title="Formations" html={htmlFormations} />
<CvSection title="Soft skills" html={htmlSoftSkills} />
<CvSection title="Centres d'intérêt" html={htmlCentresInteret} />
```
Règles :
- Titre stylé selon le token « titres de section » (petit, majuscules, espacement).
- Contenu : `dangerouslySetInnerHTML={{ __html: sanitizedHtml }}` **uniquement** si le HTML a été produit par `lib/markdown` (marked + DOMPurify) — jamais de HTML saisi brut.
- Si une section est vide → masquée (pas de cadre vide) ; le GOAL.md ne demande pas d'emplacement fixe vide.
### 4.4 `CvRightColumn` (expériences, 2/3)
- Titre fixe : **« Expériences professionnelles »** (selon token titres de section).
- Liste des `data.experiences` **dans l'ordre du tableau** = ordre de saisie (la 1ʳᵉ entrée en haut, cf. module 03).
- Chaque `CvExperiencePreview` : **titre** en gras + rendu Markdown du `contenu`.
- Section vide → texte léger : instruction « Ajoutez une expérience dans le panneau gauche » (rendu écran uniquement, masqué à l'impression via `print:hidden`).
### 4.5 Dépassement de contenu
- Sur le feuille : la hauteur est le **hapax A4** à l'impression (une page). En revanche, le contenu peut dépasser → `overflow` tolérable sur une « page 2 » de preview est hors scope v1. Comportement choisi : la feuille s'allonge (`min-h` seulement) pour montrer tout le contenu ; **l'impression** sera paginée par le navigateur (2 pages éventuelles). Documenté comme limitation assumée.
## 5. États UI transverses
| État | Signal |
| --- | --- |
| Erreur de sauvegarde | `console.warn` de l'erreur (enregistrement silencieux, sans pastille) |
| Onglet actif | fond accent (haut contraste) |
| Formulaire expérience (nouveau / modification) | zone surlignée `border-accent` |
| Suppression | état de confirmation inline (Oui / Non) |
## 6. Responsive minimal (nice to have)
- **≥ 1024 px** : mise en page 2 colonnes (1/3 – 2/3) inchangée.
- **< 1024 px** : le panneau gauche passe en `w-full` au-dessus de la preview (empilés verticalement) ; la preview se centre. Tablette viable ; mobile lisible mais non optimisé (desktop first).
- L'impression ne dépend **pas** du viewport (elle est pilotée par `@media print`, voir module 02 §7).
## 7. Accessibilité (bonnes pratiques)
- Onglets : `role="tablist"/"tab"/"tabpanel"`, navigation clavier (flèches ←/→), `aria-selected`, `aria-controls`, `id` liés.
- Boutons icônes : `aria-label` explicites (« Modifier l'expérience », « Supprimer l'expérience »).
- Inputs/textarea : `<label htmlFor>` avec `id` unique.
- Lien focus : contour `focus-visible` accent (Tailwind `focus-visible:outline-2` + `focus-visible:outline-accent`).
+418
View File
@@ -0,0 +1,418 @@
# 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 <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 |
+88
View File
@@ -0,0 +1,88 @@
# 06 — Sécurité et conformité RGPD
## 1. Analyse de risque (résumé)
| Menace | Vecteur | Niveau (v1) | Contre-mesure |
| --- | --- | --- | --- |
| XSS — injection de HTML/JS saisi par l'utilisateur | champ Markdown rendu via `dangerouslySetInnerHTML` | **élevé** si non traité | **DOMPurify.sanitize()** systématique sur le HTML produit par `marked` |
| Vol de données | exfiltration réseau | nul | **aucune donnée quitte le navigateur** (pas d'API, pas d'appel distant) |
| Corruption de la sauvegarde | JSON invalide en localStorage | faible | parsing try/catch + validation de forme + valeur par défaut |
| Quota localStorage dépassé | très gros CV | faible | erreur catchée + `console.warn` (enregistrement silencieux) |
| Données lues par un tiers sur le poste | accès local au navigateur | hors périmètre | relatives au poste utilisateur (aucune garantie applicative possible) |
Rappel : **le seul point d'entrée présentant un risque d'exécution** est le rendu HTML. Mitigé par la règle « aucun HTML brut n'est injecté, uniquement le résultat de `markdownToHtml` (marked → DOMPurify) ».
## 2. Chaîne de rendu sécurisée (obligatoire)
```
Markdown brut (textarea)
│ marked.parse (GFM, breaks)
▼
HTML intermédiaire non sûr
│ DOMPurify.sanitize(html, { USE_PROFILES: { html: true } })
▼
HTML assaini
│ dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
▼
DOM
```
### Règles d'implémentation
1. Toute injection dans le DOM passe **exclusivement** par `lib/markdown.ts` → `markdownToHtml`.
2. Ne jamais injecter la saisie brute (une `textarea` n'est pas un `<script>` : le texte est du texte ; le risque n'apparaît que lors du rendu HTML).
3. Ne pas surcharger la config DOMPurify par zone ; une configuration unique module `06-` est utilisée partout.
4. `marked` options stables : `gfm: true`, `breaks: true`. Ne pas activer d'extension/plugin non maîtrisé.
5. Le rendu est **déterministe** (pas de rendu serveur du Markdown utilisateur pour les zones interactives — le client est le seul excécutant).
6. `dompurify` est importé seulement dans les composants client (prérequis `window`), jamais dans un composant serveur/réseau.
### Ce que DOMPurify bloque (défauts)
- balises `<script>`, `<iframe>`, `<object>`, `<embed>`, `<svg>` non sûres ;
- gestionnaires d'événements inline (`onclick=`, `onerror=`) ;
- URLs `javascript:` dans `href`/`src` ;
- tout futur composant exotique inconnu.
## 3. Données personnelles : état de l'analyse
Les zones 1 à 5 du CV contiennent des **données à caractère personnel** (nom, e-mail, téléphone, histoire professionnelle).
| Propriété | État |
| --- | --- |
| Stockage | **localStorage du navigateur, sur le poste seulement** |
| Transmission | **aucune** (pas d'envoi réseau, pas de cookies, pas de tracker, pas d'analytics) |
| Durée de conservation | jusqu'à effacement des données du navigateur, ou « Réinitialiser le CV » dans l'application |
| Accès | celui qui possède le poste/navigateur |
| Base légale (contrôle qualité RGPD) | l'utilisateur est le seul responsable et destinataire ; aucune RGDP externe ne traite ces données |
**Conclusion** : l'application n'effectue **aucun traitement** débordant l'usage personnel de l'utilisateur. La page `/mentions-legales` doit le formuler clairement et fournir les droits usuels (accès, rectification, suppression — effectuables directement dans l'app/browser).
## 4. Contenu de la page `/mentions-legales` (modèle de rédaction)
Sections à inclure (v1 : **sans paragraphe contact**) :
1. **Titre** — « Mentions légales ».
2. **Éditeur / responsable de traitement** : l'utilisateur lui-même (pas d'entité tierce).
3. **Données traitées** : les contenus saisis (état civil, contacts, parcours…).
4. **Finalité** : production d'un CV local ; aucune transmission.
5. **Stockage** : localStorage du navigateur (clé `cv-maker:v1`), jamais envoyé à un serveur.
6. **Durée de conservation** : tant que les données du navigateur existent ; possibilité de suppression immédiate.
7. **Droits de la personne concernée** : libre disposition des données — modification/suppression dans l'éditeur ; effacement possible via les outils du navigateur (suggestion d'actions concrètes) ou le bouton « Réinitialiser ».
8. **Sécurité** : rendu assaini (aucune exécution de contenu saisi), aucune dépendance réseau.
9. **Contact** : *section retirée en v1* (cf. demande utilisateur).
10. **Date de mise à jour** de la page.
## 5. Sécurité applicative transversale
- **PWA/service worker** : le SW ne précache que des routes statiques internes ; aucun champ utilisateur n'y est inscrit (les données restent dans localStorage, hors périmètre du cache de pages).
- **Dépendances** : audit régulier `pnpm audit` ; mise à jour de `marked`/`dompurify` (les deux librairies de sécurité critiques).
- **Headers de réponse** (side) : valeurs par défaut convenables pour Next.js de production ; pas d'en-tête supplémentaire requis côté local (pas de cookies).
- **Content-Security-Policy** (recommandation) : à documenter comme évolution — les pages statiques ne chargent aucun script tiers, une CSP stricte est triviale à ajouter hors v1 si hébergé.
## 6. Checklist sécurité avant livraison
- [ ] Toute zone rendue passe par `markdownToHtml` (relire chaque `dangerouslySetInnerHTML`).
- [ ] Aucun appel réseau dans le code applicatif (grep `fetch(`, `axios`, `XMLHttpRequest` dans `app/` et `components/`).
- [ ] `pnpm audit` sans vulnérabilité de haut niveau.
- [ ] Test manuel d'injection : saisir `<img src=x onerror=alert(1)>` et `<script>alert(1)</script>` dans chaque zone → aucune exécution.
- [ ] Page `/mentions-legales` présente et linker depuis le footer (masqué à l'impression).
+135
View File
@@ -0,0 +1,135 @@
# 07 — Plan de tests (recette manuelle)
> Périmètre v1 : **aucun test automatisé** (décision du GOAL.md : « pas de test pour démarrer — à prévoir pour plus tard »). La recette ci-dessous est un **plan de tests manuels** aligné sur les critères d'acceptation du GOAL.md. Elle servira aussi de **cahier de recette** pour le livrable ECF.
## 1. Environnement de test
| Paramètre | Valeur |
| --- | --- |
| Navigateur principal | Chrome / Edge (dernière version) — engine Chromium recommandé |
| Navigateur secondaire | Firefox (contrôle croisé) |
| Résolution principale | ≥ 1024 px (desktop) |
| Commande de lancement | `pnpm dev` (ou `pnpm build && pnpm start` pour la recette finale) |
## 2. Critères d'acceptation globaux (rappel GOAL.md)
- [ ] Application « desktop first » opérationnelle.
- [ ] Interface en deux colonnes respectant le ratio 1/3 – 2/3.
- [ ] Saisie Markdown à gauche, preview temps réel à droite au **format A4**.
- [ ] Sauvegarde localStorage à chaque frappe + restauration au rechargement.
- [ ] Impression du CV en PDF.
- [ ] Pages Documentation et Mentions légales accessibles.
## 3. Tests fonctionnels détaillés
### 3.1 Écran général et layout
| # | Action | Résultat attendu |
| --- | --- | --- |
| G1 | Ouvrir `/` | Deux colonnes : gauche (1/3) avec tab 1 actif, droite (2/3) avec feuille A4 (blanche, ombragée) |
| G2 | Réduire la fenêtre sous 1024 px | Les panneaux s'empilent (gauche au-dessus de la droite) sans casse |
| G3 | Vérifier la barre d'app et le footer | Barre : logo, bouton « Imprimer / PDF », lien Documentation. Footer : lien Mentions légales (masqué à l'impression) |
### 3.2 Onglets 1 à 5 (zones de saisie)
| # | Action | Résultat attendu |
| --- | --- | --- |
| T1 | Cliquer sur les onglets 1→5 | L'onglet actif change ; le textarea affiche le contenu **de cet onglet uniquement** |
| T2 | Saisir du Markdown dans l'onglet 1 (`**Jean Dupont**`) | Rendu temps réel dans le **header** de la feuille A4 (gras) |
| T3 | Idem dans l'onglet 2 (nom, e-mail, site, GitHub en listes) | Rendu dans la section **État civil** (colonne 1/3) |
| T4 | Onglet 3 (formation ex. `### Master` + liste) | Rendu dans la section **Formations** |
| T5 | Onglet 4 | Rendu dans la section **Soft skills** |
| T6 | Onglet 5 | Rendu dans la section **Centres d'intérêt** |
| T7 | Basculer entre onglets après saisie | Chaque contenu est conservé, aucun écrasement croisé |
| T8 | Vider complètement une section | La section correspondante disparaît de la preview |
### 3.3 Expériences professionnelles
| # | Action | Résultat attendu |
| --- | --- | --- |
| X1 | Cliquer sur le bouton **+** | Un formulaire (titre + contenu) apparaît, vide |
| X2 | Saisir titre + contenu puis **Enregistrer** | L'expérience apparaît dans la liste numérotée du panneau gauche (numéro 1) ET en tête de la section « Expériences professionnelles » |
| X3 | Ajouter une 2ᵉ expérience | Elle s'affiche **sous** la 1ʳᵉ ; les listes se renumérotent (1, 2… dans l'ordre de saisie) |
| X4 | Cliquer **Modifier** sur une entrée | Le formulaire se remplit avec titre + contenu de cette expérience (mode modification) |
| X5 | Modifier puis **Enregistrer** | Le rendu preview met à jour la section Expériences |
| X6 | Cliquer **Annuler** en cours de modification | Aucune modification appliquée |
| X7 | Cliquer **×** sur une entrée | Confirmation demandée → confirmer → l'expérience disparaît (panneau gauche + preview) |
| X8 | Supprimer la dernière expérience | La section affiche le message d'incitation « Ajoutez une expérience… » |
| X9 | Contenu Markdown d'une expérience | Rendu temps réel en HTML dans la preview |
### 3.4 Persistance (localStorage)
| # | Action | Résultat attendu |
| --- | --- | --- |
| P1 | Saisir du contenu dans 2-3 zones puis **recharger la page** (F5) | Tous les contenus et la liste des expériences sont restaurés |
| P2 | Vérifier en devtools → Application → localStorage | Une clé `cv-maker:v1` existe, au format JSON avec `version: 1` |
| P3 | Vider localStorage (ou supprimer la clé) puis recharger | L'application démarre avec des zones vides, **sans erreur** |
| P4 | Introduire un JSON corrompu dans la clé (`cv-maker:v1`) puis recharger | Pas de crash ; état par défaut restauré |
| P5 | Cliquer **Exporter JSON** | Un fichier `cv-maker.json` est téléchargé contenant le contenu saisi |
| P6 | Créer/exporter un CV, **Importer JSON** sur ce fichier après modification | Un second clic sur **Importer** (bouton/badge) remplace le contenu par celui du fichier (cas nominal validé) |
### 3.5 Impression / PDF
| # | Action | Résultat attendu |
| --- | --- | --- |
| M1 | Cliquer **Imprimer / PDF** | L'aperçu d'impression ne contient que la feuille A4 (pas de panneau de saisie ni toolbar) |
| M2 | Choisir « Enregistrer en PDF » avec format A4 | Un PDF d'une page (contenu court) ; mise en page 1/3–2/3 respectée |
| M3 | Format A4 par défaut | `@page size: A4` appliqué (aucun choix manuel requis) |
### 3.6 Rendu Markdown et sécurité
| # | Action | Résultat attendu |
| --- | --- | --- |
| S1 | Saisir tous les éléments supportés (titres ###, gras, liste `-`, lien `[t](url)`, code `` ` ``) | Rendu HTML correct dans la preview |
| S2 | Saisir `<img src=x onerror=alert(1)>` dans une zone | Aucune alerte/exécution ; la balise est neutralisée |
| S3 | Saisir `<script>alert(1)</script>` | Affiché sans exécution (ou supprimé) — pas d'alerte |
| S4 | Saisir `<a href="javascript:alert(1)">x</a>` | Lien inoffensif ou supprimé ; aucun dialogue |
### 3.7 Pages Documentation et Mentions légales
| # | Action | Résultat attendu |
| --- | --- | --- |
| D1 | Naviguer vers `/documentation` depuis la toolbar | Page d'aide affichée (prise en main + syntaxe Markdown) |
| D2 | Naviguer vers `/mentions-legales` depuis le footer | Page affichée avec les sections attendues (module `06-` §4), **sans** paragraphe contact |
| D3 | Retour vers l'éditeur | Lien retour fonctionnel ; les données du CV n'ont pas bougé |
### 3.8 PWA (recette avancée — build production requis)
| # | Action | Résultat attendu |
| --- | --- | --- |
| W1 | `pnpm build && pnpm start` | Aucune erreur de build liée à Serwist |
| W2 | DevTools → Application | Service worker enregistré, manifest chargé |
| W3 | Clic « Installer l'application » | Installation proposée (icône et nom corrects) |
| W4 | Passer hors-ligne puis recharger `/` | La page (squelette + app shell) est servie par le cache |
| W5 | Données du CV hors-ligne | Le CV reste intact (localStorage) — pas de régression |
## 4. Hygiène du code (vérifications finales)
- [ ] `pnpm lint` sans erreur ni warning bloquant.
- [ ] `pnpm typecheck` sans erreur.
- [ ] `pnpm audit` sans vulnérabilité critique.
- [ ] Aucun `console.log` résiduel (à l'exception éventuelle du `console.warn` d'échec de sauvegarde documenté).
## 5. Évolutions testables (v2 — hors périmètre v1)
Dès leur mise en place, ajouter à la recette :
- Tests **Vitest** pour `lib/markdown.ts` (rendu + assainissement) et `lib/storage.ts` (parse/corruption) ;
- Mise en place de **drag & drop** des expériences (réordonnancement) avec tests correspondants ;
- Tests composants (React Testing Library) sur `CvTabs`, `ExperienceManager`, `CvPreview` (rendu conditionnel des sections vides).
## 6. Bilan de recette
| Critère global GOAL.md | Tests concernés | Statut final |
| --- | --- | --- |
| Desktop first, 2 colonnes 1/3–2/3 | G1, G2 | ☐ |
| Markdown → HTML temps réel | T2–T6, X9, S1 | ☐ |
| Preview A4 | G1, M1–M3 | ☐ |
| Persistance localStorage | P1–P5 | ☐ |
| Impression PDF | M1–M3 | ☐ |
| Expériences (+, liste, modif, ×) | X1–X9 | ☐ |
| Onglets 1-5 | T1–T8 | ☐ |
| Documentation + Mentions légales | D1–D3 | ☐ |
| PWA | W1–W5 | ☐ |
Tout critère ☐ non coché en livraison = anomalie à corriger avant livraison finale.
+205
View File
@@ -0,0 +1,205 @@
# 08 — Déploiement conteneurisé (Docker / DockerHub)
> Ce document est la **spécification** de la dockerisation de CV-Maker. Les fichiers `Dockerfile`, `docker-compose.yml` et `.dockerignore` seront créés lors de l'implémentation, **conformément** au contenu détaillé ci-dessous.
## 1. Objectif
1. Packager l'application Next.js dans une **image Docker** de production reproductible.
2. Pouvoir **pousser cette image sur Docker Hub** (`docker push`) pour la distribuer et la déployer n'importe où (`docker run` / `docker compose up`).
3. Maîtriser la taille de l'image : seule la **sortie standalone** de Next.js est embarquée (pas le code source ni les dépendances de build).
## 2. Architecture de l'image (multi-stage)
```
node:20-alpine
│
├─ stage [deps] pnpm install --frozen-lockfile (lockfile seul)
│
├─ stage [builder] sources + node_modules → pnpm build
│ → produit : .next/standalone, .next/static, public/
│
└─ stage [runner] .next/standalone + .next/static + public/
→ node server.js (HOSTNAME=0.0.0.0 PORT=3000)
```
| Stage | Image | Contenu | Rôle |
| --- | --- | --- | --- |
| `deps` | `node:20-alpine` | `package.json` + `pnpm-lock.yaml` | Télécharge et fige les dépendances (cache de build efficace) |
| `builder` | `node:20-alpine` | sources + `node_modules` | Compile `pnpm build` ; produit l'artefact standalone |
| `runner` | `node:20-alpine` | `.next/standalone`, `.next/static`, `public/` | Exécution seule : image légère, aucun source ni tooling |
### Pourquoi `output: "standalone"` ?
Next.js trace à la compilation toutes les dépendances réellement utilisées et génère un `server.js` autonome dans `.next/standalone`. Cela permet une image de production **sans node_modules complète** (économie de l'ordre de ×5 à ×10 en taille).
### Pourquoi `node:20-alpine` ?
Base minimale (≈50 Mo) pour la phase runtime, cohérente avec le prérequis Node 20 du projet. Compatible glibc/musl — aucune dépendance native dans ce projet (marked/dompurify/lucide sont des packages JS purs).
## 3. Fichiers à créer — spécifications
### 3.1 `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 . .
# Build de production ; la PWA (Serwist) génère public/sw.js ici
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
# .next/standalone auto-contain server.js, package.json…
COPY --from=builder /app/.next/standalone ./
# Assets statiques (CSS/JS optimisés) + public/ (manifest, icônes, sw.js)
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
```
Notes d'intégration :
- `corepack enable` active pnpm à partir du champ `packageManager` du `package.json` (écrit par `create-next-app`) → version de pnpm **figée**.
- `pnpm install --frozen-lockfile` : échec si le lockfile n'est pas à jour (déploiement déterministe).
- `public/` contient génère tout ce qui est généré pendant le build (ex. `sw.js` de Serwist) mais est copié depuis le builder → toujours cohérent.
### 3.2 `.dockerignore`
```gitignore
node_modules
.next
out
.git
.gitignore
docs
*.md
Dockerfile
docker-compose.yml
.dockerignore
*.log
.env*
.DS_Store
```
Justification :
- `node_modules`, `.next`, `docs`, `*.md` : ne doivent **jamais** entrer dans le contexte de build (cache, rapidité, confidentialité — les secrets éventuels d'une doc `.env` ne sont pas copiés).
- Le contexte `docker build` doit contenir **uniquement** ce dont le builder a besoin : sources `app/`, `components/`, `lib/`, `public/`, `next.config.ts`, `tsconfig.json`, `package.json`, `pnpm-lock.yaml`.
### 3.3 `docker-compose.yml`
```yaml
services:
cv-maker:
build:
context: .
dockerfile: Dockerfile
image: cv-maker:local # nom local ; remplacé par <mon-user-dockerhub>/cv-maker:tag au push
container_name: cv-maker
restart: unless-stopped
ports:
- "3000:3000"
environment:
- HOSTNAME=0.0.0.0
- PORT=3000
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
```
- `wget` est fourni par busybox sur Alpine (utilisable pour le healthcheck).
- `127.0.0.1` (et non `localhost`) : busybox wget privilégie `::1` (IPv6) quand `/etc/hosts` définit `localhost` en dual-stack ; le serveur Next écoute en IPv4 (`0.0.0.0`) uniquement → pas de fallback, erreur `Connection refused`. L'adresse IP explicite évite ce piège (validé C6).
- Le **stockage des données reste dans le localStorage du navigateur** : le conteneur n'a besoin d'aucun volume persistant.
## 4. Workflow Docker Hub
### 4.1 Build et test local
```bash
# Build de l'image
docker build -t cv-maker:latest .
# Test rapide
docker run --rm -p 3000:3000 cv-maker:latest
# → http://localhost:3000
# Inspection (taille, couches)
docker images cv-maker
```
### 4.2 Publication sur Docker Hub
```bash
# 1. Se connecter au registre
docker login
# 2. Taguer avec le namespace du compte (placeholder à remplacer)
docker tag cv-maker:latest <mon-user-dockerhub>/cv-maker:latest
# 3. Optionnel : tag avec version (semver)
docker tag cv-maker:latest <mon-user-dockerhub>/cv-maker:1.0.0
# 4. Pousser
docker push <mon-user-dockerhub>/cv-maker:latest
docker push <mon-user-dockerhub>/cv-maker:1.0.0
```
> Remplacez `<mon-user-dockerhub>` par votre identifiant Docker Hub. Le pattern `latest` + `1.0.0` (semver) est recommandé : `latest` pour les déploiements courants, un tag précis pour les retours arrière.
### 4.3 Déploiement depuis l'image publiée
```bash
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 (après édition de image: dans docker-compose.yml)
docker compose up -d
docker compose down # arrêt
```
## 5. Recette de validation conteneurisée
| # | Action | Résultat attendu |
| --- | --- | --- |
| C1 | `docker build -t cv-maker:latest .` | Build multi-stage sans erreur ; maintenant `deps` cache le layer npm |
| C2 | `docker images cv-maker` | Image < 200 Mo (standalone) |
| C3 | `docker run --rm -p 3000:3000 cv-maker:latest` puis ouvrir `localhost:3000` | L'éditeur s'affiche ; le PWA `sw.js` des réponses |
| C4 | Saisir du contenu, recharger | Persistance localStorage fonctionnelle dans le conteneur |
| C5 | `docker exec <ctr> wget -qO- localhost:3000/documentation` | HTTP 200, contenu de la page Documentation |
| C6 | `docker compose up -d` puis `docker compose ps` | Conteneur `healthy`, port mappé |
| C7 | `docker push <mon-user-dockerhub>/cv-maker:latest` | Image visible sur Docker Hub (page du compte) |
| C8 | `docker pull/run` depuis Docker Hub sur une autre machine | L'application démarre identiquement |
## 6. Dépannage
| Problème | Cause probable | Remède |
| --- | --- | --- |
| `pnpm: command not found` dans le build | `corepack enable` avant `pnpm` | Vérifier l'ordre des instructions dans `deps` |
| `Error: Cannot find module server.js` | sortie standalone absente | Vérifier `output: "standalone"` dans `next.config.ts` avant `pnpm build` |
| `sw.js` (PWA) non servi | `public/` oublié dans le runner | Ajouter `COPY --from=builder /app/public ./public` |
| Port déjà occupé en local | un autre process sur 3000 | Changer la mappage : `-p 3001:3000` |
| Image trop lourde | `.dockerignore` absent/incomplet | Re-vérifier les exclusions `node_modules`, `.next` |
| `frozen-lockfile` échoue | `pnpm-lock.yaml` obsolète | Re-synchroniser en local (`pnpm install`) puis commit du lockfile |
## 7. Évolutions (non bloquantes v1)
- **CI/CD** : workflow GitHub Actions (ou GitLab CI) : stage `build` → `push` sur Docker Hub uniquement sur les tags/`main` ; secrets `DOCKERHUB_USERNAME` / `DOCKERHUB_TOKEN` stockés dans les paramètres du dépôt.
- **Multiplateformes** : `docker buildx build --platform linux/amd64,linux/arm64` pour ARM (Raspberry, Apple Silicon serveurs).
- **Scan de sécurité** : `docker scout cves <image>` avant chaque push.
+32
View File
@@ -0,0 +1,32 @@
# Dossier de conception — CV-Maker
Dossier de conception détaillé du projet **CV-Maker**, application web de création de CV en temps réel.
## Sommaire
| Document | Contenu |
| --- | --- |
| [01-analyse-fonctionnelle.md](./01-analyse-fonctionnelle.md) | Analyse du besoin, fonctionnalités détaillées issues du GOAL.md, spécifications écran par écran |
| [02-architecture-technique.md](./02-architecture-technique.md) | Choix technique, architecture en couches, organisation des dossiers `app/`, `components/`, `lib/` |
| [03-modele-donnees.md](./03-modele-donnees.md) | Contracts TypeScript, schéma de données localStorage, hooks de persistance |
| [04-specifications-ui.md](./04-specifications-ui.md) | Arborescence des composants, design tokens Tailwind, contraintes du format A4, états UI |
| [05-etapes-realisation.md](./05-etapes-realisation.md) | Plan d'implémentation pas-à-pas : commandes exactes (pnpm), ordre des fichiers, livrables |
| [06-securite-rgpd.md](./06-securite-rgpd.md) | Sécurité (DOMPurify / XSS), traitement des données, contenu de la page Mentions légales |
| [07-plan-tests.md](./07-plan-tests.md) | Recette manuelle fonctionnelle, critères d'acceptation liés au GOAL.md |
| [08-deploiement-docker.md](./08-deploiement-docker.md) | Dockerisation : Dockerfile multi-stage, docker-compose, publication sur Docker Hub |
## Ordre de lecture conseillé
1. `01-analyse-fonctionnelle.md` — comprendre le produit
2. `02-architecture-technique.md` — comprendre la stack
3. `03-modele-donnees.md` — comprendre les données
4. `04-specifications-ui.md` — comprendre l'interface
5. `05-etapes-realisation.md` — implémenter (feuille de route)
6. `06-securite-rgpd.md` — traiter sécurité et conformité
7. `07-plan-tests.md` — valider la livraison
8. `08-deploiement-docker.md` — conteneuriser et publier l'image
## Références
- Document source : [`GOAL.md`](../../GOAL.md)
- Documentation technique d'exploitation : [`README.md`](../../README.md)