chore init

This commit is contained in:
devcodetools committed 2026-09-16 21:34:09 +02:00
1 parent 1b7406b1d3
commit a0d312a884
60 files changed
+5046 -125

No files matched your search

+141
View File
@@ -0,0 +1,141 @@
"use client";
import { useState, type FormEvent } from "react";
import { Plus, Save, X } from "lucide-react";
import { Button } from "@/components/ui/Button";
import { Textarea } from "@/components/ui/Textarea";
import { ExperienceItem } from "@/components/cv/ExperienceItem";
import type { Experience } from "@/lib/types/cv";
type ExperienceEditor =
| { mode: "closed" }
| { mode: "new" }
| { mode: "edit"; id: Experience["id"] };
interface ExperienceManagerProps {
experiences: Experience[];
onAdd: (titre: string, contenu: string) => void;
onUpdate: (id: string, titre: string, contenu: string) => void;
onRemove: (id: string) => void;
}
export function ExperienceManager({
experiences,
onAdd,
onUpdate,
onRemove,
}: ExperienceManagerProps) {
const [editor, setEditor] = useState<ExperienceEditor>({ mode: "closed" });
const [titre, setTitre] = useState("");
const [contenu, setContenu] = useState("");
const isEditing = editor.mode !== "closed";
const editingId = editor.mode === "edit" ? editor.id : null;
function startNew() {
setEditor({ mode: "new" });
setTitre("");
setContenu("");
}
function startEdit(id: string) {
const experience = experiences.find((exp) => exp.id === id);
if (!experience) return;
setEditor({ mode: "edit", id });
setTitre(experience.titre);
setContenu(experience.contenu);
}
function close() {
setEditor({ mode: "closed" });
setTitre("");
setContenu("");
}
function handleSubmit(event: FormEvent) {
event.preventDefault();
const trimmedTitre = titre.trim();
const title = trimmedTitre || "Expérience";
if (editor.mode === "new") {
onAdd(title, contenu);
} else if (editor.mode === "edit") {
onUpdate(editor.id, title, contenu);
}
close();
}
return (
<section aria-labelledby="experiences-title" className="mt-6 border-t border-border pt-4">
<div className="flex items-center justify-between gap-2">
<h2 id="experiences-title" className="text-sm font-semibold text-ink">
Expériences professionnelles
</h2>
<Button size="sm" onClick={startNew}>
<Plus size={14} aria-hidden />
Ajouter
</Button>
</div>
{isEditing && (
<form
onSubmit={handleSubmit}
className="mt-3 flex flex-col gap-3 rounded-md border border-accent bg-white p-3"
>
<div className="flex flex-col gap-1">
<label htmlFor="experience-titre" className="text-xs font-medium text-ink">
Titre
</label>
<input
id="experience-titre"
type="text"
value={titre}
onChange={(event) => setTitre(event.target.value)}
placeholder="Développeur Full-Stack"
autoFocus
className="rounded-md border border-border px-3 py-2 text-sm text-ink placeholder:text-muted focus:outline-2 focus:outline-offset-2 focus:outline-accent"
/>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="experience-contenu" className="text-xs font-medium text-ink">
Contenu (Markdown)
</label>
<Textarea
id="experience-contenu"
value={contenu}
onChange={(event) => setContenu(event.target.value)}
placeholder="- Stack : React, Next.js&#10;- Missions : …"
className="min-h-[120px]"
/>
</div>
<div className="flex gap-2">
<Button type="submit" size="sm">
<Save size={14} aria-hidden />
{editingId ? "Enregistrer" : "Ajouter"}
</Button>
<Button variant="ghost" size="sm" type="button" onClick={close}>
<X size={14} aria-hidden />
Annuler
</Button>
</div>
</form>
)}
<ol className="mt-3 flex flex-col gap-1.5">
{experiences.map((experience, index) => (
<ExperienceItem
key={experience.id}
index={index}
experience={experience}
onEdit={() => startEdit(experience.id)}
onRemove={() => onRemove(experience.id)}
/>
))}
{experiences.length === 0 && (
<p className="text-sm text-muted">
Aucune expérience. Utilisez le bouton « Ajouter » pour commencer.
</p>
)}
</ol>
</section>
);
}