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).
192 lines
11 KiB
TypeScript
192 lines
11 KiB
TypeScript
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 src="https://unpkg.com/htmx.org@1.9.10"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
|
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.2/dist/cdn.min.js"></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>
|
|
<style>
|
|
/* SortableJS drag feedback */
|
|
.sortable-chosen {
|
|
cursor: grabbing !important;
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.2); /* Lift effect */
|
|
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 */
|
|
}
|
|
</style>
|
|
<style>
|
|
@media print {
|
|
@page {
|
|
margin: 1.5cm;
|
|
size: auto;
|
|
}
|
|
|
|
/* Global Reset for Print */
|
|
body {
|
|
background-color: #fff !important;
|
|
color: #111 !important;
|
|
font-family: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; /* Serif looks more professional in print */
|
|
font-size: 11pt;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
/* Hide elements */
|
|
header, footer, nav, .admin-controls, .drag-handle, button, form[action*='/admin/'],
|
|
#hero > div.absolute, /* Hide background blob */
|
|
#hero > div.mt-8.flex.justify-center.gap-4, /* Hide social links */
|
|
#about, /* Hide About section */
|
|
.toggle-theme-btn {
|
|
display: none !important;
|
|
}
|
|
/* Layout Overrides */
|
|
.container, main, section {
|
|
padding: 0 !important;
|
|
margin: 0 !important;
|
|
max-width: 100% !important;
|
|
width: 100% !important;
|
|
box-shadow: none !important;
|
|
}
|
|
|
|
/* Hero Section - Compact */
|
|
#hero {
|
|
padding-top: 0 !important;
|
|
padding-bottom: 1rem !important;
|
|
text-align: left !important; /* Standard CV header alignment */
|
|
border-bottom: 1px solid #333;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
#hero img {
|
|
width: 80px !important;
|
|
height: 80px !important;
|
|
float: right; /* Avatar to right */
|
|
margin-left: 1rem;
|
|
border: none !important;
|
|
border-radius: 0 !important; /* Square looks sharper or keep circle */
|
|
}
|
|
#hero h1 {
|
|
font-size: 24pt !important;
|
|
margin: 0 !important;
|
|
line-height: 1.2;
|
|
}
|
|
#hero h2 {
|
|
font-size: 14pt !important;
|
|
color: #444 !important;
|
|
margin: 0.2rem 0 0.5rem 0 !important;
|
|
}
|
|
#hero .flex.justify-center {
|
|
justify-content: flex-start !important; /* Align icons left */
|
|
}
|
|
|
|
/* Sections */
|
|
h2 {
|
|
font-size: 16pt !important;
|
|
border-bottom: 1px solid #ccc;
|
|
padding-bottom: 0.2rem;
|
|
margin-top: 1.5rem !important;
|
|
margin-bottom: 1rem !important;
|
|
color: #000 !important;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
/* Hide the colored span/dot in headers */
|
|
h2 span { display: none !important; }
|
|
|
|
/* Experience & Education - Remove timeline line, adjust grid */
|
|
.relative.border-l-2 {
|
|
border-left: none !important;
|
|
margin-left: 0 !important;
|
|
padding-left: 0 !important;
|
|
}
|
|
.relative.group {
|
|
margin-bottom: 1.5rem !important;
|
|
break-inside: avoid; /* Don't split jobs across pages */
|
|
}
|
|
/* Flatten the grid: Date on top or left? Let's try Side-by-Side but wider */
|
|
.md\:grid-cols-\[1fr_3fr\] {
|
|
display: grid !important;
|
|
grid-template-columns: 18% 82% !important; /* Fixed width for dates */
|
|
gap: 1rem !important;
|
|
}
|
|
|
|
/* Typography refinements */
|
|
h3 {
|
|
font-size: 12pt !important;
|
|
margin: 0 !important;
|
|
color: #000 !important;
|
|
}
|
|
.text-blue-600, .text-purple-600, .group-hover\:text-blue-600 {
|
|
color: #000 !important; /* Remove colors */
|
|
}
|
|
.text-sm {
|
|
font-size: 10pt !important;
|
|
}
|
|
|
|
/* Skills - Plain list */
|
|
#skills .flex.flex-wrap {
|
|
display: block !important;
|
|
}
|
|
#skills .px-4.py-2 {
|
|
background: none !important;
|
|
border: none !important;
|
|
padding: 0 !important;
|
|
display: inline-block;
|
|
margin-right: 0.5rem;
|
|
box-shadow: none !important;
|
|
}
|
|
#skills .px-4.py-2:not(:last-child):after {
|
|
content: ", ";
|
|
}
|
|
#skills .px-4.py-2 span {
|
|
display: none; /* Hide category label if it's too cluttered, or keep it? Hiding for clean list. */
|
|
}
|
|
|
|
/* Links - Clean */
|
|
a { text-decoration: none !important; color: #000 !important; }
|
|
a[href^="http"]:after { content: ""; } /* Remove URL expansion */
|
|
|
|
/* Dark Mode override */
|
|
html.dark body { background: white !important; color: black !important; }
|
|
.dark\:bg-gray-800\/50 { background: none !important; border: none !important; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white transition-colors duration-300" x-data>
|
|
${children}
|
|
</body></html>
|
|
`;
|