返回 CodeWhale
page.tsx
根目录 / web / app / [locale] / product / page.tsx
1 import Link from "next/link";
2 import { getFacts } from "@/lib/facts";
3 import { PRODUCT_COPY } from "@/lib/content/product";
4 import { fill, getHome, pickText } from "@/lib/i18n/dictionaries";
5 import { buildPageMetadata } from "@/lib/page-meta";
6
7 export const revalidate = 300;
8
9 export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
10 const { locale } = await params;
11 return buildPageMetadata({
12 path: "/product",
13 locale,
14 title: pickText(PRODUCT_COPY.metadata.title, locale),
15 description: pickText(PRODUCT_COPY.metadata.description, locale),
16 });
17 }
18
19 /**
20 * /product — what Codewhale is and what a person gains, with availability
21 * stated per surface. Copy is the `PRODUCT_COPY` content module; counts come
22 * from the facts layer; the surface list is the same dictionary the homepage
23 * renders, so the two never drift.
24 */
25 export default async function ProductPage({ params }: { params: Promise<{ locale: string }> }) {
26 const { locale } = await params;
27 const t = (text: { en: string; zh: string }) => pickText(text, locale);
28 const facts = await getFacts();
29 const home = getHome(locale);
30 const providerCount = facts.providers.length;
31
32 return (
33 <div className="portal-home product-page">
34 <section className="hero">
35 <div className="portal-container community-welcome-inner">
36 <h1>{t(PRODUCT_COPY.title)}</h1>
37 <p>{t(PRODUCT_COPY.lede)}</p>
38 <div className="portal-actions">
39 <Link href={`/${locale}/install`} className="portal-button portal-button-primary">
40 {t(PRODUCT_COPY.actions.install)}
41 </Link>
42 <Link href={`/${locale}/docs`} className="portal-button portal-button-secondary">
43 {t(PRODUCT_COPY.actions.docs)}
44 </Link>
45 </div>
46 </div>
47 </section>
48
49 <section className="folio-section">
50 <div className="product-container">
51 <h2>{t(PRODUCT_COPY.gainHeading)}</h2>
52 <div className="folio-gain-grid">
53 {PRODUCT_COPY.gain.map((row) => (
54 <div key={row.title.en}>
55 <h3>{t(row.title)}</h3>
56 <p>{fill(t(row.body), { count: providerCount })}</p>
57 </div>
58 ))}
59 </div>
60 <Link href={`/${locale}/models`} className="folio-link">
61 {t(PRODUCT_COPY.actions.models)}
62 </Link>
63 </div>
64 </section>
65
66 <section className="folio-section product-page-surfaces">
67 <div className="product-container folio-chapter-grid">
68 <div>
69 <h2>{t(PRODUCT_COPY.surfacesHeading)}</h2>
70 <p className="folio-section-lede">{t(PRODUCT_COPY.surfacesLede)}</p>
71 <Link href={`/${locale}/runtime`} className="folio-link">
72 {t(PRODUCT_COPY.surfacesLink)}
73 </Link>
74 </div>
75 <dl className="folio-fact-list">
76 {home.surfaces.map(([name, description]) => (
77 <div key={name}>
78 <dt>{name}</dt>
79 <dd>{description}</dd>
80 </div>
81 ))}
82 </dl>
83 </div>
84 </section>
85
86 <section className="folio-section">
87 <div className="product-container folio-chapter-grid">
88 <div>
89 <h2>{t(PRODUCT_COPY.controlHeading)}</h2>
90 <p className="folio-section-lede">{t(PRODUCT_COPY.controlLede)}</p>
91 </div>
92 <div>
93 <dl className="folio-fact-list">
94 {PRODUCT_COPY.modes.map((row) => (
95 <div key={row.title.en}>
96 <dt>{t(row.title)}</dt>
97 <dd>{t(row.body)}</dd>
98 </div>
99 ))}
100 </dl>
101 <dl className="folio-fact-list mt-8">
102 {PRODUCT_COPY.permissions.map((row) => (
103 <div key={row.title.en}>
104 <dt>{t(row.title)}</dt>
105 <dd>{t(row.body)}</dd>
106 </div>
107 ))}
108 </dl>
109 </div>
110 </div>
111 </section>
112
113 <section className="folio-section">
114 <div className="product-container">
115 <h2>{t(PRODUCT_COPY.availabilityHeading)}</h2>
116 <p className="folio-section-lede">{t(PRODUCT_COPY.availabilityLede)}</p>
117 <dl className="folio-availability-list product-availability-paper">
118 {PRODUCT_COPY.availability.map((row) => (
119 <div key={row.surface.en}>
120 <dt>{t(row.surface)}</dt>
121 <dd>
122 <strong>{t(row.status)}</strong>
123 {t(row.detail)}
124 {row.href && row.linkLabel && (
125 <>
126 {" "}
127 <Link href={`/${locale}${row.href}`} className="body-link">
128 {t(row.linkLabel)}
129 </Link>
130 </>
131 )}
132 </dd>
133 </div>
134 ))}
135 </dl>
136 </div>
137 </section>
138 </div>
139 );
140 }
141
141 lines Plain Text