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">
|
|
|
|
|
|
<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>
|
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-07 20:14:45 +00:00
|
|
|
|
<iconify-icon icon="mdi:file-account" width="24" height="24" class="site-icon"></iconify-icon>
|
|
|
|
|
|
|
2025-11-07 19:11:21 +00:00
|
|
|
|
<!-- 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>
|
2025-11-08 16:37:14 +00:00
|
|
|
|
</div>
|
2025-11-06 09:11:17 +00:00
|
|
|
|
|
2025-11-08 16:37:14 +00:00
|
|
|
|
<!-- Center: Language selector + View controls with labels -->
|
|
|
|
|
|
<div class="view-controls-center">
|
|
|
|
|
|
<!-- Language selector (centered) -->
|
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>
|
|
|
|
|
|
<!-- 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-04 13:34:44 +00:00
|
|
|
|
<a
|
2025-11-07 11:49:47 +00:00
|
|
|
|
class="action-btn pdf-btn"
|
2025-11-04 13:34:44 +00:00
|
|
|
|
href="/export/pdf?lang={{.Lang}}"
|
|
|
|
|
|
download
|
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-04 13:34:44 +00:00
|
|
|
|
</a>
|
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-09 13:43:29 +00:00
|
|
|
|
<a href="#" class="menu-item" 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>
|
2025-11-07 19:11:21 +00:00
|
|
|
|
</a>
|
2025-11-09 13:43:29 +00:00
|
|
|
|
<a href="#" class="menu-item" 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>
|
2025-11-07 19:11:21 +00:00
|
|
|
|
</a>
|
2025-11-09 13:43:29 +00:00
|
|
|
|
<div class="menu-item-submenu">
|
|
|
|
|
|
<a href="#" class="menu-item has-submenu" onclick="toggleSubmenu(event)">
|
|
|
|
|
|
<iconify-icon icon="mdi:menu" width="20" height="20"></iconify-icon>
|
|
|
|
|
|
<span>{{if eq .Lang "es"}}Secciones CV{{else}}CV Sections{{end}}</span>
|
|
|
|
|
|
<iconify-icon icon="mdi:chevron-down" width="16" height="16" class="submenu-arrow"></iconify-icon>
|
|
|
|
|
|
</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-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-10-27 22:32:32 +00:00
|
|
|
|
<script>
|
2025-11-07 19:11:21 +00:00
|
|
|
|
// 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');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-09 11:42:52 +00:00
|
|
|
|
// Flag to keep header visible after navigation
|
|
|
|
|
|
let keepHeaderVisible = false;
|
|
|
|
|
|
|
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');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Toggle submenu
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-07 11:49:47 +00:00
|
|
|
|
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');
|
2025-11-06 09:11:17 +00:00
|
|
|
|
|
|
|
|
|
|
// Use HTMX to load new content
|
|
|
|
|
|
htmx.ajax('GET', `/cv?lang=${lang}`, {
|
|
|
|
|
|
target: '#cv-content',
|
|
|
|
|
|
swap: 'innerHTML swap:200ms settle:200ms',
|
|
|
|
|
|
indicator: '#loading'
|
2025-10-27 22:32:32 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-06 09:11:17 +00:00
|
|
|
|
// 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() {
|
2025-11-07 11:49:47 +00:00
|
|
|
|
const toggle = document.getElementById('lengthToggle');
|
2025-10-27 22:32:32 +00:00
|
|
|
|
const paper = document.querySelector('.cv-paper');
|
2025-11-06 09:11:17 +00:00
|
|
|
|
|
2025-11-07 22:12:05 +00:00
|
|
|
|
// Save current scroll position
|
|
|
|
|
|
const currentScrollY = window.scrollY || window.pageYOffset;
|
|
|
|
|
|
|
2025-11-07 11:49:47 +00:00
|
|
|
|
if (toggle.checked) {
|
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-07 11:49:47 +00:00
|
|
|
|
const toggle = document.getElementById('logoToggle');
|
2025-11-05 12:15:43 +00:00
|
|
|
|
const paper = document.querySelector('.cv-paper');
|
|
|
|
|
|
|
2025-11-07 22:13:16 +00:00
|
|
|
|
// Save current scroll position
|
|
|
|
|
|
const currentScrollY = window.scrollY || window.pageYOffset;
|
|
|
|
|
|
|
2025-11-07 11:49:47 +00:00
|
|
|
|
if (toggle.checked) {
|
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() {
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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');
|
|
|
|
|
|
|
|
|
|
|
|
// Restore CV length preference
|
|
|
|
|
|
const savedLength = localStorage.getItem('cv-length') || 'short';
|
|
|
|
|
|
if (savedLength === 'long') {
|
|
|
|
|
|
paper.classList.add('cv-long');
|
|
|
|
|
|
paper.classList.remove('cv-short');
|
|
|
|
|
|
document.getElementById('lengthToggle').checked = true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
paper.classList.add('cv-short');
|
|
|
|
|
|
paper.classList.remove('cv-long');
|
|
|
|
|
|
document.getElementById('lengthToggle').checked = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Restore logos preference
|
|
|
|
|
|
const savedLogos = localStorage.getItem('cv-logos') || 'show';
|
|
|
|
|
|
if (savedLogos === 'show') {
|
|
|
|
|
|
paper.classList.add('show-logos');
|
|
|
|
|
|
document.getElementById('logoToggle').checked = true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
paper.classList.remove('show-logos');
|
|
|
|
|
|
document.getElementById('logoToggle').checked = false;
|
|
|
|
|
|
}
|
2025-11-07 11:49:47 +00:00
|
|
|
|
|
|
|
|
|
|
// Restore theme preference
|
|
|
|
|
|
const savedTheme = localStorage.getItem('cv-theme') || 'default';
|
|
|
|
|
|
if (savedTheme === 'clean') {
|
|
|
|
|
|
document.getElementById('themeToggle').checked = true;
|
|
|
|
|
|
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-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-10-27 22:32:32 +00:00
|
|
|
|
</script>
|
2025-10-20 08:54:21 +01:00
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|