返回 CodeWhale
page.tsx
根目录 / web / app / [locale] / product / page.tsx
1 import Link from "next/link";
2 import { Icon, type IconName } from "@/components/icon";
3 import { PageHeader, Section } from "@/components/page-header";
4 import { Status, type StatusTone } from "@/components/status-badge";
5 import { NativeTerminalGallery } from "@/components/native-terminal-gallery";
6 import { getFacts } from "@/lib/facts";
7 import { PRODUCT_COPY } from "@/lib/content/product";
8 import { fill, getHome, pickText } from "@/lib/i18n/dictionaries";
9 import { buildPageMetadata } from "@/lib/page-meta";
10 import { getNativeTerminalCopy } from "@/lib/content/native-terminal";
11 import { TERMINAL_SCREENSHOT } from "@/lib/media-manifest";
12
13 export const revalidate = 300;
14
15 export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
16 const { locale } = await params;
17 return buildPageMetadata({
18 path: "/product",
19 locale,
20 title: pickText(PRODUCT_COPY.metadata.title, locale),
21 description: pickText(PRODUCT_COPY.metadata.description, locale),
22 });
23 }
24
25 // Row order is fixed by PRODUCT_COPY and the home dictionary, so marks and
26 // states follow the row, not a translated word.
27 const GAIN_ICONS: IconName[] = ["layers", "users", "shield"];
28 // Terminal released · local browser ships with it · CodeWhale GUI
29 // available · hosted web preview · desktop development build · cloud
30 // computers in development.
31 const AVAILABILITY_TONES: StatusTone[] = ["ready", "ready", "ready", "attention", "idle", "idle"];
32
33 /**
34 * /product — what Codewhale is and what a person gains, with availability
35 * stated per surface. Copy is the `PRODUCT_COPY` content module; counts come
36 * from the facts layer; the surface list is the same dictionary the homepage
37 * renders, so the two never drift.
38 */
39 export default async function ProductPage({ params }: { params: Promise<{ locale: string }> }) {
40 const { locale } = await params;
41 const t = (text: { en: string; zh: string }) => pickText(text, locale);
42 const facts = await getFacts();
43 const home = getHome(locale);
44 const terminal = getNativeTerminalCopy(locale);
45 const providerCount = facts.providers.length;
46
47 return (
48 <>
49 <PageHeader
50 title={t(PRODUCT_COPY.title)}
51 lede={t(PRODUCT_COPY.lede)}
52 pose="think"
53 actions={
54 <>
55 <Link href={`/${locale}/install`} className="btn btn-primary btn-lg">
56 {t(PRODUCT_COPY.actions.install)}
57 </Link>
58 <Link href={`/${locale}/docs`} className="btn btn-secondary btn-lg">
59 {t(PRODUCT_COPY.actions.docs)}
60 </Link>
61 </>
62 }
63 />
64
65 <div className="page-body">
66 <Section id="product-terminal" title={terminal.title} scope={terminal.description}>
67 <div className="figure-frame">
68 <NativeTerminalGallery
69 locale={locale}
70 defaultFrame="composer"
71 regionLabel={home.shotPreview}
72 label={fill(home.screenshotAlt, { version: TERMINAL_SCREENSHOT.version })}
73 />
74 </div>
75 </Section>
76
77 <Section
78 id="product-gain"
79 title={t(PRODUCT_COPY.gainHeading)}
80 link={
81 <Link href={`/${locale}/models`} className="section-link">
82 {t(PRODUCT_COPY.actions.models)}
83 <Icon name="arrow-right" className="icon icon-flip" />
84 </Link>
85 }
86 >
87 <div className="ruled-cols">
88 {PRODUCT_COPY.gain.map((row, index) => (
89 <div key={row.title.en}>
90 <span className="ruled-icon" aria-hidden="true">
91 <Icon name={GAIN_ICONS[index] ?? "layers"} />
92 </span>
93 <h3>{t(row.title)}</h3>
94 <p>{fill(t(row.body), { count: providerCount })}</p>
95 </div>
96 ))}
97 </div>
98 </Section>
99
100 <Section
101 id="product-surfaces"
102 layout="split"
103 title={t(PRODUCT_COPY.surfacesHeading)}
104 scope={t(PRODUCT_COPY.surfacesLede)}
105 link={
106 <Link href={`/${locale}/runtime`} className="section-link">
107 {t(PRODUCT_COPY.surfacesLink)}
108 <Icon name="arrow-right" className="icon icon-flip" />
109 </Link>
110 }
111 >
112 <dl className="ruled-list">
113 {home.surfaces.map(([name, description]) => (
114 <div key={name}>
115 <dt>{name}</dt>
116 <dd>{description}</dd>
117 </div>
118 ))}
119 </dl>
120 </Section>
121
122 <Section
123 id="product-control"
124 layout="split"
125 title={t(PRODUCT_COPY.controlHeading)}
126 scope={t(PRODUCT_COPY.controlLede)}
127 >
128 <div className="stack">
129 <dl className="ruled-list">
130 {PRODUCT_COPY.modes.map((row) => (
131 <div key={row.title.en}>
132 <dt>{t(row.title)}</dt>
133 <dd>{t(row.body)}</dd>
134 </div>
135 ))}
136 </dl>
137 <dl className="ruled-list">
138 {PRODUCT_COPY.permissions.map((row) => (
139 <div key={row.title.en}>
140 <dt>{t(row.title)}</dt>
141 <dd>{t(row.body)}</dd>
142 </div>
143 ))}
144 </dl>
145 </div>
146 </Section>
147
148 <Section
149 id="product-availability"
150 layout="split"
151 title={t(PRODUCT_COPY.availabilityHeading)}
152 scope={t(PRODUCT_COPY.availabilityLede)}
153 >
154 <dl className="ruled-list">
155 {PRODUCT_COPY.availability.map((row, index) => (
156 <div key={row.surface.en}>
157 <dt>{t(row.surface)}</dt>
158 <dd>
159 <Status tone={AVAILABILITY_TONES[index] ?? "idle"}>{t(row.status)}</Status>
160 {t(row.detail)}
161 {row.href && row.linkLabel && (
162 <>
163 {" "}
164 {row.href.startsWith("http") ? (
165 <a
166 href={row.href}
167 className="link"
168 target="_blank"
169 rel="noopener noreferrer"
170 >
171 {t(row.linkLabel)}
172 </a>
173 ) : (
174 <Link href={`/${locale}${row.href}`} className="link">
175 {t(row.linkLabel)}
176 </Link>
177 )}
178 </>
179 )}
180 </dd>
181 </div>
182 ))}
183 </dl>
184 </Section>
185 </div>
186 </>
187 );
188 }
189
189 lines Plain Text