spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import type { MetroSummary } from "@dci/core";2import type { Metadata } from "next";3import Link from "next/link";4import { Breadcrumbs } from "@/components/entity/breadcrumbs";5import { EstimateMark } from "@/components/ui/badge";6import { Button } from "@/components/ui/button";7import { Page, PageHeader } from "@/components/ui/card";8import { CountryFlag } from "@/components/ui/country-flag";9import { FilterChips, type Chip } from "@/components/ui/filter-chips";10import { Pagination } from "@/components/ui/pagination";11import { EmptyState } from "@/components/ui/states";12import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";13import { api } from "@/lib/api";14import { fmtInt, fmtMw } from "@/lib/format";15import { routes } from "@/lib/routes";16import { pageMetadata } from "@/lib/seo";17import { compareBy, hrefWith, paginate, parseSort, sortTh, spGet, spInt, type SearchParams } from "@/lib/sort";18import { countryHref, countrySlugMap, pipelineMw } from "../country-links";1920export const revalidate = 120;2122export const metadata: Metadata = pageMetadata({23 title: "Data center markets",24 description: "Metro markets ranked by facilities, known capacity and pipeline — operators, cloud regions and internet exchanges per market.",25 path: routes.markets(),26});2728const SORT_KEYS = ["facilities", "mw", "pipeline", "operators", "cloud", "name"] as const;29type SortKey = (typeof SORT_KEYS)[number];30const PER_PAGE = 50;31const BASE = routes.markets();3233const getter: Record<SortKey, (m: MetroSummary) => number | string | null> = {34 facilities: (m) => m.facilityCount,35 mw: (m) => m.knownMw,36 pipeline: (m) => pipelineMw(m),37 operators: (m) => m.operatorCount,38 cloud: (m) => m.cloudRegionCount,39 name: (m) => m.name,40};4142export default async function MarketsPage({ searchParams }: { searchParams: Promise<SearchParams> }) {43 const sp = await searchParams;44 const q = spGet(sp, "q");45 const country = spGet(sp, "country")?.toUpperCase();46 const sort = parseSort<SortKey>(sp, SORT_KEYS, { key: "facilities", order: "desc" });47 const [res, { map, countries }] = await Promise.all([api.metros({ q, country }), countrySlugMap()]);48 const ok = res.items.length > 0;4950 const sorted = [...res.items].sort((a, b) => compareBy(getter[sort.key], sort.order)(a, b) || a.name.localeCompare(b.name));51 const { items, page } = paginate(sorted, spInt(sp, "page"), PER_PAGE);52 const th = (key: SortKey) => sortTh(BASE, sp, sort, key);5354 const chips: Chip[] = [];55 if (q) chips.push({ key: "q", label: "Search", value: q, removeHref: hrefWith(BASE, sp, { q: undefined }) });56 if (country) chips.push({ key: "country", label: "Country", value: countries.find((c) => c.iso2 === country)?.name ?? country, removeHref: hrefWith(BASE, sp, { country: undefined }) });5758 return (59 <Page>60 <Breadcrumbs items={[{ name: "Data centers", href: routes.datacenters() }, { name: "Markets", href: BASE }]} className="pt-3" />61 <PageHeader62 eyebrow="Index"63 title="Data center markets"64 description={ok ? `${fmtInt(sorted.length)} metro markets. Ranked by facilities, known capacity and pipeline; MW figures are lower bounds from published values only.` : "Metro markets ranked by facilities, known capacity and pipeline."}65 aside={66 <Button href={routes.map()} size="sm">67 Open map68 </Button>69 }70 />71 <form method="get" action={BASE} className="flex flex-col gap-2 border-y border-hair py-3 sm:flex-row sm:items-center">72 <input type="search" name="q" defaultValue={q ?? ""} placeholder="Search markets or sub-markets" aria-label="Search markets" className="h-9 min-w-0 flex-1 rounded-sm border border-hair-2 bg-surface px-3 text-[13px] text-ink outline-none placeholder:text-ink-4 focus-visible:ring-2 focus-visible:ring-accent" />73 <div className="flex gap-2">74 <select name="country" defaultValue={country ?? ""} aria-label="Country" className="h-9 min-w-0 flex-1 rounded-sm border border-hair-2 bg-surface px-2 text-[13px] text-ink outline-none focus-visible:ring-2 focus-visible:ring-accent sm:w-[200px] sm:flex-none">75 <option value="">All countries</option>76 {countries.map((c) => (77 <option key={c.iso2} value={c.iso2}>78 {c.name}79 </option>80 ))}81 </select>82 {sort.key !== "facilities" && <input type="hidden" name="sort" value={sort.key} />}83 {sort.key !== "facilities" && <input type="hidden" name="order" value={sort.order} />}84 <Button type="submit" variant="primary" size="md">85 Filter86 </Button>87 </div>88 </form>89 <FilterChips chips={chips} resetHref={BASE} className="py-2" />9091 <div className="pt-1">92 {items.length === 0 ? (93 <EmptyState compact title={ok ? "No markets match" : "Markets unavailable"} description={ok ? "Try a different search or country." : "The index API is not reachable right now."} />94 ) : (95 <Table>96 <THead>97 <tr>98 <Th {...th("name")}>Market</Th>99 <Th className="hidden sm:table-cell">Country</Th>100 <Th numeric {...th("facilities")}>101 Facilities102 </Th>103 <Th className="hidden lg:table-cell" title="operational · construction · planned">104 Op · Con · Plan105 </Th>106 <Th numeric {...th("mw")}>107 Known MW108 </Th>109 <Th numeric {...th("pipeline")}>110 Pipeline111 </Th>112 <Th numeric className="hidden md:table-cell" {...th("operators")}>113 Operators114 </Th>115 <Th numeric className="hidden md:table-cell" {...th("cloud")}>116 Cloud regions117 </Th>118 <Th numeric className="hidden lg:table-cell">119 IXPs120 </Th>121 <Th numeric className="hidden lg:table-cell">122 Projects123 </Th>124 </tr>125 </THead>126 <TBody>127 {items.map((m) => {128 const pipe = pipelineMw(m);129 const cHref = countryHref(map, m.countryIso2);130 return (131 <Tr key={m.id}>132 <Td primary>133 <RowLink href={routes.market(m.slug)}>{m.name}</RowLink>134 <div className="text-[11.5px] text-ink-3 sm:hidden">135 {m.countryName ?? m.countryIso2}136 {m.regionName && m.regionName !== m.name ? ` · ${m.regionName}` : ""}137 </div>138 </Td>139 <Td label="Country" className="hidden sm:table-cell">140 {cHref ? (141 <Link href={cHref} className="text-ink-2 hover:text-ink hover:underline">142 <CountryFlag iso2={m.countryIso2} withName className="max-w-[180px]" />143 </Link>144 ) : (145 <CountryFlag iso2={m.countryIso2} withName />146 )}147 </Td>148 <Td label="Facilities" numeric lead>149 {fmtInt(m.facilityCount)}150 </Td>151 <Td label="Op · Con · Plan" className="figure hidden text-[12px] text-ink-3 lg:table-cell" title="operational · under construction · planned">152 {fmtInt(m.operationalCount)} · {fmtInt(m.constructionCount)} · {fmtInt(m.plannedCount)}153 </Td>154 <Td label="Known MW" numeric>155 {m.knownMw === null ? <span className="text-ink-4">—</span> : fmtMw(m.knownMw)}156 </Td>157 <Td label="Pipeline" numeric>158 {pipe === null ? (159 <span className="text-ink-4">—</span>160 ) : (161 <span title="Under construction + planned power (published figures only)">162 <EstimateMark className="mr-0.5" />163 {fmtMw(pipe)}164 </span>165 )}166 </Td>167 <Td label="Operators" numeric className="hidden md:table-cell">168 {fmtInt(m.operatorCount)}169 </Td>170 <Td label="Cloud regions" numeric className="hidden md:table-cell">171 {m.cloudRegionCount ? fmtInt(m.cloudRegionCount) : <span className="text-ink-4">—</span>}172 </Td>173 <Td label="IXPs" numeric className="hidden lg:table-cell">174 {m.ixpCount ? fmtInt(m.ixpCount) : <span className="text-ink-4">—</span>}175 </Td>176 <Td label="Projects" numeric className="hidden lg:table-cell">177 {m.projectCount ? fmtInt(m.projectCount) : <span className="text-ink-4">—</span>}178 </Td>179 </Tr>180 );181 })}182 </TBody>183 </Table>184 )}185 </div>186 <Pagination page={page} perPage={PER_PAGE} total={sorted.length} hrefFor={(p) => hrefWith(BASE, sp, { page: p > 1 ? p : undefined })} label="markets" className="mt-3" />187 <p className="mt-6 text-[11.5px] text-ink-4">188 Facilities are assigned to a market when they lie within its radius or their city matches one of its sub-markets; markets never span borders.{" "}189 <Link href={routes.methodology()} className="underline decoration-hair-2 hover:text-ink">190 Methodology191 </Link>192 </p>193 </Page>194 );195}196