Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions apps/sim/app/(landing)/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { buildPostGraphJsonLd, buildPostMetadata } from '@/lib/blog/seo'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { ShareButton } from '@/app/(landing)/blog/[slug]/share-button'
import { BackLink } from '@/app/(landing)/components'
import { JsonLd } from '@/app/(landing)/components/json-ld'

export const dynamicParams = false

Expand Down Expand Up @@ -37,10 +38,7 @@ export default async function Page({ params }: { params: Promise<{ slug: string

return (
<article className='w-full bg-[var(--bg)]' itemScope itemType='https://schema.org/TechArticle'>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(graphJsonLd) }}
/>
<JsonLd data={graphJsonLd} />
<header className='mx-auto w-full max-w-[1446px] px-12 pt-[112px] max-sm:px-5 max-sm:pt-20 max-lg:px-8'>
<div className='mb-6'>
<BackLink href='/blog' label='Back to Blog' />
Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/(landing)/blog/[slug]/share-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export function ShareButton({ url, title }: ShareButtonProps) {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type='button'
className='flex items-center gap-1.5 text-[var(--text-muted)] text-sm hover:text-[var(--text-primary)]'
aria-label='Share this post'
>
Expand Down
6 changes: 2 additions & 4 deletions apps/sim/app/(landing)/blog/authors/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Image from 'next/image'
import Link from 'next/link'
import { getAllPostMeta } from '@/lib/blog/registry'
import { SITE_URL } from '@/lib/core/utils/urls'
import { JsonLd } from '@/app/(landing)/components/json-ld'

export const revalidate = 3600

Expand Down Expand Up @@ -82,10 +83,7 @@ export default async function AuthorPage({ params }: { params: Promise<{ id: str
}
return (
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(graphJsonLd) }}
/>
<JsonLd data={graphJsonLd} />
<div className='mb-6 flex items-center gap-3'>
{author.avatarUrl ? (
<Image
Expand Down
6 changes: 2 additions & 4 deletions apps/sim/app/(landing)/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Link from 'next/link'
import { getAllPostMeta } from '@/lib/blog/registry'
import { buildCollectionPageJsonLd } from '@/lib/blog/seo'
import { SITE_URL } from '@/lib/core/utils/urls'
import { JsonLd } from '@/app/(landing)/components/json-ld'

export async function generateMetadata({
searchParams,
Expand Down Expand Up @@ -91,10 +92,7 @@ export default async function BlogIndex({

return (
<section className='bg-[var(--bg)]'>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(collectionJsonLd) }}
/>
<JsonLd data={collectionJsonLd} />

{/* Section header */}
<div className='mx-auto w-full max-w-[1446px] px-12 pt-[112px] max-sm:px-5 max-sm:pt-20 max-lg:px-8'>
Expand Down
6 changes: 2 additions & 4 deletions apps/sim/app/(landing)/blog/tags/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ChipLink } from '@sim/emcn'
import type { Metadata } from 'next'
import { getAllTags } from '@/lib/blog/registry'
import { SITE_URL } from '@/lib/core/utils/urls'
import { JsonLd } from '@/app/(landing)/components/json-ld'

export const metadata: Metadata = {
title: 'Tags',
Expand Down Expand Up @@ -37,10 +38,7 @@ export default async function TagsIndex() {
const tags = await getAllTags()
return (
<section className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
/>
<JsonLd data={breadcrumbJsonLd} />
<h1 className='mb-6 text-[32px] text-[var(--text-primary)] leading-tight'>Browse by tag</h1>
<div className='flex flex-wrap gap-3'>
<ChipLink href='/blog' className='border border-[var(--border-1)]'>
Expand Down
6 changes: 4 additions & 2 deletions apps/sim/app/(landing)/careers/careers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ interface CareersProps {
* never flashes unfiltered before the client board hydrates.
*/
export default async function Careers({ searchParams }: CareersProps) {
const { team, location } = await careersSearchParamsCache.parse(searchParams)
const postings = await getAshbyJobs()
const [{ team, location }, postings] = await Promise.all([
careersSearchParamsCache.parse(searchParams),
getAshbyJobs(),
])
const fallbackGroups = groupByDepartment(filterPostings(postings, team, location))

return (
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/(landing)/careers/components/job-board/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { JobBoard } from './job-board'
export { filterPostings, groupByDepartment, hasActiveFilters, JobGroups } from './job-groups'
export { JobGroups } from './job-groups'
export { filterPostings, groupByDepartment, hasActiveFilters } from './utils'
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
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,
JobGroups,
} from '@/app/(landing)/careers/components/job-board/job-groups'
} from '@/app/(landing)/careers/components/job-board/utils'
import {
ALL_FILTER_VALUE,
careersParsers,
Expand Down Expand Up @@ -46,7 +46,7 @@ export function JobBoard({ postings }: JobBoardProps) {

const teamOptions = toFilterOptions(uniqueSorted(postings.map((p) => p.department)), 'All teams')
const locationOptions = toFilterOptions(
uniqueSorted(postings.map((p) => p.location).filter(Boolean)),
uniqueSorted(postings.flatMap((p) => (p.location ? [p.location] : []))),
'All locations'
)
const groups = groupByDepartment(filterPostings(postings, team, location))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,55 +1,13 @@
import { cn } from '@sim/emcn'
import { ArrowRight } from '@sim/emcn/icons'
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
}
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.'

/**
* 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 }))
}

interface JobGroupsProps {
groups: DepartmentGroup[]
/**
Expand Down
44 changes: 44 additions & 0 deletions apps/sim/app/(landing)/careers/components/job-board/utils.ts
Original file line number Diff line number Diff line change
@@ -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 }))
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { type ReactNode, useState } from 'react'
import { type ReactNode, useRef, useState } from 'react'
import { Streamdown } from 'streamdown'
import 'streamdown/styles.css'
import { Avatar, AvatarFallback, AvatarImage, Chip, cn } from '@sim/emcn'
Expand Down Expand Up @@ -63,15 +63,15 @@ function formatDate(value: string): string {

export function ChangelogTimeline({ initialEntries }: ChangelogTimelineProps) {
const [entries, setEntries] = useState<ChangelogEntry[]>(initialEntries)
const [page, setPage] = useState<number>(1)
const [loading, setLoading] = useState<boolean>(false)
const [done, setDone] = useState<boolean>(false)
const pageRef = useRef(1)

const loadMore = async () => {
if (loading || done) return
setLoading(true)
try {
const nextPage = page + 1
const nextPage = pageRef.current + 1
// boundary-raw-fetch: external GitHub Releases API (cross-origin), not a same-origin contract
const res = await fetch(releasesEndpoint(nextPage), {
headers: { Accept: 'application/vnd.github+json' },
Expand All @@ -83,7 +83,7 @@ export function ChangelogTimeline({ initialEntries }: ChangelogTimelineProps) {
setDone(true)
} else {
setEntries((prev) => [...prev, ...mapped])
setPage(nextPage)
pageRef.current = nextPage
}
} catch {
setDone(true)
Expand Down
27 changes: 15 additions & 12 deletions apps/sim/app/(landing)/changelog/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,20 @@ export function extractMentions(body: string): string[] {

/** Maps non-prerelease GitHub releases to normalized {@link ChangelogEntry} items. */
export function mapReleases(releases: GitHubRelease[]): ChangelogEntry[] {
return releases
.filter((release) => !release.prerelease)
.map((release) => {
const body = String(release.body ?? '')
return {
tag: release.tag_name,
title: release.name || release.tag_name,
content: sanitizeContent(body),
date: release.published_at,
url: release.html_url,
contributors: extractMentions(body),
}
return releases.reduce<ChangelogEntry[]>((acc, release) => {
if (release.prerelease) {
return acc
}

const body = String(release.body ?? '')
acc.push({
tag: release.tag_name,
title: release.name || release.tag_name,
content: sanitizeContent(body),
date: release.published_at,
url: release.html_url,
contributors: extractMentions(body),
})
return acc
}, [])
}
Loading
Loading