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"`
|
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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user