9a848e8c53
Implement a command palette accessible via CMD+K/Ctrl+K using the ninja-keys web component. Features include: - New /api/cmd-k endpoint serving dynamic CV entries (experiences, projects, courses) - Language-aware responses with 1-hour cache headers - Scroll-to-section functionality for quick navigation - Enhanced keyboard shortcuts modal with CMD+K documentation - Comprehensive test coverage for API and UI interactions Also includes cleanup of deprecated debug test files and various UI polish improvements to contact form, themes, and action bar components.
304 lines
10 KiB
Go
304 lines
10 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// CV represents the complete curriculum vitae structure
|
|
type CV struct {
|
|
Personal Personal `json:"personal"`
|
|
Summary string `json:"summary"`
|
|
Experience []Experience `json:"experience"`
|
|
Education []Education `json:"education"`
|
|
Skills Skills `json:"skills"`
|
|
Languages []Language `json:"languages"`
|
|
Projects []Project `json:"projects"`
|
|
Awards []Award `json:"awards"`
|
|
Certifications []Certification `json:"certifications"`
|
|
Courses []Course `json:"courses"`
|
|
References []Reference `json:"references"`
|
|
Other Other `json:"other"`
|
|
Meta Meta `json:"meta"`
|
|
}
|
|
|
|
type Personal struct {
|
|
Name string `json:"name"`
|
|
Title string `json:"title"`
|
|
Location string `json:"location"`
|
|
Email string `json:"email"`
|
|
Phone string `json:"phone"`
|
|
DateOfBirth string `json:"dateOfBirth"`
|
|
PlaceOfBirth string `json:"placeOfBirth"`
|
|
Citizenship string `json:"citizenship"`
|
|
LinkedIn string `json:"linkedin"`
|
|
GitHub string `json:"github"`
|
|
Domestika string `json:"domestika"`
|
|
Website string `json:"website"`
|
|
Photo string `json:"photo"`
|
|
}
|
|
|
|
type Experience struct {
|
|
Position string `json:"position"`
|
|
Company string `json:"company"`
|
|
CompanyID string `json:"companyID,omitempty"` // Unique ID for scrolling/navigation
|
|
CompanyURL string `json:"companyURL,omitempty"` // Optional URL for company website
|
|
CompanyLogo string `json:"companyLogo"`
|
|
Location string `json:"location"`
|
|
StartDate string `json:"startDate"`
|
|
EndDate string `json:"endDate"`
|
|
Current bool `json:"current"`
|
|
Expired bool `json:"expired,omitempty"` // Optional flag for expired companies
|
|
ShortDescription string `json:"shortDescription"`
|
|
Responsibilities []string `json:"responsibilities"`
|
|
Technologies []string `json:"technologies"`
|
|
Highlights []string `json:"highlights"`
|
|
Duration string `json:"-"` // Calculated field, not from JSON
|
|
}
|
|
|
|
type Education struct {
|
|
Degree string `json:"degree"`
|
|
Institution string `json:"institution"`
|
|
Location string `json:"location"`
|
|
StartDate string `json:"startDate"`
|
|
EndDate string `json:"endDate"`
|
|
Field string `json:"field"`
|
|
}
|
|
|
|
type Skills struct {
|
|
Technical []SkillCategory `json:"technical"`
|
|
SoftSkills []string `json:"soft_skills"`
|
|
}
|
|
|
|
type SkillCategory struct {
|
|
Category string `json:"category"`
|
|
Proficiency int `json:"proficiency"`
|
|
Items []string `json:"items"`
|
|
Sidebar string `json:"sidebar"` // "left" or "right"
|
|
}
|
|
|
|
type Language struct {
|
|
Language string `json:"language"`
|
|
Proficiency string `json:"proficiency"`
|
|
Level int `json:"level"`
|
|
Detail string `json:"detail,omitempty"` // Optional detail like "Oral (Medio/Alto) Escrito (Alto)"
|
|
}
|
|
|
|
type Project struct {
|
|
Title string `json:"title"`
|
|
ProjectName string `json:"projectName,omitempty"` // Optional: linkable part of title
|
|
ProjectDesc string `json:"projectDesc,omitempty"` // Optional: non-linkable description part
|
|
ProjectID string `json:"projectID,omitempty"` // Unique ID for scrolling/navigation
|
|
URL string `json:"url"`
|
|
ProjectLogo string `json:"projectLogo,omitempty"` // Optional logo filename
|
|
GitRepoUrl string `json:"gitRepoUrl,omitempty"` // Optional git repository URL for dynamic dates
|
|
Location string `json:"location"`
|
|
StartDate string `json:"startDate,omitempty"` // Optional static start date
|
|
Current bool `json:"current"`
|
|
MaintainedBy string `json:"maintainedBy,omitempty"` // Optional maintainer name (e.g., "SAP")
|
|
Technologies []string `json:"technologies"`
|
|
ShortDescription string `json:"shortDescription"`
|
|
Responsibilities []string `json:"responsibilities"`
|
|
|
|
// Computed fields (not stored in JSON)
|
|
ComputedStartDate string `json:"-"` // Dynamically calculated from git repo or system
|
|
DynamicDate string `json:"-"` // Current date for ongoing projects
|
|
}
|
|
|
|
type Award struct {
|
|
Title string `json:"title"`
|
|
Issuer string `json:"issuer"`
|
|
Date string `json:"date"`
|
|
Description string `json:"description"`
|
|
ShortDescription string `json:"shortDescription,omitempty"`
|
|
Responsibilities []string `json:"responsibilities,omitempty"`
|
|
AwardLogo string `json:"awardLogo"`
|
|
}
|
|
|
|
type Certification struct {
|
|
Name string `json:"name"`
|
|
Issuer string `json:"issuer"`
|
|
Date string `json:"date"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
type Course struct {
|
|
Title string `json:"title"`
|
|
Institution string `json:"institution"`
|
|
CourseID string `json:"courseID,omitempty"` // Unique ID for scrolling/navigation
|
|
CourseLogo string `json:"courseLogo,omitempty"` // Optional logo filename
|
|
Location string `json:"location"`
|
|
Date string `json:"date"`
|
|
Duration string `json:"duration"`
|
|
Description string `json:"description"`
|
|
ShortDescription string `json:"shortDescription,omitempty"`
|
|
Responsibilities []string `json:"responsibilities,omitempty"`
|
|
}
|
|
|
|
type Reference struct {
|
|
Title string `json:"title"`
|
|
URL string `json:"url"`
|
|
Type string `json:"type"` // "recommendation", "portfolio", "profile", "cv", "presentation"
|
|
Action string `json:"action,omitempty"` // Optional action like "downloadPDF"
|
|
TextBefore string `json:"textBefore,omitempty"` // Text before the link
|
|
LinkText string `json:"linkText,omitempty"` // Bold text inside the link
|
|
TextAfter string `json:"textAfter,omitempty"` // Text after the link
|
|
}
|
|
|
|
type Other struct {
|
|
DriverLicense string `json:"driverLicense"`
|
|
}
|
|
|
|
type Meta struct {
|
|
Version string `json:"version"`
|
|
LastUpdated string `json:"lastUpdated"`
|
|
Format string `json:"format"`
|
|
Language string `json:"language"`
|
|
}
|
|
|
|
type UI struct {
|
|
InfoModal InfoModal `json:"infoModal"`
|
|
ShortcutsModal ShortcutsModal `json:"shortcutsModal"`
|
|
}
|
|
|
|
type InfoModal struct {
|
|
Title string `json:"title"`
|
|
Description template.HTML `json:"description"`
|
|
TechStack TechStack `json:"techStack"`
|
|
ViewSource string `json:"viewSource"`
|
|
ViewSourceSubtext string `json:"viewSourceSubtext"`
|
|
}
|
|
|
|
type TechStack struct {
|
|
GoHono string `json:"goHono"`
|
|
HTMX string `json:"htmx"`
|
|
HTML5 string `json:"html5"`
|
|
CSS3 string `json:"css3"`
|
|
}
|
|
|
|
type ShortcutsModal struct {
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
Sections ShortcutsSections `json:"sections"`
|
|
}
|
|
|
|
type ShortcutsSections struct {
|
|
Zoom ShortcutGroup `json:"zoom"`
|
|
ViewControls ShortcutGroup `json:"viewControls"`
|
|
Navigation ShortcutGroup `json:"navigation"`
|
|
Actions ShortcutGroup `json:"actions"`
|
|
Browser ShortcutGroup `json:"browser"`
|
|
}
|
|
|
|
type ShortcutGroup struct {
|
|
Title string `json:"title"`
|
|
ZoomIn *ShortcutItem `json:"zoomIn,omitempty"`
|
|
ZoomOut *ShortcutItem `json:"zoomOut,omitempty"`
|
|
ZoomReset *ShortcutItem `json:"zoomReset,omitempty"`
|
|
ToggleLength *ShortcutItem `json:"toggleLength,omitempty"`
|
|
ToggleIcons *ShortcutItem `json:"toggleIcons,omitempty"`
|
|
ToggleTheme *ShortcutItem `json:"toggleTheme,omitempty"`
|
|
ExpandAll *ShortcutItem `json:"expandAll,omitempty"`
|
|
CollapseAll *ShortcutItem `json:"collapseAll,omitempty"`
|
|
ScrollToTop *ShortcutItem `json:"scrollToTop,omitempty"`
|
|
Print *ShortcutItem `json:"print,omitempty"`
|
|
CloseModal *ShortcutItem `json:"closeModal,omitempty"`
|
|
ShowHelp *ShortcutItem `json:"showHelp,omitempty"`
|
|
Tab *ShortcutItem `json:"tab,omitempty"`
|
|
Enter *ShortcutItem `json:"enter,omitempty"`
|
|
}
|
|
|
|
type ShortcutItem struct {
|
|
Key string `json:"key"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
// findDataFile locates a data file by searching up the directory tree
|
|
func findDataFile(filename string) (string, error) {
|
|
// Try current directory first
|
|
if _, err := os.Stat(filename); err == nil {
|
|
return filename, nil
|
|
}
|
|
|
|
// Try parent directories (for tests running from subdirectories)
|
|
paths := []string{
|
|
filename, // Current dir
|
|
"../" + filename, // One level up
|
|
"../../" + filename, // Two levels up (for tests in internal/handlers)
|
|
"../../../" + filename, // Three levels up
|
|
}
|
|
|
|
for _, path := range paths {
|
|
if _, err := os.Stat(path); err == nil {
|
|
return path, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("file not found: %s (searched: current dir, ../, ../../, ../../../)", filename)
|
|
}
|
|
|
|
// LoadCV loads CV data from a JSON file for the specified language
|
|
func LoadCV(lang string) (*CV, error) {
|
|
if lang != "en" && lang != "es" {
|
|
return nil, fmt.Errorf("unsupported language: %s", lang)
|
|
}
|
|
|
|
filename := fmt.Sprintf("data/cv-%s.json", lang)
|
|
filepath, err := findDataFile(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
data, err := os.ReadFile(filepath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading file %s: %w", filename, err)
|
|
}
|
|
|
|
var cv CV
|
|
if err := json.Unmarshal(data, &cv); err != nil {
|
|
return nil, fmt.Errorf("error parsing JSON: %w", err)
|
|
}
|
|
|
|
// Replace {{YEAR}} placeholder in reference URLs with current year
|
|
currentYear := fmt.Sprintf("%d", time.Now().Year())
|
|
for i := range cv.References {
|
|
cv.References[i].URL = replaceYearPlaceholder(cv.References[i].URL, currentYear)
|
|
}
|
|
|
|
return &cv, nil
|
|
}
|
|
|
|
// replaceYearPlaceholder replaces {{YEAR}} with the current year
|
|
func replaceYearPlaceholder(url string, year string) string {
|
|
return strings.ReplaceAll(url, "{{YEAR}}", year)
|
|
}
|
|
|
|
// LoadUI loads UI translations from a JSON file for the specified language
|
|
func LoadUI(lang string) (*UI, error) {
|
|
if lang != "en" && lang != "es" {
|
|
return nil, fmt.Errorf("unsupported language: %s", lang)
|
|
}
|
|
|
|
filename := fmt.Sprintf("data/ui-%s.json", lang)
|
|
filepath, err := findDataFile(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
data, err := os.ReadFile(filepath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading file %s: %w", filename, err)
|
|
}
|
|
|
|
var ui UI
|
|
if err := json.Unmarshal(data, &ui); err != nil {
|
|
return nil, fmt.Errorf("error parsing JSON: %w", err)
|
|
}
|
|
|
|
return &ui, nil
|
|
}
|