feat(ui): add cv application frontend and configuration

This commit is contained in:
Tuan-Dat Tran
2026-02-23 13:47:08 +01:00
parent cbf40908a6
commit 7f06ee7f53
43 changed files with 2849 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { fetchCV as apiFetchCV, updateCV as apiUpdateCV, exportCV as apiExportCV, importCV as apiImportCV } from '../../lib/api';
import { DEFAULT_DATA, validateCV } from '../../lib/cv-data';
const CVContext = createContext(null);
export function CVProvider({ children }) {
const [data, setData] = useState(DEFAULT_DATA);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
apiFetchCV()
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
console.error('Failed to fetch CV:', err);
setError(err.message);
setLoading(false);
});
}, []);
const updatePersonal = useCallback(async (personal) => {
const newData = { ...data, personal };
setData(newData);
await apiUpdateCV(newData);
}, [data]);
const updateExperience = useCallback(async (experience) => {
const newData = { ...data, experience };
setData(newData);
await apiUpdateCV(newData);
}, [data]);
const updateSkills = useCallback(async (skills) => {
const newData = { ...data, skills };
setData(newData);
await apiUpdateCV(newData);
}, [data]);
const updateEducation = useCallback(async (education) => {
const newData = { ...data, education };
setData(newData);
await apiUpdateCV(newData);
}, [data]);
const updateProjects = useCallback(async (projects) => {
const newData = { ...data, projects };
setData(newData);
await apiUpdateCV(newData);
}, [data]);
const exportJSON = useCallback(async () => {
const validation = validateCV(data);
if (!validation.valid) {
console.warn('Exporting CV with validation warnings:', validation.errors);
}
const blob = await apiExportCV();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'cv.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, [data]);
const importJSON = useCallback(async (jsonString) => {
try {
const parsed = JSON.parse(jsonString);
const validation = validateCV(parsed);
if (!validation.valid) {
return { success: false, errors: validation.errors };
}
await apiImportCV(parsed);
setData(parsed);
return { success: true };
} catch (e) {
return { success: false, errors: [`JSON parse error: ${e.message}`] };
}
}, []);
const resetToDefault = useCallback(async () => {
await apiUpdateCV(DEFAULT_DATA);
setData(DEFAULT_DATA);
}, []);
const refresh = useCallback(async () => {
setLoading(true);
try {
const data = await apiFetchCV();
setData(data);
setError(null);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, []);
return (
<CVContext.Provider value={{
data,
loading,
error,
updatePersonal,
updateExperience,
updateSkills,
updateEducation,
updateProjects,
exportJSON,
importJSON,
resetToDefault,
refresh
}}>
{children}
</CVContext.Provider>
);
}
// eslint-disable-next-line react-refresh/only-export-components
export function useCVData() {
const context = useContext(CVContext);
if (!context) {
throw new Error('useCVData must be used within a CVProvider');
}
return context;
}