| 1 | import { Fragment } from "react"; |
| 2 | import Image from "next/image"; |
| 3 | import Link from "next/link"; |
| 4 | import { GettingStartedSteps } from "@/components/getting-started-steps"; |
| 5 | import { InstallCodeBlock } from "@/components/install-code-block"; |
| 6 | import { Seal } from "@/components/seal"; |
| 7 | import { TerminalPlayer } from "@/components/terminal-player"; |
| 8 | import { Ticker } from "@/components/ticker"; |
| 9 | import { Whale } from "@/components/whale"; |
| 10 | import { getFacts } from "@/lib/facts"; |
| 11 | import { fetchFeed } from "@/lib/github"; |
| 12 | import { fill, getChrome, getHome, splitToken } from "@/lib/i18n/dictionaries"; |
| 13 | import { REPO_ISSUES_URL, REPO_RELEASES_URL, REPO_URL, DISCORD_URL } from "@/lib/i18n/links"; |
| 14 | import { getEnv } from "@/lib/kv"; |
| 15 | import type { FeedItem } from "@/lib/types"; |
| 16 | |
| 17 | // Revalidate against source-proven runtime facts without giving up static edge |
| 18 | // caching. `getFacts()` rejects legacy or older KV snapshots. |
| 19 | export const revalidate = 300; |
| 20 | |
| 21 | /** |
| 22 | * The newspaper-ocean homepage. |
| 23 | * |
| 24 | * Every visible string resolves through `getHome(locale)` / `getChrome(locale)` |
| 25 | * — English, Chinese, and every other routed locale take the identical path, |
| 26 | * with the English dictionary as the build-time-guaranteed fallback. The only |
| 27 | * literals left in this file are code-owned per docs/VOICE.md: the product |
| 28 | * control vocabulary (`Plan · Act · Operate`, `Ask · Auto-Review · Full |
| 29 | * Access`, `TUI · exec · web · API`), the install command, `cargo test |
| 30 | * --locked`, the receipt verbs, package-manager and mirror proper nouns, and |
| 31 | * the screenshot path. |
| 32 | */ |
| 33 | export default async function HomePage({ params }: { params: Promise<{ locale: string }> }) { |
| 34 | const { locale } = await params; |
| 35 | const d = getHome(locale); |
| 36 | const chrome = getChrome(locale); |
| 37 | const facts = await getFacts(); |
| 38 | const sourceVersion = facts.version ?? "unknown"; |
| 39 | const publishedRelease = facts.latestPublishedRelease; |
| 40 | const sourceIsPublished = publishedRelease?.version === sourceVersion; |
| 41 | const providerCount = facts.providers.length; |
| 42 | const providerRoutes = fill(d.providerRoutes, { count: providerCount }); |
| 43 | |
| 44 | // The lede typesets the brand in its own span. Splitting on the {brand} |
| 45 | // token keeps the sentence a single translated unit — no concatenation of |
| 46 | // fragments around a variable, and a locale may place the brand anywhere. |
| 47 | const ledeParts = splitToken(d.heroIntro, "brand"); |
| 48 | |
| 49 | let feed: FeedItem[] = []; |
| 50 | try { |
| 51 | const env = await getEnv(); |
| 52 | feed = await fetchFeed(env.GITHUB_TOKEN, 20); |
| 53 | } catch { |
| 54 | /* ticker is optional chrome */ |
| 55 | } |
| 56 | |
| 57 | return ( |
| 58 | <div className="product-home paper-home"> |
| 59 | {/* HERO — newspaper split: claim + live terminal proof */} |
| 60 | <section className="product-hero paper-hero"> |
| 61 | <div className="product-container product-hero-grid paper-hero-grid"> |
| 62 | <div className="product-hero-copy paper-hero-copy"> |
| 63 | <div className="mb-5"> |
| 64 | <span className="pill pill-hot">{d.kicker}</span> |
| 65 | </div> |
| 66 | |
| 67 | <h1 className="font-display tracking-crisp"> |
| 68 | {d.heroTitleA} |
| 69 | <br /> |
| 70 | <span className="paper-hero-accent">{d.heroTitleB}</span> |
| 71 | </h1> |
| 72 | |
| 73 | <p className="paper-hero-lede"> |
| 74 | {ledeParts.map((part, index) => ( |
| 75 | <Fragment key={index}> |
| 76 | {index > 0 && ( |
| 77 | <span className="font-cjk text-indigo font-semibold">Codewhale</span> |
| 78 | )} |
| 79 | {part} |
| 80 | </Fragment> |
| 81 | ))} |
| 82 | </p> |
| 83 | |
| 84 | <div className="product-actions paper-actions"> |
| 85 | <Link href={`/${locale}/install`} className="product-button product-button-primary"> |
| 86 | {d.install} <span aria-hidden>→</span> |
| 87 | </Link> |
| 88 | <Link href={`/${locale}/docs`} className="product-button"> |
| 89 | {d.docs} <span aria-hidden>→</span> |
| 90 | </Link> |
| 91 | <a href={REPO_URL} className="product-button product-button-ghost"> |
| 92 | GitHub |
| 93 | </a> |
| 94 | </div> |
| 95 | |
| 96 | <div className="product-install paper-install"> |
| 97 | <div className="eyebrow mb-2">{d.installEyebrow}</div> |
| 98 | <InstallCodeBlock |
| 99 | cmd="npm install -g codewhale" |
| 100 | copyLabel={d.copy} |
| 101 | copiedLabel={d.copied} |
| 102 | /> |
| 103 | <div className="paper-install-meta"> |
| 104 | <span>{d.installRequirement}</span> |
| 105 | <Link href={`/${locale}/install`} className="text-indigo hover:underline"> |
| 106 | {d.installOtherWays} |
| 107 | </Link> |
| 108 | </div> |
| 109 | </div> |
| 110 | |
| 111 | {/* |
| 112 | The TUI header grammar: a `cw` chip and a dot chain. Each fact is |
| 113 | its own translated unit — the separators are CSS punctuation, so |
| 114 | nothing is concatenated around a token and no locale inherits an |
| 115 | English joining word. `cw` is the binary's own name, code-owned |
| 116 | exactly like `Codewhale`. |
| 117 | */} |
| 118 | <p |
| 119 | className="product-facts paper-facts dotline" |
| 120 | data-source-state={sourceIsPublished ? "published release" : "source candidate"} |
| 121 | data-source-state-label={sourceIsPublished ? d.publishedRelease : d.figcaptionSourceCandidate} |
| 122 | > |
| 123 | <span className="dotline-chip">cw</span> |
| 124 | <span> |
| 125 | {publishedRelease |
| 126 | ? fill(d.latestRelease, { tag: publishedRelease.tag }) |
| 127 | : d.releaseUnavailable} |
| 128 | </span> |
| 129 | <span> |
| 130 | {`${sourceIsPublished ? d.currentSource : d.sourceCandidate} v${sourceVersion}`} |
| 131 | </span> |
| 132 | <span>{providerRoutes}</span> |
| 133 | <span>{facts.license ?? "MIT"}</span> |
| 134 | </p> |
| 135 | </div> |
| 136 | |
| 137 | <figure className="product-shot paper-shot"> |
| 138 | <div className="product-shot-toolbar paper-shot-toolbar"> |
| 139 | <span> |
| 140 | <Whale size={18} /> |
| 141 | Codewhale TUI |
| 142 | </span> |
| 143 | <span>{d.shotSession}</span> |
| 144 | </div> |
| 145 | <Image |
| 146 | src="/codewhale-tui.png" |
| 147 | alt={d.screenshotAlt} |
| 148 | width={1562} |
| 149 | height={1256} |
| 150 | sizes="(max-width: 900px) calc(100vw - 2rem), 52vw" |
| 151 | priority |
| 152 | /> |
| 153 | <figcaption>{d.figcaption}</figcaption> |
| 154 | </figure> |
| 155 | </div> |
| 156 | </section> |
| 157 | |
| 158 | {/* Live repo wire — merges, issues, and releases straight from GitHub, |
| 159 | with the contributor named. Absent entirely when the feed is empty. */} |
| 160 | {feed.length > 0 ? ( |
| 161 | <Ticker |
| 162 | items={feed} |
| 163 | labels={{ |
| 164 | liveLabel: chrome.tickerLiveLabel, |
| 165 | liveTag: chrome.tickerLiveTag, |
| 166 | ariaLabel: chrome.tickerAria, |
| 167 | merged: chrome.tickerMerged, |
| 168 | opened: chrome.tickerOpened, |
| 169 | closed: chrome.tickerClosed, |
| 170 | released: chrome.tickerReleased, |
| 171 | firstContribution: chrome.tickerFirstContribution, |
| 172 | by: chrome.tickerBy, |
| 173 | dateLocale: chrome.dateLocale, |
| 174 | }} |
| 175 | /> |
| 176 | ) : null} |
| 177 | |
| 178 | {/* Proof strip */} |
| 179 | <section className="product-proof paper-proof"> |
| 180 | <div className="product-container product-proof-grid"> |
| 181 | <h2 className="font-display">{d.proofHeading}</h2> |
| 182 | <p>{d.proofBody}</p> |
| 183 | </div> |
| 184 | </section> |
| 185 | |
| 186 | {/* See how it decides — constitution traces in terminal chrome */} |
| 187 | <section className="paper-decides"> |
| 188 | <div className="product-container paper-decides-grid"> |
| 189 | <div> |
| 190 | <div className="flex items-baseline gap-4 mb-3 hairline-b pb-3"> |
| 191 | <Seal char={d.sealDecides} size="sm" variant="indigo" /> |
| 192 | <div> |
| 193 | <div className="eyebrow mb-1">{d.decidesEyebrow}</div> |
| 194 | <h2 className="font-display text-2xl sm:text-3xl">{d.decidesHeading}</h2> |
| 195 | </div> |
| 196 | </div> |
| 197 | <p className="paper-decides-lede">{d.decidesLede}</p> |
| 198 | </div> |
| 199 | <div> |
| 200 | <TerminalPlayer |
| 201 | locale={locale} |
| 202 | traceLabel={chrome.traceLabel} |
| 203 | tabsAria={chrome.traceTabsAria} |
| 204 | /> |
| 205 | </div> |
| 206 | </div> |
| 207 | </section> |
| 208 | |
| 209 | {/* Workflow */} |
| 210 | <section className="product-workflow paper-workflow"> |
| 211 | <div className="product-container"> |
| 212 | <div className="flex items-baseline gap-4 mb-6 hairline-b pb-4"> |
| 213 | <Seal char={d.sealWorkflow} size="sm" /> |
| 214 | <h2 className="font-display">{d.workflowHeading}</h2> |
| 215 | </div> |
| 216 | <ol className="product-workflow-steps"> |
| 217 | {d.workflow.map(([title, description], index) => ( |
| 218 | <li key={title}> |
| 219 | <span>{String(index + 1).padStart(2, "0")}</span> |
| 220 | <h3>{title}</h3> |
| 221 | <p>{description}</p> |
| 222 | </li> |
| 223 | ))} |
| 224 | </ol> |
| 225 | <div className="product-receipt" aria-label={d.receiptAria}> |
| 226 | <span>$ codewhale exec "fix the failing test"</span> |
| 227 | <span>inspect {d.receiptInspect}</span> |
| 228 | <span>act {d.receiptAct}</span> |
| 229 | <span>verify cargo test --locked</span> |
| 230 | <strong>report {d.receiptReport}</strong> |
| 231 | </div> |
| 232 | </div> |
| 233 | </section> |
| 234 | |
| 235 | {/* Getting started */} |
| 236 | <section className="product-start paper-start"> |
| 237 | <div className="product-container"> |
| 238 | <div className="flex items-baseline gap-4 mb-4 hairline-b pb-4"> |
| 239 | <Seal char={d.sealStart} size="sm" /> |
| 240 | <h2 className="font-display">{d.startHeading}</h2> |
| 241 | </div> |
| 242 | <p className="product-start-lede">{d.startLede}</p> |
| 243 | <GettingStartedSteps locale={locale} /> |
| 244 | <div className="product-start-links"> |
| 245 | <Link href={`/${locale}/docs/guide`}>{d.startGuideLink}</Link> |
| 246 | <Link href={`/${locale}/docs/vocabulary`}>{d.startVocabularyLink}</Link> |
| 247 | </div> |
| 248 | </div> |
| 249 | </section> |
| 250 | |
| 251 | {/* Boundaries */} |
| 252 | <section className="product-boundaries paper-boundaries"> |
| 253 | <div className="product-container product-boundaries-grid"> |
| 254 | <div> |
| 255 | <div className="flex items-baseline gap-4 mb-4"> |
| 256 | <Seal char={d.sealBoundaries} size="sm" /> |
| 257 | <h2 className="font-display"> |
| 258 | {d.boundariesHeadingA} |
| 259 | <br /> |
| 260 | <span>{d.boundariesHeadingB}</span> |
| 261 | </h2> |
| 262 | </div> |
| 263 | <p>{d.boundariesBody}</p> |
| 264 | </div> |
| 265 | <dl className="product-boundary-list"> |
| 266 | <div> |
| 267 | <dt>{providerRoutes}</dt> |
| 268 | <dd>{d.hostedGatewayLocal}</dd> |
| 269 | </div> |
| 270 | <div> |
| 271 | <dt>Plan · Act · Operate</dt> |
| 272 | <dd>{d.planActOperateDesc}</dd> |
| 273 | </div> |
| 274 | <div> |
| 275 | <dt>Ask · Auto-Review · Full Access</dt> |
| 276 | <dd>{d.askAutoReviewDesc}</dd> |
| 277 | </div> |
| 278 | <div> |
| 279 | <dt>TUI · exec · web · API</dt> |
| 280 | <dd>{d.tuiExecWebDesc}</dd> |
| 281 | </div> |
| 282 | </dl> |
| 283 | </div> |
| 284 | </section> |
| 285 | |
| 286 | {/* |
| 287 | THE WATERLINE. Everything below here is one water column: a single |
| 288 | gradient on the wrapper, sampled by absolute page position across the |
| 289 | three bands inside it, continuing into the footer's identical deep |
| 290 | stop as the seabed. The bands themselves carry no field of their own. |
| 291 | */} |
| 292 | <div className="ocean-column"> |
| 293 | {/* Surfaces */} |
| 294 | <section className="product-surfaces paper-surfaces"> |
| 295 | <div className="product-container"> |
| 296 | <div className="flex items-baseline gap-4 mb-6 hairline-b pb-4"> |
| 297 | <Seal char={d.sealSurfaces} size="sm" /> |
| 298 | <h2 className="font-display">{d.surfacesHeading}</h2> |
| 299 | </div> |
| 300 | <div className="product-surface-list"> |
| 301 | {d.surfaces.map(([name, description]) => ( |
| 302 | <div key={name}> |
| 303 | <strong>{name}</strong> |
| 304 | <span>{description}</span> |
| 305 | </div> |
| 306 | ))} |
| 307 | </div> |
| 308 | <Link href={`/${locale}/runtime`}>{d.runtimeLink}</Link> |
| 309 | </div> |
| 310 | </section> |
| 311 | |
| 312 | {/* Install band */} |
| 313 | <section className="product-install-band paper-install-band"> |
| 314 | <div className="product-container product-install-grid"> |
| 315 | <h2 className="font-display">{d.installBandHeading}</h2> |
| 316 | <div> |
| 317 | {/* The composer plate. `❯` is a code-owned literal, like the |
| 318 | install command it prompts for — it is the product's glyph, |
| 319 | not a sentence, and no locale renders it differently. */} |
| 320 | <div className="product-composer"> |
| 321 | <span className="product-composer-prompt" aria-hidden> |
| 322 | ❯ |
| 323 | </span> |
| 324 | <InstallCodeBlock |
| 325 | cmd="npm install -g codewhale" |
| 326 | copyLabel={d.copy} |
| 327 | copiedLabel={d.copied} |
| 328 | /> |
| 329 | </div> |
| 330 | {/* Already a dot chain in every locale; the separators just stop |
| 331 | being characters in the markup and become CSS punctuation. */} |
| 332 | <p className="dotline"> |
| 333 | <span>Cargo</span> |
| 334 | <span>{d.binaries}</span> |
| 335 | <span>Docker</span> |
| 336 | <span>Nix</span> |
| 337 | <span>Windows</span> |
| 338 | <span>Android / Termux</span> |
| 339 | <span>{d.chinaMirrors}</span> |
| 340 | </p> |
| 341 | <Link href={`/${locale}/install`}>{d.installGuideLink}</Link> |
| 342 | </div> |
| 343 | </div> |
| 344 | </section> |
| 345 | |
| 346 | {/* Community */} |
| 347 | <section className="product-community paper-community"> |
| 348 | <div className="product-container product-community-grid"> |
| 349 | <div className="product-community-illustration" aria-hidden="true"> |
| 350 | <Seal char={d.sealCommunity} size="lg" /> |
| 351 | </div> |
| 352 | <div> |
| 353 | <h2 className="font-display">{d.communityHeading}</h2> |
| 354 | <p>{d.communityBody}</p> |
| 355 | </div> |
| 356 | <nav aria-label={d.communityLinksAria}> |
| 357 | <a href={REPO_URL}>GitHub</a> |
| 358 | <a href={REPO_ISSUES_URL}>Issues</a> |
| 359 | <a href={DISCORD_URL}>Discord</a> |
| 360 | <Link href={`/${locale}/contribute`}>{d.contribute}</Link> |
| 361 | {publishedRelease ? ( |
| 362 | <a href={publishedRelease.url}>{publishedRelease.tag}</a> |
| 363 | ) : ( |
| 364 | <a href={REPO_RELEASES_URL}>Releases</a> |
| 365 | )} |
| 366 | </nav> |
| 367 | </div> |
| 368 | </section> |
| 369 | </div> |
| 370 | </div> |
| 371 | ); |
| 372 | } |
| 373 |