diff --git a/next.config.ts b/next.config.ts index 65c102b..00c940e 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,9 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "export", + images: { + unoptimized: true, + }, }; export default nextConfig; diff --git a/public/life/niko-major-champion/falcons-trophy.jpg b/public/life/niko-major-champion/falcons-trophy.jpg new file mode 100644 index 0000000..545d414 Binary files /dev/null and b/public/life/niko-major-champion/falcons-trophy.jpg differ diff --git a/public/life/niko-major-champion/niko-celebration.jpg b/public/life/niko-major-champion/niko-celebration.jpg new file mode 100644 index 0000000..6a4b6eb Binary files /dev/null and b/public/life/niko-major-champion/niko-celebration.jpg differ diff --git a/public/life/niko-major-champion/niko-emotion.jpg b/public/life/niko-major-champion/niko-emotion.jpg new file mode 100644 index 0000000..3526c81 Binary files /dev/null and b/public/life/niko-major-champion/niko-emotion.jpg differ diff --git a/src/app/life/[slug]/page.tsx b/src/app/life/[slug]/page.tsx new file mode 100644 index 0000000..38e09e4 --- /dev/null +++ b/src/app/life/[slug]/page.tsx @@ -0,0 +1,45 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { LifeArticlePage } from "@/components/life-article-page"; +import { lifePosts, getLifePost } from "@/content/life/posts"; + +type PageProps = { + params: Promise<{ + slug: string; + }>; +}; + +export function generateStaticParams() { + return lifePosts.map((post) => ({ slug: post.slug })); +} + +export async function generateMetadata({ params }: PageProps): Promise { + const { slug } = await params; + const post = getLifePost(slug); + + if (!post) { + return { + title: "文章未找到 | fly", + }; + } + + return { + title: `${post.title} | fly 日常分享`, + description: post.description, + }; +} + +export default async function LifeArticlePageRoute({ params }: PageProps) { + const { slug } = await params; + const post = getLifePost(slug); + + if (!post) { + notFound(); + } + + const currentIndex = lifePosts.findIndex((p) => p.slug === slug); + const prevPost = currentIndex > 0 ? lifePosts[currentIndex - 1] : undefined; + const nextPost = currentIndex < lifePosts.length - 1 ? lifePosts[currentIndex + 1] : undefined; + + return ; +} diff --git a/src/app/life/page.tsx b/src/app/life/page.tsx index dcc690e..ac72ded 100644 --- a/src/app/life/page.tsx +++ b/src/app/life/page.tsx @@ -1,12 +1,12 @@ -import { PlaceholderPage } from "@/components/placeholder-page"; +import type { Metadata } from "next"; +import { LifePage } from "@/components/life-page"; +import { lifePosts } from "@/content/life/posts"; -export default function LifePage() { - return ( - - ); +export const metadata: Metadata = { + title: "日常分享 | fly", + description: "fly 的日常分享,记录生活片段、照片和阶段性的想法。", +}; + +export default function LifeIndexPage() { + return ; } diff --git a/src/components/life-article-page.tsx b/src/components/life-article-page.tsx new file mode 100644 index 0000000..2b04dc1 --- /dev/null +++ b/src/components/life-article-page.tsx @@ -0,0 +1,298 @@ +"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 ( +
+ {/* 背景装饰 */} +
+
+
+ +
+ {/* 顶部导航 */} + + + + + 返回列表 + + + + + + + fly + + + + + {/* 文章内容 */} +
+ + {/* 文章头部 */} + +
+ + + {post.date} + + + + 约 {Math.ceil(post.paragraphs.length * 0.8)} 分钟阅读 + +
+ +

+ {post.title} +

+ +

+ {post.description} +

+
+ + {/* 图片画廊 */} + {post.images && post.images.length > 0 && ( + +
+ {/* 主图片 */} +
+ {post.images[currentImageIndex]?.alt} + {/* 图片渐变遮罩 */} +
+ + {/* 图片切换按钮 */} + {post.images.length > 1 && ( + <> + + + + )} + + {/* 图片计数 */} + {post.images.length > 1 && ( +
+ {currentImageIndex + 1} / {post.images.length} +
+ )} +
+ + {/* 图片说明 */} +
+

+ {post.images[currentImageIndex]?.caption} +

+ {post.images.length > 1 && ( +
+ {post.images.map((_, index) => ( +
+ )} +
+
+ + )} + + {/* 正文内容 */} + + {post.paragraphs.map((paragraph, index) => ( +

+ {paragraph} +

+ ))} +
+ + {/* 文章结尾装饰 */} + +
+
+ end +
+
+ + +
+ + {/* 上一篇/下一篇导航 */} + + + 继续阅读 + + +
+ {prevPost ? ( + + +

+ ← 上一篇 +

+

+ {prevPost.title} +

+

+ {prevPost.description} +

+ +
+ ) : ( +
+ )} + + {nextPost ? ( + + +

+ 下一篇 → +

+

+ {nextPost.title} +

+

+ {nextPost.description} +

+ +
+ ) : ( +
+ )} +
+ + + {/* 页脚 */} + + + fly / life + + + made with ♥ + + +
+ + +
+ ); +} diff --git a/src/components/life-page.tsx b/src/components/life-page.tsx new file mode 100644 index 0000000..92bffdc --- /dev/null +++ b/src/components/life-page.tsx @@ -0,0 +1,358 @@ +"use client"; + +import Link from "next/link"; +import { motion, useReducedMotion } from "motion/react"; +import { ArrowLeft, ArrowUpRight, Calendar, Clock, Sparkles, BookOpen, Image, Coffee, Music, Camera, Plane, Brain } from "lucide-react"; +import type { LifePost } from "@/content/life/types"; + +const topics = [ + { name: "Daily", icon: Coffee }, + { name: "Reading", icon: BookOpen }, + { name: "Music", icon: Music }, + { name: "Photo", icon: Camera }, + { name: "Travel", icon: Plane }, + { name: "Thoughts", icon: Brain }, +]; + +type LifePageProps = { + posts: readonly LifePost[]; +}; + +export function LifePage({ posts }: LifePageProps) { + 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 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 latestPost = posts[0]; + + return ( +
+ {/* 背景装饰 */} +
+ {/* 顶部绿色光晕 */} +
+ {/* 右下角暖绿色光晕 */} +
+ {/* 左下角深绿色光晕 */} +
+ {/* 细微纹理网格 */} +
+
+ +
+ {/* 顶部导航 */} + + + + + fly + + + + + + 首页 + + + 技术笔记 + + + + 生活分享 + + + + + {/* Hero 区域 */} + +
+ + + daily notes / life stream + + + 日常分享 + + + 技术之外的日常片段,记录生活中的想法、照片和阶段性的思考。 + + + {/* 主题标签 */} + + {topics.map((topic) => { + const Icon = topic.icon; + return ( + + + {topic.name} + + ); + })} + +
+ + +
+

+ 文章总数 +

+

+ {posts.length.toString().padStart(2, "0")} +

+
+
+

+ 最新更新 +

+

{latestPost?.date}

+
+
+ + 不定期更新,记录生活点滴 +
+
+
+ + {/* 精选文章(最新一篇) */} + {latestPost && ( + + + + featured / 最新文章 + + + + {/* 渐变装饰 */} +
+ {/* 悬停时的光晕效果 */} +
+ +
+
+
+ + + {latestPost.date} + + + + {latestPost.images?.length || 0} 张图片 + +
+

+ {latestPost.title} +

+

+ {latestPost.description} +

+
+ + 阅读全文 + + +
+
+ + {/* 图片预览区域 */} + {latestPost.images?.[0] && ( +
+
+ {latestPost.images[0].alt} + {/* 图片渐变遮罩 */} +
+
+ {/* 图片装饰角标 */} +
+ + {latestPost.images.length}P +
+
+ )} +
+ + + + )} + + {/* 文章列表 */} + + +

+ + all posts / 全部文章 +

+ + {posts.length} 篇 + +
+ +
+ {posts.map((post, index) => ( + + + {/* 左侧装饰条 */} +
+ + {/* 序号 */} + + {String(index + 1).padStart(2, "0")} + + + {/* 日期和图片数 */} +
+ + + + {post.images?.length || 0} 张 + +
+ + {/* 标题和描述 */} +
+

+ {post.title} +

+

+ {post.description} +

+
+ + {/* 箭头 */} + + + + + + ))} +
+ + {/* 底部装饰 */} + + + fly / life + + + made with ♥ + + + +
+
+ ); +} diff --git a/src/content/life/articles/niko-major-champion.ts b/src/content/life/articles/niko-major-champion.ts new file mode 100644 index 0000000..e807360 --- /dev/null +++ b/src/content/life/articles/niko-major-champion.ts @@ -0,0 +1,31 @@ +import type { LifePost } from "../types"; + +export const nikoMajorChampion = { + slug: "niko-major-champion", + date: "2026.06.22", + title: "NiKo 终于等到了这个冠军", + description: "关于 NiKo 与 Falcons 捧起科隆 Major 冠军的一点观赛感想。", + images: [ + { + src: "/life/niko-major-champion/falcons-trophy.jpg", + alt: "Falcons 队员在舞台上举起冠军奖杯", + caption: "Falcons 举起冠军奖杯,舞台上的金色纸屑和火花刚好把这个瞬间托住。", + }, + { + src: "/life/niko-major-champion/niko-emotion.jpg", + alt: "NiKo 站在观众席前情绪激动", + caption: "NiKo 站在观众面前,像是终于从漫长的遗憾里走了出来。", + }, + { + src: "/life/niko-major-champion/niko-celebration.jpg", + alt: "NiKo 在比赛席上握拳庆祝", + caption: "比赛席上的握拳庆祝,比任何解释都更直接。", + }, + ], + paragraphs: [ + "看到 NiKo 跟 Falcons 捧起科隆 Major 冠军的时候,第一反应不是“他终于证明自己了”,而是“这个故事总算没有一直卡在遗憾那里”。", + "CS 圈里很少有人会怀疑 NiKo 的个人能力。那些年的精准定位、干拉、残局处理,还有他身上那种很固执的强者气质,早就把他放进了历史级步枪手的讨论里。可越是这样,Major 冠军这个缺口就越刺眼。天赋被看见了,巅峰被看见了,遗憾也被反复看见了。", + "所以这次夺冠最打动我的地方,不只是奖杯本身,而是一个选手在漫长职业生涯里终于把叙事拧回来了。第 17 次 Major,29 岁,换过队友,换过体系,也经历过足够多次“差一点”。这种冠军不是爽文式的轻松圆梦,更像是把很多年没说完的话,终于稳稳地放在了桌面上。", + "我挺喜欢这样的体育瞬间:它提醒人,很多东西不是只属于最年轻、最锋利、最顺风的时候。只要还在场上,还愿意承担,还能把自己交给比赛,故事就没有真正写完。", + ], +} satisfies LifePost; diff --git a/src/content/life/posts.ts b/src/content/life/posts.ts new file mode 100644 index 0000000..0c21659 --- /dev/null +++ b/src/content/life/posts.ts @@ -0,0 +1,8 @@ +import { nikoMajorChampion } from "./articles/niko-major-champion"; +import type { LifePost } from "./types"; + +export const lifePosts = [nikoMajorChampion] as const satisfies readonly LifePost[]; + +export function getLifePost(slug: string) { + return lifePosts.find((post) => post.slug === slug); +} diff --git a/src/content/life/types.ts b/src/content/life/types.ts new file mode 100644 index 0000000..19b702c --- /dev/null +++ b/src/content/life/types.ts @@ -0,0 +1,12 @@ +export type LifePost = { + slug: string; + date: string; + title: string; + description: string; + images: { + src: string; + alt: string; + caption: string; + }[]; + paragraphs: string[]; +};