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:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -108,5 +108,19 @@
|
||||
"loadFailed": "Failed to load",
|
||||
"retry": "Retry",
|
||||
"hotKeyword": "“{word}” 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
@@ -106,7 +106,20 @@
|
||||
"subtitle": "从 “大型语言模型” 到 “Agent 工作流”。探索 AI 话语的演变历程。",
|
||||
"loading": "加载中...",
|
||||
"loadFailed": "加载失败",
|
||||
"retry": "重试",
|
||||
"hotKeyword": "“{word}” 是本季度最热门词汇!"
|
||||
"retry": "重试"
|
||||
},
|
||||
"timeline": {
|
||||
"metaTitle": "AI 发展时间轴 - Agent Park",
|
||||
"metaDescription": "探索人工智能大语言模型的发展历程,从 2017 年 Transformer 到今天",
|
||||
"title": "AI 的故事",
|
||||
"subtitle": "钉住。堆叠。之字形。",
|
||||
"emptyData": "暂无数据",
|
||||
"collecting": "时间轴数据正在收集中...",
|
||||
"joinThePark": "加入 Agent Park",
|
||||
"subscribeDesc": "订阅 Agent Park 周刊。没有垃圾邮件,只有干货。",
|
||||
"emailPlaceholder": "您的电子邮箱...",
|
||||
"subscribe": "订阅",
|
||||
"agreeToBeCool": "我同意保持礼貌。",
|
||||
"earlier": "更早"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user