spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1"use client";23import type { NearbyInfrastructure } from "@dci/core";4import Link from "next/link";5import { useEffect, useState } from "react";6import { EstimateMark, FlagBadges, PrecisionBadge, StatusBadge } from "@/components/ui/badge";7import { cn } from "@/components/ui/cn";8import { CountryFlag } from "@/components/ui/country-flag";9import { SegmentedControl } from "@/components/ui/segmented";10import { Skeleton } from "@/components/ui/skeleton";11import { apiGet, clientApiBase } from "@/lib/api";12import { bestMw, fmtInt, fmtMw } from "@/lib/format";13import { isImprecise } from "@/lib/labels";14import { routes } from "@/lib/routes";1516const RADII = [5, 10, 25, 50, 100] as const;17type Radius = (typeof RADII)[number];1819function km(d: number): string {20 return d < 1 ? `${Math.round(d * 1000)} m` : `${d.toFixed(d < 10 ? 1 : 0)} km`;21}2223function Group({ title, count, children, note }: { title: string; count: number; children?: React.ReactNode; note?: string }) {24 return (25 <div>26 <div className="mb-1 flex items-baseline gap-2">27 <h3 className="text-[13px] font-semibold text-ink">{title}</h3>28 <span className="figure text-[11.5px] text-ink-3">{fmtInt(count)}</span>29 </div>30 {count === 0 ? <p className="text-[12px] text-ink-3">{note ?? "None within this radius."}</p> : children}31 </div>32 );33}3435/**36 * NEARBY INFRASTRUCTURE (§23): facilities, projects, IXPs, cloud regions and metros within a radius, from GET /nearby.37 * Wording is always "nearby" — proximity is a great-circle distance between published coordinates and never implies a38 * connection. Layers whose connectors do not exist yet (landing stations, substations, power plants) show the API note.39 */40export function NearbyInfrastructurePanel({ initial, center, excludeId }: { initial: NearbyInfrastructure | null; center: { lat: number; lng: number }; excludeId?: string }) {41 const [radius, setRadius] = useState<Radius>((initial?.radiusKm as Radius | undefined) && RADII.includes(initial!.radiusKm as Radius) ? (initial!.radiusKm as Radius) : 25);42 // results cached per radius; the server-provided payload seeds its own radius43 const [fetched, setFetched] = useState<Partial<Record<Radius, NearbyInfrastructure>>>({});44 const [error, setError] = useState<string | null>(null);45 const data: NearbyInfrastructure | null = initial && radius === initial.radiusKm ? initial : (fetched[radius] ?? null);46 const loading = !data && !error;4748 useEffect(() => {49 if (data) return;50 const ctrl = new AbortController();51 apiGet<NearbyInfrastructure>("/nearby", { lat: center.lat, lng: center.lng, radius_km: radius }, { base: clientApiBase(), signal: ctrl.signal, timeoutMs: 10000 }).then((r) => {52 if (ctrl.signal.aborted) return;53 if (r.ok) {54 setFetched((f) => ({ ...f, [radius]: r.data }));55 setError(null);56 } else setError(r.error);57 });58 return () => ctrl.abort();59 }, [radius, center.lat, center.lng, data]);6061 const facilities = (data?.facilities ?? []).filter((f) => f.id !== excludeId);62 const empty = ["landingStations", "substations", "powerPlants"] as const;63 const emptyLabels: Record<(typeof empty)[number], string> = { landingStations: "Cable landing stations", substations: "Substations", powerPlants: "Power plants" };6465 return (66 <div className="flex flex-col gap-4">67 <div className="flex flex-wrap items-center gap-3">68 <SegmentedControl label="Search radius" value={String(radius)} onChange={(v) => { setError(null); setRadius(Number(v) as Radius); }} options={RADII.map((r) => ({ value: String(r), label: `${r} km` }))} size="xs" />69 <span className="figure text-[12px] text-ink-3" aria-live="polite">70 {loading ? "Loading…" : error ? `Unavailable (${error})` : data ? `${fmtInt(facilities.length)} facilities · ${fmtInt(data.ixps.length)} IXPs · ${fmtInt(data.cloudRegions.length)} cloud regions · ${fmtInt(data.projects.length)} projects` : ""}71 </span>72 <Link href={routes.map(`lat=${center.lat.toFixed(4)}&lng=${center.lng.toFixed(4)}&z=${radius <= 10 ? 11 : radius <= 25 ? 9.5 : radius <= 50 ? 8.5 : 7.5}`)} className="ml-auto text-[12.5px] text-ink-3 hover:text-ink">73 Open on map →74 </Link>75 </div>7677 {loading && !data ? (78 <Skeleton className="h-24" />79 ) : !data ? (80 <p className="rounded-sm border border-dashed border-hair-2 px-3 py-2.5 text-[12.5px] text-ink-3">Nearby infrastructure is unavailable right now.</p>81 ) : (82 <div className={cn("grid gap-5 md:grid-cols-2", loading && "opacity-60")}>83 <Group title="Nearby facilities" count={facilities.length}>84 <ul className="divide-y divide-hair">85 {facilities.slice(0, 12).map((n) => {86 const { mw, kind } = bestMw(n);87 return (88 <li key={n.id} className="grid grid-cols-[minmax(0,1fr)_auto] gap-x-3 py-2">89 <div className="min-w-0">90 <div className="flex flex-wrap items-center gap-x-2 gap-y-1">91 <Link href={routes.facility(n.slug)} className="truncate text-[13px] font-medium text-ink hover:text-accent hover:underline">92 {n.name}93 </Link>94 <StatusBadge status={n.status} short size="xs" />95 <FlagBadges isAi={n.isAi} isHyperscale={n.isHyperscale} />96 </div>97 <div className="mt-0.5 flex flex-wrap items-center gap-x-1.5 text-[11.5px] text-ink-3">98 <span>{n.operator?.name ?? "Operator unknown"}</span>99 {isImprecise(n.geoPrecision) && <PrecisionBadge precision={n.geoPrecision} size="xs" />}100 </div>101 </div>102 <div className="flex flex-col items-end text-right">103 <span className="figure text-[13px] font-medium text-ink">104 {mw === null ? <span className="text-ink-4">—</span> : (105 <>106 {kind === "planned" && <EstimateMark className="mr-0.5 text-[11px]" />}107 {fmtMw(mw)}108 </>109 )}110 </span>111 <span className="figure text-[11px] text-ink-3">{km(n.distanceKm)}</span>112 </div>113 </li>114 );115 })}116 </ul>117 {facilities.length > 12 && <p className="mt-1 text-[11.5px] text-ink-4">Showing 12 of {fmtInt(facilities.length)}.</p>}118 </Group>119120 <div className="flex flex-col gap-5">121 <Group title="Nearby internet exchanges" count={data.ixps.length} note="No IXP with published coordinates within this radius.">122 <ul className="divide-y divide-hair">123 {data.ixps.slice(0, 8).map((x) => (124 <li key={x.id} className="flex items-baseline justify-between gap-3 py-1.5 text-[13px]">125 <Link href={routes.ixp(x.slug)} className="truncate text-ink hover:text-accent hover:underline">126 {x.name}127 </Link>128 <span className="figure shrink-0 text-[11.5px] text-ink-3">129 {x.networkCount !== null ? `${fmtInt(x.networkCount)} nets · ` : ""}130 {km(x.distanceKm)}131 </span>132 </li>133 ))}134 </ul>135 </Group>136 <Group title="Nearby cloud regions" count={data.cloudRegions.length} note="No public cloud region is published for this area.">137 <ul className="divide-y divide-hair">138 {data.cloudRegions.slice(0, 8).map((c) => (139 <li key={c.id} className="flex items-baseline justify-between gap-3 py-1.5 text-[13px]">140 <span className="min-w-0 truncate">141 <Link href={routes.cloudRegion(c.slug)} className="text-ink hover:text-accent hover:underline">142 {c.provider.name} <span className="figure text-ink-3">{c.code}</span>143 </Link>144 </span>145 <span className="figure shrink-0 text-[11.5px] text-ink-3">{km(c.distanceKm)}</span>146 </li>147 ))}148 </ul>149 </Group>150 <Group title="Nearby projects" count={data.projects.length} note="No located project within this radius (most projects carry city-level coordinates only).">151 <ul className="divide-y divide-hair">152 {data.projects.slice(0, 8).map((p) => (153 <li key={p.id} className="flex items-baseline justify-between gap-3 py-1.5 text-[13px]">154 <span className="min-w-0 inline-flex items-center gap-1.5 truncate">155 <Link href={routes.project(p.slug)} className="truncate text-ink hover:text-accent hover:underline">156 {p.name}157 </Link>158 <StatusBadge status={p.status} short size="xs" />159 </span>160 <span className="figure shrink-0 text-[11.5px] text-ink-3">161 {p.plannedMw !== null ? `≈ ${fmtMw(p.plannedMw)} · ` : ""}162 {km(p.distanceKm)}163 </span>164 </li>165 ))}166 </ul>167 </Group>168 {data.metros.length > 0 && (169 <Group title="Markets" count={data.metros.length}>170 <ul className="flex flex-wrap gap-1.5">171 {data.metros.map((m) => (172 <li key={m.id}>173 <Link href={routes.market(m.slug)} className="inline-flex h-7 items-center gap-1.5 rounded-xs border border-hair px-2 text-[12px] text-ink-2 hover:text-ink">174 <CountryFlag iso2={m.countryIso2} size={11} /> {m.name} <span className="figure text-ink-4">{km(m.distanceKm)}</span>175 </Link>176 </li>177 ))}178 </ul>179 </Group>180 )}181 </div>182 </div>183 )}184185 {data && (186 <div className="grid gap-2 border-t border-hair pt-3 sm:grid-cols-3">187 {empty.map((k) => (188 <div key={k} className="text-[12px]">189 <div className="flex items-baseline gap-2">190 <span className="font-medium text-ink-2">{emptyLabels[k]}</span>191 <span className="figure text-ink-3">{fmtInt(data[k].length)}</span>192 </div>193 {data[k].length === 0 && <p className="text-ink-4">Empty until a licensed connector for this layer exists — never inferred.</p>}194 </div>195 ))}196 <p className="text-[11px] text-ink-4 sm:col-span-3">{data.note}</p>197 </div>198 )}199 </div>200 );201}202