aae1a28627
- 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.)
504 lines
17 KiB
JavaScript
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)`);
|
|
}
|
|
})();
|