package ui
import (
"encoding/json"
"html/template"
"io"
"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, "greenhouse") {
return "🌿 Greenhouse"
} else if strings.Contains(p, "lever") {
return "⚙️ Lever"
}
return "🌐 Web Target"
},
}
type PageData struct {
Targets []db.SearchTarget
Jobs []db.JobWithAnalysis
Applications []db.JobWithAnalysis
ActiveTab string
LLMProvider string
QualifiedJobsCount int
TotalIngestedCount int
ActiveApplications int
}
var (
rootTemplate *template.Template
)
func init() {
var err error
rootTemplate = template.New("root").Funcs(funcMap)
rootTemplate, err = rootTemplate.Parse(mainLayoutHTML)
if err != nil {
panic(err)
}
rootTemplate, err = rootTemplate.Parse(targetListHTML)
if err != nil {
panic(err)
}
rootTemplate, err = rootTemplate.Parse(jobListHTML)
if err != nil {
panic(err)
}
rootTemplate, err = rootTemplate.Parse(jobDetailHTML)
if err != nil {
panic(err)
}
rootTemplate, err = rootTemplate.Parse(kanbanBoardHTML)
if err != nil {
panic(err)
}
rootTemplate, err = rootTemplate.Parse(kanbanCardHTML)
if err != nil {
panic(err)
}
}
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)
}
const mainLayoutHTML = `{{ define "page" }}
Hireme | Job Command Center
H
Hireme
Job Application Command Center
LLM Provider:
{{ .LLMProvider }}
Total Search Targets
{{ len .Targets }}
Active Crawler Pipelines
Qualified Jobs Matched
{{ .QualifiedJobsCount }}
Passed Keyword & Score Filter
Total Ingested Postings
{{ .TotalIngestedCount }}
Deduplicated Raw Crawls
Active Applications
{{ .ActiveApplications }}
Tracked in Kanban Pipeline
Qualified Job Opportunities
Ranked by rule score + cached LLM skill gap analysis
{{ template "jobs" .Jobs }}
Job Application Tracking
Manage saved, applied, interviewing, and offer states
{{ template "kanban" .Applications }}
Add Target Search URL
{{ template "targets" .Targets }}
{{ end }}`
const targetListHTML = `{{ define "targets" }}
| Platform |
Name |
Target URL |
Last Crawled |
Status |
Action |
{{ range . }}
| {{ platformIcon .Platform }} |
{{ .Name }} |
{{ .TargetURL }}
|
{{ formatTime .LastCrawledAt }} |
{{ if .IsActive }}
Active
{{ else }}
Paused
{{ end }}
|
|
{{ else }}
| No search targets configured yet. |
{{ end }}
{{ end }}`
const jobListHTML = `{{ define "jobs" }}
| Job Title & Company |
Original Link |
Analysis Status |
Score & Qualification |
Keyword Tags |
Crawled Date |
Action |
{{ range . }}
|
{{ .Company }}
•
📍 {{ .Location }}
|
🔗
|
{{ if .Analysis }}
{{ if eq .Analysis.Status "PARSED" }}
PARSED
{{ else if eq .Analysis.Status "FILTERED_OUT" }}
FILTERED_OUT
{{ else }}
{{ .Analysis.Status }}
{{ end }}
{{ else }}
UNANALYZED
{{ end }}
|
{{ if .Analysis }}
{{ if .Analysis.IsQualified }}
★ {{ printf "%.0f" .Analysis.Score }} Qualified
{{ else }}
Not Qualified
{{ $missing := parseJSON .Analysis.MissingHighlights }}
{{ if $missing }}
{{ index $missing 0 }}
{{ end }}
{{ end }}
{{ else }}
-
{{ end }}
|
{{ if .Analysis }}
{{ range parseJSON .Analysis.MatchedHighlights }}
{{ . }}
{{ else }}
No tags
{{ end }}
{{ else }}
No tags
{{ end }}
|
{{ formatDate .CrawledAt }}
|
{{ if .App }}
{{ .App.Status }}
{{ else }}
{{ end }}
|
{{ else }}
|
No job postings found yet. Click 🔍 Discover Jobs to enqueue crawler tasks.
|
{{ end }}
{{ end }}`
const jobDetailHTML = `{{ define "jobDetail" }}
{{ .Company }}
📍 {{ .Location }} • Ingested {{ formatDate .CrawledAt }}
{{ if .Analysis }}
Match Relevance Score
{{ printf "%.1f" .Analysis.Score }} / 100
LLM Status
{{ .Analysis.Status }}
✓ Matched Technical Highlights
{{ range parseJSON .Analysis.MatchedHighlights }}
{{ . }}
{{ end }}
! Skill & Experience Gaps
{{ range parseJSON .Analysis.MissingHighlights }}
{{ . }}
{{ end }}
💡 Resume Tailoring Action Points
{{ range parseJSON .Analysis.TailoringSuggestions }}
-
• {{ . }}
{{ end }}
{{ end }}
Job Description Payload
{{ .RawContent }}
{{ end }}`
const kanbanBoardHTML = `{{ define "kanban" }}
💾 Saved
{{ range . }}
{{ if eq .App.Status "SAVED" }}
{{ template "kanbanCard" . }}
{{ end }}
{{ end }}
🚀 Applied
{{ range . }}
{{ if eq .App.Status "APPLIED" }}
{{ template "kanbanCard" . }}
{{ end }}
{{ end }}
💬 Interview
{{ range . }}
{{ if eq .App.Status "INTERVIEW" }}
{{ template "kanbanCard" . }}
{{ end }}
{{ end }}
🎉 Offer / Closed
{{ range . }}
{{ if or (eq .App.Status "OFFER") (eq .App.Status "REJECTED") }}
{{ template "kanbanCard" . }}
{{ end }}
{{ end }}
{{ end }}`
const kanbanCardHTML = `
{{ define "kanbanCard" }}
{{ .Company }}
{{ .Title }}
📍 {{ .Location }}
Status: {{ .App.Status }}
{{ end }}`