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:
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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 `<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} work job career`.toLowerCase(),
|
||||
icon: '<iconify-icon icon="mdi:office-building" width="20"></iconify-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: '<iconify-icon icon="mdi:web" width="20"></iconify-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: '<iconify-icon icon="mdi:school" width="20"></iconify-icon>',
|
||||
keywords: course.keywords.toLowerCase(),
|
||||
icon: makeIcon(course.icon, 'courses', 'mdi:school'),
|
||||
handler: () => scrollToSection(course.id)
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user