Files

8.4 KiB
Raw Permalink Blame History

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) ; 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 :

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 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.