返回 CodeWhale
page.tsx
根目录 / web / app / [locale] / page.tsx
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
342 lines Plain Text