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)
This commit is contained in:
juanatsap
2025-11-06 09:11:17 +00:00
parent 2c372eee49
commit 51597c074b
7 changed files with 349 additions and 101 deletions
+136 -14
View File
@@ -339,25 +339,72 @@
] ]
}, },
{ {
"category": "JavaScript Ecosystem", "category": "Programming Languages",
"proficiency": 5, "proficiency": 4,
"items": [ "items": [
"Advanced JavaScript (ES6+)", "JAVA/Groovy",
"React & React Ecosystem", "PHP",
"Node.js & Express", "Scala",
"Webpack, Vite, Modern Build Tools" "XML/XSLT",
"Action Script",
"Shell Scripts (Unix)",
"C/C++"
] ]
}, },
{ {
"category": "Web Development", "category": "Web Development",
"proficiency": 5, "proficiency": 5,
"items": [ "items": [
"HTML5, CSS3, Semantic Web", "JSP/PHP",
"HTML(5)/XHTML/Handlebars/Moustache/Velocity/Freemarker",
"CSS/Less/Sass/Javascript/jQuery/mooTools",
"DOM/Ajax/SEO/WebServices",
"REST API Design & Development", "REST API Design & Development",
"LESS, SASS, CSS Preprocessors",
"Responsive & Mobile-First Design" "Responsive & Mobile-First Design"
] ]
}, },
{
"category": "JavaScript Frameworks",
"proficiency": 5,
"items": [
"Node & ReactJS",
"Redux/Flux",
"Webpack2/Express",
"Gulp/Grunt"
]
},
{
"category": "PHP Frameworks",
"proficiency": 4,
"items": [
"Yii Framework",
"Zend Framework",
"Wordpress API",
"Joomla API"
]
},
{
"category": "Java Frameworks",
"proficiency": 4,
"items": [
"Play! Framework",
"Struts",
"Spring",
"Hibernate",
"Ibatis",
"Magnolia CMS",
"XWiki",
"TESEO Framework"
]
},
{
"category": "Application Servers",
"proficiency": 4,
"items": [
"Apache/WAMP/MAMP",
"Tomcat/JBoss/Resin/Jetty/Websphere/Weblogic"
]
},
{ {
"category": "Backend Technologies", "category": "Backend Technologies",
"proficiency": 4, "proficiency": 4,
@@ -373,11 +420,44 @@
"category": "Databases", "category": "Databases",
"proficiency": 4, "proficiency": 4,
"items": [ "items": [
"PostgreSQL",
"MySQL",
"Oracle", "Oracle",
"MongoDB (NoSQL)", "MySql",
"Database Design & Optimization" "Postgresql",
"Hypersonic",
"SQL Knowledge"
]
},
{
"category": "CMS's and Web Production Environments",
"proficiency": 4,
"items": [
"Joomla",
"Wordpress",
"RapidWeaver",
"Servoy",
"WebRatio",
"Play! Framework"
]
},
{
"category": "Design Tools",
"proficiency": 3,
"items": [
"Corel Draw",
"Adobe PhotoShop",
"Adobe Illustrator",
"GIMP"
]
},
{
"category": "Team Management",
"proficiency": 4,
"items": [
"Preparation and projects startup",
"Fluid communication with clients",
"Recruitment",
"Tasks management",
"Monthly reports"
] ]
}, },
{ {
@@ -472,7 +552,7 @@
"name": "AI-Powered Development Workflows", "name": "AI-Powered Development Workflows",
"role": "Independent Research & Development", "role": "Independent Research & Development",
"period": "2023 - Present", "period": "2023 - Present",
"description": "Pioneered AI-assisted development workflows using Claude Code and modern tools. Successfully experimented with migrating projects from React to HTMX+Go architecture, reducing complexity while maintaining functionality.", "description": "Pioneered AI-assisted development workflows using Claude Code and modern tools. Successfully experimented with migrating projects from React to HTMX + Go architecture, reducing complexity while maintaining functionality.",
"technologies": [ "technologies": [
"Claude Code", "Claude Code",
"HTMX", "HTMX",
@@ -564,8 +644,50 @@
"description": "Data protection and GDPR compliance certification" "description": "Data protection and GDPR compliance certification"
} }
], ],
"references": [
{
"title": "Recommendations Letter from TwenTIC",
"url": "http://www.drolosoft.com/2010/downloads/recomendacion.pdf",
"type": "recommendation"
},
{
"title": "Presentation Letter",
"url": "http://www.domestika.org/empleo/demanda/txeo",
"type": "presentation"
},
{
"title": "Complete Portfolio (English)",
"url": "http://www.behance.net/txeo",
"type": "portfolio"
},
{
"title": "Complete Portfolio (Spanish)",
"url": "http://www.domestika.org/portfolios/txeo",
"type": "portfolio"
},
{
"title": "LinkedIn Profile",
"url": "https://www.linkedin.com/in/juan-andr%C3%A9s-moreno-rubio-3277729/",
"type": "profile"
},
{
"title": "Tecnoempleo Profile",
"url": "https://www.tecnoempleo.com/juan-andres-moreno-rubio.mpt",
"type": "profile"
},
{
"title": "Currículum Vitae in PDF (Spanish)",
"url": "http://www.morenoyrubio.com/cv/cv_jamr_2021_es.pdf",
"type": "cv"
},
{
"title": "Currículum Vitae chronological (7 pages)",
"url": "http://morenoyrubio.com/cv/cv_cronologico_jamr_2021_en.pdf",
"type": "cv"
}
],
"other": { "other": {
"driverLicense": "Type C" "driverLicense": "Type B"
}, },
"meta": { "meta": {
"version": "2024", "version": "2024",
+12 -1
View File
@@ -495,6 +495,17 @@
"Adobe Illustrator", "Adobe Illustrator",
"GIMP" "GIMP"
] ]
},
{
"category": "Gestión de Equipos",
"proficiency": 4,
"items": [
"Preparación y puesta en marcha de proyectos",
"Comunicación fluida con los clientes",
"Contratación de personal",
"Gestión de tareas",
"Reportes mensuales"
]
} }
], ],
"soft_skills": [ "soft_skills": [
@@ -582,7 +593,7 @@
"name": "Flujos de Trabajo de Desarrollo Potenciados por IA", "name": "Flujos de Trabajo de Desarrollo Potenciados por IA",
"role": "Investigación y Desarrollo Independiente", "role": "Investigación y Desarrollo Independiente",
"period": "2023 - Presente", "period": "2023 - Presente",
"description": "Desarrollo pionero de flujos de trabajo asistidos por IA usando Claude Code y herramientas modernas. Experimentación exitosa con migración de proyectos de arquitectura React a HTMX+Go, reduciendo complejidad mientras se mantiene funcionalidad.", "description": "Desarrollo pionero de flujos de trabajo asistidos por IA usando Claude Code y herramientas modernas. Experimentación exitosa con migración de proyectos de arquitectura React a HTMX + Go, reduciendo complejidad mientras se mantiene funcionalidad.",
"technologies": [ "technologies": [
"Claude Code", "Claude Code",
"HTMX", "HTMX",
+37 -8
View File
@@ -51,12 +51,16 @@ func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
// Split skills between left and right sidebars // Split skills between left and right sidebars
skillsLeft, skillsRight := splitSkills(cv.Skills.Technical) skillsLeft, skillsRight := splitSkills(cv.Skills.Technical)
// Calculate years of experience
yearsOfExperience := calculateYearsOfExperience()
// Prepare template data // Prepare template data
data := map[string]interface{}{ data := map[string]interface{}{
"CV": cv, "CV": cv,
"Lang": lang, "Lang": lang,
"SkillsLeft": skillsLeft, "SkillsLeft": skillsLeft,
"SkillsRight": skillsRight, "SkillsRight": skillsRight,
"YearsOfExperience": yearsOfExperience,
} }
// Render template // Render template
@@ -97,12 +101,16 @@ func (h *CVHandler) CVContent(w http.ResponseWriter, r *http.Request) {
// Split skills between left and right sidebars // Split skills between left and right sidebars
skillsLeft, skillsRight := splitSkills(cv.Skills.Technical) skillsLeft, skillsRight := splitSkills(cv.Skills.Technical)
// Calculate years of experience
yearsOfExperience := calculateYearsOfExperience()
// Prepare template data // Prepare template data
data := map[string]interface{}{ data := map[string]interface{}{
"CV": cv, "CV": cv,
"Lang": lang, "Lang": lang,
"SkillsLeft": skillsLeft, "SkillsLeft": skillsLeft,
"SkillsRight": skillsRight, "SkillsRight": skillsRight,
"YearsOfExperience": yearsOfExperience,
} }
// Render template // Render template
@@ -178,3 +186,24 @@ func splitSkills(skills []models.SkillCategory) (left, right []models.SkillCateg
return left, right return left, right
} }
// calculateYearsOfExperience calculates years of experience since April 1, 2005
// This matches the original React implementation that calculated from 01/04/2005
func calculateYearsOfExperience() int {
// First day at work: April 1, 2005
firstDay := time.Date(2005, time.April, 1, 9, 0, 0, 0, time.UTC)
// Current date
now := time.Now()
// Calculate the difference in years
years := now.Year() - firstDay.Year()
// Adjust if we haven't reached the anniversary this year yet
if now.Month() < firstDay.Month() ||
(now.Month() == firstDay.Month() && now.Day() < firstDay.Day()) {
years--
}
return years
}
+35 -15
View File
@@ -1,16 +1,19 @@
/* Logo Toggle Component */ /* Toggle Components - Unified Design */
.language-toggle,
.cv-length-toggle,
.logo-toggle { .logo-toggle {
display: flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 0.5rem;
white-space: nowrap;
} }
.toggle-switch { .toggle-switch {
display: flex; display: inline-block;
align-items: center;
gap: 0.5rem;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
position: relative;
} }
.toggle-switch input[type="checkbox"] { .toggle-switch input[type="checkbox"] {
@@ -23,23 +26,24 @@
.toggle-slider { .toggle-slider {
position: relative; position: relative;
display: inline-block; display: inline-block;
width: 44px; width: 50px;
height: 24px; height: 26px;
background-color: #ccc; background-color: #555;
border-radius: 24px; border-radius: 26px;
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
} }
.toggle-slider::after { .toggle-slider::after {
content: ''; content: '';
position: absolute; position: absolute;
width: 18px; width: 20px;
height: 18px; height: 20px;
border-radius: 50%; border-radius: 50%;
background-color: white; background-color: white;
top: 3px; top: 3px;
left: 3px; left: 3px;
transition: transform 0.3s ease; transition: transform 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
} }
.toggle-switch input[type="checkbox"]:checked + .toggle-slider { .toggle-switch input[type="checkbox"]:checked + .toggle-slider {
@@ -47,17 +51,33 @@
} }
.toggle-switch input[type="checkbox"]:checked + .toggle-slider::after { .toggle-switch input[type="checkbox"]:checked + .toggle-slider::after {
transform: translateX(20px); transform: translateX(24px);
} }
.toggle-switch input[type="checkbox"]:focus + .toggle-slider { .toggle-switch input[type="checkbox"]:focus + .toggle-slider {
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.2); box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.2);
} }
.toggle-label { .toggle-label-left,
font-size: 0.9rem; .toggle-label-right {
font-size: 0.8rem;
font-weight: 500; font-weight: 500;
color: #ccc; color: #999;
transition: color 0.3s ease;
white-space: nowrap;
}
/* Highlight active label based on parent container state */
.language-toggle:has(#langToggle:not(:checked)) .toggle-label-left,
.cv-length-toggle:has(#lengthToggle:not(:checked)) .toggle-label-left,
.logo-toggle:has(#logoToggle:not(:checked)) .toggle-label-left {
color: #fff;
}
.language-toggle:has(#langToggle:checked) .toggle-label-right,
.cv-length-toggle:has(#lengthToggle:checked) .toggle-label-right,
.logo-toggle:has(#logoToggle:checked) .toggle-label-right {
color: #fff;
} }
/* Experience Item with Logo Support */ /* Experience Item with Logo Support */
+63 -12
View File
@@ -49,19 +49,61 @@ a:hover {
} }
.action-bar-content { .action-bar-content {
max-width: 1200px; max-width: 100%;
margin: 0 auto; margin: 0 auto;
padding: 1rem 2rem; padding: 0.75rem 1.5rem;
display: grid; display: grid;
grid-template-columns: auto auto auto 1fr; grid-template-columns: 1fr auto 1fr;
align-items: center; align-items: center;
gap: 2rem; gap: 2rem;
} }
.language-toggle { /* Left: Site Title */
.site-title {
display: flex; display: flex;
gap: 0.5rem; align-items: center;
gap: 0.75rem;
justify-self: start; justify-self: start;
white-space: nowrap;
}
.site-icon {
color: #fff;
flex-shrink: 0;
}
.site-title-text {
font-size: 1.1rem;
font-weight: 600;
color: #fff;
letter-spacing: 0.5px;
}
/* Center: Toggle controls */
.toggle-controls-center {
display: flex;
flex-direction: row;
align-items: center;
gap: 1.5rem;
justify-self: center;
flex-shrink: 0;
white-space: nowrap;
}
.language-toggle,
.cv-length-toggle,
.logo-toggle {
flex-shrink: 0;
}
/* Right: Action buttons */
.action-buttons {
justify-self: end;
flex-shrink: 0;
}
.htmx-indicator {
flex-shrink: 0;
} }
.lang-btn { .lang-btn {
@@ -89,19 +131,20 @@ a:hover {
} }
.export-btn { .export-btn {
padding: 0.4rem 1rem; padding: 0.4rem 0.75rem;
background: transparent; background: transparent;
color: white; color: white;
border: 1px solid rgba(255,255,255,0.3); border: 1px solid rgba(255,255,255,0.3);
border-radius: 3px; border-radius: 3px;
cursor: pointer; cursor: pointer;
font-size: 0.875rem; font-size: 0.8rem;
font-weight: 400; font-weight: 400;
transition: all 0.2s ease; transition: all 0.2s ease;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.4rem;
text-decoration: none; /* For anchor tags */ text-decoration: none;
white-space: nowrap;
} }
.export-btn:hover { .export-btn:hover {
@@ -141,12 +184,11 @@ a:hover {
font-weight: 600; font-weight: 600;
} }
/* Action buttons on right */ /* Action buttons styling (already positioned by grid) */
.action-buttons { .action-buttons {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
align-items: center; align-items: center;
justify-self: end;
} }
/* Loading Indicator */ /* Loading Indicator */
@@ -177,7 +219,7 @@ a:hover {
width: 100%; width: 100%;
max-width: 100%; /* Full width to accommodate pages */ max-width: 100%; /* Full width to accommodate pages */
margin: 0 auto; margin: 0 auto;
padding: 100px 0 0 0; /* Top padding to prevent sticky action bar overlap */ padding: 20px 0 0 0; /* Top padding to prevent sticky action bar overlap */
display: block; /* Changed from flex */ display: block; /* Changed from flex */
} }
@@ -320,6 +362,15 @@ a:hover {
margin: 0; margin: 0;
} }
.years-experience {
font-family: 'Quicksand', sans-serif;
font-size: 0.85em;
font-weight: 400;
color: #666;
margin: 4px 0 0 0;
line-height: 1.4;
}
/* Intro/Excerpt Text - Positioned inside header, matching old React CV */ /* Intro/Excerpt Text - Positioned inside header, matching old React CV */
.intro-text { .intro-text {
font-family: 'Quicksand', sans-serif; font-family: 'Quicksand', sans-serif;
+13 -3
View File
@@ -2,13 +2,15 @@
<div class="cv-page page-1"> <div class="cv-page page-1">
<!-- Professional Title Badges - Full Width Top Bar --> <!-- Professional Title Badges - Full Width Top Bar -->
<div class="cv-title-badges-header"> <div class="cv-title-badges-header">
<span class="title-badge">{{if eq .Lang "es"}}ANALISTA PROGRAMADOR{{else}}ANALYST PROGRAMMER{{end}}</span> <span class="title-badge">{{if eq .Lang "es"}}ANALISTA{{else}}ANALYST{{end}}</span>
<span class="badge-separator">|</span>
<span class="title-badge">{{if eq .Lang "es"}}CONSULTOR TÉCNICO{{else}}TECHNICAL CONSULTANT{{end}}</span>
<span class="badge-separator">|</span> <span class="badge-separator">|</span>
<span class="title-badge">NODEJS + REACTJS {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span> <span class="title-badge">NODEJS + REACTJS {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span>
<span class="badge-separator">|</span> <span class="badge-separator">|</span>
<span class="title-badge">WEB {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span> <span class="title-badge">WEB {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span>
<span class="badge-separator">|</span> <span class="badge-separator">|</span>
<span class="title-badge">GO+HTMX {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span> <span class="title-badge">GO + HTMX {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span>
<span class="badge-separator">|</span> <span class="badge-separator">|</span>
<span class="title-badge">PHP {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span> <span class="title-badge">PHP {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span>
</div> </div>
@@ -34,6 +36,7 @@
<div class="cv-header-content"> <div class="cv-header-content">
<div class="cv-header-left"> <div class="cv-header-left">
<h1 class="cv-name">{{.CV.Personal.Name}}</h1> <h1 class="cv-name">{{.CV.Personal.Name}}</h1>
<p class="years-experience">{{.YearsOfExperience}} {{if eq .Lang "es"}}años de experiencia{{else}}years of experience{{end}}</p>
<!-- Intro/Excerpt Text - No section heading, just the text --> <!-- Intro/Excerpt Text - No section heading, just the text -->
<div class="intro-text">{{.CV.Summary}}</div> <div class="intro-text">{{.CV.Summary}}</div>
</div> </div>
@@ -124,7 +127,7 @@
<span class="badge-separator">|</span> <span class="badge-separator">|</span>
<span class="title-badge">WEB {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span> <span class="title-badge">WEB {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span>
<span class="badge-separator">|</span> <span class="badge-separator">|</span>
<span class="title-badge">GO+HTMX {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span> <span class="title-badge">GO + HTMX {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span>
<span class="badge-separator">|</span> <span class="badge-separator">|</span>
<span class="title-badge">PHP {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span> <span class="title-badge">PHP {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span>
</div> </div>
@@ -241,6 +244,13 @@
<a href="mailto:{{.CV.Personal.Email}}" target="_blank" rel="noopener noreferrer">{{.CV.Personal.Email}}</a> <a href="mailto:{{.CV.Personal.Email}}" target="_blank" rel="noopener noreferrer">{{.CV.Personal.Email}}</a>
</div> </div>
</li> </li>
<li>
<div class="footer-label">phone#</div>
<div class="footer-separator"><i class="fa fa-circle"></i></div>
<div class="footer-value">
<a href="tel:+34676875420" target="_blank" rel="noopener noreferrer">+34 676 875 420</a>
</div>
</li>
</ul> </ul>
</footer> </footer>
</div> </div>
+53 -48
View File
@@ -94,55 +94,45 @@
<!-- Single Black Bar with Everything --> <!-- Single Black Bar with Everything -->
<div class="action-bar no-print" role="navigation" aria-label="Language and export controls"> <div class="action-bar no-print" role="navigation" aria-label="Language and export controls">
<div class="action-bar-content"> <div class="action-bar-content">
<!-- Left: Language buttons --> <!-- Left: Site Title -->
<div class="language-toggle" role="group" aria-label="Language selection"> <div class="site-title">
<button <svg class="site-icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
class="lang-btn {{if eq .Lang "en"}}active{{end}}" <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"/>
hx-get="/cv?lang=en" </svg>
hx-target="#cv-content" <span class="site-title-text">{{if eq .Lang "es"}}Curriculum Vitae 2025{{else}}Curriculum Vitae 2025{{end}}</span>
hx-swap="innerHTML swap:200ms settle:200ms" </div>
hx-push-url="/?lang=en"
hx-indicator="#loading" <!-- Center: Toggle controls -->
aria-label="Switch to English" <div class="toggle-controls-center">
aria-pressed="{{if eq .Lang "en"}}true{{else}}false{{end}}"> <!-- Language toggle -->
English <div class="language-toggle">
</button> <span class="toggle-label-left">EN</span>
<button <label class="toggle-switch">
class="lang-btn {{if eq .Lang "es"}}active{{end}}" <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}}">
hx-get="/cv?lang=es" <span class="toggle-slider"></span>
hx-target="#cv-content" </label>
hx-swap="innerHTML swap:200ms settle:200ms" <span class="toggle-label-right">ES</span>
hx-push-url="/?lang=es"
hx-indicator="#loading"
aria-label="Switch to Spanish"
aria-pressed="{{if eq .Lang "es"}}true{{else}}false{{end}}">
Español
</button>
</div> </div>
<!-- Center: CV Length Toggle --> <!-- Center: CV Length Toggle -->
<div class="cv-length-toggle"> <div class="cv-length-toggle">
<button <span class="toggle-label-left">{{if eq .Lang "es"}}Corto{{else}}Short{{end}}</span>
class="length-btn active" <label class="toggle-switch">
onclick="toggleCVLength('short')" <input type="checkbox" id="lengthToggle" onclick="toggleCVLength()" aria-label="{{if eq .Lang "es"}}Cambiar longitud del CV{{else}}Toggle CV length{{end}}">
aria-label="{{if eq .Lang "es"}}Ver CV corto{{else}}View short CV{{end}}"> <span class="toggle-slider"></span>
{{if eq .Lang "es"}}Corto{{else}}Short{{end}} </label>
</button> <span class="toggle-label-right">{{if eq .Lang "es"}}Largo{{else}}Long{{end}}</span>
<button
class="length-btn"
onclick="toggleCVLength('long')"
aria-label="{{if eq .Lang "es"}}Ver CV largo{{else}}View long CV{{end}}">
{{if eq .Lang "es"}}Largo{{else}}Long{{end}}
</button>
</div> </div>
<!-- Center Right: Logo Toggle --> <!-- Center Right: Logo Toggle -->
<div class="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"> <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}}"> <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> <span class="toggle-slider"></span>
<span class="toggle-label">{{if eq .Lang "es"}}Mostrar logos{{else}}Show logos{{end}}</span>
</label> </label>
<span class="toggle-label-right">{{if eq .Lang "es"}}Logos{{else}}Logos{{end}}</span>
</div>
</div> </div>
<!-- Right: Action buttons --> <!-- Right: Action buttons -->
@@ -202,21 +192,36 @@
</div> </div>
<script> <script>
function toggleCVLength(length) { function toggleLanguage() {
// Update button states const checkbox = document.getElementById('langToggle');
document.querySelectorAll('.length-btn').forEach(btn => { const lang = checkbox.checked ? 'es' : 'en';
btn.classList.remove('active');
});
event.target.classList.add('active');
// Toggle visibility // 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'); const paper = document.querySelector('.cv-paper');
if (length === 'short') {
paper.classList.add('cv-short'); if (checkbox.checked) {
paper.classList.remove('cv-long');
} else {
paper.classList.add('cv-long'); paper.classList.add('cv-long');
paper.classList.remove('cv-short'); paper.classList.remove('cv-short');
} else {
paper.classList.add('cv-short');
paper.classList.remove('cv-long');
} }
} }