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.)
This commit is contained in:
juanatsap
2026-05-04 15:23:15 +01:00
parent b9db689981
commit aae1a28627
2 changed files with 67 additions and 28 deletions
+42 -7
View File
@@ -15,6 +15,8 @@ type CmdKAction struct {
Title string `json:"title"` Title string `json:"title"`
Section string `json:"section"` Section string `json:"section"`
Keywords string `json:"keywords"` 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 // 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 // Map experiences
for _, exp := range cv.Experience { for _, exp := range cv.Experience {
if exp.CompanyID == "" { 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{ response.Experiences = append(response.Experiences, CmdKAction{
ID: "exp-" + exp.CompanyID, ID: "exp-" + exp.CompanyID,
Title: exp.Company, Title: exp.Company + " — " + exp.Position,
Section: "Experience", Section: "Experience",
Keywords: exp.Company + " " + exp.Position, Keywords: keywords,
Icon: icon,
}) })
} }
// Map projects // Map projects
for _, proj := range cv.Projects { for _, proj := range cv.Projects {
if proj.ProjectID == "" { if proj.ProjectID == "" {
continue // Skip entries without ID continue
} }
title := proj.ProjectName title := proj.ProjectName
if title == "" { if title == "" {
title = proj.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{ response.Projects = append(response.Projects, CmdKAction{
ID: "proj-" + proj.ProjectID, ID: "proj-" + proj.ProjectID,
Title: title, Title: title,
Section: "Projects", Section: "Projects",
Keywords: title + " " + proj.ShortDescription, Keywords: keywords,
Category: proj.Category,
Icon: icon,
}) })
} }
// Map courses // Map courses
for _, course := range cv.Courses { for _, course := range cv.Courses {
if course.CourseID == "" { 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{ response.Courses = append(response.Courses, CmdKAction{
ID: "course-" + course.CourseID, ID: "course-" + course.CourseID,
Title: course.Title, Title: course.Title,
Section: "Courses", Section: "Courses",
Keywords: course.Title + " " + course.Institution, Keywords: keywords,
Icon: icon,
}) })
} }
+25 -21
View File
@@ -111,50 +111,54 @@
} }
} }
/** /** Category icon mapping */
* Convert API experience entries to ninja-keys actions const categoryIcons = {
* @param {Array} experiences - Experience entries from API cli: 'mdi:console',
* @returns {Array} ninja-keys actions 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) { function mapExperienceActions(experiences) {
return experiences.map(exp => ({ return experiences.map(exp => ({
id: exp.id, id: exp.id,
title: exp.title, title: exp.title,
section: exp.section, section: exp.section,
keywords: `${exp.keywords} work job career`.toLowerCase(), keywords: exp.keywords.toLowerCase(),
icon: '<iconify-icon icon="mdi:office-building" width="20"></iconify-icon>', icon: makeIcon(exp.icon, 'companies', 'mdi:office-building'),
handler: () => scrollToSection(exp.id) 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) { function mapProjectActions(projects) {
return projects.map(proj => ({ return projects.map(proj => ({
id: proj.id, id: proj.id,
title: proj.title, title: proj.title,
section: proj.section, section: proj.section,
keywords: `${proj.keywords} project website app`.toLowerCase(), keywords: proj.keywords.toLowerCase(),
icon: '<iconify-icon icon="mdi:web" width="20"></iconify-icon>', icon: makeIcon(proj.icon, 'projects', categoryIcons[proj.category] || 'mdi:web'),
handler: () => scrollToSection(proj.id) 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) { function mapCourseActions(courses) {
return courses.map(course => ({ return courses.map(course => ({
id: course.id, id: course.id,
title: course.title, title: course.title,
section: course.section, section: course.section,
keywords: `${course.keywords} course training certification`.toLowerCase(), keywords: course.keywords.toLowerCase(),
icon: '<iconify-icon icon="mdi:school" width="20"></iconify-icon>', icon: makeIcon(course.icon, 'courses', 'mdi:school'),
handler: () => scrollToSection(course.id) handler: () => scrollToSection(course.id)
})); }));
} }