Files
cv-site/static/js/ninja-keys-init.js
juanatsap aae1a28627 feat: Cmd+K search — rich keywords, real logos, category icons
- Projects searchable by technologies, "open source", category (cli/app/web)
- Experience searchable by technologies, shows company logo + position
- Courses show institution logos
- Project logos used as icons instead of generic mdi:web
- Category-specific fallback icons (console, apple, puzzle, etc.)
2026-05-04 15:23:15 +01:00

504 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();
// Dispatch 'show' event for hyperscript handlers
modal.dispatchEvent(new CustomEvent('show'));
}
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: [] };
}
}
/** Category icon mapping */
const categoryIcons = {
cli: 'mdi:console',
app: 'mdi:apple',
web: 'mdi:web',
webapp: 'mdi:web',
plugin: 'mdi:puzzle',
sdk: 'mdi:package-variant',
contrib: 'mdi:source-pull'
};
/** Build icon HTML — use project logo if available, fallback to iconify */
function makeIcon(logoFile, folder, fallbackIcon) {
if (logoFile) {
return `<img src="/static/images/${folder}/${logoFile}" style="width:20px;height:20px;object-fit:contain;border-radius:3px" alt="">`;
}
return `<iconify-icon icon="${fallbackIcon}" width="20"></iconify-icon>`;
}
function mapExperienceActions(experiences) {
return experiences.map(exp => ({
id: exp.id,
title: exp.title,
section: exp.section,
keywords: exp.keywords.toLowerCase(),
icon: makeIcon(exp.icon, 'companies', 'mdi:office-building'),
handler: () => scrollToSection(exp.id)
}));
}
function mapProjectActions(projects) {
return projects.map(proj => ({
id: proj.id,
title: proj.title,
section: proj.section,
keywords: proj.keywords.toLowerCase(),
icon: makeIcon(proj.icon, 'projects', categoryIcons[proj.category] || 'mdi:web'),
handler: () => scrollToSection(proj.id)
}));
}
function mapCourseActions(courses) {
return courses.map(course => ({
id: course.id,
title: course.title,
section: course.section,
keywords: course.keywords.toLowerCase(),
icon: makeIcon(course.icon, 'courses', 'mdi:school'),
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)`);
}
})();