9a848e8c53
Implement a command palette accessible via CMD+K/Ctrl+K using the ninja-keys web component. Features include: - New /api/cmd-k endpoint serving dynamic CV entries (experiences, projects, courses) - Language-aware responses with 1-hour cache headers - Scroll-to-section functionality for quick navigation - Enhanced keyboard shortcuts modal with CMD+K documentation - Comprehensive test coverage for API and UI interactions Also includes cleanup of deprecated debug test files and various UI polish improvements to contact form, themes, and action bar components.
498 lines
17 KiB
JavaScript
498 lines
17 KiB
JavaScript
/**
|
|
* Ninja Keys Initialization
|
|
* CMD+K command palette for CV site navigation and actions
|
|
*
|
|
* Dynamic entries are fetched from the backend API:
|
|
* GET /api/cmd-k?lang={en|es}
|
|
* Returns: { experiences: [], projects: [], courses: [] }
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
// Wait for DOM to be ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', initNinjaKeys);
|
|
} else {
|
|
initNinjaKeys();
|
|
}
|
|
|
|
async function initNinjaKeys() {
|
|
const ninjaKeys = document.getElementById('cmd-k-bar');
|
|
if (!ninjaKeys) {
|
|
console.warn('ninja-keys element not found');
|
|
return;
|
|
}
|
|
|
|
// Get current language from HTML lang attribute
|
|
const lang = document.documentElement.lang || 'en';
|
|
const currentYear = new Date().getFullYear();
|
|
|
|
/**
|
|
* Smooth scroll to section
|
|
* @param {string} sectionId - The ID of the section to scroll to
|
|
*/
|
|
function scrollToSection(sectionId) {
|
|
const section = document.getElementById(sectionId);
|
|
if (section) {
|
|
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
ninjaKeys.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open a modal dialog
|
|
* @param {string} modalId - The ID of the modal to open
|
|
*/
|
|
function openModal(modalId) {
|
|
const modal = document.getElementById(modalId);
|
|
if (modal && modal.showModal) {
|
|
modal.showModal();
|
|
}
|
|
ninjaKeys.close();
|
|
}
|
|
|
|
/**
|
|
* Trigger a toggle checkbox click
|
|
* @param {string} toggleId - The ID of the toggle to click
|
|
*/
|
|
function clickToggle(toggleId) {
|
|
const toggle = document.getElementById(toggleId);
|
|
if (toggle) {
|
|
toggle.checked = !toggle.checked;
|
|
toggle.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
ninjaKeys.close();
|
|
}
|
|
|
|
/**
|
|
* Download file with specific parameters
|
|
* @param {string} url - The URL to navigate to or download from
|
|
* @param {boolean} newTab - Whether to open in new tab
|
|
*/
|
|
function downloadFile(url, newTab = false) {
|
|
if (newTab) {
|
|
window.open(url, '_blank');
|
|
} else {
|
|
window.location.href = url;
|
|
}
|
|
ninjaKeys.close();
|
|
}
|
|
|
|
/**
|
|
* Open external link
|
|
* @param {string} url - The URL to open
|
|
*/
|
|
function openLink(url) {
|
|
window.open(url, '_blank', 'noopener,noreferrer');
|
|
ninjaKeys.close();
|
|
}
|
|
|
|
// ========================================================================
|
|
// DYNAMIC ENTRIES - Fetched from API
|
|
// ========================================================================
|
|
|
|
/**
|
|
* Fetch dynamic entries from the backend API
|
|
* @returns {Promise<{experiences: Array, projects: Array, courses: Array}>}
|
|
*/
|
|
async function fetchDynamicEntries() {
|
|
try {
|
|
const response = await fetch(`/api/cmd-k?lang=${lang}`);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('Failed to fetch CMD+K data:', error);
|
|
return { experiences: [], projects: [], courses: [] };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert API experience entries to ninja-keys actions
|
|
* @param {Array} experiences - Experience entries from API
|
|
* @returns {Array} ninja-keys actions
|
|
*/
|
|
function mapExperienceActions(experiences) {
|
|
return experiences.map(exp => ({
|
|
id: exp.id,
|
|
title: exp.title,
|
|
section: exp.section,
|
|
keywords: `${exp.keywords} work job career`.toLowerCase(),
|
|
icon: '<iconify-icon icon="mdi:office-building" width="20"></iconify-icon>',
|
|
handler: () => scrollToSection(exp.id)
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Convert API project entries to ninja-keys actions
|
|
* @param {Array} projects - Project entries from API
|
|
* @returns {Array} ninja-keys actions
|
|
*/
|
|
function mapProjectActions(projects) {
|
|
return projects.map(proj => ({
|
|
id: proj.id,
|
|
title: proj.title,
|
|
section: proj.section,
|
|
keywords: `${proj.keywords} project website app`.toLowerCase(),
|
|
icon: '<iconify-icon icon="mdi:web" width="20"></iconify-icon>',
|
|
handler: () => scrollToSection(proj.id)
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Convert API course entries to ninja-keys actions
|
|
* @param {Array} courses - Course entries from API
|
|
* @returns {Array} ninja-keys actions
|
|
*/
|
|
function mapCourseActions(courses) {
|
|
return courses.map(course => ({
|
|
id: course.id,
|
|
title: course.title,
|
|
section: course.section,
|
|
keywords: `${course.keywords} course training certification`.toLowerCase(),
|
|
icon: '<iconify-icon icon="mdi:school" width="20"></iconify-icon>',
|
|
handler: () => scrollToSection(course.id)
|
|
}));
|
|
}
|
|
|
|
// ========================================================================
|
|
// STATIC ACTIONS - Navigation, Shortcuts, Downloads, etc.
|
|
// ========================================================================
|
|
|
|
const staticActions = [
|
|
// ============================================
|
|
// NAVIGATION SECTION
|
|
// ============================================
|
|
{
|
|
id: 'nav-top',
|
|
title: 'Jump to Top',
|
|
section: 'Navigation',
|
|
keywords: 'scroll up beginning start',
|
|
icon: '<iconify-icon icon="mdi:arrow-up-bold" width="20"></iconify-icon>',
|
|
handler: () => {
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
ninjaKeys.close();
|
|
}
|
|
},
|
|
{
|
|
id: 'nav-experience',
|
|
title: 'Jump to Experience',
|
|
section: 'Navigation',
|
|
keywords: 'work jobs career employment',
|
|
icon: '<iconify-icon icon="mdi:office-building" width="20"></iconify-icon>',
|
|
handler: () => scrollToSection('experience')
|
|
},
|
|
{
|
|
id: 'nav-education',
|
|
title: 'Jump to Education',
|
|
section: 'Navigation',
|
|
keywords: 'university degree school college',
|
|
icon: '<iconify-icon icon="mdi:school" width="20"></iconify-icon>',
|
|
handler: () => scrollToSection('education')
|
|
},
|
|
{
|
|
id: 'nav-skills',
|
|
title: 'Jump to Skills',
|
|
section: 'Navigation',
|
|
keywords: 'technologies abilities competencies',
|
|
icon: '<iconify-icon icon="mdi:brain" width="20"></iconify-icon>',
|
|
handler: () => scrollToSection('skills')
|
|
},
|
|
{
|
|
id: 'nav-projects',
|
|
title: 'Jump to Projects',
|
|
section: 'Navigation',
|
|
keywords: 'portfolio websites apps',
|
|
icon: '<iconify-icon icon="mdi:web" width="20"></iconify-icon>',
|
|
handler: () => scrollToSection('projects')
|
|
},
|
|
{
|
|
id: 'nav-courses',
|
|
title: 'Jump to Courses',
|
|
section: 'Navigation',
|
|
keywords: 'training certifications learning',
|
|
icon: '<iconify-icon icon="mdi:book-open-page-variant" width="20"></iconify-icon>',
|
|
handler: () => scrollToSection('courses')
|
|
},
|
|
{
|
|
id: 'nav-languages',
|
|
title: 'Jump to Languages',
|
|
section: 'Navigation',
|
|
keywords: 'spanish english portuguese',
|
|
icon: '<iconify-icon icon="mdi:translate" width="20"></iconify-icon>',
|
|
handler: () => scrollToSection('languages')
|
|
},
|
|
{
|
|
id: 'nav-awards',
|
|
title: 'Jump to Awards',
|
|
section: 'Navigation',
|
|
keywords: 'achievements recognition prizes',
|
|
icon: '<iconify-icon icon="mdi:trophy" width="20"></iconify-icon>',
|
|
handler: () => scrollToSection('awards')
|
|
},
|
|
{
|
|
id: 'nav-other',
|
|
title: 'Jump to Other Info',
|
|
section: 'Navigation',
|
|
keywords: 'additional references personal',
|
|
icon: '<iconify-icon icon="mdi:information" width="20"></iconify-icon>',
|
|
handler: () => scrollToSection('other')
|
|
},
|
|
|
|
// ============================================
|
|
// SOCIAL LINKS
|
|
// ============================================
|
|
{
|
|
id: 'social-linkedin',
|
|
title: 'Open LinkedIn Profile',
|
|
section: 'Social',
|
|
keywords: 'linkedin professional network connect',
|
|
icon: '<iconify-icon icon="mdi:linkedin" width="20"></iconify-icon>',
|
|
handler: () => openLink('https://www.linkedin.com/in/juan-andres-moreno-rubio')
|
|
},
|
|
{
|
|
id: 'social-github',
|
|
title: 'Open GitHub Profile',
|
|
section: 'Social',
|
|
keywords: 'github code repositories open source',
|
|
icon: '<iconify-icon icon="mdi:github" width="20"></iconify-icon>',
|
|
handler: () => openLink('https://github.com/juanatsap')
|
|
},
|
|
{
|
|
id: 'social-domestika',
|
|
title: 'Open Domestika Portfolio',
|
|
section: 'Social',
|
|
keywords: 'domestika portfolio design creative',
|
|
icon: '<iconify-icon icon="mdi:palette" width="20"></iconify-icon>',
|
|
handler: () => openLink('https://www.domestika.org/es/txeo/portfolio')
|
|
},
|
|
{
|
|
id: 'social-website',
|
|
title: 'Open Personal Website',
|
|
section: 'Social',
|
|
keywords: 'website personal portfolio cv',
|
|
icon: '<iconify-icon icon="mdi:web" width="20"></iconify-icon>',
|
|
handler: () => openLink('https://juan.andres.morenorub.io')
|
|
},
|
|
|
|
// ============================================
|
|
// KEYBOARD SHORTCUTS SECTION
|
|
// ============================================
|
|
{
|
|
id: 'shortcut-length',
|
|
title: 'Toggle CV Length',
|
|
hotkey: 'l',
|
|
section: 'Shortcuts',
|
|
keywords: 'short long extended compact full',
|
|
icon: '<iconify-icon icon="mdi:text-short" width="20"></iconify-icon>',
|
|
handler: () => clickToggle('lengthToggle')
|
|
},
|
|
{
|
|
id: 'shortcut-icons',
|
|
title: 'Toggle Icons Visibility',
|
|
hotkey: 'i',
|
|
section: 'Shortcuts',
|
|
keywords: 'icons show hide emoji',
|
|
icon: '<iconify-icon icon="mdi:emoticon-outline" width="20"></iconify-icon>',
|
|
handler: () => clickToggle('iconToggle')
|
|
},
|
|
{
|
|
id: 'shortcut-theme',
|
|
title: 'Toggle Visual Theme',
|
|
hotkey: 'v',
|
|
section: 'Shortcuts',
|
|
keywords: 'theme clean default style visual',
|
|
icon: '<iconify-icon icon="mdi:palette-outline" width="20"></iconify-icon>',
|
|
handler: () => clickToggle('themeToggle')
|
|
},
|
|
{
|
|
id: 'shortcut-help',
|
|
title: 'Show Shortcuts Help',
|
|
hotkey: '?',
|
|
section: 'Shortcuts',
|
|
keywords: 'help shortcuts keyboard keys',
|
|
icon: '<iconify-icon icon="mdi:help-circle-outline" width="20"></iconify-icon>',
|
|
handler: () => openModal('shortcuts-modal')
|
|
},
|
|
{
|
|
id: 'shortcut-print',
|
|
title: 'Print CV',
|
|
hotkey: 'mod+p',
|
|
section: 'Shortcuts',
|
|
keywords: 'print pdf paper',
|
|
icon: '<iconify-icon icon="mdi:printer" width="20"></iconify-icon>',
|
|
handler: () => {
|
|
ninjaKeys.close();
|
|
setTimeout(() => window.print(), 100);
|
|
}
|
|
},
|
|
|
|
// ============================================
|
|
// PDF DOWNLOADS SECTION
|
|
// ============================================
|
|
{
|
|
id: 'download-pdf-default',
|
|
title: 'Download PDF (Default - 5 pages)',
|
|
section: 'Downloads',
|
|
keywords: 'pdf download default recommended',
|
|
icon: '<iconify-icon icon="catppuccin:pdf" width="20"></iconify-icon>',
|
|
handler: () => downloadFile(`/cv-jamr-${currentYear}-${lang}.pdf`)
|
|
},
|
|
{
|
|
id: 'download-pdf-short',
|
|
title: 'Download PDF (Short - 4 pages)',
|
|
section: 'Downloads',
|
|
keywords: 'pdf download short compact brief',
|
|
icon: '<iconify-icon icon="mdi:file-pdf-box" width="20"></iconify-icon>',
|
|
handler: () => downloadFile(`/export/pdf?lang=${lang}&length=short&icons=show&version=clean`)
|
|
},
|
|
{
|
|
id: 'download-pdf-extended',
|
|
title: 'Download PDF (Extended - 9 pages)',
|
|
section: 'Downloads',
|
|
keywords: 'pdf download extended long full complete',
|
|
icon: '<iconify-icon icon="mdi:book-open-variant" width="20"></iconify-icon>',
|
|
handler: () => downloadFile(`/export/pdf?lang=${lang}&length=long&icons=show&version=with_skills`)
|
|
},
|
|
{
|
|
id: 'open-pdf-modal',
|
|
title: 'Open PDF Options',
|
|
section: 'Downloads',
|
|
keywords: 'pdf options modal choose select',
|
|
icon: '<iconify-icon icon="mdi:file-cog" width="20"></iconify-icon>',
|
|
handler: () => openModal('pdf-modal')
|
|
},
|
|
|
|
// ============================================
|
|
// TEXT CV SECTION
|
|
// ============================================
|
|
{
|
|
id: 'view-text-cv',
|
|
title: 'View Text CV (Plain Text)',
|
|
section: 'Downloads',
|
|
keywords: 'text plain txt view terminal cli',
|
|
icon: '<iconify-icon icon="mdi:file-document-outline" width="20"></iconify-icon>',
|
|
handler: () => downloadFile(`/text?lang=${lang}`, true)
|
|
},
|
|
{
|
|
id: 'download-text-cv',
|
|
title: 'Download Text CV (.txt)',
|
|
section: 'Downloads',
|
|
keywords: 'text download txt file save',
|
|
icon: '<iconify-icon icon="mdi:download" width="20"></iconify-icon>',
|
|
handler: () => downloadFile(`/text?lang=${lang}&download=true`)
|
|
},
|
|
|
|
// ============================================
|
|
// ACTIONS SECTION
|
|
// ============================================
|
|
{
|
|
id: 'action-contact',
|
|
title: 'Open Contact Form',
|
|
section: 'Actions',
|
|
keywords: 'contact email message send hire',
|
|
icon: '<iconify-icon icon="mdi:email-outline" width="20"></iconify-icon>',
|
|
handler: () => openModal('contact-modal')
|
|
},
|
|
{
|
|
id: 'action-info',
|
|
title: 'Show Site Info',
|
|
section: 'Actions',
|
|
keywords: 'info about site technology stack',
|
|
icon: '<iconify-icon icon="mdi:information-outline" width="20"></iconify-icon>',
|
|
handler: () => openModal('info-modal')
|
|
},
|
|
{
|
|
id: 'action-zoom',
|
|
title: 'Toggle Zoom Controls',
|
|
section: 'Actions',
|
|
keywords: 'zoom magnify scale size',
|
|
icon: '<iconify-icon icon="mdi:magnify-plus-outline" width="20"></iconify-icon>',
|
|
handler: () => {
|
|
const zoomControl = document.getElementById('zoom-control');
|
|
if (zoomControl) {
|
|
const isVisible = zoomControl.style.display !== 'none';
|
|
zoomControl.style.display = isVisible ? 'none' : 'block';
|
|
}
|
|
ninjaKeys.close();
|
|
}
|
|
},
|
|
{
|
|
id: 'action-language-en',
|
|
title: 'Switch to English',
|
|
section: 'Actions',
|
|
keywords: 'english language en switch',
|
|
icon: '<iconify-icon icon="emojione:flag-for-united-kingdom" width="20"></iconify-icon>',
|
|
handler: () => {
|
|
if (lang !== 'en') {
|
|
window.location.href = '/?lang=en';
|
|
}
|
|
ninjaKeys.close();
|
|
}
|
|
},
|
|
{
|
|
id: 'action-language-es',
|
|
title: 'Switch to Spanish',
|
|
section: 'Actions',
|
|
keywords: 'spanish espanol language es switch cambiar idioma',
|
|
icon: '<iconify-icon icon="emojione:flag-for-spain" width="20"></iconify-icon>',
|
|
handler: () => {
|
|
if (lang !== 'es') {
|
|
window.location.href = '/?lang=es';
|
|
}
|
|
ninjaKeys.close();
|
|
}
|
|
},
|
|
{
|
|
id: 'action-color-theme',
|
|
title: 'Change Color Theme',
|
|
section: 'Actions',
|
|
keywords: 'dark light color theme mode',
|
|
icon: '<iconify-icon icon="mdi:theme-light-dark" width="20"></iconify-icon>',
|
|
handler: () => {
|
|
const colorSwitcher = document.querySelector('.color-theme-switcher');
|
|
if (colorSwitcher) {
|
|
colorSwitcher.click();
|
|
}
|
|
ninjaKeys.close();
|
|
}
|
|
}
|
|
];
|
|
|
|
// ========================================================================
|
|
// BUILD FINAL ACTIONS ARRAY
|
|
// ========================================================================
|
|
|
|
// Fetch dynamic entries from API and combine with static actions
|
|
const dynamicData = await fetchDynamicEntries();
|
|
|
|
const actions = [
|
|
...staticActions,
|
|
...mapExperienceActions(dynamicData.experiences || []),
|
|
...mapProjectActions(dynamicData.projects || []),
|
|
...mapCourseActions(dynamicData.courses || [])
|
|
];
|
|
|
|
// Assign actions to ninja-keys
|
|
ninjaKeys.data = actions;
|
|
|
|
// Listen for ninja-keys events
|
|
ninjaKeys.addEventListener('selected', (event) => {
|
|
console.log('Ninja Keys: Selected action', event.detail);
|
|
});
|
|
|
|
// Apply custom styling
|
|
ninjaKeys.style.setProperty('--ninja-z-index', '10000');
|
|
ninjaKeys.style.setProperty('--ninja-accent-color', '#667eea');
|
|
ninjaKeys.style.setProperty('--ninja-font-family', 'Quicksand, sans-serif');
|
|
|
|
// Log counts for debugging
|
|
const expCount = (dynamicData.experiences || []).length;
|
|
const projCount = (dynamicData.projects || []).length;
|
|
const courseCount = (dynamicData.courses || []).length;
|
|
console.log(`Ninja Keys initialized with ${actions.length} actions (${expCount} experiences, ${projCount} projects, ${courseCount} courses)`);
|
|
}
|
|
})();
|