chore init
This commit is contained in:
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`) |
|
||||
Reference in new issue
Block a user