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/ranking/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: Full index — every scored occupation ranked, paginated by 50.910import Link from "next/link";11import { prisma } from "@airiskindex/db";12import { ScoreBar } from "@/components/score-marks";13import { formatWage } from "@/lib/soc-groups";1415export const dynamic = "force-dynamic";1617export const metadata = {18 title: "Full index — AI Risk Index",19 description: "Every scored occupation ranked by substitution pressure, with confidence intervals.",20};2122const PER_PAGE = 50;2324export default async function RankingPage({25 searchParams,26}: {27 searchParams: { page?: string };28}): Promise<JSX.Element> {29 const run = await prisma.scoreRun.findFirst({ orderBy: { createdAt: "desc" } });30 const total = run ? await prisma.occupationScore.count({ where: { runId: run.id } }) : 0;31 const pages = Math.max(1, Math.ceil(total / PER_PAGE));32 const page = Math.min(pages, Math.max(1, Number(searchParams.page ?? "1") || 1));3334 const rows = run35 ? await prisma.occupationScore.findMany({36 where: { runId: run.id },37 orderBy: { substitution: "desc" },38 skip: (page - 1) * PER_PAGE,39 take: PER_PAGE,40 include: {41 occupation: {42 select: { code: true, title: true, medianWageCents: true, employment: true },43 },44 },45 })46 : [];4748 const pager = (49 <div className="flex flex-wrap items-center justify-between gap-3 text-sm">50 <div className="flex gap-2">51 <Link52 href={`/ranking?page=${page - 1}`}53 aria-disabled={page <= 1}54 className={`card px-4 py-2 font-medium ${page <= 1 ? "pointer-events-none opacity-40" : "card-hover"}`}55 >56 ← Previous57 </Link>58 <Link59 href={`/ranking?page=${page + 1}`}60 aria-disabled={page >= pages}61 className={`card px-4 py-2 font-medium ${page >= pages ? "pointer-events-none opacity-40" : "card-hover"}`}62 >63 Next →64 </Link>65 </div>66 <p className="text-[var(--muted)]">67 Page {page} / {pages} · ranks {(page - 1) * PER_PAGE + 1}–68 {Math.min(total, page * PER_PAGE)} of {total.toLocaleString("en-US")}69 </p>70 </div>71 );7273 return (74 <main className="mx-auto max-w-4xl px-4 py-10 sm:px-6 sm:py-14">75 <p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--muted)]">76 Full index{run ? ` · run ${run.indexVersion} · ${run.createdAt.toISOString().slice(0, 10)}` : ""}77 </p>78 <h1 className="mt-2 text-3xl font-bold tracking-tight sm:text-4xl">79 All {total.toLocaleString("en-US")} scored occupations80 </h1>81 <p className="mt-3 max-w-2xl text-[var(--ink-2)]">82 Ranked by composite substitution pressure (0–100). The whisker is the confidence interval83 from rater disagreement. Wages are U.S. national medians (BLS OEWS).84 </p>8586 <div className="mt-8">{pager}</div>8788 <ol className="card mt-4 overflow-hidden">89 {rows.map((row, index) => {90 const rank = (page - 1) * PER_PAGE + index + 1;91 const wage = formatWage(row.occupation.medianWageCents);92 return (93 <li key={row.occupationCode} className="border-b border-[var(--grid)] last:border-b-0">94 <Link95 href={`/occupations/${row.occupationCode}`}96 className="block px-4 py-3.5 transition-colors hover:bg-[var(--wash)] sm:px-5"97 >98 <div className="flex items-baseline gap-3">99 <span className="w-8 shrink-0 text-right text-sm tabular-nums text-[var(--muted)]">100 {rank}101 </span>102 <span className="min-w-0 truncate font-medium">{row.occupation.title}</span>103 <span className="ml-auto shrink-0 pl-3 text-sm font-semibold tabular-nums">104 {row.substitution.toFixed(0)}105 </span>106 </div>107 <div className="mt-2 pl-11">108 <ScoreBar109 band={{110 low: row.substitutionLow,111 score: row.substitution,112 high: row.substitutionHigh,113 }}114 thick={8}115 />116 <p className="mt-1.5 text-xs text-[var(--muted)]">117 CI {row.substitutionLow.toFixed(0)}–{row.substitutionHigh.toFixed(0)} ·118 exposure {row.exposure.toFixed(0)} · augmentation{" "}119 {row.augmentation.toFixed(0)}120 {wage && <> · {wage}</>}121 {row.occupation.employment != null && (122 <> · {row.occupation.employment.toLocaleString("en-US")} employed</>123 )}124 </p>125 </div>126 </Link>127 </li>128 );129 })}130 </ol>131132 <div className="mt-4">{pager}</div>133 </main>134 );135}136