返回 CodeWhale
page.tsx
根目录 / web / app / [locale] / digest / page.tsx
1 // Bypassing the '@' alias to force TypeScript to find the file
2 import { EmptyState } from "../../../components/surface-state";
3 import { getEnv } from "../../../lib/kv";
4 import { buildPageMetadata } from "../../../lib/page-meta";
5
6 // Define the exact structure of the Digest data to fix all the 'any' type errors
7 interface DigestSection {
8 heading: string;
9 items: string[];
10 }
11
12 interface WeeklyDigest {
13 weekId: string;
14 titleEn: string;
15 titleZh: string;
16 summaryEn: string;
17 summaryZh: string;
18 sections: DigestSection[];
19 generatedAt: string;
20 }
21
22 export const revalidate = 3600; // Cache page updates hourly
23
24 export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
25 const { locale } = await params;
26 const isZh = locale === "zh";
27 return buildPageMetadata({
28 path: "/digest",
29 locale,
30 title: isZh ? "社区摘要 · Codewhale" : "Community Digest · Codewhale",
31 description: isZh
32 ? "Codewhale 每周社区更新存档:由维护者审核的摘要。"
33 : "Archive of weekly Codewhale community updates — maintainer-approved summaries.",
34 });
35 }
36
37 export default async function DigestArchivePage({ params }: { params: Promise<{ locale: string }> }) {
38 const { locale } = await params;
39 const isZh = locale === "zh";
40
41 // 1. Get the correct project environment bindings
42 const env = await getEnv();
43 const kv = env.CURATED_KV;
44
45 // Fallback array if no data is found or if KV isn't active in dev
46 let digests: WeeklyDigest[] = [];
47
48 if (kv) {
49 try {
50 // Fetch all weekly digest keys generated by the agent tasks
51 const { keys } = await kv.list({ prefix: "digest:weekly-" });
52
53 if (keys && keys.length > 0) {
54 const digestsRaw = await Promise.all(
55 keys.map(async (k: { name: string }) => {
56 const data = await kv.get(k.name);
57 return data;
58 })
59 );
60
61 // Parse each entry independently so a single malformed record can't
62 // blank the entire archive — skip only the bad ones.
63 digests = digestsRaw
64 .filter((item: string | null): item is string => Boolean(item))
65 .flatMap((item: string) => {
66 try {
67 return [JSON.parse(item) as WeeklyDigest];
68 } catch (e) {
69 console.error("Skipping malformed digest entry:", e);
70 return [];
71 }
72 })
73 .sort((a: WeeklyDigest, b: WeeklyDigest) =>
74 new Date(b.generatedAt).getTime() - new Date(a.generatedAt).getTime()
75 );
76 }
77 } catch (e) {
78 console.error("Error reading from KV:", e);
79 }
80 }
81
82 // Handle empty state gracefully
83 if (digests.length === 0) {
84 return (
85 <div className="route-state">
86 <h1 className="font-display text-3xl mb-6 tracking-crisp text-ink">
87 {isZh ? "社区摘要" : "Community Digest"}
88 </h1>
89 <EmptyState
90 locale={locale}
91 body={
92 isZh
93 ? "还没有经维护者审核的每周摘要。摘要在 cron 生成并审核后才会出现在这里。"
94 : "No maintainer-approved weekly digest exists yet. One appears here only after the cron draft has been reviewed."
95 }
96 />
97 </div>
98 );
99 }
100
101 return (
102 <div className="max-w-4xl mx-auto py-12 px-6">
103 <h1 className="font-display text-4xl mb-2 tracking-crisp text-ink">
104 {isZh ? "每周社区更新" : "Weekly Community Updates"}
105 </h1>
106 <p className="text-ink-mute mb-8">
107 {isZh ? "由 Codewhale 维护者审核的摘要" : "Maintainer-approved summaries from Codewhale"}
108 </p>
109
110 <div className="space-y-12">
111 {digests.map((digest: WeeklyDigest) => (
112 <article key={digest.weekId} className="border hairline rounded-lg p-6 bg-paper-card">
113 <header className="mb-6 hairline-b pb-4">
114 <span className="pill">
115 {digest.weekId}
116 </span>
117 <h2 className="font-display text-2xl mt-3 text-ink">{digest.titleEn}</h2>
118 <h3 className="font-cjk text-lg text-ink-soft mt-1">{digest.titleZh}</h3>
119 </header>
120
121 <div className="mb-6 space-y-4">
122 <p className="text-ink-soft leading-relaxed">{digest.summaryEn}</p>
123 <p className="text-ink-mute leading-relaxed italic">{digest.summaryZh}</p>
124 </div>
125
126 <div className="space-y-6">
127 {digest.sections.map((section: DigestSection, idx: number) => (
128 <section key={idx} className="border-l-2 border-paper-edge pl-4">
129 <h4 className="font-display text-lg mb-2 text-ink">{section.heading}</h4>
130 <ul className="list-disc list-inside space-y-1 text-ink-soft font-mono text-sm">
131 {section.items.map((item: string, itemIdx: number) => (
132 <li key={itemIdx}>{item}</li>
133 ))}
134 </ul>
135 </section>
136 ))}
137 </div>
138 </article>
139 ))}
140 </div>
141 </div>
142 );
143 }
144
144 lines Plain Text