feat: ajouter la section Compétences (6e bloc)

This commit is contained in:
devcodetools committed 2026-09-17 07:01:05 +02:00
1 parent a0d312a884
commit 92aaafb4aa
13 files changed
+75 -47

No files matched your search

+6 -5
View File
@@ -91,13 +91,13 @@ cv-maker/
│ ├── CvWorkspace.tsx # Assemblage 2 colonnes + toolbar │ ├── CvWorkspace.tsx # Assemblage 2 colonnes + toolbar
│ ├── CvToolbar.tsx # Barre d'app (PDF, JSON, liens) │ ├── CvToolbar.tsx # Barre d'app (PDF, JSON, liens)
│ ├── CvInputPanel.tsx # Panneau gauche (tabs + expériences) │ ├── CvInputPanel.tsx # Panneau gauche (tabs + expériences)
│ ├── CvTabs.tsx # Onglets 1..5 │ ├── CvTabs.tsx # Onglets 1..6
│ ├── MarkdownTextarea.tsx # Textarea contrôlé │ ├── MarkdownTextarea.tsx # Textarea contrôlé
│ ├── ExperienceManager.tsx # Formulaire + liste expériences │ ├── ExperienceManager.tsx # Formulaire + liste expériences
│ ├── ExperienceItem.tsx # Ligne « n. titre [Modifier] [×] » │ ├── ExperienceItem.tsx # Ligne « n. titre [Modifier] [×] »
│ ├── CvPreview.tsx # Feuille A4 │ ├── CvPreview.tsx # Feuille A4
│ ├── CvHeader.tsx # Accroche (zone 1) │ ├── CvHeader.tsx # Accroche (zone 1)
│ ├── CvLeftColumn.tsx # Sections 2→5 (1/3) │ ├── CvLeftColumn.tsx # Sections 2→6 (1/3)
│ ├── CvSection.tsx # Section générique │ ├── CvSection.tsx # Section générique
│ ├── CvRightColumn.tsx # Expériences pro (2/3) │ ├── CvRightColumn.tsx # Expériences pro (2/3)
│ └── CvExperiencePreview.tsx │ └── CvExperiencePreview.tsx
@@ -131,8 +131,9 @@ interface CVCareerData {
accroche: string; // onglet 1 — message d'accroche accroche: string; // onglet 1 — message d'accroche
etatCivil: string; // onglet 2 — état civil + contacts + site + GitHub etatCivil: string; // onglet 2 — état civil + contacts + site + GitHub
formations: string; // onglet 3 — formations formations: string; // onglet 3 — formations
softSkills: string; // onglet 4 — soft skills competences: string; // onglet 4 — compétences techniques
centresInteret: string; // onglet 5 — centres d'intérêt softSkills: string; // onglet 5 — soft skills
centresInteret: string; // onglet 6 — centres d'intérêt
experiences: Experience[]; experiences: Experience[];
} }
@@ -145,7 +146,7 @@ interface Experience {
} }
``` ```
- Les 5 zones sont du **Markdown brut** (le HTML rendu n'est jamais stocké). - Les 6 zones sont du **Markdown brut** (le HTML rendu n'est jamais stocké).
- Les expériences sont affichées **dans l'ordre de saisie** : la 1ʳᵉ ajoutée en haut de la section « Expériences professionnelles », la suivante en dessous, etc. (la 1ʳᵉ porte le numéro 1). - Les expériences sont affichées **dans l'ordre de saisie** : la 1ʳᵉ ajoutée en haut de la section « Expériences professionnelles », la suivante en dessous, etc. (la 1ʳᵉ porte le numéro 1).
- Sauvegarde invalide/corrompue → état par défaut, sans crash. - Sauvegarde invalide/corrompue → état par défaut, sans crash.
+8 -7
View File
@@ -22,9 +22,10 @@ export default function DocumentationPage() {
<h2 className="text-xl font-semibold text-ink">Prise en main</h2> <h2 className="text-xl font-semibold text-ink">Prise en main</h2>
<ol className="mt-3 list-decimal space-y-2 pl-5 text-sm leading-relaxed text-ink"> <ol className="mt-3 list-decimal space-y-2 pl-5 text-sm leading-relaxed text-ink">
<li> <li>
<strong>Onglets 1 à 5</strong> — sélectionnez une zone de saisie (accroche, <strong>Onglets 1 à 6</strong> — sélectionnez une zone de saisie (accroche,
état civil + contacts, formations, soft skills, centres d&apos;intérêt). état civil + contacts, formations, compétences, soft skills, centres
Un seul onglet est actif à la fois ; chaque zone conserve son texte. d&apos;intérêt). Un seul onglet est actif à la fois ; chaque zone conserve son
texte.
</li> </li>
<li> <li>
<strong>Saisie Markdown</strong> — le texte est écrit en Markdown dans le <strong>Saisie Markdown</strong> — le texte est écrit en Markdown dans le
@@ -64,7 +65,7 @@ export default function DocumentationPage() {
<section className="mt-8"> <section className="mt-8">
<h2 className="text-xl font-semibold text-ink">Syntaxe Markdown supportée</h2> <h2 className="text-xl font-semibold text-ink">Syntaxe Markdown supportée</h2>
<p className="mt-2 text-sm leading-relaxed text-muted"> <p className="mt-2 text-sm leading-relaxed text-muted">
Les 5 zones et le contenu des expériences sont rendus en Markdown (GFM). Les 6 zones et le contenu des expériences sont rendus en Markdown (GFM).
</p> </p>
<div className="mt-4 overflow-x-auto rounded-md border border-border bg-white"> <div className="mt-4 overflow-x-auto rounded-md border border-border bg-white">
<table className="w-full text-sm text-ink"> <table className="w-full text-sm text-ink">
@@ -105,9 +106,9 @@ export default function DocumentationPage() {
<p className="mt-2 text-sm leading-relaxed text-ink"> <p className="mt-2 text-sm leading-relaxed text-ink">
Utilisez l&apos;onglet 1 pour le nom et le poste visé, l&apos;onglet 2 pour vos Utilisez l&apos;onglet 1 pour le nom et le poste visé, l&apos;onglet 2 pour vos
coordonnées (e-mail, téléphone, site web, GitHub), l&apos;onglet 3 pour vos coordonnées (e-mail, téléphone, site web, GitHub), l&apos;onglet 3 pour vos
formations, l&apos;onglet 4 pour vos soft skills et l&apos;onglet 5 pour vos formations, l&apos;onglet 4 pour vos compétences techniques, l&apos;onglet 5 pour
centres d&apos;intérêt. Complétez avec vos expériences professionnelles, de la plus vos soft skills et l&apos;onglet 6 pour vos centres d&apos;intérêt. Complétez avec
récente à la plus ancienne. vos expériences professionnelles, de la plus récente à la plus ancienne.
</p> </p>
</section> </section>
</main> </main>
+2
View File
@@ -12,6 +12,8 @@ const PLACEHOLDERS: Record<CVTabId, string> = {
"**Jean Dupont**\n- jean.dupont@email.fr\n- 06 12 34 56 78\n- [Site web](https://mon-site.fr)\n- [GitHub](https://github.com/jean-dupont)", "**Jean Dupont**\n- jean.dupont@email.fr\n- 06 12 34 56 78\n- [Site web](https://mon-site.fr)\n- [GitHub](https://github.com/jean-dupont)",
formations: formations:
"### Master Informatique\nUniversité — 2020\n\n### Licence Informatique\nUniversité — 2018", "### Master Informatique\nUniversité — 2020\n\n### Licence Informatique\nUniversité — 2018",
competences:
"- JavaScript / TypeScript\n- React / Next.js\n- Node.js\n- SQL",
softSkills: softSkills:
"- Esprit d'équipe\n- Autonomie\n- Curiosité technique", "- Esprit d'équipe\n- Autonomie\n- Curiosité technique",
centresInteret: centresInteret:
+3
View File
@@ -3,6 +3,7 @@ import { CvSection } from "@/components/cv/CvSection";
interface CvLeftColumnProps { interface CvLeftColumnProps {
etatCivilHtml: string; etatCivilHtml: string;
formationsHtml: string; formationsHtml: string;
competencesHtml: string;
softSkillsHtml: string; softSkillsHtml: string;
centresInteretHtml: string; centresInteretHtml: string;
} }
@@ -10,6 +11,7 @@ interface CvLeftColumnProps {
export function CvLeftColumn({ export function CvLeftColumn({
etatCivilHtml, etatCivilHtml,
formationsHtml, formationsHtml,
competencesHtml,
softSkillsHtml, softSkillsHtml,
centresInteretHtml, centresInteretHtml,
}: CvLeftColumnProps) { }: CvLeftColumnProps) {
@@ -17,6 +19,7 @@ export function CvLeftColumn({
<aside> <aside>
<CvSection title="État civil" html={etatCivilHtml} /> <CvSection title="État civil" html={etatCivilHtml} />
<CvSection title="Formations" html={formationsHtml} /> <CvSection title="Formations" html={formationsHtml} />
<CvSection title="Compétences" html={competencesHtml} />
<CvSection title="Soft skills" html={softSkillsHtml} /> <CvSection title="Soft skills" html={softSkillsHtml} />
<CvSection title="Centres d'intérêt" html={centresInteretHtml} /> <CvSection title="Centres d'intérêt" html={centresInteretHtml} />
</aside> </aside>
+5
View File
@@ -12,6 +12,10 @@ export function CvPreview({ data }: { data: CVCareerData }) {
const accrocheHtml = useMemo(() => markdownToHtml(data.accroche), [data.accroche]); const accrocheHtml = useMemo(() => markdownToHtml(data.accroche), [data.accroche]);
const etatCivilHtml = useMemo(() => markdownToHtml(data.etatCivil), [data.etatCivil]); const etatCivilHtml = useMemo(() => markdownToHtml(data.etatCivil), [data.etatCivil]);
const formationsHtml = useMemo(() => markdownToHtml(data.formations), [data.formations]); const formationsHtml = useMemo(() => markdownToHtml(data.formations), [data.formations]);
const competencesHtml = useMemo(
() => markdownToHtml(data.competences),
[data.competences],
);
const softSkillsHtml = useMemo(() => markdownToHtml(data.softSkills), [data.softSkills]); const softSkillsHtml = useMemo(() => markdownToHtml(data.softSkills), [data.softSkills]);
const centresInteretHtml = useMemo( const centresInteretHtml = useMemo(
() => markdownToHtml(data.centresInteret), () => markdownToHtml(data.centresInteret),
@@ -34,6 +38,7 @@ export function CvPreview({ data }: { data: CVCareerData }) {
<CvLeftColumn <CvLeftColumn
etatCivilHtml={etatCivilHtml} etatCivilHtml={etatCivilHtml}
formationsHtml={formationsHtml} formationsHtml={formationsHtml}
competencesHtml={competencesHtml}
softSkillsHtml={softSkillsHtml} softSkillsHtml={softSkillsHtml}
centresInteretHtml={centresInteretHtml} centresInteretHtml={centresInteretHtml}
/> />
+11 -7
View File
@@ -39,7 +39,7 @@ CV-Maker est une application web **sans backend** permettant à un utilisateur d
│ Panneau GAUCHE │ Panneau DROIT │ │ Panneau GAUCHE │ Panneau DROIT │
│ 1/3 de la largeur │ 2/3 de la largeur │ │ 1/3 de la largeur │ 2/3 de la largeur │
│ │ │ │ │ │
│ [Tabs 1 ▸ 2 ▸ 3 ▸ 4 ▸ 5] │ ┌────────────────────────┐ │ │ [Tabs 1 ▸ 2 ▸ 3 ▸ 4 ▸ 5 ▸ 6] │ ┌────────────────────────┐ │
│ │ │ Feuille A4 │ │ │ │ │ Feuille A4 │ │
│ [MarkdownTextarea : │ │ ┌────────────────────┐ │ │ │ [MarkdownTextarea : │ │ ┌────────────────────┐ │ │
│ zone active] │ │ │ Header accroche │ │ │ │ zone active] │ │ │ Header accroche │ │ │
@@ -50,6 +50,8 @@ CV-Maker est une application web **sans backend** permettant à un utilisateur d
│ • 1. Titre exp [Modif] [×] │ │ │ civil │ profession.│ │ │ │ • 1. Titre exp [Modif] [×] │ │ │ civil │ profession.│ │ │
│ • 2. Titre exp [Modif] [×] │ │ │ Forma-│ ... │ │ │ │ • 2. Titre exp [Modif] [×] │ │ │ Forma-│ ... │ │ │
│ │ │ │ tions │ │ │ │ │ │ │ │ tions │ │ │ │
│ │ │ │ Compé- │ │ │ │
│ │ │ │ tences │ │ │ │
│ │ │ │ Soft │ │ │ │ │ │ │ │ Soft │ │ │ │
│ │ │ │ skills│ │ │ │ │ │ │ │ skills│ │ │ │
│ │ │ │ Centr.│ │ │ │ │ │ │ │ Centr.│ │ │ │
@@ -63,15 +65,16 @@ CV-Maker est une application web **sans backend** permettant à un utilisateur d
#### Structure #### Structure
- **Zones 1 à 5** gérées par un **système d'onglets (tabs)** numérotés de 1 à 5. Un seul onglet actif à la fois ; chaque onglet affiche une `textarea` Markdown dédiée. - **Zones 1 à 6** gérées par un **système d'onglets (tabs)** numérotés de 1 à 6. Un seul onglet actif à la fois ; chaque onglet affiche une `textarea` Markdown dédiée.
| N° onglet | Zone | Champ(s) du CV associé(s) | Préfixe de navigation | | N° onglet | Zone | Champ(s) du CV associé(s) | Préfixe de navigation |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| 1 | Message d'accroche | En-tête du CV (sous le nom/poste) | | | 1 | Message d'accroche | En-tête du CV (sous le nom/poste) | |
| 2 | État civil + contacts | Nom, prénom, adresse, e-mail, téléphone, **site web**, **lien GitHub** | | | 2 | État civil + contacts | Nom, prénom, adresse, e-mail, téléphone, **site web**, **lien GitHub** | |
| 3 | Formations | Liste des formations / diplômes | | | 3 | Formations | Liste des formations / diplômes | |
| 4 | Soft skills | Compétences comportementales | | | 4 | Compétences | Compétences techniques (langages, frameworks, outils) | |
| 5 | Centres d'intérêt | Loisirs, activités | | | 5 | Soft skills | Compétences comportementales | |
| 6 | Centres d'intérêt | Loisirs, activités | |
- **Zone « Expériences »** : fonctionne **hors du système d'onglets**. Elle est toujours visible sous la zone d'onglets. Elle sert à saisir **toutes les expériences professionnelles**, une par une. - **Zone « Expériences »** : fonctionne **hors du système d'onglets**. Elle est toujours visible sous la zone d'onglets. Elle sert à saisir **toutes les expériences professionnelles**, une par une.
@@ -88,11 +91,12 @@ CV-Maker est une application web **sans backend** permettant à un utilisateur d
- Une seule feuille blanche au **format A4** (210 × 297 mm), avec une marge définie, représentant 1 page de CV. - Une seule feuille blanche au **format A4** (210 × 297 mm), avec une marge définie, représentant 1 page de CV.
- **Header** (toute la largeur de la feuille) : rendu de l'**onglet 1 — message d'accroche**. - **Header** (toute la largeur de la feuille) : rendu de l'**onglet 1 — message d'accroche**.
- **Colonne gauche (1/3 de la largeur utile)** : rendus des sections correspondant aux **onglets 2 à 5** : - **Colonne gauche (1/3 de la largeur utile)** : rendus des sections correspondant aux **onglets 2 à 6** :
- État civil (zone 2) : contacts, site web, GitHub ; - État civil (zone 2) : contacts, site web, GitHub ;
- Formations (zone 3) ; - Formations (zone 3) ;
- Soft skills (zone 4) ; - Compétences (zone 4) : compétences techniques ;
- Centres d'intérêt (zone 5). - Soft skills (zone 5) ;
- Centres d'intérêt (zone 6).
- **Colonne droite (2/3 de la largeur utile)** : rendu des **expériences professionnelles**, sous le titre fixe **« Expériences professionnelles »**. - **Colonne droite (2/3 de la largeur utile)** : rendu des **expériences professionnelles**, sous le titre fixe **« Expériences professionnelles »**.
- Le rendu est **temps réel** : toute frappe dans une zone de saisie met à jour la zone cible correspondante instantanément. - Le rendu est **temps réel** : toute frappe dans une zone de saisie met à jour la zone cible correspondante instantanément.
+2 -2
View File
@@ -108,13 +108,13 @@ components/
├── CvWorkspace.tsx → layout 2 colonnes + barre d'app ├── CvWorkspace.tsx → layout 2 colonnes + barre d'app
├── CvToolbar.tsx → barre d'application (print, export/import JSON, liens) ├── CvToolbar.tsx → barre d'application (print, export/import JSON, liens)
├── CvInputPanel.tsx → panneau gauche ├── CvInputPanel.tsx → panneau gauche
│ ├── CvTabs.tsx → tabs 1..5 (labels + numéros) │ ├── CvTabs.tsx → tabs 1..6 (labels + numéros)
│ ├── MarkdownTextarea.tsx → {label, value, onChange} (textarea md) │ ├── MarkdownTextarea.tsx → {label, value, onChange} (textarea md)
│ └── ExperienceManager.tsx │ └── ExperienceManager.tsx
│ └── ExperienceItem.tsx → ligne « n. titre [Modifier] [×] » │ └── ExperienceItem.tsx → ligne « n. titre [Modifier] [×] »
└── CvPreview.tsx → panneau droit (feuille A4) └── CvPreview.tsx → panneau droit (feuille A4)
├── CvHeader.tsx → accroche (zone 1) ├── CvHeader.tsx → accroche (zone 1)
├── CvLeftColumn.tsx → 1/3 (zones 2-5) ├── CvLeftColumn.tsx → 1/3 (zones 2-6)
│ └── CvSection.tsx → section générique {titre, html} │ └── CvSection.tsx → section générique {titre, html}
└── CvRightColumn.tsx → 2/3 (Expériences professionnelles) └── CvRightColumn.tsx → 2/3 (Expériences professionnelles)
└── CvExperiencePreview.tsx └── CvExperiencePreview.tsx
+14 -9
View File
@@ -5,26 +5,28 @@
- **Aucun serveur** : le « modèle de données » = structure de l'état applicatif + son image persistée dans **localStorage**. - **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`. - **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é. - **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. - Les expériences sont une **liste d'objets** (avec identifiant stable), contrairement aux 6 zones simples.
## 2. Contracts TypeScript ## 2. Contracts TypeScript
`lib/types/cv.ts` : `lib/types/cv.ts` :
```ts ```ts
/** Identifiant des 5 zones de saisie éditées par onglets. */ /** Identifiant des 6 zones de saisie éditées par onglets. */
export type CVTabId = export type CVTabId =
| "accroche" | "accroche"
| "etatCivil" | "etatCivil"
| "formations" | "formations"
| "competences"
| "softSkills" | "softSkills"
| "centresInteret"; | "centresInteret";
/** Ordre d'affichage des onglets (1 → 5). */ /** Ordre d'affichage des onglets (1 → 6). */
export const CV_TAB_ORDER: CVTabId[] = [ export const CV_TAB_ORDER: CVTabId[] = [
"accroche", "accroche",
"etatCivil", "etatCivil",
"formations", "formations",
"competences",
"softSkills", "softSkills",
"centresInteret", "centresInteret",
]; ];
@@ -32,7 +34,7 @@ export const CV_TAB_ORDER: CVTabId[] = [
/** Métadonnées d'affichage d'un onglet. */ /** Métadonnées d'affichage d'un onglet. */
export interface CVTabMeta { export interface CVTabMeta {
id: CVTabId; id: CVTabId;
numero: 1 | 2 | 3 | 4 | 5; // numéro affiché sur l'onglet numero: 1 | 2 | 3 | 4 | 5 | 6; // numéro affiché sur l'onglet
label: string; // libellé court de l'onglet label: string; // libellé court de l'onglet
} }
@@ -40,8 +42,9 @@ export const CV_TABS: Record<CVTabId, CVTabMeta> = {
accroche: { id: "accroche", numero: 1, label: "Accroche" }, accroche: { id: "accroche", numero: 1, label: "Accroche" },
etatCivil: { id: "etatCivil", numero: 2, label: "État civil" }, etatCivil: { id: "etatCivil", numero: 2, label: "État civil" },
formations: { id: "formations", numero: 3, label: "Formations" }, formations: { id: "formations", numero: 3, label: "Formations" },
softSkills: { id: "softSkills", numero: 4, label: "Soft skills" }, competences: { id: "competences", numero: 4, label: "Compétences" },
centresInteret: { id: "centresInteret", numero: 5, label: "Centres d'intérêt" }, softSkills: { id: "softSkills", numero: 5, label: "Soft skills" },
centresInteret: { id: "centresInteret", numero: 6, label: "Centres d'intérêt" },
}; };
/** Une expérience professionnelle. */ /** Une expérience professionnelle. */
@@ -59,8 +62,9 @@ export interface CVCareerData {
accroche: string; // zone 1 accroche: string; // zone 1
etatCivil: string; // zone 2 etatCivil: string; // zone 2
formations: string; // zone 3 formations: string; // zone 3
softSkills: string; // zone 4 competences: string; // zone 4
centresInteret: string; // zone 5 softSkills: string; // zone 5
centresInteret: string; // zone 6
experiences: Experience[]; // liste, ordre = ordre d'affichage dans le CV experiences: Experience[]; // liste, ordre = ordre d'affichage dans le CV
} }
@@ -81,6 +85,7 @@ export function createDefaultCVData(): CVCareerData {
accroche: "", accroche: "",
etatCivil: "", etatCivil: "",
formations: "", formations: "",
competences: "",
softSkills: "", softSkills: "",
centresInteret: "", centresInteret: "",
experiences: [], experiences: [],
@@ -141,7 +146,7 @@ Regroupe toute la logique d'édition du CV, façade unique pour les composants :
export interface UseCVDataReturn { export interface UseCVDataReturn {
data: CVCareerData; data: CVCareerData;
setText(tab: CVTabId, value: string): void; // édition d'une zone 1..5 setText(tab: CVTabId, value: string): void; // édition d'une zone 1..6
addExperience(titre: string, contenu: string): void; // ajoute en fin de liste addExperience(titre: string, contenu: string): void; // ajoute en fin de liste
updateExperience(id: string, titre: string, contenu: string): void; updateExperience(id: string, titre: string, contenu: string): void;
removeExperience(id: string): void; // suppression + confirmation côté UI removeExperience(id: string): void; // suppression + confirmation côté UI
+5 -4
View File
@@ -44,10 +44,10 @@ Rendre les liens actifs en `font-semibold` et inactifs en `text-muted` (page cou
## 3. Panneau latéral gauche ## 3. Panneau latéral gauche
### 3.1 `CvTabs` — onglets 1 à 5 ### 3.1 `CvTabs` — onglets 1 à 6
- Basé sur la primitive `ui/Tabs` : `role="tablist"`, boutons `role="tab"`, `aria-selected`, `role="tabpanel"`. - Basé sur la primitive `ui/Tabs` : `role="tablist"`, boutons `role="tab"`, `aria-selected`, `role="tabpanel"`.
- Affichage : barre de 5 boutons horizontaux, libellé = `numéro + label` (`1 · Accroche`, `2 · État civil`, …). - Affichage : barre de 6 boutons horizontaux, libellé = `numéro + label` (`1 · Accroche`, `2 · État civil`, …).
- Onglet actif : fond `accent`, texte blanc ; inactifs : bordure `border` + texte `muted`. - Onglet actif : fond `accent`, texte blanc ; inactifs : bordure `border` + texte `muted`.
- **Contenu piloté par le parent** (`activeTab` dans `CvInputPanel`) : un seul `MarkdownTextarea` monté pour l'onglet actif. Le texte de l'onglet actif est le `value` contrôlé. - **Contenu piloté par le parent** (`activeTab` dans `CvInputPanel`) : un seul `MarkdownTextarea` monté pour l'onglet actif. Le texte de l'onglet actif est le `value` contrôlé.
@@ -64,7 +64,7 @@ Rendre les liens actifs en `font-semibold` et inactifs en `text-muted` (page cou
Composition : Composition :
- **En-tête** : titre « Expériences » + bouton **+** (`ui/Button`, variante `primary`) → prépare une nouvelle expérience (zones titre/contenu vidées, focus du formulaire). - **En-tête** : titre « Expériences » + bouton **+** (`ui/Button`, variante `primary`) → prépare une nouvelle expérience (zones titre/contenu vidées, focus du formulaire).
- **Formulaire** : deux champs — `titre` (input texte) + `contenu` (textarea Markdown, même composant que les zones 1-5). Boutons : **Enregistrer** et **Annuler** (annulation quand modification). - **Formulaire** : deux champs — `titre` (input texte) + `contenu` (textarea Markdown, même composant que les zones 1-6). Boutons : **Enregistrer** et **Annuler** (annulation quand modification).
- **Liste numérotée** : sous le formulaire. - **Liste numérotée** : sous le formulaire.
États internes suivants : États internes suivants :
@@ -118,13 +118,14 @@ Ratio 1/3–2/3 : `.cv-sheet-body { display:grid; grid-template-columns: 1fr 2fr
- Rendu Markdown de `data.accroche` (contenu typiquement : nom + poste + accroche). - Rendu Markdown de `data.accroche` (contenu typiquement : nom + poste + accroche).
- `border-bottom` de séparation avec le corps de feuille. - `border-bottom` de séparation avec le corps de feuille.
### 4.3 `CvLeftColumn` (sections 2 à 5, 1/3) ### 4.3 `CvLeftColumn` (sections 2 à 6, 1/3)
Section générique `CvSection` : Section générique `CvSection` :
```tsx ```tsx
<CvSection title="État civil" html={htmlEtatCivil} /> <CvSection title="État civil" html={htmlEtatCivil} />
<CvSection title="Formations" html={htmlFormations} /> <CvSection title="Formations" html={htmlFormations} />
<CvSection title="Compétences" html={htmlCompetences} />
<CvSection title="Soft skills" html={htmlSoftSkills} /> <CvSection title="Soft skills" html={htmlSoftSkills} />
<CvSection title="Centres d'intérêt" html={htmlCentresInteret} /> <CvSection title="Centres d'intérêt" html={htmlCentresInteret} />
``` ```
+2 -2
View File
@@ -148,9 +148,9 @@ Contrôle visuel : saisir du texte dans chaque onglet, ajouter/modifier/supprime
Créer dans `components/cv/` : Créer dans `components/cv/` :
1. `CvPreview.tsx` — wrapper feuille (dimensions `210mm`/`297mm`, ombre, classe d'impression) ; appelle `markdownToHtml` depuis `lib/markdown` pour les 5 zones + chaque expérience ; compose header/colonnes. 1. `CvPreview.tsx` — wrapper feuille (dimensions `210mm`/`297mm`, ombre, classe d'impression) ; appelle `markdownToHtml` depuis `lib/markdown` pour les 6 zones + chaque expérience ; compose header/colonnes.
2. `CvHeader.tsx` — accroche (zone 1). 2. `CvHeader.tsx` — accroche (zone 1).
3. `CvLeftColumn.tsx` — 4 × `CvSection` (État civil, Formations, Soft skills, Centres d'intérêt). 3. `CvLeftColumn.tsx` — 5 × `CvSection` (État civil, Formations, Compétences, Soft skills, Centres d'intérêt).
4. `CvSection.tsx` — titre stylé + `dangerouslySetInnerHTML` (HTML assaini) ; masquée si vide. 4. `CvSection.tsx` — titre stylé + `dangerouslySetInnerHTML` (HTML assaini) ; masquée si vide.
5. `CvRightColumn.tsx` — titre « Expériences professionnelles » + liste `CvExperiencePreview` + message vide alternatif (`print:hidden`). 5. `CvRightColumn.tsx` — titre « Expériences professionnelles » + liste `CvExperiencePreview` + message vide alternatif (`print:hidden`).
6. `CvExperiencePreview.tsx` — titre en gras + HTML assaini du contenu. 6. `CvExperiencePreview.tsx` — titre en gras + HTML assaini du contenu.
+1 -1
View File
@@ -45,7 +45,7 @@ DOM
## 3. Données personnelles : état de l'analyse ## 3. Données personnelles : état de l'analyse
Les zones 1 à 5 du CV contiennent des **données à caractère personnel** (nom, e-mail, téléphone, histoire professionnelle). Les zones 1 à 6 du CV contiennent des **données à caractère personnel** (nom, e-mail, téléphone, histoire professionnelle).
| Propriété | État | | Propriété | État |
| --- | --- | | --- | --- |
+8 -7
View File
@@ -30,18 +30,19 @@
| G2 | Réduire la fenêtre sous 1024 px | Les panneaux s'empilent (gauche au-dessus de la droite) sans casse | | G2 | Réduire la fenêtre sous 1024 px | Les panneaux s'empilent (gauche au-dessus de la droite) sans casse |
| G3 | Vérifier la barre d'app et le footer | Barre : logo, bouton « Imprimer / PDF », lien Documentation. Footer : lien Mentions légales (masqué à l'impression) | | G3 | Vérifier la barre d'app et le footer | Barre : logo, bouton « Imprimer / PDF », lien Documentation. Footer : lien Mentions légales (masqué à l'impression) |
### 3.2 Onglets 1 à 5 (zones de saisie) ### 3.2 Onglets 1 à 6 (zones de saisie)
| # | Action | Résultat attendu | | # | Action | Résultat attendu |
| --- | --- | --- | | --- | --- | --- |
| T1 | Cliquer sur les onglets 1→5 | L'onglet actif change ; le textarea affiche le contenu **de cet onglet uniquement** | | T1 | Cliquer sur les onglets 1→6 | L'onglet actif change ; le textarea affiche le contenu **de cet onglet uniquement** |
| T2 | Saisir du Markdown dans l'onglet 1 (`**Jean Dupont**`) | Rendu temps réel dans le **header** de la feuille A4 (gras) | | T2 | Saisir du Markdown dans l'onglet 1 (`**Jean Dupont**`) | Rendu temps réel dans le **header** de la feuille A4 (gras) |
| T3 | Idem dans l'onglet 2 (nom, e-mail, site, GitHub en listes) | Rendu dans la section **État civil** (colonne 1/3) | | T3 | Idem dans l'onglet 2 (nom, e-mail, site, GitHub en listes) | Rendu dans la section **État civil** (colonne 1/3) |
| T4 | Onglet 3 (formation ex. `### Master` + liste) | Rendu dans la section **Formations** | | T4 | Onglet 3 (formation ex. `### Master` + liste) | Rendu dans la section **Formations** |
| T5 | Onglet 4 | Rendu dans la section **Soft skills** | | T5 | Onglet 4 (compétences ex. `- React\n- TypeScript`) | Rendu dans la section **Compétences** |
| T6 | Onglet 5 | Rendu dans la section **Centres d'intérêt** | | T6 | Onglet 5 | Rendu dans la section **Soft skills** |
| T7 | Basculer entre onglets après saisie | Chaque contenu est conservé, aucun écrasement croisé | | T7 | Onglet 6 | Rendu dans la section **Centres d'intérêt** |
| T8 | Vider complètement une section | La section correspondante disparaît de la preview | | T8 | Basculer entre onglets après saisie | Chaque contenu est conservé, aucun écrasement croisé |
| T9 | Vider complètement une section | La section correspondante disparaît de la preview |
### 3.3 Expériences professionnelles ### 3.3 Expériences professionnelles
@@ -128,7 +129,7 @@ Dès leur mise en place, ajouter à la recette :
| Persistance localStorage | P1–P5 | ☐ | | Persistance localStorage | P1–P5 | ☐ |
| Impression PDF | M1–M3 | ☐ | | Impression PDF | M1–M3 | ☐ |
| Expériences (+, liste, modif, ×) | X1–X9 | ☐ | | Expériences (+, liste, modif, ×) | X1–X9 | ☐ |
| Onglets 1-5 | T1–T8 | ☐ | | Onglets 1-6 | T1–T9 | ☐ |
| Documentation + Mentions légales | D1–D3 | ☐ | | Documentation + Mentions légales | D1–D3 | ☐ |
| PWA | W1–W5 | ☐ | | PWA | W1–W5 | ☐ |
+8 -3
View File
@@ -2,6 +2,7 @@ export type CVTabId =
| "accroche" | "accroche"
| "etatCivil" | "etatCivil"
| "formations" | "formations"
| "competences"
| "softSkills" | "softSkills"
| "centresInteret"; | "centresInteret";
@@ -9,13 +10,14 @@ export const CV_TAB_ORDER: CVTabId[] = [
"accroche", "accroche",
"etatCivil", "etatCivil",
"formations", "formations",
"competences",
"softSkills", "softSkills",
"centresInteret", "centresInteret",
]; ];
export interface CVTabMeta { export interface CVTabMeta {
id: CVTabId; id: CVTabId;
numero: 1 | 2 | 3 | 4 | 5; numero: 1 | 2 | 3 | 4 | 5 | 6;
label: string; label: string;
} }
@@ -23,10 +25,11 @@ export const CV_TABS: Record<CVTabId, CVTabMeta> = {
accroche: { id: "accroche", numero: 1, label: "Accroche" }, accroche: { id: "accroche", numero: 1, label: "Accroche" },
etatCivil: { id: "etatCivil", numero: 2, label: "État civil" }, etatCivil: { id: "etatCivil", numero: 2, label: "État civil" },
formations: { id: "formations", numero: 3, label: "Formations" }, formations: { id: "formations", numero: 3, label: "Formations" },
softSkills: { id: "softSkills", numero: 4, label: "Soft skills" }, competences: { id: "competences", numero: 4, label: "Compétences" },
softSkills: { id: "softSkills", numero: 5, label: "Soft skills" },
centresInteret: { centresInteret: {
id: "centresInteret", id: "centresInteret",
numero: 5, numero: 6,
label: "Centres d'intérêt", label: "Centres d'intérêt",
}, },
}; };
@@ -44,6 +47,7 @@ export interface CVCareerData {
accroche: string; accroche: string;
etatCivil: string; etatCivil: string;
formations: string; formations: string;
competences: string;
softSkills: string; softSkills: string;
centresInteret: string; centresInteret: string;
experiences: Experience[]; experiences: Experience[];
@@ -57,6 +61,7 @@ export function createDefaultCVData(): CVCareerData {
accroche: "", accroche: "",
etatCivil: "", etatCivil: "",
formations: "", formations: "",
competences: "",
softSkills: "", softSkills: "",
centresInteret: "", centresInteret: "",
experiences: [], experiences: [],