chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,3 @@
export { JobBoard } from './job-board'
export { JobGroups } from './job-groups'
export { filterPostings, groupByDepartment, hasActiveFilters } from './utils'
@@ -0,0 +1,74 @@
'use client'
import { ChipSelect, type ChipSelectOption } from '@sim/emcn'
import { useQueryStates } from 'nuqs'
import type { CareerPosting } from '@/lib/ashby/jobs'
import { JobGroups } from '@/app/(landing)/careers/components/job-board/job-groups'
import {
filterPostings,
groupByDepartment,
hasActiveFilters,
} from '@/app/(landing)/careers/components/job-board/utils'
import {
ALL_FILTER_VALUE,
careersParsers,
careersUrlKeys,
} from '@/app/(landing)/careers/search-params'
interface JobBoardProps {
postings: CareerPosting[]
}
/** Builds `{ label, value }` options for a filter, with an "All" row at the top. */
function toFilterOptions(values: string[], allLabel: string): ChipSelectOption[] {
return [
{ label: allLabel, value: ALL_FILTER_VALUE },
...values.map((value) => ({ label: value, value })),
]
}
/** Distinct, alphabetically sorted values from a list. */
function uniqueSorted(values: string[]): string[] {
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b))
}
/**
* The interactive open-roles board — the single `'use client'` leaf on the
* careers page. Every posting is server-rendered into the HTML (via the static
* {@link JobGroups} Suspense fallback in `careers.tsx`), so all roles stay
* crawlable; this leaf hydrates on top to add Team/Location filtering. Filter
* state lives in the URL via nuqs (`?team=`/`?location=`) so a filtered view is
* shareable and survives reload/back-forward. The filter set is small and
* static, so filtering reads the instant URL value directly (no debounce).
*/
export function JobBoard({ postings }: JobBoardProps) {
const [{ team, location }, setFilters] = useQueryStates(careersParsers, careersUrlKeys)
const teamOptions = toFilterOptions(uniqueSorted(postings.map((p) => p.department)), 'All teams')
const locationOptions = toFilterOptions(
uniqueSorted(postings.flatMap((p) => (p.location ? [p.location] : []))),
'All locations'
)
const groups = groupByDepartment(filterPostings(postings, team, location))
return (
<div className='flex flex-col gap-10'>
<div className='flex flex-wrap items-center gap-3'>
<ChipSelect
options={teamOptions}
value={team}
onChange={(value) => setFilters({ team: value })}
aria-label='Filter roles by team'
/>
<ChipSelect
options={locationOptions}
value={location}
onChange={(value) => setFilters({ location: value })}
aria-label='Filter roles by location'
/>
</div>
<JobGroups groups={groups} filtersActive={hasActiveFilters(team, location)} />
</div>
)
}
@@ -0,0 +1,114 @@
import { cn } from '@sim/emcn'
import { ArrowRight } from '@sim/emcn/icons'
import type { CareerPosting } from '@/lib/ashby/jobs'
import type { DepartmentGroup } from '@/app/(landing)/careers/components/job-board/utils'
/** Empty-state copy: distinguishes a truly empty board from a filtered-to-zero view. */
const NO_OPEN_ROLES_MESSAGE = 'No open roles right now — check back soon.'
const NO_MATCHING_ROLES_MESSAGE =
'No roles match these filters right now. Try clearing them, or check back soon.'
interface JobGroupsProps {
groups: DepartmentGroup[]
/**
* Whether a Team/Location filter is active. Selects the empty-state copy so an
* unfiltered empty board ("no open roles") never reads as a filtered miss ("no
* matches") — and the server fallback and client board always agree.
*/
filtersActive?: boolean
}
/**
* The presentational open-roles list: one labeled section per department, each a
* list of {@link JobRow}s. Server-safe (no client hooks) so it renders both as
* the static Suspense fallback and inside the client {@link JobBoard}.
*/
export function JobGroups({ groups, filtersActive = false }: JobGroupsProps) {
if (groups.length === 0) {
return (
<p className='py-10 text-[var(--text-muted)] text-base'>
{filtersActive ? NO_MATCHING_ROLES_MESSAGE : NO_OPEN_ROLES_MESSAGE}
</p>
)
}
return (
<div className='flex flex-col gap-12'>
{groups.map((group) => (
<section
key={group.department}
aria-label={`${group.department} roles`}
className='flex flex-col'
>
<h3 className='pb-2 font-medium text-[var(--text-muted)] text-sm'>{group.department}</h3>
<ul className='flex flex-col'>
{group.postings.map((posting) => (
<li key={posting.id}>
<JobRow posting={posting} />
</li>
))}
</ul>
</section>
))}
</div>
)
}
interface JobRowProps {
posting: CareerPosting
}
/**
* A single role row: title over a metadata line, with an "Apply" affordance that
* links out to the posting on Ashby. The whole row is the link target; hovering
* tints the row and advances the arrow. The metadata values are de-duplicated
* because a remote posting normalizes both `location` and `workplaceType` to
* "Remote", which would otherwise render "Remote · Remote" and collide as keys.
*/
function JobRow({ posting }: JobRowProps) {
const meta = Array.from(
new Set(
[
posting.location,
posting.employmentType,
posting.workplaceType,
posting.compensationSummary,
].filter((value): value is string => Boolean(value))
)
)
return (
<a
href={posting.jobUrl}
target='_blank'
rel='noopener noreferrer'
className={cn(
'-mx-3 group flex items-center justify-between gap-6 rounded-lg border-[var(--border)]',
'border-t px-3 py-5 transition-colors hover:bg-[var(--surface-hover)]'
)}
>
<div className='flex min-w-0 flex-col gap-1.5'>
<h4 className='truncate font-medium text-[var(--text-primary)] text-base'>
{posting.title}
</h4>
<div className='flex flex-wrap items-center gap-x-2 gap-y-1 text-[var(--text-muted)] text-sm'>
{meta.map((item, index) => (
<span key={item} className='flex items-center gap-2'>
{index > 0 && (
<span aria-hidden className='text-[var(--text-muted)]'>
·
</span>
)}
{item}
</span>
))}
</div>
</div>
<span className='flex shrink-0 items-center gap-1.5 font-medium text-[var(--text-body)] text-sm'>
Apply
<ArrowRight className='size-[14px] text-[var(--text-icon)] transition-transform group-hover:translate-x-0.5' />
</span>
</a>
)
}
@@ -0,0 +1,44 @@
import type { CareerPosting } from '@/lib/ashby/jobs'
import { ALL_FILTER_VALUE } from '@/app/(landing)/careers/search-params'
export interface DepartmentGroup {
department: string
postings: CareerPosting[]
}
/**
* Narrows postings to a selected Team and Location, treating {@link ALL_FILTER_VALUE}
* as "any". Shared by the server-rendered fallback and the client board so a
* deep-linked filter resolves to the exact same set on both sides.
*/
export function filterPostings(
postings: CareerPosting[],
team: string,
location: string
): CareerPosting[] {
return postings.filter(
(posting) =>
(team === ALL_FILTER_VALUE || posting.department === team) &&
(location === ALL_FILTER_VALUE || posting.location === location)
)
}
/** Whether either the Team or Location filter is narrowing the board. */
export function hasActiveFilters(team: string, location: string): boolean {
return team !== ALL_FILTER_VALUE || location !== ALL_FILTER_VALUE
}
/**
* Buckets postings by department, preserving their incoming order (the fetcher
* pre-sorts by department then title). Shared by the interactive board and its
* static Suspense fallback so the two can never render a different grouping.
*/
export function groupByDepartment(postings: CareerPosting[]): DepartmentGroup[] {
const byDepartment = new Map<string, CareerPosting[]>()
for (const posting of postings) {
const bucket = byDepartment.get(posting.department)
if (bucket) bucket.push(posting)
else byDepartment.set(posting.department, [posting])
}
return Array.from(byDepartment, ([department, items]) => ({ department, postings: items }))
}