e572af0771
- Remove hardcoded startDate from La Porra project - Add gitRepoUrl field to Project struct for dynamic date fetching - Implement backend logic to fetch first commit date from git repositories - Add processProjectDates function to calculate dates dynamically - Update template to display computed dates and dynamic "Present/Presente" - Add support for both static and git-based project start dates When a project has a gitRepoUrl, the system automatically fetches the first commit date from the repository. For current projects, it displays "Present" (English) or "Presente" (Spanish) dynamically from the backend. The La Porra project now uses git repository path for date calculation instead of hardcoded JSON values.
534 lines
24 KiB
HTML
534 lines
24 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="{{if eq .Lang "es"}}es{{else}}en{{end}}">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
||
<!-- Primary Meta Tags -->
|
||
<title>{{.CV.Personal.Name}} - {{if eq .Lang "es"}}Curriculum Vitae{{else}}Curriculum Vitae{{end}}</title>
|
||
<meta name="title" content="{{.CV.Personal.Name}} - {{if eq .Lang "es"}}CV Profesional{{else}}Professional CV{{end}}">
|
||
<meta name="description" content="{{.CV.Personal.Title}} | {{if eq .Lang "es"}}18 años de experiencia en desarrollo web, SAP CDC, React, Node.js, Go, HTMX y desarrollo asistido por IA{{else}}18 years of experience in web development, SAP CDC, React, Node.js, Go, HTMX and AI-assisted development{{end}}">
|
||
<meta name="keywords" content="{{if eq .Lang "es"}}CV, Curriculum Vitae, {{.CV.Personal.Name}}, Desarrollador FullStack, SAP CDC, React, Node.js, Go, HTMX, IA, Desarrollo Web, Consultor Técnico{{else}}CV, Resume, {{.CV.Personal.Name}}, FullStack Developer, SAP CDC, React, Node.js, Go, HTMX, AI, Web Development, Technical Consultant{{end}}">
|
||
<meta name="author" content="{{.CV.Personal.Name}}">
|
||
<meta name="robots" content="index, follow">
|
||
<link rel="canonical" href="{{.CV.Personal.Website}}">
|
||
|
||
<!-- Open Graph / Facebook -->
|
||
<meta property="og:type" content="profile">
|
||
<meta property="og:url" content="{{.CV.Personal.Website}}">
|
||
<meta property="og:title" content="{{.CV.Personal.Name}} - {{if eq .Lang "es"}}CV Profesional{{else}}Professional CV{{end}}">
|
||
<meta property="og:description" content="{{.CV.Personal.Title}} | {{if eq .Lang "es"}}Consultor Técnico Senior con 18 años de experiencia{{else}}Senior Technical Consultant with 18 years of experience{{end}}">
|
||
<meta property="og:image" content="{{.CV.Personal.Website}}/static/images/profile.jpg">
|
||
<meta property="og:locale" content="{{if eq .Lang "es"}}es_ES{{else}}en_US{{end}}">
|
||
<meta property="og:site_name" content="{{.CV.Personal.Name}}">
|
||
<meta property="profile:first_name" content="Juan Andrés">
|
||
<meta property="profile:last_name" content="Moreno Rubio">
|
||
<meta property="profile:username" content="txeo">
|
||
|
||
<!-- Social Media Card (Generic) -->
|
||
<meta name="twitter:card" content="summary">
|
||
<meta name="twitter:title" content="{{.CV.Personal.Name}} - {{if eq .Lang "es"}}CV Profesional{{else}}Professional CV{{end}}">
|
||
<meta name="twitter:description" content="{{.CV.Personal.Title}}">
|
||
<meta name="twitter:image" content="{{.CV.Personal.Website}}/static/images/profile.jpg">
|
||
|
||
<!-- HTMX Configuration -->
|
||
<meta name="htmx-config" content='{"timeout":5000,"defaultSwapStyle":"innerHTML","defaultSwapDelay":0,"defaultSettleDelay":20}'>
|
||
|
||
<!-- HTMX with SRI (Subresource Integrity) -->
|
||
<script src="https://unpkg.com/htmx.org@1.9.10"
|
||
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
|
||
crossorigin="anonymous"></script>
|
||
|
||
<!-- Iconify - Load synchronously for immediate rendering -->
|
||
<script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
|
||
|
||
<!-- CSS -->
|
||
<link rel="stylesheet" href="/static/css/main.css">
|
||
<link rel="stylesheet" href="/static/css/logo-toggle.css">
|
||
<link rel="stylesheet" href="/static/css/print.css" media="print">
|
||
|
||
<!-- Fonts with Preload -->
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||
|
||
<!-- Structured Data (JSON-LD) -->
|
||
<script type="application/ld+json">
|
||
{
|
||
"@context": "https://schema.org",
|
||
"@type": "Person",
|
||
"name": "{{.CV.Personal.Name}}",
|
||
"jobTitle": "{{.CV.Personal.Title}}",
|
||
"url": "{{.CV.Personal.Website}}",
|
||
"email": "{{.CV.Personal.Email}}",
|
||
"telephone": "{{.CV.Personal.Phone}}",
|
||
"address": {
|
||
"@type": "PostalAddress",
|
||
"addressLocality": "{{.CV.Personal.Location}}"
|
||
},
|
||
"sameAs": [
|
||
"{{.CV.Personal.LinkedIn}}",
|
||
"{{.CV.Personal.GitHub}}",
|
||
"{{.CV.Personal.Domestika}}"
|
||
],
|
||
"alumniOf": {
|
||
"@type": "EducationalOrganization",
|
||
"name": "Universidad de Extremadura"
|
||
},
|
||
"knowsAbout": [
|
||
"Web Development",
|
||
"SAP Customer Data Cloud",
|
||
"React",
|
||
"Node.js",
|
||
"Go",
|
||
"HTMX",
|
||
"AI-Assisted Development",
|
||
"Full Stack Development"
|
||
],
|
||
"worksFor": {
|
||
"@type": "Organization",
|
||
"name": "Olympic Broadcasting Services"
|
||
}
|
||
}
|
||
</script>
|
||
</head>
|
||
<body>
|
||
<!-- Single Black Bar with Everything -->
|
||
<div class="action-bar no-print" role="navigation" aria-label="Language and export controls">
|
||
<div class="action-bar-content">
|
||
<!-- Left: Site Title + Hamburger Menu + Language -->
|
||
<div class="site-title">
|
||
<iconify-icon icon="mdi:file-account" width="24" height="24" class="site-icon"></iconify-icon>
|
||
|
||
<!-- Hamburger Menu Button -->
|
||
<button class="hamburger-btn" onclick="toggleMenu()" aria-label="Toggle navigation menu">
|
||
<iconify-icon icon="mdi:menu" width="24" height="24"></iconify-icon>
|
||
</button>
|
||
|
||
<span class="site-title-text">CV JAMR - {{.CurrentYear}}</span>
|
||
</div>
|
||
|
||
<!-- Center: Language selector + View controls with labels -->
|
||
<div class="view-controls-center">
|
||
<!-- Language selector (centered) -->
|
||
<div class="language-selector">
|
||
<button class="selector-btn {{if eq .Lang "en"}}active{{end}}" data-short="EN" onclick="selectLanguage('en')" aria-label="English">
|
||
English
|
||
</button>
|
||
<button class="selector-btn {{if eq .Lang "es"}}active{{end}}" data-short="ES" onclick="selectLanguage('es')" aria-label="Español">
|
||
Español
|
||
</button>
|
||
</div>
|
||
<!-- CV Length toggle -->
|
||
<div class="selector-group">
|
||
<label class="selector-label">{{if eq .Lang "es"}}Longitud{{else}}Length{{end}}:</label>
|
||
<label class="icon-toggle">
|
||
<input type="checkbox" id="lengthToggle" onchange="toggleCVLength()">
|
||
<span class="icon-toggle-slider">
|
||
<iconify-icon icon="mdi:file-document-outline" width="16" height="16" class="icon-left"></iconify-icon>
|
||
<iconify-icon icon="mdi:file-document-multiple-outline" width="16" height="16" class="icon-right"></iconify-icon>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
|
||
<!-- Logo toggle -->
|
||
<div class="selector-group">
|
||
<label class="selector-label">{{if eq .Lang "es"}}Logos{{else}}Logos{{end}}:</label>
|
||
<label class="icon-toggle">
|
||
<input type="checkbox" id="logoToggle" checked onchange="toggleLogos()">
|
||
<span class="icon-toggle-slider">
|
||
<iconify-icon icon="mdi:image-off-outline" width="16" height="16" class="icon-left"></iconify-icon>
|
||
<iconify-icon icon="mdi:image-multiple-outline" width="16" height="16" class="icon-right"></iconify-icon>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
|
||
<!-- Theme toggle -->
|
||
<div class="selector-group">
|
||
<label class="selector-label">{{if eq .Lang "es"}}Vista{{else}}View{{end}}:</label>
|
||
<label class="icon-toggle">
|
||
<input type="checkbox" id="themeToggle" onchange="toggleTheme()">
|
||
<span class="icon-toggle-slider">
|
||
<iconify-icon icon="mdi:page-layout-sidebar-left" width="16" height="16" class="icon-left"></iconify-icon>
|
||
<iconify-icon icon="mdi:page-layout-body" width="16" height="16" class="icon-right"></iconify-icon>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right: Action buttons -->
|
||
<div class="action-buttons-right">
|
||
<a
|
||
class="action-btn pdf-btn"
|
||
href="/export/pdf?lang={{.Lang}}"
|
||
download
|
||
aria-label="{{if eq .Lang "es"}}Descargar como PDF{{else}}Download as PDF{{end}}">
|
||
<iconify-icon icon="mdi:download" width="18" height="18"></iconify-icon>
|
||
{{if eq .Lang "es"}}Descargar como PDF{{else}}Download as PDF{{end}}
|
||
</a>
|
||
<button
|
||
class="action-btn print-btn"
|
||
onclick="printFriendly()"
|
||
aria-label="{{if eq .Lang "es"}}Imprimir amigable{{else}}Print Friendly{{end}}">
|
||
<iconify-icon icon="mdi:leaf" width="18" height="18"></iconify-icon>
|
||
{{if eq .Lang "es"}}Imprimir amigable{{else}}Print Friendly{{end}}
|
||
</button>
|
||
</div>
|
||
|
||
<span id="loading"
|
||
class="htmx-indicator"
|
||
role="status"
|
||
aria-live="polite"
|
||
aria-label="Loading">
|
||
<span class="loader"></span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Navigation Menu (Hidden by default) -->
|
||
<nav id="navigation-menu" class="navigation-menu no-print" role="navigation" aria-label="CV sections">
|
||
<div class="menu-content">
|
||
<a href="#education" class="menu-item" onclick="scrollToSection('education')">
|
||
<iconify-icon icon="mdi:school" width="20" height="20"></iconify-icon>
|
||
<span>{{if eq .Lang "es"}}Formación{{else}}Training{{end}}</span>
|
||
</a>
|
||
<a href="#skills" class="menu-item" onclick="scrollToSection('skills')">
|
||
<iconify-icon icon="mdi:brain" width="20" height="20"></iconify-icon>
|
||
<span>{{if eq .Lang "es"}}Competencias{{else}}Skills{{end}}</span>
|
||
</a>
|
||
<a href="#experience" class="menu-item" onclick="scrollToSection('experience')">
|
||
<iconify-icon icon="mdi:office-building" width="20" height="20"></iconify-icon>
|
||
<span>{{if eq .Lang "es"}}Experiencia{{else}}Experience{{end}}</span>
|
||
</a>
|
||
<a href="#awards" class="menu-item" onclick="scrollToSection('awards')">
|
||
<iconify-icon icon="mdi:trophy" width="20" height="20"></iconify-icon>
|
||
<span>{{if eq .Lang "es"}}Premios y Reconocimientos{{else}}Awards{{end}}</span>
|
||
</a>
|
||
<a href="#projects" class="menu-item" onclick="scrollToSection('projects')">
|
||
<iconify-icon icon="mdi:web" width="20" height="20"></iconify-icon>
|
||
<span>{{if eq .Lang "es"}}Proyectos Personales / Freelance{{else}}Personal / Freelance Projects{{end}}</span>
|
||
</a>
|
||
<a href="#courses" class="menu-item" onclick="scrollToSection('courses')">
|
||
<iconify-icon icon="mdi:school" width="20" height="20"></iconify-icon>
|
||
<span>{{if eq .Lang "es"}}Cursos Realizados{{else}}Courses{{end}}</span>
|
||
</a>
|
||
<a href="#languages" class="menu-item" onclick="scrollToSection('languages')">
|
||
<iconify-icon icon="mdi:translate" width="20" height="20"></iconify-icon>
|
||
<span>{{if eq .Lang "es"}}Idiomas{{else}}Languages{{end}}</span>
|
||
</a>
|
||
<a href="#references" class="menu-item" onclick="scrollToSection('references')">
|
||
<iconify-icon icon="mdi:link-variant" width="20" height="20"></iconify-icon>
|
||
<span>{{if eq .Lang "es"}}Referencias{{else}}References{{end}}</span>
|
||
</a>
|
||
<a href="#other" class="menu-item" onclick="scrollToSection('other')">
|
||
<iconify-icon icon="mdi:information" width="20" height="20"></iconify-icon>
|
||
<span>{{if eq .Lang "es"}}Otros{{else}}Other{{end}}</span>
|
||
</a>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- CV Content Container -->
|
||
<div class="cv-container">
|
||
<main id="cv-content"
|
||
class="cv-paper"
|
||
role="main"
|
||
aria-live="polite">
|
||
{{template "cv-content.html" .}}
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Footer (hidden in print) -->
|
||
<footer class="no-print">
|
||
<p>© {{.CV.Meta.LastUpdated}} {{.CV.Personal.Name}} |
|
||
{{if eq .Lang "es"}}Última actualización{{else}}Last updated{{end}}: {{.CV.Meta.LastUpdated}}</p>
|
||
</footer>
|
||
|
||
<!-- Error Toast -->
|
||
<div id="error-toast" class="error-toast no-print" role="alert" aria-live="assertive" style="display: none;">
|
||
<span class="error-icon">⚠️</span>
|
||
<span id="error-message"></span>
|
||
<button onclick="this.parentElement.style.display='none'" aria-label="Close error message" class="error-close">×</button>
|
||
</div>
|
||
|
||
<!-- Back to Top Button -->
|
||
<button id="back-to-top" class="back-to-top no-print" aria-label="{{if eq .Lang "es"}}Volver arriba{{else}}Back to top{{end}}" style="display: none;">
|
||
<iconify-icon icon="mdi:arrow-up" width="24" height="24"></iconify-icon>
|
||
</button>
|
||
|
||
<script>
|
||
// Toggle navigation menu
|
||
function toggleMenu() {
|
||
const menu = document.getElementById('navigation-menu');
|
||
const btn = document.querySelector('.hamburger-btn');
|
||
|
||
if (menu.classList.contains('menu-open')) {
|
||
menu.classList.remove('menu-open');
|
||
btn.setAttribute('aria-expanded', 'false');
|
||
} else {
|
||
menu.classList.add('menu-open');
|
||
btn.setAttribute('aria-expanded', 'true');
|
||
}
|
||
}
|
||
|
||
// Scroll to section smoothly
|
||
function scrollToSection(sectionId) {
|
||
event.preventDefault(); // Prevent default anchor behavior
|
||
|
||
const section = document.getElementById(sectionId);
|
||
if (section) {
|
||
const actionBarHeight = document.querySelector('.action-bar').offsetHeight;
|
||
const menuHeight = document.querySelector('.navigation-menu').offsetHeight;
|
||
const offset = actionBarHeight + (menuHeight || 0) + 20; // Add 20px padding
|
||
|
||
const elementPosition = section.getBoundingClientRect().top;
|
||
const offsetPosition = elementPosition + window.pageYOffset - offset;
|
||
|
||
window.scrollTo({
|
||
top: offsetPosition,
|
||
behavior: 'smooth'
|
||
});
|
||
|
||
// Close menu after clicking
|
||
const menu = document.getElementById('navigation-menu');
|
||
menu.classList.remove('menu-open');
|
||
document.querySelector('.hamburger-btn').setAttribute('aria-expanded', 'false');
|
||
}
|
||
}
|
||
|
||
// Close menu when clicking outside
|
||
document.addEventListener('click', function(event) {
|
||
const menu = document.getElementById('navigation-menu');
|
||
const btn = document.querySelector('.hamburger-btn');
|
||
|
||
if (menu && btn && menu.classList.contains('menu-open')) {
|
||
if (!menu.contains(event.target) && !btn.contains(event.target)) {
|
||
menu.classList.remove('menu-open');
|
||
btn.setAttribute('aria-expanded', 'false');
|
||
}
|
||
}
|
||
});
|
||
|
||
function selectLanguage(lang) {
|
||
// Update button states
|
||
document.querySelectorAll('.language-selector .selector-btn').forEach(btn => {
|
||
btn.classList.remove('active');
|
||
});
|
||
event.target.closest('.selector-btn').classList.add('active');
|
||
|
||
// Use HTMX to load new content
|
||
htmx.ajax('GET', `/cv?lang=${lang}`, {
|
||
target: '#cv-content',
|
||
swap: 'innerHTML swap:200ms settle:200ms',
|
||
indicator: '#loading'
|
||
});
|
||
|
||
// Update URL
|
||
const url = new URL(window.location);
|
||
url.searchParams.set('lang', lang);
|
||
window.history.pushState({}, '', url);
|
||
|
||
// Update html lang attribute
|
||
document.documentElement.lang = lang;
|
||
}
|
||
|
||
function toggleCVLength() {
|
||
const toggle = document.getElementById('lengthToggle');
|
||
const paper = document.querySelector('.cv-paper');
|
||
|
||
// Save current scroll position
|
||
const currentScrollY = window.scrollY || window.pageYOffset;
|
||
|
||
if (toggle.checked) {
|
||
paper.classList.add('cv-long');
|
||
paper.classList.remove('cv-short');
|
||
} else {
|
||
paper.classList.add('cv-short');
|
||
paper.classList.remove('cv-long');
|
||
}
|
||
|
||
// Restore scroll position after DOM updates
|
||
requestAnimationFrame(() => {
|
||
window.scrollTo(0, currentScrollY);
|
||
});
|
||
}
|
||
|
||
function toggleLogos() {
|
||
const toggle = document.getElementById('logoToggle');
|
||
const paper = document.querySelector('.cv-paper');
|
||
|
||
// Save current scroll position
|
||
const currentScrollY = window.scrollY || window.pageYOffset;
|
||
|
||
if (toggle.checked) {
|
||
paper.classList.add('show-logos');
|
||
} else {
|
||
paper.classList.remove('show-logos');
|
||
}
|
||
|
||
// Restore scroll position after DOM updates
|
||
requestAnimationFrame(() => {
|
||
window.scrollTo(0, currentScrollY);
|
||
});
|
||
}
|
||
|
||
function toggleTheme() {
|
||
const toggle = document.getElementById('themeToggle');
|
||
const container = document.querySelector('.cv-container');
|
||
|
||
if (toggle.checked) {
|
||
container.classList.add('theme-clean');
|
||
localStorage.setItem('cv-theme', 'clean');
|
||
} else {
|
||
container.classList.remove('theme-clean');
|
||
localStorage.setItem('cv-theme', 'default');
|
||
}
|
||
}
|
||
|
||
// Print Friendly - applies Clean theme before printing
|
||
function printFriendly() {
|
||
const container = document.querySelector('.cv-container');
|
||
const wasClean = container.classList.contains('theme-clean');
|
||
|
||
// Apply clean theme for printing
|
||
if (!wasClean) {
|
||
container.classList.add('theme-clean');
|
||
}
|
||
|
||
// Print
|
||
window.print();
|
||
|
||
// Restore original theme after print dialog
|
||
setTimeout(() => {
|
||
if (!wasClean) {
|
||
container.classList.remove('theme-clean');
|
||
}
|
||
}, 100);
|
||
}
|
||
|
||
// Initialize with short version, logos enabled, and saved theme
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
document.querySelector('.cv-paper').classList.add('cv-short');
|
||
document.querySelector('.cv-paper').classList.add('show-logos');
|
||
|
||
// Restore theme preference
|
||
const savedTheme = localStorage.getItem('cv-theme') || 'default';
|
||
if (savedTheme === 'clean') {
|
||
document.getElementById('themeToggle').checked = true;
|
||
toggleTheme();
|
||
}
|
||
});
|
||
|
||
// Scroll Direction Detection - Hide/Show Header
|
||
let lastScrollTop = 0;
|
||
let scrollThreshold = 100; // Start hiding after 100px scroll
|
||
|
||
window.addEventListener('scroll', function() {
|
||
const actionBar = document.querySelector('.action-bar');
|
||
const navMenu = document.querySelector('.navigation-menu');
|
||
const backToTopBtn = document.getElementById('back-to-top');
|
||
const currentScroll = window.pageYOffset || document.documentElement.scrollTop;
|
||
const isMenuOpen = navMenu.classList.contains('menu-open');
|
||
|
||
// Hide/show header based on scroll direction
|
||
if (currentScroll > scrollThreshold) {
|
||
if (currentScroll > lastScrollTop) {
|
||
// Scrolling down - hide header
|
||
actionBar.classList.add('header-hidden');
|
||
// Only hide menu if it's open
|
||
if (isMenuOpen) {
|
||
navMenu.classList.add('header-hidden');
|
||
}
|
||
} else {
|
||
// Scrolling up - show header
|
||
actionBar.classList.remove('header-hidden');
|
||
// Only show menu if it's open
|
||
if (isMenuOpen) {
|
||
navMenu.classList.remove('header-hidden');
|
||
}
|
||
}
|
||
} else {
|
||
// At top - always show header
|
||
actionBar.classList.remove('header-hidden');
|
||
// Only affect menu if it's open
|
||
if (isMenuOpen) {
|
||
navMenu.classList.remove('header-hidden');
|
||
}
|
||
}
|
||
|
||
// Show/hide back to top button
|
||
if (currentScroll > 300) {
|
||
backToTopBtn.style.display = 'flex';
|
||
} else {
|
||
backToTopBtn.style.display = 'none';
|
||
}
|
||
|
||
lastScrollTop = currentScroll <= 0 ? 0 : currentScroll;
|
||
}, false);
|
||
|
||
// Back to top button click handler
|
||
document.getElementById('back-to-top').addEventListener('click', function() {
|
||
window.scrollTo({
|
||
top: 0,
|
||
behavior: 'smooth'
|
||
});
|
||
});
|
||
|
||
// Error handling utility
|
||
function showError(message) {
|
||
const errorToast = document.getElementById('error-toast');
|
||
const errorMessage = document.getElementById('error-message');
|
||
errorMessage.textContent = message;
|
||
errorToast.style.display = 'flex';
|
||
|
||
// Auto-hide after 5 seconds
|
||
setTimeout(() => {
|
||
errorToast.style.display = 'none';
|
||
}, 5000);
|
||
}
|
||
|
||
// HTMX Global Error Handlers
|
||
document.body.addEventListener('htmx:responseError', function(evt) {
|
||
console.error('HTMX Response Error:', evt.detail);
|
||
const lang = document.documentElement.lang;
|
||
const message = lang === 'es'
|
||
? 'Error al cargar el contenido. Por favor, inténtelo de nuevo.'
|
||
: 'Failed to load content. Please try again.';
|
||
showError(message);
|
||
});
|
||
|
||
document.body.addEventListener('htmx:sendError', function(evt) {
|
||
console.error('HTMX Send Error:', evt.detail);
|
||
const lang = document.documentElement.lang;
|
||
const message = lang === 'es'
|
||
? 'Error de conexión. Verifique su conexión a internet.'
|
||
: 'Connection error. Please check your internet connection.';
|
||
showError(message);
|
||
});
|
||
|
||
document.body.addEventListener('htmx:timeout', function(evt) {
|
||
console.error('HTMX Timeout:', evt.detail);
|
||
const lang = document.documentElement.lang;
|
||
const message = lang === 'es'
|
||
? 'La solicitud tardó demasiado. Por favor, inténtelo de nuevo.'
|
||
: 'Request timed out. Please try again.';
|
||
showError(message);
|
||
});
|
||
|
||
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
||
// Smooth scroll to top on language change
|
||
if (evt.detail.target.id === 'cv-content') {
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
}
|
||
});
|
||
|
||
// Log successful swaps for debugging
|
||
document.body.addEventListener('htmx:afterRequest', function(evt) {
|
||
if (evt.detail.successful) {
|
||
console.log('HTMX request successful:', evt.detail.pathInfo.requestPath);
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|