feat: add hamburger navigation menu with smooth scrolling to CV sections

Implemented a complete navigation system with the following features:

**Navigation Menu:**
- Hamburger button in top-left of action bar
- Slide-out navigation menu with all CV sections
- Smooth close on click outside or after selection
- Mobile-responsive design (280px desktop, 240px mobile)

**Section Anchors:**
- Added ID anchors to all CV sections:
  - #education (Training/Formación)
  - #skills (Skills/Competencias)
  - #experience (Experience/Experiencia)
  - #awards (Awards/Premios y Reconocimientos)
  - #courses (Courses/Cursos Realizados)
  - #languages (Languages/Idiomas)
  - #references (References/Referencias)
  - #other (Other/Otros)

**Section Icons:**
- Added descriptive icons to all section titles
- Icons match their purpose (school for education/courses, trophy for awards, etc.)
- Consistent 24x24 size for section titles, 20x20 for menu items

**Smooth Scrolling:**
- Implemented smooth scroll behavior with proper offset calculation
- Accounts for fixed header height
- Added scroll-padding-top for better anchor positioning

**Accessibility:**
- Proper ARIA labels and roles
- aria-expanded attribute for hamburger button
- Keyboard navigation support
- Screen reader friendly

**Styling:**
- Clean white menu background with shadow
- Blue hover states matching CV accent color
- Left border indicator on hover
- Smooth transitions (0.3s ease-in-out)
- Print-friendly (menu hidden in print mode)

**Bilingual Support:**
- Menu items automatically translate based on language
- Works seamlessly with English/Spanish switching
This commit is contained in:
juanatsap
2025-11-07 19:11:21 +00:00
parent 14db8df663
commit 59b95c4448
16 changed files with 257 additions and 18 deletions
+120
View File
@@ -1523,3 +1523,123 @@ a:focus {
text-decoration: underline;
color: #0052a3;
}
/* ===============================================
HAMBURGER MENU & NAVIGATION
=============================================== */
/* Hamburger button */
.hamburger-btn {
background: transparent;
border: none;
color: #fff;
cursor: pointer;
padding: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease;
border-radius: 4px;
margin-right: 0.5rem;
}
.hamburger-btn:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.hamburger-btn:active {
background-color: rgba(255, 255, 255, 0.2);
}
/* Navigation Menu */
.navigation-menu {
position: fixed;
top: 50px; /* Height of action bar */
left: 0;
width: 280px;
max-height: calc(100vh - 50px);
background: #ffffff;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.15);
transform: translateX(-100%);
transition: transform 0.3s ease-in-out;
overflow-y: auto;
z-index: 99;
}
.navigation-menu.menu-open {
transform: translateX(0);
}
.menu-content {
padding: 1rem 0;
}
.menu-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.875rem 1.5rem;
color: var(--text-dark);
text-decoration: none;
transition: background-color 0.2s ease, color 0.2s ease;
font-size: 0.95rem;
font-weight: 500;
border-left: 3px solid transparent;
}
.menu-item:hover {
background-color: rgba(0, 102, 204, 0.08);
color: var(--accent-blue);
border-left-color: var(--accent-blue);
text-decoration: none;
}
.menu-item iconify-icon {
color: var(--text-gray);
flex-shrink: 0;
transition: color 0.2s ease;
}
.menu-item:hover iconify-icon {
color: var(--accent-blue);
}
/* Section icons in titles */
.section-icon {
vertical-align: middle;
margin-right: 0.5rem;
color: var(--accent-blue);
}
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* Add scroll padding to account for fixed header */
html {
scroll-padding-top: 70px; /* Action bar height + some spacing */
}
/* Mobile responsive */
@media (max-width: 768px) {
.navigation-menu {
width: 240px;
}
.menu-item {
padding: 0.75rem 1rem;
font-size: 0.9rem;
}
}
/* Hide menu overlay on print */
@media print {
.navigation-menu {
display: none !important;
}
.hamburger-btn {
display: none !important;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 B

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

+40 -16
View File
@@ -47,8 +47,11 @@
</div>
<!-- Education -->
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Formación{{else}}Training{{end}}</h3>
<section id="education" class="cv-section">
<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}}
</h3>
{{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}})
@@ -57,8 +60,11 @@
</section>
<!-- Skills Summary -->
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Competencias{{else}}Skills{{end}}</h3>
<section id="skills" class="cv-section">
<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}}
</h3>
<p class="summary-text">
{{if eq .Lang "es"}}
Amplio conocimiento en entornos web, tanto J2EE como PHP. Experto en tecnologías front-end, aunque con considerable experiencia en sistemas back-end. Receptivo al aprendizaje de nuevas tecnologías, y con una gran dosis de creatividad. Capacidad de analizar problemas y aportar soluciones específicas adaptadas a cada tipo de cliente. Me gusta trabajar tanto solo como en grupos.
@@ -69,8 +75,11 @@
</section>
<!-- Experience -->
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Experiencia{{else}}Experience{{end}}</h3>
<section id="experience" class="cv-section">
<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}}
</h3>
{{range .CV.Experience}}
<div class="experience-item">
@@ -150,8 +159,11 @@
<main class="cv-main">
<!-- Awards Section -->
{{if .CV.Awards}}
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Premios y Reconocimientos{{else}}Awards{{end}}</h3>
<section id="awards" class="cv-section">
<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}}
</h3>
{{range .CV.Awards}}
<div class="award-item">
{{if .AwardLogo}}
@@ -182,8 +194,11 @@
<!-- Courses Section -->
{{if .CV.Courses}}
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Cursos Realizados{{else}}Courses{{end}}</h3>
<section id="courses" class="cv-section">
<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}}
</h3>
{{range .CV.Courses}}
<div class="course-item">
<div class="course-icon">
@@ -222,8 +237,11 @@
{{end}}
<!-- Languages Section -->
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Idiomas{{else}}Languages{{end}}</h3>
<section id="languages" class="cv-section">
<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}}
</h3>
{{range .CV.Languages}}
<div class="language-item">
<strong>{{.Language}}:</strong> {{.Proficiency}}{{if .Detail}} {{.Detail}}{{end}}
@@ -233,8 +251,11 @@
<!-- References Section -->
{{if .CV.References}}
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Referencias{{else}}References{{end}}</h3>
<section id="references" class="cv-section">
<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}}
</h3>
{{range .CV.References}}
<div class="reference-item">
{{if .TextBefore}}{{.TextBefore}} {{end}}<a href="{{.URL}}" target="_blank" rel="noopener noreferrer"><strong>{{if .LinkText}}{{.LinkText}}{{else}}{{.Title}}{{end}}</strong></a>{{if .TextAfter}} {{.TextAfter}}{{end}}
@@ -245,8 +266,11 @@
<!-- Other Section (Driver's License) -->
{{if .CV.Other.DriverLicense}}
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Otros{{else}}Other{{end}}</h3>
<section id="other" class="cv-section">
<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}}
</h3>
<div class="other-content">
{{if eq .Lang "es"}}Carnet de conducir tipo {{.CV.Other.DriverLicense}}{{else}}Driving License type {{.CV.Other.DriverLicense}}{{end}}
</div>
+97 -2
View File
@@ -97,10 +97,15 @@
<!-- 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: Site Title and Language -->
<!-- Left: Hamburger Menu + Site Title and Language -->
<div class="site-title">
<!-- Hamburger Menu Button -->
<button class="hamburger-btn" onclick="toggleMenu()" aria-label="Toggle navigation menu">
<iconify-icon icon="mdi:menu" width="24" height="24"></iconify-icon>
</button>
<iconify-icon icon="mdi:file-account" width="24" height="24" class="site-icon"></iconify-icon>
<span class="site-title-text">CV {{.CurrentYear}} - JAMR</span>
<span class="site-title-text">CV JAMR - {{.CurrentYear}}</span>
<!-- Language selector (after title) -->
<div class="language-selector">
@@ -181,6 +186,44 @@
</div>
</div>
<!-- Navigation Menu (Hidden by default) -->
<nav id="navigation-menu" class="navigation-menu no-print" role="navigation" aria-label="CV sections">
<div class="menu-content">
<a href="#education" class="menu-item" onclick="scrollToSection('education')">
<iconify-icon icon="mdi:school" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Formación{{else}}Training{{end}}</span>
</a>
<a href="#skills" class="menu-item" onclick="scrollToSection('skills')">
<iconify-icon icon="mdi:brain" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Competencias{{else}}Skills{{end}}</span>
</a>
<a href="#experience" class="menu-item" onclick="scrollToSection('experience')">
<iconify-icon icon="mdi:office-building" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Experiencia{{else}}Experience{{end}}</span>
</a>
<a href="#awards" class="menu-item" onclick="scrollToSection('awards')">
<iconify-icon icon="mdi:trophy" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Premios y Reconocimientos{{else}}Awards{{end}}</span>
</a>
<a href="#courses" class="menu-item" onclick="scrollToSection('courses')">
<iconify-icon icon="mdi:school" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Cursos Realizados{{else}}Courses{{end}}</span>
</a>
<a href="#languages" class="menu-item" onclick="scrollToSection('languages')">
<iconify-icon icon="mdi:translate" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Idiomas{{else}}Languages{{end}}</span>
</a>
<a href="#references" class="menu-item" onclick="scrollToSection('references')">
<iconify-icon icon="mdi:link-variant" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Referencias{{else}}References{{end}}</span>
</a>
<a href="#other" class="menu-item" onclick="scrollToSection('other')">
<iconify-icon icon="mdi:information" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Otros{{else}}Other{{end}}</span>
</a>
</div>
</nav>
<!-- CV Content Container -->
<div class="cv-container">
<main id="cv-content"
@@ -205,6 +248,58 @@
</div>
<script>
// Toggle navigation menu
function toggleMenu() {
const menu = document.getElementById('navigation-menu');
const btn = document.querySelector('.hamburger-btn');
if (menu.classList.contains('menu-open')) {
menu.classList.remove('menu-open');
btn.setAttribute('aria-expanded', 'false');
} else {
menu.classList.add('menu-open');
btn.setAttribute('aria-expanded', 'true');
}
}
// Scroll to section smoothly
function scrollToSection(sectionId) {
event.preventDefault(); // Prevent default anchor behavior
const section = document.getElementById(sectionId);
if (section) {
const actionBarHeight = document.querySelector('.action-bar').offsetHeight;
const menuHeight = document.querySelector('.navigation-menu').offsetHeight;
const offset = actionBarHeight + (menuHeight || 0) + 20; // Add 20px padding
const elementPosition = section.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - offset;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
// Close menu after clicking
const menu = document.getElementById('navigation-menu');
menu.classList.remove('menu-open');
document.querySelector('.hamburger-btn').setAttribute('aria-expanded', 'false');
}
}
// Close menu when clicking outside
document.addEventListener('click', function(event) {
const menu = document.getElementById('navigation-menu');
const btn = document.querySelector('.hamburger-btn');
if (menu && btn && menu.classList.contains('menu-open')) {
if (!menu.contains(event.target) && !btn.contains(event.target)) {
menu.classList.remove('menu-open');
btn.setAttribute('aria-expanded', 'false');
}
}
});
function selectLanguage(lang) {
// Update button states
document.querySelectorAll('.language-selector .selector-btn').forEach(btn => {