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