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)