From aae1a28627e16edc6645549fd9e0d3d350805fb7 Mon Sep 17 00:00:00 2001 From: juanatsap Date: Mon, 4 May 2026 15:23:15 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Cmd+K=20search=20=E2=80=94=20rich=20key?= =?UTF-8?q?words,=20real=20logos,=20category=20icons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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.) --- internal/handlers/cv_cmdk.go | 49 ++++++++++++++++++++++++++++++------ static/js/ninja-keys-init.js | 46 +++++++++++++++++---------------- 2 files changed, 67 insertions(+), 28 deletions(-) diff --git a/internal/handlers/cv_cmdk.go b/internal/handlers/cv_cmdk.go index ffd6f29..d876c01 100644 --- a/internal/handlers/cv_cmdk.go +++ b/internal/handlers/cv_cmdk.go @@ -15,6 +15,8 @@ type CmdKAction struct { Title string `json:"title"` Section string `json:"section"` Keywords string `json:"keywords"` + Category string `json:"category,omitempty"` // cli, app, web, plugin, sdk, contrib + Icon string `json:"icon,omitempty"` // Project logo filename } // CmdKResponse represents the response for the CMD+K API endpoint @@ -48,43 +50,76 @@ func (h *CVHandler) CmdKData(w http.ResponseWriter, r *http.Request) { // Map experiences for _, exp := range cv.Experience { if exp.CompanyID == "" { - continue // Skip entries without ID + continue + } + keywords := exp.Company + " " + exp.Position + for _, tech := range exp.Technologies { + keywords += " " + tech + } + keywords += " work job career" + icon := "" + if exp.CompanyLogo != "" { + icon = exp.CompanyLogo } response.Experiences = append(response.Experiences, CmdKAction{ ID: "exp-" + exp.CompanyID, - Title: exp.Company, + Title: exp.Company + " — " + exp.Position, Section: "Experience", - Keywords: exp.Company + " " + exp.Position, + Keywords: keywords, + Icon: icon, }) } // Map projects for _, proj := range cv.Projects { if proj.ProjectID == "" { - continue // Skip entries without ID + continue } title := proj.ProjectName if title == "" { title = proj.Title } + keywords := title + " " + proj.ShortDescription + for _, tech := range proj.Technologies { + keywords += " " + tech + } + if proj.OpenSource { + keywords += " open source open-source oss github" + } + if proj.Category != "" { + keywords += " " + proj.Category + } + icon := "" + if proj.ProjectLogo != "" { + icon = proj.ProjectLogo + } response.Projects = append(response.Projects, CmdKAction{ ID: "proj-" + proj.ProjectID, Title: title, Section: "Projects", - Keywords: title + " " + proj.ShortDescription, + Keywords: keywords, + Category: proj.Category, + Icon: icon, }) } // Map courses for _, course := range cv.Courses { if course.CourseID == "" { - continue // Skip entries without ID + continue + } + keywords := course.Title + " " + course.Institution + keywords += " course training certification" + icon := "" + if course.CourseLogo != "" { + icon = course.CourseLogo } response.Courses = append(response.Courses, CmdKAction{ ID: "course-" + course.CourseID, Title: course.Title, Section: "Courses", - Keywords: course.Title + " " + course.Institution, + Keywords: keywords, + Icon: icon, }) } diff --git a/static/js/ninja-keys-init.js b/static/js/ninja-keys-init.js index 37059d5..7265b81 100644 --- a/static/js/ninja-keys-init.js +++ b/static/js/ninja-keys-init.js @@ -111,50 +111,54 @@ } } - /** - * Convert API experience entries to ninja-keys actions - * @param {Array} experiences - Experience entries from API - * @returns {Array} ninja-keys actions - */ + /** 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 ``; + } + return ``; + } + function mapExperienceActions(experiences) { return experiences.map(exp => ({ id: exp.id, title: exp.title, section: exp.section, - keywords: `${exp.keywords} work job career`.toLowerCase(), - icon: '', + keywords: exp.keywords.toLowerCase(), + icon: makeIcon(exp.icon, 'companies', 'mdi:office-building'), 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: '', + keywords: proj.keywords.toLowerCase(), + icon: makeIcon(proj.icon, 'projects', categoryIcons[proj.category] || 'mdi:web'), 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: '', + keywords: course.keywords.toLowerCase(), + icon: makeIcon(course.icon, 'courses', 'mdi:school'), handler: () => scrollToSection(course.id) })); }