8.4 KiB
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 6 zones simples.
2. Contracts TypeScript
lib/types/cv.ts :
/** Identifiant des 6 zones de saisie éditées par onglets. */
export type CVTabId =
| "accroche"
| "etatCivil"
| "formations"
| "competences"
| "softSkills"
| "centresInteret";
/** Ordre d'affichage des onglets (1 → 6). */
export const CV_TAB_ORDER: CVTabId[] = [
"accroche",
"etatCivil",
"formations",
"competences",
"softSkills",
"centresInteret",
];
/** Métadonnées d'affichage d'un onglet. */
export interface CVTabMeta {
id: CVTabId;
numero: 1 | 2 | 3 | 4 | 5 | 6; // 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" },
competences: { id: "competences", numero: 4, label: "Compétences" },
softSkills: { id: "softSkills", numero: 5, label: "Soft skills" },
centresInteret: { id: "centresInteret", numero: 6, 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
competences: string; // zone 4
softSkills: string; // zone 5
centresInteret: string; // zone 6
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
export function createDefaultCVData(): CVCareerData {
return {
version: 1,
accroche: "",
etatCivil: "",
formations: "",
competences: "",
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).
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 :
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) ; leload()est décalé dans un effet (setTimeout 0) après montage pour éviter toute divergence serveur/client. - Écriture automatique :
useEffectsurvalue→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 :
export interface UseCVDataReturn {
data: CVCareerData;
setText(tab: CVTabId, value: string): void; // édition d'une zone 1..6
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 champscreatedAt/updatedAtsont 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
markedchange) → on stocke toujours le Markdown source. - ❌ Utiliser l'index du tableau comme identifiant (cas d'une suppression → mauvaise référence dans React
keyet dansupdateExperience). - ❌ Écrire dans localStorage pendant le rendu initial (dépendances d'effet non maîtrisées) → écriture uniquement dans
useEffectdéclenché par un changement d'état.