Initial commit: Go + HTMX CV Site

- Minimal, professional CV design with paper-on-gray layout
- Bilingual support (Spanish/English) with HTMX language switching
- JSON-based content management (cv-en.json, cv-es.json)
- Print-optimized CSS for PDF export
- Responsive design for all devices
- Go backend with stdlib net/http
- Clean, maintainable codebase

Features:
- 18+ years professional experience
- SAP CDC expertise
- Complete project history
- Education, certifications, awards
- Multi-language support

Tech stack: Go, HTMX, vanilla CSS
This commit is contained in:
juanatsap
2025-10-20 08:54:21 +01:00
commit dab68f34f2
28 changed files with 5862 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
<!-- CV Content Template - Minimal Design -->
<div class="cv-header">
<div class="cv-header-main">
<h1 class="cv-name">{{.CV.Personal.Name}}</h1>
<h2 class="cv-title">{{.CV.Personal.Title}}</h2>
</div>
<div class="cv-contact">
<div class="contact-item">{{.CV.Personal.Location}}</div>
<div class="contact-item"><a href="mailto:{{.CV.Personal.Email}}">{{.CV.Personal.Email}}</a></div>
<div class="contact-item">{{.CV.Personal.Phone}}</div>
<div class="contact-item"><a href="{{.CV.Personal.LinkedIn}}" target="_blank">LinkedIn</a></div>
<div class="contact-item"><a href="{{.CV.Personal.GitHub}}" target="_blank">GitHub</a></div>
</div>
</div>
<!-- Summary -->
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Resumen{{else}}Summary{{end}}</h3>
<p class="summary-text">{{.CV.Summary}}</p>
</section>
<!-- Experience -->
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Experiencia Laboral{{else}}Work History{{end}}</h3>
{{range .CV.Experience}}
<div class="experience-item">
<div class="experience-header">
<div class="experience-title">
<h4 class="position">{{.Position}}</h4>
<div class="company">{{.Company}}, {{.Location}}</div>
</div>
<div class="experience-period">
{{.StartDate}} - {{if .Current}}{{if eq $.Lang "es"}}Presente{{else}}Present{{end}}{{else}}{{.EndDate}}{{end}}
</div>
</div>
<ul class="responsibilities">
{{range .Responsibilities}}
<li>{{.}}</li>
{{end}}
</ul>
{{if .Technologies}}
<div class="technologies">
{{range $index, $tech := .Technologies}}{{if $index}}, {{end}}{{$tech}}{{end}}
</div>
{{end}}
</div>
{{end}}
</section>
<!-- Education -->
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Formación{{else}}Education{{end}}</h3>
{{range .CV.Education}}
<div class="education-item">
<div class="education-header">
<h4 class="degree">{{.Degree}}</h4>
<div class="education-period">{{.StartDate}} - {{.EndDate}}</div>
</div>
<div class="institution">{{.Institution}}, {{.Location}}</div>
</div>
{{end}}
</section>
<!-- Skills -->
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Competencias{{else}}Skills{{end}}</h3>
{{range .CV.Skills.Technical}}
<div class="skill-block">
<h4 class="skill-title">{{.Category}}</h4>
<p class="skill-list">
{{range $index, $item := .Items}}{{if $index}}, {{end}}{{$item}}{{end}}
</p>
</div>
{{end}}
</section>
<!-- Projects -->
{{if .CV.Projects}}
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Proyectos{{else}}Projects{{end}}</h3>
{{range .CV.Projects}}
<div class="project-item">
<div class="project-header">
<h4 class="project-name">{{.Name}}</h4>
<div class="project-period">{{.Period}}</div>
</div>
<div class="project-role">{{.Role}}</div>
<p class="project-description">{{.Description}}</p>
</div>
{{end}}
</section>
{{end}}
<!-- Certifications -->
{{if .CV.Certifications}}
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Certificaciones{{else}}Certifications{{end}}</h3>
{{range .CV.Certifications}}
<div class="cert-item">
<strong>{{.Name}}</strong> - {{.Issuer}} ({{.Date}})
</div>
{{end}}
</section>
{{end}}
<!-- Awards -->
{{if .CV.Awards}}
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Premios{{else}}Awards{{end}}</h3>
{{range .CV.Awards}}
<div class="award-item">
<strong>{{.Title}}</strong> - {{.Issuer}} ({{.Date}})
</div>
{{end}}
</section>
{{end}}
<!-- Languages -->
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Idiomas{{else}}Languages{{end}}</h3>
<div class="languages-list">
{{range .CV.Languages}}
<div class="language-item">
<strong>{{.Language}}</strong>: {{.Proficiency}}
</div>
{{end}}
</div>
</section>
+171
View File
@@ -0,0 +1,171 @@
<!DOCTYPE html>
<html lang="{{if eq .Lang "es"}}es{{else}}en{{end}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{{.CV.Personal.Name}} - {{.CV.Personal.Title}}">
<meta name="keywords" content="CV, Resume, {{.CV.Personal.Name}}, Developer, SAP, AI">
<meta name="author" content="{{.CV.Personal.Name}}">
<meta name="robots" content="index, follow">
<!-- Open Graph Meta Tags -->
<meta property="og:title" content="{{.CV.Personal.Name}} - Curriculum Vitae">
<meta property="og:description" content="{{.CV.Personal.Title}}">
<meta property="og:type" content="profile">
<meta property="og:url" content="{{.CV.Personal.Website}}">
<title>{{.CV.Personal.Name}} - Curriculum Vitae</title>
<!-- HTMX with Integrity Check -->
<script src="https://unpkg.com/htmx.org@1.9.10"
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
crossorigin="anonymous"></script>
<!-- CSS -->
<link rel="stylesheet" href="/static/css/main.css">
<link rel="stylesheet" href="/static/css/print.css" media="print">
<!-- Fonts with Preload -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- Favicon -->
<link rel="icon" type="image/png" href="/static/favicon.png">
<!-- HTMX Configuration -->
<meta name="htmx-config" content='{"timeout":5000,"defaultSwapStyle":"innerHTML","defaultSwapDelay":0,"defaultSettleDelay":20}'>
</head>
<body>
<!-- Language & Export Bar (hidden in print) -->
<div class="action-bar no-print" role="navigation" aria-label="Language and export controls">
<div class="action-bar-content">
<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-indicator="#loading"
hx-push-url="/?lang=en"
hx-on::before-request="this.setAttribute('aria-busy', 'true')"
hx-on::after-request="this.setAttribute('aria-busy', 'false')"
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-indicator="#loading"
hx-push-url="/?lang=es"
hx-on::before-request="this.setAttribute('aria-busy', 'true')"
hx-on::after-request="this.setAttribute('aria-busy', 'false')"
aria-label="Switch to Spanish"
aria-pressed="{{if eq .Lang "es"}}true{{else}}false{{end}}">
🇪🇸 Español
</button>
</div>
<div class="export-actions">
<button
class="export-btn"
onclick="window.print()"
aria-label="{{if eq .Lang "es"}}Descargar PDF del CV{{else}}Download CV as PDF{{end}}">
📄 {{if eq .Lang "es"}}Descargar PDF{{else}}Download PDF{{end}}
</button>
</div>
<span id="loading" class="htmx-indicator" role="status" aria-live="polite" aria-label="Loading">
<span class="loader" aria-hidden="true"></span>
<span class="sr-only">Loading...</span>
</span>
</div>
</div>
<!-- CV Content Container -->
<div class="cv-container">
<main id="cv-content"
class="cv-paper"
role="main"
aria-live="polite"
aria-atomic="false">
{{template "cv-content.html" .}}
</main>
</div>
<!-- Error Toast (hidden by default) -->
<div id="error-toast" class="error-toast no-print" role="alert" aria-live="assertive" style="display: none;">
<span id="error-message"></span>
<button onclick="this.parentElement.style.display='none'" aria-label="Close error message">×</button>
</div>
<!-- Footer (hidden in print) -->
<footer class="no-print" role="contentinfo">
<p>&copy; {{.CV.Meta.LastUpdated}} {{.CV.Personal.Name}} |
{{if eq .Lang "es"}}Última actualización{{else}}Last updated{{end}}: {{.CV.Meta.LastUpdated}}</p>
</footer>
<!-- HTMX Event Handlers -->
<script>
// Global error handler for HTMX requests
document.body.addEventListener('htmx:responseError', function(evt) {
const errorToast = document.getElementById('error-toast');
const errorMessage = document.getElementById('error-message');
errorMessage.textContent = '{{if eq .Lang "es"}}Error al cargar el contenido. Por favor, inténtelo de nuevo.{{else}}Failed to load content. Please try again.{{end}}';
errorToast.style.display = 'flex';
// Auto-hide after 5 seconds
setTimeout(() => {
errorToast.style.display = 'none';
}, 5000);
});
// Smooth scroll to top on language change
document.body.addEventListener('htmx:afterSwap', function(evt) {
if (evt.detail.target.id === 'cv-content') {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
});
// Save language preference
document.body.addEventListener('htmx:afterRequest', function(evt) {
const url = new URL(evt.detail.xhr.responseURL);
const lang = url.searchParams.get('lang');
if (lang) {
localStorage.setItem('cv-lang', lang);
}
});
// Load saved language preference on page load
window.addEventListener('DOMContentLoaded', function() {
const savedLang = localStorage.getItem('cv-lang');
const currentLang = '{{.Lang}}';
if (savedLang && savedLang !== currentLang) {
document.querySelector(`[hx-get="/cv?lang=${savedLang}"]`)?.click();
}
});
// Keyboard shortcuts
document.addEventListener('keydown', function(evt) {
// Ctrl/Cmd + P for print
if ((evt.ctrlKey || evt.metaKey) && evt.key === 'p') {
evt.preventDefault();
window.print();
}
// Ctrl/Cmd + E for English, Ctrl/Cmd + S for Spanish
if (evt.ctrlKey || evt.metaKey) {
if (evt.key === 'e') {
evt.preventDefault();
document.querySelector('[hx-get="/cv?lang=en"]')?.click();
} else if (evt.key === 's' && evt.shiftKey) {
evt.preventDefault();
document.querySelector('[hx-get="/cv?lang=es"]')?.click();
}
}
});
</script>
</body>
</html>
+72
View File
@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="{{if eq .Lang "es"}}es{{else}}en{{end}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{{.CV.Personal.Name}} - {{.CV.Personal.Title}}">
<meta name="keywords" content="CV, Resume, {{.CV.Personal.Name}}, Developer, SAP, AI">
<title>{{.CV.Personal.Name}} - Curriculum Vitae</title>
<!-- HTMX -->
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<!-- CSS -->
<link rel="stylesheet" href="/static/css/main.css">
<link rel="stylesheet" href="/static/css/print.css" media="print">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<!-- Language & Export Bar (hidden in print) -->
<div class="action-bar no-print">
<div class="action-bar-content">
<div class="language-toggle">
<button
class="lang-btn {{if eq .Lang "en"}}active{{end}}"
hx-get="/cv?lang=en"
hx-target="#cv-content"
hx-swap="innerHTML"
hx-indicator="#loading">
🇬🇧 English
</button>
<button
class="lang-btn {{if eq .Lang "es"}}active{{end}}"
hx-get="/cv?lang=es"
hx-target="#cv-content"
hx-swap="innerHTML"
hx-indicator="#loading">
🇪🇸 Español
</button>
</div>
<div class="export-actions">
<button
class="export-btn"
onclick="window.print()">
📄 {{if eq .Lang "es"}}Descargar PDF{{else}}Download PDF{{end}}
</button>
</div>
<span id="loading" class="htmx-indicator">
<span class="loader"></span>
</span>
</div>
</div>
<!-- CV Content Container -->
<div class="cv-container">
<div id="cv-content" class="cv-paper">
{{template "cv-content.html" .}}
</div>
</div>
<!-- Footer (hidden in print) -->
<footer class="no-print">
<p>© {{.CV.Meta.LastUpdated}} {{.CV.Personal.Name}} |
{{if eq .Lang "es"}}Última actualización{{else}}Last updated{{end}}: {{.CV.Meta.LastUpdated}}</p>
</footer>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
# Placeholder for future partial templates