2025-10-20 08:54:21 +01:00
<!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" >
2025-10-31 11:06:38 +00:00
<!-- 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" >
2025-11-11 13:53:14 +00:00
< link rel = "canonical" href = "{{.CanonicalURL}}" >
<!-- Hreflang tags for international SEO -->
< link rel = "alternate" hreflang = "en" href = "{{.AlternateEN}}" >
< link rel = "alternate" hreflang = "es" href = "{{.AlternateES}}" >
< link rel = "alternate" hreflang = "x-default" href = "https://juan.andres.morenorub.io/?lang=en" >
2025-10-31 11:06:38 +00:00
<!-- 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 >
2025-10-20 08:54:21 +01:00
2025-11-07 11:49:47 +00:00
<!-- Iconify - Load synchronously for immediate rendering -->
< script src = "https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js" > < / script >
2025-11-06 10:36:00 +00:00
2025-10-20 08:54:21 +01:00
<!-- CSS -->
< link rel = "stylesheet" href = "/static/css/main.css" >
2025-11-05 12:15:43 +00:00
< link rel = "stylesheet" href = "/static/css/logo-toggle.css" >
2025-10-20 08:54:21 +01:00
< link rel = "stylesheet" href = "/static/css/print.css" media = "print" >
2025-10-31 11:06:38 +00:00
<!-- Fonts with Preload -->
2025-10-20 08:54:21 +01:00
< link rel = "preconnect" href = "https://fonts.googleapis.com" >
< link rel = "preconnect" href = "https://fonts.gstatic.com" crossorigin >
2025-10-31 11:06:38 +00:00
< link rel = "dns-prefetch" href = "https://fonts.googleapis.com" >
2025-10-20 08:54:21 +01:00
< link href = "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel = "stylesheet" >
2025-10-31 11:06:38 +00:00
<!-- 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}}" ,
2025-11-08 10:23:31 +00:00
"{{.CV.Personal.Domestika}}"
2025-10-31 11:06:38 +00:00
] ,
"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 >
2025-10-20 08:54:21 +01:00
< / head >
< body >
2025-10-29 14:04:24 +00:00
<!-- Single Black Bar with Everything -->
2025-10-31 11:06:38 +00:00
< div class = "action-bar no-print" role = "navigation" aria-label = "Language and export controls" >
2025-10-20 08:54:21 +01:00
< div class = "action-bar-content" >
2025-11-07 20:14:45 +00:00
<!-- Left: Site Title + Hamburger Menu + Language -->
2025-11-06 09:11:17 +00:00
< div class = "site-title" >
2025-11-10 15:45:55 +00:00
<!-- Left group: Logo + Hamburger -->
< div class = "site-title-left" >
< a href = "/" class = "site-logo-link" id = "logoLink" aria-label = "Home" >
< iconify-icon icon = "mdi:file-account" width = "24" height = "24" class = "site-icon" > < / iconify-icon >
< / a >
2025-11-07 20:14:45 +00:00
2025-11-10 15:45:55 +00:00
<!-- Hamburger Menu Button -->
< button class = "hamburger-btn" aria-label = "Toggle navigation menu" >
< iconify-icon icon = "mdi:menu" width = "24" height = "24" > < / iconify-icon >
< / button >
< / div >
2025-11-07 19:11:21 +00:00
2025-11-10 15:45:55 +00:00
<!-- Center: Title -->
2025-11-09 15:10:13 +00:00
< a href = "/" class = "site-title-link" id = "titleLink" >
2025-11-10 15:45:55 +00:00
< iconify-icon icon = "mdi:file-account" width = "20" height = "20" class = "site-icon-mobile" > < / iconify-icon >
< span class = "site-title-text" > CV JAMR< span class = "site-title-year" > - {{.CurrentYear}}< / span > < / span >
2025-11-09 15:04:42 +00:00
< / a >
2025-11-06 09:11:17 +00:00
2025-11-10 15:45:55 +00:00
<!-- Right: Language selector -->
2025-11-07 11:49:47 +00:00
< div class = "language-selector" >
2025-11-08 15:05:54 +00:00
< button class = "selector-btn {{if eq .Lang " en " } } active { { end } } " data-short = "EN" onclick = "selectLanguage('en')" aria-label = "English" >
2025-11-07 11:49:47 +00:00
English
< / button >
2025-11-08 15:05:54 +00:00
< button class = "selector-btn {{if eq .Lang " es " } } active { { end } } " data-short = "ES" onclick = "selectLanguage('es')" aria-label = "Español" >
2025-11-07 11:49:47 +00:00
Español
< / button >
< / div >
2025-11-09 15:06:38 +00:00
< / div >
<!-- Center: View controls with labels -->
< div class = "view-controls-center" >
2025-11-07 11:49:47 +00:00
<!-- 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 >
2025-10-27 22:32:32 +00:00
2025-11-07 11:49:47 +00:00
<!-- 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 >
2025-11-05 12:15:43 +00:00
< / div >
2025-10-29 14:04:24 +00:00
<!-- Right: Action buttons -->
2025-11-07 11:49:47 +00:00
< div class = "action-buttons-right" >
2025-11-10 16:03:29 +00:00
< button
2025-11-07 11:49:47 +00:00
class = "action-btn pdf-btn"
2025-11-10 16:03:29 +00:00
onclick = "openPdfModal()"
2025-11-07 11:49:47 +00:00
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}}
2025-11-10 16:03:29 +00:00
< / button >
2025-10-20 08:54:21 +01:00
< button
2025-11-07 11:49:47 +00:00
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}}
2025-10-20 08:54:21 +01:00
< / button >
< / div >
2025-10-31 11:06:38 +00:00
< span id = "loading"
class = "htmx-indicator"
role = "status"
aria-live = "polite"
aria-label = "Loading" >
2025-10-20 08:54:21 +01:00
< span class = "loader" > < / span >
< / span >
< / div >
< / div >
2025-11-07 19:11:21 +00:00
<!-- Navigation Menu (Hidden by default) -->
< nav id = "navigation-menu" class = "navigation-menu no-print" role = "navigation" aria-label = "CV sections" >
< div class = "menu-content" >
2025-11-10 14:00:32 +00:00
<!-- CV Sections - Quick Navigation -->
2025-11-09 13:43:29 +00:00
< div class = "menu-item-submenu" >
2025-11-09 20:05:24 +00:00
< a href = "#" class = "menu-item has-submenu" >
2025-11-09 13:43:29 +00:00
< iconify-icon icon = "mdi:menu" width = "20" height = "20" > < / iconify-icon >
< span > {{if eq .Lang "es"}}Secciones CV{{else}}CV Sections{{end}}< / span >
2025-11-10 14:00:32 +00:00
< iconify-icon icon = "mdi:chevron-right" width = "16" height = "16" class = "submenu-arrow" > < / iconify-icon >
2025-11-09 13:43:29 +00:00
< / a >
< div class = "submenu-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 >
< / div >
2025-11-10 14:00:32 +00:00
<!-- Quick Actions Section -->
< div class = "menu-section-wrapper" >
< div class = "menu-item menu-item-header" >
< iconify-icon icon = "mdi:cog-outline" width = "20" height = "20" > < / iconify-icon >
< span > {{if eq .Lang "es"}}Acciones Rápidas{{else}}Quick Actions{{end}}< / span >
< / div >
< a href = "#" class = "menu-item menu-item-action" onclick = "collapseAllSections(event)" >
< iconify-icon icon = "mdi:arrow-collapse-all" width = "20" height = "20" > < / iconify-icon >
< span > {{if eq .Lang "es"}}Colapsar Todo{{else}}Collapse All{{end}}< / span >
< / a >
2025-11-10 19:29:55 +00:00
< a href = "#" class = "menu-item menu-item-action" onclick = "expandAllSections(event)" >
< iconify-icon icon = "mdi:arrow-expand-all" width = "20" height = "20" > < / iconify-icon >
< span > {{if eq .Lang "es"}}Expandir Todo{{else}}Expand All{{end}}< / span >
< / a >
2025-11-10 14:00:32 +00:00
< / div >
<!-- View Controls in menu (visible only on mobile < 900px) -->
< div class = "menu-controls-section" >
< div class = "menu-item menu-item-header" >
< iconify-icon icon = "mdi:tune-variant" width = "20" height = "20" > < / iconify-icon >
< span > {{if eq .Lang "es"}}Controles de Vista{{else}}View Controls{{end}}< / span >
< / div >
<!-- CV Length toggle -->
< div class = "menu-control-item" >
< label class = "menu-control-label" >
< iconify-icon icon = "mdi:file-document-outline" width = "20" height = "20" > < / iconify-icon >
< span > {{if eq .Lang "es"}}Longitud{{else}}Length{{end}}< / span >
< / label >
< label class = "icon-toggle" >
< input type = "checkbox" id = "lengthToggleMenu" 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 = "menu-control-item" >
< label class = "menu-control-label" >
< iconify-icon icon = "mdi:image-multiple-outline" width = "20" height = "20" > < / iconify-icon >
< span > {{if eq .Lang "es"}}Logos{{else}}Logos{{end}}< / span >
< / label >
< label class = "icon-toggle" >
< input type = "checkbox" id = "logoToggleMenu" 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 = "menu-control-item" >
< label class = "menu-control-label" >
< iconify-icon icon = "mdi:page-layout-sidebar-left" width = "20" height = "20" > < / iconify-icon >
< span > {{if eq .Lang "es"}}Vista{{else}}View{{end}}< / span >
< / label >
< label class = "icon-toggle" >
< input type = "checkbox" id = "themeToggleMenu" 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 >
<!-- Action Buttons in menu (visible only on mobile < 900px) -->
< div class = "menu-actions-section" >
< div class = "menu-item menu-item-header" >
< iconify-icon icon = "mdi:lightning-bolt" width = "20" height = "20" > < / iconify-icon >
< span > {{if eq .Lang "es"}}Acciones{{else}}Actions{{end}}< / span >
< / div >
2025-11-10 16:03:29 +00:00
< button class = "menu-action-btn" onclick = "openPdfModal()" >
2025-11-10 14:00:32 +00:00
< iconify-icon icon = "mdi:download" width = "20" height = "20" > < / iconify-icon >
< span > {{if eq .Lang "es"}}Descargar como PDF{{else}}Download as PDF{{end}}< / span >
2025-11-10 16:03:29 +00:00
< / button >
2025-11-10 14:00:32 +00:00
< button class = "menu-action-btn" onclick = "printFriendly()" >
< iconify-icon icon = "mdi:leaf" width = "20" height = "20" > < / iconify-icon >
< span > {{if eq .Lang "es"}}Imprimir amigable{{else}}Print Friendly{{end}}< / span >
< / button >
< / div >
2025-11-07 19:11:21 +00:00
< / div >
< / nav >
2025-10-20 08:54:21 +01:00
<!-- CV Content Container -->
< div class = "cv-container" >
2025-10-31 11:06:38 +00:00
< main id = "cv-content"
class = "cv-paper"
role = "main"
aria-live = "polite" >
2025-10-20 08:54:21 +01:00
{{template "cv-content.html" .}}
2025-10-31 11:06:38 +00:00
< / main >
2025-10-20 08:54:21 +01:00
< / div >
<!-- Footer (hidden in print) -->
< footer class = "no-print" >
2025-11-09 02:52:38 +00:00
< p style = "text-align: center; margin-bottom: 0.5rem;" >
2025-11-09 02:55:10 +00:00
< a href = "https://github.com/juanatsap/cv-site" target = "_blank" rel = "noopener noreferrer" class = "github-repo-link" style = "text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem;" >
2025-11-09 02:52:38 +00:00
< iconify-icon icon = "mdi:github" width = "20" height = "20" > < / iconify-icon >
{{if eq .Lang "es"}}Ver este proyecto en GitHub{{else}}View this project on GitHub{{end}}
< / a >
< / p >
2025-10-20 08:54:21 +01:00
< p > © {{.CV.Meta.LastUpdated}} {{.CV.Personal.Name}} |
{{if eq .Lang "es"}}Última actualización{{else}}Last updated{{end}}: {{.CV.Meta.LastUpdated}}< / p >
< / footer >
2025-10-27 22:32:32 +00:00
2025-10-31 11:06:38 +00:00
<!-- 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 >
2025-11-07 21:38:34 +00:00
<!-- 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 >
2025-11-09 19:30:05 +00:00
<!-- Info Button (Bottom Left) -->
< button id = "info-button" class = "info-button no-print" aria-label = "{{if eq .Lang " es " } } Información { { else } } Information { { end } } " onclick = "openInfoModal()" >
< iconify-icon icon = "mdi:information-outline" width = "24" height = "24" > < / iconify-icon >
< / button >
<!-- Info Modal -->
< div id = "info-modal" class = "info-modal no-print" onclick = "closeInfoModalOnBackdrop(event)" >
< div class = "info-modal-content" onclick = "event.stopPropagation()" >
< button class = "info-modal-close" onclick = "closeInfoModal()" aria-label = "Close" >
< iconify-icon icon = "mdi:close" width = "24" height = "24" > < / iconify-icon >
< / button >
< div class = "info-modal-header" >
2025-11-09 21:03:39 +00:00
< h2 > {{.UI.InfoModal.Title}}< / h2 >
2025-11-10 17:37:17 +00:00
< div class = "info-modal-cv-title" >
CV {{.CurrentYear}} JAMR -
2025-11-10 19:19:41 +00:00
< span class = "photo-bracket-wrapper" >
< img src = "/static/images/profile/dni.jpeg" alt = "JAMR" class = "info-modal-photo" >
< / span >
2025-11-10 17:37:17 +00:00
< / div >
2025-11-09 19:30:05 +00:00
< / div >
< div class = "info-modal-body" >
< p class = "info-modal-description" >
2025-11-09 21:03:39 +00:00
{{.UI.InfoModal.Description}}
2025-11-09 19:30:05 +00:00
< / p >
< div class = "info-modal-tech" >
< div class = "info-tech-item" >
< iconify-icon icon = "mdi:language-go" width = "32" height = "32" > < / iconify-icon >
2025-11-09 21:03:39 +00:00
< span > {{.UI.InfoModal.TechStack.GoHono}}< / span >
2025-11-09 19:30:05 +00:00
< / div >
< div class = "info-tech-item" >
< iconify-icon icon = "mdi:lightning-bolt" width = "32" height = "32" > < / iconify-icon >
2025-11-09 21:03:39 +00:00
< span > {{.UI.InfoModal.TechStack.HTMX}}< / span >
2025-11-09 19:30:05 +00:00
< / div >
< div class = "info-tech-item" >
< iconify-icon icon = "mdi:language-html5" width = "32" height = "32" > < / iconify-icon >
2025-11-09 21:03:39 +00:00
< span > {{.UI.InfoModal.TechStack.HTML5}}< / span >
2025-11-09 19:30:05 +00:00
< / div >
< div class = "info-tech-item" >
< iconify-icon icon = "mdi:language-css3" width = "32" height = "32" > < / iconify-icon >
2025-11-09 21:03:39 +00:00
< span > {{.UI.InfoModal.TechStack.CSS3}}< / span >
2025-11-09 19:30:05 +00:00
< / div >
< / div >
2025-11-10 19:26:54 +00:00
< p class = "info-modal-github-subtext" > {{.UI.InfoModal.ViewSourceSubtext}}< / p >
2025-11-09 19:30:05 +00:00
< a href = "https://github.com/juanatsap/cv-site" target = "_blank" rel = "noopener noreferrer" class = "info-modal-github" >
< iconify-icon icon = "mdi:github" width = "24" height = "24" > < / iconify-icon >
2025-11-09 21:03:39 +00:00
< span > {{.UI.InfoModal.ViewSource}}< / span >
2025-11-09 19:30:05 +00:00
< iconify-icon icon = "mdi:arrow-right" width = "20" height = "20" > < / iconify-icon >
< / a >
< / div >
< / div >
< / div >
2025-11-10 16:03:29 +00:00
<!-- PDF Export Modal -->
< div id = "pdf-modal" class = "info-modal no-print" onclick = "closePdfModalOnBackdrop(event)" >
< div class = "info-modal-content" onclick = "event.stopPropagation()" >
< button class = "info-modal-close" onclick = "closePdfModal()" aria-label = "Close" >
< iconify-icon icon = "mdi:close" width = "24" height = "24" > < / iconify-icon >
< / button >
< div class = "info-modal-header" >
< div class = "icon" style = "font-size: 3rem; margin-bottom: 1rem;" > 🚧< / div >
< h2 > {{if eq .Lang "es"}}Exportación PDF - En Desarrollo{{else}}PDF Export - Work in Progress{{end}}< / h2 >
< / div >
< div class = "info-modal-body" >
< p class = "info-modal-description" >
{{if eq .Lang "es"}}
La función de exportación a PDF está siendo mejorada. Por favor, usa el botón < strong > Imprimir Amigable< / strong > en su lugar (Ctrl+P o Cmd+P para guardar como PDF).
{{else}}
The PDF export feature is currently being improved. Please use the < strong > Print Friendly< / strong > button instead (Ctrl+P or Cmd+P to save as PDF).
{{end}}
< / p >
< / div >
< / div >
< / div >
2025-10-27 22:32:32 +00:00
< script >
2025-11-09 20:05:24 +00:00
// Hover-based menu control
document . addEventListener ( 'DOMContentLoaded' , function ( ) {
const hamburgerBtn = document . querySelector ( '.hamburger-btn' ) ;
const menu = document . getElementById ( 'navigation-menu' ) ;
// Show menu on hamburger hover
hamburgerBtn . addEventListener ( 'mouseenter' , function ( ) {
menu . classList . add ( 'menu-hover' ) ;
hamburgerBtn . setAttribute ( 'aria-expanded' , 'true' ) ;
} ) ;
// Hide menu when leaving hamburger (only if not hovering menu)
hamburgerBtn . addEventListener ( 'mouseleave' , function ( ) {
setTimeout ( ( ) => {
if ( ! menu . matches ( ':hover' ) ) {
menu . classList . remove ( 'menu-hover' ) ;
hamburgerBtn . setAttribute ( 'aria-expanded' , 'false' ) ;
}
} , 100 ) ;
} ) ;
// Hide menu when leaving menu itself
menu . addEventListener ( 'mouseleave' , function ( ) {
menu . classList . remove ( 'menu-hover' ) ;
hamburgerBtn . setAttribute ( 'aria-expanded' , 'false' ) ;
} ) ;
2025-11-10 14:00:32 +00:00
// Position submenu dynamically
const submenuTrigger = document . querySelector ( '.menu-item-submenu' ) ;
const submenuContent = document . querySelector ( '.submenu-content' ) ;
if ( submenuTrigger && submenuContent ) {
submenuTrigger . addEventListener ( 'mouseenter' , function ( ) {
const triggerRect = submenuTrigger . getBoundingClientRect ( ) ;
submenuContent . style . top = ` ${ triggerRect . top } px ` ;
} ) ;
}
2025-11-09 20:05:24 +00:00
} ) ;
// Legacy toggle function - kept for compatibility
2025-11-07 19:11:21 +00:00
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' ) ;
}
}
2025-11-09 11:42:52 +00:00
// Flag to keep header visible after navigation
let keepHeaderVisible = false ;
2025-11-10 18:16:54 +00:00
// Toggle sidebar accordion (mobile only)
function toggleSidebar ( header ) {
const content = header . nextElementSibling ;
const isActive = header . classList . contains ( 'active' ) ;
if ( isActive ) {
// Close
header . classList . remove ( 'active' ) ;
content . classList . remove ( 'active' ) ;
} else {
// Open
header . classList . add ( 'active' ) ;
content . classList . add ( 'active' ) ;
}
}
2025-11-09 13:43:29 +00:00
// Expand all sections
function expandAllSections ( event ) {
event . preventDefault ( ) ;
const allDetails = document . querySelectorAll ( 'details' ) ;
allDetails . forEach ( detail => {
detail . setAttribute ( 'open' , '' ) ;
} ) ;
}
// Collapse all sections
function collapseAllSections ( event ) {
event . preventDefault ( ) ;
const allDetails = document . querySelectorAll ( 'details' ) ;
allDetails . forEach ( detail => {
detail . removeAttribute ( 'open' ) ;
} ) ;
}
2025-11-09 20:05:24 +00:00
// Toggle submenu - no longer needed for hover, but kept for compatibility
2025-11-09 13:43:29 +00:00
function toggleSubmenu ( event ) {
event . preventDefault ( ) ;
const submenuContainer = event . currentTarget . parentElement ;
submenuContainer . classList . toggle ( 'submenu-open' ) ;
}
2025-11-07 19:11:21 +00:00
// Scroll to section smoothly
function scrollToSection ( sectionId ) {
event . preventDefault ( ) ; // Prevent default anchor behavior
const section = document . getElementById ( sectionId ) ;
if ( section ) {
2025-11-09 11:42:52 +00:00
// Ensure header is visible before scrolling
const actionBar = document . querySelector ( '.action-bar' ) ;
const navMenu = document . querySelector ( '.navigation-menu' ) ;
actionBar . classList . remove ( 'header-hidden' ) ;
navMenu . classList . remove ( 'header-hidden' ) ;
2025-11-07 19:11:21 +00:00
2025-11-09 11:42:52 +00:00
// Set flag to keep header visible
keepHeaderVisible = true ;
2025-11-07 19:11:21 +00:00
// Close menu after clicking
2025-11-09 11:42:52 +00:00
navMenu . classList . remove ( 'menu-open' ) ;
2025-11-07 19:11:21 +00:00
document . querySelector ( '.hamburger-btn' ) . setAttribute ( 'aria-expanded' , 'false' ) ;
2025-11-09 11:42:52 +00:00
// Wait a bit for header to be visible, then calculate offset
setTimeout ( ( ) => {
const actionBarHeight = actionBar . offsetHeight ;
const offset = actionBarHeight + 20 ; // Add 20px padding
const elementPosition = section . getBoundingClientRect ( ) . top ;
const offsetPosition = elementPosition + window . pageYOffset - offset ;
window . scrollTo ( {
top : offsetPosition ,
behavior : 'smooth'
} ) ;
} , 100 ) ;
2025-11-07 19:11:21 +00:00
}
}
2025-11-09 20:05:24 +00:00
// Close menu when clicking outside (only for legacy click-opened menus)
2025-11-07 19:11:21 +00:00
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' ) ;
}
}
} ) ;
2025-11-09 15:10:13 +00:00
// Track if URL originally had lang parameter
const urlHadLangParam = new URLSearchParams ( window . location . search ) . has ( 'lang' ) ;
2025-11-07 11:49:47 +00:00
function selectLanguage ( lang ) {
2025-11-09 15:31:03 +00:00
// Save language preference to localStorage
localStorage . setItem ( 'cv-language' , lang ) ;
2025-11-10 19:29:22 +00:00
// Reload page with new language parameter
2025-11-06 09:11:17 +00:00
const url = new URL ( window . location ) ;
2025-11-10 19:29:22 +00:00
url . searchParams . set ( 'lang' , lang ) ;
window . location . href = url . toString ( ) ;
2025-11-06 09:11:17 +00:00
}
function toggleCVLength ( ) {
2025-11-10 14:00:32 +00:00
const headerToggle = document . getElementById ( 'lengthToggle' ) ;
const menuToggle = document . getElementById ( 'lengthToggleMenu' ) ;
2025-10-27 22:32:32 +00:00
const paper = document . querySelector ( '.cv-paper' ) ;
2025-11-06 09:11:17 +00:00
2025-11-10 14:00:32 +00:00
// Get the state from whichever toggle was clicked
const isChecked = event ? . target ? . id === 'lengthToggleMenu' ? menuToggle ? . checked : headerToggle ? . checked ;
// Sync both toggles
if ( headerToggle ) headerToggle . checked = isChecked ;
if ( menuToggle ) menuToggle . checked = isChecked ;
2025-11-07 22:12:05 +00:00
// Save current scroll position
const currentScrollY = window . scrollY || window . pageYOffset ;
2025-11-10 14:00:32 +00:00
if ( isChecked ) {
2025-10-27 22:32:32 +00:00
paper . classList . add ( 'cv-long' ) ;
paper . classList . remove ( 'cv-short' ) ;
2025-11-09 11:42:52 +00:00
localStorage . setItem ( 'cv-length' , 'long' ) ;
2025-11-06 09:11:17 +00:00
} else {
paper . classList . add ( 'cv-short' ) ;
paper . classList . remove ( 'cv-long' ) ;
2025-11-09 11:42:52 +00:00
localStorage . setItem ( 'cv-length' , 'short' ) ;
2025-10-27 22:32:32 +00:00
}
2025-11-07 22:12:05 +00:00
// Restore scroll position after DOM updates
requestAnimationFrame ( ( ) => {
window . scrollTo ( 0 , currentScrollY ) ;
} ) ;
2025-10-27 22:32:32 +00:00
}
2025-11-05 12:15:43 +00:00
function toggleLogos ( ) {
2025-11-10 14:00:32 +00:00
const headerToggle = document . getElementById ( 'logoToggle' ) ;
const menuToggle = document . getElementById ( 'logoToggleMenu' ) ;
2025-11-05 12:15:43 +00:00
const paper = document . querySelector ( '.cv-paper' ) ;
2025-11-10 14:00:32 +00:00
// Get the state from whichever toggle was clicked
const isChecked = event ? . target ? . id === 'logoToggleMenu' ? menuToggle ? . checked : headerToggle ? . checked ;
// Sync both toggles
if ( headerToggle ) headerToggle . checked = isChecked ;
if ( menuToggle ) menuToggle . checked = isChecked ;
2025-11-07 22:13:16 +00:00
// Save current scroll position
const currentScrollY = window . scrollY || window . pageYOffset ;
2025-11-10 14:00:32 +00:00
if ( isChecked ) {
2025-11-05 12:15:43 +00:00
paper . classList . add ( 'show-logos' ) ;
2025-11-09 11:42:52 +00:00
localStorage . setItem ( 'cv-logos' , 'show' ) ;
2025-11-05 12:15:43 +00:00
} else {
paper . classList . remove ( 'show-logos' ) ;
2025-11-09 11:42:52 +00:00
localStorage . setItem ( 'cv-logos' , 'hide' ) ;
2025-11-05 12:15:43 +00:00
}
2025-11-07 22:13:16 +00:00
// Restore scroll position after DOM updates
requestAnimationFrame ( ( ) => {
window . scrollTo ( 0 , currentScrollY ) ;
} ) ;
2025-11-05 12:15:43 +00:00
}
2025-11-07 11:49:47 +00:00
function toggleTheme ( ) {
2025-11-10 14:00:32 +00:00
const headerToggle = document . getElementById ( 'themeToggle' ) ;
const menuToggle = document . getElementById ( 'themeToggleMenu' ) ;
2025-11-07 11:49:47 +00:00
const container = document . querySelector ( '.cv-container' ) ;
2025-11-10 14:00:32 +00:00
// Get the state from whichever toggle was clicked
const isChecked = event ? . target ? . id === 'themeToggleMenu' ? menuToggle ? . checked : headerToggle ? . checked ;
// Sync both toggles
if ( headerToggle ) headerToggle . checked = isChecked ;
if ( menuToggle ) menuToggle . checked = isChecked ;
if ( isChecked ) {
2025-11-07 11:49:47 +00:00
container . classList . add ( 'theme-clean' ) ;
localStorage . setItem ( 'cv-theme' , 'clean' ) ;
} else {
container . classList . remove ( 'theme-clean' ) ;
localStorage . setItem ( 'cv-theme' , 'default' ) ;
}
}
2025-11-10 14:00:32 +00:00
// Print Friendly - Apply Clean Theme + Short Version for minimal printing
2025-11-07 11:49:47 +00:00
function printFriendly ( ) {
const container = document . querySelector ( '.cv-container' ) ;
2025-11-10 14:00:32 +00:00
const paper = document . querySelector ( '.cv-paper' ) ;
2025-11-07 11:49:47 +00:00
const wasClean = container . classList . contains ( 'theme-clean' ) ;
2025-11-10 14:00:32 +00:00
const wasLong = paper . classList . contains ( 'cv-long' ) ;
2025-11-07 11:49:47 +00:00
2025-11-10 14:00:32 +00:00
// Apply clean theme for minimal print (no sidebars, no header, no icons)
2025-11-07 11:49:47 +00:00
if ( ! wasClean ) {
container . classList . add ( 'theme-clean' ) ;
}
2025-11-10 14:00:32 +00:00
// Force SHORT version for print (hide detailed content)
paper . classList . remove ( 'cv-long' ) ;
paper . classList . add ( 'cv-short' ) ;
2025-11-07 11:49:47 +00:00
2025-11-10 14:00:32 +00:00
// Small delay to let CSS apply
2025-11-07 11:49:47 +00:00
setTimeout ( ( ) => {
2025-11-10 14:00:32 +00:00
window . print ( ) ;
// Restore original theme and length after print dialog closes
setTimeout ( ( ) => {
if ( ! wasClean ) {
container . classList . remove ( 'theme-clean' ) ;
}
// Restore original length
if ( wasLong ) {
paper . classList . remove ( 'cv-short' ) ;
paper . classList . add ( 'cv-long' ) ;
}
} , 100 ) ;
} , 50 ) ;
2025-11-07 11:49:47 +00:00
}
2025-11-09 11:42:52 +00:00
// Initialize with saved preferences or defaults
2025-10-27 22:32:32 +00:00
document . addEventListener ( 'DOMContentLoaded' , function ( ) {
2025-11-09 11:42:52 +00:00
const paper = document . querySelector ( '.cv-paper' ) ;
2025-11-09 15:31:03 +00:00
// Handle language preference
const urlLang = new URLSearchParams ( window . location . search ) . get ( 'lang' ) ;
const savedLang = localStorage . getItem ( 'cv-language' ) ;
if ( ! urlLang && savedLang ) {
2025-11-10 19:32:20 +00:00
// URL is clean but we have a saved preference - redirect with lang parameter
const url = new URL ( window . location ) ;
url . searchParams . set ( 'lang' , savedLang ) ;
window . location . replace ( url . toString ( ) ) ;
2025-11-09 15:31:03 +00:00
} else if ( urlLang ) {
// Save URL language to localStorage
localStorage . setItem ( 'cv-language' , urlLang ) ;
2025-11-09 15:10:13 +00:00
}
2025-11-09 11:42:52 +00:00
// Restore CV length preference
const savedLength = localStorage . getItem ( 'cv-length' ) || 'short' ;
2025-11-10 14:00:32 +00:00
const lengthChecked = savedLength === 'long' ;
if ( lengthChecked ) {
2025-11-09 11:42:52 +00:00
paper . classList . add ( 'cv-long' ) ;
paper . classList . remove ( 'cv-short' ) ;
} else {
paper . classList . add ( 'cv-short' ) ;
paper . classList . remove ( 'cv-long' ) ;
}
2025-11-10 14:00:32 +00:00
// Sync both header and menu toggles
const headerLengthToggle = document . getElementById ( 'lengthToggle' ) ;
const menuLengthToggle = document . getElementById ( 'lengthToggleMenu' ) ;
if ( headerLengthToggle ) headerLengthToggle . checked = lengthChecked ;
if ( menuLengthToggle ) menuLengthToggle . checked = lengthChecked ;
2025-11-09 11:42:52 +00:00
// Restore logos preference
const savedLogos = localStorage . getItem ( 'cv-logos' ) || 'show' ;
2025-11-10 14:00:32 +00:00
const logosChecked = savedLogos === 'show' ;
if ( logosChecked ) {
2025-11-09 11:42:52 +00:00
paper . classList . add ( 'show-logos' ) ;
} else {
paper . classList . remove ( 'show-logos' ) ;
}
2025-11-10 14:00:32 +00:00
// Sync both header and menu toggles
const headerLogoToggle = document . getElementById ( 'logoToggle' ) ;
const menuLogoToggle = document . getElementById ( 'logoToggleMenu' ) ;
if ( headerLogoToggle ) headerLogoToggle . checked = logosChecked ;
if ( menuLogoToggle ) menuLogoToggle . checked = logosChecked ;
2025-11-07 11:49:47 +00:00
// Restore theme preference
const savedTheme = localStorage . getItem ( 'cv-theme' ) || 'default' ;
2025-11-10 14:00:32 +00:00
const themeChecked = savedTheme === 'clean' ;
// Sync both header and menu toggles
const headerThemeToggle = document . getElementById ( 'themeToggle' ) ;
const menuThemeToggle = document . getElementById ( 'themeToggleMenu' ) ;
if ( headerThemeToggle ) headerThemeToggle . checked = themeChecked ;
if ( menuThemeToggle ) menuThemeToggle . checked = themeChecked ;
if ( themeChecked ) {
2025-11-07 11:49:47 +00:00
toggleTheme ( ) ;
}
2025-10-27 22:32:32 +00:00
} ) ;
2025-10-31 11:06:38 +00:00
2025-11-07 21:38:34 +00:00
// 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 ;
2025-11-07 21:40:41 +00:00
const isMenuOpen = navMenu . classList . contains ( 'menu-open' ) ;
2025-11-07 21:38:34 +00:00
2025-11-09 11:42:52 +00:00
// If scrolling up, reset the keepHeaderVisible flag
if ( currentScroll < lastScrollTop ) {
keepHeaderVisible = false ;
}
2025-11-07 21:38:34 +00:00
// Hide/show header based on scroll direction
if ( currentScroll > scrollThreshold ) {
2025-11-09 11:42:52 +00:00
if ( currentScroll > lastScrollTop && ! keepHeaderVisible ) {
// Scrolling down - hide header (only if keepHeaderVisible is false)
2025-11-07 21:38:34 +00:00
actionBar . classList . add ( 'header-hidden' ) ;
2025-11-07 21:40:41 +00:00
// Only hide menu if it's open
if ( isMenuOpen ) {
navMenu . classList . add ( 'header-hidden' ) ;
}
2025-11-07 21:38:34 +00:00
} else {
// Scrolling up - show header
actionBar . classList . remove ( 'header-hidden' ) ;
2025-11-07 21:40:41 +00:00
// Only show menu if it's open
if ( isMenuOpen ) {
navMenu . classList . remove ( 'header-hidden' ) ;
}
2025-11-07 21:38:34 +00:00
}
} else {
// At top - always show header
actionBar . classList . remove ( 'header-hidden' ) ;
2025-11-07 21:40:41 +00:00
// Only affect menu if it's open
if ( isMenuOpen ) {
navMenu . classList . remove ( 'header-hidden' ) ;
}
2025-11-07 21:38:34 +00:00
}
// 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'
} ) ;
} ) ;
2025-11-09 19:30:05 +00:00
// Info Modal Functions
function openInfoModal ( ) {
const modal = document . getElementById ( 'info-modal' ) ;
modal . classList . add ( 'active' ) ;
document . body . style . overflow = 'hidden' ; // Prevent scrolling when modal is open
}
function closeInfoModal ( ) {
const modal = document . getElementById ( 'info-modal' ) ;
modal . classList . remove ( 'active' ) ;
document . body . style . overflow = '' ; // Restore scrolling
}
function closeInfoModalOnBackdrop ( event ) {
if ( event . target . id === 'info-modal' ) {
closeInfoModal ( ) ;
}
}
2025-11-10 16:03:29 +00:00
// PDF Modal Functions
function openPdfModal ( ) {
const modal = document . getElementById ( 'pdf-modal' ) ;
modal . classList . add ( 'active' ) ;
document . body . style . overflow = 'hidden' ; // Prevent scrolling when modal is open
}
function closePdfModal ( ) {
const modal = document . getElementById ( 'pdf-modal' ) ;
modal . classList . remove ( 'active' ) ;
document . body . style . overflow = '' ; // Restore scrolling
}
function closePdfModalOnBackdrop ( event ) {
if ( event . target . id === 'pdf-modal' ) {
closePdfModal ( ) ;
}
}
// Close modals with Escape key
2025-11-09 19:30:05 +00:00
document . addEventListener ( 'keydown' , function ( event ) {
if ( event . key === 'Escape' ) {
closeInfoModal ( ) ;
2025-11-10 16:03:29 +00:00
closePdfModal ( ) ;
2025-11-09 19:30:05 +00:00
}
} ) ;
2025-10-31 11:06:38 +00:00
// 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 ) ;
}
} ) ;
2025-11-09 15:02:31 +00:00
// Track HTMX navigation events with Matomo
document . body . addEventListener ( 'htmx:afterSwap' , function ( evt ) {
if ( typeof _paq !== 'undefined' && evt . detail . target . id === 'cv-content' ) {
// Track language change as virtual pageview
const lang = new URLSearchParams ( window . location . search ) . get ( 'lang' ) || 'en' ;
_paq . push ( [ 'setCustomUrl' , window . location . href ] ) ;
_paq . push ( [ 'setDocumentTitle' , document . title ] ) ;
_paq . push ( [ 'trackPageView' ] ) ;
}
} ) ;
< / script >
<!-- Matomo -->
< script >
var _paq = window . _paq = window . _paq || [ ] ;
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq . push ( [ 'trackPageView' ] ) ;
_paq . push ( [ 'enableLinkTracking' ] ) ;
( function ( ) {
var u = "https://matomo.drolo.club/" ;
_paq . push ( [ 'setTrackerUrl' , u + 'matomo.php' ] ) ;
_paq . push ( [ 'setSiteId' , '4' ] ) ;
var d = document , g = d . createElement ( 'script' ) , s = d . getElementsByTagName ( 'script' ) [ 0 ] ;
g . async = true ; g . src = u + 'matomo.js' ; s . parentNode . insertBefore ( g , s ) ;
} ) ( ) ;
2025-10-27 22:32:32 +00:00
< / script >
2025-11-09 15:02:31 +00:00
<!-- End Matomo Code -->
2025-10-20 08:54:21 +01:00
< / body >
< / html >