fix: add animations, timeline path, and fix layout to match design

This commit is contained in:
2026-01-27 21:58:08 +08:00
parent e1d14fcd0b
commit 7d78bb2d52
2 changed files with 206 additions and 22 deletions
+186 -22
View File
@@ -1,5 +1,4 @@
import { getAIEvents } from '@/hooks/useAIEvents';
import { TimelineSection } from '@/components/timeline/TimelineSection';
import { Metadata } from 'next';
export const revalidate = 3600;
@@ -22,13 +21,17 @@ export async function generateMetadata({
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);
@@ -50,27 +53,188 @@ export default async function TimelinePage() {
return (
<div className="min-h-screen bg-background-light dark:bg-background-dark">
<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">
AI
</h1>
<p className="font-mono text-lg md:text-xl max-w-2xl mx-auto bg-white dark:bg-black border border-black dark:border-white p-4 shadow-hard">
Transformer AGI: 大语言模型的进化之路
</p>
</div>
</header>
{/* Grid background */}
<div className="fixed inset-0 pointer-events-none z-0 opacity-10 dark:opacity-20 overflow-hidden">
<div className="absolute inset-0 bg-[size:40px_40px] bg-[linear-gradient(to_right,#e5e5e5_1px,transparent_1px),linear-gradient(to_bottom,#e5e5e5_1px,transparent_1px)] dark:bg-[linear-gradient(to_right,#333_1px,transparent_1px),linear-gradient(to_bottom,#333_1px,transparent_1px)]" />
</div>
<main className="px-4 md:px-12 max-w-[1600px] mx-auto pb-32">
{sortedYears.map((year, index) => (
<TimelineSection
key={year}
year={year}
events={eventsByYear[year]}
index={index}
/>
))}
</main>
{/* Header */}
<div className="relative z-10 pt-32 pb-20 px-4 max-w-[1600px] mx-auto">
<header className="text-center mb-20 relative">
<div className="inline-block relative">
{/* Decorative SVG */}
<svg
className="absolute -top-6 -left-8 w-[120%] h-[150%] text-primary opacity-80 -z-10 animate-pulse"
viewBox="0 0 200 200"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M44.7,-51.2C57.1,-41.5,66.1,-27.6,68.9,-12.8C71.7,2,68.3,17.7,60.4,30.9C52.5,44.1,40.1,54.8,26.4,60.1C12.7,65.4,-2.3,65.3,-17.1,61.1C-31.9,56.9,-46.5,48.6,-56.3,36.4C-66.1,24.2,-71.1,8.1,-67.3,-5.7C-63.5,-19.5,-50.9,-31,-38.7,-40.8C-26.5,-50.6,-14.7,-58.7,0.1,-58.8C14.9,-59,29.8,-51.2,32.3,-60.9"
fill="currentColor"
transform="translate(100 100)"
/>
</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.
</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.
</p>
</header>
{/* Main Timeline */}
<main className="relative px-4 md:px-12 pb-32">
<div className="relative w-full">
{/* Continuous timeline path */}
<div className="absolute left-8 md:left-1/2 top-0 bottom-32 w-1 bg-gradient-to-b from-primary via-secondary to-primary opacity-50 hidden md:block" />
{/* Timeline dots */}
{sortedYears.map((year, yearIndex) => (
<div
key={`dot-${year}`}
className="absolute left-8 md:left-1/2 w-4 h-4 bg-primary border-4 border-black dark:border-white rounded-full -translate-x-1/2 hidden md:block"
style={{
top: `${20 + yearIndex * 35}rem`
}}
/>
))}
{/* Year sections */}
{sortedYears.map((year, yearIndex) => (
<section
key={year}
className={`relative min-h-[500px] mb-32 flex ${
yearIndex % 2 === 0 ? 'flex-row' : 'flex-row-reverse'
}`}
>
{/* Year Label */}
<div
className={`absolute ${yearIndex % 2 === 0 ? 'left-0 md:left-auto md:right-0' : 'left-0 md:left-0 md:right-auto'} -top-8 z-20`}
>
<div
className={`${
yearIndex % 2 === 0 ? 'bg-primary' : 'bg-secondary'
} border-4 border-black px-6 py-2 ${
yearIndex % 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 ${
yearIndex % 2 === 0 ? 'pl-0 md:pl-16 pr-0 md:pr-32' : 'pl-0 md:pl-32 pr-0 md:pr-16'
} pt-20`}
>
<div className="flex flex-nowrap overflow-x-visible items-center justify-start py-10">
{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"
style={{
zIndex: Math.max(1, 40 - eventIndex * 10),
transform: `rotate(${(eventIndex % 7 - 3) * 2}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>
))}
</div>
</div>
</section>
))}
</div>
</main>
{/* Newsletter section */}
<div className="max-w-3xl mx-auto px-4">
<div className="bg-primary border-4 border-black p-8 relative shadow-[12px_12px_0px_0px_#000] dark:shadow-[12px_12px_0px_0px_#fff]">
<div className="absolute -top-10 -right-6 w-20 h-20 bg-white dark:bg-gray-800 border-4 border-black flex items-center justify-center rounded-full animate-bounce">
<span className="material-icons-round text-4xl text-black dark:text-white">
mail
</span>
</div>
<h2 className="font-display font-black text-3xl md:text-5xl mb-4 text-black uppercase tracking-tight">
Join the Park
</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.
</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..."
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
</button>
</form>
<div className="mt-6 flex items-center gap-3">
<input
className="w-6 h-6 border-4 border-black text-black focus:ring-0 rounded-none bg-white checked:bg-black"
id="check"
type="checkbox"
/>
<label className="text-sm font-black text-black uppercase" htmlFor="check">
I agree to be cool.
</label>
</div>
</div>
</div>
</div>
{/* Back to top button */}
<div className="fixed bottom-6 right-6 z-50">
<div className="bg-white dark:bg-black border-4 border-black dark:border-white p-3 shadow-hard dark:shadow-hard-dark cursor-pointer hover:-translate-y-2 transition-transform">
<span className="material-icons-round text-3xl text-black dark:text-white">
arrow_upward
</span>
</div>
</div>
</div>
);
}
+20
View File
@@ -103,4 +103,24 @@
@apply border border-black dark:border-gray-500 px-2 py-1 text-xs
font-display font-bold uppercase bg-white dark:bg-gray-800;
}
/* Timeline stack card animations */
.stack-card {
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.stack-card:hover {
z-index: 50 !important;
transform: translateY(-20px) scale(1.05) rotate(0deg) !important;
margin-right: 20px !important;
margin-left: 20px !important;
}
/* Tape decoration styling */
.tape {
background-color: rgba(255, 255, 255, 0.4);
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
backdrop-filter: blur(2px);
border: 1px solid rgba(255,255,255,0.6);
}
}