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

+101
View File
@@ -0,0 +1,101 @@
"use client";
import { useCallback } from "react";
import {
createDefaultCVData,
type CVCareerData,
type CVTabId,
type Experience,
} from "@/lib/types/cv";
import { localStorageAdapter } from "@/lib/storage";
import { useLocalStorage } from "@/lib/hooks/useLocalStorage";
export interface UseCVDataReturn {
data: CVCareerData;
setText: (tab: CVTabId, value: string) => void;
addExperience: (titre: string, contenu: string) => void;
updateExperience: (id: string, titre: string, contenu: string) => void;
removeExperience: (id: string) => void;
resetCV: () => void;
importCV: (value: CVCareerData) => void;
}
const DEFAULT_TITLE = "Expérience";
export function useCVData(): UseCVDataReturn {
const [data, setData] = useLocalStorage<CVCareerData>(
localStorageAdapter,
createDefaultCVData(),
);
const setText = useCallback(
(tab: CVTabId, value: string) => {
setData((prev) => ({ ...prev, [tab]: value }));
},
[setData],
);
const addExperience = useCallback(
(titre: string, contenu: string) => {
setData((prev) => {
const now = Date.now();
const experience: Experience = {
id: crypto.randomUUID(),
titre: titre.trim() || DEFAULT_TITLE,
contenu,
createdAt: now,
updatedAt: now,
};
return { ...prev, experiences: [...prev.experiences, experience] };
});
},
[setData],
);
const updateExperience = useCallback(
(id: string, titre: string, contenu: string) => {
setData((prev) => ({
...prev,
experiences: prev.experiences.map((exp) =>
exp.id === id
? {
...exp,
titre: titre.trim() || DEFAULT_TITLE,
contenu,
updatedAt: Date.now(),
}
: exp,
),
}));
},
[setData],
);
const removeExperience = useCallback(
(id: string) => {
setData((prev) => ({
...prev,
experiences: prev.experiences.filter((exp) => exp.id !== id),
}));
},
[setData],
);
const resetCV = useCallback(() => {
setData(createDefaultCVData());
}, [setData]);
const importCV = useCallback((value: CVCareerData) => {
setData(value);
}, [setData]);
return {
data,
setText,
addExperience,
updateExperience,
removeExperience,
resetCV,
importCV,
};
}
+52
View File
@@ -0,0 +1,52 @@
"use client";
import { useEffect, useState } from "react";
export interface StorageLike<T> {
load(): T | null;
save(value: T): void;
}
const SAVE_DELAY_MS = 60;
export function useLocalStorage<T>(
storage: StorageLike<T>,
initialValue: T,
enable = true,
): [T, (value: T | ((prev: T) => T)) => void] {
const [value, setValue] = useState<T>(initialValue);
useEffect(() => {
if (!enable) return;
let disposed = false;
const timer = window.setTimeout(() => {
try {
const stored = storage.load();
if (disposed || stored === null || stored === undefined) return;
setValue(stored);
} catch (error) {
console.warn("Échec du chargement localStorage", error);
}
}, 0);
return () => {
disposed = true;
window.clearTimeout(timer);
};
}, [storage, enable]);
useEffect(() => {
if (!enable) return;
const timer = window.setTimeout(() => {
try {
storage.save(value);
} catch (error) {
console.warn("Échec de la sauvegarde localStorage", error);
}
}, SAVE_DELAY_MS);
return () => {
window.clearTimeout(timer);
};
}, [storage, value, enable]);
return [value, setValue];
}
+10
View File
@@ -0,0 +1,10 @@
import { marked } from "marked";
import DOMPurify from "dompurify";
marked.setOptions({ gfm: true, breaks: true });
export function markdownToHtml(source: string): string {
const rawHtml = marked.parse(source, { async: false }) as string;
if (typeof window === "undefined") return rawHtml;
return DOMPurify.sanitize(rawHtml, { USE_PROFILES: { html: true } });
}
+53
View File
@@ -0,0 +1,53 @@
import {
createDefaultCVData,
type CVCareerData,
} from "@/lib/types/cv";
export const STORAGE_KEY = "cv-maker:v1";
export interface CVStorageAdapter {
load(): CVCareerData | null;
save(data: CVCareerData): void;
clear(): void;
}
export function isCVCareerData(value: unknown): value is CVCareerData {
if (typeof value !== "object" || value === null) return false;
const data = value as Record<string, unknown>;
if (data.version !== 1) return false;
if (!Array.isArray(data.experiences)) return false;
return true;
}
export function normalizeCVCareerData(value: unknown): CVCareerData | null {
if (!isCVCareerData(value)) return null;
return { ...createDefaultCVData(), ...value, experiences: value.experiences };
}
export function createLocalStorageAdapter(): CVStorageAdapter {
return {
load() {
try {
const raw = window.localStorage.getItem(STORAGE_KEY);
if (raw === null) return null;
const parsed: unknown = JSON.parse(raw);
return normalizeCVCareerData(parsed);
} catch {
return null;
}
},
save(data) {
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
} catch (error) {
console.warn("Échec de la sauvegarde localStorage", error);
throw error;
}
},
clear() {
window.localStorage.removeItem(STORAGE_KEY);
},
};
}
export const localStorageAdapter = createLocalStorageAdapter();
+64
View File
@@ -0,0 +1,64 @@
export type CVTabId =
| "accroche"
| "etatCivil"
| "formations"
| "softSkills"
| "centresInteret";
export const CV_TAB_ORDER: CVTabId[] = [
"accroche",
"etatCivil",
"formations",
"softSkills",
"centresInteret",
];
export interface CVTabMeta {
id: CVTabId;
numero: 1 | 2 | 3 | 4 | 5;
label: string;
}
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" },
softSkills: { id: "softSkills", numero: 4, label: "Soft skills" },
centresInteret: {
id: "centresInteret",
numero: 5,
label: "Centres d'intérêt",
},
};
export interface Experience {
id: string;
titre: string;
contenu: string;
createdAt: number;
updatedAt: number;
}
export interface CVCareerData {
version: 1;
accroche: string;
etatCivil: string;
formations: string;
softSkills: string;
centresInteret: string;
experiences: Experience[];
}
export type CVStorage = CVCareerData;
export function createDefaultCVData(): CVCareerData {
return {
version: 1,
accroche: "",
etatCivil: "",
formations: "",
softSkills: "",
centresInteret: "",
experiences: [],
};
}