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",
"proficiency": 5,
"category": "Programming Languages",
"proficiency": 4,
"items": [
"Advanced JavaScript (ES6+)",
"React & React Ecosystem",
"Node.js & Express",
"Webpack, Vite, Modern Build Tools"
"JAVA/Groovy",
"PHP",
"Scala",
"XML/XSLT",
"Action Script",
"Shell Scripts (Unix)",
"C/C++"
]
},
{
"category": "Web Development",
"proficiency": 5,
"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",
"LESS, SASS, CSS Preprocessors",
"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",
"proficiency": 4,
@@ -373,11 +420,44 @@
"category": "Databases",
"proficiency": 4,
"items": [
"PostgreSQL",
"MySQL",
"Oracle",
"MongoDB (NoSQL)",
"Database Design & Optimization"
"MySql",
"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",
"role": "Independent Research & Development",
"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": [
"Claude Code",
"HTMX",
@@ -564,8 +644,50 @@
"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": {
"driverLicense": "Type C"
"driverLicense": "Type B"
},
"meta": {
"version": "2024",
+12 -1
View File
@@ -495,6 +495,17 @@
"Adobe Illustrator",
"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": [
@@ -582,7 +593,7 @@
"name": "Flujos de Trabajo de Desarrollo Potenciados por IA",
"role": "Investigación y Desarrollo Independiente",
"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": [
"Claude Code",
"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
skillsLeft, skillsRight := splitSkills(cv.Skills.Technical)
// Calculate years of experience
yearsOfExperience := calculateYearsOfExperience()
// Prepare template data
data := map[string]interface{}{
"CV": cv,
"Lang": lang,
"SkillsLeft": skillsLeft,
"SkillsRight": skillsRight,
"CV": cv,
"Lang": lang,
"SkillsLeft": skillsLeft,
"SkillsRight": skillsRight,
"YearsOfExperience": yearsOfExperience,
}
// Render template
@@ -97,12 +101,16 @@ func (h *CVHandler) CVContent(w http.ResponseWriter, r *http.Request) {
// Split skills between left and right sidebars
skillsLeft, skillsRight := splitSkills(cv.Skills.Technical)
// Calculate years of experience
yearsOfExperience := calculateYearsOfExperience()
// Prepare template data
data := map[string]interface{}{
"CV": cv,
"Lang": lang,
"SkillsLeft": skillsLeft,
"SkillsRight": skillsRight,
"CV": cv,
"Lang": lang,
"SkillsLeft": skillsLeft,
"SkillsRight": skillsRight,
"YearsOfExperience": yearsOfExperience,
}
// Render template
@@ -178,3 +186,24 @@ func splitSkills(skills []models.SkillCategory) (left, right []models.SkillCateg
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 {
display: flex;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
white-space: nowrap;
}
.toggle-switch {
display: flex;
align-items: center;
gap: 0.5rem;
display: inline-block;
cursor: pointer;
user-select: none;
position: relative;
}
.toggle-switch input[type="checkbox"] {
@@ -23,23 +26,24 @@
.toggle-slider {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
background-color: #ccc;
border-radius: 24px;
width: 50px;
height: 26px;
background-color: #555;
border-radius: 26px;
transition: background-color 0.3s ease;
}
.toggle-slider::after {
content: '';
position: absolute;
width: 18px;
height: 18px;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: white;
top: 3px;
left: 3px;
transition: transform 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.toggle-switch input[type="checkbox"]:checked + .toggle-slider {
@@ -47,17 +51,33 @@
}
.toggle-switch input[type="checkbox"]:checked + .toggle-slider::after {
transform: translateX(20px);
transform: translateX(24px);
}
.toggle-switch input[type="checkbox"]:focus + .toggle-slider {
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.2);
}
.toggle-label {
font-size: 0.9rem;
.toggle-label-left,
.toggle-label-right {
font-size: 0.8rem;
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 */
+63 -12
View File
@@ -49,19 +49,61 @@ a:hover {
}
.action-bar-content {
max-width: 1200px;
max-width: 100%;
margin: 0 auto;
padding: 1rem 2rem;
padding: 0.75rem 1.5rem;
display: grid;
grid-template-columns: auto auto auto 1fr;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 2rem;
}
.language-toggle {
/* Left: Site Title */
.site-title {
display: flex;
gap: 0.5rem;
align-items: center;
gap: 0.75rem;
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 {
@@ -89,19 +131,20 @@ a:hover {
}
.export-btn {
padding: 0.4rem 1rem;
padding: 0.4rem 0.75rem;
background: transparent;
color: white;
border: 1px solid rgba(255,255,255,0.3);
border-radius: 3px;
cursor: pointer;
font-size: 0.875rem;
font-size: 0.8rem;
font-weight: 400;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none; /* For anchor tags */
gap: 0.4rem;
text-decoration: none;
white-space: nowrap;
}
.export-btn:hover {
@@ -141,12 +184,11 @@ a:hover {
font-weight: 600;
}
/* Action buttons on right */
/* Action buttons styling (already positioned by grid) */
.action-buttons {
display: flex;
gap: 0.5rem;
align-items: center;
justify-self: end;
}
/* Loading Indicator */
@@ -177,7 +219,7 @@ a:hover {
width: 100%;
max-width: 100%; /* Full width to accommodate pages */
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 */
}
@@ -320,6 +362,15 @@ a:hover {
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-text {
font-family: 'Quicksand', sans-serif;
+13 -3
View File
@@ -2,13 +2,15 @@
<div class="cv-page page-1">
<!-- Professional Title Badges - Full Width Top Bar -->
<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="title-badge">NODEJS + REACTJS {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span>
<span class="badge-separator">|</span>
<span class="title-badge">WEB {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</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="title-badge">PHP {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span>
</div>
@@ -34,6 +36,7 @@
<div class="cv-header-content">
<div class="cv-header-left">
<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 -->
<div class="intro-text">{{.CV.Summary}}</div>
</div>
@@ -124,7 +127,7 @@
<span class="badge-separator">|</span>
<span class="title-badge">WEB {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</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="title-badge">PHP {{if eq .Lang "es"}}DESARROLLADOR{{else}}DEVELOPER{{end}}</span>
</div>
@@ -241,6 +244,13 @@
<a href="mailto:{{.CV.Personal.Email}}" target="_blank" rel="noopener noreferrer">{{.CV.Personal.Email}}</a>
</div>
</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>
</footer>
</div>
+53 -48
View File
@@ -94,55 +94,45 @@
<!-- 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: Language buttons -->
<div class="language-toggle" role="group" aria-label="Language selection">
<button
class="lang-btn {{if eq .Lang "en"}}active{{end}}"
hx-get="/cv?lang=en"
hx-target="#cv-content"
hx-swap="innerHTML swap:200ms settle:200ms"
hx-push-url="/?lang=en"
hx-indicator="#loading"
aria-label="Switch to English"
aria-pressed="{{if eq .Lang "en"}}true{{else}}false{{end}}">
English
</button>
<button
class="lang-btn {{if eq .Lang "es"}}active{{end}}"
hx-get="/cv?lang=es"
hx-target="#cv-content"
hx-swap="innerHTML swap:200ms settle:200ms"
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>
<!-- 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">
<button
class="length-btn active"
onclick="toggleCVLength('short')"
aria-label="{{if eq .Lang "es"}}Ver CV corto{{else}}View short CV{{end}}">
{{if eq .Lang "es"}}Corto{{else}}Short{{end}}
</button>
<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>
<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>
<span class="toggle-label">{{if eq .Lang "es"}}Mostrar logos{{else}}Show logos{{end}}</span>
</label>
<span class="toggle-label-right">{{if eq .Lang "es"}}Logos{{else}}Logos{{end}}</span>
</div>
</div>
<!-- Right: Action buttons -->
@@ -202,21 +192,36 @@
</div>
<script>
function toggleCVLength(length) {
// Update button states
document.querySelectorAll('.length-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
function toggleLanguage() {
const checkbox = document.getElementById('langToggle');
const lang = checkbox.checked ? 'es' : 'en';
// 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');
if (length === 'short') {
paper.classList.add('cv-short');
paper.classList.remove('cv-long');
} else {
if (checkbox.checked) {
paper.classList.add('cv-long');
paper.classList.remove('cv-short');
} else {
paper.classList.add('cv-short');
paper.classList.remove('cv-long');
}
}