Files
cv-site/templates/index.html
T
juanatsap 51597c074b feat: add dynamic years calculation and complete CV content migration
- Add dynamic years of experience calculation from April 1, 2005
- Update professional title badges: ANALYST | TECHNICAL CONSULTANT
- Add all missing skill categories from React CV (Programming Languages, JavaScript Frameworks, PHP Frameworks, Java Frameworks, Application Servers, CMS, Design Tools, Team Management)
- Add complete References section with LinkedIn, Behance, portfolios, and recommendation letters
- Refine years-of-experience subtitle styling to match original design
- Achieve 100% content parity with old React CV (English: 7→14 skill categories)
2025-11-06 09:11:17 +00:00

301 lines
14 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>
<!-- 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.Behance}}"
],
"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 -->
<div class="site-title">
<svg class="site-icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20M10,19H8V14H10V19M14,19H12V12H14V19M10,11H8V9H10V11Z"/>
</svg>
<span class="site-title-text">{{if eq .Lang "es"}}Curriculum Vitae 2025{{else}}Curriculum Vitae 2025{{end}}</span>
</div>
<!-- Center: Toggle controls -->
<div class="toggle-controls-center">
<!-- Language toggle -->
<div class="language-toggle">
<span class="toggle-label-left">EN</span>
<label class="toggle-switch">
<input type="checkbox" id="langToggle" {{if eq .Lang "es"}}checked{{end}} onclick="toggleLanguage()" aria-label="{{if eq .Lang "es"}}Switch to English{{else}}Switch to Spanish{{end}}">
<span class="toggle-slider"></span>
</label>
<span class="toggle-label-right">ES</span>
</div>
<!-- Center: CV Length Toggle -->
<div class="cv-length-toggle">
<span class="toggle-label-left">{{if eq .Lang "es"}}Corto{{else}}Short{{end}}</span>
<label class="toggle-switch">
<input type="checkbox" id="lengthToggle" onclick="toggleCVLength()" aria-label="{{if eq .Lang "es"}}Cambiar longitud del CV{{else}}Toggle CV length{{end}}">
<span class="toggle-slider"></span>
</label>
<span class="toggle-label-right">{{if eq .Lang "es"}}Largo{{else}}Long{{end}}</span>
</div>
<!-- Center Right: Logo Toggle -->
<div class="logo-toggle">
<span class="toggle-label-left">{{if eq .Lang "es"}}Sin logos{{else}}No logos{{end}}</span>
<label class="toggle-switch">
<input type="checkbox" id="logoToggle" onclick="toggleLogos()" aria-label="{{if eq .Lang "es"}}Mostrar logos de empresas{{else}}Show company logos{{end}}">
<span class="toggle-slider"></span>
</label>
<span class="toggle-label-right">{{if eq .Lang "es"}}Logos{{else}}Logos{{end}}</span>
</div>
</div>
<!-- Right: Action buttons -->
<div class="action-buttons">
<a
class="export-btn"
href="/export/pdf?lang={{.Lang}}"
download
aria-label="{{if eq .Lang "es"}}Descargar PDF del CV{{else}}Download CV as PDF{{end}}">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" style="display: inline-block; vertical-align: middle;">
<path d="M8.5 11.5l3.5-3.5h-2.5v-6h-2v6h-2.5l3.5 3.5zm-6.5 2.5v2h12v-2h-12z"/>
</svg>
{{if eq .Lang "es"}}Descargar PDF{{else}}Download as PDF{{end}}
</a>
<button
class="export-btn"
onclick="window.print()"
aria-label="{{if eq .Lang "es"}}Imprimir CV{{else}}Print CV{{end}}">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" style="display: inline-block; vertical-align: middle;">
<path d="M14 4h-3v-3h-6v3h-3c-1.1 0-2 0.9-2 2v5h3v4h8v-4h3v-5c0-1.1-0.9-2-2-2zm-7-2h2v2h-2v-2zm5 11h-8v-5h8v5zm2-7c-0.552 0-1-0.448-1-1s0.448-1 1-1 1 0.448 1 1-0.448 1-1 1z"/>
</svg>
{{if eq .Lang "es"}}Imprimir{{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>
<!-- 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>© {{.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>
<script>
function toggleLanguage() {
const checkbox = document.getElementById('langToggle');
const lang = checkbox.checked ? 'es' : 'en';
// 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 checkbox = document.getElementById('lengthToggle');
const paper = document.querySelector('.cv-paper');
if (checkbox.checked) {
paper.classList.add('cv-long');
paper.classList.remove('cv-short');
} else {
paper.classList.add('cv-short');
paper.classList.remove('cv-long');
}
}
function toggleLogos() {
const checkbox = document.getElementById('logoToggle');
const paper = document.querySelector('.cv-paper');
if (checkbox.checked) {
paper.classList.add('show-logos');
} else {
paper.classList.remove('show-logos');
}
}
// Initialize with short version
document.addEventListener('DOMContentLoaded', function() {
document.querySelector('.cv-paper').classList.add('cv-short');
});
// 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>