spb/airiskindex Public
The most methodologically rigorous, fully transparent AI job-exposure index.
TypeScript 88%
Python 6.1%
SQL 2.7%
CSS 1.2%
JavaScript 0.9%
Shell 0.8%
1// File: page.tsx2// Path: apps/web/app/occupations/page.tsx3// Project: AI Risk Index — airiskindex.io4// Author: Simon-Pierre Boucher5// Contact: contact@spboucher.ai6// Copyright © 2026 Simon-Pierre Boucher. All rights reserved.7//8// Description: Browse page: all occupations grouped by SOC major group with score chips.910import Link from "next/link";11import { prisma } from "@airiskindex/db";12import { SOC_MAJOR_GROUPS } from "@/lib/soc-groups";1314export const dynamic = "force-dynamic";1516export const metadata = {17 title: "All occupations — AI Risk Index",18};1920export default async function OccupationsPage(): Promise<JSX.Element> {21 const run = await prisma.scoreRun.findFirst({ orderBy: { createdAt: "desc" } });22 const [occupations, scores] = await Promise.all([23 prisma.occupation.findMany({24 orderBy: { code: "asc" },25 select: { code: true, title: true },26 }),27 run28 ? prisma.occupationScore.findMany({29 where: { runId: run.id },30 select: { occupationCode: true, substitution: true },31 })32 : Promise.resolve([]),33 ]);34 const scoreByCode = new Map(scores.map((s) => [s.occupationCode, s.substitution]));3536 const groups = new Map<string, typeof occupations>();37 for (const occupation of occupations) {38 const prefix = occupation.code.slice(0, 2);39 const list = groups.get(prefix) ?? [];40 list.push(occupation);41 groups.set(prefix, list);42 }4344 return (45 <main className="mx-auto max-w-5xl px-4 py-10 sm:px-6 sm:py-12">46 <h1 className="text-3xl font-bold tracking-tight">All occupations</h1>47 <p className="mt-2 text-[var(--ink-2)]">48 {occupations.length.toLocaleString("en-US")} O*NET occupations across{" "}49 {groups.size} major groups. Occupations without a chip have not been scored in the latest50 run yet.51 </p>5253 <nav className="mt-6 flex flex-wrap gap-2 text-xs">54 {[...groups.keys()].sort().map((prefix) => (55 <a56 key={prefix}57 href={`#g${prefix}`}58 className="rounded-full border border-[var(--border)] bg-[var(--surface-1)] px-3 py-1 text-[var(--ink-2)] hover:border-[var(--seq)]"59 >60 {SOC_MAJOR_GROUPS[prefix] ?? prefix}61 </a>62 ))}63 </nav>6465 {[...groups.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([prefix, list]) => (66 <section key={prefix} id={`g${prefix}`} className="mt-10 scroll-mt-6">67 <h2 className="text-lg font-semibold tracking-tight">68 {SOC_MAJOR_GROUPS[prefix] ?? prefix}{" "}69 <span className="text-sm font-normal text-[var(--muted)]">({list.length})</span>70 </h2>71 <ul className="mt-3 grid gap-x-8 sm:grid-cols-2">72 {list.map((occupation) => {73 const score = scoreByCode.get(occupation.code);74 return (75 <li key={occupation.code} className="border-b border-[var(--grid)]">76 <Link77 href={`/occupations/${occupation.code}`}78 className="flex items-baseline gap-2 py-2 hover:bg-[var(--wash)]"79 >80 <span className="min-w-0 truncate text-sm">{occupation.title}</span>81 {score !== undefined && (82 <span className="ml-auto shrink-0 rounded-full bg-[var(--seq-track)] px-2 py-0.5 text-xs font-semibold tabular-nums">83 {score.toFixed(0)}84 </span>85 )}86 </Link>87 </li>88 );89 })}90 </ul>91 </section>92 ))}93 </main>94 );95}96