feat: add Timeline i18n support and deterministic EventCard rotation

- Add complete Chinese/English translations for timeline page
- Use event ID-based rotation instead of Math.random() for SSR consistency
- Enhance tape decoration styling with blur and transparency
- Fix locale-aware date formatting
This commit is contained in:
2026-02-01 15:40:01 +08:00
parent e6ff7470a1
commit 47a278ca1b
4 changed files with 70 additions and 25 deletions
+25 -17
View File
@@ -1,5 +1,6 @@
import { getAIEvents } from '@/hooks/useAIEvents';
import { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
export const revalidate = 3600;
@@ -9,16 +10,23 @@ export async function generateMetadata({
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations('timeline');
return {
title: locale === 'zh' ? 'AI 发展时间轴' : 'AI Timeline',
description: locale === 'zh'
? '探索人工智能大语言模型的发展历程'
: 'Explore the evolution of AI large language models',
title: t('metaTitle'),
description: t('metaDescription'),
};
}
export default async function TimelinePage() {
export default async function TimelinePage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const t = await getTranslations('timeline');
const tCommon = await getTranslations('common');
const events = await getAIEvents();
// 按年份分组
@@ -41,10 +49,10 @@ export default async function TimelinePage() {
<div className="min-h-screen bg-background-light dark:bg-background-dark flex items-center justify-center">
<div className="text-center">
<h1 className="font-display font-black text-4xl mb-4">
{t('emptyData')}
</h1>
<p className="font-mono text-gray-600">
...
{t('collecting')}
</p>
</div>
</div>
@@ -75,11 +83,11 @@ export default async function TimelinePage() {
/>
</svg>
<h1 className="font-display font-black text-6xl md:text-8xl tracking-tight leading-none text-black dark:text-white drop-shadow-sm">
THE STORY <br /> OF A.I.
{t('title')}
</h1>
</div>
<p className="mt-6 text-lg md:text-xl font-mono max-w-2xl mx-auto bg-white dark:bg-black border border-black dark:border-white p-2 rotate-1 inline-block shadow-[4px_4px_0px_0px_#000] dark:shadow-[4px_4px_0px_0px_#fff]">
Pinned. Stacked. Zigzagged.
{t('subtitle')}
</p>
</header>
@@ -132,7 +140,7 @@ export default async function TimelinePage() {
} pt-20`}
>
<div className="flex flex-nowrap overflow-x-visible items-center justify-start py-10">
{eventsByYear[year].map((event, eventIndex) => (
{eventsByYear[year]?.map((event, eventIndex) => (
<div
key={event.id}
className="stack-card relative w-72 h-96 flex-shrink-0 bg-surface-light dark:bg-surface-dark border-4 border-black dark:border-white p-4 shadow-hard -mr-48 md:-mr-56"
@@ -163,7 +171,7 @@ export default async function TimelinePage() {
{/* Date */}
<div className="absolute bottom-4 left-4 text-[10px] font-bold bg-black text-white px-2">
{new Date(event.eventDate).toLocaleDateString('zh-CN')}
{new Date(event.eventDate).toLocaleDateString(locale === 'zh' ? 'zh-CN' : 'en-US')}
</div>
{/* Source Link */}
@@ -174,7 +182,7 @@ export default async function TimelinePage() {
rel="noopener noreferrer"
className="absolute bottom-4 right-4 text-[10px] font-bold underline"
>
{locale === 'zh' ? '来源 →' : 'Source →'}
</a>
)}
</div>
@@ -195,22 +203,22 @@ export default async function TimelinePage() {
</span>
</div>
<h2 className="font-display font-black text-3xl md:text-5xl mb-4 text-black uppercase tracking-tight">
Join the Park
{t('joinThePark')}
</h2>
<p className="font-mono text-black mb-8 text-base font-bold">
Subscribe to the Agent Park weekly zine. No spam, just ducks and data.
{t('subscribeDesc')}
</p>
<form className="flex flex-col md:flex-row gap-4">
<input
className="flex-1 bg-white border-4 border-black px-6 py-4 font-mono focus:ring-0 focus:border-black focus:shadow-[4px_4px_0px_0px_#000] transition-all placeholder:text-gray-500 text-black text-lg"
placeholder="Your email here..."
placeholder={t('emailPlaceholder')}
type="email"
/>
<button
className="bg-black text-white px-10 py-4 font-black border-4 border-transparent hover:bg-white hover:text-black hover:border-black transition-all hover:shadow-[6px_6px_0px_0px_#000] uppercase text-lg"
type="button"
>
SUBSCRIBE
{t('subscribe')}
</button>
</form>
<div className="mt-6 flex items-center gap-3">
@@ -220,7 +228,7 @@ export default async function TimelinePage() {
type="checkbox"
/>
<label className="text-sm font-black text-black uppercase" htmlFor="check">
I agree to be cool.
{t('agreeToBeCool')}
</label>
</div>
</div>
+16 -6
View File
@@ -3,22 +3,32 @@ import { AIEvent } from '@prisma/client';
interface EventCardProps {
event: AIEvent;
index: number;
baseIndex?: number;
}
export function EventCard({ event, index }: EventCardProps) {
const rotation = (Math.random() - 0.5) * 6;
const zIndex = Math.max(1, 50 - index * 10);
export function EventCard({ event, index, baseIndex = 30 }: EventCardProps) {
// Generate consistent rotation based on event ID
const rotation = ((parseInt(event.id.slice(-4), 36) % 14) - 7); // -7 to 7 degrees
const zIndex = Math.max(1, baseIndex - index * 10);
return (
<div
className="stack-card relative w-72 h-96 flex-shrink-0 bg-surface-light dark:bg-surface-dark border-4 border-black dark:border-white p-4 shadow-hard -mr-48 md:-mr-56 hover:z-50 hover:-translate-y-5 hover:scale-105 transition-all duration-300"
className="stack-card relative w-72 h-96 flex-shrink-0 bg-surface-light dark:bg-surface-dark border-4 border-black dark:border-white p-4 shadow-hard -mr-48 md:-mr-56"
style={{
zIndex,
transform: `rotate(${rotation}deg)`,
}}
>
{/* Tape decoration */}
<div className="tape absolute -top-3 left-1/2 -translate-x-1/2 w-20 h-6" />
{/* Tape decoration with transparency and blur */}
<div
className="tape absolute -top-3 left-1/2 -translate-x-1/2 w-20 h-6 rotate-1"
style={{
backgroundColor: 'rgba(255, 255, 255, 0.4)',
boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
backdropFilter: 'blur(2px)',
border: '1px solid rgba(255,255,255,0.6)',
}}
/>
{/* Image */}
<div className="h-40 bg-primary border-2 border-black dark:border-white mb-4 flex items-center justify-center overflow-hidden">
+14
View File
@@ -108,5 +108,19 @@
"loadFailed": "Failed to load",
"retry": "Retry",
"hotKeyword": "&ldquo;{word}&rdquo; is the hottest keyword this quarter!"
},
"timeline": {
"metaTitle": "AI Timeline - Agent Park",
"metaDescription": "Explore the evolution of AI large language models from 2017 Transformer to today",
"title": "THE STORY OF A.I.",
"subtitle": "Pinned. Stacked. Zigzagged.",
"emptyData": "No data available",
"collecting": "Timeline data is being collected...",
"joinThePark": "Join the Park",
"subscribeDesc": "Subscribe to the Agent Park weekly zine. No spam, just ducks and data.",
"emailPlaceholder": "Your email here...",
"subscribe": "SUBSCRIBE",
"agreeToBeCool": "I agree to be cool.",
"earlier": "Earlier"
}
}
+15 -2
View File
@@ -106,7 +106,20 @@
"subtitle": "从 &ldquo;大型语言模型&rdquo; 到 &ldquo;Agent 工作流&rdquo;。探索 AI 话语的演变历程。",
"loading": "加载中...",
"loadFailed": "加载失败",
"retry": "重试",
"hotKeyword": "&ldquo;{word}&rdquo; 是本季度最热门词汇!"
"retry": "重试"
},
"timeline": {
"metaTitle": "AI 发展时间轴 - Agent Park",
"metaDescription": "探索人工智能大语言模型的发展历程,从 2017 年 Transformer 到今天",
"title": "AI 的故事",
"subtitle": "钉住。堆叠。之字形。",
"emptyData": "暂无数据",
"collecting": "时间轴数据正在收集中...",
"joinThePark": "加入 Agent Park",
"subscribeDesc": "订阅 Agent Park 周刊。没有垃圾邮件,只有干货。",
"emailPlaceholder": "您的电子邮箱...",
"subscribe": "订阅",
"agreeToBeCool": "我同意保持礼貌。",
"earlier": "更早"
}
}