SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
6 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
10.6 KB · 235 lines tsx
Raw Blame History
1import type { CountrySummary } from "@dci/core";2import type { Metadata } from "next";3import Link from "next/link";4import { EstimateMark } from "@/components/ui/badge";5import { Page, PageHeader } from "@/components/ui/card";6import { cn } from "@/components/ui/cn";7import { CountryFlag } from "@/components/ui/country-flag";8import { EmptyState, OfflineNotice } from "@/components/ui/states";9import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";10import { api } from "@/lib/api";11import { fmtInt, fmtMw, fmtPct, pluralize } from "@/lib/format";12import { routes } from "@/lib/routes";13import { compareBy, hrefWith, parseSort, sortTh, spGet, type SearchParams, type SortState } from "@/lib/sort";1415export const revalidate = 120;1617export const metadata: Metadata = {18  title: "Countries",19  description: "Data center facilities, known and pipeline capacity, operators and cloud regions by country, grouped by world region.",20  alternates: { canonical: "/countries" },21};2223const SORTS = ["name", "facilities", "mw", "pipeline", "cloud", "operators", "per_million", "hyperscale"] as const;24type SortKey = (typeof SORTS)[number];25const REGION_ORDER = ["Americas", "Europe", "Asia", "Oceania", "Africa"];2627const pipelineMw = (c: CountrySummary): number | null => (c.constructionMw === null && c.plannedMw === null ? null : (c.constructionMw ?? 0) + (c.plannedMw ?? 0));28const perMillion = (c: CountrySummary): number | null => (c.population && c.facilityCount >= 5 ? c.facilityCount / (c.population / 1_000_000) : null);2930function sorter(s: SortState<SortKey>) {31  switch (s.key) {32    case "name":33      return compareBy<CountrySummary>((c) => c.name, s.order);34    case "mw":35      return compareBy<CountrySummary>((c) => c.knownMw, s.order);36    case "pipeline":37      return compareBy<CountrySummary>(pipelineMw, s.order);38    case "cloud":39      return compareBy<CountrySummary>((c) => c.cloudRegionCount, s.order);40    case "operators":41      return compareBy<CountrySummary>((c) => c.operatorCount, s.order);42    case "per_million":43      return compareBy<CountrySummary>(perMillion, s.order);44    case "hyperscale":45      return compareBy<CountrySummary>((c) => c.hyperscaleCount + c.aiCount, s.order);46    default:47      return compareBy<CountrySummary>((c) => c.facilityCount, s.order);48  }49}5051function CountryTable({ rows, base, sp, sort }: { rows: CountrySummary[]; base: string; sp: SearchParams; sort: SortState<SortKey> }) {52  const th = (key: SortKey) => sortTh(base, sp, sort, key);53  return (54    <Table mobileCollapse={{ total: rows.length, label: "countries" }}>55      <THead>56        <tr>57          <Th {...th("name")}>Country</Th>58          <Th numeric {...th("facilities")}>59            Facilities60          </Th>61          <Th numeric {...th("mw")}>62            Known MW63          </Th>64          <Th numeric {...th("pipeline")}>65            Pipeline66          </Th>67          <Th numeric {...th("cloud")}>68            Cloud regions69          </Th>70          <Th numeric className="hidden md:table-cell" {...th("operators")}>71            Operators72          </Th>73          <Th numeric className="hidden lg:table-cell" {...th("per_million")}>74            Per million75          </Th>76          <Th numeric className="hidden lg:table-cell" {...th("hyperscale")}>77            Hyperscale · AI78          </Th>79        </tr>80      </THead>81      <TBody>82        {rows.map((c) => {83          const pipe = pipelineMw(c);84          const pm = perMillion(c);85          return (86            <Tr key={c.iso2}>87              <Td primary>88                <RowLink href={routes.country(c.slug)} className="inline-flex items-center gap-2">89                  <CountryFlag iso2={c.iso2} size={14} />90                  {c.name}91                </RowLink>92                <div className="text-[11.5px] text-ink-3 sm:hidden">{c.subregion ?? c.region ?? ""}</div>93              </Td>94              <Td label="Facilities" numeric lead>95                {fmtInt(c.facilityCount)}96              </Td>97              <Td label="Known MW" numeric>98                {c.knownMw === null ? (99                  <span className="text-ink-4" title={`No published MW figure · coverage ${fmtPct(c.mwCoverage)}`}>100                    —101                  </span>102                ) : (103                  <span>104                    {fmtMw(c.knownMw)}105                    <span className="hidden text-[10.5px] text-ink-4 sm:block" title="Share of facilities with a published MW figure">106                      {fmtPct(c.mwCoverage)} coverage107                    </span>108                  </span>109                )}110              </Td>111              <Td label="Pipeline" numeric>112                {pipe === null ? (113                  <span className="text-ink-4">—</span>114                ) : (115                  <span title="Under construction + planned power (lower bound)">116                    <EstimateMark className="mr-0.5" />117                    {fmtMw(pipe)}118                  </span>119                )}120              </Td>121              <Td label="Cloud regions" numeric>122                {fmtInt(c.cloudRegionCount)}123              </Td>124              <Td label="Operators" numeric className="hidden md:table-cell">125                {fmtInt(c.operatorCount)}126              </Td>127              <Td label="Per million" numeric className="hidden lg:table-cell">128                {pm === null ? <span className="text-ink-4" title="Needs ≥ 5 facilities and a population figure">—</span> : pm.toFixed(2)}129              </Td>130              <Td label="Hyperscale · AI" numeric className="hidden lg:table-cell">131                {fmtInt(c.hyperscaleCount)} · {fmtInt(c.aiCount)}132              </Td>133            </Tr>134          );135        })}136      </TBody>137    </Table>138  );139}140141export default async function CountriesPage({ searchParams }: { searchParams: Promise<SearchParams> }) {142  const sp = await searchParams;143  const base = routes.countries();144  const region = spGet(sp, "region");145  const hasSort = Boolean(spGet(sp, "sort"));146  const sort = parseSort(sp, SORTS, { key: "facilities", order: "desc" });147148  const res = await api.countries();149  const ok = res.items.length > 0 || (await api.health()).ok;150  const all = res.items.filter((c) => c.facilityCount > 0 || c.cloudRegionCount > 0);151  const regions = Array.from(new Set(all.map((c) => c.region).filter((x): x is string => Boolean(x)))).sort((a, b) => {152    const ia = REGION_ORDER.indexOf(a);153    const ib = REGION_ORDER.indexOf(b);154    return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib) || a.localeCompare(b);155  });156  const filtered = (region ? all.filter((c) => c.region === region) : all).sort(sorter(sort));157  const totals = filtered.reduce((a, c) => ({ f: a.f + c.facilityCount, cr: a.cr + c.cloudRegionCount, mw: a.mw + (c.knownMw ?? 0), cov: a.cov + (c.knownMw !== null ? 1 : 0) }), { f: 0, cr: 0, mw: 0, cov: 0 });158159  const chipCls = (active: boolean) => cn("inline-flex h-7 shrink-0 items-center whitespace-nowrap rounded-sm border px-2 text-[12px]", active ? "border-ink bg-ink text-paper" : "border-hair-2 bg-surface text-ink hover:bg-surface-2");160161  return (162    <Page>163      <PageHeader164        eyebrow="Index"165        title="Countries"166        description={167          ok168            ? `${fmtInt(filtered.length)} ${pluralize(filtered.length, "country", "countries")} with indexed infrastructure${region ? ` in ${region}` : ""} — ${fmtInt(totals.f)} facilities, ${fmtInt(totals.cr)} cloud regions${totals.mw ? `, ${fmtMw(totals.mw)} known operational capacity` : ""}. MW totals are lower bounds from published figures; coverage is shown per country.`169            : "Data center facilities, capacity and cloud regions by country."170        }171      />172      {!ok && <OfflineNotice className="mb-3" />}173174      <nav aria-label="Region" className="-mx-3 mb-3 flex items-center gap-1.5 overflow-x-auto px-3 no-scrollbar sm:mx-0 sm:flex-wrap sm:px-0">175        <Link href={hrefWith(base, sp, { region: undefined })} scroll={false} className={chipCls(!region)} aria-current={!region ? "page" : undefined}>176          All regions177        </Link>178        {regions.map((rg) => (179          <Link key={rg} href={hrefWith(base, sp, { region: rg })} scroll={false} className={chipCls(region === rg)} aria-current={region === rg ? "page" : undefined}>180            {rg}181            <span className="figure ml-1.5 text-[11px] opacity-70">{all.filter((c) => c.region === rg).length}</span>182          </Link>183        ))}184        {hasSort && (185          <Link href={hrefWith(base, sp, { sort: undefined, order: undefined })} scroll={false} className="shrink-0 px-1 text-[12px] text-ink-3 hover:text-ink">186            Group by region187          </Link>188        )}189      </nav>190191      {filtered.length === 0 ? (192        <EmptyState title={ok ? "No countries match" : "Countries unavailable"} description={ok ? "Pick another region." : "The index API is not reachable right now."} compact />193      ) : hasSort || region ? (194        <CountryTable rows={filtered} base={base} sp={sp} sort={sort} />195      ) : (196        <div className="flex flex-col gap-8">197          {regions.map((rg) => {198            const rows = filtered.filter((c) => c.region === rg);199            if (!rows.length) return null;200            const f = rows.reduce((a, c) => a + c.facilityCount, 0);201            return (202              <section key={rg} aria-labelledby={`region-${rg}`}>203                <div className="mb-2 flex items-baseline justify-between gap-3 border-t border-hair pt-4">204                  <h2 id={`region-${rg}`} className="text-[15px] font-semibold text-ink">205                    {rg}206                  </h2>207                  <span className="figure text-[12px] text-ink-3">208                    {fmtInt(rows.length)} {pluralize(rows.length, "country", "countries")} · {fmtInt(f)} facilities209                  </span>210                </div>211                <CountryTable rows={rows} base={base} sp={sp} sort={sort} />212              </section>213            );214          })}215          {filtered.some((c) => !c.region) && (216            <section>217              <div className="mb-2 border-t border-hair pt-4">218                <h2 className="text-[15px] font-semibold text-ink">Other</h2>219              </div>220              <CountryTable rows={filtered.filter((c) => !c.region)} base={base} sp={sp} sort={sort} />221            </section>222          )}223        </div>224      )}225      <p className="mt-6 text-[11.5px] text-ink-4">226        Per-million uses the latest World Bank population vintage and requires at least 5 facilities. Known MW = IT capacity, or total power when only that is published, over operational facilities. See the{" "}227        <Link href={routes.methodology()} className="underline decoration-hair-2 hover:text-ink">228          methodology229        </Link>230        .231      </p>232    </Page>233  );234}235