Files
fly-personal-site/src/components/life-article-page.tsx
T

299 lines
13 KiB
TypeScript
Raw Normal View History

"use client";
import Link from "next/link";
import { motion, useReducedMotion } from "motion/react";
import { ArrowLeft, ArrowUpRight, Calendar, Clock, Sparkles, ChevronLeft, ChevronRight } from "lucide-react";
import { useState } from "react";
import type { LifePost } from "@/content/life/types";
import { BackToTopButton } from "@/components/back-to-top-button";
type LifeArticlePageProps = {
post: LifePost;
prevPost?: LifePost;
nextPost?: LifePost;
};
export function LifeArticlePage({ post, prevPost, nextPost }: LifeArticlePageProps) {
const reduceMotion = useReducedMotion();
const distance = reduceMotion ? 0 : 24;
const duration = reduceMotion ? 0 : 0.55;
const easeOut = [0.22, 1, 0.36, 1] as const;
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const container = {
hidden: {},
show: {
transition: {
staggerChildren: reduceMotion ? 0 : 0.08,
delayChildren: reduceMotion ? 0 : 0.1,
},
},
};
const reveal = {
hidden: { opacity: 0, y: distance, filter: reduceMotion ? "none" : "blur(8px)" },
show: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration, ease: easeOut } },
};
const scaleReveal = {
hidden: { opacity: 0, y: distance, scale: reduceMotion ? 1 : 0.97 },
show: { opacity: 1, y: 0, scale: 1, transition: { duration, ease: easeOut } },
};
const nextImage = () => {
if (post.images.length > 0) {
setCurrentImageIndex((prev) => (prev + 1) % post.images.length);
}
};
const prevImage = () => {
if (post.images.length > 0) {
setCurrentImageIndex((prev) => (prev - 1 + post.images.length) % post.images.length);
}
};
return (
<main className="relative min-h-dvh overflow-x-clip bg-[#f4f5f1] text-[#151515]">
{/* 背景装饰 */}
<div className="pointer-events-none absolute inset-0 -z-10">
<div
className="absolute -top-20 left-1/2 h-96 w-[80%] -translate-x-1/2 rounded-full opacity-25 blur-3xl"
style={{
background:
"radial-gradient(circle, rgba(95, 111, 82, 0.35) 0%, rgba(95, 111, 82, 0.1) 40%, transparent 70%)",
}}
/>
</div>
<div className="mx-auto flex min-h-dvh w-full max-w-4xl flex-col px-5 py-5 sm:px-8 lg:px-10">
{/* 顶部导航 */}
<motion.header
initial="hidden"
animate="show"
variants={container}
className="relative z-10 flex items-center justify-between rounded-full border border-[#151515]/10 bg-white/60 px-4 py-3 shadow-lg shadow-[#5f6f52]/5 backdrop-blur-xl"
>
<motion.div variants={scaleReveal}>
<Link
href="/life"
className="group inline-flex h-11 items-center gap-2 rounded-full border border-[#151515]/10 bg-white/70 px-4 text-sm font-medium text-[#151515] outline-none transition hover:border-[#5f6f52]/30 hover:bg-[#5f6f52]/5 hover:text-[#5f6f52] focus-visible:ring-2 focus-visible:ring-[#5f6f52]/40"
>
<ArrowLeft className="h-4 w-4 transition group-hover:-translate-x-0.5" />
返回列表
</Link>
</motion.div>
<motion.div variants={reveal}>
<Link
href="/"
className="group inline-flex h-11 items-center gap-2 rounded-full bg-[#151515] px-4 text-sm font-black text-white outline-none transition hover:bg-[#5f6f52] focus-visible:ring-2 focus-visible:ring-[#5f6f52]/40"
>
<Sparkles className="h-4 w-4 transition group-hover:rotate-12" />
fly
</Link>
</motion.div>
</motion.header>
{/* 文章内容 */}
<article className="flex-1 py-12 sm:py-16">
<motion.div
initial="hidden"
animate="show"
variants={container}
className="space-y-8"
>
{/* 文章头部 */}
<motion.header variants={reveal} className="space-y-6">
<div className="flex flex-wrap items-center gap-3">
<span className="inline-flex items-center gap-1.5 rounded-full bg-[#5f6f52]/10 px-3 py-1 font-mono text-xs font-medium text-[#5f6f52]">
<Calendar className="h-3 w-3" />
{post.date}
</span>
<span className="inline-flex items-center gap-1.5 rounded-full border border-[#151515]/8 bg-white/50 px-3 py-1 font-mono text-xs text-[#151515]/50">
<Clock className="h-3 w-3" />
{Math.ceil(post.paragraphs.length * 0.8)} 分钟阅读
</span>
</div>
<h1 className="font-display text-4xl font-semibold leading-tight tracking-tight text-[#151515] sm:text-5xl lg:text-6xl">
{post.title}
</h1>
<p className="text-lg leading-8 text-[#151515]/60">
{post.description}
</p>
</motion.header>
{/* 图片画廊 */}
{post.images && post.images.length > 0 && (
<motion.div variants={scaleReveal}>
<div className="relative overflow-hidden rounded-[1.5rem] border border-[#151515]/8 bg-white shadow-xl shadow-[#5f6f52]/8">
{/* 主图片 */}
<div className="relative aspect-[16/9] w-full overflow-hidden">
<img
src={post.images[currentImageIndex]?.src}
alt={post.images[currentImageIndex]?.alt}
className="h-full w-full object-cover transition-opacity duration-300"
key={currentImageIndex}
/>
{/* 图片渐变遮罩 */}
<div className="absolute inset-0 bg-gradient-to-t from-black/20 via-transparent to-transparent" />
{/* 图片切换按钮 */}
{post.images.length > 1 && (
<>
<button
onClick={prevImage}
className="absolute left-4 top-1/2 -translate-y-1/2 inline-flex h-10 w-10 items-center justify-center rounded-full bg-white/80 text-[#151515]/70 shadow-lg backdrop-blur transition hover:bg-white hover:text-[#5f6f52] focus-visible:ring-2 focus-visible:ring-[#5f6f52]/40"
aria-label="上一张图片"
>
<ChevronLeft className="h-5 w-5" />
</button>
<button
onClick={nextImage}
className="absolute right-4 top-1/2 -translate-y-1/2 inline-flex h-10 w-10 items-center justify-center rounded-full bg-white/80 text-[#151515]/70 shadow-lg backdrop-blur transition hover:bg-white hover:text-[#5f6f52] focus-visible:ring-2 focus-visible:ring-[#5f6f52]/40"
aria-label="下一张图片"
>
<ChevronRight className="h-5 w-5" />
</button>
</>
)}
{/* 图片计数 */}
{post.images.length > 1 && (
<div className="absolute right-4 top-4 inline-flex items-center gap-1 rounded-full bg-black/40 px-2.5 py-1 font-mono text-xs text-white backdrop-blur">
{currentImageIndex + 1} / {post.images.length}
</div>
)}
</div>
{/* 图片说明 */}
<div className="p-5 sm:p-6">
<p className="text-sm leading-6 text-[#151515]/55">
{post.images[currentImageIndex]?.caption}
</p>
{post.images.length > 1 && (
<div className="mt-4 flex items-center justify-center gap-2">
{post.images.map((_, index) => (
<button
key={index}
onClick={() => setCurrentImageIndex(index)}
className={`h-2 rounded-full transition-all ${
index === currentImageIndex
? "w-6 bg-[#5f6f52]"
: "w-2 bg-[#151515]/20 hover:bg-[#151515]/30"
}`}
aria-label={`查看第 ${index + 1} 张图片`}
/>
))}
</div>
)}
</div>
</div>
</motion.div>
)}
{/* 正文内容 */}
<motion.div variants={reveal} className="space-y-6 pt-4">
{post.paragraphs.map((paragraph, index) => (
<p
key={index}
className="text-base leading-8 text-[#151515]/75 sm:text-lg sm:leading-9"
>
{paragraph}
</p>
))}
</motion.div>
{/* 文章结尾装饰 */}
<motion.div variants={reveal} className="pt-8">
<div className="flex items-center gap-4">
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-[#5f6f52]/30 to-transparent" />
<span className="font-mono text-xs uppercase text-[#5f6f52]/60">end</span>
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-[#5f6f52]/30 to-transparent" />
</div>
</motion.div>
</motion.div>
</article>
{/* 上一篇/下一篇导航 */}
<motion.section
initial="hidden"
whileInView="show"
viewport={{ once: true, margin: "-80px" }}
variants={container}
className="border-t border-[#151515]/8 py-10"
>
<motion.p
variants={reveal}
className="mb-6 font-mono text-xs uppercase tracking-normal text-[#5f6f52]"
>
继续阅读
</motion.p>
<div className="grid gap-4 sm:grid-cols-2">
{prevPost ? (
<motion.div variants={reveal}>
<Link
href={`/life/${prevPost.slug}`}
className="group block rounded-2xl border border-[#151515]/8 bg-white/60 p-5 transition hover:border-[#5f6f52]/25 hover:bg-white hover:shadow-lg hover:shadow-[#5f6f52]/8 focus-visible:ring-2 focus-visible:ring-[#5f6f52]/40"
>
<p className="mb-2 font-mono text-xs uppercase text-[#151515]/38">
上一篇
</p>
<h3 className="text-lg font-medium text-[#151515] transition group-hover:text-[#5f6f52]">
{prevPost.title}
</h3>
<p className="mt-2 line-clamp-1 text-sm text-[#151515]/50">
{prevPost.description}
</p>
</Link>
</motion.div>
) : (
<div />
)}
{nextPost ? (
<motion.div variants={reveal}>
<Link
href={`/life/${nextPost.slug}`}
className="group block rounded-2xl border border-[#151515]/8 bg-white/60 p-5 text-right transition hover:border-[#5f6f52]/25 hover:bg-white hover:shadow-lg hover:shadow-[#5f6f52]/8 focus-visible:ring-2 focus-visible:ring-[#5f6f52]/40"
>
<p className="mb-2 font-mono text-xs uppercase text-[#151515]/38">
下一篇
</p>
<h3 className="text-lg font-medium text-[#151515] transition group-hover:text-[#5f6f52]">
{nextPost.title}
</h3>
<p className="mt-2 line-clamp-1 text-sm text-[#151515]/50">
{nextPost.description}
</p>
</Link>
</motion.div>
) : (
<div />
)}
</div>
</motion.section>
{/* 页脚 */}
<motion.footer
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: reduceMotion ? 0 : 0.6, delay: reduceMotion ? 0 : 0.5 }}
className="flex items-center justify-between border-t border-[#151515]/8 py-6"
>
<span className="font-mono text-xs uppercase tracking-normal text-[#151515]/32">
fly / life
</span>
<span className="font-mono text-xs text-[#151515]/32">
made with
</span>
</motion.footer>
</div>
<BackToTopButton />
</main>
);
}