Files
cv-site/templates/index.html
T

989 lines
46 KiB
HTML
Raw Normal View History

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
<!-- Iconify - Load synchronously for immediate rendering -->
<script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
2025-10-20 08:54:21 +01:00
<!-- CSS -->
<link rel="stylesheet" href="/static/css/main.css">
<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}}",
"{{.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>
<!-- 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">
<!-- Left: Site Title + Hamburger Menu + Language -->
<div class="site-title">
<!-- 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>
<!-- 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>
<!-- Center: Title -->
<a href="/" class="site-title-link" id="titleLink">
<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>
</a>
<!-- Right: Language selector -->
<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>
2025-11-09 15:06:38 +00:00
</div>
<!-- Center: View controls with labels -->
<div class="view-controls-center">
<!-- 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">
<button
class="action-btn pdf-btn"
onclick="openPdfModal()"
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}}
</button>
2025-10-20 08:54:21 +01:00
<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}}
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>
<!-- Navigation Menu (Hidden by default) -->
<nav id="navigation-menu" class="navigation-menu no-print" role="navigation" aria-label="CV sections">
<div class="menu-content">
<!-- CV Sections - Quick Navigation -->
<div class="menu-item-submenu">
2025-11-09 20:05:24 +00:00
<a href="#" class="menu-item has-submenu">
<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-right" 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>
<!-- 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="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>
<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>
</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>
<button class="menu-action-btn" onclick="openPdfModal()">
<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>
</button>
<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>
</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;">
<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-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>
<!-- 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>
<!-- 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">
<h2>{{.UI.InfoModal.Title}}</h2>
<div class="info-modal-cv-title">
CV {{.CurrentYear}} JAMR -
<img src="/static/images/profile/dni.jpeg" alt="JAMR" class="info-modal-photo">
</div>
</div>
<div class="info-modal-body">
<p class="info-modal-description">
{{.UI.InfoModal.Description}}
</p>
<div class="info-modal-tech">
<div class="info-tech-item">
<iconify-icon icon="mdi:language-go" width="32" height="32"></iconify-icon>
<span>{{.UI.InfoModal.TechStack.GoHono}}</span>
</div>
<div class="info-tech-item">
<iconify-icon icon="mdi:lightning-bolt" width="32" height="32"></iconify-icon>
<span>{{.UI.InfoModal.TechStack.HTMX}}</span>
</div>
<div class="info-tech-item">
<iconify-icon icon="mdi:language-html5" width="32" height="32"></iconify-icon>
<span>{{.UI.InfoModal.TechStack.HTML5}}</span>
</div>
<div class="info-tech-item">
<iconify-icon icon="mdi:language-css3" width="32" height="32"></iconify-icon>
<span>{{.UI.InfoModal.TechStack.CSS3}}</span>
</div>
</div>
<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>
<span>{{.UI.InfoModal.ViewSource}}</span>
<iconify-icon icon="mdi:arrow-right" width="20" height="20"></iconify-icon>
</a>
</div>
</div>
</div>
<!-- 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>
<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');
});
// 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
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');
}
}
// Flag to keep header visible after navigation
let keepHeaderVisible = false;
// 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
function toggleSubmenu(event) {
event.preventDefault();
const submenuContainer = event.currentTarget.parentElement;
submenuContainer.classList.toggle('submenu-open');
}
// Scroll to section smoothly
function scrollToSection(sectionId) {
event.preventDefault(); // Prevent default anchor behavior
const section = document.getElementById(sectionId);
if (section) {
// 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');
// Set flag to keep header visible
keepHeaderVisible = true;
// Close menu after clicking
navMenu.classList.remove('menu-open');
document.querySelector('.hamburger-btn').setAttribute('aria-expanded', 'false');
// 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-09 20:05:24 +00:00
// Close menu when clicking outside (only for legacy click-opened menus)
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');
}
}
});
// Track if URL originally had lang parameter
const urlHadLangParam = new URLSearchParams(window.location.search).has('lang');
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');
// Save language preference to localStorage
localStorage.setItem('cv-language', lang);
// 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 only if it originally had lang parameter
const url = new URL(window.location);
if (urlHadLangParam) {
url.searchParams.set('lang', lang);
window.history.pushState({}, '', url);
}
// If URL is clean, keep it clean (don't add lang parameter)
// Update html lang attribute
document.documentElement.lang = lang;
}
function toggleCVLength() {
const headerToggle = document.getElementById('lengthToggle');
const menuToggle = document.getElementById('lengthToggleMenu');
const paper = document.querySelector('.cv-paper');
// 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;
// Save current scroll position
const currentScrollY = window.scrollY || window.pageYOffset;
if (isChecked) {
paper.classList.add('cv-long');
paper.classList.remove('cv-short');
localStorage.setItem('cv-length', 'long');
} else {
paper.classList.add('cv-short');
paper.classList.remove('cv-long');
localStorage.setItem('cv-length', 'short');
}
// Restore scroll position after DOM updates
requestAnimationFrame(() => {
window.scrollTo(0, currentScrollY);
});
}
function toggleLogos() {
const headerToggle = document.getElementById('logoToggle');
const menuToggle = document.getElementById('logoToggleMenu');
const paper = document.querySelector('.cv-paper');
// 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;
// Save current scroll position
const currentScrollY = window.scrollY || window.pageYOffset;
if (isChecked) {
paper.classList.add('show-logos');
localStorage.setItem('cv-logos', 'show');
} else {
paper.classList.remove('show-logos');
localStorage.setItem('cv-logos', 'hide');
}
// Restore scroll position after DOM updates
requestAnimationFrame(() => {
window.scrollTo(0, currentScrollY);
});
}
function toggleTheme() {
const headerToggle = document.getElementById('themeToggle');
const menuToggle = document.getElementById('themeToggleMenu');
const container = document.querySelector('.cv-container');
// 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) {
container.classList.add('theme-clean');
localStorage.setItem('cv-theme', 'clean');
} else {
container.classList.remove('theme-clean');
localStorage.setItem('cv-theme', 'default');
}
}
// Print Friendly - Apply Clean Theme + Short Version for minimal printing
function printFriendly() {
const container = document.querySelector('.cv-container');
const paper = document.querySelector('.cv-paper');
const wasClean = container.classList.contains('theme-clean');
const wasLong = paper.classList.contains('cv-long');
// Apply clean theme for minimal print (no sidebars, no header, no icons)
if (!wasClean) {
container.classList.add('theme-clean');
}
// Force SHORT version for print (hide detailed content)
paper.classList.remove('cv-long');
paper.classList.add('cv-short');
// Small delay to let CSS apply
setTimeout(() => {
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);
}
// Initialize with saved preferences or defaults
document.addEventListener('DOMContentLoaded', function() {
const paper = document.querySelector('.cv-paper');
// Handle language preference
const urlLang = new URLSearchParams(window.location.search).get('lang');
const savedLang = localStorage.getItem('cv-language');
if (!urlLang && savedLang) {
// URL is clean but we have a saved preference - load that language
htmx.ajax('GET', `/cv?lang=${savedLang}`, {
target: '#cv-content',
swap: 'innerHTML swap:0ms settle:0ms'
});
// Update button states
document.querySelectorAll('.language-selector .selector-btn').forEach(btn => {
btn.classList.remove('active');
if (btn.getAttribute('onclick').includes(savedLang)) {
btn.classList.add('active');
}
});
// Update html lang attribute
document.documentElement.lang = savedLang;
} else if (urlLang) {
// Save URL language to localStorage
localStorage.setItem('cv-language', urlLang);
}
// Restore CV length preference
const savedLength = localStorage.getItem('cv-length') || 'short';
const lengthChecked = savedLength === 'long';
if (lengthChecked) {
paper.classList.add('cv-long');
paper.classList.remove('cv-short');
} else {
paper.classList.add('cv-short');
paper.classList.remove('cv-long');
}
// 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;
// Restore logos preference
const savedLogos = localStorage.getItem('cv-logos') || 'show';
const logosChecked = savedLogos === 'show';
if (logosChecked) {
paper.classList.add('show-logos');
} else {
paper.classList.remove('show-logos');
}
// 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;
// Restore theme preference
const savedTheme = localStorage.getItem('cv-theme') || 'default';
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) {
toggleTheme();
}
});
2025-10-31 11:06:38 +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;
const isMenuOpen = navMenu.classList.contains('menu-open');
// If scrolling up, reset the keepHeaderVisible flag
if (currentScroll < lastScrollTop) {
keepHeaderVisible = false;
}
// Hide/show header based on scroll direction
if (currentScroll > scrollThreshold) {
if (currentScroll > lastScrollTop && !keepHeaderVisible) {
// Scrolling down - hide header (only if keepHeaderVisible is false)
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'
});
});
// 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();
}
}
// 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
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
closeInfoModal();
closePdfModal();
}
});
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);
})();
</script>
2025-11-09 15:02:31 +00:00
<!-- End Matomo Code -->
2025-10-20 08:54:21 +01:00
</body>
</html>