Files
elysiacv/src/components/BaseHtml.tsx
Tuan-Dat Tran 3de8f6a971 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).
2025-11-22 11:20:03 +01:00

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>
`;