diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..735f871 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +# Use the official Bun image +FROM oven/bun:1 as base +WORKDIR /usr/src/app + +# Install dependencies into temp directory +# This will cache them and speed up future builds +FROM base AS install +RUN mkdir -p /temp/dev +COPY package.json bun.lock /temp/dev/ +RUN cd /temp/dev && bun install --frozen-lockfile + +# Install with --production (exclude devDependencies) +RUN mkdir -p /temp/prod +COPY package.json bun.lock /temp/prod/ +RUN cd /temp/prod && bun install --frozen-lockfile --production + +# Copy node_modules from temp directory +# Then copy all (non-ignored) project files into the image +FROM base AS prerelease +COPY --from=install /temp/dev/node_modules node_modules +COPY . . + +# [Optional] Tests & Build +ENV NODE_ENV=production +# RUN bun test +# RUN bun run build + +# Final stage for app image +FROM base AS release +COPY --from=install /temp/prod/node_modules node_modules +COPY --from=prerelease /usr/src/app/src src +COPY --from=prerelease /usr/src/app/package.json . +COPY --from=prerelease /usr/src/app/bun.lock . +COPY --from=prerelease /usr/src/app/tsconfig.json . +COPY --from=prerelease /usr/src/app/tailwind.config.js . +COPY --from=prerelease /usr/src/app/postcss.config.js . + +# Ensure the DB directory/file exists or is volume mounted +# We will handle the DB via volume in docker-compose +# Copy the seed script so we can run it if needed +COPY --from=prerelease /usr/src/app/src/db/seed.ts src/db/seed.ts +COPY --from=prerelease /usr/src/app/src/db/schema.ts src/db/schema.ts + +# Expose port +EXPOSE 3000/tcp + +# Start the server +ENTRYPOINT [ "bun", "run", "src/index.tsx" ] diff --git a/docker-compose.yml b/docker-compose.yml index 56c85f5..36db8c3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,9 @@ services: keycloak: image: quay.io/keycloak/keycloak:23.0 - command: start-dev + command: start-dev --import-realm + volumes: + - ./realm-export.json:/opt/keycloak/data/import/realm.json environment: KC_DB: postgres KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak @@ -22,6 +24,7 @@ services: KC_DB_PASSWORD: password KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin + KC_HOSTNAME_URL: http://localhost:8080/ ports: - "8080:8080" depends_on: @@ -34,4 +37,4 @@ volumes: networks: keycloak_network: - driver: bridge + driver: bridge \ No newline at end of file diff --git a/package.json b/package.json index f6bf4f3..91fb298 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "version": "1.0.50", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "dev": "bun run --watch src/index.ts" + "dev": "bun run --watch src/index.ts", + "db:migrate": "bun run src/db/migrator.ts" }, "dependencies": { "@elysiajs/cookie": "^0.8.0", diff --git a/realm-export.json b/realm-export.json new file mode 100644 index 0000000..fabf3d0 --- /dev/null +++ b/realm-export.json @@ -0,0 +1,35 @@ +{ + "id": "myrealm", + "realm": "myrealm", + "enabled": true, + "users": [ + { + "username": "user", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "password" + } + ], + "realmRoles": ["admin"] + } + ], + "clients": [ + { + "clientId": "cv-app", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "urBMT3daJQQvPc05RvePnOlaK6MdQdSJ", + "redirectUris": [ + "http://localhost:3000/admin/callback" + ], + "webOrigins": [ + "http://localhost:3000" + ], + "publicClient": false, + "standardFlowEnabled": true, + "directAccessGrantsEnabled": true + } + ] +} diff --git a/src/components/AdminForms.tsx b/src/components/AdminForms.tsx index 51a3253..c208853 100644 --- a/src/components/AdminForms.tsx +++ b/src/components/AdminForms.tsx @@ -122,4 +122,53 @@ export const EducationForm = ({ edu }: any) => ( -); \ No newline at end of file +); + +export const ProjectForm = ({ proj }: any) => ( +
+ {/* Header / Drag Handle */} +
+
+
+ +
+
+

+ {proj.name_en || "New Project"} +

+
{proj.project_url || "No URL"}
+
+
+
+ +
+
+ + {/* Collapsible Content */} +
+
+ + +
+ + + +
+
+
ENGLISH
+ + +
+
+
GERMAN
+ + +
+
+
+ + +
+
+
+); diff --git a/src/components/BaseHtml.tsx b/src/components/BaseHtml.tsx index 6595889..ba56242 100644 --- a/src/components/BaseHtml.tsx +++ b/src/components/BaseHtml.tsx @@ -44,13 +44,148 @@ export const BaseHtml = ({ children }: elements.Children) => ` opacity: 0.8; /* Slight transparency */ } /* Class for the ghost/placeholder when item is dragged */ - .sortable-ghost { - background-color: theme('colors.blue.50'); /* Lighter background */ - border: 1px dashed theme('colors.blue.300'); /* Dashed border */ - } - - - - ${children} - + .sortable-ghost { + background-color: theme('colors.blue.50'); /* Lighter background */ + border: 1px dashed theme('colors.blue.300'); /* Dashed border */ + } + + + + + ${children} + `; diff --git a/src/components/Sections.tsx b/src/components/Sections.tsx index 7e483c2..ab94a54 100644 --- a/src/components/Sections.tsx +++ b/src/components/Sections.tsx @@ -98,6 +98,37 @@ export const EducationSection = ({ education }: any) => ( ); +export const ProjectsSection = ({ projects }: any) => ( +
+

+ + Projects +

+
+ {projects.map((proj: any) => ( + + {proj.image_url && ( +
+ {proj.name} +
+ )} +
+

{proj.name}

+ {proj.description &&

{proj.description}

} + {proj.tech_stack && ( +
+ {proj.tech_stack.split(',').map((tech: string) => ( + {tech.trim()} + ))} +
+ )} +
+
+ ))} +
+
+); + export const SkillsSection = ({ skills }: any) => (

@@ -117,4 +148,4 @@ export const SkillsSection = ({ skills }: any) => ( ))}

-); \ No newline at end of file +); diff --git a/src/db/migrations/001_baseline.ts b/src/db/migrations/001_baseline.ts new file mode 100644 index 0000000..466d4bd --- /dev/null +++ b/src/db/migrations/001_baseline.ts @@ -0,0 +1,54 @@ +import { Database } from "bun:sqlite"; + +export function up(db: Database) { + db.run("PRAGMA foreign_keys = ON;"); + + db.run(`CREATE TABLE IF NOT EXISTS languages (code TEXT PRIMARY KEY);`); + + db.run(` + CREATE TABLE IF NOT EXISTS profile ( + id INTEGER PRIMARY KEY CHECK (id = 1), + email TEXT NOT NULL, + phone TEXT, + website TEXT, + github_url TEXT, + linkedin_url TEXT, + avatar_url TEXT + ); + `); + + db.run(` + CREATE TABLE IF NOT EXISTS profile_translations ( + profile_id INTEGER NOT NULL, + language_code TEXT NOT NULL, + name TEXT NOT NULL, + job_title TEXT NOT NULL, + summary TEXT, + location TEXT, + PRIMARY KEY (profile_id, language_code), + FOREIGN KEY (profile_id) REFERENCES profile(id) ON DELETE CASCADE, + FOREIGN KEY (language_code) REFERENCES languages(code) + ); + `); + + db.run(` + CREATE TABLE IF NOT EXISTS skills ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category TEXT NOT NULL, + icon TEXT, + display_order INTEGER DEFAULT 0 + ); + `); + + db.run(` + CREATE TABLE IF NOT EXISTS skill_translations ( + skill_id INTEGER NOT NULL, + language_code TEXT NOT NULL, + name TEXT NOT NULL, + category_display TEXT, + PRIMARY KEY (skill_id, language_code), + FOREIGN KEY (skill_id) REFERENCES skills(id) ON DELETE CASCADE, + FOREIGN KEY (language_code) REFERENCES languages(code) + ); + `); +} diff --git a/src/db/migrations/002_experience_education.ts b/src/db/migrations/002_experience_education.ts new file mode 100644 index 0000000..67690d6 --- /dev/null +++ b/src/db/migrations/002_experience_education.ts @@ -0,0 +1,50 @@ +import { Database } from "bun:sqlite"; + +export function up(db: Database) { + // Experience + db.run(` + CREATE TABLE IF NOT EXISTS experience ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + start_date TEXT NOT NULL, + end_date TEXT, + company_url TEXT + ); + `); + + db.run(` + CREATE TABLE IF NOT EXISTS experience_translations ( + experience_id INTEGER NOT NULL, + language_code TEXT NOT NULL, + company_name TEXT NOT NULL, + role TEXT NOT NULL, + description TEXT, + location TEXT, + PRIMARY KEY (experience_id, language_code), + FOREIGN KEY (experience_id) REFERENCES experience(id) ON DELETE CASCADE, + FOREIGN KEY (language_code) REFERENCES languages(code) + ); + `); + + // Education + db.run(` + CREATE TABLE IF NOT EXISTS education ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + start_date TEXT NOT NULL, + end_date TEXT, + institution_url TEXT + ); + `); + + db.run(` + CREATE TABLE IF NOT EXISTS education_translations ( + education_id INTEGER NOT NULL, + language_code TEXT NOT NULL, + institution TEXT NOT NULL, + degree TEXT NOT NULL, + description TEXT, + PRIMARY KEY (education_id, language_code), + FOREIGN KEY (education_id) REFERENCES education(id) ON DELETE CASCADE, + FOREIGN KEY (language_code) REFERENCES languages(code) + ); + `); +} diff --git a/src/db/migrations/003_add_display_order.ts b/src/db/migrations/003_add_display_order.ts new file mode 100644 index 0000000..aaa153a --- /dev/null +++ b/src/db/migrations/003_add_display_order.ts @@ -0,0 +1,15 @@ +import { Database } from "bun:sqlite"; + +export function up(db: Database) { + try { + db.run("ALTER TABLE experience ADD COLUMN display_order INTEGER DEFAULT 0"); + } catch (e) { + // Column likely already exists, ignore + } + + try { + db.run("ALTER TABLE education ADD COLUMN display_order INTEGER DEFAULT 0"); + } catch (e) { + // Column likely already exists, ignore + } +} diff --git a/src/db/migrations/004_add_projects.ts b/src/db/migrations/004_add_projects.ts new file mode 100644 index 0000000..7bfe84a --- /dev/null +++ b/src/db/migrations/004_add_projects.ts @@ -0,0 +1,25 @@ +import { Database } from "bun:sqlite"; + +export function up(db: Database) { + db.run(` + CREATE TABLE IF NOT EXISTS projects ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + project_url TEXT, + image_url TEXT, + tech_stack TEXT, + display_order INTEGER DEFAULT 0 + ); + `); + + db.run(` + CREATE TABLE IF NOT EXISTS project_translations ( + project_id INTEGER NOT NULL, + language_code TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT, + PRIMARY KEY (project_id, language_code), + FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY (language_code) REFERENCES languages(code) + ); + `); +} diff --git a/src/db/migrator.ts b/src/db/migrator.ts new file mode 100644 index 0000000..c1b3ed4 --- /dev/null +++ b/src/db/migrator.ts @@ -0,0 +1,48 @@ +import { Database } from "bun:sqlite"; +import { db } from "./schema"; +import { readdir } from "node:fs/promises"; +import { join } from "node:path"; + +async function migrate() { + console.log("🔄 Starting database migrations..."); + + // 1. Create migrations table if it doesn't exist + db.run(` + CREATE TABLE IF NOT EXISTS _migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + applied_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + `); + + // 2. Get list of applied migrations + const applied = new Set( + db.query("SELECT name FROM _migrations").all().map((row: any) => row.name) + ); + + // 3. Get list of migration files + const migrationsDir = join(import.meta.dir, "migrations"); + const files = (await readdir(migrationsDir)).filter(f => f.endsWith(".ts")).sort(); + + // 4. Run pending migrations + for (const file of files) { + if (!applied.has(file)) { + console.log(` Running migration: ${file}`); + + const migration = await import(join(migrationsDir, file)); + + db.transaction(() => { + if (migration.up) migration.up(db); + db.run("INSERT INTO _migrations (name) VALUES (?)", [file]); + })(); + + console.log(` ✅ Applied: ${file}`); + } + } + + console.log("✨ Migrations complete."); +} + +if (import.meta.main) { + migrate(); +} diff --git a/src/db/mutations.ts b/src/db/mutations.ts index bd68b54..f4e1216 100644 --- a/src/db/mutations.ts +++ b/src/db/mutations.ts @@ -111,3 +111,44 @@ export function updateEducationOrder(ids: number[]) { }); })(); } + +// --- Projects --- +export function deleteProject(id: number) { + db.run("DELETE FROM projects WHERE id = $id", { $id: id }); +} + +export function createProject() { + const maxOrder = db.query("SELECT MAX(display_order) FROM projects").get() as number || 0; + const res = db.run(`INSERT INTO projects (display_order) VALUES ($order)`, { $order: maxOrder + 1 }); + const id = (res as any).lastInsertRowid; + db.run(`INSERT INTO project_translations (project_id, language_code, name, description) VALUES (?, 'en', 'New Project', 'Description')`, [id]); + db.run(`INSERT INTO project_translations (project_id, language_code, name, description) VALUES (?, 'de', 'Neues Projekt', 'Beschreibung')`, [id]); + return id; +} + +export function updateProject(id: number, data: any) { + db.run(` + UPDATE projects SET project_url = $url, image_url = $img, tech_stack = $stack, display_order = $order + WHERE id = $id + `, { $url: data.project_url, $img: data.image_url, $stack: data.tech_stack, $order: data.display_order || 0, $id: id }); + + for (const lang of ["en", "de"]) { + db.run(` + UPDATE project_translations + SET name = $name, description = $desc + WHERE project_id = $id AND language_code = $lang + `, { + $name: data[`name_${lang}`], $desc: data[`description_${lang}`], + $id: id, $lang: lang + }); + } +} + +export function updateProjectOrder(ids: number[]) { + db.transaction(() => { + const stmt = db.prepare("UPDATE projects SET display_order = $order WHERE id = $id"); + ids.forEach((id, index) => { + stmt.run({ $order: index, $id: id }); + }); + })(); +} \ No newline at end of file diff --git a/src/db/queries.ts b/src/db/queries.ts index 2965d26..65195db 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -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; +} diff --git a/src/db/schema.ts b/src/db/schema.ts index 17ab315..3a63584 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -5,117 +5,4 @@ const db = new Database("cv.sqlite", { create: true }); // Enable foreign keys db.run("PRAGMA foreign_keys = ON;"); -export function initDB() { - // 1. Languages Table (to ensure referential integrity) - db.run(` - CREATE TABLE IF NOT EXISTS languages ( - code TEXT PRIMARY KEY - ); - `); - - // 2. Profile (Singleton - structural info) - db.run(` - CREATE TABLE IF NOT EXISTS profile ( - id INTEGER PRIMARY KEY CHECK (id = 1), -- Ensure only one profile exists - email TEXT NOT NULL, - phone TEXT, - website TEXT, - github_url TEXT, - linkedin_url TEXT, - avatar_url TEXT - ); - `); - - // 3. Profile Translations - db.run(` - CREATE TABLE IF NOT EXISTS profile_translations ( - profile_id INTEGER NOT NULL, - language_code TEXT NOT NULL, - name TEXT NOT NULL, - job_title TEXT NOT NULL, - summary TEXT, - location TEXT, - PRIMARY KEY (profile_id, language_code), - FOREIGN KEY (profile_id) REFERENCES profile(id) ON DELETE CASCADE, - FOREIGN KEY (language_code) REFERENCES languages(code) - ); - `); - - // 4. Experience (Structural) - db.run(` - CREATE TABLE IF NOT EXISTS experience ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - start_date TEXT NOT NULL, -- ISO8601 YYYY-MM - end_date TEXT, -- NULL = Current - company_url TEXT, - display_order INTEGER DEFAULT 0 - ); - `); - - // 5. Experience Translations - db.run(` - CREATE TABLE IF NOT EXISTS experience_translations ( - experience_id INTEGER NOT NULL, - language_code TEXT NOT NULL, - company_name TEXT NOT NULL, - role TEXT NOT NULL, - description TEXT, -- Supports Markdown/HTML - location TEXT, - PRIMARY KEY (experience_id, language_code), - FOREIGN KEY (experience_id) REFERENCES experience(id) ON DELETE CASCADE, - FOREIGN KEY (language_code) REFERENCES languages(code) - ); - `); - - // 6. Education (Structural) - db.run(` - CREATE TABLE IF NOT EXISTS education ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - start_date TEXT NOT NULL, - end_date TEXT, - institution_url TEXT, - display_order INTEGER DEFAULT 0 - ); - `); - - // 7. Education Translations - db.run(` - CREATE TABLE IF NOT EXISTS education_translations ( - education_id INTEGER NOT NULL, - language_code TEXT NOT NULL, - institution TEXT NOT NULL, - degree TEXT NOT NULL, - description TEXT, - PRIMARY KEY (education_id, language_code), - FOREIGN KEY (education_id) REFERENCES education(id) ON DELETE CASCADE, - FOREIGN KEY (language_code) REFERENCES languages(code) - ); - `); - - // 8. Skills (Categories & Items) - // Simply storing skills as items with a category. - db.run(` - CREATE TABLE IF NOT EXISTS skills ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - category TEXT NOT NULL, -- e.g., "frontend", "backend", "tools" (Internal key) - icon TEXT, -- class name for an icon library - display_order INTEGER DEFAULT 0 - ); - `); - - db.run(` - CREATE TABLE IF NOT EXISTS skill_translations ( - skill_id INTEGER NOT NULL, - language_code TEXT NOT NULL, - name TEXT NOT NULL, -- The display name - category_display TEXT, -- "Frontend Development" vs "Frontend Entwicklung" - PRIMARY KEY (skill_id, language_code), - FOREIGN KEY (skill_id) REFERENCES skills(id) ON DELETE CASCADE, - FOREIGN KEY (language_code) REFERENCES languages(code) - ); - `); - - console.log("Database schema initialized."); -} - -export { db }; +export { db }; \ No newline at end of file diff --git a/src/db/seed.ts b/src/db/seed.ts index 214dad8..2202a9c 100644 --- a/src/db/seed.ts +++ b/src/db/seed.ts @@ -1,8 +1,15 @@ -import { db, initDB } from "./schema"; +import { db } from "./schema"; +import { spawnSync } from "bun"; export function seedDB() { - // Initialize tables first - initDB(); + // Run migrations first + console.log("Running migrations..."); + const migrate = spawnSync(["bun", "run", "src/db/migrator.ts"]); + console.log(migrate.stdout.toString()); + if (migrate.exitCode !== 0) { + console.error("Migration failed:", migrate.stderr.toString()); + return; + } // Check if data exists to avoid duplicates const check = db.query("SELECT count(*) as count FROM languages").get() as { count: number }; @@ -100,6 +107,19 @@ export function seedDB() { insertSkillTrans.run({ $sid: s1.id, $code: "en", $name: "TypeScript", $catDisplay: "Technologies" }); insertSkillTrans.run({ $sid: s1.id, $code: "de", $name: "TypeScript", $catDisplay: "Technologien" }); + // 6. Projects + const insertProj = db.prepare(` + INSERT INTO projects (project_url, image_url, tech_stack, display_order) VALUES ($url, $img, $stack, $order) RETURNING id + `); + const insertProjTrans = db.prepare(` + INSERT INTO project_translations (project_id, language_code, name, description) VALUES ($pid, $code, $name, $desc) + `); + + const p1 = insertProj.get({ $url: "https://github.com/example/cv-app", $img: "https://placehold.co/600x400", $stack: "ElysiaJS, SQLite, Tailwind", $order: 1 }) as { id: number }; + + insertProjTrans.run({ $pid: p1.id, $code: "en", $name: "CV Website", $desc: "A high-performance SSR portfolio built with Bun." }); + insertProjTrans.run({ $pid: p1.id, $code: "de", $name: "CV Webseite", $desc: "Ein hochperformantes SSR-Portfolio basierend auf Bun." }); + console.log("Seeding complete."); } diff --git a/src/routes/admin.tsx b/src/routes/admin.tsx index e1b29dd..2918af4 100644 --- a/src/routes/admin.tsx +++ b/src/routes/admin.tsx @@ -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'); }); `} @@ -173,6 +181,7 @@ export const adminRoutes = new Elysia() const profile = getAdminProfile(); const experience = getAdminExperience(); const education = getAdminEducation(); + const projects = getAdminProjects(); return html( @@ -212,6 +221,19 @@ export const adminRoutes = new Elysia() + {/* Projects Section */} +
+
+

Projects

+ +
+
+ {projects.map((proj: any) => ( + + ))} +
+
+ {/* Experience Section */}
@@ -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 ; + }) + + .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 ; + }) + + .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 ""; - }); + }); \ No newline at end of file diff --git a/src/routes/public.tsx b/src/routes/public.tsx index 8041f15..ef52730 100644 --- a/src/routes/public.tsx +++ b/src/routes/public.tsx @@ -3,7 +3,7 @@ import { html } from "@elysiajs/html"; import * as elements from "typed-html"; import { BaseHtml } from "../components/BaseHtml"; import { Layout } from "../components/Layout"; -import { HeroSection, AboutSection, ExperienceSection, EducationSection, SkillsSection } from "../components/Sections"; +import { HeroSection, AboutSection, ExperienceSection, EducationSection, SkillsSection, ProjectsSection } from "../components/Sections"; import { getAllData } from "../db/queries"; export const publicRoutes = new Elysia() @@ -41,6 +41,11 @@ export const publicRoutes = new Elysia() {(data.education && data.education.length > 0) ? ( ) : ""} + + {/* Projects Section */} + {(data.projects && data.projects.length > 0) ? ( + + ) : ""}