| 1 | import { Fragment } from "react"; |
| 2 | import Link from "next/link"; |
| 3 | import { GettingStartedSteps } from "@/components/getting-started-steps"; |
| 4 | import { HeroInstall } from "@/components/hero-install"; |
| 5 | import { Icon, type IconName } from "@/components/icon"; |
| 6 | import { InstallCodeBlock } from "@/components/install-code-block"; |
| 7 | import { Section } from "@/components/page-header"; |
| 8 | import { Status, type StatusTone } from "@/components/status-badge"; |
| 9 | import { NativeTerminalGallery } from "@/components/native-terminal-gallery"; |
| 10 | import { WhaleLive } from "@/components/whale-live"; |
| 11 | import { getFacts } from "@/lib/facts"; |
| 12 | import { GETTING_STARTED_STEPS } from "@/lib/content/getting-started"; |
| 13 | import { fill, getHome, splitToken } from "@/lib/i18n/dictionaries"; |
| 14 | import { |
| 15 | APP_SIGNUP_URL, |
| 16 | DISCORD_URL, |
| 17 | REPO_ISSUES_URL, |
| 18 | REPO_RELEASES_URL, |
| 19 | REPO_URL, |
| 20 | } from "@/lib/i18n/links"; |
| 21 | import { serializeJsonLd } from "@/lib/json-ld"; |
| 22 | import { buildSoftwareApplicationJsonLd } from "@/lib/software-application-schema"; |
| 23 | import { TERMINAL_SCREENSHOT } from "@/lib/media-manifest"; |
| 24 | |
| 25 | // Revalidate against source-proven runtime facts without giving up static edge |
| 26 | // caching. `getFacts()` rejects legacy or older KV snapshots. |
| 27 | export const revalidate = 300; |
| 28 | |
| 29 | // Row order is shared by every locale's `gain` and `availability` lists, so |
| 30 | // the marks and states follow the row, not a word. |
| 31 | const GAIN_ICONS: IconName[] = ["terminal", "repeat", "shield"]; |
| 32 | // Released · GUI available · development preview · development build · in |
| 33 | // development. |
| 34 | const AVAILABILITY_TONES: StatusTone[] = ["ready", "ready", "attention", "idle", "idle"]; |
| 35 | |
| 36 | /** |
| 37 | * The whale-road homepage, paper above and sea below. The promise and the |
| 38 | * install command sit on paper; the live v2 whale (the desktop app's own |
| 39 | * Director) rests on one horizon line; the real terminal floats in the deep |
| 40 | * water just under it. The reading sections return to paper, and the page |
| 41 | * ends in the sea with the install command, running into the footer. |
| 42 | * |
| 43 | * One memorable thing moves: the whale. It breathes, glances toward the |
| 44 | * pointer, and acts out the terminal view a reader picks. Everything else is |
| 45 | * still. Reduced motion shows its poster pose. |
| 46 | * |
| 47 | * Every visible string resolves through `getHome(locale)`. The only literals |
| 48 | * left here are code-owned per docs/VOICE.md: the product control vocabulary |
| 49 | * (`Plan · Work · Operate`, `Ask · Auto-Review · Full Access`) and package |
| 50 | * channel proper nouns. |
| 51 | */ |
| 52 | export default async function HomePage({ params }: { params: Promise<{ locale: string }> }) { |
| 53 | const { locale } = await params; |
| 54 | const d = getHome(locale); |
| 55 | const facts = await getFacts(); |
| 56 | const sourceVersion = facts.version ?? "unknown"; |
| 57 | const publishedRelease = facts.latestPublishedRelease; |
| 58 | const sourceIsPublished = publishedRelease?.version === sourceVersion; |
| 59 | |
| 60 | // The install URL resolves published artifacts, so its structured version |
| 61 | // must come from the published-release receipt rather than source-candidate |
| 62 | // facts. When no release is known, the schema omits softwareVersion. |
| 63 | const jsonLd = buildSoftwareApplicationJsonLd(publishedRelease); |
| 64 | |
| 65 | // The lede typesets the brand in its own span. Splitting on the {brand} |
| 66 | // token keeps the sentence a single translated unit — no concatenation of |
| 67 | // fragments around a variable, and a locale may place the brand anywhere. |
| 68 | const ledeParts = splitToken(d.heroIntro, "brand"); |
| 69 | |
| 70 | return ( |
| 71 | <div className="home"> |
| 72 | <script |
| 73 | type="application/ld+json" |
| 74 | dangerouslySetInnerHTML={{ __html: serializeJsonLd(jsonLd) }} |
| 75 | /> |
| 76 | |
| 77 | {/* PAPER — the promise, one primary action, the install command, and |
| 78 | the whale resting on the horizon. */} |
| 79 | <section className="home-hero" aria-labelledby="home-title"> |
| 80 | <div className="home-hero-inner"> |
| 81 | <div className="home-hero-copy"> |
| 82 | <h1 id="home-title" className="home-title">{d.heroTitle}</h1> |
| 83 | <p className="home-lede"> |
| 84 | {ledeParts.map((part, index) => ( |
| 85 | <Fragment key={index}> |
| 86 | {index > 0 && <strong>Codewhale</strong>} |
| 87 | {part} |
| 88 | </Fragment> |
| 89 | ))} |
| 90 | </p> |
| 91 | <HeroInstall ariaLabel={d.heroInstallAria} copyLabel={d.copy} copiedLabel={d.copied} /> |
| 92 | <div className="actions"> |
| 93 | <Link href={`/${locale}/install`} className="btn btn-primary btn-lg"> |
| 94 | {d.getCodewhale} |
| 95 | </Link> |
| 96 | <Link href={`/${locale}/product`} className="btn btn-ghost btn-lg"> |
| 97 | {d.exploreProduct} |
| 98 | <Icon name="arrow-right" className="icon icon-flip" /> |
| 99 | </Link> |
| 100 | </div> |
| 101 | </div> |
| 102 | <div className="home-whale"> |
| 103 | <WhaleLive locale={locale} perform /> |
| 104 | </div> |
| 105 | </div> |
| 106 | </section> |
| 107 | |
| 108 | {/* THE SEA — the horizon, then the real terminal just under the |
| 109 | surface: live text from an exact-build PTY cell capture. No |
| 110 | fabricated conversation, connected tools or completion metrics. */} |
| 111 | <section className="home-terminal stage" aria-labelledby="home-terminal"> |
| 112 | <div className="home-terminal-inner"> |
| 113 | <h2 className="home-terminal-title" id="home-terminal">{d.chapterTerminalTitle}</h2> |
| 114 | <figure className="figure home-shot"> |
| 115 | <div className="figure-frame"> |
| 116 | <NativeTerminalGallery |
| 117 | locale={locale} |
| 118 | defaultFrame="composer" |
| 119 | regionLabel={d.shotPreview} |
| 120 | label={fill(d.screenshotAlt, { version: TERMINAL_SCREENSHOT.version })} |
| 121 | /> |
| 122 | </div> |
| 123 | <figcaption className="figure-caption"> |
| 124 | <span> |
| 125 | {d.shotPreview} · {fill(d.shotBuild, { version: TERMINAL_SCREENSHOT.version })} |
| 126 | </span> |
| 127 | {/* Each fact is its own translated unit; nothing is |
| 128 | concatenated around a token. */} |
| 129 | <span |
| 130 | className="status-line" |
| 131 | data-source-state={sourceIsPublished ? "published release" : "source candidate"} |
| 132 | data-source-state-label={sourceIsPublished ? d.publishedRelease : d.figcaptionSourceCandidate} |
| 133 | > |
| 134 | <Status tone={publishedRelease ? "ready" : "idle"}> |
| 135 | {publishedRelease |
| 136 | ? fill(d.latestRelease, { tag: publishedRelease.tag }) |
| 137 | : d.releaseUnavailable} |
| 138 | </Status> |
| 139 | <span>{`${sourceIsPublished ? d.currentSource : d.sourceCandidate} v${sourceVersion}`}</span> |
| 140 | <span>{facts.license ?? "MIT"}</span> |
| 141 | </span> |
| 142 | </figcaption> |
| 143 | </figure> |
| 144 | </div> |
| 145 | </section> |
| 146 | |
| 147 | {/* PAPER AGAIN — what it does, how it connects, how to start. */} |
| 148 | <div className="home-body"> |
| 149 | <Section id="home-gain" title={d.gainHeading} scope={d.gainLede} className="home-section"> |
| 150 | <div className="ruled-cols"> |
| 151 | {d.gain.map(([title, body], index) => ( |
| 152 | <div key={title}> |
| 153 | <span className="ruled-icon" aria-hidden="true"> |
| 154 | <Icon name={GAIN_ICONS[index] ?? "terminal"} /> |
| 155 | </span> |
| 156 | <h3>{title}</h3> |
| 157 | <p>{body}</p> |
| 158 | </div> |
| 159 | ))} |
| 160 | </div> |
| 161 | </Section> |
| 162 | |
| 163 | <Section |
| 164 | id="home-models" |
| 165 | layout="split" |
| 166 | className="home-section" |
| 167 | title={d.modelsHeading} |
| 168 | scope={d.modelsBody} |
| 169 | link={ |
| 170 | <Link href={`/${locale}/models`} className="section-link"> |
| 171 | {d.modelsLink} |
| 172 | <Icon name="arrow-right" className="icon icon-flip" /> |
| 173 | </Link> |
| 174 | } |
| 175 | > |
| 176 | <dl className="ruled-list"> |
| 177 | {d.modelsFacts.map(([kind, description]) => ( |
| 178 | <div key={kind}> |
| 179 | <dt>{kind}</dt> |
| 180 | <dd>{description}</dd> |
| 181 | </div> |
| 182 | ))} |
| 183 | <div className="home-modes"> |
| 184 | <dt>Plan · Work · Operate</dt> |
| 185 | <dd>Ask · Auto-Review · Full Access</dd> |
| 186 | </div> |
| 187 | </dl> |
| 188 | </Section> |
| 189 | |
| 190 | <Section |
| 191 | id="home-surfaces" |
| 192 | layout="split" |
| 193 | className="home-section" |
| 194 | title={d.surfacesHeading} |
| 195 | link={ |
| 196 | <Link href={`/${locale}/runtime`} className="section-link"> |
| 197 | {d.runtimeLink} |
| 198 | <Icon name="arrow-right" className="icon icon-flip" /> |
| 199 | </Link> |
| 200 | } |
| 201 | > |
| 202 | <dl className="ruled-list"> |
| 203 | {d.surfaces.map(([name, description]) => ( |
| 204 | <div key={name}> |
| 205 | <dt>{name}</dt> |
| 206 | <dd>{description}</dd> |
| 207 | </div> |
| 208 | ))} |
| 209 | </dl> |
| 210 | </Section> |
| 211 | |
| 212 | {/* The start is a real sequence, so its steps keep their numbers. */} |
| 213 | <Section |
| 214 | id="home-start" |
| 215 | className="home-section product-start" |
| 216 | title={d.startHeading} |
| 217 | scope={d.startLede} |
| 218 | > |
| 219 | <GettingStartedSteps locale={locale} /> |
| 220 | <div className="product-start-links"> |
| 221 | <Link href={`/${locale}/docs/guide`} className="section-link"> |
| 222 | {d.startGuideLink} |
| 223 | <Icon name="arrow-right" className="icon icon-flip" /> |
| 224 | </Link> |
| 225 | <Link href={`/${locale}/docs/vocabulary`} className="section-link"> |
| 226 | {d.startVocabularyLink} |
| 227 | <Icon name="arrow-right" className="icon icon-flip" /> |
| 228 | </Link> |
| 229 | </div> |
| 230 | </Section> |
| 231 | |
| 232 | {/* WHERE IT RUNS TODAY — each surface with a mark and a word. */} |
| 233 | <Section |
| 234 | id="home-availability" |
| 235 | layout="split" |
| 236 | className="home-section" |
| 237 | title={d.availabilityHeading} |
| 238 | scope={d.availabilityLede} |
| 239 | link={ |
| 240 | <a href={APP_SIGNUP_URL} className="section-link" data-usage="signup"> |
| 241 | {d.accountLink} |
| 242 | <Icon name="arrow-right" className="icon icon-flip" /> |
| 243 | </a> |
| 244 | } |
| 245 | > |
| 246 | <dl className="ruled-list"> |
| 247 | {d.availability.map(([surface, status, detail, href], index) => ( |
| 248 | <div key={surface}> |
| 249 | <dt>{href ? <a href={href} className="body-link">{surface}</a> : surface}</dt> |
| 250 | <dd> |
| 251 | <Status tone={AVAILABILITY_TONES[index] ?? "idle"}>{status}</Status> |
| 252 | {detail} |
| 253 | </dd> |
| 254 | </div> |
| 255 | ))} |
| 256 | </dl> |
| 257 | <p className="section-scope mt-4">{d.availabilityNote}</p> |
| 258 | </Section> |
| 259 | |
| 260 | {/* COMMUNITY */} |
| 261 | <section className="home-section" aria-labelledby="home-community"> |
| 262 | <div className="section-split"> |
| 263 | <div className="section-head-text"> |
| 264 | <h2 className="section-title" id="home-community">{d.communityHeading}</h2> |
| 265 | <p className="section-scope">{d.communityBody}</p> |
| 266 | </div> |
| 267 | <nav aria-label={d.communityLinksAria}> |
| 268 | <ul className="dir-list" role="list"> |
| 269 | <li> |
| 270 | <a href={REPO_URL} className="dir-row"> |
| 271 | <span className="dir-mark" aria-hidden="true"><Icon name="github" /></span> |
| 272 | <span className="dir-text"><span className="dir-title">GitHub</span></span> |
| 273 | <span className="dir-action" aria-hidden="true"><Icon name="external" /></span> |
| 274 | </a> |
| 275 | </li> |
| 276 | <li> |
| 277 | <a href={REPO_ISSUES_URL} className="dir-row"> |
| 278 | <span className="dir-mark" aria-hidden="true"><Icon name="alert" /></span> |
| 279 | <span className="dir-text"><span className="dir-title">Issues</span></span> |
| 280 | <span className="dir-action" aria-hidden="true"><Icon name="external" /></span> |
| 281 | </a> |
| 282 | </li> |
| 283 | <li> |
| 284 | <a href={DISCORD_URL} className="dir-row"> |
| 285 | <span className="dir-mark" aria-hidden="true"><Icon name="message" /></span> |
| 286 | <span className="dir-text"><span className="dir-title">Discord</span></span> |
| 287 | <span className="dir-action" aria-hidden="true"><Icon name="external" /></span> |
| 288 | </a> |
| 289 | </li> |
| 290 | <li> |
| 291 | <Link href={`/${locale}/contribute`} className="dir-row"> |
| 292 | <span className="dir-mark" aria-hidden="true"><Icon name="git-pull-request" /></span> |
| 293 | <span className="dir-text"><span className="dir-title">{d.contribute}</span></span> |
| 294 | <span className="dir-action" aria-hidden="true"><Icon name="chevron-right" className="icon icon-flip" /></span> |
| 295 | </Link> |
| 296 | </li> |
| 297 | <li> |
| 298 | {publishedRelease ? ( |
| 299 | <a href={publishedRelease.url} className="dir-row"> |
| 300 | <span className="dir-mark" aria-hidden="true"><Icon name="package" /></span> |
| 301 | <span className="dir-text"><span className="dir-title">{publishedRelease.tag}</span></span> |
| 302 | <span className="dir-action" aria-hidden="true"><Icon name="external" /></span> |
| 303 | </a> |
| 304 | ) : ( |
| 305 | <a href={REPO_RELEASES_URL} className="dir-row"> |
| 306 | <span className="dir-mark" aria-hidden="true"><Icon name="package" /></span> |
| 307 | <span className="dir-text"><span className="dir-title">Releases</span></span> |
| 308 | <span className="dir-action" aria-hidden="true"><Icon name="external" /></span> |
| 309 | </a> |
| 310 | )} |
| 311 | </li> |
| 312 | </ul> |
| 313 | </nav> |
| 314 | </div> |
| 315 | </section> |
| 316 | </div> |
| 317 | |
| 318 | {/* THE SEA AGAIN — the install command where the page ends; the water |
| 319 | runs on into the footer. */} |
| 320 | <section className="home-install stage sea-continues" aria-labelledby="home-install"> |
| 321 | <div className="home-install-inner"> |
| 322 | <div className="section-head-text"> |
| 323 | <h2 className="section-title" id="home-install">{d.installBandHeading}</h2> |
| 324 | <p className="home-install-channels"> |
| 325 | GitHub Releases ({d.binaries}) · npm · Cargo · Docker · Windows · Android / Termux · {d.chinaMirrors} |
| 326 | </p> |
| 327 | <Link href={`/${locale}/install`} className="section-link"> |
| 328 | {d.installGuideLink} |
| 329 | <Icon name="arrow-right" className="icon icon-flip" /> |
| 330 | </Link> |
| 331 | </div> |
| 332 | <InstallCodeBlock |
| 333 | cmd={GETTING_STARTED_STEPS[0].commands[0]} |
| 334 | copyLabel={d.copy} |
| 335 | copiedLabel={d.copied} |
| 336 | /> |
| 337 | </div> |
| 338 | </section> |
| 339 | </div> |
| 340 | ); |
| 341 | } |
| 342 |