SPB Git

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%
3.6 KB · 96 lines tsx
Raw Blame History
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