Files
cv-site/templates/index.html
T
juanatsap 5447b1e0a5 feat: add GitHub repository link to footer
- Add centered GitHub link above copyright line
- Include GitHub icon using iconify-icon
- Link points to https://github.com/juanatsap/cv-site
- Bilingual support: 'View this project on GitHub' / 'Ver este proyecto en GitHub'
- Styled with accent blue color and inline-flex for icon alignment
2025-11-09 02:52:38 +00:00

540 lines
24 KiB
HTML
Raw Blame History

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