181 lines
8.2 KiB
Markdown
181 lines
8.2 KiB
Markdown
# 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. |