spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
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