Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ attached_assets/Pasted-*
/tmp/
coverage/
.nyc_output/
.card-preview/
24 changes: 22 additions & 2 deletions client/src/components/home/FeaturesShowcase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,32 @@ function PreviewSkeleton() {
interface Module { number: string; name: string; caption: string; cta: string; route: string; preview: () => JSX.Element; }
const MODULES: Module[] = [
{ number: "01", name: "Equity Heatmap", caption: "One hundred public companies behind the buildout, priced live.", cta: "Open the heatmap", route: "/stack", preview: SectorHeatStrip },
{ number: "02", name: "Power Map", caption: "Thirty-three tracked facilities, plotted by operator and grid region.", cta: "Open the map", route: "/power-map", preview: RealUSMap },
// Caption is filled in from /api/datacenters at render; see facilityCaption.
// A hardcoded count cannot survive here: the datacenter ingester appends new
// facilities every 6 hours, which is how this card came to claim 33 while the
// map directly beneath it plotted 58.
{ number: "02", name: "Power Map", caption: "", cta: "Open the map", route: "/power-map", preview: RealUSMap },
{ number: "03", name: "Supply Chain Flow", caption: "Where the buildout can get stuck, mapped to the companies exposed.", cta: "Trace the chain", route: "/stack?view=flow", preview: SupplyChainMini },
{ number: "04", name: "Catalyst Tracker", caption: "Earnings dates, rule changes, and policy votes. One calendar.", cta: "See what's next", route: "/catalysts", preview: CatalystRows },
{ number: "05", name: "Analyze: Portfolio", caption: "Type a ticker. See how exposed it is to the power story.", cta: "Score a ticker", route: "/analyze?tab=portfolio", preview: PortfolioPentagon },
{ number: "06", name: "Analyze: Scenario", caption: "Pick how fast demand grows. See what it does to the grid by 2030.", cta: "Run a scenario", route: "/analyze?tab=scenario", preview: DemandSparkline },
];

/**
* Live caption for the Power Map card. Shares the /api/datacenters query key
* with RealUSMap, so react-query serves both from one fetch and the sentence
* can never disagree with the dots underneath it. Falls back to a claim with
* no number rather than guessing one while the request is in flight.
*/
function useFacilityCaption(): string {
const { data } = useQuery<Facility[]>({ queryKey: ["/api/datacenters"] });
const suffix = "plotted by operator and grid region.";
if (!data) return `Tracked facilities, ${suffix}`;
return `${data.length} tracked facilities, ${suffix}`;
}

export function FeaturesShowcase() {
const facilityCaption = useFacilityCaption();
return (
<section className="border-b border-border bg-background" data-testid="home-features">
<div className="mx-auto max-w-[1200px] px-6 py-16 sm:py-20">
Expand All @@ -231,7 +249,9 @@ export function FeaturesShowcase() {
<h3 className="text-[17px] font-semibold text-foreground">{m.name}</h3>
<span className="font-mono text-[11px] tabular-nums text-muted-foreground/50">{m.number}</span>
</div>
<p className="mt-1.5 text-[13px] leading-relaxed text-muted-foreground">{m.caption}</p>
<p className="mt-1.5 text-[13px] leading-relaxed text-muted-foreground">
{m.caption || facilityCaption}
</p>
<div className="my-4 h-[120px] w-full rounded border border-border/60 bg-background/60 p-2.5">
<Preview />
</div>
Expand Down
124 changes: 101 additions & 23 deletions client/src/components/home/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,31 @@ import { Wordmark } from "./Wordmark";
import { GridPulse } from "./grid-pulse";
import { MarketTape } from "./market-tape";

// The wordmark leads and the tagline stays as shipped; the hero's job here is
// to prove the numbers under it are real. Every figure carries the date its
// dataset was last refreshed, read from the same API that serves the figure.
// Nothing is hardcoded, and a stat with no refresh date says so rather than
// borrowing today's.

interface ClusterMetrics {
clusterCount: number;
operationalMW: number;
totalPlannedMW: number;
byOperator: { operator: string }[];
lastRefreshed: string | null;
}
interface DealMetrics {
dealCount: number;
totalContractedMW: number;
lastRefreshed: string | null;
}
interface GpuMetrics {
fleetAvg: number;
fleetAvg1yChange: number;
modelCount: number;
lastRefreshed: string | null;
asOf: string | null;
}
interface DealMetrics { dealCount: number; totalContractedMW: number; }
interface GpuMetrics { fleetAvg: number; fleetAvg1yChange: number; modelCount: number; }

/** rAF count-up toward a target once it arrives; honest "--" before data. */
function useCountUp(target: number | null, decimals = 1, ms = 1100): string | null {
Expand All @@ -35,11 +52,32 @@ function useCountUp(target: number | null, decimals = 1, ms = 1100): string | nu
return display;
}

const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

/** "2026-06-26" -> "26 Jun 2026". Null when the API gave us nothing usable. */
function shortDate(iso: string | null | undefined): string | null {
if (!iso) return null;
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso);
if (!m) return null;
const mon = MONTHS[Number(m[2]) - 1];
return mon ? `${Number(m[3])} ${mon} ${m[1]}` : null;
}

const fadeUp: Variants = {
hidden: { opacity: 0, y: 14 },
show: { opacity: 1, y: 0, transition: { duration: 0.55, ease: "easeOut" } },
hidden: { opacity: 0, y: 12 },
show: { opacity: 1, y: 0, transition: { duration: 0.5, ease: "easeOut" } },
};

interface HeroStat {
icon: typeof Zap;
label: string;
value: string;
sub?: string;
/** Dataset refresh date. Null renders "no date" rather than inventing one. */
asOf: string | null;
href: string;
}

export function Hero() {
const { data: clusters } = useQuery<ClusterMetrics>({ queryKey: ["/api/clusters/metrics"] });
const { data: deals } = useQuery<DealMetrics>({ queryKey: ["/api/deals/metrics"] });
Expand All @@ -50,11 +88,39 @@ export function Hero() {
const gpuHr = useCountUp(gpu ? gpu.fleetAvg : null, 2);
const clusterCount = useCountUp(clusters ? clusters.clusterCount : null, 0);

const stats: { icon: typeof Zap; label: string; value: string; sub?: string }[] = [
{ icon: Zap, label: "Operational power for compute", value: opGw ? `${opGw} GW` : "--", sub: clusters ? `${(clusters.totalPlannedMW / 1000).toFixed(1)} GW planned` : undefined },
{ icon: Handshake, label: "Contracted power deals", value: dealGw ? `${dealGw} GW` : "--", sub: deals ? `${deals.dealCount} corporate deals` : undefined },
{ icon: Cpu, label: "Cost of compute", value: gpuHr ? `$${gpuHr}/hr` : "--", sub: gpu ? `${gpu.fleetAvg1yChange}% over a year` : undefined },
{ icon: MapPin, label: "Tracked clusters", value: clusterCount ?? "--", sub: clusters?.byOperator ? `${clusters.byOperator.length} operators` : undefined },
const stats: HeroStat[] = [
{
icon: Zap,
label: "Operational power for compute",
value: opGw ? `${opGw} GW` : "--",
sub: clusters ? `${(clusters.totalPlannedMW / 1000).toFixed(1)} GW planned` : undefined,
asOf: shortDate(clusters?.lastRefreshed),
href: "/compute-frontier",
},
{
icon: Handshake,
label: "Contracted power deals",
value: dealGw ? `${dealGw} GW` : "--",
sub: deals ? `${deals.dealCount} corporate deals` : undefined,
asOf: shortDate(deals?.lastRefreshed),
href: "/power-deals",
},
{
icon: Cpu,
label: "Cost of compute",
value: gpuHr ? `$${gpuHr}/hr` : "--",
sub: gpu ? `${gpu.fleetAvg1yChange}% over a year` : undefined,
asOf: shortDate(gpu?.asOf ?? gpu?.lastRefreshed),
href: "/neocloud-intel",
},
{
icon: MapPin,
label: "Tracked clusters",
value: clusterCount ?? "--",
sub: clusters?.byOperator ? `${clusters.byOperator.length} operators` : undefined,
asOf: shortDate(clusters?.lastRefreshed),
href: "/power-map",
},
];

return (
Expand Down Expand Up @@ -99,27 +165,39 @@ export function Hero() {
</Link>
</motion.div>

{/* The ledger. Each figure links to the module that shows its working
and prints the date its own dataset was last refreshed, so a stale
number is visible as stale instead of reading as today's. */}
<motion.div
variants={fadeUp}
className="mt-14 grid w-full grid-cols-2 gap-3 sm:gap-4 lg:grid-cols-4"
data-testid="hero-stats"
>
{stats.map(({ icon: Icon, label, value, sub }) => (
<motion.div
{stats.map(({ icon: Icon, label, value, sub, asOf, href }, i) => (
<Link
key={label}
whileHover={{ y: -3 }}
transition={{ type: "spring", stiffness: 300, damping: 22 }}
className="rounded-md border border-border bg-card/80 px-4 py-3.5 text-left"
href={href}
className="no-underline"
data-testid={`hero-stat-${i}`}
>
<div className="flex items-center gap-2">
<Icon className="h-4 w-4 text-brand" aria-hidden />
<span className="text-[12px] leading-tight text-muted-foreground">{label}</span>
</div>
<p className="mt-1.5 font-mono text-[24px] font-bold leading-none tracking-tight text-foreground tabular-nums">
{value}
</p>
{sub && <p className="mt-1 text-[11.5px] text-muted-foreground/80">{sub}</p>}
</motion.div>
<motion.div
whileHover={{ y: -3 }}
transition={{ type: "spring", stiffness: 300, damping: 22 }}
className="group h-full rounded-md border border-border bg-card/80 px-4 py-3.5 text-left transition-colors hover:border-brand/50"
>
<div className="flex items-center gap-2">
<Icon className="h-4 w-4 text-brand" aria-hidden />
<span className="text-[12px] leading-tight text-muted-foreground">{label}</span>
</div>
<p className="mt-1.5 font-mono text-[24px] font-bold leading-none tracking-tight text-foreground tabular-nums transition-colors group-hover:text-brand">
{value}
</p>
{sub && <p className="mt-1 text-[11.5px] text-muted-foreground/80">{sub}</p>}
<p className="mt-2 font-mono text-[10.5px] uppercase tracking-[0.14em] text-muted-foreground/70">
{asOf ? `as of ${asOf}` : "no date"}
</p>
</motion.div>
</Link>
))}
</motion.div>
</motion.div>
Expand Down
145 changes: 0 additions & 145 deletions client/src/components/home/HomeFooter.tsx

This file was deleted.

15 changes: 13 additions & 2 deletions client/src/components/home/Wordmark.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import { useEffect, useState } from "react";

const TEXT = "GridTilt";
const SPLIT = 4; // "Grid" then "tilt"
/**
* The brand lockup, exported so every surface splits the same string the same
* way. The footer used to hardcode its own "Grid" + "tilt" and rendered the
* second half lowercase, so the header and footer disagreed on the company's
* own name.
*/
export const BRAND_TEXT = "GridTilt";
export const BRAND_SPLIT = 4;
export const BRAND_HEAD = BRAND_TEXT.slice(0, BRAND_SPLIT); // "Grid"
export const BRAND_TAIL = BRAND_TEXT.slice(BRAND_SPLIT); // "Tilt"

const TEXT = BRAND_TEXT;
const SPLIT = BRAND_SPLIT; // "Grid" then "Tilt"
const STEP_MS = 95;
const FIRST_DELAY_MS = 220;
const TILT_DELAY_MS = 240;
Expand Down
Loading