refactor: Extract all hardcoded content to JSON files
- Move all bilingual text from templates to UI JSON (labels, buttons, modals) - Move skills summary paragraph to CV JSON with HTML support - Add new UI sections: navigation, viewControls, sections, footer, portfolio, pdfModal, shortcutsModal, infoModal, widgets - Update Go structs to match expanded JSON structure - Add template.HTML type for CV.SkillsSummary field - Add JSON content validation test (70-json-content-validation.test.mjs) Templates now contain only structural logic (CSS classes, HTML attributes) while all user-visible text loads from JSON files for proper i18n support.
This commit is contained in:
+18
-1
@@ -2,6 +2,12 @@
|
||||
"personal": {
|
||||
"name": "Juan Andrés Moreno Rubio",
|
||||
"title": "Lead Technical Consultant, FullStack Developer",
|
||||
"titleBadges": [
|
||||
"Technical Consultant",
|
||||
"Full-Stack Engineer",
|
||||
"Authentication Specialist",
|
||||
"Solution Architect"
|
||||
],
|
||||
"location": "Arrecife, Las Palmas de Gran Canaria, Spain",
|
||||
"email": "txeo.msx@gmail.com",
|
||||
"phone": "+34 676875420",
|
||||
@@ -12,9 +18,20 @@
|
||||
"github": "https://github.com/juanatsap",
|
||||
"domestika": "https://www.domestika.org/es/txeo/portfolio",
|
||||
"website": "https://juan.andres.morenorub.io",
|
||||
"photo": "/static/images/profile.jpg"
|
||||
"photo": "/static/images/profile.jpg",
|
||||
"firstName": "Juan Andrés",
|
||||
"lastName": "Moreno Rubio",
|
||||
"username": "txeo"
|
||||
},
|
||||
"seo": {
|
||||
"pageTitle": "Curriculum Vitae",
|
||||
"metaTitle": "Professional CV",
|
||||
"metaDescription": "18 years of experience in web development, SAP CDC, React, Node.js, Go, HTMX and AI-assisted development",
|
||||
"ogDescription": "Senior Technical Consultant with 18 years of experience",
|
||||
"keywords": "CV, Resume, FullStack Developer, SAP CDC, React, Node.js, Go, HTMX, AI, Web Development, Technical Consultant"
|
||||
},
|
||||
"summary": "Full-stack developer specialized in high-availability systems. I've worked on Olympic Games platforms, airport authentication systems with millions of users, and built around 20 websites for diverse sectors (e-commerce, enterprise, institutional). Certified SAP Customer Data Cloud consultant, advising 35-40 international clients on digital identity solutions.",
|
||||
"skillsSummary": "<strong>Full-stack</strong> developer with experience in <strong>Go</strong>, <strong>Node.js</strong>, <strong>React</strong>, and <strong>HTMX</strong> for <strong>modern applications</strong>, plus Java and PHP knowledge for legacy projects. I've worked on <strong>around 20 websites</strong> and provided <strong>consulting for 35-40 international clients</strong>, from e-commerce and enterprise platforms to <strong>authentication systems</strong> managing <strong>millions of users</strong>. Familiar with <strong>AI-assisted development</strong> workflows and infrastructure management (<strong>Linux</strong>, <strong>Docker</strong>, <strong>CI/CD</strong>). I adapt well to both independent work and collaborative teams across different countries.",
|
||||
"experience": [
|
||||
{
|
||||
"position": "Senior SAP Technical Consultant",
|
||||
|
||||
+18
-1
@@ -2,6 +2,12 @@
|
||||
"personal": {
|
||||
"name": "Juan Andrés Moreno Rubio",
|
||||
"title": "Consultor Técnico Senior, Desarrollador FullStack",
|
||||
"titleBadges": [
|
||||
"Consultor Técnico",
|
||||
"Ingeniero Full-Stack",
|
||||
"Especialista en Autenticación",
|
||||
"Arquitecto de Soluciones"
|
||||
],
|
||||
"location": "Arrecife, Las Palmas de Gran Canaria, España",
|
||||
"email": "txeo.msx@gmail.com",
|
||||
"phone": "+34 676875420",
|
||||
@@ -12,9 +18,20 @@
|
||||
"github": "https://github.com/juanatsap",
|
||||
"domestika": "https://www.domestika.org/es/txeo/portfolio",
|
||||
"website": "https://juan.andres.morenorub.io",
|
||||
"photo": "/static/images/profile.jpg"
|
||||
"photo": "/static/images/profile.jpg",
|
||||
"firstName": "Juan Andrés",
|
||||
"lastName": "Moreno Rubio",
|
||||
"username": "txeo"
|
||||
},
|
||||
"seo": {
|
||||
"pageTitle": "Curriculum Vitae",
|
||||
"metaTitle": "CV Profesional",
|
||||
"metaDescription": "18 años de experiencia en desarrollo web, SAP CDC, React, Node.js, Go, HTMX y desarrollo asistido por IA",
|
||||
"ogDescription": "Consultor Técnico Senior con 18 años de experiencia",
|
||||
"keywords": "CV, Curriculum Vitae, Desarrollador FullStack, SAP CDC, React, Node.js, Go, HTMX, IA, Desarrollo Web, Consultor Técnico"
|
||||
},
|
||||
"summary": "Desarrollador full-stack especializado en sistemas de alta disponibilidad. He participado en plataformas de Juegos Olímpicos, sistemas de autenticación aeroportuaria con millones de usuarios, y desarrollado unos 20 sitios web para diversos sectores (e-commerce, empresariales, institucionales). Consultor certificado de SAP Customer Data Cloud, asesorando a 35-40 clientes internacionales en soluciones de identidad digital.",
|
||||
"skillsSummary": "Desarrollador <strong>full-stack</strong> con experiencia en <strong>Go</strong>, <strong>Node.js</strong>, <strong>React</strong> y <strong>HTMX</strong> para <strong>aplicaciones modernas</strong>, además de conocimientos en Java y PHP para proyectos legacy. He trabajado en <strong>unos 20 sitios web</strong> y realizado <strong>consultoría para 35-40 clientes internacionales</strong>, desde e-commerce y plataformas empresariales hasta <strong>sistemas de autenticación</strong> que gestionan <strong>millones de usuarios</strong>. Familiarizado con flujos de trabajo asistidos por <strong>IA</strong> y gestión de infraestructura (<strong>Linux</strong>, <strong>Docker</strong>, <strong>CI/CD</strong>). Me adapto bien tanto al trabajo independiente como colaborativo en equipos internacionales.",
|
||||
"experience": [
|
||||
{
|
||||
"position": "Consultor Técnico Senior SAP",
|
||||
|
||||
+123
-10
@@ -1,19 +1,78 @@
|
||||
{
|
||||
"infoModal": {
|
||||
"title": "About this CV",
|
||||
"description": "This interactive CV was built by myself with <strong>Go + HTMX</strong>, showcasing modern hypermedia architecture without heavy JavaScript frameworks.",
|
||||
"techStack": {
|
||||
"goHono": "Go + Hono",
|
||||
"htmx": "HTMX",
|
||||
"html5": "Semantic HTML5",
|
||||
"css3": "Pure CSS3"
|
||||
"navigation": {
|
||||
"cvSections": "CV Sections",
|
||||
"training": "Training",
|
||||
"skills": "Skills",
|
||||
"experience": "Experience",
|
||||
"awards": "Awards",
|
||||
"projects": "Personal / Freelance Projects",
|
||||
"courses": "Courses",
|
||||
"languages": "Languages",
|
||||
"references": "References",
|
||||
"other": "Other",
|
||||
"quickActions": "Quick Actions",
|
||||
"collapseAll": "Collapse All",
|
||||
"expandAll": "Expand All",
|
||||
"zoom": "Zoom",
|
||||
"viewControls": "View Controls",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"viewControls": {
|
||||
"length": "Length",
|
||||
"icons": "Icons",
|
||||
"view": "View"
|
||||
},
|
||||
"sections": {
|
||||
"technicalSkills": "Technical Skills",
|
||||
"moreSkills": "More Skills",
|
||||
"yearsOfExperience": "years of experience",
|
||||
"drivingLicense": "Driving License type",
|
||||
"obtainedFrom": "obtained from the",
|
||||
"currentBadge": "CURRENT",
|
||||
"expiredBadge": "EXPIRED",
|
||||
"present": "now",
|
||||
"technologies": "Technologies:",
|
||||
"maintainedBy": "MAINTAINED BY"
|
||||
},
|
||||
"footer": {
|
||||
"viewOnGithub": "View this project on GitHub",
|
||||
"lastUpdated": "Last updated"
|
||||
},
|
||||
"portfolio": {
|
||||
"seeAllProjects": "See all projects on my",
|
||||
"domestikaPortfolio": "Domestika portfolio"
|
||||
},
|
||||
"pdfModal": {
|
||||
"title": "Download PDF",
|
||||
"subtitle": "Choose your preferred format",
|
||||
"preparingPdf": "Preparing PDF...",
|
||||
"pleaseWait": "Please wait while we generate your CV",
|
||||
"close": "Close",
|
||||
"downloadButton": "Download PDF",
|
||||
"shortCv": {
|
||||
"title": "Short CV (4 pages)",
|
||||
"pages": "4 Pages",
|
||||
"description": "Essential info",
|
||||
"ariaLabel": "Short CV - 4 pages, essential information"
|
||||
},
|
||||
"viewSource": "View Project in Github",
|
||||
"viewSourceSubtext": "Want to know how it's built?"
|
||||
"defaultCv": {
|
||||
"title": "Default CV (5 pages)",
|
||||
"pages": "5 Pages",
|
||||
"description": "Short with skills - Recommended",
|
||||
"ariaLabel": "Default CV - 5 pages with skills (Recommended)"
|
||||
},
|
||||
"extendedCv": {
|
||||
"title": "Extended CV (9 pages)",
|
||||
"pages": "9 Pages",
|
||||
"description": "All details",
|
||||
"ariaLabel": "Extended CV - 9 pages, full version"
|
||||
}
|
||||
},
|
||||
"shortcutsModal": {
|
||||
"title": "Keyboard Shortcuts",
|
||||
"subtitle": "Learn the Shortcuts",
|
||||
"description": "Use these keyboard shortcuts to navigate and control the CV more efficiently.",
|
||||
"close": "Close",
|
||||
"sections": {
|
||||
"zoom": {
|
||||
"title": "Zoom Control",
|
||||
@@ -87,5 +146,59 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"infoModal": {
|
||||
"title": "About this CV",
|
||||
"description": "This interactive CV was built by myself with <strong>Go + HTMX</strong>, showcasing modern hypermedia architecture without heavy JavaScript frameworks.",
|
||||
"techStack": {
|
||||
"goHono": "Go + Hono",
|
||||
"htmx": "HTMX",
|
||||
"html5": "Semantic HTML5",
|
||||
"css3": "Pure CSS3"
|
||||
},
|
||||
"viewSource": "View Project in Github",
|
||||
"viewSourceSubtext": "Want to know how it's built?"
|
||||
},
|
||||
"widgets": {
|
||||
"backToTop": {
|
||||
"ariaLabel": "Back to top",
|
||||
"tooltip": "Back to top"
|
||||
},
|
||||
"info": {
|
||||
"ariaLabel": "Information",
|
||||
"tooltip": "Information"
|
||||
},
|
||||
"download": {
|
||||
"ariaLabel": "Download as PDF",
|
||||
"tooltip": "Download as PDF"
|
||||
},
|
||||
"print": {
|
||||
"ariaLabel": "Print Friendly",
|
||||
"tooltip": "Print Friendly"
|
||||
},
|
||||
"shortcuts": {
|
||||
"ariaLabel": "Keyboard shortcuts",
|
||||
"tooltip": "Keyboard shortcuts (?)"
|
||||
},
|
||||
"zoomToggle": {
|
||||
"ariaLabel": "Toggle zoom control",
|
||||
"tooltip": "Zoom control"
|
||||
},
|
||||
"zoomControl": {
|
||||
"groupLabel": "Zoom control",
|
||||
"closeLabel": "Close zoom control",
|
||||
"closeTitle": "Close",
|
||||
"sliderLabel": "Adjust CV zoom level",
|
||||
"resetLabel": "Reset zoom to 100%",
|
||||
"resetTitle": "Reset"
|
||||
},
|
||||
"pdfToast": {
|
||||
"title": "Preparing PDF",
|
||||
"closeLabel": "Close notification"
|
||||
},
|
||||
"actionButtons": {
|
||||
"downloadPdf": "Download as PDF",
|
||||
"printFriendly": "Print Friendly"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+123
-10
@@ -1,19 +1,78 @@
|
||||
{
|
||||
"infoModal": {
|
||||
"title": "Acerca de este CV",
|
||||
"description": "Este CV interactivo fue construido por mí mismo con <strong>Go + HTMX</strong>, demostrando arquitectura moderna de hipermedia sin frameworks pesados de JavaScript.",
|
||||
"techStack": {
|
||||
"goHono": "Go + Hono",
|
||||
"htmx": "HTMX",
|
||||
"html5": "HTML5 Semántico",
|
||||
"css3": "CSS3 Puro"
|
||||
"navigation": {
|
||||
"cvSections": "Secciones CV",
|
||||
"training": "Formación",
|
||||
"skills": "Competencias",
|
||||
"experience": "Experiencia",
|
||||
"awards": "Premios y Reconocimientos",
|
||||
"projects": "Proyectos Personales / Freelance",
|
||||
"courses": "Cursos Realizados",
|
||||
"languages": "Idiomas",
|
||||
"references": "Referencias",
|
||||
"other": "Otros",
|
||||
"quickActions": "Acciones Rápidas",
|
||||
"collapseAll": "Colapsar Todo",
|
||||
"expandAll": "Expandir Todo",
|
||||
"zoom": "Zoom",
|
||||
"viewControls": "Controles de Vista",
|
||||
"actions": "Acciones"
|
||||
},
|
||||
"viewControls": {
|
||||
"length": "Longitud",
|
||||
"icons": "Iconos",
|
||||
"view": "Vista"
|
||||
},
|
||||
"sections": {
|
||||
"technicalSkills": "Competencias Técnicas",
|
||||
"moreSkills": "Más Competencias",
|
||||
"yearsOfExperience": "años de experiencia",
|
||||
"drivingLicense": "Carnet de conducir tipo",
|
||||
"obtainedFrom": "obtenido de",
|
||||
"currentBadge": "ACTUAL",
|
||||
"expiredBadge": "EXPIRADO",
|
||||
"present": "presente",
|
||||
"technologies": "Tecnologías:",
|
||||
"maintainedBy": "MANTENIDO POR"
|
||||
},
|
||||
"footer": {
|
||||
"viewOnGithub": "Ver este proyecto en GitHub",
|
||||
"lastUpdated": "Última actualización"
|
||||
},
|
||||
"portfolio": {
|
||||
"seeAllProjects": "Ver todos los proyectos en mi",
|
||||
"domestikaPortfolio": "portfolio de Domestika"
|
||||
},
|
||||
"pdfModal": {
|
||||
"title": "Descargar PDF",
|
||||
"subtitle": "Elige tu formato preferido",
|
||||
"preparingPdf": "Preparando PDF...",
|
||||
"pleaseWait": "Por favor espera mientras generamos tu CV",
|
||||
"close": "Cerrar",
|
||||
"downloadButton": "Descargar PDF",
|
||||
"shortCv": {
|
||||
"title": "CV Corto (4 páginas)",
|
||||
"pages": "4 Páginas",
|
||||
"description": "Información esencial",
|
||||
"ariaLabel": "CV Corto - 4 páginas, información esencial"
|
||||
},
|
||||
"viewSource": "Ver proyecto en Github",
|
||||
"viewSourceSubtext": "¿Quieres saber cómo está hecho?"
|
||||
"defaultCv": {
|
||||
"title": "CV Por Defecto (5 páginas)",
|
||||
"pages": "5 Páginas",
|
||||
"description": "Corto con habilidades - Recomendado",
|
||||
"ariaLabel": "CV Por Defecto - 5 páginas con habilidades (Recomendado)"
|
||||
},
|
||||
"extendedCv": {
|
||||
"title": "CV Extendido (9 páginas)",
|
||||
"pages": "9 Páginas",
|
||||
"description": "Todos los detalles",
|
||||
"ariaLabel": "CV Extendido - 9 páginas, versión completa"
|
||||
}
|
||||
},
|
||||
"shortcutsModal": {
|
||||
"title": "Atajos de Teclado",
|
||||
"subtitle": "Aprende los Atajos",
|
||||
"description": "Usa estos atajos de teclado para navegar y controlar el CV de forma más eficiente.",
|
||||
"close": "Cerrar",
|
||||
"sections": {
|
||||
"zoom": {
|
||||
"title": "Control de Zoom",
|
||||
@@ -87,5 +146,59 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"infoModal": {
|
||||
"title": "Acerca de este CV",
|
||||
"description": "Este CV interactivo fue construido por mí mismo con <strong>Go + HTMX</strong>, demostrando arquitectura moderna de hipermedia sin frameworks pesados de JavaScript.",
|
||||
"techStack": {
|
||||
"goHono": "Go + Hono",
|
||||
"htmx": "HTMX",
|
||||
"html5": "HTML5 Semántico",
|
||||
"css3": "CSS3 Puro"
|
||||
},
|
||||
"viewSource": "Ver proyecto en Github",
|
||||
"viewSourceSubtext": "¿Quieres saber cómo está hecho?"
|
||||
},
|
||||
"widgets": {
|
||||
"backToTop": {
|
||||
"ariaLabel": "Volver arriba",
|
||||
"tooltip": "Volver arriba"
|
||||
},
|
||||
"info": {
|
||||
"ariaLabel": "Información",
|
||||
"tooltip": "Información"
|
||||
},
|
||||
"download": {
|
||||
"ariaLabel": "Descargar PDF",
|
||||
"tooltip": "Descargar PDF"
|
||||
},
|
||||
"print": {
|
||||
"ariaLabel": "Imprimir CV",
|
||||
"tooltip": "Imprimir CV"
|
||||
},
|
||||
"shortcuts": {
|
||||
"ariaLabel": "Atajos de teclado",
|
||||
"tooltip": "Atajos de teclado (?)"
|
||||
},
|
||||
"zoomToggle": {
|
||||
"ariaLabel": "Alternar control de zoom",
|
||||
"tooltip": "Control de zoom"
|
||||
},
|
||||
"zoomControl": {
|
||||
"groupLabel": "Control de zoom",
|
||||
"closeLabel": "Cerrar control de zoom",
|
||||
"closeTitle": "Cerrar",
|
||||
"sliderLabel": "Ajustar nivel de zoom del CV",
|
||||
"resetLabel": "Restablecer zoom al 100%",
|
||||
"resetTitle": "Restablecer"
|
||||
},
|
||||
"pdfToast": {
|
||||
"title": "Preparando PDF",
|
||||
"closeLabel": "Cerrar notificación"
|
||||
},
|
||||
"actionButtons": {
|
||||
"downloadPdf": "Descargar como PDF",
|
||||
"printFriendly": "Imprimir amigable"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-13
@@ -1,9 +1,13 @@
|
||||
package cv
|
||||
|
||||
import "html/template"
|
||||
|
||||
// CV represents the complete curriculum vitae structure
|
||||
type CV struct {
|
||||
Personal Personal `json:"personal"`
|
||||
SEO SEO `json:"seo"`
|
||||
Summary string `json:"summary"`
|
||||
SkillsSummary template.HTML `json:"skillsSummary"`
|
||||
Experience []Experience `json:"experience"`
|
||||
Education []Education `json:"education"`
|
||||
Skills Skills `json:"skills"`
|
||||
@@ -18,19 +22,31 @@ type CV struct {
|
||||
}
|
||||
|
||||
type Personal struct {
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
Location string `json:"location"`
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
DateOfBirth string `json:"dateOfBirth"`
|
||||
PlaceOfBirth string `json:"placeOfBirth"`
|
||||
Citizenship string `json:"citizenship"`
|
||||
LinkedIn string `json:"linkedin"`
|
||||
GitHub string `json:"github"`
|
||||
Domestika string `json:"domestika"`
|
||||
Website string `json:"website"`
|
||||
Photo string `json:"photo"`
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
TitleBadges []string `json:"titleBadges"`
|
||||
Location string `json:"location"`
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
DateOfBirth string `json:"dateOfBirth"`
|
||||
PlaceOfBirth string `json:"placeOfBirth"`
|
||||
Citizenship string `json:"citizenship"`
|
||||
LinkedIn string `json:"linkedin"`
|
||||
GitHub string `json:"github"`
|
||||
Domestika string `json:"domestika"`
|
||||
Website string `json:"website"`
|
||||
Photo string `json:"photo"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type SEO struct {
|
||||
PageTitle string `json:"pageTitle"`
|
||||
MetaTitle string `json:"metaTitle"`
|
||||
MetaDescription string `json:"metaDescription"`
|
||||
OgDescription string `json:"ogDescription"`
|
||||
Keywords string `json:"keywords"`
|
||||
}
|
||||
|
||||
type Experience struct {
|
||||
|
||||
+131
-12
@@ -4,28 +4,95 @@ import "html/template"
|
||||
|
||||
// UI represents user interface translations and configuration
|
||||
type UI struct {
|
||||
InfoModal InfoModal `json:"infoModal"`
|
||||
Navigation Navigation `json:"navigation"`
|
||||
ViewControls ViewControls `json:"viewControls"`
|
||||
Sections Sections `json:"sections"`
|
||||
Footer Footer `json:"footer"`
|
||||
Portfolio Portfolio `json:"portfolio"`
|
||||
PdfModal PdfModal `json:"pdfModal"`
|
||||
ShortcutsModal ShortcutsModal `json:"shortcutsModal"`
|
||||
InfoModal InfoModal `json:"infoModal"`
|
||||
Widgets Widgets `json:"widgets"`
|
||||
}
|
||||
|
||||
type InfoModal struct {
|
||||
Title string `json:"title"`
|
||||
Description template.HTML `json:"description"`
|
||||
TechStack TechStack `json:"techStack"`
|
||||
ViewSource string `json:"viewSource"`
|
||||
ViewSourceSubtext string `json:"viewSourceSubtext"`
|
||||
// Navigation labels for hamburger menu
|
||||
type Navigation struct {
|
||||
CvSections string `json:"cvSections"`
|
||||
Training string `json:"training"`
|
||||
Skills string `json:"skills"`
|
||||
Experience string `json:"experience"`
|
||||
Awards string `json:"awards"`
|
||||
Projects string `json:"projects"`
|
||||
Courses string `json:"courses"`
|
||||
Languages string `json:"languages"`
|
||||
References string `json:"references"`
|
||||
Other string `json:"other"`
|
||||
QuickActions string `json:"quickActions"`
|
||||
CollapseAll string `json:"collapseAll"`
|
||||
ExpandAll string `json:"expandAll"`
|
||||
Zoom string `json:"zoom"`
|
||||
ViewControls string `json:"viewControls"`
|
||||
Actions string `json:"actions"`
|
||||
}
|
||||
|
||||
type TechStack struct {
|
||||
GoHono string `json:"goHono"`
|
||||
HTMX string `json:"htmx"`
|
||||
HTML5 string `json:"html5"`
|
||||
CSS3 string `json:"css3"`
|
||||
// ViewControls labels for toggle buttons
|
||||
type ViewControls struct {
|
||||
Length string `json:"length"`
|
||||
Icons string `json:"icons"`
|
||||
View string `json:"view"`
|
||||
}
|
||||
|
||||
// Sections labels for CV section headers
|
||||
type Sections struct {
|
||||
TechnicalSkills string `json:"technicalSkills"`
|
||||
MoreSkills string `json:"moreSkills"`
|
||||
YearsOfExperience string `json:"yearsOfExperience"`
|
||||
DrivingLicense string `json:"drivingLicense"`
|
||||
ObtainedFrom string `json:"obtainedFrom"`
|
||||
CurrentBadge string `json:"currentBadge"`
|
||||
ExpiredBadge string `json:"expiredBadge"`
|
||||
Present string `json:"present"`
|
||||
Technologies string `json:"technologies"`
|
||||
MaintainedBy string `json:"maintainedBy"`
|
||||
}
|
||||
|
||||
// Footer labels
|
||||
type Footer struct {
|
||||
ViewOnGithub string `json:"viewOnGithub"`
|
||||
LastUpdated string `json:"lastUpdated"`
|
||||
}
|
||||
|
||||
// Portfolio labels
|
||||
type Portfolio struct {
|
||||
SeeAllProjects string `json:"seeAllProjects"`
|
||||
DomestikaPortfolio string `json:"domestikaPortfolio"`
|
||||
}
|
||||
|
||||
// PdfModal labels
|
||||
type PdfModal struct {
|
||||
Title string `json:"title"`
|
||||
Subtitle string `json:"subtitle"`
|
||||
PreparingPdf string `json:"preparingPdf"`
|
||||
PleaseWait string `json:"pleaseWait"`
|
||||
Close string `json:"close"`
|
||||
DownloadButton string `json:"downloadButton"`
|
||||
ShortCv PdfCvOption `json:"shortCv"`
|
||||
DefaultCv PdfCvOption `json:"defaultCv"`
|
||||
ExtendedCv PdfCvOption `json:"extendedCv"`
|
||||
}
|
||||
|
||||
type PdfCvOption struct {
|
||||
Title string `json:"title"`
|
||||
Pages string `json:"pages"`
|
||||
Description string `json:"description"`
|
||||
AriaLabel string `json:"ariaLabel"`
|
||||
}
|
||||
|
||||
type ShortcutsModal struct {
|
||||
Title string `json:"title"`
|
||||
Subtitle string `json:"subtitle"`
|
||||
Description string `json:"description"`
|
||||
Close string `json:"close"`
|
||||
Sections ShortcutsSections `json:"sections"`
|
||||
}
|
||||
|
||||
@@ -59,3 +126,55 @@ type ShortcutItem struct {
|
||||
Key string `json:"key"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type InfoModal struct {
|
||||
Title string `json:"title"`
|
||||
Description template.HTML `json:"description"`
|
||||
TechStack TechStack `json:"techStack"`
|
||||
ViewSource string `json:"viewSource"`
|
||||
ViewSourceSubtext string `json:"viewSourceSubtext"`
|
||||
}
|
||||
|
||||
type TechStack struct {
|
||||
GoHono string `json:"goHono"`
|
||||
HTMX string `json:"htmx"`
|
||||
HTML5 string `json:"html5"`
|
||||
CSS3 string `json:"css3"`
|
||||
}
|
||||
|
||||
// Widget label types
|
||||
type Widgets struct {
|
||||
BackToTop WidgetLabel `json:"backToTop"`
|
||||
Info WidgetLabel `json:"info"`
|
||||
Download WidgetLabel `json:"download"`
|
||||
Print WidgetLabel `json:"print"`
|
||||
Shortcuts WidgetLabel `json:"shortcuts"`
|
||||
ZoomToggle WidgetLabel `json:"zoomToggle"`
|
||||
ZoomControl ZoomControlLabel `json:"zoomControl"`
|
||||
PdfToast PdfToastLabel `json:"pdfToast"`
|
||||
ActionButtons ActionButtonsLabel `json:"actionButtons"`
|
||||
}
|
||||
|
||||
type WidgetLabel struct {
|
||||
AriaLabel string `json:"ariaLabel"`
|
||||
Tooltip string `json:"tooltip"`
|
||||
}
|
||||
|
||||
type ZoomControlLabel struct {
|
||||
GroupLabel string `json:"groupLabel"`
|
||||
CloseLabel string `json:"closeLabel"`
|
||||
CloseTitle string `json:"closeTitle"`
|
||||
SliderLabel string `json:"sliderLabel"`
|
||||
ResetLabel string `json:"resetLabel"`
|
||||
ResetTitle string `json:"resetTitle"`
|
||||
}
|
||||
|
||||
type PdfToastLabel struct {
|
||||
Title string `json:"title"`
|
||||
CloseLabel string `json:"closeLabel"`
|
||||
}
|
||||
|
||||
type ActionButtonsLabel struct {
|
||||
DownloadPdf string `json:"downloadPdf"`
|
||||
PrintFriendly string `json:"printFriendly"`
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<details class="sidebar-accordion">
|
||||
<summary class="sidebar-accordion-header">
|
||||
<iconify-icon icon="mdi:brain" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Competencias Técnicas{{else}}Technical Skills{{end}}</span>
|
||||
<span>{{.UI.Sections.TechnicalSkills}}</span>
|
||||
<iconify-icon icon="mdi:chevron-down" width="20" height="20" class="chevron"></iconify-icon>
|
||||
</summary>
|
||||
<div class="sidebar-accordion-content">
|
||||
@@ -96,7 +96,7 @@
|
||||
<details class="sidebar-accordion">
|
||||
<summary class="sidebar-accordion-header">
|
||||
<iconify-icon icon="mdi:brain" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Más Competencias{{else}}More Skills{{end}}</span>
|
||||
<span>{{.UI.Sections.MoreSkills}}</span>
|
||||
<iconify-icon icon="mdi:chevron-down" width="20" height="20" class="chevron"></iconify-icon>
|
||||
</summary>
|
||||
<div class="sidebar-accordion-content">
|
||||
|
||||
+10
-10
@@ -5,10 +5,10 @@
|
||||
<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}}">
|
||||
<title>{{.CV.Personal.Name}} - {{.CV.SEO.PageTitle}}</title>
|
||||
<meta name="title" content="{{.CV.Personal.Name}} - {{.CV.SEO.MetaTitle}}">
|
||||
<meta name="description" content="{{.CV.Personal.Title}} | {{.CV.SEO.MetaDescription}}">
|
||||
<meta name="keywords" content="{{.CV.Personal.Name}}, {{.CV.SEO.Keywords}}">
|
||||
<meta name="author" content="{{.CV.Personal.Name}}">
|
||||
<meta name="robots" content="index, follow">
|
||||
<link rel="canonical" href="{{.CanonicalURL}}">
|
||||
@@ -21,18 +21,18 @@
|
||||
<!-- 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:title" content="{{.CV.Personal.Name}} - {{.CV.SEO.MetaTitle}}">
|
||||
<meta property="og:description" content="{{.CV.Personal.Title}} | {{.CV.SEO.OgDescription}}">
|
||||
<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">
|
||||
<meta property="profile:first_name" content="{{.CV.Personal.FirstName}}">
|
||||
<meta property="profile:last_name" content="{{.CV.Personal.LastName}}">
|
||||
<meta property="profile:username" content="{{.CV.Personal.Username}}">
|
||||
|
||||
<!-- 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:title" content="{{.CV.Personal.Name}} - {{.CV.SEO.MetaTitle}}">
|
||||
<meta name="twitter:description" content="{{.CV.Personal.Title}}">
|
||||
<meta name="twitter:image" content="{{.CV.Personal.Website}}/static/images/profile.jpg">
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<details class="sidebar-accordion">
|
||||
<summary class="sidebar-accordion-header">
|
||||
<iconify-icon icon="mdi:brain" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Competencias Técnicas{{else}}Technical Skills{{end}}</span>
|
||||
<span>{{.UI.Sections.TechnicalSkills}}</span>
|
||||
<iconify-icon icon="mdi:chevron-down" width="20" height="20" class="chevron"></iconify-icon>
|
||||
</summary>
|
||||
<div class="sidebar-accordion-content">
|
||||
@@ -89,7 +89,7 @@
|
||||
<details class="sidebar-accordion">
|
||||
<summary class="sidebar-accordion-header">
|
||||
<iconify-icon icon="mdi:brain" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Más Competencias{{else}}More Skills{{end}}</span>
|
||||
<span>{{.UI.Sections.MoreSkills}}</span>
|
||||
<iconify-icon icon="mdi:chevron-down" width="20" height="20" class="chevron"></iconify-icon>
|
||||
</summary>
|
||||
<div class="sidebar-accordion-content">
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
<p style="text-align: center; margin-bottom: 0.5rem;">
|
||||
<a href="https://github.com/juanatsap/cv-site" target="_blank" rel="noopener noreferrer" class="github-repo-link" style="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}}
|
||||
{{.UI.Footer.ViewOnGithub}}
|
||||
</a>
|
||||
</p>
|
||||
<p>© {{.CV.Meta.LastUpdated}} {{.CV.Personal.Name}} |
|
||||
{{if eq .Lang "es"}}Última actualización{{else}}Last updated{{end}}: {{.CV.Meta.LastUpdated}}</p>
|
||||
{{.UI.Footer.LastUpdated}}: {{.CV.Meta.LastUpdated}}</p>
|
||||
</footer>
|
||||
{{end}}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
{{define "title-badges"}}
|
||||
<!-- Professional Title Badges - Full Width Top Bar -->
|
||||
<div class="cv-title-badges-header">
|
||||
<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">{{if eq .Lang "es"}}INGENIERO FULL-STACK{{else}}FULL-STACK ENGINEER{{end}}</span>
|
||||
<span class="badge-separator">|</span>
|
||||
<span class="title-badge">{{if eq .Lang "es"}}ESPECIALISTA EN AUTENTICACIÓN{{else}}AUTHENTICATION SPECIALIST{{end}}</span>
|
||||
<span class="badge-separator">|</span>
|
||||
<span class="title-badge">{{if eq .Lang "es"}}ARQUITECTO DE SOLUCIONES{{else}}SOLUTION ARCHITECT{{end}}</span>
|
||||
{{range $i, $badge := .CV.Personal.TitleBadges}}{{if $i}}<span class="badge-separator">|</span>{{end}}<span class="title-badge">{{$badge}}</span>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<dialog id="info-modal" class="info-modal no-print"
|
||||
_="on click call closeOnBackdrop(me, event)">
|
||||
<div class="info-modal-content">
|
||||
<button class="info-modal-close" onclick="document.getElementById('info-modal').close()" aria-label="Close">
|
||||
<button class="info-modal-close" onclick="document.getElementById('info-modal').close()" aria-label="{{.UI.PdfModal.Close}}">
|
||||
<iconify-icon icon="mdi:close" width="24" height="24"></iconify-icon>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<div class="pdf-loading-content">
|
||||
<div class="pdf-loading-spinner"></div>
|
||||
<h3 class="pdf-loading-title" id="pdf-loading-title">
|
||||
{{if eq .Lang "es"}}Preparando PDF...{{else}}Preparing PDF...{{end}}
|
||||
{{.UI.PdfModal.PreparingPdf}}
|
||||
</h3>
|
||||
<p class="pdf-loading-message" id="pdf-loading-message">
|
||||
{{if eq .Lang "es"}}Por favor espera mientras generamos tu CV{{else}}Please wait while we generate your CV{{end}}
|
||||
{{.UI.PdfModal.PleaseWait}}
|
||||
</p>
|
||||
<p class="pdf-loading-estimate" id="pdf-loading-estimate"></p>
|
||||
</div>
|
||||
@@ -20,16 +20,16 @@
|
||||
<!-- Close Button -->
|
||||
<button class="info-modal-close"
|
||||
onclick="document.getElementById('pdf-modal').close()"
|
||||
aria-label="{{if eq .Lang "es"}}Cerrar{{else}}Close{{end}}">
|
||||
aria-label="{{.UI.PdfModal.Close}}">
|
||||
<iconify-icon icon="mdi:close" width="24" height="24"></iconify-icon>
|
||||
</button>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="info-modal-header">
|
||||
<iconify-icon icon="catppuccin:pdf" width="40" height="40" style="margin-bottom: 0.5rem;"></iconify-icon>
|
||||
<h2>{{if eq .Lang "es"}}Descargar PDF{{else}}Download PDF{{end}}</h2>
|
||||
<h2>{{.UI.PdfModal.Title}}</h2>
|
||||
<p class="pdf-modal-subtitle">
|
||||
{{if eq .Lang "es"}}Elige tu formato preferido{{else}}Choose your preferred format{{end}}
|
||||
{{.UI.PdfModal.Subtitle}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
data-cv-format="short"
|
||||
role="radio"
|
||||
aria-checked="false"
|
||||
aria-label="{{if eq .Lang "es"}}CV Corto - 4 páginas, información esencial{{else}}Short CV - 4 pages, essential information{{end}}"
|
||||
aria-label="{{.UI.PdfModal.ShortCv.AriaLabel}}"
|
||||
tabindex="0"
|
||||
_="on click call selectPdfCard(me)
|
||||
on keydown call handlePdfCardKey(me, event)">
|
||||
@@ -57,13 +57,13 @@
|
||||
|
||||
<!-- Page count badge -->
|
||||
<div class="thumbnail-badge">
|
||||
{{if eq .Lang "es"}}4 Páginas{{else}}4 Pages{{end}}
|
||||
{{.UI.PdfModal.ShortCv.Pages}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pdf-option-info">
|
||||
<h3>{{if eq .Lang "es"}}CV Corto (4 páginas){{else}}Short CV (4 pages){{end}}</h3>
|
||||
<p>{{if eq .Lang "es"}}Información esencial{{else}}Essential info{{end}}</p>
|
||||
<h3>{{.UI.PdfModal.ShortCv.Title}}</h3>
|
||||
<p>{{.UI.PdfModal.ShortCv.Description}}</p>
|
||||
</div>
|
||||
|
||||
<div class="pdf-option-badge">
|
||||
@@ -76,7 +76,7 @@
|
||||
data-cv-format="default"
|
||||
role="radio"
|
||||
aria-checked="true"
|
||||
aria-label="{{if eq .Lang "es"}}CV Por Defecto - 5 páginas con habilidades (Recomendado){{else}}Default CV - 5 pages with skills (Recommended){{end}}"
|
||||
aria-label="{{.UI.PdfModal.DefaultCv.AriaLabel}}"
|
||||
tabindex="0"
|
||||
_="on click call selectPdfCard(me)
|
||||
on keydown call handlePdfCardKey(me, event)">
|
||||
@@ -97,16 +97,16 @@
|
||||
|
||||
<!-- Page count badge with star -->
|
||||
<div class="thumbnail-badge" style="font-weight: 600;">
|
||||
⭐ {{if eq .Lang "es"}}5 Páginas{{else}}5 Pages{{end}}
|
||||
⭐ {{.UI.PdfModal.DefaultCv.Pages}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pdf-option-info">
|
||||
<h3>
|
||||
{{if eq .Lang "es"}}CV Por Defecto (5 páginas){{else}}Default CV (5 pages){{end}}
|
||||
{{.UI.PdfModal.DefaultCv.Title}}
|
||||
<span style="color: #667eea; font-size: 0.9em;">⭐</span>
|
||||
</h3>
|
||||
<p style="font-weight: 500;">{{if eq .Lang "es"}}Corto con habilidades - Recomendado{{else}}Short with skills - Recommended{{end}}</p>
|
||||
<p style="font-weight: 500;">{{.UI.PdfModal.DefaultCv.Description}}</p>
|
||||
</div>
|
||||
|
||||
<div class="pdf-option-badge">
|
||||
@@ -119,7 +119,7 @@
|
||||
data-cv-format="long"
|
||||
role="radio"
|
||||
aria-checked="false"
|
||||
aria-label="{{if eq .Lang "es"}}CV Extendido - 9 páginas, versión completa{{else}}Extended CV - 9 pages, full version{{end}}"
|
||||
aria-label="{{.UI.PdfModal.ExtendedCv.AriaLabel}}"
|
||||
tabindex="0"
|
||||
_="on click call selectPdfCard(me)
|
||||
on keydown call handlePdfCardKey(me, event)">
|
||||
@@ -137,13 +137,13 @@
|
||||
|
||||
<!-- Page count badge -->
|
||||
<div class="thumbnail-badge">
|
||||
{{if eq .Lang "es"}}9 Páginas{{else}}9 Pages{{end}}
|
||||
{{.UI.PdfModal.ExtendedCv.Pages}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pdf-option-info">
|
||||
<h3>{{if eq .Lang "es"}}CV Extendido (9 páginas){{else}}Extended CV (9 pages){{end}}</h3>
|
||||
<p>{{if eq .Lang "es"}}Todos los detalles{{else}}All details{{end}}</p>
|
||||
<h3>{{.UI.PdfModal.ExtendedCv.Title}}</h3>
|
||||
<p>{{.UI.PdfModal.ExtendedCv.Description}}</p>
|
||||
</div>
|
||||
|
||||
<div class="pdf-option-badge">
|
||||
@@ -158,7 +158,7 @@
|
||||
id="pdf-download-btn"
|
||||
onclick="downloadPDF()">
|
||||
<iconify-icon icon="mdi:download" width="20" height="20"></iconify-icon>
|
||||
{{if eq .Lang "es"}}Descargar PDF{{else}}Download PDF{{end}}
|
||||
{{.UI.PdfModal.DownloadButton}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<dialog id="shortcuts-modal" class="info-modal no-print"
|
||||
_="on click call closeOnBackdrop(me, event)">
|
||||
<div class="info-modal-content">
|
||||
<button class="info-modal-close" onclick="document.getElementById('shortcuts-modal').close()" aria-label="{{if eq .Lang "es"}}Cerrar{{else}}Close{{end}}">
|
||||
<button class="info-modal-close" onclick="document.getElementById('shortcuts-modal').close()" aria-label="{{.UI.ShortcutsModal.Close}}">
|
||||
<iconify-icon icon="mdi:close" width="24" height="24"></iconify-icon>
|
||||
</button>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<span class="keyboard-icon-wrapper">
|
||||
<iconify-icon icon="mdi:keyboard-outline" width="32" height="32"></iconify-icon>
|
||||
</span>
|
||||
{{if eq .Lang "es"}}Aprende los Atajos{{else}}Learn the Shortcuts{{end}}
|
||||
{{.UI.ShortcutsModal.Subtitle}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,22 +5,22 @@
|
||||
id="action-bar-pdf-btn"
|
||||
class="action-btn pdf-btn has-tooltip"
|
||||
onclick="document.getElementById('pdf-modal').showModal()"
|
||||
aria-label="{{if eq .Lang "es"}}Descargar como PDF{{else}}Download as PDF{{end}}"
|
||||
data-tooltip="{{if eq .Lang "es"}}Descargar como PDF{{else}}Download as PDF{{end}}"
|
||||
aria-label="{{.UI.Widgets.ActionButtons.DownloadPdf}}"
|
||||
data-tooltip="{{.UI.Widgets.ActionButtons.DownloadPdf}}"
|
||||
_="on mouseenter call syncPdfHover(true)
|
||||
on mouseleave call syncPdfHover(false)">
|
||||
<iconify-icon icon="catppuccin:pdf" width="24" height="24"></iconify-icon>
|
||||
{{if eq .Lang "es"}}Descargar como PDF{{else}}Download as PDF{{end}}
|
||||
{{.UI.Widgets.ActionButtons.DownloadPdf}}
|
||||
</button>
|
||||
<button
|
||||
class="action-btn print-btn action-bar-print-btn has-tooltip"
|
||||
aria-label="{{if eq .Lang "es"}}Imprimir amigable{{else}}Print Friendly{{end}}"
|
||||
data-tooltip="{{if eq .Lang "es"}}Imprimir amigable{{else}}Print Friendly{{end}}"
|
||||
aria-label="{{.UI.Widgets.ActionButtons.PrintFriendly}}"
|
||||
data-tooltip="{{.UI.Widgets.ActionButtons.PrintFriendly}}"
|
||||
_="on click call printFriendly()
|
||||
on mouseenter call syncPrintHover(true)
|
||||
on mouseleave call syncPrintHover(false)">
|
||||
<iconify-icon icon="mdi:leaf" width="24" height="24"></iconify-icon>
|
||||
{{if eq .Lang "es"}}Imprimir amigable{{else}}Print Friendly{{end}}
|
||||
{{.UI.Widgets.ActionButtons.PrintFriendly}}
|
||||
</button>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -6,54 +6,54 @@
|
||||
<div class="menu-item-submenu">
|
||||
<a href="#" class="menu-item has-submenu">
|
||||
<iconify-icon icon="mdi:menu" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Secciones CV{{else}}CV Sections{{end}}</span>
|
||||
<span>{{.UI.Navigation.CvSections}}</span>
|
||||
<iconify-icon icon="mdi:chevron-right" width="16" height="16" class="submenu-arrow"></iconify-icon>
|
||||
</a>
|
||||
<div class="submenu-content">
|
||||
<a href="#education" class="menu-item"
|
||||
_="on click call scrollToSection(event, 'education')">
|
||||
<iconify-icon icon="mdi:school" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Formación{{else}}Training{{end}}</span>
|
||||
<span>{{.UI.Navigation.Training}}</span>
|
||||
</a>
|
||||
<a href="#skills" class="menu-item"
|
||||
_="on click call scrollToSection(event, 'skills')">
|
||||
<iconify-icon icon="mdi:brain" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Competencias{{else}}Skills{{end}}</span>
|
||||
<span>{{.UI.Navigation.Skills}}</span>
|
||||
</a>
|
||||
<a href="#experience" class="menu-item"
|
||||
_="on click call scrollToSection(event, 'experience')">
|
||||
<iconify-icon icon="mdi:office-building" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Experiencia{{else}}Experience{{end}}</span>
|
||||
<span>{{.UI.Navigation.Experience}}</span>
|
||||
</a>
|
||||
<a href="#awards" class="menu-item"
|
||||
_="on click call scrollToSection(event, 'awards')">
|
||||
<iconify-icon icon="mdi:trophy" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Premios y Reconocimientos{{else}}Awards{{end}}</span>
|
||||
<span>{{.UI.Navigation.Awards}}</span>
|
||||
</a>
|
||||
<a href="#projects" class="menu-item"
|
||||
_="on click call scrollToSection(event, '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>
|
||||
<span>{{.UI.Navigation.Projects}}</span>
|
||||
</a>
|
||||
<a href="#courses" class="menu-item"
|
||||
_="on click call scrollToSection(event, 'courses')">
|
||||
<iconify-icon icon="mdi:school" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Cursos Realizados{{else}}Courses{{end}}</span>
|
||||
<span>{{.UI.Navigation.Courses}}</span>
|
||||
</a>
|
||||
<a href="#languages" class="menu-item"
|
||||
_="on click call scrollToSection(event, 'languages')">
|
||||
<iconify-icon icon="mdi:translate" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Idiomas{{else}}Languages{{end}}</span>
|
||||
<span>{{.UI.Navigation.Languages}}</span>
|
||||
</a>
|
||||
<a href="#references" class="menu-item"
|
||||
_="on click call scrollToSection(event, 'references')">
|
||||
<iconify-icon icon="mdi:link-variant" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Referencias{{else}}References{{end}}</span>
|
||||
<span>{{.UI.Navigation.References}}</span>
|
||||
</a>
|
||||
<a href="#other" class="menu-item"
|
||||
_="on click call scrollToSection(event, 'other')">
|
||||
<iconify-icon icon="mdi:information" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Otros{{else}}Other{{end}}</span>
|
||||
<span>{{.UI.Navigation.Other}}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,21 +62,21 @@
|
||||
<div class="menu-section-wrapper">
|
||||
<div class="menu-item menu-item-header">
|
||||
<iconify-icon icon="mdi:cog-outline" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Acciones Rápidas{{else}}Quick Actions{{end}}</span>
|
||||
<span>{{.UI.Navigation.QuickActions}}</span>
|
||||
</div>
|
||||
|
||||
<a href="#" class="menu-item menu-item-action" _="on click call collapseAllSections(event)">
|
||||
<iconify-icon icon="mdi:arrow-collapse-all" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Colapsar Todo{{else}}Collapse All{{end}}</span>
|
||||
<span>{{.UI.Navigation.CollapseAll}}</span>
|
||||
</a>
|
||||
<a href="#" class="menu-item menu-item-action" _="on click call expandAllSections(event)">
|
||||
<iconify-icon icon="mdi:arrow-expand-all" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Expandir Todo{{else}}Expand All{{end}}</span>
|
||||
<span>{{.UI.Navigation.ExpandAll}}</span>
|
||||
</a>
|
||||
<a href="#" id="show-zoom-menu-btn" class="menu-item menu-item-action zoom-hidden"
|
||||
_="on click call showZoomControl()">
|
||||
<iconify-icon icon="mdi:magnify" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Zoom{{else}}Zoom{{end}}</span>
|
||||
<span>{{.UI.Navigation.Zoom}}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -84,14 +84,14 @@
|
||||
<div class="menu-controls-section">
|
||||
<div class="menu-item menu-item-header">
|
||||
<iconify-icon icon="mdi:tune-variant" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Controles de Vista{{else}}View Controls{{end}}</span>
|
||||
<span>{{.UI.Navigation.ViewControls}}</span>
|
||||
</div>
|
||||
|
||||
<!-- CV Length toggle -->
|
||||
<div class="menu-control-item" id="mobile-length-toggle">
|
||||
<label class="menu-control-label">
|
||||
<iconify-icon icon="mdi:file-document-outline" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Longitud{{else}}Length{{end}}</span>
|
||||
<span>{{.UI.ViewControls.Length}}</span>
|
||||
</label>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<label class="icon-toggle">
|
||||
@@ -118,7 +118,7 @@
|
||||
<div class="menu-control-item" id="mobile-icon-toggle">
|
||||
<label class="menu-control-label">
|
||||
<iconify-icon icon="mdi:image-multiple-outline" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Iconos{{else}}Icons{{end}}</span>
|
||||
<span>{{.UI.ViewControls.Icons}}</span>
|
||||
</label>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<label class="icon-toggle">
|
||||
@@ -145,7 +145,7 @@
|
||||
<div class="menu-control-item" id="mobile-theme-toggle">
|
||||
<label class="menu-control-label">
|
||||
<iconify-icon icon="mdi:page-layout-sidebar-left" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Vista{{else}}View{{end}}</span>
|
||||
<span>{{.UI.ViewControls.View}}</span>
|
||||
</label>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<label class="icon-toggle">
|
||||
@@ -173,7 +173,7 @@
|
||||
<div class="menu-actions-section">
|
||||
<div class="menu-item menu-item-header">
|
||||
<iconify-icon icon="mdi:lightning-bolt" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Acciones{{else}}Actions{{end}}</span>
|
||||
<span>{{.UI.Navigation.Actions}}</span>
|
||||
</div>
|
||||
|
||||
<button class="menu-action-btn menu-pdf-btn"
|
||||
@@ -181,7 +181,7 @@
|
||||
_="on mouseenter call syncPdfHover(true)
|
||||
on mouseleave call syncPdfHover(false)">
|
||||
<iconify-icon icon="catppuccin:pdf" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Descargar como PDF{{else}}Download as PDF{{end}}</span>
|
||||
<span>{{.UI.Widgets.ActionButtons.DownloadPdf}}</span>
|
||||
</button>
|
||||
|
||||
<button class="menu-action-btn menu-print-btn"
|
||||
@@ -189,7 +189,7 @@
|
||||
on mouseenter call syncPrintHover(true)
|
||||
on mouseleave call syncPrintHover(false)">
|
||||
<iconify-icon icon="mdi:leaf" width="20" height="20"></iconify-icon>
|
||||
<span>{{if eq .Lang "es"}}Imprimir amigable{{else}}Print Friendly{{end}}</span>
|
||||
<span>{{.UI.Widgets.ActionButtons.PrintFriendly}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="view-controls-center">
|
||||
<!-- CV Length toggle -->
|
||||
<div class="selector-group" id="desktop-length-toggle">
|
||||
<label class="selector-label">{{if eq .Lang "es"}}Longitud{{else}}Length{{end}}:</label>
|
||||
<label class="selector-label">{{.UI.ViewControls.Length}}:</label>
|
||||
<label class="icon-toggle">
|
||||
<input type="checkbox"
|
||||
id="lengthToggle"
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<!-- Icon toggle -->
|
||||
<div class="selector-group" id="desktop-icon-toggle">
|
||||
<label class="selector-label">{{if eq .Lang "es"}}Iconos{{else}}Icons{{end}}:</label>
|
||||
<label class="selector-label">{{.UI.ViewControls.Icons}}:</label>
|
||||
<label class="icon-toggle">
|
||||
<input type="checkbox"
|
||||
id="iconToggle"
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
<!-- Theme toggle -->
|
||||
<div class="selector-group" id="desktop-theme-toggle">
|
||||
<label class="selector-label">{{if eq .Lang "es"}}Vista{{else}}View{{end}}:</label>
|
||||
<label class="selector-label">{{.UI.ViewControls.View}}:</label>
|
||||
<label class="icon-toggle">
|
||||
<input type="checkbox"
|
||||
id="themeToggle"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<summary>
|
||||
<h3 class="section-title">
|
||||
<iconify-icon icon="mdi:trophy" width="24" height="24" class="section-icon"></iconify-icon>
|
||||
{{if eq .Lang "es"}}Premios y Reconocimientos{{else}}Awards{{end}}
|
||||
{{.UI.Navigation.Awards}}
|
||||
</h3>
|
||||
</summary>
|
||||
{{range .CV.Awards}}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<summary>
|
||||
<h3 class="section-title">
|
||||
<iconify-icon icon="mdi:school" width="24" height="24" class="section-icon"></iconify-icon>
|
||||
{{if eq .Lang "es"}}Cursos Realizados{{else}}Courses{{end}}
|
||||
{{.UI.Navigation.Courses}}
|
||||
</h3>
|
||||
</summary>
|
||||
{{range .CV.Courses}}
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
<summary>
|
||||
<h3 class="section-title">
|
||||
<iconify-icon icon="mdi:school" width="24" height="24" class="section-icon"></iconify-icon>
|
||||
{{if eq .Lang "es"}}Formación{{else}}Training{{end}}
|
||||
{{.UI.Navigation.Training}}
|
||||
</h3>
|
||||
</summary>
|
||||
{{range .CV.Education}}
|
||||
<div class="education-item">
|
||||
<strong>{{.Degree}}</strong> ({{.StartDate}}-{{.EndDate}}) {{if eq $.Lang "es"}}obtenido de{{else}}obtained from the{{end}} <strong>{{.Institution}}</strong> ({{.Location}})
|
||||
<strong>{{.Degree}}</strong> ({{.StartDate}}-{{.EndDate}}) {{$.UI.Sections.ObtainedFrom}} <strong>{{.Institution}}</strong> ({{.Location}})
|
||||
</div>
|
||||
{{end}}
|
||||
</details>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<summary>
|
||||
<h3 class="section-title">
|
||||
<iconify-icon icon="mdi:office-building" width="24" height="24" class="section-icon"></iconify-icon>
|
||||
{{if eq .Lang "es"}}Experiencia{{else}}Experience{{end}}
|
||||
{{.UI.Navigation.Experience}}
|
||||
</h3>
|
||||
</summary>
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
</div>
|
||||
<div class="experience-content">
|
||||
<strong>{{.Position}}{{if .Company}} - {{if .CompanyURL}}<a href="{{.CompanyURL}}" target="_blank" rel="noopener noreferrer">{{.Company}}</a>{{else}}{{.Company}}{{end}}{{if .Duration}} - <span class="duration-text">{{.Duration}}</span>{{end}}{{end}}</strong>
|
||||
{{if .Current}}<span class="current-badge">{{if eq $.Lang "es"}}ACTUAL{{else}}CURRENT{{end}}</span>{{end}}
|
||||
{{if .Expired}}<span class="expired-badge">{{if eq $.Lang "es"}}EXPIRADO{{else}}EXPIRED{{end}}</span>{{end}}
|
||||
{{if .Current}}<span class="current-badge">{{$.UI.Sections.CurrentBadge}}</span>{{end}}
|
||||
{{if .Expired}}<span class="expired-badge">{{$.UI.Sections.ExpiredBadge}}</span>{{end}}
|
||||
<br>
|
||||
<small>{{.StartDate}} / {{if .Current}}{{if eq $.Lang "es"}}presente{{else}}now{{end}}{{else}}{{.EndDate}}{{end}} - ({{.Location}})</small>
|
||||
<small>{{.StartDate}} / {{if .Current}}{{$.UI.Sections.Present}}{{else}}{{.EndDate}}{{end}} - ({{.Location}})</small>
|
||||
|
||||
{{if .ShortDescription}}
|
||||
<p class="experience-desc short-desc">{{.ShortDescription | safeHTML}}</p>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="cv-header-content">
|
||||
<div class="cv-header-left">
|
||||
<h1 class="cv-name">Moreno Rubio, Juan Andrés</h1>
|
||||
<p class="years-experience">{{.YearsOfExperience}} {{if eq .Lang "es"}}años de experiencia{{else}}years of experience{{end}}</p>
|
||||
<p class="years-experience">{{.YearsOfExperience}} {{.UI.Sections.YearsOfExperience}}</p>
|
||||
|
||||
<!-- Photo positioned for mobile (centered between name and intro) -->
|
||||
<div class="cv-photo">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<summary>
|
||||
<h3 class="section-title">
|
||||
<iconify-icon icon="mdi:translate" width="24" height="24" class="section-icon"></iconify-icon>
|
||||
{{if eq .Lang "es"}}Idiomas{{else}}Languages{{end}}
|
||||
{{.UI.Navigation.Languages}}
|
||||
</h3>
|
||||
</summary>
|
||||
{{range .CV.Languages}}
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
<summary>
|
||||
<h3 class="section-title">
|
||||
<iconify-icon icon="mdi:information" width="24" height="24" class="section-icon"></iconify-icon>
|
||||
{{if eq .Lang "es"}}Otros{{else}}Other{{end}}
|
||||
{{.UI.Navigation.Other}}
|
||||
</h3>
|
||||
</summary>
|
||||
<div class="other-content">
|
||||
{{if eq .Lang "es"}}Carnet de conducir tipo <strong>{{.CV.Other.DriverLicense}}</strong>{{else}}Driving License type <strong>{{.CV.Other.DriverLicense}}</strong>{{end}}
|
||||
{{.UI.Sections.DrivingLicense}} <strong>{{.CV.Other.DriverLicense}}</strong>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<summary>
|
||||
<h3 class="section-title">
|
||||
<iconify-icon icon="mdi:web" width="24" height="24" class="section-icon"></iconify-icon>
|
||||
{{if eq .Lang "es"}}Proyectos Personales / Freelance{{else}}Personal / Freelance Projects{{end}}
|
||||
{{.UI.Navigation.Projects}}
|
||||
</h3>
|
||||
</summary>
|
||||
{{range .CV.Projects}}
|
||||
@@ -31,9 +31,9 @@
|
||||
{{end}}
|
||||
</strong>
|
||||
{{if .Current}}<span class="live-badge"><iconify-icon icon="mdi:wifi" width="14" height="14"></iconify-icon>LIVE</span>{{end}}
|
||||
{{if .MaintainedBy}}<span class="maintained-badge">{{if eq $.Lang "es"}}MANTENIDO POR{{else}}MAINTAINED BY{{end}} {{.MaintainedBy}}</span>{{end}}
|
||||
{{if .MaintainedBy}}<span class="maintained-badge">{{$.UI.Sections.MaintainedBy}} {{.MaintainedBy}}</span>{{end}}
|
||||
<br>
|
||||
<small>{{if .StartDate}}{{.StartDate}}{{if .Current}}{{if .DynamicDate}} / {{.DynamicDate}}{{else}} / {{if eq $.Lang "es"}}presente{{else}}ahora{{end}}{{end}}{{end}}{{end}} - ({{.Location}})</small>
|
||||
<small>{{if .StartDate}}{{.StartDate}}{{if .Current}}{{if .DynamicDate}} / {{.DynamicDate}}{{else}} / {{$.UI.Sections.Present}}{{end}}{{end}}{{end}} - ({{.Location}})</small>
|
||||
|
||||
{{if .ShortDescription}}
|
||||
<p class="project-desc short-desc">{{.ShortDescription | safeHTML}}</p>
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
{{if .Technologies}}
|
||||
<div class="project-technologies long-only">
|
||||
<strong>{{if eq $.Lang "es"}}Tecnologías:{{else}}Technologies:{{end}}</strong>
|
||||
<strong>{{$.UI.Sections.Technologies}}</strong>
|
||||
{{range $index, $tech := .Technologies}}{{if $index}}, {{end}}{{$tech}}{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -59,8 +59,8 @@
|
||||
|
||||
<!-- Link to full portfolio -->
|
||||
<div class="projects-footer">
|
||||
<p>{{if eq .Lang "es"}}Ver todos los proyectos en mi{{else}}See all projects on my{{end}}
|
||||
<a href="{{.CV.Personal.Domestika}}" target="_blank" rel="noopener noreferrer"><strong>{{if eq .Lang "es"}}portfolio de Domestika{{else}}Domestika portfolio{{end}}</strong></a></p>
|
||||
<p>{{.UI.Portfolio.SeeAllProjects}}
|
||||
<a href="{{.CV.Personal.Domestika}}" target="_blank" rel="noopener noreferrer"><strong>{{.UI.Portfolio.DomestikaPortfolio}}</strong></a></p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<summary>
|
||||
<h3 class="section-title">
|
||||
<iconify-icon icon="mdi:link-variant" width="24" height="24" class="section-icon"></iconify-icon>
|
||||
{{if eq .Lang "es"}}Referencias{{else}}References{{end}}
|
||||
{{.UI.Navigation.References}}
|
||||
</h3>
|
||||
</summary>
|
||||
{{range .CV.References}}
|
||||
|
||||
@@ -7,16 +7,10 @@
|
||||
<summary>
|
||||
<h3 class="section-title">
|
||||
<iconify-icon icon="mdi:brain" width="24" height="24" class="section-icon"></iconify-icon>
|
||||
{{if eq .Lang "es"}}Competencias{{else}}Skills{{end}}
|
||||
{{.UI.Navigation.Skills}}
|
||||
</h3>
|
||||
</summary>
|
||||
<p class="summary-text">
|
||||
{{if eq .Lang "es"}}
|
||||
Desarrollador <strong>full-stack</strong> con experiencia en <strong>Go</strong>, <strong>Node.js</strong>, <strong>React</strong> y <strong>HTMX</strong> para <strong>aplicaciones modernas</strong>, además de conocimientos en Java y PHP para proyectos legacy. He trabajado en <strong>unos 20 sitios web</strong> y realizado <strong>consultoría para 35-40 clientes internacionales</strong>, desde e-commerce y plataformas empresariales hasta <strong>sistemas de autenticación</strong> que gestionan <strong>millones de usuarios</strong>. Familiarizado con flujos de trabajo asistidos por <strong>IA</strong> y gestión de infraestructura (<strong>Linux</strong>, <strong>Docker</strong>, <strong>CI/CD</strong>). Me adapto bien tanto al trabajo independiente como colaborativo en equipos internacionales.
|
||||
{{else}}
|
||||
<strong>Full-stack</strong> developer with experience in <strong>Go</strong>, <strong>Node.js</strong>, <strong>React</strong>, and <strong>HTMX</strong> for <strong>modern applications</strong>, plus Java and PHP knowledge for legacy projects. I've worked on <strong>around 20 websites</strong> and provided <strong>consulting for 35-40 international clients</strong>, from e-commerce and enterprise platforms to <strong>authentication systems</strong> managing <strong>millions of users</strong>. Familiar with <strong>AI-assisted development</strong> workflows and infrastructure management (<strong>Linux</strong>, <strong>Docker</strong>, <strong>CI/CD</strong>). I adapt well to both independent work and collaborative teams across different countries.
|
||||
{{end}}
|
||||
</p>
|
||||
<p class="summary-text">{{.CV.SkillsSummary}}</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<!-- Back to Top Link - Hyperscript smooth scroll without URL pollution -->
|
||||
<button id="back-to-top"
|
||||
class="back-to-top no-print has-tooltip tooltip-left"
|
||||
aria-label="{{if eq .Lang "es"}}Volver arriba{{else}}Back to top{{end}}"
|
||||
data-tooltip="{{if eq .Lang "es"}}Volver arriba{{else}}Back to top{{end}}"
|
||||
aria-label="{{.UI.Widgets.BackToTop.AriaLabel}}"
|
||||
data-tooltip="{{.UI.Widgets.BackToTop.Tooltip}}"
|
||||
style="display: none;"
|
||||
_="on click call scrollToTop(event)">
|
||||
<iconify-icon icon="mdi:arrow-up" width="24" height="24"></iconify-icon>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<button
|
||||
id="download-button"
|
||||
class="fixed-btn download-btn no-print has-tooltip"
|
||||
aria-label="{{if eq .Lang "es"}}Descargar PDF{{else}}Download as PDF{{end}}"
|
||||
data-tooltip="{{if eq .Lang "es"}}Descargar PDF{{else}}Download as PDF{{end}}"
|
||||
aria-label="{{.UI.Widgets.Download.AriaLabel}}"
|
||||
data-tooltip="{{.UI.Widgets.Download.Tooltip}}"
|
||||
onclick="openPdfModal()"
|
||||
_="on mouseenter call syncPdfHover(true)
|
||||
on mouseleave call syncPdfHover(false)">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{{define "info-button"}}
|
||||
<!-- Info Button (Bottom Left) -->
|
||||
<button id="info-button" class="info-button no-print has-tooltip"
|
||||
aria-label="{{if eq .Lang "es"}}Información{{else}}Information{{end}}"
|
||||
data-tooltip="{{if eq .Lang "es"}}Información{{else}}Information{{end}}"
|
||||
aria-label="{{.UI.Widgets.Info.AriaLabel}}"
|
||||
data-tooltip="{{.UI.Widgets.Info.Tooltip}}"
|
||||
onclick="document.getElementById('info-modal').showModal()">
|
||||
<iconify-icon icon="mdi:information-outline" width="24" height="24"></iconify-icon>
|
||||
</button>
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
<div id="pdf-toast" class="success-toast no-print" role="status" aria-live="polite" aria-atomic="true">
|
||||
<span class="toast-icon" id="pdf-toast-icon">📥</span>
|
||||
<div class="toast-content">
|
||||
<p class="toast-title" id="pdf-toast-title">{{if eq .Lang "es"}}Preparando PDF{{else}}Preparing PDF{{end}}</p>
|
||||
<p class="toast-title" id="pdf-toast-title">{{.UI.Widgets.PdfToast.Title}}</p>
|
||||
<p class="toast-message" id="pdf-toast-message"></p>
|
||||
</div>
|
||||
<button aria-label="{{if eq .Lang "es"}}Cerrar notificación{{else}}Close notification{{end}}" class="toast-close" onclick="document.getElementById('pdf-toast').classList.remove('show')">×</button>
|
||||
<button aria-label="{{.UI.Widgets.PdfToast.CloseLabel}}" class="toast-close" onclick="document.getElementById('pdf-toast').classList.remove('show')">×</button>
|
||||
<div class="toast-progress">
|
||||
<div class="toast-progress-bar" id="pdf-toast-progress"></div>
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<button
|
||||
id="print-friendly-button"
|
||||
class="fixed-btn print-friendly-btn no-print has-tooltip"
|
||||
aria-label="{{if eq .Lang "es"}}Imprimir CV{{else}}Print Friendly{{end}}"
|
||||
data-tooltip="{{if eq .Lang "es"}}Imprimir CV{{else}}Print Friendly{{end}}"
|
||||
aria-label="{{.UI.Widgets.Print.AriaLabel}}"
|
||||
data-tooltip="{{.UI.Widgets.Print.Tooltip}}"
|
||||
onclick="window.print()"
|
||||
_="on mouseenter call syncPrintHover(true)
|
||||
on mouseleave call syncPrintHover(false)">
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
id="shortcuts-button"
|
||||
class="fixed-btn shortcuts-btn no-print has-tooltip"
|
||||
onclick="document.getElementById('shortcuts-modal').showModal()"
|
||||
aria-label="{{if eq .Lang "es"}}Atajos de teclado{{else}}Keyboard shortcuts{{end}}"
|
||||
data-tooltip="{{if eq .Lang "es"}}Atajos de teclado (?){{else}}Keyboard shortcuts (?){{end}}">
|
||||
aria-label="{{.UI.Widgets.Shortcuts.AriaLabel}}"
|
||||
data-tooltip="{{.UI.Widgets.Shortcuts.Tooltip}}">
|
||||
<iconify-icon icon="mdi:keyboard-outline" width="28" height="28"></iconify-icon>
|
||||
</button>
|
||||
{{end}}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{define "zoom-control"}}
|
||||
<!-- Zoom Control (Fixed Bottom Center, Draggable) - Hyperscript Enhanced -->
|
||||
<div id="zoom-control" class="zoom-control no-print zoom-hidden" role="group" aria-label="{{if eq .Lang "es"}}Control de zoom{{else}}Zoom control{{end}}"
|
||||
<div id="zoom-control" class="zoom-control no-print zoom-hidden" role="group" aria-label="{{.UI.Widgets.ZoomControl.GroupLabel}}"
|
||||
_="on load call initZoomControl(me)
|
||||
on mousedown(clientX, clientY) if isZoomDragTarget(event.target) call startZoomDrag(me, clientX, clientY) then halt the event end
|
||||
on mousemove(clientX, clientY) from document if moveZoomDrag(me, clientX, clientY) halt the event end
|
||||
@@ -9,8 +9,8 @@
|
||||
<button
|
||||
id="zoom-close"
|
||||
class="zoom-close-btn"
|
||||
aria-label="{{if eq .Lang "es"}}Cerrar control de zoom{{else}}Close zoom control{{end}}"
|
||||
title="{{if eq .Lang "es"}}Cerrar{{else}}Close{{end}}"
|
||||
aria-label="{{.UI.Widgets.ZoomControl.CloseLabel}}"
|
||||
title="{{.UI.Widgets.ZoomControl.CloseTitle}}"
|
||||
_="on click call hideZoomControl()">
|
||||
<iconify-icon icon="mdi:close" width="16" height="16" style="pointer-events: none;"></iconify-icon>
|
||||
</button>
|
||||
@@ -25,7 +25,7 @@
|
||||
max="300"
|
||||
step="1"
|
||||
value="100"
|
||||
aria-label="{{if eq .Lang "es"}}Ajustar nivel de zoom del CV{{else}}Adjust CV zoom level{{end}}"
|
||||
aria-label="{{.UI.Widgets.ZoomControl.SliderLabel}}"
|
||||
aria-valuemin="25"
|
||||
aria-valuemax="300"
|
||||
aria-valuenow="100"
|
||||
@@ -37,8 +37,8 @@
|
||||
<button
|
||||
id="zoom-reset"
|
||||
class="zoom-reset-btn"
|
||||
aria-label="{{if eq .Lang "es"}}Restablecer zoom al 100%{{else}}Reset zoom to 100%{{end}}"
|
||||
title="{{if eq .Lang "es"}}Restablecer{{else}}Reset{{end}}"
|
||||
aria-label="{{.UI.Widgets.ZoomControl.ResetLabel}}"
|
||||
title="{{.UI.Widgets.ZoomControl.ResetTitle}}"
|
||||
aria-live="polite"
|
||||
_="on click call handleZoomReset()">
|
||||
<span id="zoom-value-current">100</span>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<button
|
||||
id="zoom-toggle-button"
|
||||
class="fixed-btn zoom-toggle-btn no-print has-tooltip"
|
||||
aria-label="{{if eq .Lang "es"}}Alternar control de zoom{{else}}Toggle zoom control{{end}}"
|
||||
data-tooltip="{{if eq .Lang "es"}}Control de zoom{{else}}Zoom control{{end}}"
|
||||
aria-label="{{.UI.Widgets.ZoomToggle.AriaLabel}}"
|
||||
data-tooltip="{{.UI.Widgets.ZoomToggle.Tooltip}}"
|
||||
_="on click call toggleZoomControl()
|
||||
on mouseenter call highlightZoomControl(true)
|
||||
on mouseleave call highlightZoomControl(false)">
|
||||
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* JSON CONTENT VALIDATION TEST
|
||||
* ============================
|
||||
* Tests that CV content is loaded from JSON files, not hardcoded in templates.
|
||||
* Validates:
|
||||
* - Title badges rendered from CV JSON
|
||||
* - SEO meta tags from CV JSON
|
||||
* - Widget labels from UI JSON
|
||||
* - Both EN and ES languages
|
||||
*/
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
const URL = "http://localhost:1999";
|
||||
|
||||
// Load JSON files for comparison
|
||||
const dataDir = join(process.cwd(), 'data');
|
||||
const cvEN = JSON.parse(readFileSync(join(dataDir, 'cv-en.json'), 'utf-8'));
|
||||
const cvES = JSON.parse(readFileSync(join(dataDir, 'cv-es.json'), 'utf-8'));
|
||||
const uiEN = JSON.parse(readFileSync(join(dataDir, 'ui-en.json'), 'utf-8'));
|
||||
const uiES = JSON.parse(readFileSync(join(dataDir, 'ui-es.json'), 'utf-8'));
|
||||
|
||||
async function testJSONContentValidation() {
|
||||
console.log('📋 JSON CONTENT VALIDATION TEST\n');
|
||||
console.log('='.repeat(70));
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const testResults = [];
|
||||
|
||||
// ========================================================================
|
||||
// TEST 1: English - Title Badges from JSON
|
||||
// ========================================================================
|
||||
console.log("\n1️⃣ Testing English Title Badges...");
|
||||
|
||||
const pageEN = await browser.newPage();
|
||||
await pageEN.goto(`${URL}/?lang=en`);
|
||||
await pageEN.waitForTimeout(1000);
|
||||
|
||||
const titleBadgesEN = await pageEN.evaluate(() => {
|
||||
const badges = document.querySelectorAll('.title-badge');
|
||||
return Array.from(badges).map(b => b.textContent.trim());
|
||||
});
|
||||
|
||||
// Compare with JSON (CSS makes them uppercase, so compare case-insensitively)
|
||||
const expectedBadgesEN = cvEN.personal.titleBadges.map(b => b.toUpperCase());
|
||||
const actualBadgesEN = titleBadgesEN.map(b => b.toUpperCase());
|
||||
|
||||
const badgesMatchEN = expectedBadgesEN.every((badge, i) => actualBadgesEN[i] === badge);
|
||||
console.log(` Expected: ${expectedBadgesEN.join(' | ')}`);
|
||||
console.log(` Actual: ${actualBadgesEN.join(' | ')}`);
|
||||
console.log(` ${badgesMatchEN ? '✅ PASS' : '❌ FAIL'} - Title badges match JSON`);
|
||||
testResults.push({ test: 'EN Title Badges', passed: badgesMatchEN });
|
||||
|
||||
// ========================================================================
|
||||
// TEST 2: English - SEO Meta Tags from JSON
|
||||
// ========================================================================
|
||||
console.log("\n2️⃣ Testing English SEO Meta Tags...");
|
||||
|
||||
const metaEN = await pageEN.evaluate(() => {
|
||||
return {
|
||||
title: document.title,
|
||||
description: document.querySelector('meta[name="description"]')?.content,
|
||||
keywords: document.querySelector('meta[name="keywords"]')?.content,
|
||||
ogTitle: document.querySelector('meta[property="og:title"]')?.content,
|
||||
ogDescription: document.querySelector('meta[property="og:description"]')?.content,
|
||||
firstName: document.querySelector('meta[property="profile:first_name"]')?.content,
|
||||
lastName: document.querySelector('meta[property="profile:last_name"]')?.content,
|
||||
username: document.querySelector('meta[property="profile:username"]')?.content,
|
||||
};
|
||||
});
|
||||
|
||||
const seoTestsEN = [
|
||||
{ name: 'Page title contains SEO pageTitle', passed: metaEN.title.includes(cvEN.seo.pageTitle) },
|
||||
{ name: 'Description contains SEO metaDescription', passed: metaEN.description.includes(cvEN.seo.metaDescription) },
|
||||
{ name: 'Keywords contain SEO keywords', passed: metaEN.keywords.includes(cvEN.seo.keywords.split(',')[0].trim()) },
|
||||
{ name: 'OG description contains SEO ogDescription', passed: metaEN.ogDescription.includes(cvEN.seo.ogDescription) },
|
||||
{ name: 'First name from JSON', passed: metaEN.firstName === cvEN.personal.firstName },
|
||||
{ name: 'Last name from JSON', passed: metaEN.lastName === cvEN.personal.lastName },
|
||||
{ name: 'Username from JSON', passed: metaEN.username === cvEN.personal.username },
|
||||
];
|
||||
|
||||
seoTestsEN.forEach(t => {
|
||||
console.log(` ${t.passed ? '✅' : '❌'} ${t.name}`);
|
||||
testResults.push({ test: `EN SEO: ${t.name}`, passed: t.passed });
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// TEST 3: English - Widget Labels from UI JSON
|
||||
// ========================================================================
|
||||
console.log("\n3️⃣ Testing English Widget Labels...");
|
||||
|
||||
const widgetsEN = await pageEN.evaluate(() => {
|
||||
return {
|
||||
backToTop: document.querySelector('#back-to-top')?.getAttribute('aria-label'),
|
||||
infoButton: document.querySelector('#info-button')?.getAttribute('aria-label'),
|
||||
downloadButton: document.querySelector('#download-button')?.getAttribute('aria-label'),
|
||||
printButton: document.querySelector('#print-friendly-button')?.getAttribute('aria-label'),
|
||||
shortcutsButton: document.querySelector('#shortcuts-button')?.getAttribute('aria-label'),
|
||||
zoomToggle: document.querySelector('#zoom-toggle-button')?.getAttribute('aria-label'),
|
||||
zoomControl: document.querySelector('#zoom-control')?.getAttribute('aria-label'),
|
||||
};
|
||||
});
|
||||
|
||||
const widgetTestsEN = [
|
||||
{ name: 'Back to top label', passed: widgetsEN.backToTop === uiEN.widgets.backToTop.ariaLabel },
|
||||
{ name: 'Info button label', passed: widgetsEN.infoButton === uiEN.widgets.info.ariaLabel },
|
||||
{ name: 'Download button label', passed: widgetsEN.downloadButton === uiEN.widgets.download.ariaLabel },
|
||||
{ name: 'Print button label', passed: widgetsEN.printButton === uiEN.widgets.print.ariaLabel },
|
||||
{ name: 'Shortcuts button label', passed: widgetsEN.shortcutsButton === uiEN.widgets.shortcuts.ariaLabel },
|
||||
{ name: 'Zoom toggle label', passed: widgetsEN.zoomToggle === uiEN.widgets.zoomToggle.ariaLabel },
|
||||
{ name: 'Zoom control label', passed: widgetsEN.zoomControl === uiEN.widgets.zoomControl.groupLabel },
|
||||
];
|
||||
|
||||
widgetTestsEN.forEach(t => {
|
||||
console.log(` ${t.passed ? '✅' : '❌'} ${t.name}: "${t.passed ? 'matches' : 'MISMATCH'}"`);
|
||||
testResults.push({ test: `EN Widget: ${t.name}`, passed: t.passed });
|
||||
});
|
||||
|
||||
await pageEN.close();
|
||||
|
||||
// ========================================================================
|
||||
// TEST 4: Spanish - Title Badges from JSON
|
||||
// ========================================================================
|
||||
console.log("\n4️⃣ Testing Spanish Title Badges...");
|
||||
|
||||
const pageES = await browser.newPage();
|
||||
await pageES.goto(`${URL}/?lang=es`);
|
||||
await pageES.waitForTimeout(1000);
|
||||
|
||||
const titleBadgesES = await pageES.evaluate(() => {
|
||||
const badges = document.querySelectorAll('.title-badge');
|
||||
return Array.from(badges).map(b => b.textContent.trim());
|
||||
});
|
||||
|
||||
const expectedBadgesES = cvES.personal.titleBadges.map(b => b.toUpperCase());
|
||||
const actualBadgesES = titleBadgesES.map(b => b.toUpperCase());
|
||||
|
||||
const badgesMatchES = expectedBadgesES.every((badge, i) => actualBadgesES[i] === badge);
|
||||
console.log(` Expected: ${expectedBadgesES.join(' | ')}`);
|
||||
console.log(` Actual: ${actualBadgesES.join(' | ')}`);
|
||||
console.log(` ${badgesMatchES ? '✅ PASS' : '❌ FAIL'} - Title badges match JSON`);
|
||||
testResults.push({ test: 'ES Title Badges', passed: badgesMatchES });
|
||||
|
||||
// ========================================================================
|
||||
// TEST 5: Spanish - SEO Meta Tags from JSON
|
||||
// ========================================================================
|
||||
console.log("\n5️⃣ Testing Spanish SEO Meta Tags...");
|
||||
|
||||
const metaES = await pageES.evaluate(() => {
|
||||
return {
|
||||
title: document.title,
|
||||
description: document.querySelector('meta[name="description"]')?.content,
|
||||
ogDescription: document.querySelector('meta[property="og:description"]')?.content,
|
||||
};
|
||||
});
|
||||
|
||||
const seoTestsES = [
|
||||
{ name: 'Page title contains SEO pageTitle', passed: metaES.title.includes(cvES.seo.pageTitle) },
|
||||
{ name: 'Description contains SEO metaDescription', passed: metaES.description.includes(cvES.seo.metaDescription) },
|
||||
{ name: 'OG description contains SEO ogDescription', passed: metaES.ogDescription.includes(cvES.seo.ogDescription) },
|
||||
];
|
||||
|
||||
seoTestsES.forEach(t => {
|
||||
console.log(` ${t.passed ? '✅' : '❌'} ${t.name}`);
|
||||
testResults.push({ test: `ES SEO: ${t.name}`, passed: t.passed });
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// TEST 6: Spanish - Widget Labels from UI JSON
|
||||
// ========================================================================
|
||||
console.log("\n6️⃣ Testing Spanish Widget Labels...");
|
||||
|
||||
const widgetsES = await pageES.evaluate(() => {
|
||||
return {
|
||||
backToTop: document.querySelector('#back-to-top')?.getAttribute('aria-label'),
|
||||
infoButton: document.querySelector('#info-button')?.getAttribute('aria-label'),
|
||||
downloadButton: document.querySelector('#download-button')?.getAttribute('aria-label'),
|
||||
printButton: document.querySelector('#print-friendly-button')?.getAttribute('aria-label'),
|
||||
};
|
||||
});
|
||||
|
||||
const widgetTestsES = [
|
||||
{ name: 'Back to top label', passed: widgetsES.backToTop === uiES.widgets.backToTop.ariaLabel },
|
||||
{ name: 'Info button label', passed: widgetsES.infoButton === uiES.widgets.info.ariaLabel },
|
||||
{ name: 'Download button label', passed: widgetsES.downloadButton === uiES.widgets.download.ariaLabel },
|
||||
{ name: 'Print button label', passed: widgetsES.printButton === uiES.widgets.print.ariaLabel },
|
||||
];
|
||||
|
||||
widgetTestsES.forEach(t => {
|
||||
console.log(` ${t.passed ? '✅' : '❌'} ${t.name}: "${t.passed ? 'matches' : 'MISMATCH'}"`);
|
||||
testResults.push({ test: `ES Widget: ${t.name}`, passed: t.passed });
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// TEST 7: Verify NO hardcoded language conditionals in rendered output
|
||||
// ========================================================================
|
||||
console.log("\n7️⃣ Testing for hardcoded content elimination...");
|
||||
|
||||
// Check that title badges don't contain "if eq .Lang" template artifacts
|
||||
const noTemplateArtifacts = await pageES.evaluate(() => {
|
||||
const html = document.body.innerHTML;
|
||||
return !html.includes('{{if eq .Lang') && !html.includes('{{else}}');
|
||||
});
|
||||
|
||||
console.log(` ${noTemplateArtifacts ? '✅' : '❌'} No template artifacts in rendered HTML`);
|
||||
testResults.push({ test: 'No template artifacts', passed: noTemplateArtifacts });
|
||||
|
||||
await pageES.close();
|
||||
await browser.close();
|
||||
|
||||
// ========================================================================
|
||||
// FINAL SUMMARY
|
||||
// ========================================================================
|
||||
console.log("\n" + "=".repeat(70));
|
||||
console.log("📊 TEST SUMMARY\n");
|
||||
|
||||
const passedCount = testResults.filter(r => r.passed).length;
|
||||
const totalCount = testResults.length;
|
||||
|
||||
// Group by category
|
||||
const categories = {
|
||||
'EN Title Badges': testResults.filter(r => r.test.includes('EN Title')),
|
||||
'EN SEO': testResults.filter(r => r.test.includes('EN SEO')),
|
||||
'EN Widgets': testResults.filter(r => r.test.includes('EN Widget')),
|
||||
'ES Title Badges': testResults.filter(r => r.test.includes('ES Title')),
|
||||
'ES SEO': testResults.filter(r => r.test.includes('ES SEO')),
|
||||
'ES Widgets': testResults.filter(r => r.test.includes('ES Widget')),
|
||||
'Other': testResults.filter(r => !r.test.includes('EN ') && !r.test.includes('ES ')),
|
||||
};
|
||||
|
||||
for (const [category, tests] of Object.entries(categories)) {
|
||||
if (tests.length === 0) continue;
|
||||
const catPassed = tests.filter(t => t.passed).length;
|
||||
const icon = catPassed === tests.length ? '✅' : '❌';
|
||||
console.log(` ${icon} ${category}: ${catPassed}/${tests.length}`);
|
||||
}
|
||||
|
||||
console.log(`\n Total: ${passedCount}/${totalCount} tests passed`);
|
||||
console.log("=".repeat(70) + "\n");
|
||||
|
||||
if (passedCount === totalCount) {
|
||||
console.log("🎉 JSON CONTENT VALIDATION PASSED!");
|
||||
console.log(" All content is correctly loaded from JSON files.");
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log("⚠️ SOME TESTS FAILED");
|
||||
console.log(" Check that templates use JSON data instead of hardcoded values.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
await testJSONContentValidation();
|
||||
Reference in New Issue
Block a user