spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import type { Pulse } from "@dci/core";2import Link from "next/link";3import { EventList } from "@/components/event-list";4import { cn } from "@/components/ui/cn";5import { CountryFlag } from "@/components/ui/country-flag";6import { fmtInt, fmtMw } from "@/lib/format";7import { routes } from "@/lib/routes";89function Stat({ label, value, href, muted }: { label: string; value: string; href?: string; muted?: boolean }) {10 const inner = (11 <>12 <div className={cn("figure text-[18px] font-medium leading-none tracking-[-0.02em]", muted ? "text-ink-4" : "text-ink group-hover:text-accent")}>{value}</div>13 <div className="mt-1 text-[11px] leading-tight text-ink-3">{label}</div>14 </>15 );16 return href ? (17 <Link href={href} className="group block min-w-0 py-2 pr-3">18 {inner}19 </Link>20 ) : (21 <div className="min-w-0 py-2 pr-3">{inner}</div>22 );23}2425/** WHAT CHANGED TODAY (§16): deterministic pulse counts + the major events of the window; nothing scored, everything countable. */26export function PulseBlock({ pulse, compact }: { pulse: Pulse; compact?: boolean }) {27 const win = pulse.window === "24h" ? "24 h" : pulse.window;28 const live = (type: string) => routes.live(`type=${type}&since=${encodeURIComponent(pulse.since)}`);29 const stats: Array<{ label: string; value: number | null; href?: string; mw?: boolean }> = [30 { label: "new projects", value: pulse.newProjects, href: live("project_announced") },31 { label: "entered construction", value: pulse.projectsEnteredConstruction, href: live("construction_started") },32 { label: "facilities opened", value: pulse.facilitiesOpened, href: live("facility_opened") },33 { label: "newly announced (published MW)", value: pulse.newlyAnnouncedMw, mw: true },34 { label: "construction started (published MW)", value: pulse.constructionStartedMw, mw: true },35 { label: "cloud regions announced", value: pulse.cloudRegionsAnnounced, href: live("cloud_region_announced") },36 { label: "power agreements", value: pulse.powerAgreements, href: live("power_agreement") },37 { label: "grid constraint events", value: pulse.gridConstraintEvents, href: live("grid_constraint") },38 { label: "acquisitions", value: pulse.acquisitions, href: live("acquisition") },39 { label: "financing events", value: pulse.financingEvents, href: live("investment_announced") },40 { label: "facilities newly indexed", value: pulse.newFacilitiesIndexed, href: live("facility_discovered") },41 { label: "capacity changes", value: pulse.capacityChanges, href: live("capacity_changed") },42 ];43 return (44 <div className="grid gap-6 lg:grid-cols-[minmax(0,5fr)_minmax(0,7fr)]">45 <div className="min-w-0">46 <div className={cn("grid grid-cols-2 divide-hair sm:grid-cols-3", compact && "sm:grid-cols-2")}>47 {stats.map((s) => (48 <Stat key={s.label} label={s.label} value={s.value === null ? "—" : s.mw ? fmtMw(s.value) : fmtInt(s.value)} href={s.href} muted={s.value === null || s.value === 0} />49 ))}50 </div>51 {pulse.operatorsNewMarkets.length > 0 && (52 <div className="mt-3 border-t border-hair pt-3">53 <div className="label-caps mb-1.5">Operators entering new markets · {win}</div>54 <ul className="flex flex-wrap gap-1.5">55 {pulse.operatorsNewMarkets.slice(0, 8).map((o, i) => (56 <li key={`${o.operator.id}-${o.market?.id ?? o.countryIso2 ?? i}`}>57 <Link href={routes.operator(o.operator.slug)} className="inline-flex h-10 max-w-[260px] items-center gap-1.5 rounded-xs border border-hair px-2 text-[12px] text-ink-2 hover:text-ink sm:h-7">58 <span className="truncate">{o.operator.name}</span>59 <span aria-hidden className="text-ink-4">→</span>60 <CountryFlag iso2={o.countryIso2} size={11} />61 <span className="truncate text-ink-3">{o.market?.name ?? ""}</span>62 </Link>63 </li>64 ))}65 {pulse.operatorsNewMarkets.length > 8 && <li className="inline-flex h-10 items-center sm:h-7 text-[12px] text-ink-4">+{fmtInt(pulse.operatorsNewMarkets.length - 8)} more</li>}66 </ul>67 </div>68 )}69 {(pulse.byCountry.length > 0 || pulse.byOperator.length > 0) && (70 <div className="mt-3 grid gap-4 border-t border-hair pt-3 sm:grid-cols-2">71 {pulse.byCountry.length > 0 && (72 <div>73 <div className="label-caps mb-1.5">Most active countries</div>74 <ul className="divide-y divide-hair text-[12.5px]">75 {pulse.byCountry.slice(0, 5).map((c) => (76 <li key={c.iso2} className="flex min-h-10 items-center justify-between gap-2 py-1 sm:min-h-0">77 <Link href={routes.country(c.slug)} className="inline-flex min-h-10 min-w-0 items-center gap-1.5 text-ink-2 hover:text-ink sm:min-h-0">78 <CountryFlag iso2={c.iso2} size={11} /> <span className="truncate">{c.name}</span>79 </Link>80 <span className="figure shrink-0 text-ink-3">81 {fmtInt(c.events)} ev · {fmtInt(c.newProjects)} prj82 </span>83 </li>84 ))}85 </ul>86 </div>87 )}88 {pulse.byOperator.length > 0 && (89 <div>90 <div className="label-caps mb-1.5">Most active operators</div>91 <ul className="divide-y divide-hair text-[12.5px]">92 {pulse.byOperator.slice(0, 5).map((o) => (93 <li key={o.id} className="flex min-h-10 items-center justify-between gap-2 py-1 sm:min-h-0">94 <Link href={routes.operator(o.slug)} className="inline-flex min-h-10 items-center truncate text-ink-2 hover:text-ink sm:min-h-0">95 {o.name}96 </Link>97 <span className="figure shrink-0 text-ink-3">98 {fmtInt(o.events)} ev · {fmtInt(o.newProjects)} prj99 </span>100 </li>101 ))}102 </ul>103 </div>104 )}105 </div>106 )}107 </div>108 <div className="min-w-0">109 <div className="mb-1.5 flex items-baseline justify-between">110 <div className="label-caps">Major events · {win}</div>111 <Link href={routes.live("significance=major")} className="text-[12px] text-ink-3 hover:text-ink">112 All major →113 </Link>114 </div>115 <EventList events={pulse.majorEvents.slice(0, 6)} dense emptyTitle="No major event in this window" emptyDescription="Major = large MW, new market entries, grid constraints, openings." />116 </div>117 </div>118 );119}120