package ui import ( "encoding/json" "html/template" "io" "regexp" "strings" "time" "github.com/hireme/hireme/internal/db" ) var funcMap = template.FuncMap{ "formatTime": func(t *time.Time) string { if t == nil { return "Never" } return t.Format("Jan 02, 2006 15:04") }, "formatDate": func(t time.Time) string { return t.Format("Jan 02, 2006") }, "parseJSON": func(jsonStr string) []string { if jsonStr == "" { return nil } var list []string _ = json.Unmarshal([]byte(jsonStr), &list) return list }, "scoreBadgeClass": func(score float64) string { if score >= 50 { return "bg-emerald-500/20 text-emerald-400 border-emerald-500/30" } else if score >= 20 { return "bg-amber-500/20 text-amber-400 border-amber-500/30" } return "bg-slate-700/50 text-slate-400 border-slate-600/30" }, "platformIcon": func(platform string) string { p := strings.ToLower(platform) if strings.Contains(p, "linkedin") { return "πŸ’Ό LinkedIn" } else if strings.Contains(p, "google") { return "πŸ” Google Jobs" } else if strings.Contains(p, "apple") { return "🍏 Apple" } else if strings.Contains(p, "meta") || strings.Contains(p, "facebook") { return "♾️ Meta" } else if strings.Contains(p, "greenhouse") { return "🌿 Greenhouse" } else if strings.Contains(p, "lever") { return "βš™οΈ Lever" } return "🌐 Web Target" }, // rawContent returns the stored Markdown string (already clean) "rawContent": func(s string) string { return strings.TrimSpace(s) }, // htmlStripTags is a safety net for any legacy HTML still in the DB "htmlStripTags": func(s string) string { re := regexp.MustCompile(`<[^>]+>`) clean := re.ReplaceAllString(s, " ") space := regexp.MustCompile(`\s+`) return strings.TrimSpace(space.ReplaceAllString(clean, " ")) }, } // PageData is the data payload for the main page render. type PageData struct { Targets []db.SearchTarget Jobs []db.JobWithAnalysis Applications []db.JobWithAnalysis ActiveTab string LLMProvider string ToReviewCount int TotalIngestedCount int InPipelineCount int } var rootTemplate *template.Template func init() { // All template HTML strings are collected here in a defined order. // Sub-files do NOT register via init() β€” they export named constants that // are referenced directly in this slice to avoid init ordering issues. sources := []string{ mainLayoutHTML, jobsTabHTML, jobListHTML, jobDetailHTML, targetsTabHTML, targetListHTML, applyTabHTML, applyTableHTML, kanbanBoardHTML, kanbanCardHTML, } var err error rootTemplate = template.New("root").Funcs(funcMap) for _, src := range sources { rootTemplate, err = rootTemplate.Parse(src) if err != nil { panic("template parse error: " + err.Error()) } } } func RenderPage(w io.Writer, data PageData) error { return rootTemplate.ExecuteTemplate(w, "page", data) } func RenderTargetList(w io.Writer, targets []db.SearchTarget) error { return rootTemplate.ExecuteTemplate(w, "targets", targets) } func RenderJobList(w io.Writer, jobs []db.JobWithAnalysis) error { return rootTemplate.ExecuteTemplate(w, "jobs", jobs) } func RenderJobDetail(w io.Writer, job *db.JobWithAnalysis) error { return rootTemplate.ExecuteTemplate(w, "jobDetail", job) } func RenderKanbanBoard(w io.Writer, apps []db.JobWithAnalysis) error { return rootTemplate.ExecuteTemplate(w, "kanban", apps) } func RenderApplyTable(w io.Writer, apps []db.JobWithAnalysis) error { return rootTemplate.ExecuteTemplate(w, "applyTable", apps) }