170 lines
5.6 KiB
JavaScript
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)
|