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