spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import type { OperatorKind } from "@dci/core";2import { OPERATOR_KINDS } from "@dci/core";3import type { Metadata } from "next";4import { Search } from "lucide-react";5import { Badge, 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, OfflineNotice } from "@/components/ui/states";12import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";13import { api } from "@/lib/api";14import { countryName, fmtInt, fmtMw } from "@/lib/format";15import { OPERATOR_KIND_LABEL } from "@/lib/labels";16import { routes } from "@/lib/routes";17import { hrefWith, parseSort, sortTh, spGet, spInt, type SearchParams } from "@/lib/sort";1819export const revalidate = 120;2021export const metadata: Metadata = {22 title: "Operators",23 description: "Data center operators, hyperscalers, carriers and developers indexed worldwide: facilities, country footprint, known and pipeline capacity.",24 alternates: { canonical: "/operators" },25};2627const PER_PAGE = 50;28const SORTS = ["facilities", "name", "mw"] as const;29const KIND_SET = new Set<string>(OPERATOR_KINDS);3031const selectCls = "h-9 min-w-0 rounded-sm border border-hair-2 bg-surface px-2 text-[13px] text-ink focus:outline-none focus-visible:ring-2 focus-visible:ring-accent";3233export default async function OperatorsPage({ searchParams }: { searchParams: Promise<SearchParams> }) {34 const sp = await searchParams;35 const base = routes.operators();36 const q = spGet(sp, "q");37 const kindRaw = spGet(sp, "kind");38 const kind = kindRaw && KIND_SET.has(kindRaw) ? (kindRaw as OperatorKind) : undefined;39 const country = spGet(sp, "country")?.toUpperCase();40 const page = spInt(sp, "page", 1);41 const sort = parseSort(sp, SORTS, { key: "facilities", order: "desc" });4243 const [res, countriesRes] = await Promise.all([44 api.operators({ q, kind, country, sort: sort.key, order: sort.order, page, per_page: PER_PAGE }),45 api.countries(),46 ]);47 const ok = res.total > 0 || res.items.length > 0 || (await api.health()).ok;48 const countries = countriesRes.items.filter((c) => c.facilityCount > 0).sort((a, b) => a.name.localeCompare(b.name));4950 const chips: Chip[] = [];51 if (q) chips.push({ key: "q", label: "Search", value: q, removeHref: hrefWith(base, sp, { q: undefined }) });52 if (kind) chips.push({ key: "kind", label: "Kind", value: OPERATOR_KIND_LABEL[kind], removeHref: hrefWith(base, sp, { kind: undefined }) });53 if (country) chips.push({ key: "country", label: "HQ", value: countryName(country), removeHref: hrefWith(base, sp, { country: undefined }) });5455 const th = (key: (typeof SORTS)[number]) => sortTh(base, sp, sort, key);5657 return (58 <Page>59 <PageHeader eyebrow="Index" title="Operators" description={ok ? `${fmtInt(res.total)} operators indexed — colocation providers, hyperscalers, carriers, developers and enterprises. Capacity figures are lower bounds from published values.` : "Operators, their facilities, country footprint and known capacity."} />60 {!ok && <OfflineNotice className="mb-3" />}6162 <form method="get" action={base} className="mb-3 flex flex-wrap items-center gap-2" role="search">63 <label className="relative min-w-0 flex-1 basis-[200px]">64 <span className="sr-only">Search operators</span>65 <Search className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-ink-4" strokeWidth={1.75} />66 <input type="search" name="q" defaultValue={q ?? ""} placeholder="Search operators…" className={`${selectCls} w-full pl-8`} />67 </label>68 <select name="kind" defaultValue={kind ?? ""} aria-label="Kind" className={`${selectCls} basis-[140px]`}>69 <option value="">All kinds</option>70 {OPERATOR_KINDS.map((k) => (71 <option key={k} value={k}>72 {OPERATOR_KIND_LABEL[k]}73 </option>74 ))}75 </select>76 <select name="country" defaultValue={country ?? ""} aria-label="Headquarters country" className={`${selectCls} basis-[160px]`}>77 <option value="">Any HQ country</option>78 {countries.map((c) => (79 <option key={c.iso2} value={c.iso2}>80 {c.name}81 </option>82 ))}83 </select>84 {sort.key !== "facilities" && <input type="hidden" name="sort" value={sort.key} />}85 {sort.key !== "facilities" && <input type="hidden" name="order" value={sort.order} />}86 <Button type="submit" size="md" variant="primary">87 Filter88 </Button>89 </form>90 <FilterChips chips={chips} resetHref={base} className="mb-2" />9192 {res.items.length === 0 ? (93 <EmptyState title={ok ? "No operators match" : "Operators unavailable"} description={ok ? "Loosen a filter or clear them all." : "The index API is not reachable right now."} compact />94 ) : (95 <Table>96 <THead>97 <tr>98 <Th {...th("name")}>Operator</Th>99 <Th className="hidden sm:table-cell">Kind</Th>100 <Th>HQ</Th>101 <Th numeric {...th("facilities")}>102 Facilities103 </Th>104 <Th numeric>Countries</Th>105 <Th numeric className="hidden md:table-cell">106 Metros107 </Th>108 <Th numeric {...th("mw")}>109 Known MW110 </Th>111 <Th numeric>Pipeline</Th>112 <Th numeric className="hidden lg:table-cell">113 Projects114 </Th>115 </tr>116 </THead>117 <TBody>118 {res.items.map((o) => (119 <Tr key={o.id}>120 <Td primary>121 <div className="flex flex-wrap items-center gap-2">122 <RowLink href={routes.operator(o.slug)}>{o.name}</RowLink>123 {o.kind && (124 <Badge tone="outline" size="xs" className="sm:hidden">125 {OPERATOR_KIND_LABEL[o.kind]}126 </Badge>127 )}128 </div>129 </Td>130 <Td label="Kind" className="hidden sm:table-cell">131 {o.kind ? <Badge tone="outline" size="xs">{OPERATOR_KIND_LABEL[o.kind]}</Badge> : <span className="text-ink-4">—</span>}132 </Td>133 <Td label="HQ">134 <CountryFlag iso2={o.hqCountryIso2} withName className="max-w-[160px] text-ink-2" />135 </Td>136 <Td label="Facilities" numeric lead>137 {fmtInt(o.facilityCount)}138 </Td>139 <Td label="Countries" numeric>140 {fmtInt(o.countryCount)}141 </Td>142 <Td label="Metros" numeric className="hidden md:table-cell">143 {o.metroCount === undefined ? <span className="text-ink-4">—</span> : fmtInt(o.metroCount)}144 </Td>145 <Td label="Known MW" numeric>146 {o.knownMw === null || o.knownMw === undefined ? <span className="text-ink-4">—</span> : fmtMw(o.knownMw)}147 </Td>148 <Td label="Pipeline" numeric>149 {o.plannedMw === null || o.plannedMw === undefined ? (150 <span className="text-ink-4">—</span>151 ) : (152 <span title="Planned + under-construction power (lower bound)">153 <EstimateMark className="mr-0.5" />154 {fmtMw(o.plannedMw)}155 </span>156 )}157 </Td>158 <Td label="Projects" numeric className="hidden lg:table-cell">159 {o.projectCount === undefined ? <span className="text-ink-4">—</span> : fmtInt(o.projectCount)}160 </Td>161 </Tr>162 ))}163 </TBody>164 </Table>165 )}166 <Pagination page={page} perPage={PER_PAGE} total={res.total} hrefFor={(p) => hrefWith(base, sp, { page: p > 1 ? p : undefined })} label="operators" />167 </Page>168 );169}170