Files
apps/jobs/desktop/DesktopJobsList.js
metacryst 0d6c7683ff init
2026-04-28 20:05:00 -05:00

170 lines
5.6 KiB
JavaScript

class DesktopJobsList extends Shadow {
constructor(jobs, selectedId, onSelect) {
super()
this.jobs = jobs
this.selectedId = selectedId
this.onSelect = onSelect
}
render() {
VStack(() => {
p(this.jobs.length === 1 ? "1 job" : `${this.jobs.length} jobs`)
.margin(0)
.marginBottom(0.75, em)
.fontSize(0.78, em)
.fontWeight("600")
.color("var(--headertext)")
.opacity(0.45)
.flexShrink(0)
if (this.jobs.length === 0) {
VStack(() => {
p("No jobs match your filters")
.margin(0)
.fontSize(0.9, em)
.color("var(--headertext)")
.opacity(0.4)
.textAlign("center")
})
.flex(1)
.justifyContent("center")
.alignItems("center")
} else {
this.jobs.forEach(job => this.renderCard(job))
}
})
.paddingHorizontal(0.75, em)
.paddingTop(1, em)
.paddingBottom(1, em)
.gap(0.5, em)
.overflowY("auto")
.height(100, pct)
.boxSizing("border-box")
}
renderCard(job) {
const self = this
const isSelected = job.id === this.selectedId;
VStack(() => {
// Logo placeholder + title row
HStack(() => {
// Company logo circle
VStack(() => {
p(job.company ? job.company[0].toUpperCase() : "?")
.margin(0)
.fontSize(1.1, em)
.fontWeight("700")
.color("white")
})
.width(2.6, em)
.height(2.6, em)
.borderRadius(0.45, em)
.background(this.companyColor(job.company))
.justifyContent("center")
.alignItems("center")
.flexShrink(0)
VStack(() => {
p(job.title)
.margin(0)
.fontSize(0.92, em)
.fontWeight("600")
.color("var(--headertext)")
.lineHeight("1.3")
p(job.company || "Unknown Company")
.margin(0)
.marginTop(0.1, em)
.fontSize(0.78, em)
.color("var(--headertext)")
.opacity(0.55)
})
.gap(0)
.flex(1)
.minWidth(0)
})
.gap(0.65, em)
.alignItems("flex-start")
// Meta row
HStack(() => {
if (job.location) {
this.metaChip("📍 " + job.location)
}
if (job.employment_type) {
this.metaChip(this.formatType(job.employment_type))
}
if (job.salary_number) {
this.metaChip(this.salaryLabel(job.salary_number, job.salary_period))
}
})
.gap(0.35, em)
.flexWrap("wrap")
// Posted date
if (job.posted_at) {
p(this.relativeDate(job.posted_at))
.margin(0)
.fontSize(0.7, em)
.color("var(--headertext)")
.opacity(0.35)
}
})
.gap(0.55, em)
.padding(0.9, em)
.borderRadius(0.65, em)
.background(isSelected ? "var(--app)" : "var(--darkaccent)")
.border(`1px solid ${isSelected ? "var(--quillred)" : "var(--divider)"}`)
.cursor("pointer")
.boxSizing("border-box")
.width(100, pct)
.onClick(function(done){ if(done){ self.onSelect(job.id) } })
}
metaChip(text) {
p(text)
.margin(0)
.fontSize(0.7, em)
.color("var(--headertext)")
.opacity(0.65)
.paddingHorizontal(0.5, em)
.paddingVertical(0.18, em)
.borderRadius(100, px)
.whiteSpace("nowrap")
}
formatType(type) {
return { "full-time": "Full-time", "part-time": "Part-time", "contract": "Contract", "internship": "Internship" }[type] || type
}
salaryLabel(number, period) {
const n = new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(Number(number));
if (period === "one-time") return `$${n} one-time`;
if (period === "year") return `$${n}/yr`;
if (period === "month") return `$${n}/mo`;
if (period === "hour") return `$${n}/hr`;
return `$${n}`;
}
relativeDate(date) {
const d = new Date(date);
const days = Math.floor((Date.now() - d) / 86400000);
if (days === 0) return "Posted today";
if (days === 1) return "Posted yesterday";
if (days < 7) return `Posted ${days} days ago`;
if (days < 30) return `Posted ${Math.floor(days / 7)}w ago`;
return `Posted ${Math.floor(days / 30)}mo ago`;
}
companyColor(company) {
const colors = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6", "#ec4899", "#06b6d4", "#84cc16"];
if (!company) return colors[0];
let hash = 0;
for (let i = 0; i < company.length; i++) hash = company.charCodeAt(i) + ((hash << 5) - hash);
return colors[Math.abs(hash) % colors.length];
}
}
register(DesktopJobsList)