523e882286
Changes: - Save selected language to localStorage when user switches languages - On page load with clean URL: check localStorage and load saved language preference - If no localStorage preference exists, default to server's language choice - Update language button active states based on localStorage preference - Keep URLs clean (no lang parameter added) while maintaining language choice - If URL has lang parameter, save it to localStorage for consistency How it works: 1. Visit / (clean) → Shows English (default) 2. Switch to Spanish → Saves 'es' to localStorage, loads Spanish content 3. Click logo (clean URL /) → Page loads, checks localStorage, loads Spanish 4. Language persists across clean URL navigations via localStorage This solves: Clean URLs + Language persistence
692 lines
31 KiB
HTML
692 lines
31 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="{{if eq .Lang "es"}}es{{else}}en{{end}}">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
||
<!-- Primary Meta Tags -->
|
||
<title>{{.CV.Personal.Name}} - {{if eq .Lang "es"}}Curriculum Vitae{{else}}Curriculum Vitae{{end}}</title>
|
||
<meta name="title" content="{{.CV.Personal.Name}} - {{if eq .Lang "es"}}CV Profesional{{else}}Professional CV{{end}}">
|
||
<meta name="description" content="{{.CV.Personal.Title}} | {{if eq .Lang "es"}}18 años de experiencia en desarrollo web, SAP CDC, React, Node.js, Go, HTMX y desarrollo asistido por IA{{else}}18 years of experience in web development, SAP CDC, React, Node.js, Go, HTMX and AI-assisted development{{end}}">
|
||
<meta name="keywords" content="{{if eq .Lang "es"}}CV, Curriculum Vitae, {{.CV.Personal.Name}}, Desarrollador FullStack, SAP CDC, React, Node.js, Go, HTMX, IA, Desarrollo Web, Consultor Técnico{{else}}CV, Resume, {{.CV.Personal.Name}}, FullStack Developer, SAP CDC, React, Node.js, Go, HTMX, AI, Web Development, Technical Consultant{{end}}">
|
||
<meta name="author" content="{{.CV.Personal.Name}}">
|
||
<meta name="robots" content="index, follow">
|
||
<link rel="canonical" href="{{.CV.Personal.Website}}">
|
||
|
||
<!-- Open Graph / Facebook -->
|
||
<meta property="og:type" content="profile">
|
||
<meta property="og:url" content="{{.CV.Personal.Website}}">
|
||
<meta property="og:title" content="{{.CV.Personal.Name}} - {{if eq .Lang "es"}}CV Profesional{{else}}Professional CV{{end}}">
|
||
<meta property="og:description" content="{{.CV.Personal.Title}} | {{if eq .Lang "es"}}Consultor Técnico Senior con 18 años de experiencia{{else}}Senior Technical Consultant with 18 years of experience{{end}}">
|
||
<meta property="og:image" content="{{.CV.Personal.Website}}/static/images/profile.jpg">
|
||
<meta property="og:locale" content="{{if eq .Lang "es"}}es_ES{{else}}en_US{{end}}">
|
||
<meta property="og:site_name" content="{{.CV.Personal.Name}}">
|
||
<meta property="profile:first_name" content="Juan Andrés">
|
||
<meta property="profile:last_name" content="Moreno Rubio">
|
||
<meta property="profile:username" content="txeo">
|
||
|
||
<!-- Social Media Card (Generic) -->
|
||
<meta name="twitter:card" content="summary">
|
||
<meta name="twitter:title" content="{{.CV.Personal.Name}} - {{if eq .Lang "es"}}CV Profesional{{else}}Professional CV{{end}}">
|
||
<meta name="twitter:description" content="{{.CV.Personal.Title}}">
|
||
<meta name="twitter:image" content="{{.CV.Personal.Website}}/static/images/profile.jpg">
|
||
|
||
<!-- HTMX Configuration -->
|
||
<meta name="htmx-config" content='{"timeout":5000,"defaultSwapStyle":"innerHTML","defaultSwapDelay":0,"defaultSettleDelay":20}'>
|
||
|
||
<!-- HTMX with SRI (Subresource Integrity) -->
|
||
<script src="https://unpkg.com/htmx.org@1.9.10"
|
||
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
|
||
crossorigin="anonymous"></script>
|
||
|
||
<!-- Iconify - Load synchronously for immediate rendering -->
|
||
<script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
|
||
|
||
<!-- CSS -->
|
||
<link rel="stylesheet" href="/static/css/main.css">
|
||
<link rel="stylesheet" href="/static/css/logo-toggle.css">
|
||
<link rel="stylesheet" href="/static/css/print.css" media="print">
|
||
|
||
<!-- Fonts with Preload -->
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||
|
||
<!-- Structured Data (JSON-LD) -->
|
||
<script type="application/ld+json">
|
||
{
|
||
"@context": "https://schema.org",
|
||
"@type": "Person",
|
||
"name": "{{.CV.Personal.Name}}",
|
||
"jobTitle": "{{.CV.Personal.Title}}",
|
||
"url": "{{.CV.Personal.Website}}",
|
||
"email": "{{.CV.Personal.Email}}",
|
||
"telephone": "{{.CV.Personal.Phone}}",
|
||
"address": {
|
||
"@type": "PostalAddress",
|
||
"addressLocality": "{{.CV.Personal.Location}}"
|
||
},
|
||
"sameAs": [
|
||
"{{.CV.Personal.LinkedIn}}",
|
||
"{{.CV.Personal.GitHub}}",
|
||
"{{.CV.Personal.Domestika}}"
|
||
],
|
||
"alumniOf": {
|
||
"@type": "EducationalOrganization",
|
||
"name": "Universidad de Extremadura"
|
||
},
|
||
"knowsAbout": [
|
||
"Web Development",
|
||
"SAP Customer Data Cloud",
|
||
"React",
|
||
"Node.js",
|
||
"Go",
|
||
"HTMX",
|
||
"AI-Assisted Development",
|
||
"Full Stack Development"
|
||
],
|
||
"worksFor": {
|
||
"@type": "Organization",
|
||
"name": "Olympic Broadcasting Services"
|
||
}
|
||
}
|
||
</script>
|
||
</head>
|
||
<body>
|
||
<!-- Single Black Bar with Everything -->
|
||
<div class="action-bar no-print" role="navigation" aria-label="Language and export controls">
|
||
<div class="action-bar-content">
|
||
<!-- Left: Site Title + Hamburger Menu + Language -->
|
||
<div class="site-title">
|
||
<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" onclick="toggleMenu()" aria-label="Toggle navigation menu">
|
||
<iconify-icon icon="mdi:menu" width="24" height="24"></iconify-icon>
|
||
</button>
|
||
|
||
<a href="/" class="site-title-link" id="titleLink">
|
||
<span class="site-title-text">CV JAMR - {{.CurrentYear}}</span>
|
||
</a>
|
||
|
||
<!-- Language selector (next to title) -->
|
||
<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>
|
||
</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">
|
||
<a
|
||
class="action-btn pdf-btn"
|
||
href="/export/pdf?lang={{.Lang}}"
|
||
download
|
||
aria-label="{{if eq .Lang "es"}}Descargar como PDF{{else}}Download as PDF{{end}}">
|
||
<iconify-icon icon="mdi:download" width="18" height="18"></iconify-icon>
|
||
{{if eq .Lang "es"}}Descargar como PDF{{else}}Download as PDF{{end}}
|
||
</a>
|
||
<button
|
||
class="action-btn print-btn"
|
||
onclick="printFriendly()"
|
||
aria-label="{{if eq .Lang "es"}}Imprimir amigable{{else}}Print Friendly{{end}}">
|
||
<iconify-icon icon="mdi:leaf" width="18" height="18"></iconify-icon>
|
||
{{if eq .Lang "es"}}Imprimir amigable{{else}}Print Friendly{{end}}
|
||
</button>
|
||
</div>
|
||
|
||
<span id="loading"
|
||
class="htmx-indicator"
|
||
role="status"
|
||
aria-live="polite"
|
||
aria-label="Loading">
|
||
<span class="loader"></span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Navigation Menu (Hidden by default) -->
|
||
<nav id="navigation-menu" class="navigation-menu no-print" role="navigation" aria-label="CV sections">
|
||
<div class="menu-content">
|
||
<a href="#" 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>
|
||
</a>
|
||
<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>
|
||
</a>
|
||
<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>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- CV Content Container -->
|
||
<div class="cv-container">
|
||
<main id="cv-content"
|
||
class="cv-paper"
|
||
role="main"
|
||
aria-live="polite">
|
||
{{template "cv-content.html" .}}
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Footer (hidden in print) -->
|
||
<footer class="no-print">
|
||
<p 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;">
|
||
<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>
|
||
<p>© {{.CV.Meta.LastUpdated}} {{.CV.Personal.Name}} |
|
||
{{if eq .Lang "es"}}Última actualización{{else}}Last updated{{end}}: {{.CV.Meta.LastUpdated}}</p>
|
||
</footer>
|
||
|
||
<!-- Error Toast -->
|
||
<div id="error-toast" class="error-toast no-print" role="alert" aria-live="assertive" style="display: none;">
|
||
<span class="error-icon">⚠️</span>
|
||
<span id="error-message"></span>
|
||
<button onclick="this.parentElement.style.display='none'" aria-label="Close error message" class="error-close">×</button>
|
||
</div>
|
||
|
||
<!-- Back to Top Button -->
|
||
<button id="back-to-top" class="back-to-top no-print" aria-label="{{if eq .Lang "es"}}Volver arriba{{else}}Back to top{{end}}" style="display: none;">
|
||
<iconify-icon icon="mdi:arrow-up" width="24" height="24"></iconify-icon>
|
||
</button>
|
||
|
||
<script>
|
||
// Toggle navigation menu
|
||
function toggleMenu() {
|
||
const menu = document.getElementById('navigation-menu');
|
||
const btn = document.querySelector('.hamburger-btn');
|
||
|
||
if (menu.classList.contains('menu-open')) {
|
||
menu.classList.remove('menu-open');
|
||
btn.setAttribute('aria-expanded', 'false');
|
||
} else {
|
||
menu.classList.add('menu-open');
|
||
btn.setAttribute('aria-expanded', 'true');
|
||
}
|
||
}
|
||
|
||
// 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');
|
||
});
|
||
}
|
||
|
||
// Toggle submenu
|
||
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);
|
||
}
|
||
}
|
||
|
||
// 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');
|
||
}
|
||
}
|
||
});
|
||
|
||
// 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 toggle = document.getElementById('lengthToggle');
|
||
const paper = document.querySelector('.cv-paper');
|
||
|
||
// Save current scroll position
|
||
const currentScrollY = window.scrollY || window.pageYOffset;
|
||
|
||
if (toggle.checked) {
|
||
paper.classList.add('cv-long');
|
||
paper.classList.remove('cv-short');
|
||
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 toggle = document.getElementById('logoToggle');
|
||
const paper = document.querySelector('.cv-paper');
|
||
|
||
// Save current scroll position
|
||
const currentScrollY = window.scrollY || window.pageYOffset;
|
||
|
||
if (toggle.checked) {
|
||
paper.classList.add('show-logos');
|
||
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 toggle = document.getElementById('themeToggle');
|
||
const container = document.querySelector('.cv-container');
|
||
|
||
if (toggle.checked) {
|
||
container.classList.add('theme-clean');
|
||
localStorage.setItem('cv-theme', 'clean');
|
||
} else {
|
||
container.classList.remove('theme-clean');
|
||
localStorage.setItem('cv-theme', 'default');
|
||
}
|
||
}
|
||
|
||
// Print Friendly - applies Clean theme before printing
|
||
function printFriendly() {
|
||
const container = document.querySelector('.cv-container');
|
||
const wasClean = container.classList.contains('theme-clean');
|
||
|
||
// Apply clean theme for printing
|
||
if (!wasClean) {
|
||
container.classList.add('theme-clean');
|
||
}
|
||
|
||
// Print
|
||
window.print();
|
||
|
||
// Restore original theme after print dialog
|
||
setTimeout(() => {
|
||
if (!wasClean) {
|
||
container.classList.remove('theme-clean');
|
||
}
|
||
}, 100);
|
||
}
|
||
|
||
// Initialize with 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';
|
||
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;
|
||
}
|
||
|
||
// Restore theme preference
|
||
const savedTheme = localStorage.getItem('cv-theme') || 'default';
|
||
if (savedTheme === 'clean') {
|
||
document.getElementById('themeToggle').checked = true;
|
||
toggleTheme();
|
||
}
|
||
});
|
||
|
||
// Scroll Direction Detection - Hide/Show Header
|
||
let lastScrollTop = 0;
|
||
let scrollThreshold = 100; // Start hiding after 100px scroll
|
||
|
||
window.addEventListener('scroll', function() {
|
||
const actionBar = document.querySelector('.action-bar');
|
||
const navMenu = document.querySelector('.navigation-menu');
|
||
const backToTopBtn = document.getElementById('back-to-top');
|
||
const currentScroll = window.pageYOffset || document.documentElement.scrollTop;
|
||
const isMenuOpen = navMenu.classList.contains('menu-open');
|
||
|
||
// 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'
|
||
});
|
||
});
|
||
|
||
// 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);
|
||
}
|
||
});
|
||
|
||
// 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>
|
||
<!-- End Matomo Code -->
|
||
</body>
|
||
</html>
|