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

@@ -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`) |