init
This commit is contained in:
169
jobs/desktop/DesktopJobsList.js
Normal file
169
jobs/desktop/DesktopJobsList.js
Normal file
@@ -0,0 +1,169 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user