package models import ( "encoding/json" "fmt" "html/template" "os" "strings" "time" c "github.com/juanatsap/cv-site/internal/constants" ) // 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"` LogoIndex *int `json:"logoIndex,omitempty"` // Sprite sheet index (nil means no sprite) 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"` Category string `json:"category,omitempty"` // Project type: cli, app, web, webapp, plugin, sdk, contrib 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 LogoIndex *int `json:"logoIndex,omitempty"` // Sprite sheet index (nil means no sprite) GitRepoUrl string `json:"gitRepoUrl,omitempty"` // Optional git repository URL for dynamic dates OpenSource bool `json:"openSource,omitempty"` // True if project is open source (shows stars badge) 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 Stars int `json:"-"` // GitHub star count (fetched at runtime) } 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 LogoIndex *int `json:"logoIndex,omitempty"` // Sprite sheet index (nil means no sprite) 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("%s/cv-%s.json", c.DirData, 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("%s/ui-%s.json", c.DirData, 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 }