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,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,
|
||||
};
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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 } });
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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: [],
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user