返回 CodeWhale
page.tsx
根目录 / web / app / [locale] / page.tsx
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 &quot;fix the failing test&quot;</span>
227 <span>inspect&nbsp;&nbsp; {d.receiptInspect}</span>
228 <span>act&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {d.receiptAct}</span>
229 <span>verify&nbsp;&nbsp;&nbsp; cargo test --locked</span>
230 <strong>report&nbsp;&nbsp;&nbsp; {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
373 lines Plain Text