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

294 lines
11 KiB
JavaScript

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)