Initial commit: CV website with ElysiaJS, SQLite, and TailwindCSS
This commit is contained in:
41
src/components/BaseHtml.tsx
Normal file
41
src/components/BaseHtml.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as elements from "typed-html";
|
||||
|
||||
export const BaseHtml = ({ children }: elements.Children) => `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CV Website</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
}
|
||||
// Dark mode toggle script
|
||||
const storedTheme = localStorage.getItem('theme');
|
||||
if (storedTheme === 'dark' || (!storedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
if (document.documentElement.classList.contains('dark')) {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('theme', 'light');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white transition-colors duration-300">
|
||||
${children}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
38
src/components/Layout.tsx
Normal file
38
src/components/Layout.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import * as elements from "typed-html";
|
||||
|
||||
interface LayoutProps extends elements.Children {
|
||||
lang: "en" | "de";
|
||||
}
|
||||
|
||||
export const Layout = ({ children, lang }: LayoutProps) => {
|
||||
return (
|
||||
<div class="container mx-auto p-4 max-w-4xl">
|
||||
<header class="flex justify-between items-center py-4 mb-8">
|
||||
<div class="text-xl font-bold">
|
||||
<a href={`/${lang}`} class="hover:text-blue-500 dark:hover:text-blue-400">
|
||||
My CV
|
||||
</a>
|
||||
</div>
|
||||
<nav class="flex space-x-4 items-center">
|
||||
<a href="/en" class={lang === "en" ? "font-bold text-blue-600 dark:text-blue-300" : "text-gray-600 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400"}>EN</a>
|
||||
<a href="/de" class={lang === "de" ? "font-bold text-blue-600 dark:text-blue-300" : "text-gray-600 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400"}>DE</a>
|
||||
<button onclick="toggleTheme()" class="p-2 rounded-md bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white transition-colors duration-200">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
{/* Sun icon */}
|
||||
<path class="block dark:hidden" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 00-.707-.293h-.001c-.264 0-.524.103-.717.293l-.707.707a1 1 0 101.414 1.414l.707-.707c.2-.193.293-.453.293-.717v-.001zm-10.607.707l-.707-.707a1 1 0 00-1.414 1.414l.707.707c.193.2.453.293.717.293h.001a1 1 0 00.707-1.414zm-.707 4.95a1 1 0 000 1.414l.707.707a1 1 0 101.414-1.414l-.707-.707a1 1 0 00-1.414 0zM10 15a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zm6.929-7.778l.707-.707a1 1 0 00-1.414-1.414l-.707.707a1 1 0 001.414 1.414zM3.071 5.222l.707-.707a1 1 0 00-1.414-1.414l-.707.707a1 1 0 001.414 1.414z" />
|
||||
{/* Moon icon */}
|
||||
<path class="hidden dark:block" d="M17.293 13.293A8 8 0 016.707 6.707a8.001 8.001 0 1010.586 6.586z" />
|
||||
</svg>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
<footer class="text-center py-8 mt-12 text-gray-600 dark:text-gray-400 border-t border-gray-200 dark:border-gray-700">
|
||||
<p>© {new Date().getFullYear()} John Doe. All rights reserved.</p>
|
||||
<p>Built with <a href="https://elysiajs.com/" target="_blank" class="text-blue-500 hover:underline">ElysiaJS</a> & <a href="https://tailwindcss.com/" target="_blank" class="text-blue-500 hover:underline">TailwindCSS</a>.</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
122
src/components/Sections.tsx
Normal file
122
src/components/Sections.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import * as elements from "typed-html";
|
||||
|
||||
export const HeroSection = ({ profile }: any) => (
|
||||
<section id="hero" class="text-center py-20 animate-fade-in relative overflow-hidden">
|
||||
{/* Decorative background element */}
|
||||
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[500px] bg-blue-100/50 dark:bg-blue-900/20 blur-[100px] rounded-full -z-10"></div>
|
||||
|
||||
<div class="relative inline-block mb-6 group">
|
||||
<img
|
||||
src={profile.avatar_url}
|
||||
alt={profile.name}
|
||||
class="w-40 h-40 rounded-full object-cover border-4 border-white dark:border-gray-800 shadow-xl transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
<div class="absolute inset-0 rounded-full ring-2 ring-blue-500/20 dark:ring-blue-400/20 group-hover:ring-4 transition-all duration-500"></div>
|
||||
</div>
|
||||
|
||||
<h1 class="text-6xl font-black mb-3 text-gray-900 dark:text-white tracking-tight">
|
||||
{profile.name}
|
||||
</h1>
|
||||
<h2 class="text-2xl font-medium text-blue-600 dark:text-blue-400 mb-2">
|
||||
{profile.job_title}
|
||||
</h2>
|
||||
{profile.location && (
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 flex justify-center items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
||||
{profile.location}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div class="mt-8 flex justify-center gap-4">
|
||||
{profile.github_url && <a href={profile.github_url} target="_blank" class="p-2 text-gray-600 hover:text-black dark:text-gray-400 dark:hover:text-white transition-colors"><svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg></a>}
|
||||
{profile.email && <a href={`mailto:${profile.email}`} class="p-2 text-gray-600 hover:text-black dark:text-gray-400 dark:hover:text-white transition-colors"><svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg></a>}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
export const AboutSection = ({ summary }: { summary: string }) => (
|
||||
<section id="about" class="py-12 max-w-2xl mx-auto">
|
||||
<div class="bg-white dark:bg-gray-800/50 backdrop-blur-sm rounded-2xl p-8 shadow-sm border border-gray-100 dark:border-gray-700/50">
|
||||
<h3 class="text-sm uppercase tracking-widest text-gray-500 dark:text-gray-400 font-bold mb-4">About</h3>
|
||||
<p class="text-lg text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||
{summary}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
export const ExperienceSection = ({ experience }: any) => (
|
||||
<section id="experience" class="py-16">
|
||||
<h2 class="text-3xl font-bold mb-12 text-gray-900 dark:text-white flex items-center gap-3">
|
||||
<span class="w-8 h-1 bg-blue-500 rounded-full"></span>
|
||||
Experience
|
||||
</h2>
|
||||
<div class="relative border-l-2 border-gray-200 dark:border-gray-700 ml-3 pl-8 space-y-12">
|
||||
{experience.map((exp: any) => (
|
||||
<div class="relative group">
|
||||
{/* Hover highlight on line - optional subtle effect */}
|
||||
<div class="absolute -left-[33px] top-2 w-3 h-3 bg-gray-200 dark:bg-gray-700 rounded-full border-2 border-white dark:border-gray-900 group-hover:bg-blue-500 group-hover:scale-125 transition-all duration-300"></div>
|
||||
|
||||
<div class="grid md:grid-cols-[1fr_3fr] gap-4">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400 pt-1 font-mono">
|
||||
{exp.start_date} — {exp.end_date || "Present"}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
|
||||
{exp.company_name}
|
||||
</h3>
|
||||
<div class="text-blue-600 dark:text-blue-400 font-medium mb-2">{exp.role}</div>
|
||||
{exp.description && (
|
||||
<p class="text-gray-600 dark:text-gray-300 leading-relaxed text-base">
|
||||
{exp.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
export const EducationSection = ({ education }: any) => (
|
||||
<section id="education" class="py-12">
|
||||
<h2 class="text-3xl font-bold mb-12 text-gray-900 dark:text-white flex items-center gap-3">
|
||||
<span class="w-8 h-1 bg-purple-500 rounded-full"></span>
|
||||
Education
|
||||
</h2>
|
||||
<div class="grid gap-6">
|
||||
{education.map((edu: any) => (
|
||||
<div class="bg-gray-50 dark:bg-gray-800/50 p-6 rounded-xl border border-gray-100 dark:border-gray-700 hover:border-purple-500/30 transition-colors">
|
||||
<div class="flex flex-col md:flex-row md:justify-between md:items-baseline mb-2">
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-white">{edu.institution}</h3>
|
||||
<span class="text-sm text-gray-500 font-mono">{edu.start_date} — {edu.end_date || "Present"}</span>
|
||||
</div>
|
||||
<div class="text-purple-600 dark:text-purple-400 font-medium">{edu.degree}</div>
|
||||
{edu.description && <p class="mt-2 text-gray-600 dark:text-gray-400 text-sm">{edu.description}</p>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
export const SkillsSection = ({ skills }: any) => (
|
||||
<section id="skills" class="py-16">
|
||||
<h2 class="text-3xl font-bold mb-10 text-gray-900 dark:text-white flex items-center gap-3">
|
||||
<span class="w-8 h-1 bg-green-500 rounded-full"></span>
|
||||
Skills
|
||||
</h2>
|
||||
|
||||
{/* Grouping by category could be done here if the data supports it nicely,
|
||||
but for now we'll do a clean tag cloud with categories visually distinct if needed */}
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
{skills.map((skill: any) => (
|
||||
<div class="px-4 py-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-200 shadow-sm hover:shadow-md hover:-translate-y-0.5 transition-all duration-200 cursor-default">
|
||||
<span class="text-xs text-gray-400 block mb-1 uppercase tracking-wider">{skill.category_display || skill.category}</span>
|
||||
{skill.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
118
src/db/queries.ts
Normal file
118
src/db/queries.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { db } from "./schema";
|
||||
|
||||
interface ProfileTranslation {
|
||||
name: string;
|
||||
job_title: string;
|
||||
summary: string | null;
|
||||
location: string | null;
|
||||
}
|
||||
|
||||
interface Profile extends ProfileTranslation {
|
||||
email: string;
|
||||
phone: string | null;
|
||||
website: string | null;
|
||||
github_url: string | null;
|
||||
linkedin_url: string | null;
|
||||
avatar_url: string | null;
|
||||
}
|
||||
|
||||
interface ExperienceTranslation {
|
||||
company_name: string;
|
||||
role: string;
|
||||
description: string | null;
|
||||
location: string | null;
|
||||
}
|
||||
|
||||
interface Experience extends ExperienceTranslation {
|
||||
start_date: string;
|
||||
end_date: string | null;
|
||||
company_url: string | null;
|
||||
}
|
||||
|
||||
interface SkillTranslation {
|
||||
name: string;
|
||||
category_display: string | null;
|
||||
}
|
||||
|
||||
interface Skill extends SkillTranslation {
|
||||
category: string;
|
||||
icon: 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,
|
||||
pt.name, pt.job_title, pt.summary, pt.location
|
||||
FROM profile p
|
||||
JOIN profile_translations pt ON p.id = pt.profile_id
|
||||
WHERE pt.language_code = $lang
|
||||
`).get({ $lang: lang }) as Profile | null;
|
||||
return profile;
|
||||
}
|
||||
|
||||
export function getExperience(lang: string): Experience[] {
|
||||
const experience = db.query(`
|
||||
SELECT e.start_date, e.end_date, e.company_url, e.display_order,
|
||||
et.company_name, et.role, et.description, et.location
|
||||
FROM experience e
|
||||
JOIN experience_translations et ON e.id = et.experience_id
|
||||
WHERE et.language_code = $lang
|
||||
ORDER BY e.display_order ASC, e.start_date DESC
|
||||
`).all({ $lang: lang }) as Experience[];
|
||||
return experience;
|
||||
}
|
||||
|
||||
export function getSkills(lang: string): Skill[] {
|
||||
const skills = db.query(`
|
||||
SELECT s.category, s.icon, s.display_order,
|
||||
st.name, st.category_display
|
||||
FROM skills s
|
||||
JOIN skill_translations st ON s.id = st.skill_id
|
||||
WHERE st.language_code = $lang
|
||||
ORDER BY s.display_order ASC, s.category ASC, st.name ASC
|
||||
`).all({ $lang: lang }) as Skill[];
|
||||
return skills;
|
||||
}
|
||||
|
||||
export function getEducation(lang: string): Education[] {
|
||||
|
||||
const education = db.query(`
|
||||
|
||||
SELECT e.start_date, e.end_date, e.institution_url, e.display_order,
|
||||
|
||||
et.institution, et.degree, et.description
|
||||
|
||||
FROM education e
|
||||
|
||||
JOIN education_translations et ON e.id = et.education_id
|
||||
|
||||
WHERE et.language_code = $lang
|
||||
|
||||
ORDER BY e.display_order ASC, e.start_date DESC
|
||||
|
||||
`).all({ $lang: lang }) as Education[];
|
||||
|
||||
return education;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function getAllData(lang: string) {
|
||||
|
||||
return {
|
||||
|
||||
profile: getProfile(lang),
|
||||
|
||||
experience: getExperience(lang),
|
||||
|
||||
education: getEducation(lang),
|
||||
|
||||
skills: getSkills(lang),
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
121
src/db/schema.ts
Normal file
121
src/db/schema.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
|
||||
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 };
|
||||
86
src/db/seed.ts
Normal file
86
src/db/seed.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { db, initDB } from "./schema";
|
||||
|
||||
export function seedDB() {
|
||||
// Initialize tables first
|
||||
initDB();
|
||||
|
||||
// Check if data exists to avoid duplicates
|
||||
const check = db.query("SELECT count(*) as count FROM languages").get() as { count: number };
|
||||
if (check.count > 0) {
|
||||
console.log("Database already seeded.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Seeding database...");
|
||||
|
||||
// 1. Languages
|
||||
const insertLang = db.prepare("INSERT INTO languages (code) VALUES ($code)");
|
||||
insertLang.run({ $code: "en" });
|
||||
insertLang.run({ $code: "de" });
|
||||
|
||||
// 2. Profile
|
||||
db.run(`
|
||||
INSERT INTO profile (id, email, phone, website, github_url, linkedin_url, avatar_url)
|
||||
VALUES (1, 'contact@johndoe.dev', '+49 123 456789', 'https://johndoe.dev', 'https://github.com/johndoe', 'https://linkedin.com/in/johndoe', 'https://placehold.co/400')
|
||||
`);
|
||||
|
||||
const insertProfileTrans = db.prepare(`
|
||||
INSERT INTO profile_translations (profile_id, language_code, name, job_title, summary, location)
|
||||
VALUES ($pid, $code, $name, $title, $summary, $loc)
|
||||
`);
|
||||
|
||||
insertProfileTrans.run({
|
||||
$pid: 1, $code: "en", $name: "John Doe", $title: "Full Stack Developer",
|
||||
$summary: "Passionate developer building minimalist and high-performance web applications.",
|
||||
$loc: "Berlin, Germany"
|
||||
});
|
||||
insertProfileTrans.run({
|
||||
$pid: 1, $code: "de", $name: "John Doe", $title: "Full Stack Entwickler",
|
||||
$summary: "Leidenschaftlicher Entwickler für minimalistische und hochperformante Webanwendungen.",
|
||||
$loc: "Berlin, Deutschland"
|
||||
});
|
||||
|
||||
// 3. Experience
|
||||
const insertExp = db.prepare(`
|
||||
INSERT INTO experience (start_date, end_date, company_url, display_order)
|
||||
VALUES ($start, $end, $url, $order)
|
||||
RETURNING id
|
||||
`);
|
||||
|
||||
// Job 1
|
||||
const job1 = insertExp.get({ $start: "2023-01", $end: null, $url: "https://techcorp.com", $order: 1 }) as { id: number };
|
||||
|
||||
const insertExpTrans = db.prepare(`
|
||||
INSERT INTO experience_translations (experience_id, language_code, company_name, role, description)
|
||||
VALUES ($eid, $code, $comp, $role, $desc)
|
||||
`);
|
||||
|
||||
insertExpTrans.run({
|
||||
$eid: job1.id, $code: "en", $comp: "Tech Corp", $role: "Senior Engineer",
|
||||
$desc: "Leading the frontend team and migrating legacy codebase to ElysiaJS."
|
||||
});
|
||||
insertExpTrans.run({
|
||||
$eid: job1.id, $code: "de", $comp: "Tech Corp", $role: "Senior Entwickler",
|
||||
$desc: "Leitung des Frontend-Teams und Migration der Legacy-Codebasis zu ElysiaJS."
|
||||
});
|
||||
|
||||
// 4. Skills
|
||||
const insertSkill = db.prepare(`
|
||||
INSERT INTO skills (category, icon, display_order) VALUES ($cat, $icon, $order) RETURNING id
|
||||
`);
|
||||
const insertSkillTrans = db.prepare(`
|
||||
INSERT INTO skill_translations (skill_id, language_code, name, category_display)
|
||||
VALUES ($sid, $code, $name, $catDisplay)
|
||||
`);
|
||||
|
||||
const s1 = insertSkill.get({ $cat: "tech", $icon: "code", $order: 1 }) as { id: number };
|
||||
insertSkillTrans.run({ $sid: s1.id, $code: "en", $name: "TypeScript", $catDisplay: "Technologies" });
|
||||
insertSkillTrans.run({ $sid: s1.id, $code: "de", $name: "TypeScript", $catDisplay: "Technologien" });
|
||||
|
||||
console.log("Seeding complete.");
|
||||
}
|
||||
|
||||
// Allow running directly: bun src/db/seed.ts
|
||||
if (import.meta.main) {
|
||||
seedDB();
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Elysia } from "elysia";
|
||||
|
||||
const app = new Elysia().get("/", () => "Hello Elysia").listen(3000);
|
||||
|
||||
console.log(
|
||||
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
|
||||
);
|
||||
51
src/index.tsx
Normal file
51
src/index.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Elysia, NotFoundError } from "elysia";
|
||||
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 { getAllData } from "./db/queries";
|
||||
|
||||
const app = new Elysia()
|
||||
.use(html())
|
||||
.get("/", () => {
|
||||
return Response.redirect("/en"); // Default to English
|
||||
})
|
||||
.get("/:lang", ({ params, html, set }) => {
|
||||
const lang = params.lang as "en" | "de";
|
||||
if (!["en", "de"].includes(lang)) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
const data = getAllData(lang);
|
||||
if (!data.profile) {
|
||||
throw new NotFoundError("Profile data not found for selected language.");
|
||||
}
|
||||
|
||||
return html(
|
||||
<BaseHtml>
|
||||
<Layout lang={lang}>
|
||||
<HeroSection profile={data.profile} />
|
||||
|
||||
{/* Separate About Section using the summary */}
|
||||
{data.profile.summary && <AboutSection summary={data.profile.summary} />}
|
||||
|
||||
<div class="grid gap-12">
|
||||
<ExperienceSection experience={data.experience} />
|
||||
|
||||
{/* Only render Education section if data exists */}
|
||||
{data.education && data.education.length > 0 && (
|
||||
<EducationSection education={data.education} />
|
||||
)}
|
||||
|
||||
<SkillsSection skills={data.skills} />
|
||||
</div>
|
||||
</Layout>
|
||||
</BaseHtml>
|
||||
);
|
||||
})
|
||||
.listen(3000);
|
||||
|
||||
console.log(
|
||||
`Elysia is running at http://${app.server?.hostname}:${app.server?.port}`
|
||||
);
|
||||
Reference in New Issue
Block a user