diff --git a/.gitignore b/.gitignore index 70e48a1..d0b3597 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ attached_assets/Pasted-* /tmp/ coverage/ .nyc_output/ +.card-preview/ diff --git a/client/src/components/home/FeaturesShowcase.tsx b/client/src/components/home/FeaturesShowcase.tsx index eb38b7d..015bed9 100644 --- a/client/src/components/home/FeaturesShowcase.tsx +++ b/client/src/components/home/FeaturesShowcase.tsx @@ -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({ 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 (
@@ -231,7 +249,9 @@ export function FeaturesShowcase() {

{m.name}

{m.number}
-

{m.caption}

+

+ {m.caption || facilityCaption} +

diff --git a/client/src/components/home/Hero.tsx b/client/src/components/home/Hero.tsx index 2d89833..ce3e5d8 100644 --- a/client/src/components/home/Hero.tsx +++ b/client/src/components/home/Hero.tsx @@ -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 { @@ -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({ queryKey: ["/api/clusters/metrics"] }); const { data: deals } = useQuery({ queryKey: ["/api/deals/metrics"] }); @@ -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 ( @@ -99,27 +165,39 @@ export function Hero() { + {/* 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. */} - {stats.map(({ icon: Icon, label, value, sub }) => ( - ( + -
- - {label} -
-

- {value} -

- {sub &&

{sub}

} -
+ +
+ + {label} +
+

+ {value} +

+ {sub &&

{sub}

} +

+ {asOf ? `as of ${asOf}` : "no date"} +

+
+ ))}
diff --git a/client/src/components/home/HomeFooter.tsx b/client/src/components/home/HomeFooter.tsx deleted file mode 100644 index 8e3d265..0000000 --- a/client/src/components/home/HomeFooter.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import logoPath from "@assets/Image_[Vectorized]_(2)_1773890483514.png"; - -const HORIZ_PAD = "clamp(24px, 5vw, 96px)"; - -export function HomeFooter() { - return ( -
-
-
-
-
- GridTilt - - Gridtilt - -
- - -
- -
-

- Sources: Yahoo Finance, EIA, DOE, NERC, LBNL. RSS: Utility Dive, Data - Center Dynamics, World Nuclear News, Power Engineering. -

- - gridtilt1@gmail.com - -
- -
- © 2026 GridTilt - gridtilt.com -
-
-
- ); -} - -function XIcon() { - return ( - - - - ); -} - -function GithubIcon() { - return ( - - - - ); -} diff --git a/client/src/components/home/Wordmark.tsx b/client/src/components/home/Wordmark.tsx index a0fe974..92b4284 100644 --- a/client/src/components/home/Wordmark.tsx +++ b/client/src/components/home/Wordmark.tsx @@ -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; diff --git a/client/src/components/site-footer.tsx b/client/src/components/site-footer.tsx index 9b03ceb..10300fe 100644 --- a/client/src/components/site-footer.tsx +++ b/client/src/components/site-footer.tsx @@ -66,6 +66,19 @@ export function SiteFooter() { @gridtilt + {/* Carried up from the marketing footer when the landing stopped + rendering a second one. These were previously reachable only + from "/"; every page has them now. */} +
  • + + Source on GitHub + +
  • +
  • + + Contact + +
  • diff --git a/client/src/data/us-states.geo.json b/client/src/data/us-states.geo.json index 1c1ebe5..40f3325 100644 --- a/client/src/data/us-states.geo.json +++ b/client/src/data/us-states.geo.json @@ -45,7 +45,7 @@ {"type":"Feature","id":"48","properties":{"name":"Texas","density":98.07},"geometry":{"type":"Polygon","coordinates":[[[-101.812942,36.501861],[-100.000075,36.501861],[-100.000075,34.563024],[-99.923398,34.573978],[-99.698843,34.382285],[-99.57835,34.415147],[-99.260688,34.404193],[-99.189488,34.2125],[-98.986841,34.223454],[-98.767763,34.135823],[-98.570593,34.146777],[-98.488439,34.064623],[-98.36247,34.157731],[-98.170777,34.113915],[-98.088623,34.004376],[-97.946222,33.987946],[-97.869545,33.851022],[-97.694283,33.982469],[-97.458774,33.905791],[-97.371143,33.823637],[-97.256128,33.861976],[-97.173974,33.736006],[-96.922034,33.960561],[-96.850834,33.845545],[-96.631756,33.845545],[-96.423633,33.774345],[-96.346956,33.686714],[-96.149786,33.840068],[-95.936185,33.889361],[-95.8376,33.834591],[-95.602092,33.933176],[-95.547322,33.878407],[-95.289906,33.87293],[-95.224183,33.960561],[-94.966767,33.861976],[-94.868182,33.74696],[-94.484796,33.637421],[-94.380734,33.544313],[-94.183564,33.593606],[-94.041164,33.54979],[-94.041164,33.018527],[-94.041164,31.994339],[-93.822086,31.775262],[-93.816609,31.556184],[-93.542762,31.15089],[-93.526331,30.93729],[-93.630393,30.679874],[-93.728978,30.575812],[-93.696116,30.438888],[-93.767317,30.334826],[-93.690639,30.143133],[-93.926148,29.787132],[-93.838517,29.688547],[-94.002825,29.68307],[-94.523134,29.546147],[-94.70935,29.622824],[-94.742212,29.787132],[-94.873659,29.672117],[-94.966767,29.699501],[-95.016059,29.557101],[-94.911997,29.496854],[-94.895566,29.310638],[-95.081782,29.113469],[-95.383014,28.867006],[-95.985477,28.604113],[-96.045724,28.647929],[-96.226463,28.582205],[-96.23194,28.642452],[-96.478402,28.598636],[-96.593418,28.724606],[-96.664618,28.697221],[-96.401725,28.439805],[-96.593418,28.357651],[-96.774157,28.406943],[-96.801542,28.226204],[-97.026096,28.039988],[-97.256128,27.694941],[-97.404005,27.333463],[-97.513544,27.360848],[-97.540929,27.229401],[-97.425913,27.262263],[-97.480682,26.99937],[-97.557359,26.988416],[-97.562836,26.840538],[-97.469728,26.758384],[-97.442344,26.457153],[-97.332805,26.353091],[-97.30542,26.161398],[-97.217789,25.991613],[-97.524498,25.887551],[-97.650467,26.018997],[-97.885976,26.06829],[-98.198161,26.057336],[-98.466531,26.221644],[-98.669178,26.238075],[-98.822533,26.369522],[-99.030656,26.413337],[-99.173057,26.539307],[-99.266165,26.840538],[-99.446904,27.021277],[-99.424996,27.174632],[-99.50715,27.33894],[-99.479765,27.48134],[-99.605735,27.640172],[-99.709797,27.656603],[-99.879582,27.799003],[-99.934351,27.979742],[-100.082229,28.14405],[-100.29583,28.280974],[-100.399891,28.582205],[-100.498476,28.66436],[-100.629923,28.905345],[-100.673738,29.102515],[-100.799708,29.244915],[-101.013309,29.370885],[-101.062601,29.458516],[-101.259771,29.535193],[-101.413125,29.754271],[-101.851281,29.803563],[-102.114174,29.792609],[-102.338728,29.869286],[-102.388021,29.765225],[-102.629006,29.732363],[-102.809745,29.524239],[-102.919284,29.190146],[-102.97953,29.184669],[-103.116454,28.987499],[-103.280762,28.982022],[-103.527224,29.135376],[-104.146119,29.381839],[-104.266611,29.513285],[-104.507597,29.639255],[-104.677382,29.924056],[-104.688336,30.181472],[-104.858121,30.389596],[-104.896459,30.570335],[-105.005998,30.685351],[-105.394861,30.855136],[-105.602985,31.085167],[-105.77277,31.167321],[-105.953509,31.364491],[-106.205448,31.468553],[-106.38071,31.731446],[-106.528588,31.786216],[-106.643603,31.901231],[-106.616219,31.999816],[-103.067161,31.999816],[-103.067161,33.002096],[-103.045254,34.01533],[-103.039777,36.501861],[-103.001438,36.501861],[-101.812942,36.501861]]]}}, {"type":"Feature","id":"49","properties":{"name":"Utah","density":34.30},"geometry":{"type":"Polygon","coordinates":[[[-112.164359,41.995232],[-111.047063,42.000709],[-111.047063,40.998429],[-109.04798,40.998429],[-109.053457,39.125316],[-109.058934,38.27639],[-109.042503,38.166851],[-109.042503,37.000263],[-110.499369,37.00574],[-114.048427,37.000263],[-114.04295,41.995232],[-112.164359,41.995232]]]}}, {"type":"Feature","id":"50","properties":{"name":"Vermont","density":67.73},"geometry":{"type":"Polygon","coordinates":[[[-71.503554,45.013027],[-71.4926,44.914442],[-71.629524,44.750133],[-71.536416,44.585825],[-71.700724,44.41604],[-72.034817,44.322932],[-72.02934,44.07647],[-72.116971,43.994316],[-72.204602,43.769761],[-72.379864,43.572591],[-72.456542,43.150867],[-72.445588,43.008466],[-72.533219,42.953697],[-72.544173,42.80582],[-72.456542,42.729142],[-73.267129,42.745573],[-73.278083,42.833204],[-73.245221,43.523299],[-73.404052,43.687607],[-73.349283,43.769761],[-73.436914,44.043608],[-73.321898,44.246255],[-73.294514,44.437948],[-73.387622,44.618687],[-73.332852,44.804903],[-73.343806,45.013027],[-72.308664,45.002073],[-71.503554,45.013027]]]}}, -{"type":"Feature","id":"51","properties":{"name":"Virginia","density":204.5},"geometry":{"type":"MultiPolygon","coordinates":[[[[-75.397659,38.013497],[-75.244304,38.029928],[-75.375751,37.860142],[-75.512674,37.799896],[-75.594828,37.569865],[-75.802952,37.197433],[-75.972737,37.120755],[-76.027507,37.257679],[-75.939876,37.564388],[-75.671506,37.95325],[-75.397659,38.013497]]],[[[-76.016553,37.95325],[-75.994645,37.95325],[-76.043938,37.95325],[-76.016553,37.95325]]],[[[-78.349729,39.464886],[-77.82942,39.130793],[-77.719881,39.322485],[-77.566527,39.306055],[-77.456988,39.223901],[-77.456988,39.076023],[-77.248864,39.026731],[-77.117418,38.933623],[-77.040741,38.791222],[-77.128372,38.632391],[-77.248864,38.588575],[-77.325542,38.446175],[-77.281726,38.342113],[-77.013356,38.374975],[-76.964064,38.216144],[-76.613539,38.15042],[-76.514954,38.024451],[-76.235631,37.887527],[-76.3616,37.608203],[-76.246584,37.389126],[-76.383508,37.285064],[-76.399939,37.159094],[-76.273969,37.082417],[-76.410893,36.961924],[-76.619016,37.120755],[-76.668309,37.065986],[-76.48757,36.95097],[-75.994645,36.923586],[-75.868676,36.551154],[-79.510841,36.5402],[-80.294043,36.545677],[-80.978661,36.562108],[-81.679709,36.589492],[-83.673316,36.600446],[-83.136575,36.742847],[-83.070852,36.852385],[-82.879159,36.890724],[-82.868205,36.978355],[-82.720328,37.044078],[-82.720328,37.120755],[-82.353373,37.268633],[-81.969987,37.537003],[-81.986418,37.454849],[-81.849494,37.285064],[-81.679709,37.20291],[-81.55374,37.208387],[-81.362047,37.339833],[-81.225123,37.235771],[-80.967707,37.290541],[-80.513121,37.482234],[-80.474782,37.421987],[-80.29952,37.509618],[-80.294043,37.690357],[-80.184505,37.849189],[-79.998289,37.997066],[-79.921611,38.177805],[-79.724442,38.364021],[-79.647764,38.594052],[-79.477979,38.457129],[-79.313671,38.413313],[-79.209609,38.495467],[-78.996008,38.851469],[-78.870039,38.763838],[-78.404499,39.169131],[-78.349729,39.464886]]]]}}, +{"type":"Feature","id":"51","properties":{"name":"Virginia","density":204.5},"geometry":{"type":"MultiPolygon","coordinates":[[[[-75.397659,38.013497],[-75.244304,38.029928],[-75.375751,37.860142],[-75.512674,37.799896],[-75.594828,37.569865],[-75.802952,37.197433],[-75.972737,37.120755],[-76.027507,37.257679],[-75.939876,37.564388],[-75.671506,37.95325],[-75.397659,38.013497]]],[[[-76.016553,37.95325],[-76.043938,37.95325],[-75.994645,37.95325],[-76.016553,37.95325]]],[[[-78.349729,39.464886],[-77.82942,39.130793],[-77.719881,39.322485],[-77.566527,39.306055],[-77.456988,39.223901],[-77.456988,39.076023],[-77.248864,39.026731],[-77.117418,38.933623],[-77.040741,38.791222],[-77.128372,38.632391],[-77.248864,38.588575],[-77.325542,38.446175],[-77.281726,38.342113],[-77.013356,38.374975],[-76.964064,38.216144],[-76.613539,38.15042],[-76.514954,38.024451],[-76.235631,37.887527],[-76.3616,37.608203],[-76.246584,37.389126],[-76.383508,37.285064],[-76.399939,37.159094],[-76.273969,37.082417],[-76.410893,36.961924],[-76.619016,37.120755],[-76.668309,37.065986],[-76.48757,36.95097],[-75.994645,36.923586],[-75.868676,36.551154],[-79.510841,36.5402],[-80.294043,36.545677],[-80.978661,36.562108],[-81.679709,36.589492],[-83.673316,36.600446],[-83.136575,36.742847],[-83.070852,36.852385],[-82.879159,36.890724],[-82.868205,36.978355],[-82.720328,37.044078],[-82.720328,37.120755],[-82.353373,37.268633],[-81.969987,37.537003],[-81.986418,37.454849],[-81.849494,37.285064],[-81.679709,37.20291],[-81.55374,37.208387],[-81.362047,37.339833],[-81.225123,37.235771],[-80.967707,37.290541],[-80.513121,37.482234],[-80.474782,37.421987],[-80.29952,37.509618],[-80.294043,37.690357],[-80.184505,37.849189],[-79.998289,37.997066],[-79.921611,38.177805],[-79.724442,38.364021],[-79.647764,38.594052],[-79.477979,38.457129],[-79.313671,38.413313],[-79.209609,38.495467],[-78.996008,38.851469],[-78.870039,38.763838],[-78.404499,39.169131],[-78.349729,39.464886]]]]}}, {"type":"Feature","id":"53","properties":{"name":"Washington","density":102.6},"geometry":{"type":"MultiPolygon","coordinates":[[[[-117.033359,49.000239],[-117.044313,47.762451],[-117.038836,46.426077],[-117.055267,46.343923],[-116.92382,46.168661],[-116.918344,45.993399],[-118.988627,45.998876],[-119.125551,45.933153],[-119.525367,45.911245],[-119.963522,45.823614],[-120.209985,45.725029],[-120.505739,45.697644],[-120.637186,45.746937],[-121.18488,45.604536],[-121.217742,45.670259],[-121.535404,45.725029],[-121.809251,45.708598],[-122.247407,45.549767],[-122.762239,45.659305],[-122.811531,45.960537],[-122.904639,46.08103],[-123.11824,46.185092],[-123.211348,46.174138],[-123.370179,46.146753],[-123.545441,46.261769],[-123.72618,46.300108],[-123.874058,46.239861],[-124.065751,46.327492],[-124.027412,46.464416],[-123.895966,46.535616],[-124.098612,46.74374],[-124.235536,47.285957],[-124.31769,47.357157],[-124.427229,47.740543],[-124.624399,47.88842],[-124.706553,48.184175],[-124.597014,48.381345],[-124.394367,48.288237],[-123.983597,48.162267],[-123.704273,48.167744],[-123.424949,48.118452],[-123.162056,48.167744],[-123.036086,48.080113],[-122.800578,48.08559],[-122.636269,47.866512],[-122.515777,47.882943],[-122.493869,47.587189],[-122.422669,47.318818],[-122.324084,47.346203],[-122.422669,47.576235],[-122.395284,47.800789],[-122.230976,48.030821],[-122.362422,48.123929],[-122.373376,48.288237],[-122.471961,48.468976],[-122.422669,48.600422],[-122.488392,48.753777],[-122.647223,48.775685],[-122.795101,48.8907],[-122.756762,49.000239],[-117.033359,49.000239]]],[[[-122.718423,48.310145],[-122.586977,48.35396],[-122.608885,48.151313],[-122.767716,48.227991],[-122.718423,48.310145]]],[[[-123.025132,48.583992],[-122.915593,48.715438],[-122.767716,48.556607],[-122.811531,48.419683],[-123.041563,48.458022],[-123.025132,48.583992]]]]}}, {"type":"Feature","id":"54","properties":{"name":"West Virginia","density":77.06},"geometry":{"type":"Polygon","coordinates":[[[-80.518598,40.636951],[-80.518598,39.722302],[-79.477979,39.722302],[-79.488933,39.20747],[-79.291763,39.300578],[-79.094593,39.470363],[-78.963147,39.437501],[-78.765977,39.585379],[-78.470222,39.514178],[-78.431884,39.623717],[-78.267575,39.61824],[-78.174467,39.694917],[-78.004682,39.601809],[-77.834897,39.601809],[-77.719881,39.322485],[-77.82942,39.130793],[-78.349729,39.464886],[-78.404499,39.169131],[-78.870039,38.763838],[-78.996008,38.851469],[-79.209609,38.495467],[-79.313671,38.413313],[-79.477979,38.457129],[-79.647764,38.594052],[-79.724442,38.364021],[-79.921611,38.177805],[-79.998289,37.997066],[-80.184505,37.849189],[-80.294043,37.690357],[-80.29952,37.509618],[-80.474782,37.421987],[-80.513121,37.482234],[-80.967707,37.290541],[-81.225123,37.235771],[-81.362047,37.339833],[-81.55374,37.208387],[-81.679709,37.20291],[-81.849494,37.285064],[-81.986418,37.454849],[-81.969987,37.537003],[-82.101434,37.553434],[-82.293127,37.668449],[-82.342419,37.783465],[-82.50125,37.931343],[-82.621743,38.123036],[-82.594358,38.424267],[-82.331465,38.446175],[-82.293127,38.577622],[-82.172634,38.632391],[-82.221926,38.785745],[-82.03571,39.026731],[-81.887833,38.873376],[-81.783771,38.966484],[-81.811156,39.0815],[-81.685186,39.273193],[-81.57017,39.267716],[-81.455155,39.410117],[-81.345616,39.344393],[-81.219646,39.388209],[-80.830783,39.711348],[-80.737675,40.078303],[-80.600752,40.319289],[-80.595275,40.472643],[-80.666475,40.582182],[-80.518598,40.636951]]]}}, {"type":"Feature","id":"55","properties":{"name":"Wisconsin","density":105.2},"geometry":{"type":"Polygon","coordinates":[[[-90.415429,46.568478],[-90.229213,46.508231],[-90.119674,46.338446],[-89.09001,46.135799],[-88.662808,45.987922],[-88.531362,46.020784],[-88.10416,45.922199],[-87.989145,45.796229],[-87.781021,45.675736],[-87.791975,45.500474],[-87.885083,45.363551],[-87.649574,45.341643],[-87.742682,45.199243],[-87.589328,45.095181],[-87.627666,44.974688],[-87.819359,44.95278],[-87.983668,44.722749],[-88.043914,44.563917],[-87.928898,44.536533],[-87.775544,44.640595],[-87.611236,44.837764],[-87.403112,44.914442],[-87.238804,45.166381],[-87.03068,45.22115],[-87.047111,45.089704],[-87.189511,44.969211],[-87.468835,44.552964],[-87.545512,44.322932],[-87.540035,44.158624],[-87.644097,44.103854],[-87.737205,43.8793],[-87.704344,43.687607],[-87.791975,43.561637],[-87.912467,43.249452],[-87.885083,43.002989],[-87.76459,42.783912],[-87.802929,42.493634],[-88.788778,42.493634],[-90.639984,42.510065],[-90.711184,42.636034],[-91.067185,42.75105],[-91.143862,42.909881],[-91.176724,43.134436],[-91.056231,43.254929],[-91.204109,43.353514],[-91.215062,43.501391],[-91.269832,43.616407],[-91.242447,43.775238],[-91.43414,43.994316],[-91.592971,44.032654],[-91.877772,44.202439],[-91.927065,44.333886],[-92.233773,44.443425],[-92.337835,44.552964],[-92.545959,44.569394],[-92.808852,44.750133],[-92.737652,45.117088],[-92.75956,45.286874],[-92.644544,45.440228],[-92.770513,45.566198],[-92.885529,45.577151],[-92.869098,45.719552],[-92.639067,45.933153],[-92.354266,46.015307],[-92.29402,46.075553],[-92.29402,46.667063],[-92.091373,46.749217],[-92.014696,46.705401],[-91.790141,46.694447],[-91.09457,46.864232],[-90.837154,46.95734],[-90.749522,46.88614],[-90.886446,46.754694],[-90.55783,46.584908],[-90.415429,46.568478]]]}}, diff --git a/client/src/lib/__tests__/brand-lockup.test.ts b/client/src/lib/__tests__/brand-lockup.test.ts new file mode 100644 index 0000000..498c789 --- /dev/null +++ b/client/src/lib/__tests__/brand-lockup.test.ts @@ -0,0 +1,51 @@ +// The header and the footer used to disagree on the company's own name: the +// animated Wordmark rendered "Grid" + "Tilt" while HomeFooter hardcoded its +// own "Grid" + "tilt". Both now split one exported constant. These tests scan +// source rather than importing the .tsx, so they need no DOM. +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync, readdirSync } from "fs"; +import { join } from "path"; + +const ROOT = process.cwd(); +const CLIENT = join(ROOT, "client", "src"); + +function sourceFiles(dir: string, acc: string[] = []): string[] { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const p = join(dir, entry.name); + if (entry.isDirectory()) { + if (entry.name !== "node_modules" && entry.name !== "__tests__") sourceFiles(p, acc); + } else if (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts")) { + acc.push(p); + } + } + return acc; +} + +test("the wordmark exports one lockup and the halves rebuild the brand", () => { + const src = readFileSync(join(CLIENT, "components", "home", "Wordmark.tsx"), "utf-8"); + assert.match(src, /export const BRAND_TEXT = "GridTilt";/, "brand text must stay GridTilt"); + assert.match(src, /export const BRAND_HEAD/); + assert.match(src, /export const BRAND_TAIL/); + + const split = Number(/export const BRAND_SPLIT = (\d+);/.exec(src)?.[1]); + assert.equal("GridTilt".slice(0, split), "Grid"); + assert.equal("GridTilt".slice(split), "Tilt", "the accent half is Tilt, not tilt"); +}); + +test("no surface renders the brand with a lowercase second half", () => { + const offenders: string[] = []; + for (const file of sourceFiles(CLIENT)) { + const src = readFileSync(file, "utf-8"); + // "Grid" immediately followed by a lowercase "tilt" in rendered markup. + // The domain (gridtilt.com), the X handle and mailto addresses are all + // lowercase on purpose and never start with a capital G here. + for (const m of src.match(/Grid<[^>]*>tilt/g) ?? []) { + offenders.push(`${file.replace(ROOT + "/", "")}: ${m.slice(0, 48)}`); + } + for (const m of src.match(/>\s*Gridtilt\s* for every route, including +// "/". This page used to add HomeFooter on top of it, so the landing shipped +// two footers: two brand lockups, two copyright lines, two source lists. The +// GitHub and contact links that only HomeFooter carried moved into SiteFooter. export default function Home() { return (
    -
    );