class DesktopJobDetail extends Shadow { constructor(job, calendars) { super() this.job = job } render() { if (!this.job) { VStack(() => { p("💼") .margin(0) .fontSize(2.5, em) .opacity(0.2) p("Select a job to view details") .margin(0) .marginTop(0.75, em) .fontSize(0.92, em) .color("var(--headertext)") .opacity(0.35) .textAlign("center") }) .flex(1) .height(100, pct) .justifyContent("center") .alignItems("center") return; } const job = this.job; VStack(() => { // ── Header ──────────────────────────────────────────────── VStack(() => { // Logo + title HStack(() => { VStack(() => { p(job.company ? job.company[0].toUpperCase() : "?") .margin(0) .fontSize(1.6, em) .fontWeight("700") .color("white") }) .width(3.8, em) .height(3.8, em) .borderRadius(0.65, em) .background(this.companyColor(job.company)) .justifyContent("center") .alignItems("center") .flexShrink(0) VStack(() => { h2(job.title) .margin(0) .fontSize(1.18, em) .fontWeight("700") .color("var(--headertext)") .lineHeight("1.25") p(job.company || "Unknown Company") .margin(0) .marginTop(0.18, em) .fontSize(0.9, em) .fontWeight("500") .color("var(--headertext)") .opacity(0.6) }) .flex(1) .minWidth(0) }) .gap(0.9, em) .alignItems("center") .marginBottom(1, em) // Meta chips row HStack(() => { if (job.location) this.chip("📍 " + job.location) if (job.employment_type) this.chip(this.formatType(job.employment_type)) if (job.experience_level) this.chip(this.formatLevel(job.experience_level)) if (job.department) this.chip(job.department) if (job.salary_number) this.chip(this.salaryLabel(job.salary_number, job.salary_period)) }) .gap(0.45, em) .flexWrap("wrap") .marginBottom(0.85, em) // Stats row HStack(() => { if (job.applicants !== undefined) { this.stat(job.applicants + " applicants") } if (job.posted_at) { this.stat(this.relativeDate(job.posted_at)) } }) .gap(1.2, em) .marginBottom(1.1, em) // CTA buttons HStack(() => { button("Apply now") .paddingHorizontal(1.4, em) .paddingVertical(0.58, em) .background("var(--quillred)") .color("white") .border("none") .borderRadius(0.45, em) .fontWeight("600") .fontSize(0.88, em) .cursor("pointer") button("Save job") .paddingHorizontal(1.2, em) .paddingVertical(0.58, em) .background("transparent") .color("var(--headertext)") .border("1px solid var(--divider)") .borderRadius(0.45, em) .fontWeight("500") .fontSize(0.88, em) .cursor("pointer") }) .gap(0.65, em) }) .paddingHorizontal(1.75, em) .paddingTop(1.6, em) .paddingBottom(1.25, em) .borderBottom("1px solid var(--divider)") .flexShrink(0) // ── Body ────────────────────────────────────────────────── VStack(() => { // About the role if (job.description) { this.section("About the role", () => { p(job.description) .margin(0) .fontSize(0.88, em) .color("var(--headertext)") .opacity(0.8) .lineHeight("1.65") .whiteSpace("pre-wrap") }) } // Skills / requirements if (job.skills && job.skills.length > 0) { this.section("Skills & requirements", () => { HStack(() => { job.skills.forEach(skill => { p(skill) .margin(0) .paddingHorizontal(0.75, em) .paddingVertical(0.3, em) .background("var(--darkaccent)") .border("1px solid var(--divider)") .borderRadius(100, px) .fontSize(0.78, em) .color("var(--headertext)") .opacity(0.8) .whiteSpace("nowrap") }) }) .gap(0.45, em) .flexWrap("wrap") }) } // Job details table this.section("Job details", () => { VStack(() => { this.detailRow("Employment type", this.formatType(job.employment_type) || "—") this.detailRow("Experience level", this.formatLevel(job.experience_level) || "—") this.detailRow("Location", job.location || "—") this.detailRow("Compensation", job.salary_number ? this.salaryLabel(job.salary_number, job.salary_period) : "—") if (job.department) this.detailRow("Department", job.department) }) .gap(0) }) }) .paddingHorizontal(1.75, em) .paddingTop(0.25, em) .paddingBottom(2, em) .gap(0) .overflowY("auto") .flex(1) }) .height(100, pct) .width(100, pct) .overflowY("hidden") .boxSizing("border-box") } section(title, contentFn) { VStack(() => { p(title) .margin(0) .marginBottom(0.7, em) .fontSize(0.82, em) .fontWeight("700") .letterSpacing("0.04em") .color("var(--headertext)") .opacity(0.38) .textTransform("uppercase") contentFn() }) .paddingTop(1.35, em) .paddingBottom(0.35, em) .borderBottom("1px solid var(--divider)") .width(100, pct) .boxSizing("border-box") } detailRow(label, value) { HStack(() => { p(label) .margin(0) .fontSize(0.85, em) .color("var(--headertext)") .opacity(0.45) .width(9, em) .flexShrink(0) p(value) .margin(0) .fontSize(0.85, em) .color("var(--headertext)") .fontWeight("500") }) .paddingVertical(0.55, em) .alignItems("flex-start") .borderBottom("1px solid var(--divider)") .width(100, pct) } chip(text) { p(text) .margin(0) .paddingHorizontal(0.65, em) .paddingVertical(0.25, em) .background("var(--darkaccent)") .border("1px solid var(--divider)") .borderRadius(100, px) .fontSize(0.78, em) .color("var(--headertext)") .opacity(0.75) .whiteSpace("nowrap") } stat(text) { p(text) .margin(0) .fontSize(0.78, em) .color("var(--headertext)") .opacity(0.4) } formatType(type) { return { "full-time": "Full-time", "part-time": "Part-time", "contract": "Contract", "internship": "Internship" }[type] || type } formatLevel(level) { return { "entry": "Entry level", "mid": "Mid level", "senior": "Senior" }[level] || level } 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(DesktopJobDetail)