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