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)
}