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:
@@ -4,9 +4,9 @@ import { cookie } from "@elysiajs/cookie";
|
||||
import { KeyCloak, generateState, generateCodeVerifier } from "arctic";
|
||||
import * as elements from "typed-html";
|
||||
import { BaseHtml } from "../components/BaseHtml";
|
||||
import { InputGroup, TextAreaGroup, ExperienceForm, EducationForm } from "../components/AdminForms";
|
||||
import { getAdminProfile, getAdminExperience, getAdminEducation, getAdminExperienceById, getAdminEducationById } from "../db/queries";
|
||||
import { updateProfile, createExperience, updateExperience, deleteExperience, createEducation, updateEducation, deleteEducation, updateExperienceOrder, updateEducationOrder } from "../db/mutations";
|
||||
import { InputGroup, TextAreaGroup, ExperienceForm, EducationForm, ProjectForm } from "../components/AdminForms";
|
||||
import { getAdminProfile, getAdminExperience, getAdminEducation, getAdminProjects, getAdminExperienceById, getAdminEducationById, getAdminProjectById } from "../db/queries";
|
||||
import { updateProfile, createExperience, updateExperience, deleteExperience, createEducation, updateEducation, deleteEducation, updateExperienceOrder, updateEducationOrder, createProject, updateProject, deleteProject, updateProjectOrder } from "../db/mutations";
|
||||
|
||||
// Initialize Keycloak (Arctic)
|
||||
const realmURL = process.env.KEYCLOAK_REALM_URL || "";
|
||||
@@ -44,14 +44,20 @@ const AdminLayout = ({ children }: elements.Children) => (
|
||||
|
||||
if (el._sortable) return;
|
||||
|
||||
console.log('Initializing Sortable on:', id);
|
||||
el._sortable = new Sortable(el, {
|
||||
group: id,
|
||||
draggable: 'form',
|
||||
handle: '.drag-handle',
|
||||
animation: 150,
|
||||
ghostClass: 'bg-blue-100',
|
||||
onStart: function(evt) {
|
||||
console.log('Drag started', evt);
|
||||
},
|
||||
onEnd: function (evt) {
|
||||
console.log('Drag ended', evt);
|
||||
const ids = Array.from(el.querySelectorAll('form')).map(f => f.getAttribute('data-id'));
|
||||
console.log('New order:', ids);
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -64,11 +70,13 @@ const AdminLayout = ({ children }: elements.Children) => (
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initSortable('experience-list', '/admin/experience/reorder');
|
||||
initSortable('education-list', '/admin/education/reorder');
|
||||
initSortable('project-list', '/admin/project/reorder');
|
||||
});
|
||||
|
||||
document.body.addEventListener('htmx:afterSwap', (evt) => {
|
||||
initSortable('experience-list', '/admin/experience/reorder');
|
||||
initSortable('education-list', '/admin/education/reorder');
|
||||
initSortable('project-list', '/admin/project/reorder');
|
||||
});
|
||||
`}
|
||||
</script>
|
||||
@@ -173,6 +181,7 @@ export const adminRoutes = new Elysia()
|
||||
const profile = getAdminProfile();
|
||||
const experience = getAdminExperience();
|
||||
const education = getAdminEducation();
|
||||
const projects = getAdminProjects();
|
||||
|
||||
return html(
|
||||
<BaseHtml>
|
||||
@@ -212,6 +221,19 @@ export const adminRoutes = new Elysia()
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Projects Section */}
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded shadow mb-8">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-2xl font-bold">Projects</h2>
|
||||
<button hx-post="/admin/project/new" hx-target="#project-list" hx-swap="beforeend" class="bg-orange-500 text-white px-3 py-1 rounded text-sm hover:bg-orange-600">+ Add Project</button>
|
||||
</div>
|
||||
<div id="project-list" class="">
|
||||
{projects.map((proj: any) => (
|
||||
<ProjectForm proj={proj} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Experience Section */}
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded shadow mb-8">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
@@ -249,6 +271,34 @@ export const adminRoutes = new Elysia()
|
||||
return Response.redirect("/admin/dashboard");
|
||||
})
|
||||
|
||||
// Projects
|
||||
.post("/admin/project/new", ({ set }) => {
|
||||
const id = createProject();
|
||||
const newItem = getAdminProjectById(Number(id));
|
||||
if (!newItem) return "";
|
||||
return <ProjectForm proj={newItem} />;
|
||||
})
|
||||
|
||||
.post("/admin/project/reorder", ({ body }) => {
|
||||
const { ids } = body as { ids: string[] };
|
||||
updateProjectOrder(ids.map(Number));
|
||||
return "OK";
|
||||
})
|
||||
|
||||
.post("/admin/project/:id", ({ params, body, set }) => {
|
||||
const id = Number(params.id);
|
||||
updateProject(id, body);
|
||||
const updatedProj = getAdminProjectById(id);
|
||||
if (!updatedProj) return "";
|
||||
return <ProjectForm proj={updatedProj} />;
|
||||
})
|
||||
|
||||
.post("/admin/project/:id/delete", ({ params, set }) => {
|
||||
deleteProject(Number(params.id));
|
||||
return "";
|
||||
})
|
||||
|
||||
// Experience
|
||||
.post("/admin/experience/new", ({ set }) => {
|
||||
const id = createExperience();
|
||||
const newItem = getAdminExperienceById(Number(id));
|
||||
@@ -275,6 +325,7 @@ export const adminRoutes = new Elysia()
|
||||
return "";
|
||||
})
|
||||
|
||||
// Education
|
||||
.post("/admin/education/new", ({ set }) => {
|
||||
const id = createEducation();
|
||||
const newItem = getAdminEducationById(Number(id));
|
||||
@@ -299,4 +350,4 @@ export const adminRoutes = new Elysia()
|
||||
.post("/admin/education/:id/delete", ({ params, set }) => {
|
||||
deleteEducation(Number(params.id));
|
||||
return "";
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user