Files
cv-site/templates/index.html
T
juanatsap f3cce51fb3 feat: implement color theme switcher with dynamic button colors
Complete color theme system (light/dark/auto) with dynamic UI:

Features:
- Color theme switcher with auto/light/dark modes
- Dynamic button colors on hover (purple/yellow/blue per theme)
- localStorage persistence across sessions
- Proper button positioning (desktop and mobile)
- Mobile: 5-button layout with theme before info button

Fixes:
- CSP updated to allow jsDelivr CDN for iconify icons
- Button repositioning: Download PDF and Print Friendly at top
- Hover-only colors (not persistent)
- Mobile button order corrected

Files:
- static/css/color-theme.css - Theme system with CSS variables
- static/js/color-theme.js - Theme switching logic
- templates/partials/color-theme-switcher.html - Button component
- internal/middleware/security.go - CSP fix for jsDelivr
- tests/mjs/13-color-theme-switcher.test.mjs - Comprehensive test
- tests/TEST-SUMMARY.md - Updated test documentation
2025-11-18 15:49:30 +00:00

221 lines
10 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="{{.CanonicalURL}}">
<!-- Hreflang tags for international SEO -->
<link rel="alternate" hreflang="en" href="{{.AlternateEN}}">
<link rel="alternate" hreflang="es" href="{{.AlternateES}}">
<link rel="alternate" hreflang="x-default" href="https://juan.andres.morenorub.io/?lang=en">
<!-- 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}'>
<!-- FOUC Prevention: Apply color theme before page render -->
<script>
(function() {
const theme = localStorage.getItem('color-theme-mode') || 'auto';
document.documentElement.setAttribute('data-color-theme', theme);
})();
</script>
<!-- HTMX with SRI (Subresource Integrity) -->
<script src="https://unpkg.com/htmx.org@1.9.10"
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
crossorigin="anonymous"></script>
<!-- CV Core Functions - Regular JavaScript (global reusable functions) -->
<script src="/static/js/cv-functions.js"></script>
<!-- Hyperscript Functions - Must load BEFORE hyperscript library -->
<!-- ✅ NO def limit with latest hyperscript - organized by category -->
<script type="text/hyperscript" src="/static/hyperscript/utils._hs"></script>
<script type="text/hyperscript" src="/static/hyperscript/toggles._hs"></script>
<script type="text/hyperscript" src="/static/hyperscript/hover-sync._hs"></script>
<!-- Color Theme System (JavaScript - hyperscript had parsing issues with colons in strings) -->
<script src="/static/js/color-theme.js"></script>
<!-- Hyperscript - Declarative event handling for enhanced interactivity -->
<script src="https://unpkg.com/hyperscript.org@0.9.14"></script>
<!-- Iconify - Load synchronously for immediate rendering -->
<!-- Using unpkg CDN (more reliable than code.iconify.design) -->
<script src="https://cdn.jsdelivr.net/npm/iconify-icon@2.1.0/dist/iconify-icon.min.js"></script>
<!-- CSS -->
<link rel="stylesheet" href="/static/css/main.css">
<link rel="stylesheet" href="/static/css/color-theme.css">
<link rel="stylesheet" href="/static/css/logo-toggle.css">
<link rel="stylesheet" href="/static/css/skeleton.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 {{if .ThemeClean}}class="theme-clean"{{end}}
_="on load call initScrollBehavior()
on scroll from window call handleScroll()
-- Skeleton loader for language transitions (global listener)
-- Add .loading to PARENT containers that persist across OOB swaps
on htmx:beforeRequest
if event.detail.elt.classList.contains('selector-btn')
add .loading to #cv-inner-content-page-1
add .loading to #cv-inner-content-page-2
end
end
on keydown
set tagName to event.target.tagName
set isInputField to (tagName is 'INPUT' or tagName is 'TEXTAREA')
-- Show shortcuts modal with '?'
if event.key is '?' and not event.ctrlKey and not event.metaKey and not event.altKey and not isInputField
halt the event
set modal to #shortcuts-modal
if modal then call modal.showModal() end
end
-- Toggle CV length with 'L'
if (event.key is 'l' or event.key is 'L') and not event.ctrlKey and not event.metaKey and not event.altKey and not isInputField
halt the event
set lengthToggle to (#lengthToggle or #lengthToggleMenu)
if lengthToggle then set lengthToggle's checked to (not lengthToggle's checked) then send change to lengthToggle end
end
-- Toggle icons with 'I'
if (event.key is 'i' or event.key is 'I') and not event.ctrlKey and not event.metaKey and not event.altKey and not isInputField
halt the event
set iconToggle to (#iconToggle or #iconToggleMenu)
if iconToggle then set iconToggle's checked to (not iconToggle's checked) then send change to iconToggle end
end
-- Toggle theme with 'V'
if (event.key is 'v' or event.key is 'V') and not event.ctrlKey and not event.metaKey and not event.altKey and not isInputField
halt the event
set themeToggle to (#themeToggle or #themeToggleMenu)
if themeToggle then set themeToggle's checked to (not themeToggle's checked) then send change to themeToggle end
end
end">
<!-- Top anchor for back-to-top link -->
<div id="top"></div>
{{template "action-bar" .}}
{{template "hamburger-menu" .}}
{{template "color-theme-switcher" .}}
<!-- Zoom Wrapper (for zoom functionality) -->
<div id="zoom-wrapper" class="zoom-wrapper">
<!-- CV Content Container -->
<div class="cv-container">
{{template "cv-content.html" .}}
</div>
</div> <!-- End zoom-wrapper -->
{{template "page-footer" .}}
{{template "error-toast" .}}
{{template "back-to-top" .}}
{{template "info-button" .}}
{{template "download-button" .}}
{{template "print-friendly-button" .}}
{{template "zoom-toggle-button" .}}
{{template "shortcuts-button" .}}
{{template "info-modal" .}}
{{template "shortcuts-modal" .}}
{{template "pdf-modal" .}}
{{template "zoom-control" .}}
<!-- External JavaScript - CSP Compliant -->
<script src="/static/js/main.js"></script>
<!-- Matomo Analytics - Nonce-based CSP -->
<script nonce="{{.CSPNonce}}">
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>