Files
hireme/internal/ui/template_jobs.go
T

325 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package ui
const jobsTabHTML = `{{ define "jobsTab" }}
<div class="space-y-4">
<!-- Filter Bar -->
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
<div>
<h2 class="text-base font-bold text-white">Job Opportunities</h2>
<p class="text-xs text-slate-400 mt-0.5">All ingested postings · AI-scored · sortable</p>
</div>
<div class="flex items-center gap-2 flex-wrap">
<input id="job-filter-text" type="search" placeholder="Search title, company…"
class="px-3 py-1.5 text-xs rounded-lg bg-slate-800 border border-slate-700 text-white focus:outline-none focus:border-indigo-500 w-48 placeholder:text-slate-500">
<!-- Filter order: most frequent → least frequent use -->
<select id="job-filter-qual"
class="px-3 py-1.5 text-xs rounded-lg bg-slate-800 border border-slate-700 text-white focus:outline-none focus:border-indigo-500">
<option value="default">✅ Qualified (Default)</option>
<option value="not-qualified">❌ Not Qualified</option>
<option value="saved">💾 Saved / In Pipeline</option>
<option value="all">🌐 All Jobs</option>
<option value="expired">⚠️ Expired / Closed</option>
<option value="opted-out">🚫 Opted-out</option>
</select>
<button hx-get="/jobs" hx-target="#jobs-container" hx-swap="innerHTML"
class="px-3 py-1.5 text-xs font-semibold text-indigo-400 hover:text-indigo-300 bg-slate-800 border border-slate-700 rounded-lg transition-colors">
↺ Refresh
</button>
</div>
</div>
<!-- Job List Container (HTMX auto-refreshed every 12s) -->
<div id="jobs-container"
hx-get="/jobs" hx-trigger="refresh" hx-swap="innerHTML">
{{ template "jobs" .Jobs }}
</div>
</div>
{{ end }}`
const jobListHTML = `{{ define "jobs" }}
<div class="overflow-x-auto rounded-xl border border-slate-800">
<table id="jobs-table" class="w-full text-left text-xs text-slate-300">
<thead class="bg-slate-900/80 text-slate-400 uppercase font-semibold text-[10px]">
<tr>
<th class="px-4 py-3" data-sort="title">Job Title &amp; Company</th>
<th class="px-4 py-3 text-center">Link</th>
<th class="px-4 py-3" data-sort="status">Analysis</th>
<th class="px-4 py-3" data-sort="score">Score &amp; Qualification</th>
<th class="px-4 py-3">Reason / Tags</th>
<th class="px-4 py-3" data-sort="date">Last Updated</th>
<th class="px-4 py-3 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800 bg-slate-900/40">
{{ range . }}
{{- $qual := "pending" -}}
{{- if .Analysis }}{{ if .Analysis.IsQualified }}{{ $qual = "qualified" }}{{ else }}{{ $qual = "not-qualified" }}{{ end }}{{ end -}}
{{- $appClass := "none" -}}
{{- if .App }}{{ if eq .App.Status "OPTED_OUT" }}{{ $appClass = "opted-out" }}{{ else if eq .App.Status "EXPIRED" }}{{ $appClass = "expired" }}{{ else }}{{ $appClass = "pipeline" }}{{ end }}{{ end -}}
<tr class="hover:bg-slate-800/40 transition-colors group" data-row data-qual="{{ $qual }}" data-app="{{ $appClass }}">
<!-- Title & Company -->
<td class="px-4 py-3 font-medium" data-sort-val="{{ .Title }}">
<button hx-get="/jobs/{{ .ID }}"
hx-target="#detail-modal-body"
hx-swap="innerHTML"
onclick="openDetailModal()"
class="text-left font-bold text-white group-hover:text-indigo-300 hover:underline transition-colors block truncate max-w-xs">
{{ .Title }}
</button>
<div class="flex items-center space-x-2 mt-0.5 text-[11px] text-slate-400">
<span class="font-semibold text-indigo-400">{{ .Company }}</span>
<span>&bull;</span>
<span>📍 {{ .Location }}</span>
</div>
</td>
<!-- External Link -->
<td class="px-4 py-3 text-center">
<a href="{{ .ApplyURL }}" target="_blank" title="Open original job posting"
class="inline-flex items-center justify-center w-7 h-7 rounded bg-slate-800 hover:bg-slate-700 text-indigo-400 hover:text-indigo-300 border border-slate-700 transition-colors">
🔗
</a>
</td>
<!-- Analysis Status -->
<td class="px-4 py-3" data-sort-val="{{ if .Analysis }}{{ .Analysis.Status }}{{ else }}UNANALYZED{{ end }}">
{{ if .Analysis }}
{{ if eq .Analysis.Status "PARSED" }}
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">PARSED</span>
{{ else if eq .Analysis.Status "PENDING" }}
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-amber-500/20 text-amber-400 border border-amber-500/30">PENDING</span>
{{ else if eq .Analysis.Status "DONE" }}
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-emerald-500/20 text-emerald-400 border border-emerald-500/30">DONE</span>
{{ else }}
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-slate-500/20 text-slate-400 border border-slate-600/30">{{ .Analysis.Status }}</span>
{{ end }}
{{ else }}
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-medium bg-slate-800 text-slate-500 border border-slate-700">UNANALYZED</span>
{{ end }}
</td>
<!-- Score & Qualification -->
<td class="px-4 py-3" data-sort-val="{{ if .Analysis }}{{ printf "%.0f" .Analysis.Score }}{{ else }}-1{{ end }}">
{{ if .Analysis }}
{{ if .Analysis.IsQualified }}
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-black border {{ scoreBadgeClass .Analysis.Score }}">
★ {{ printf "%.0f" .Analysis.Score }}
</span>
{{ else }}
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold bg-rose-500/10 text-rose-400 border border-rose-500/20">
✗ Not Qualified
</span>
{{ end }}
{{ else }}
<span class="text-slate-500 text-[11px]">–</span>
{{ end }}
</td>
<!-- Reason / Matched Tags -->
<td class="px-4 py-3 max-w-[180px]">
{{ if .App }}
{{ if eq .App.Status "OPTED_OUT" }}
<span class="text-[10px] text-slate-500 italic block">Opted out</span>
{{ else if eq .App.Status "EXPIRED" }}
<span class="text-[10px] text-amber-400/80 italic block">⚠️ No longer active</span>
{{ end }}
{{ end }}
{{ if .Analysis }}
{{ if .Analysis.IsQualified }}
<div class="flex flex-wrap gap-1">
{{ range parseJSON .Analysis.MatchedHighlights }}
<span class="px-1.5 py-0.5 rounded bg-emerald-500/10 text-emerald-300 text-[10px] font-medium border border-emerald-500/20">{{ . }}</span>
{{ else }}
<span class="text-slate-500 text-[10px] italic">No tags</span>
{{ end }}
</div>
{{ else }}
{{ $missing := parseJSON .Analysis.MissingHighlights }}
{{ if $missing }}
<span class="text-[10px] text-rose-400/90 leading-snug line-clamp-2" title="{{ index $missing 0 }}">{{ index $missing 0 }}</span>
{{ end }}
{{ end }}
{{ end }}
</td>
<!-- Date -->
<td class="px-4 py-3 text-slate-400 text-[11px]" data-sort-val="{{ .LastSeenAt.Unix }}">
{{ formatDate .LastSeenAt }}
</td>
<!-- Actions: Save + Opt-out OR status badge -->
<td class="px-4 py-3 text-right">
{{ if .App }}
{{ if eq .App.Status "OPTED_OUT" }}
<!-- Opted out: show undo button -->
<div class="flex items-center justify-end gap-1.5">
<span class="text-[10px] text-slate-500">Opted-out</span>
<button hx-post="/applications"
hx-vals='{"raw_job_id": {{ .ID }}, "status": "SAVED"}'
hx-target="#jobs-container"
hx-swap="innerHTML"
title="Undo opt-out"
class="px-2 py-0.5 rounded bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700 text-[10px] transition-colors">
↩
</button>
</div>
{{ else if eq .App.Status "EXPIRED" }}
<!-- Expired -->
<div class="flex items-center justify-end gap-1.5">
<span class="px-2 py-0.5 rounded bg-slate-800 text-slate-400 text-[10px] font-bold border border-slate-700">
⚠️ EXPIRED
</span>
<button hx-post="/applications"
hx-vals='{"raw_job_id": {{ .ID }}, "status": "SAVED"}'
hx-target="#jobs-container"
hx-swap="innerHTML"
title="Re-open / Save"
class="px-2 py-0.5 rounded bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700 text-[10px] transition-colors">
↩
</button>
</div>
{{ else }}
<!-- In pipeline -->
<span class="px-2 py-1 rounded bg-violet-500/20 text-violet-300 text-[10px] font-bold border border-violet-500/30">
{{ .App.Status }}
</span>
{{ end }}
{{ else }}
<!-- No app: show Save + Opt-out -->
<div class="flex items-center justify-end gap-1.5">
<button hx-post="/applications"
hx-vals='{"raw_job_id": {{ .ID }}, "status": "SAVED"}'
hx-target="#jobs-container"
hx-swap="innerHTML"
class="px-2.5 py-1 rounded bg-indigo-700 hover:bg-indigo-600 text-indigo-100 border border-indigo-600 text-[10px] font-semibold transition-colors">
+ Save
</button>
<button hx-post="/jobs/{{ .ID }}/opt-out"
hx-target="#jobs-container"
hx-swap="innerHTML"
title="Hide this job"
class="px-2.5 py-1 rounded bg-slate-800 hover:bg-rose-900/40 text-slate-500 hover:text-rose-400 border border-slate-700 text-[10px] transition-colors">
🚫
</button>
</div>
{{ end }}
</td>
</tr>
{{ else }}
<tr>
<td colspan="7" class="px-4 py-12 text-center text-slate-500">
<p class="text-lg mb-2">📭</p>
<p>No job postings found. Click <strong>🔍 Discover</strong> in the bottom bar to start crawling.</p>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
{{ end }}`
const jobDetailHTML = `{{ define "jobDetail" }}
<div class="space-y-5">
<!-- Header -->
<div class="flex items-start justify-between border-b border-slate-800 pb-4">
<div class="flex-1 min-w-0 pr-4">
<span class="text-xs font-bold text-indigo-400 uppercase tracking-wide">{{ .Company }}</span>
<h2 class="text-xl font-extrabold text-white mt-0.5 leading-tight">{{ .Title }}</h2>
<p class="text-xs text-slate-400 mt-1">📍 {{ .Location }} &bull; Last Updated {{ formatDate .LastSeenAt }}</p>
</div>
<button onclick="closeDetailModal()" class="flex-shrink-0 text-slate-400 hover:text-white text-lg p-1 rounded hover:bg-slate-800 transition-colors">✕</button>
</div>
{{ if .Analysis }}
<!-- Score + Status Grid -->
<div class="grid grid-cols-2 gap-3">
<div class="p-4 rounded-xl bg-slate-800/60 border border-slate-700/60">
<span class="text-[10px] text-slate-400 uppercase tracking-wider font-semibold block">Match Score</span>
{{ if .Analysis.IsQualified }}
<span class="text-2xl font-black text-emerald-400 mt-1 block">{{ printf "%.1f" .Analysis.Score }} <span class="text-sm font-medium text-slate-400">/ 100</span></span>
{{ else }}
<span class="text-base font-bold text-rose-400 mt-1 block">Not Qualified</span>
{{ end }}
</div>
<div class="p-4 rounded-xl bg-slate-800/60 border border-slate-700/60">
<span class="text-[10px] text-slate-400 uppercase tracking-wider font-semibold block">Analysis Status</span>
<span class="text-sm font-bold text-indigo-300 mt-1 block uppercase">{{ .Analysis.Status }}</span>
</div>
</div>
<!-- Highlights -->
<div class="space-y-4">
{{ if .Analysis.IsQualified }}
<div>
<h4 class="text-xs font-bold text-emerald-400 uppercase tracking-wider mb-2">✓ Matched Highlights</h4>
<div class="flex flex-wrap gap-2">
{{ range parseJSON .Analysis.MatchedHighlights }}
<span class="px-2.5 py-1 rounded-lg bg-emerald-500/15 text-emerald-300 text-xs font-semibold border border-emerald-500/30">{{ . }}</span>
{{ else }}
<span class="text-slate-500 text-xs italic">None identified</span>
{{ end }}
</div>
</div>
{{ end }}
<div>
<h4 class="text-xs font-bold text-rose-400 uppercase tracking-wider mb-2">! {{ if .Analysis.IsQualified }}Gaps / Missing Skills{{ else }}Disqualification Reasons{{ end }}</h4>
<div class="flex flex-wrap gap-2">
{{ range parseJSON .Analysis.MissingHighlights }}
<span class="px-2.5 py-1 rounded-lg bg-rose-500/15 text-rose-300 text-xs font-semibold border border-rose-500/30">{{ . }}</span>
{{ else }}
<span class="text-slate-500 text-xs italic">None identified</span>
{{ end }}
</div>
</div>
{{ if .Analysis.IsQualified }}
<div>
<h4 class="text-xs font-bold text-amber-400 uppercase tracking-wider mb-2">💡 Resume Tailoring Tips</h4>
<ul class="space-y-2">
{{ range parseJSON .Analysis.TailoringSuggestions }}
<li class="p-3 rounded-lg bg-slate-800/80 border border-slate-700/60 text-xs text-slate-200 leading-relaxed">&bull; {{ . }}</li>
{{ else }}
<li class="text-slate-500 text-xs italic">None</li>
{{ end }}
</ul>
</div>
{{ end }}
</div>
{{ end }}
<!-- Job Description (stored as Markdown) -->
<div class="border-t border-slate-800 pt-4">
<h4 class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-2">Job Description</h4>
<div class="p-4 rounded-xl bg-slate-950 border border-slate-800 text-xs text-slate-300 max-h-72 overflow-y-auto leading-relaxed custom-scrollbar whitespace-pre-wrap font-mono">
{{ rawContent .RawContent }}
</div>
</div>
<!-- Actions Footer -->
<div class="flex items-center justify-between border-t border-slate-800 pt-4">
<a href="{{ .ApplyURL }}" target="_blank"
class="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-xs font-bold text-white rounded-lg shadow-md transition-all">
Apply External ↗
</a>
<div class="flex space-x-2">
<button hx-post="/jobs/{{ .ID }}/analyze"
hx-target="#detail-modal-body"
hx-swap="innerHTML"
class="px-3 py-2 bg-slate-800 hover:bg-slate-700 text-xs font-semibold text-slate-200 rounded-lg border border-slate-700">
🔄 Re-run LLM
</button>
<button hx-post="/applications"
hx-vals='{"raw_job_id": {{ .ID }}, "status": "APPLIED"}'
hx-target="#jobs-container"
hx-swap="innerHTML"
onclick="switchTab('apply'); closeDetailModal();"
class="px-3 py-2 bg-emerald-600 hover:bg-emerald-500 text-xs font-semibold text-white rounded-lg shadow-md">
+ Mark Applied
</button>
</div>
</div>
</div>
{{ end }}`