chore init
This commit is contained in:
1 parent
1b7406b1d3
commit
a0d312a884
60 files changed
+5046
-125
No files matched your search
@@ -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 - 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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user