This commit is contained in:
metacryst
2026-04-28 20:05:00 -05:00
commit 0d6c7683ff
123 changed files with 20922 additions and 0 deletions

441
jobs/jobs.js Normal file
View File

@@ -0,0 +1,441 @@
import server from "/@server/server.js"
import "/_/code/components/LoadingCircle.js"
css(`
jobs-, jobs- * { box-sizing: border-box; }
jobs- {
font-family: 'Arial';
scrollbar-width: none;
-ms-overflow-style: none;
}
jobs-::-webkit-scrollbar { display: none; }
jobs- input::placeholder, jobs- select::placeholder {
color: var(--headertext);
opacity: 0.35;
}
jobs- select {
appearance: none;
-webkit-appearance: none;
}
`)
class Jobs extends Shadow {
selectedJobId = null
searchText = ""
searchOpen = false
filtersOpen = false
filters = { type: "", level: "" }
loaded = false
constructor() {
super()
const cached = global.currentNetwork?.data?.jobs
if (cached?.length) {
this.jobs = cached
this.loaded = true
} else {
this.jobs = []
}
}
async loadJobs() {
const fetched = await server.getJobs(global.currentNetwork.id)
this.jobs = fetched || []
this.loaded = true
this.rerender()
}
get selectedJob() { return this.jobs.find(j => j.id === this.selectedJobId) || null }
get filtered() {
return this.jobs.filter(job => {
if (this.searchText) {
const q = this.searchText.toLowerCase()
const hay = [job.title, job.company, job.location, job.department].join(" ").toLowerCase()
if (!hay.includes(q)) return false
}
if (this.filters.type && job.employment_type !== this.filters.type) return false
if (this.filters.level && job.experience_level !== this.filters.level) return false
return true
})
}
render() {
VStack(() => {
if (this.selectedJobId === null) {
this.renderList()
} else {
this.renderDetail()
}
})
.height(100, pct).width(100, vw).overflow("hidden")
.onAppear(async () => {
if (!this.loaded) await this.loadJobs()
})
}
// ── List ─────────────────────────────────────────────────────────────────
renderList() {
VStack(() => {
this.renderListHeader()
if (this.filtersOpen && !global.appRefreshing) this.renderFilters()
const jobs = this.filtered
VStack(() => {
if (global.appRefreshing || !this.loaded) {
LoadingCircle()
} else if (!jobs.length) {
VStack(() => {
p("No jobs match your filters").margin(0).fontSize(0.9, em)
.color("var(--headertext)").opacity(0.38).textAlign("center")
}).flex(1).justifyContent("center").alignItems("center")
} else {
p(jobs.length === 1 ? "1 job" : `${jobs.length} jobs`)
.margin(0).paddingHorizontal(1.1, em).paddingTop(0.65, em)
.fontSize(0.72, em).fontWeight("600")
.color("var(--headertext)").opacity(0.4).flexShrink(0)
jobs.forEach(job => this.renderCard(job))
}
})
.flex(1).overflowY("auto").paddingBottom(2, em)
})
.height(100, pct).width(100, pct).overflow("hidden")
}
renderListHeader() {
VStack(() => {
HStack(() => {
p("Jobs")
.margin(0).fontSize(1.35, em).fontWeight("700")
.color("var(--headertext)").flex(1)
HStack(() => {
p("🔍")
.margin(0).fontSize(1.05, em).padding(0.4, em).cursor("pointer")
.onTouch((start) => {
if (!start) {
this.searchOpen = !this.searchOpen
if (!this.searchOpen) this.searchText = ""
this.rerender()
}
})
p("⚙️")
.margin(0).fontSize(1.05, em).padding(0.4, em).cursor("pointer")
.onTouch((start) => {
if (!start) { this.filtersOpen = !this.filtersOpen; this.rerender() }
})
})
.gap(0)
})
.alignItems("center")
.paddingHorizontal(1.1, em).paddingTop(1, em).paddingBottom(0.5, em)
if (this.searchOpen) {
HStack(() => {
p("🔍").margin(0).fontSize(0.78, em).opacity(0.4).flexShrink(0)
input()
.attr({ type: "text", placeholder: "Search jobs…", autofocus: "true" })
.flex(1).border("none").outline("none")
.background("transparent")
.color("var(--headertext)").fontSize(0.9, em)
.onInput((e) => { this.searchText = e.target.value; this.rerender() })
})
.gap(0.5, em)
.paddingHorizontal(0.85, em).paddingVertical(0.55, em)
.background("var(--darkaccent)")
.border("1px solid var(--divider)")
.borderRadius(0.6, em)
.marginHorizontal(1.1, em).marginBottom(0.5, em)
.alignItems("center")
}
})
.flexShrink(0)
}
renderFilters() {
HStack(() => {
select(() => {
option("Any type", "")
option("Full-time", "full-time")
option("Part-time", "part-time")
option("Contract", "contract")
option("Internship","internship")
})
.flex(1)
.padding(0.6, em)
.background("var(--darkaccent)")
.border("1px solid var(--divider)")
.borderRadius(0.55, em)
.color("var(--headertext)").fontSize(0.85, em)
.onInput((e) => { this.filters.type = e.target.value; this.rerender() })
select(() => {
option("Any level", "")
option("Entry", "entry")
option("Mid", "mid")
option("Senior", "senior")
})
.flex(1)
.padding(0.6, em)
.background("var(--darkaccent)")
.border("1px solid var(--divider)")
.borderRadius(0.55, em)
.color("var(--headertext)").fontSize(0.85, em)
.onInput((e) => { this.filters.level = e.target.value; this.rerender() })
})
.gap(0.65, em)
.paddingHorizontal(1.1, em).paddingBottom(0.65, em)
.flexShrink(0)
}
renderCard(job) {
VStack(() => {
HStack(() => {
VStack(() => {
p((job.company || "?")[0].toUpperCase())
.margin(0).fontSize(1.1, em).fontWeight("700").color("white")
})
.width(2.8, em).height(2.8, em).borderRadius(0.55, em)
.background(this.companyColor(job.company))
.justifyContent("center").alignItems("center").flexShrink(0)
VStack(() => {
p(job.title)
.margin(0).fontSize(0.95, em).fontWeight("600")
.color("var(--headertext)").lineHeight("1.3")
p(job.company || "Unknown")
.margin(0).marginTop(0.1, em).fontSize(0.78, em)
.color("var(--headertext)").opacity(0.55)
})
.flex(1).minWidth(0)
VStack(() => {
p(this.salaryLabel(job.salary_number, job.salary_period))
.margin(0).fontSize(0.78, em).fontWeight("600")
.color("var(--headertext)").textAlign("right")
p(this.relativeDate(job.posted_at))
.margin(0).marginTop(0.25, em).fontSize(0.68, em)
.color("var(--headertext)").opacity(0.38).textAlign("right")
})
.alignItems("flex-end").flexShrink(0)
})
.gap(0.75, em).alignItems("flex-start")
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))
})
.gap(0.4, em).flexWrap("wrap").marginTop(0.65, em)
})
.padding(1, em)
.marginHorizontal(1.1, em).marginTop(0.5, em)
.background("var(--darkaccent)")
.border("1px solid var(--divider)")
.borderRadius(0.75, em)
.onTouch((start) => {
if (!start) { this.selectedJobId = job.id; this.rerender() }
})
}
chip(text) {
p(text)
.margin(0).paddingHorizontal(0.55, em).paddingVertical(0.2, em)
.background("var(--darkaccent)").border("1px solid var(--divider)")
.borderRadius(100, px).fontSize(0.7, em)
.color("var(--headertext)").opacity(0.7).whiteSpace("nowrap")
}
// ── Detail ────────────────────────────────────────────────────────────────
renderDetail() {
const job = this.selectedJob
if (!job) return
VStack(() => {
// Header bar
HStack(() => {
p("")
.margin(0).fontSize(1.8, em).lineHeight("1")
.color("var(--headertext)").paddingRight(0.5, em).cursor("pointer")
.onTouch((start) => {
if (!start) { this.selectedJobId = null; this.rerender() }
})
p(job.company || "Job")
.margin(0).fontSize(0.95, em).fontWeight("600")
.color("var(--headertext)").opacity(0.7)
.flex(1).overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis")
})
.gap(0.25, em).paddingHorizontal(1.1, em).paddingVertical(0.85, em)
.borderBottom("1px solid var(--divider)")
.alignItems("center").flexShrink(0)
// Scrollable body
VStack(() => {
// Company + title
HStack(() => {
VStack(() => {
p((job.company || "?")[0].toUpperCase())
.margin(0).fontSize(1.6, em).fontWeight("700").color("white")
})
.width(3.5, em).height(3.5, em).borderRadius(0.65, em)
.background(this.companyColor(job.company))
.justifyContent("center").alignItems("center").flexShrink(0)
VStack(() => {
p(job.title)
.margin(0).fontSize(1.1, em).fontWeight("700")
.color("var(--headertext)").lineHeight("1.25")
p(job.company || "Unknown Company")
.margin(0).marginTop(0.15, em).fontSize(0.88, em)
.color("var(--headertext)").opacity(0.55)
})
.flex(1).minWidth(0)
})
.gap(0.9, em).alignItems("center")
.paddingHorizontal(1.1, em).paddingTop(1.25, em).paddingBottom(0.9, em)
// Meta chips
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")
.paddingHorizontal(1.1, em).paddingBottom(0.85, em)
.borderBottom("1px solid var(--divider)")
// Stats
HStack(() => {
if (job.applicants !== undefined) {
p(job.applicants + " applicants")
.margin(0).fontSize(0.78, em).color("var(--headertext)").opacity(0.4)
}
if (job.posted_at) {
p(this.relativeDate(job.posted_at))
.margin(0).fontSize(0.78, em).color("var(--headertext)").opacity(0.4)
}
})
.gap(1.25, em).paddingHorizontal(1.1, em).paddingVertical(0.75, em)
.borderBottom("1px solid var(--divider)")
// CTA buttons
HStack(() => {
button("Apply now")
.flex(1).paddingVertical(0.72, em)
.background("var(--quillred)").color("white")
.border("none").borderRadius(0.55, em)
.fontWeight("600").fontSize(0.92, em).cursor("pointer")
button("Save job")
.flex(1).paddingVertical(0.72, em)
.background("transparent").color("var(--headertext)")
.border("1px solid var(--divider)").borderRadius(0.55, em)
.fontWeight("500").fontSize(0.92, em).cursor("pointer")
})
.gap(0.65, em).paddingHorizontal(1.1, em).paddingVertical(0.85, em)
.borderBottom("1px solid var(--divider)")
// Description
if (job.description) {
this.section("About the role", () => {
p(job.description)
.margin(0).fontSize(0.88, em).lineHeight("1.65")
.color("var(--headertext)").opacity(0.8)
.whiteSpace("pre-wrap")
})
}
// Skills
if (job.skills?.length) {
this.section("Skills & requirements", () => {
HStack(() => {
job.skills.forEach(s => {
p(s)
.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")
})
}
// Details table
this.section("Job details", () => {
VStack(() => {
this.detailRow("Type", this.formatType(job.employment_type) || "—")
this.detailRow("Level", this.formatLevel(job.experience_level) || "—")
this.detailRow("Location", job.location || "—")
this.detailRow("Pay", job.salary_number ? this.salaryLabel(job.salary_number, job.salary_period) : "—")
if (job.department) this.detailRow("Dept.", job.department)
}).gap(0)
})
div().height(2, em)
})
.flex(1).overflowY("auto")
})
.height(100, pct).width(100, pct).overflow("hidden")
}
section(title, fn) {
VStack(() => {
p(title)
.margin(0).marginBottom(0.65, em)
.fontSize(0.72, em).fontWeight("700").letterSpacing("0.05em")
.color("var(--headertext)").opacity(0.38).textTransform("uppercase")
fn()
})
.paddingHorizontal(1.1, em).paddingTop(1.1, em).paddingBottom(0.9, em)
.borderBottom("1px solid var(--divider)")
}
detailRow(label, value) {
HStack(() => {
p(label).margin(0).fontSize(0.85, em).color("var(--headertext)").opacity(0.45).width(5.5, 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)")
}
// ── Helpers ───────────────────────────────────────────────────────────────
formatType(t) { return { "full-time": "Full-time", "part-time": "Part-time", "contract": "Contract", "internship": "Internship" }[t] || t }
formatLevel(l) { return { "entry": "Entry level", "mid": "Mid level", "senior": "Senior" }[l] || l }
salaryLabel(n, p) {
if (!n) return "—"
const fmt = new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(Number(n))
return { year: `$${fmt}/yr`, month: `$${fmt}/mo`, hour: `$${fmt}/hr`, "one-time": `$${fmt}` }[p] || `$${fmt}`
}
relativeDate(date) {
if (!date) return ""
const days = Math.floor((Date.now() - new Date(date)) / 86400000)
if (days === 0) return "Today"
if (days === 1) return "Yesterday"
if (days < 7) return `${days}d ago`
if (days < 30) return `${Math.floor(days / 7)}w ago`
return `${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(Jobs)