| 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 { Strata } from "@/components/strata"; |
| 7 | import { getFacts } from "@/lib/facts"; |
| 8 | import { GETTING_STARTED_STEPS } from "@/lib/content/getting-started"; |
| 9 | import { fill, getHome, splitToken } from "@/lib/i18n/dictionaries"; |
| 10 | import { |
| 11 | APP_SIGNUP_URL, |
| 12 | DISCORD_URL, |
| 13 | REPO_ISSUES_URL, |
| 14 | REPO_RELEASES_URL, |
| 15 | REPO_URL, |
| 16 | } from "@/lib/i18n/links"; |
| 17 | import { serializeJsonLd } from "@/lib/json-ld"; |
| 18 | import { buildSoftwareApplicationJsonLd } from "@/lib/software-application-schema"; |
| 19 | import { TERMINAL_SCREENSHOT } from "@/lib/media-manifest"; |
| 20 | |
| 21 | // Revalidate against source-proven runtime facts without giving up static edge |
| 22 | // caching. `getFacts()` rejects legacy or older KV snapshots. |
| 23 | export const revalidate = 300; |
| 24 | |
| 25 | /** |
| 26 | * The Tidal Folio homepage: a sheet read under the sea. |
| 27 | * |
| 28 | * Every visible string resolves through `getHome(locale)` — English, |
| 29 | * Chinese, and every other routed locale take the identical path, with the |
| 30 | * English dictionary as the build-time-guaranteed fallback. The only literals |
| 31 | * left in this file are code-owned per docs/VOICE.md: the product control |
| 32 | * vocabulary (`Plan · Work · Operate`, `Ask · Auto-Review · Full Access`), |
| 33 | * the install command, package-manager and mirror proper nouns, the chapter |
| 34 | * numerals, and the screenshot path. |
| 35 | */ |
| 36 | export default async function HomePage({ params }: { params: Promise<{ locale: string }> }) { |
| 37 | const { locale } = await params; |
| 38 | const d = getHome(locale); |
| 39 | const facts = await getFacts(); |
| 40 | const sourceVersion = facts.version ?? "unknown"; |
| 41 | const publishedRelease = facts.latestPublishedRelease; |
| 42 | const sourceIsPublished = publishedRelease?.version === sourceVersion; |
| 43 | |
| 44 | // The install URL resolves published artifacts, so its structured version |
| 45 | // must come from the published-release receipt rather than source-candidate |
| 46 | // facts. When no release is known, the schema omits softwareVersion. |
| 47 | const jsonLd = buildSoftwareApplicationJsonLd(publishedRelease); |
| 48 | |
| 49 | // The lede typesets the brand in its own span. Splitting on the {brand} |
| 50 | // token keeps the sentence a single translated unit — no concatenation of |
| 51 | // fragments around a variable, and a locale may place the brand anywhere. |
| 52 | const ledeParts = splitToken(d.heroIntro, "brand"); |
| 53 | |
| 54 | return ( |
| 55 | <div className="product-home"> |
| 56 | <script |
| 57 | type="application/ld+json" |
| 58 | dangerouslySetInnerHTML={{ __html: serializeJsonLd(jsonLd) }} |
| 59 | /> |
| 60 | |
| 61 | {/* THE PLATE — paper at the top left, the water rising from the bottom |
| 62 | right, the real terminal floating at the waterline. */} |
| 63 | <section className="folio-hero"> |
| 64 | <Strata variant="hero" /> |
| 65 | <div className="product-container folio-hero-grid"> |
| 66 | <div className="folio-hero-copy"> |
| 67 | <h1>{d.heroTitle}</h1> |
| 68 | <p className="folio-lede"> |
| 69 | {ledeParts.map((part, index) => ( |
| 70 | <Fragment key={index}> |
| 71 | {index > 0 && <span className="font-semibold text-ink">Codewhale</span>} |
| 72 | {part} |
| 73 | </Fragment> |
| 74 | ))} |
| 75 | </p> |
| 76 | <div className="folio-actions"> |
| 77 | <Link href={`/${locale}/install`} className="folio-button folio-button-primary"> |
| 78 | {d.getCodewhale} |
| 79 | </Link> |
| 80 | <Link href={`/${locale}/product`} className="folio-button"> |
| 81 | {d.exploreProduct} |
| 82 | </Link> |
| 83 | </div> |
| 84 | </div> |
| 85 | |
| 86 | {/* Exact-build PTY capture of an empty session. No fabricated |
| 87 | conversation, connected tools or completion metrics. */} |
| 88 | <figure className="folio-shot"> |
| 89 | <Image |
| 90 | src={TERMINAL_SCREENSHOT.src} |
| 91 | alt={fill(d.screenshotAlt, { version: TERMINAL_SCREENSHOT.version })} |
| 92 | width={TERMINAL_SCREENSHOT.width} |
| 93 | height={TERMINAL_SCREENSHOT.height} |
| 94 | sizes="(max-width: 58rem) calc(100vw - 2rem), 56rem" |
| 95 | unoptimized |
| 96 | priority |
| 97 | /> |
| 98 | <figcaption> |
| 99 | <p className="dotline"> |
| 100 | <span>{d.shotPreview}</span> |
| 101 | <span>{fill(d.shotBuild, { version: TERMINAL_SCREENSHOT.version })}</span> |
| 102 | </p> |
| 103 | {/* |
| 104 | The TUI header grammar: a `cw` chip and a dot chain. Each fact is |
| 105 | its own translated unit — the separators are CSS punctuation, so |
| 106 | nothing is concatenated around a token and no locale inherits an |
| 107 | English joining word. |
| 108 | */} |
| 109 | <p |
| 110 | className="paper-facts dotline" |
| 111 | data-source-state={sourceIsPublished ? "published release" : "source candidate"} |
| 112 | data-source-state-label={sourceIsPublished ? d.publishedRelease : d.figcaptionSourceCandidate} |
| 113 | > |
| 114 | <span className="dotline-chip">cw</span> |
| 115 | <span> |
| 116 | {publishedRelease |
| 117 | ? fill(d.latestRelease, { tag: publishedRelease.tag }) |
| 118 | : d.releaseUnavailable} |
| 119 | </span> |
| 120 | <span> |
| 121 | {`${sourceIsPublished ? d.currentSource : d.sourceCandidate} v${sourceVersion}`} |
| 122 | </span> |
| 123 | <span>{facts.license ?? "MIT"}</span> |
| 124 | </p> |
| 125 | </figcaption> |
| 126 | </figure> |
| 127 | |
| 128 | <aside className="folio-chapter"> |
| 129 | <span className="folio-chapter-num">01 / {d.chapterTerminal}</span> |
| 130 | <p className="folio-chapter-title">{d.chapterTerminalTitle}</p> |
| 131 | </aside> |
| 132 | </div> |
| 133 | </section> |
| 134 | |
| 135 | {/* WHAT YOU GAIN — three ruled columns on paper. */} |
| 136 | <section className="folio-section"> |
| 137 | <div className="product-container"> |
| 138 | <h2>{d.gainHeading}</h2> |
| 139 | <p className="folio-section-lede">{d.gainLede}</p> |
| 140 | <div className="folio-gain-grid"> |
| 141 | {d.gain.map(([title, body]) => ( |
| 142 | <div key={title}> |
| 143 | <h3>{title}</h3> |
| 144 | <p>{body}</p> |
| 145 | </div> |
| 146 | ))} |
| 147 | </div> |
| 148 | </div> |
| 149 | </section> |
| 150 | |
| 151 | {/* Tools and saved work, before setup details. */} |
| 152 | <section className="folio-section"> |
| 153 | <div className="product-container"> |
| 154 | <h2>{d.surfacesHeading}</h2> |
| 155 | <dl className="folio-fact-list mt-8"> |
| 156 | {d.surfaces.map(([name, description]) => ( |
| 157 | <div key={name}> |
| 158 | <dt>{name}</dt> |
| 159 | <dd>{description}</dd> |
| 160 | </div> |
| 161 | ))} |
| 162 | </dl> |
| 163 | <Link href={`/${locale}/runtime`} className="folio-link"> |
| 164 | {d.runtimeLink} |
| 165 | </Link> |
| 166 | </div> |
| 167 | </section> |
| 168 | |
| 169 | {/* 02 — YOUR MODELS */} |
| 170 | <section className="folio-section"> |
| 171 | <div className="product-container folio-chapter-grid"> |
| 172 | <div> |
| 173 | <span className="folio-running-head">02 / {d.chapterModels}</span> |
| 174 | <h2>{d.modelsHeading}</h2> |
| 175 | <p className="folio-section-lede">{d.modelsBody}</p> |
| 176 | <Link href={`/${locale}/models`} className="folio-link"> |
| 177 | {d.modelsLink} |
| 178 | </Link> |
| 179 | </div> |
| 180 | <dl className="folio-fact-list"> |
| 181 | {d.modelsFacts.map(([kind, description]) => ( |
| 182 | <div key={kind}> |
| 183 | <dt>{kind}</dt> |
| 184 | <dd>{description}</dd> |
| 185 | </div> |
| 186 | ))} |
| 187 | <div> |
| 188 | <dt>Plan · Work · Operate</dt> |
| 189 | <dd>Ask · Auto-Review · Full Access</dd> |
| 190 | </div> |
| 191 | </dl> |
| 192 | </div> |
| 193 | </section> |
| 194 | |
| 195 | {/* 03 — START */} |
| 196 | <section className="product-start"> |
| 197 | <div className="product-container"> |
| 198 | <span className="folio-running-head">03 / {d.startHeading}</span> |
| 199 | <h2>{d.startHeading}</h2> |
| 200 | <p className="product-start-lede">{d.startLede}</p> |
| 201 | <GettingStartedSteps locale={locale} /> |
| 202 | <div className="product-start-links"> |
| 203 | <Link href={`/${locale}/docs/guide`}>{d.startGuideLink}</Link> |
| 204 | <Link href={`/${locale}/docs/vocabulary`}>{d.startVocabularyLink}</Link> |
| 205 | </div> |
| 206 | </div> |
| 207 | </section> |
| 208 | |
| 209 | {/* THE WATERLINE. Everything below is one water column: the same |
| 210 | component grammar, re-inked with the dark whale tokens by the |
| 211 | shared below-the-waterline rule in globals.css. */} |
| 212 | <div className="folio-waterline" aria-hidden="true"> |
| 213 | <Strata variant="band" /> |
| 214 | </div> |
| 215 | <div className="ocean-column"> |
| 216 | {/* 04 — WHERE IT RUNS TODAY */} |
| 217 | <section className="folio-availability"> |
| 218 | <div className="product-container"> |
| 219 | <span className="folio-running-head">04 / {d.chapterAccount}</span> |
| 220 | <h2>{d.availabilityHeading}</h2> |
| 221 | <p className="folio-section-lede">{d.availabilityLede}</p> |
| 222 | <dl className="folio-availability-list"> |
| 223 | {d.availability.map(([surface, status, detail]) => ( |
| 224 | <div key={surface}> |
| 225 | <dt>{surface}</dt> |
| 226 | <dd> |
| 227 | <strong>{status}</strong> |
| 228 | {detail} |
| 229 | </dd> |
| 230 | </div> |
| 231 | ))} |
| 232 | </dl> |
| 233 | <p className="folio-availability-note">{d.availabilityNote}</p> |
| 234 | <a href={APP_SIGNUP_URL} className="folio-link" data-usage="signup"> |
| 235 | {d.accountLink} |
| 236 | </a> |
| 237 | </div> |
| 238 | </section> |
| 239 | |
| 240 | {/* Install band — the composer plate. */} |
| 241 | <section className="product-install-band"> |
| 242 | <div className="product-container product-install-grid"> |
| 243 | <h2>{d.installBandHeading}</h2> |
| 244 | <div> |
| 245 | {/* `❯` is a code-owned literal, like the install command it |
| 246 | prompts for — it is the product's glyph, not a sentence. */} |
| 247 | <div className="product-composer"> |
| 248 | <span className="product-composer-prompt" aria-hidden> |
| 249 | ❯ |
| 250 | </span> |
| 251 | <InstallCodeBlock |
| 252 | cmd={GETTING_STARTED_STEPS[0].commands[0]} |
| 253 | copyLabel={d.copy} |
| 254 | copiedLabel={d.copied} |
| 255 | /> |
| 256 | </div> |
| 257 | <p className="dotline"> |
| 258 | <span>GitHub Releases · {d.binaries}</span> |
| 259 | <span>npm</span> |
| 260 | <span>Cargo</span> |
| 261 | <span>Docker</span> |
| 262 | <span>Nix</span> |
| 263 | <span>Windows</span> |
| 264 | <span>Android / Termux</span> |
| 265 | <span>{d.chinaMirrors}</span> |
| 266 | </p> |
| 267 | <Link href={`/${locale}/install`}>{d.installGuideLink}</Link> |
| 268 | </div> |
| 269 | </div> |
| 270 | </section> |
| 271 | |
| 272 | {/* Community */} |
| 273 | <section className="product-community"> |
| 274 | <div className="product-container product-community-grid"> |
| 275 | <div> |
| 276 | <h2>{d.communityHeading}</h2> |
| 277 | <p>{d.communityBody}</p> |
| 278 | </div> |
| 279 | <nav aria-label={d.communityLinksAria}> |
| 280 | <a href={REPO_URL}>GitHub</a> |
| 281 | <a href={REPO_ISSUES_URL}>Issues</a> |
| 282 | <a href={DISCORD_URL}>Discord</a> |
| 283 | <Link href={`/${locale}/contribute`}>{d.contribute}</Link> |
| 284 | {publishedRelease ? ( |
| 285 | <a href={publishedRelease.url}>{publishedRelease.tag}</a> |
| 286 | ) : ( |
| 287 | <a href={REPO_RELEASES_URL}>Releases</a> |
| 288 | )} |
| 289 | </nav> |
| 290 | </div> |
| 291 | </section> |
| 292 | </div> |
| 293 | </div> |
| 294 | ); |
| 295 | } |
| 296 |