refactor: extract EventCard and TimelineSection components

This commit is contained in:
2026-01-27 21:41:15 +08:00
parent 1e99c9a4e6
commit a54396fb74
3 changed files with 110 additions and 91 deletions
+9 -90
View File
@@ -1,7 +1,8 @@
import { getAIEvents } from '@/hooks/useAIEvents';
import { TimelineSection } from '@/components/timeline/TimelineSection';
import { Metadata } from 'next';
export const revalidate = 3600; // ISR 1小时
export const revalidate = 3600;
export async function generateMetadata({
params,
@@ -13,25 +14,21 @@ export async function generateMetadata({
return {
title: locale === 'zh' ? 'AI 发展时间轴' : 'AI Timeline',
description: locale === 'zh'
? '探索人工智能大语言模型的发展历程,从 2017 年 Transformer 到今天'
: 'Explore the evolution of AI large language models from 2017 Transformer to today',
? '探索人工智能大语言模型的发展历程'
: 'Explore the evolution of AI large language models',
};
}
export default async function TimelinePage() {
const events = await getAIEvents();
// 按年份分组
const eventsByYear = events.reduce((acc, event) => {
const year = new Date(event.eventDate).getFullYear();
if (!acc[year]) {
acc[year] = [];
}
if (!acc[year]) acc[year] = [];
acc[year].push(event);
return acc;
}, {} as Record<number, typeof events>);
// 按年份降序排序
const sortedYears = Object.keys(eventsByYear)
.map(Number)
.sort((a, b) => b - a);
@@ -53,7 +50,6 @@ export default async function TimelinePage() {
return (
<div className="min-h-screen bg-background-light dark:bg-background-dark">
{/* Header */}
<header className="pt-32 pb-16 px-4 max-w-[1600px] mx-auto">
<div className="text-center">
<h1 className="font-display font-black text-6xl md:text-8xl tracking-tight leading-none text-black dark:text-white mb-6">
@@ -65,91 +61,14 @@ export default async function TimelinePage() {
</div>
</header>
{/* Timeline */}
<main className="px-4 md:px-12 max-w-[1600px] mx-auto pb-32">
{sortedYears.map((year, index) => (
<section
<TimelineSection
key={year}
className={`relative min-h-[500px] mb-32 flex ${
index % 2 === 0 ? 'flex-row' : 'flex-row-reverse'
}`}
>
{/* Year Label */}
<div
className={`absolute ${index % 2 === 0 ? 'left-0' : 'right-0'} -top-8 z-20`}
>
<div
className={`${
index % 2 === 0 ? 'bg-primary' : 'bg-secondary'
} border-4 border-black px-6 py-2 ${
index % 2 === 0 ? '-rotate-2' : 'rotate-2'
} shadow-hard`}
>
<span className="font-display font-black text-3xl md:text-5xl">
{year}
</span>
</div>
</div>
{/* Events Container */}
<div
className={`w-full ${
index % 2 === 0 ? 'pl-0 md:pl-8 pr-0 md:pr-32' : 'pl-0 md:pl-32 pr-0 md:pr-8'
} pt-20`}
>
<div className="flex flex-nowrap overflow-x-visible items-center justify-start">
{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 hover:z-50 hover:-translate-y-5 hover:scale-105 transition-all duration-300"
style={{
zIndex: Math.max(1, 50 - eventIndex * 10),
transform: `rotate(${(Math.random() - 0.5) * 6}deg)`,
}}
>
{/* Tape decoration */}
<div
className="tape absolute -top-3 left-1/2 -translate-x-1/2 w-20 h-6"
year={year}
events={eventsByYear[year]}
index={index}
/>
{/* Image */}
<div className="h-40 bg-primary border-2 border-black dark:border-white mb-4 flex items-center justify-center overflow-hidden">
<img
src={event.imageUrl}
alt={event.title}
className="w-full h-full object-cover"
/>
</div>
{/* Content */}
<h3 className="font-display font-bold text-xl leading-none mb-2 uppercase">
{event.title}
</h3>
<p className="text-xs leading-snug opacity-80 line-clamp-4 mb-4">
{event.description}
</p>
{/* 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')}
</div>
{/* Source Link */}
{event.sourceUrl && (
<a
href={event.sourceUrl}
target="_blank"
rel="noopener noreferrer"
className="absolute bottom-4 right-4 text-[10px] font-bold underline"
>
</a>
)}
</div>
))}
</div>
</div>
</section>
))}
</main>
</div>
+58
View File
@@ -0,0 +1,58 @@
import { AIEvent } from '@prisma/client';
interface EventCardProps {
event: AIEvent;
index: number;
}
export function EventCard({ event, index }: EventCardProps) {
const rotation = (Math.random() - 0.5) * 6;
const zIndex = Math.max(1, 50 - 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"
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" />
{/* Image */}
<div className="h-40 bg-primary border-2 border-black dark:border-white mb-4 flex items-center justify-center overflow-hidden">
<img
src={event.imageUrl}
alt={event.title}
className="w-full h-full object-cover"
/>
</div>
{/* Content */}
<h3 className="font-display font-bold text-xl leading-none mb-2 uppercase">
{event.title}
</h3>
<p className="text-xs leading-snug opacity-80 line-clamp-4 mb-4">
{event.description}
</p>
{/* 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')}
</div>
{/* Source Link */}
{event.sourceUrl && (
<a
href={event.sourceUrl}
target="_blank"
rel="noopener noreferrer"
className="absolute bottom-4 right-4 text-[10px] font-bold underline"
>
</a>
)}
</div>
);
}
@@ -0,0 +1,42 @@
import { AIEvent } from '@prisma/client';
import { EventCard } from './EventCard';
interface TimelineSectionProps {
year: number;
events: AIEvent[];
index: number;
}
export function TimelineSection({ year, events, index }: TimelineSectionProps) {
const isEven = index % 2 === 0;
return (
<section className={`relative min-h-[500px] mb-32 flex ${isEven ? 'flex-row' : 'flex-row-reverse'}`}>
{/* Year Label */}
<div className={`absolute ${isEven ? 'left-0' : 'right-0'} -top-8 z-20`}>
<div
className={`${isEven ? 'bg-primary' : 'bg-secondary'} border-4 border-black px-6 py-2 ${isEven ? '-rotate-2' : 'rotate-2'} shadow-hard`}
>
<span className="font-display font-black text-3xl md:text-5xl">
{year}
</span>
</div>
</div>
{/* Events Container */}
<div
className={`w-full ${isEven ? 'pl-0 md:pl-8 pr-0 md:pr-32' : 'pl-0 md:pl-32 pr-0 md:pr-8'} pt-20`}
>
<div className="flex flex-nowrap overflow-x-visible items-center justify-start">
{events.map((event, eventIndex) => (
<EventCard
key={event.id}
event={event}
index={eventIndex}
/>
))}
</div>
</div>
</section>
);
}