feat(db): Implement schema migrations and add Projects section

Introduces a custom migration system for SQLite, allowing incremental and safe schema evolution.
Adds a new 'Projects' section to the CV, including database tables, public UI, and full management
in the admin dashboard with live editing, drag-and-drop reordering, and collapsible forms.

Updates:
-  and  for schema management.
-  with  script.
-  to use migrations.
-  to rely on migrations.
-  and  for new project data operations.
-  and  for Projects UI.
-  and  to integrate the Projects section.

Also updates:
-  to automatically import Keycloak realm on startup.
-  for the Elysia app build.
-  with refined print styles (omitting socials and about).
This commit is contained in:
Tuan-Dat Tran
2025-11-22 11:20:03 +01:00
parent be0be3bd00
commit 3de8f6a971
18 changed files with 684 additions and 136 deletions

View File

@@ -56,6 +56,19 @@ interface Skill extends SkillTranslation {
display_order: number;
}
interface ProjectTranslation {
name: string;
description: string | null;
}
interface Project extends ProjectTranslation {
id: number;
project_url: string | null;
image_url: string | null;
tech_stack: string | null;
display_order: number;
}
export function getProfile(lang: string): Profile | null {
const profile = db.query(`
SELECT p.email, p.phone, p.website, p.github_url, p.linkedin_url, p.avatar_url,
@@ -91,6 +104,18 @@ export function getEducation(lang: string): Education[] {
return education;
}
export function getProjects(lang: string): Project[] {
const projects = db.query(`
SELECT p.id, p.project_url, p.image_url, p.tech_stack, p.display_order,
pt.name, pt.description
FROM projects p
JOIN project_translations pt ON p.id = pt.project_id
WHERE pt.language_code = $lang
ORDER BY p.display_order ASC
`).all({ $lang: lang }) as Project[];
return projects;
}
export function getSkills(lang: string): Skill[] {
const skills = db.query(`
SELECT s.category, s.icon, s.display_order,
@@ -108,6 +133,7 @@ export function getAllData(lang: string) {
profile: getProfile(lang),
experience: getExperience(lang),
education: getEducation(lang),
projects: getProjects(lang),
skills: getSkills(lang),
};
}
@@ -180,3 +206,27 @@ export function getAdminEducationById(id: number) {
});
return e;
}
export function getAdminProjects() {
const projs = db.query(`SELECT p.*, MAX(p.display_order) OVER () AS max_order FROM projects p ORDER BY p.display_order ASC`).all() as any[];
return projs.map(p => {
const trans = db.query(`SELECT * FROM project_translations WHERE project_id = $id`).all({ $id: p.id }) as any[];
trans.forEach(t => {
p[`name_${t.language_code}`] = t.name;
p[`description_${t.language_code}`] = t.description;
});
return p;
});
}
export function getAdminProjectById(id: number) {
const p = db.query(`SELECT * FROM projects WHERE id = $id`).get({ $id: id }) as any;
if (!p) return null;
const trans = db.query(`SELECT * FROM project_translations WHERE project_id = $id`).all({ $id: id }) as any[];
trans.forEach(t => {
p[`name_${t.language_code}`] = t.name;
p[`description_${t.language_code}`] = t.description;
});
return p;
}