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%
8.1 KB · 170 lines tsx
Raw Blame History
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