294 lines
11 KiB
JavaScript
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)
|