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>
|
||||
);
|
||||
Reference in New Issue
Block a user