feat: Implement live editing (HTMX) and refine UI hierarchy

Integrates HTMX for real-time updates of Experience and Education forms in the admin dashboard,
eliminating full page reloads on save and enabling instant addition of new entries.
Adjusted the visual prominence of 'Role' in Experience and 'Degree' in Education sections.

Fixes:
- Corrected database query parameter passing, resolving text field clearing issue on save.
- Enabled live 'Add New' functionality for Experience and Education entries.
This commit is contained in:
Tuan-Dat Tran
2025-11-22 00:05:50 +01:00
parent 4dc258606e
commit 3c990e5ab6
6 changed files with 249 additions and 119 deletions

View File

@@ -0,0 +1,75 @@
import * as elements from "typed-html";
export const InputGroup = ({ label, name, value, type = "text", required = false }: any) => (
<div class="mb-4">
<label class="block text-sm font-bold mb-1 text-gray-700 dark:text-gray-300">{label}</label>
<input type={type} name={name} value={value || ""} required={required}
class="w-full p-2 border rounded dark:bg-gray-800 dark:border-gray-600" />
</div>
);
export const TextAreaGroup = ({ label, name, value }: any) => (
<div class="mb-4">
<label class="block text-sm font-bold mb-1 text-gray-700 dark:text-gray-300">{label}</label>
<textarea name={name} rows="4" class="w-full p-2 border rounded dark:bg-gray-800 dark:border-gray-600">{value || ""}</textarea>
</div>
);
export const ExperienceForm = ({ exp }: any) => (
<form hx-post={`/admin/experience/${exp.id}`} hx-target="this" hx-swap="outerHTML" class="border dark:border-gray-700 p-4 rounded mb-6 relative group transition-all duration-300 hover:border-blue-300">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<InputGroup label="Start Date" name="start_date" value={exp.start_date} />
<InputGroup label="End Date" name="end_date" value={exp.end_date} />
<InputGroup label="Order" name="display_order" value={exp.display_order} type="number" />
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<div class="text-xs font-bold text-gray-500 mb-2">ENGLISH</div>
<InputGroup label="Company" name="company_name_en" value={exp.company_name_en} />
<InputGroup label="Role" name="role_en" value={exp.role_en} />
<TextAreaGroup label="Description" name="description_en" value={exp.description_en} />
</div>
<div>
<div class="text-xs font-bold text-gray-500 mb-2">GERMAN</div>
<InputGroup label="Company" name="company_name_de" value={exp.company_name_de} />
<InputGroup label="Role" name="role_de" value={exp.role_de} />
<TextAreaGroup label="Description" name="description_de" value={exp.description_de} />
</div>
</div>
<div class="flex justify-between mt-2">
{/* Delete button: We use hx-confirm to ask first */}
<button hx-post={`/admin/experience/${exp.id}/delete`} hx-confirm="Are you sure?" hx-target="closest form" hx-swap="outerHTML" class="text-red-500 text-sm hover:underline">Delete</button>
<button type="submit" class="bg-blue-500 text-white px-4 py-1 rounded hover:bg-blue-600 transition-transform active:scale-95">Save</button>
</div>
{/* Success Message Indicator (HTMX can swap this in, but for now just visual feedback on button) */}
</form>
);
export const EducationForm = ({ edu }: any) => (
<form hx-post={`/admin/education/${edu.id}`} hx-target="this" hx-swap="outerHTML" class="border dark:border-gray-700 p-4 rounded mb-6 relative hover:border-blue-300 transition-colors">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<InputGroup label="Start Date" name="start_date" value={edu.start_date} />
<InputGroup label="End Date" name="end_date" value={edu.end_date} />
<InputGroup label="Order" name="display_order" value={edu.display_order} type="number" />
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<div class="text-xs font-bold text-gray-500 mb-2">ENGLISH</div>
<InputGroup label="Institution" name="institution_en" value={edu.institution_en} />
<InputGroup label="Degree" name="degree_en" value={edu.degree_en} />
<TextAreaGroup label="Description" name="description_en" value={edu.description_en} />
</div>
<div>
<div class="text-xs font-bold text-gray-500 mb-2">GERMAN</div>
<InputGroup label="Institution" name="institution_de" value={edu.institution_de} />
<InputGroup label="Degree" name="degree_de" value={edu.degree_de} />
<TextAreaGroup label="Description" name="description_de" value={edu.description_de} />
</div>
</div>
<div class="flex justify-between mt-2">
<button hx-post={`/admin/education/${edu.id}/delete`} hx-confirm="Are you sure?" hx-target="closest form" hx-swap="outerHTML" class="text-red-500 text-sm hover:underline">Delete</button>
<button type="submit" class="bg-blue-500 text-white px-4 py-1 rounded hover:bg-blue-600 transition-transform active:scale-95">Save</button>
</div>
</form>
);

View File

@@ -8,6 +8,7 @@ export const BaseHtml = ({ children }: elements.Children) => `
<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>
tailwind.config = {
darkMode: 'class',

View File

@@ -54,7 +54,6 @@ export const ExperienceSection = ({ experience }: any) => (
<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">
<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">
@@ -62,9 +61,9 @@ export const ExperienceSection = ({ experience }: any) => (
</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}
{exp.role}
</h3>
<div class="text-blue-600 dark:text-blue-400 font-medium mb-2">{exp.role}</div>
<div class="text-blue-600 dark:text-blue-400 font-medium mb-2">{exp.company_name}</div>
{exp.description && (
<p class="text-gray-600 dark:text-gray-300 leading-relaxed text-base">
{exp.description}
@@ -88,10 +87,10 @@ export const EducationSection = ({ education }: any) => (
{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>
<h3 class="text-lg font-bold text-gray-900 dark:text-white">{edu.degree}</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>
<div class="text-purple-600 dark:text-purple-400 font-medium">{edu.institution}</div>
{edu.description && <p class="mt-2 text-gray-600 dark:text-gray-400 text-sm">{edu.description}</p>}
</div>
))}
@@ -118,4 +117,4 @@ export const SkillsSection = ({ skills }: any) => (
))}
</div>
</section>
);
);