339 lines
15 KiB
JavaScript
339 lines
15 KiB
JavaScript
import "./DesktopJobsList.js"
|
|
import "./DesktopJobDetail.js"
|
|
import server from "/@server/server.js"
|
|
import "/_/code/components/LoadingCircle.js"
|
|
|
|
css(`
|
|
jobs- {
|
|
font-family: 'Arial';
|
|
scrollbar-width: none;
|
|
-ms-overflow-style: none;
|
|
}
|
|
jobs- select {
|
|
appearance: none;
|
|
-webkit-appearance: none;
|
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E");
|
|
background-repeat: no-repeat;
|
|
background-position: right 0.65em center;
|
|
padding-right: 1.8em !important;
|
|
}
|
|
`)
|
|
|
|
class Jobs extends Shadow {
|
|
loaded = false
|
|
|
|
constructor() {
|
|
super()
|
|
this.filters = { keyword: "", location: "", type: "", level: "" }
|
|
this.selectedJobId = null
|
|
|
|
this.jobs = [
|
|
{
|
|
id: 1,
|
|
title: "Senior Frontend Engineer",
|
|
company: "Acme Corp",
|
|
location: "San Francisco, CA",
|
|
employment_type: "full-time",
|
|
experience_level: "senior",
|
|
department: "Engineering",
|
|
salary_number: 165000,
|
|
salary_period: "year",
|
|
applicants: 47,
|
|
posted_at: new Date(Date.now() - 2 * 86400000),
|
|
skills: ["React", "TypeScript", "CSS", "GraphQL", "Node.js"],
|
|
description: "We're looking for a Senior Frontend Engineer to join our growing product team. You'll work closely with designers, backend engineers, and product managers to build fast, accessible, and beautiful web experiences.\n\nYou'll have ownership over large features and be expected to make architectural decisions. We ship every week and care deeply about code quality and UX."
|
|
},
|
|
{
|
|
id: 2,
|
|
title: "Product Designer",
|
|
company: "Blue River",
|
|
location: "New York, NY",
|
|
employment_type: "full-time",
|
|
experience_level: "mid",
|
|
department: "Design",
|
|
salary_number: 120000,
|
|
salary_period: "year",
|
|
applicants: 112,
|
|
posted_at: new Date(Date.now() - 5 * 86400000),
|
|
skills: ["Figma", "Design Systems", "Prototyping", "User Research"],
|
|
description: "Blue River is hiring a Product Designer to lead design across our core consumer product. You'll partner with PMs and engineers to take features from zero to one, establish design patterns, and advocate for users in every decision.\n\nWe're a small, fast-moving team and designers have a huge impact here."
|
|
},
|
|
{
|
|
id: 3,
|
|
title: "Backend Engineer",
|
|
company: "Orbit Systems",
|
|
location: "Remote",
|
|
employment_type: "contract",
|
|
experience_level: "mid",
|
|
department: "Platform",
|
|
salary_number: 95,
|
|
salary_period: "hour",
|
|
applicants: 29,
|
|
posted_at: new Date(Date.now() - 1 * 86400000),
|
|
skills: ["Go", "PostgreSQL", "Kubernetes", "gRPC", "Redis"],
|
|
description: "6-month contract (potential to extend) for a backend engineer to help scale our platform infrastructure. You'll be responsible for building and maintaining APIs used by millions of users, optimizing database performance, and improving system reliability."
|
|
},
|
|
{
|
|
id: 4,
|
|
title: "Marketing Manager",
|
|
company: "Groundwork",
|
|
location: "Austin, TX",
|
|
employment_type: "full-time",
|
|
experience_level: "mid",
|
|
department: "Marketing",
|
|
salary_number: 98000,
|
|
salary_period: "year",
|
|
applicants: 88,
|
|
posted_at: new Date(Date.now() - 10 * 86400000),
|
|
skills: ["SEO", "Content Strategy", "Analytics", "Paid Acquisition", "Email Marketing"],
|
|
description: "We need a Marketing Manager to own our top-of-funnel growth. You'll manage content, paid channels, and email campaigns — and be the person who keeps the brand consistent across every touchpoint. You'll report directly to our Head of Growth."
|
|
},
|
|
{
|
|
id: 5,
|
|
title: "Data Analyst",
|
|
company: "Compass Data",
|
|
location: "Chicago, IL",
|
|
employment_type: "full-time",
|
|
experience_level: "entry",
|
|
department: "Analytics",
|
|
salary_number: 75000,
|
|
salary_period: "year",
|
|
applicants: 203,
|
|
posted_at: new Date(Date.now() - 3 * 86400000),
|
|
skills: ["SQL", "Python", "Tableau", "dbt", "Excel"],
|
|
description: "A great entry-level opportunity for someone who loves data. You'll work with our analytics team to build dashboards, run ad-hoc analysis, and help business teams make data-driven decisions. We'll invest in your growth and give you plenty of mentorship."
|
|
},
|
|
{
|
|
id: 6,
|
|
title: "iOS Engineer",
|
|
company: "Fieldwork",
|
|
location: "Seattle, WA",
|
|
employment_type: "full-time",
|
|
experience_level: "senior",
|
|
department: "Mobile",
|
|
salary_number: 175000,
|
|
salary_period: "year",
|
|
applicants: 34,
|
|
posted_at: new Date(Date.now() - 7 * 86400000),
|
|
skills: ["Swift", "SwiftUI", "Combine", "CoreData", "Xcode"],
|
|
description: "Fieldwork is building next-generation tools for field service teams. Our iOS app is the most important surface we have — technicians use it every day on job sites. We need a senior iOS engineer who cares about performance, offline reliability, and a great UX."
|
|
},
|
|
{
|
|
id: 7,
|
|
title: "Operations Coordinator",
|
|
company: "Maple & Co",
|
|
location: "Boston, MA",
|
|
employment_type: "part-time",
|
|
experience_level: "entry",
|
|
department: "Operations",
|
|
salary_number: 28,
|
|
salary_period: "hour",
|
|
applicants: 61,
|
|
posted_at: new Date(Date.now() - 14 * 86400000),
|
|
skills: ["Project Management", "Excel", "Communication", "Scheduling"],
|
|
description: "Part-time role (20 hrs/week) helping our operations team stay organized. You'll coordinate schedules, manage vendor relationships, and help improve internal workflows. Ideal for someone who's highly organized and excited to grow into a full-time ops role."
|
|
},
|
|
{
|
|
id: 8,
|
|
title: "Machine Learning Intern",
|
|
company: "NeuralPath",
|
|
location: "Remote",
|
|
employment_type: "internship",
|
|
experience_level: "entry",
|
|
department: "AI Research",
|
|
salary_number: 8000,
|
|
salary_period: "month",
|
|
applicants: 394,
|
|
posted_at: new Date(Date.now() - 0),
|
|
skills: ["Python", "PyTorch", "Linear Algebra", "Git"],
|
|
description: "Summer internship (12 weeks) on our ML research team. You'll work alongside researchers on real problems in NLP and recommendation systems. We expect you to ship something you're proud of by the end of the summer. Strong preference for candidates who can start June 2."
|
|
},
|
|
]
|
|
|
|
this.selectedJobId = this.jobs[0]?.id || null
|
|
this.loadJobs()
|
|
}
|
|
|
|
async loadJobs() {
|
|
const fetched = await server.getJobs(global.currentNetwork.id)
|
|
this.loaded = true
|
|
if (fetched?.length) {
|
|
this.jobs = fetched
|
|
if (!this.selectedJobId) this.selectedJobId = fetched[0]?.id || null
|
|
}
|
|
this.rerender()
|
|
}
|
|
|
|
get filteredJobs() {
|
|
const { keyword, location, type, level } = this.filters;
|
|
return this.jobs.filter(job => {
|
|
if (keyword) {
|
|
const kw = keyword.toLowerCase();
|
|
const hay = [job.title, job.company, job.description, job.department].join(" ").toLowerCase();
|
|
if (!hay.includes(kw)) return false;
|
|
}
|
|
if (location) {
|
|
if (!job.location?.toLowerCase().includes(location.toLowerCase())) return false;
|
|
}
|
|
if (type && job.employment_type !== type) return false;
|
|
if (level && job.experience_level !== level) return false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
get selectedJob() {
|
|
return this.jobs.find(j => j.id === this.selectedJobId) || null;
|
|
}
|
|
|
|
filterSelect(options, currentValue, onChange) {
|
|
select(() => {
|
|
options.forEach(opt => {
|
|
option(opt.label)
|
|
.attr({ value: opt.value, selected: opt.value === currentValue ? "" : null })
|
|
})
|
|
})
|
|
.paddingVertical(0.55, em)
|
|
.paddingHorizontal(0.75, em)
|
|
.background("var(--darkaccent)")
|
|
.border("1px solid var(--divider)")
|
|
.borderRadius(0.5, em)
|
|
.color("var(--headertext)")
|
|
.fontSize(0.85, em)
|
|
.outline("none")
|
|
.cursor("pointer")
|
|
.flexShrink(0)
|
|
.onEvent("change", (e) => onChange(e.target.value))
|
|
}
|
|
|
|
render() {
|
|
const filtered = this.filteredJobs;
|
|
|
|
VStack(() => {
|
|
// Toolbar — rendered inline so text inputs stay in this component
|
|
// and don't lose focus on rerender
|
|
HStack(() => {
|
|
HStack(() => {
|
|
p("🔍")
|
|
.margin(0).fontSize(0.85, em).opacity(0.45).flexShrink(0)
|
|
input()
|
|
.attr({ type: "text", placeholder: "Search jobs, titles, companies…", value: this.filters.keyword })
|
|
.flex(1).border("none").outline("none")
|
|
.background("transparent").color("var(--headertext)")
|
|
.fontSize(0.9, em)
|
|
.onInput((e) => {
|
|
this.filters.keyword = e.target.value
|
|
const newFiltered = this.filteredJobs
|
|
if (this.selectedJobId && !newFiltered.find(j => j.id === this.selectedJobId)) {
|
|
this.selectedJobId = newFiltered[0]?.id || null
|
|
}
|
|
this.rerender()
|
|
})
|
|
})
|
|
.gap(0.55, em)
|
|
.marginLeft(60, px)
|
|
.paddingHorizontal(0.9, em)
|
|
.paddingVertical(0.6, em)
|
|
.background("var(--darkaccent)")
|
|
.border("1px solid var(--divider)")
|
|
.borderRadius(0.5, em)
|
|
.alignItems("center")
|
|
.flex(1)
|
|
|
|
HStack(() => {
|
|
p("📍")
|
|
.margin(0).fontSize(0.85, em).opacity(0.45).flexShrink(0)
|
|
input()
|
|
.attr({ type: "text", placeholder: "Location", value: this.filters.location })
|
|
.flex(1).border("none").outline("none")
|
|
.background("transparent").color("var(--headertext)")
|
|
.fontSize(0.9, em)
|
|
.onInput((e) => {
|
|
this.filters.location = e.target.value
|
|
const newFiltered = this.filteredJobs
|
|
if (this.selectedJobId && !newFiltered.find(j => j.id === this.selectedJobId)) {
|
|
this.selectedJobId = newFiltered[0]?.id || null
|
|
}
|
|
this.rerender()
|
|
})
|
|
})
|
|
.gap(0.55, em).paddingHorizontal(0.9, em).paddingVertical(0.6, em)
|
|
.background("var(--darkaccent)").border("1px solid var(--divider)")
|
|
.borderRadius(0.5, em).alignItems("center").width(200, px).flexShrink(0)
|
|
|
|
this.filterSelect([
|
|
{ label: "All Types", value: "" },
|
|
{ label: "Full-time", value: "full-time" },
|
|
{ label: "Part-time", value: "part-time" },
|
|
{ label: "Contract", value: "contract" },
|
|
{ label: "Internship", value: "internship" },
|
|
], this.filters.type, (v) => {
|
|
this.filters.type = v
|
|
const newFiltered = this.filteredJobs
|
|
if (this.selectedJobId && !newFiltered.find(j => j.id === this.selectedJobId)) {
|
|
this.selectedJobId = newFiltered[0]?.id || null
|
|
}
|
|
this.rerender()
|
|
})
|
|
|
|
this.filterSelect([
|
|
{ label: "All Levels", value: "" },
|
|
{ label: "Entry", value: "entry" },
|
|
{ label: "Mid", value: "mid" },
|
|
{ label: "Senior", value: "senior" },
|
|
], this.filters.level, (v) => {
|
|
this.filters.level = v
|
|
const newFiltered = this.filteredJobs
|
|
if (this.selectedJobId && !newFiltered.find(j => j.id === this.selectedJobId)) {
|
|
this.selectedJobId = newFiltered[0]?.id || null
|
|
}
|
|
this.rerender()
|
|
})
|
|
})
|
|
.gap(0.65, em).paddingHorizontal(1.5, em).paddingVertical(0.85, em)
|
|
.borderBottom("1px solid var(--divider)").alignItems("center")
|
|
.width(100, pct).boxSizing("border-box").flexShrink(0)
|
|
|
|
if (!this.loaded) {
|
|
VStack(() => LoadingCircle())
|
|
.flex(1)
|
|
.justifyContent("center")
|
|
.alignItems("center")
|
|
} else {
|
|
HStack(() => {
|
|
// Left: job list
|
|
VStack(() => {
|
|
DesktopJobsList(filtered, this.selectedJobId, (id) => {
|
|
this.selectedJobId = id;
|
|
this.rerender();
|
|
})
|
|
})
|
|
.width(360, px)
|
|
.minWidth(320, px)
|
|
.maxWidth(400, px)
|
|
.height(100, pct)
|
|
.borderRight("1px solid var(--divider)")
|
|
.flexShrink(0)
|
|
.overflow("hidden")
|
|
|
|
// Right: job detail
|
|
VStack(() => {
|
|
DesktopJobDetail(this.selectedJob)
|
|
})
|
|
.flex(1)
|
|
.height(100, pct)
|
|
.overflow("hidden")
|
|
})
|
|
.flex(1)
|
|
.minHeight(0)
|
|
.width(100, pct)
|
|
.overflow("hidden")
|
|
}
|
|
})
|
|
.height(100, pct)
|
|
.width(100, pct)
|
|
.overflow("hidden")
|
|
}
|
|
}
|
|
|
|
register(Jobs)
|