+
Generation Mix
Stacked area charts showing generation by fuel type per region, with renewable vs fossil comparisons and carbon
intensity indicators.
diff --git a/src/app/globals.css b/src/app/globals.css
index de25554..26db8cc 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -121,3 +121,94 @@
@apply bg-background text-foreground;
}
}
+
+/* Ticker tape scrolling animation */
+@keyframes ticker-scroll {
+ 0% {
+ transform: translateX(0);
+ }
+ 100% {
+ transform: translateX(-100%);
+ }
+}
+
+.ticker-scroll {
+ animation: ticker-scroll 30s linear infinite;
+}
+
+.ticker-scroll-container:hover .ticker-scroll {
+ animation-play-state: paused;
+}
+
+/* Ambient glow breathing animations */
+@keyframes ambient-breathe-slow {
+ 0%,
+ 100% {
+ opacity: 0.7;
+ }
+ 50% {
+ opacity: 1;
+ }
+}
+
+@keyframes ambient-breathe-medium {
+ 0%,
+ 100% {
+ opacity: 0.6;
+ }
+ 50% {
+ opacity: 1;
+ }
+}
+
+@keyframes ambient-breathe-fast {
+ 0%,
+ 100% {
+ opacity: 0.5;
+ }
+ 50% {
+ opacity: 1;
+ }
+}
+
+@keyframes ambient-pulse {
+ 0%,
+ 100% {
+ opacity: 0.4;
+ box-shadow: 0 0 20px rgba(239, 68, 68, 0.3);
+ }
+ 50% {
+ opacity: 1;
+ box-shadow:
+ 0 0 30px rgba(239, 68, 68, 0.5),
+ 0 0 60px rgba(239, 68, 68, 0.2);
+ }
+}
+
+.ambient-glow-slow {
+ animation: ambient-breathe-slow 4s ease-in-out infinite;
+}
+
+.ambient-glow-medium {
+ animation: ambient-breathe-medium 2.5s ease-in-out infinite;
+}
+
+.ambient-glow-fast {
+ animation: ambient-breathe-fast 1.5s ease-in-out infinite;
+}
+
+.ambient-glow-pulse {
+ animation: ambient-pulse 1s ease-in-out infinite;
+}
+
+/* Datacenter marker pulsing animation */
+@keyframes marker-pulse {
+ 0% {
+ transform: scale(1);
+ opacity: 0.8;
+ }
+ 100% {
+ transform: scale(2.5);
+ opacity: 0;
+ }
+}
diff --git a/src/app/loading.tsx b/src/app/loading.tsx
new file mode 100644
index 0000000..943ed01
--- /dev/null
+++ b/src/app/loading.tsx
@@ -0,0 +1,48 @@
+import { MetricCardSkeleton } from '@/components/dashboard/metric-card-skeleton.js';
+import { Card, CardContent, CardHeader } from '@/components/ui/card.js';
+import { Skeleton } from '@/components/ui/skeleton.js';
+
+export default function DashboardLoading() {
+ return (
+
+
+
+
+
+
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+
+
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/src/app/map/loading.tsx b/src/app/map/loading.tsx
new file mode 100644
index 0000000..3ad50ec
--- /dev/null
+++ b/src/app/map/loading.tsx
@@ -0,0 +1,9 @@
+import { Skeleton } from '@/components/ui/skeleton.js';
+
+export default function MapLoading() {
+ return (
+
+
+
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 9433904..9b9d861 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,7 +1,9 @@
+import { GpuCalculator } from '@/components/dashboard/gpu-calculator.js';
+import { GridStressGauge } from '@/components/dashboard/grid-stress-gauge.js';
import { MetricCard } from '@/components/dashboard/metric-card.js';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card.js';
import { deserialize } from '@/lib/superjson.js';
-import { Activity, ArrowRight, BarChart3, Droplets, Flame, Map, Server } from 'lucide-react';
+import { Activity, ArrowRight, BarChart3, Droplets, Flame, Gauge, Map, Server } from 'lucide-react';
import Link from 'next/link';
import { fetchDatacenters } from '@/actions/datacenters.js';
@@ -43,7 +45,14 @@ export default async function DashboardHome() {
: [];
const demandRows = demandResult.ok
- ? deserialize
>(demandResult.data)
+ ? deserialize<
+ Array<{
+ avg_demand: number | null;
+ peak_demand: number | null;
+ region_code: string;
+ region_name: string;
+ }>
+ >(demandResult.data)
: [];
const avgPrice = prices.length > 0 ? prices.reduce((sum, p) => sum + p.price_mwh, 0) / prices.length : 0;
@@ -58,10 +67,44 @@ export default async function DashboardHome() {
const avgDemand =
demandRows.length > 0 ? demandRows.reduce((sum, r) => sum + (r.avg_demand ?? 0), 0) / demandRows.length : 0;
+ const regionPrices = prices.map(p => ({
+ regionCode: p.region_code,
+ regionName: p.region_name,
+ priceMwh: p.price_mwh,
+ }));
+
+ // Aggregate demand by region for stress gauges: track latest avg_demand and peak_demand
+ interface RegionDemandEntry {
+ regionCode: string;
+ regionName: string;
+ avgDemand: number;
+ peakDemand: number;
+ }
+ const regionDemandMap: Record = {};
+ for (const row of demandRows) {
+ const existing = regionDemandMap[row.region_code];
+ const avg = row.avg_demand ?? 0;
+ const peak = row.peak_demand ?? 0;
+ if (!existing) {
+ regionDemandMap[row.region_code] = {
+ regionCode: row.region_code,
+ regionName: row.region_name,
+ avgDemand: avg,
+ peakDemand: peak,
+ };
+ } else {
+ if (avg > existing.avgDemand) existing.avgDemand = avg;
+ if (peak > existing.peakDemand) existing.peakDemand = peak;
+ }
+ }
+ const regionDemandList = Object.values(regionDemandMap).filter(
+ (r): r is RegionDemandEntry => r !== undefined && r.peakDemand > 0,
+ );
+
return (
-
-
-
Dashboard
+
+
+
Dashboard
Real-time overview of AI datacenter energy impact across US grid regions.
@@ -137,6 +180,38 @@ export default async function DashboardHome() {
+ {regionPrices.length > 0 && (
+
+
+
+ )}
+
+ {regionDemandList.length > 0 && (
+
+
+
+
+
+ Grid Stress by Region
+
+
+
+
+ {regionDemandList.map(r => (
+
+ ))}
+
+
+
+
+ )}
+
{avgDemand > 0 && (
diff --git a/src/app/trends/loading.tsx b/src/app/trends/loading.tsx
new file mode 100644
index 0000000..d1b269d
--- /dev/null
+++ b/src/app/trends/loading.tsx
@@ -0,0 +1,16 @@
+import { ChartSkeleton } from '@/components/charts/chart-skeleton.js';
+import { Skeleton } from '@/components/ui/skeleton.js';
+
+export default function TrendsLoading() {
+ return (
+
+ );
+}
diff --git a/src/app/trends/page.tsx b/src/app/trends/page.tsx
index d2a86e0..e8fa1d2 100644
--- a/src/app/trends/page.tsx
+++ b/src/app/trends/page.tsx
@@ -42,9 +42,9 @@ export default async function TrendsPage() {
]);
return (
-
+
-
Price Trends
+
Price Trends
Regional electricity price charts with commodity overlays and AI milestone annotations.
diff --git a/src/components/charts/chart-skeleton.tsx b/src/components/charts/chart-skeleton.tsx
new file mode 100644
index 0000000..5892862
--- /dev/null
+++ b/src/components/charts/chart-skeleton.tsx
@@ -0,0 +1,24 @@
+import { Card, CardContent, CardHeader } from '@/components/ui/card.js';
+import { Skeleton } from '@/components/ui/skeleton.js';
+import { cn } from '@/lib/utils.js';
+
+interface ChartSkeletonProps {
+ className?: string;
+ title?: boolean;
+}
+
+export function ChartSkeleton({ className, title = true }: ChartSkeletonProps) {
+ return (
+
+ {title && (
+
+
+
+
+ )}
+
+
+
+
+ );
+}
diff --git a/src/components/charts/demand-chart.tsx b/src/components/charts/demand-chart.tsx
index 8aa92b5..8562b71 100644
--- a/src/components/charts/demand-chart.tsx
+++ b/src/components/charts/demand-chart.tsx
@@ -239,10 +239,10 @@ export function DemandChart({ initialData, summaryData }: DemandChartProps) {
))}
-
+