日常分享布局调整
This commit is contained in:
@@ -4,7 +4,7 @@ import { lifePosts } from "@/content/life/posts";
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "日常分享 | fly",
|
title: "日常分享 | fly",
|
||||||
description: "fly 的日常分享,记录生活片段、照片和阶段性的想法。",
|
description: "fly 的日常分享,记录工具使用、阶段复盘、生活观察和亲自走过的经验。",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function LifeIndexPage() {
|
export default function LifeIndexPage() {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ label: "技术文章", href: "#articles" },
|
{ label: "技术文章", href: "#articles" },
|
||||||
{ label: "生活分享", href: "#life" },
|
{ label: "日常分享", href: "#life" },
|
||||||
{ label: "联系我", href: "mailto:3505891631@qq.com" },
|
{ label: "联系我", href: "mailto:3505891631@qq.com" },
|
||||||
{ label: "Gitea", href: "http://124.221.147.173:3000/fly/fly-personal-site" },
|
{ label: "Gitea", href: "http://124.221.147.173:3000/fly/fly-personal-site" },
|
||||||
];
|
];
|
||||||
@@ -31,10 +31,10 @@ const portals = [
|
|||||||
gradient: "from-cyan-300 via-blue-500 to-violet-500",
|
gradient: "from-cyan-300 via-blue-500 to-violet-500",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "生活分享",
|
title: "日常分享",
|
||||||
href: "#life",
|
href: "#life",
|
||||||
eyebrow: "Life stream",
|
eyebrow: "Life stream",
|
||||||
description: "技术之外的日常片段,呈现更完整的个人侧面。",
|
description: "记录工具使用、阶段复盘、生活观察和那些亲自走过的经验。",
|
||||||
icon: LifeBuoy,
|
icon: LifeBuoy,
|
||||||
gradient: "from-lime-300 via-emerald-400 to-cyan-400",
|
gradient: "from-lime-300 via-emerald-400 to-cyan-400",
|
||||||
},
|
},
|
||||||
@@ -298,10 +298,10 @@ export function HomePage() {
|
|||||||
life channel
|
life channel
|
||||||
</motion.p>
|
</motion.p>
|
||||||
<motion.h2 variants={reveal} className="mt-4 font-display text-5xl font-semibold sm:text-7xl">
|
<motion.h2 variants={reveal} className="mt-4 font-display text-5xl font-semibold sm:text-7xl">
|
||||||
生活分享
|
日常分享
|
||||||
</motion.h2>
|
</motion.h2>
|
||||||
<motion.p variants={reveal} className="mt-6 max-w-xl text-base leading-8 text-white/66">
|
<motion.p variants={reveal} className="mt-6 max-w-xl text-base leading-8 text-white/66">
|
||||||
技术之外的日常切面。阅读、音乐、照片、旅行和一些阶段性的想法,让这个网站不只是一份技术档案。
|
记录真实经历里的小路标:工具使用、阶段复盘、生活观察,以及一些值得回头看的想法。
|
||||||
</motion.p>
|
</motion.p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<motion.div variants={container} className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
<motion.div variants={container} className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
import { motion, useReducedMotion } from "motion/react";
|
import { motion, useReducedMotion } from "motion/react";
|
||||||
import { ArrowLeft, ArrowUpRight, Calendar, Clock, Sparkles, ChevronLeft, ChevronRight } from "lucide-react";
|
import { ArrowLeft, Calendar, Clock, Sparkles } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { countLifePostParagraphs } from "@/content/life/content";
|
||||||
import type { LifePost } from "@/content/life/types";
|
import type { LifePost } from "@/content/life/types";
|
||||||
import { BackToTopButton } from "@/components/back-to-top-button";
|
import { BackToTopButton } from "@/components/back-to-top-button";
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ export function LifeArticlePage({ post, prevPost, nextPost }: LifeArticlePagePro
|
|||||||
const distance = reduceMotion ? 0 : 24;
|
const distance = reduceMotion ? 0 : 24;
|
||||||
const duration = reduceMotion ? 0 : 0.55;
|
const duration = reduceMotion ? 0 : 0.55;
|
||||||
const easeOut = [0.22, 1, 0.36, 1] as const;
|
const easeOut = [0.22, 1, 0.36, 1] as const;
|
||||||
const [currentImageIndex, setCurrentImageIndex] = useState(0);
|
const paragraphCount = countLifePostParagraphs(post);
|
||||||
|
|
||||||
const container = {
|
const container = {
|
||||||
hidden: {},
|
hidden: {},
|
||||||
@@ -40,18 +41,6 @@ export function LifeArticlePage({ post, prevPost, nextPost }: LifeArticlePagePro
|
|||||||
show: { opacity: 1, y: 0, scale: 1, transition: { duration, ease: easeOut } },
|
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 (
|
return (
|
||||||
<main className="relative min-h-dvh overflow-x-clip bg-[#f4f5f1] text-[#151515]">
|
<main className="relative min-h-dvh overflow-x-clip bg-[#f4f5f1] text-[#151515]">
|
||||||
{/* 背景装饰 */}
|
{/* 背景装饰 */}
|
||||||
@@ -111,7 +100,7 @@ export function LifeArticlePage({ post, prevPost, nextPost }: LifeArticlePagePro
|
|||||||
</span>
|
</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">
|
<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" />
|
<Clock className="h-3 w-3" />
|
||||||
约 {Math.ceil(post.paragraphs.length * 0.8)} 分钟阅读
|
约 {Math.ceil(paragraphCount * 0.8)} 分钟阅读
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -124,85 +113,42 @@ export function LifeArticlePage({ post, prevPost, nextPost }: LifeArticlePagePro
|
|||||||
</p>
|
</p>
|
||||||
</motion.header>
|
</motion.header>
|
||||||
|
|
||||||
{/* 图片画廊 */}
|
<motion.div variants={reveal} className="space-y-8 pt-4">
|
||||||
{post.images && post.images.length > 0 && (
|
{post.content.map((block, index) => {
|
||||||
<motion.div variants={scaleReveal}>
|
if (block.type === "paragraph") {
|
||||||
<div className="relative overflow-hidden rounded-[1.5rem] border border-[#151515]/8 bg-white shadow-xl shadow-[#5f6f52]/8">
|
return (
|
||||||
{/* 主图片 */}
|
|
||||||
<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
|
<p
|
||||||
key={index}
|
key={`paragraph-${index}`}
|
||||||
className="text-base leading-8 text-[#151515]/75 sm:text-lg sm:leading-9"
|
className="text-base leading-8 text-[#151515]/75 sm:text-lg sm:leading-9"
|
||||||
>
|
>
|
||||||
{paragraph}
|
{block.text}
|
||||||
</p>
|
</p>
|
||||||
))}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.figure
|
||||||
|
key={`image-${index}`}
|
||||||
|
variants={scaleReveal}
|
||||||
|
className="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 bg-[#e7e8df]">
|
||||||
|
<Image
|
||||||
|
src={block.src}
|
||||||
|
alt={block.alt}
|
||||||
|
fill
|
||||||
|
sizes="(max-width: 768px) 100vw, 896px"
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{block.caption ? (
|
||||||
|
<figcaption className="px-5 py-4 text-sm leading-6 text-[#151515]/55 sm:px-6">
|
||||||
|
{block.caption}
|
||||||
|
</figcaption>
|
||||||
|
) : null}
|
||||||
|
</motion.figure>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* 文章结尾装饰 */}
|
{/* 文章结尾装饰 */}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import NextImage from "next/image";
|
||||||
import { motion, useReducedMotion } from "motion/react";
|
import { motion, useReducedMotion } from "motion/react";
|
||||||
import { ArrowLeft, ArrowUpRight, Calendar, Clock, Sparkles, BookOpen, Image, Coffee, Music, Camera, Plane, Brain } from "lucide-react";
|
import { ArrowUpRight, Calendar, Clock, Sparkles, BookOpen, Image as ImageIcon, Coffee, Music, Camera, Plane, Brain } from "lucide-react";
|
||||||
|
import { getLifePostCoverImage, getLifePostImages } from "@/content/life/content";
|
||||||
import type { LifePost } from "@/content/life/types";
|
import type { LifePost } from "@/content/life/types";
|
||||||
|
|
||||||
const topics = [
|
const topics = [
|
||||||
@@ -45,6 +47,8 @@ export function LifePage({ posts }: LifePageProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const latestPost = posts[0];
|
const latestPost = posts[0];
|
||||||
|
const latestPostImages = latestPost ? getLifePostImages(latestPost) : [];
|
||||||
|
const latestPostCoverImage = latestPost ? getLifePostCoverImage(latestPost) : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="relative min-h-dvh overflow-x-clip bg-[#f4f5f1] text-[#151515]">
|
<main className="relative min-h-dvh overflow-x-clip bg-[#f4f5f1] text-[#151515]">
|
||||||
@@ -118,7 +122,7 @@ export function LifePage({ posts }: LifePageProps) {
|
|||||||
</Link>
|
</Link>
|
||||||
<span className="relative rounded-full px-4 py-2 text-sm font-semibold text-[#5f6f52]">
|
<span className="relative rounded-full px-4 py-2 text-sm font-semibold text-[#5f6f52]">
|
||||||
<span className="absolute inset-0 rounded-full bg-[#5f6f52]/10" />
|
<span className="absolute inset-0 rounded-full bg-[#5f6f52]/10" />
|
||||||
<span className="relative">生活分享</span>
|
<span className="relative">日常分享</span>
|
||||||
</span>
|
</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.header>
|
</motion.header>
|
||||||
@@ -145,7 +149,7 @@ export function LifePage({ posts }: LifePageProps) {
|
|||||||
日常分享
|
日常分享
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
<motion.p variants={reveal} className="mt-6 max-w-xl text-lg leading-8 text-[#151515]/58">
|
<motion.p variants={reveal} className="mt-6 max-w-xl text-lg leading-8 text-[#151515]/58">
|
||||||
技术之外的日常片段,记录生活中的想法、照片和阶段性的思考。
|
记录真实经历里的小路标:工具使用、阶段复盘、生活观察,以及一些值得回头看的想法。
|
||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
||||||
{/* 主题标签 */}
|
{/* 主题标签 */}
|
||||||
@@ -222,7 +226,11 @@ export function LifePage({ posts }: LifePageProps) {
|
|||||||
{/* 悬停时的光晕效果 */}
|
{/* 悬停时的光晕效果 */}
|
||||||
<div className="absolute -top-1/2 -right-1/2 h-full w-full rounded-full bg-[#5f6f52]/0 blur-3xl transition duration-700 group-hover:bg-[#5f6f52]/8" />
|
<div className="absolute -top-1/2 -right-1/2 h-full w-full rounded-full bg-[#5f6f52]/0 blur-3xl transition duration-700 group-hover:bg-[#5f6f52]/8" />
|
||||||
|
|
||||||
<div className="relative grid gap-8 p-6 sm:p-8 lg:grid-cols-[1fr_20rem] lg:items-center lg:p-10">
|
<div
|
||||||
|
className={`relative grid gap-8 p-6 sm:p-8 lg:items-center lg:p-10 ${
|
||||||
|
latestPostCoverImage ? "lg:grid-cols-[1fr_20rem]" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-[#5f6f52]/12 px-3 py-1 font-mono text-xs font-medium text-[#5f6f52]">
|
<span className="inline-flex items-center gap-1.5 rounded-full bg-[#5f6f52]/12 px-3 py-1 font-mono text-xs font-medium text-[#5f6f52]">
|
||||||
@@ -230,8 +238,8 @@ export function LifePage({ posts }: LifePageProps) {
|
|||||||
{latestPost.date}
|
{latestPost.date}
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex items-center gap-1 font-mono text-xs uppercase text-[#151515]/30">
|
<span className="inline-flex items-center gap-1 font-mono text-xs uppercase text-[#151515]/30">
|
||||||
<Image className="h-3 w-3" />
|
<ImageIcon className="h-3 w-3" />
|
||||||
{latestPost.images?.length || 0} 张图片
|
{latestPostImages.length} 张图片
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="font-display text-3xl font-semibold leading-tight text-[#151515] transition duration-300 group-hover:text-[#5f6f52] sm:text-4xl">
|
<h2 className="font-display text-3xl font-semibold leading-tight text-[#151515] transition duration-300 group-hover:text-[#5f6f52] sm:text-4xl">
|
||||||
@@ -249,21 +257,23 @@ export function LifePage({ posts }: LifePageProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 图片预览区域 */}
|
{/* 图片预览区域 */}
|
||||||
{latestPost.images?.[0] && (
|
{latestPostCoverImage && (
|
||||||
<div className="relative overflow-hidden rounded-2xl border border-[#151515]/8 bg-[#f4f5f1]">
|
<div className="relative overflow-hidden rounded-2xl border border-[#151515]/8 bg-[#f4f5f1]">
|
||||||
<div className="aspect-[4/3] w-full">
|
<div className="relative aspect-[4/3] w-full">
|
||||||
<img
|
<NextImage
|
||||||
src={latestPost.images[0].src}
|
src={latestPostCoverImage.src}
|
||||||
alt={latestPost.images[0].alt}
|
alt={latestPostCoverImage.alt}
|
||||||
className="h-full w-full object-cover transition duration-500 group-hover:scale-105"
|
fill
|
||||||
|
sizes="(max-width: 1024px) 100vw, 320px"
|
||||||
|
className="object-cover transition duration-500 group-hover:scale-105"
|
||||||
/>
|
/>
|
||||||
{/* 图片渐变遮罩 */}
|
{/* 图片渐变遮罩 */}
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-[#5f6f52]/10 via-transparent to-transparent" />
|
<div className="absolute inset-0 bg-gradient-to-t from-[#5f6f52]/10 via-transparent to-transparent" />
|
||||||
</div>
|
</div>
|
||||||
{/* 图片装饰角标 */}
|
{/* 图片装饰角标 */}
|
||||||
<div className="absolute left-3 top-3 inline-flex items-center gap-1 rounded-full bg-white/80 px-2 py-1 font-mono text-[0.65rem] font-medium text-[#5f6f52] backdrop-blur">
|
<div className="absolute left-3 top-3 inline-flex items-center gap-1 rounded-full bg-white/80 px-2 py-1 font-mono text-[0.65rem] font-medium text-[#5f6f52] backdrop-blur">
|
||||||
<Image className="h-3 w-3" />
|
<ImageIcon className="h-3 w-3" />
|
||||||
{latestPost.images.length}P
|
{latestPostImages.length}P
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -295,7 +305,10 @@ export function LifePage({ posts }: LifePageProps) {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{posts.map((post, index) => (
|
{posts.map((post, index) => {
|
||||||
|
const postImages = getLifePostImages(post);
|
||||||
|
|
||||||
|
return (
|
||||||
<motion.div key={post.slug} variants={reveal}>
|
<motion.div key={post.slug} variants={reveal}>
|
||||||
<Link
|
<Link
|
||||||
href={`/life/${post.slug}`}
|
href={`/life/${post.slug}`}
|
||||||
@@ -315,8 +328,8 @@ export function LifePage({ posts }: LifePageProps) {
|
|||||||
{post.date}
|
{post.date}
|
||||||
</time>
|
</time>
|
||||||
<span className="hidden items-center gap-1 font-mono text-[0.65rem] text-[#151515]/25 sm:inline-flex">
|
<span className="hidden items-center gap-1 font-mono text-[0.65rem] text-[#151515]/25 sm:inline-flex">
|
||||||
<Image className="h-3 w-3" />
|
<ImageIcon className="h-3 w-3" />
|
||||||
{post.images?.length || 0} 张
|
{postImages.length} 张
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -336,7 +349,8 @@ export function LifePage({ posts }: LifePageProps) {
|
|||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 底部装饰 */}
|
{/* 底部装饰 */}
|
||||||
|
|||||||
@@ -5,27 +5,38 @@ export const nikoMajorChampion = {
|
|||||||
date: "2026.06.22",
|
date: "2026.06.22",
|
||||||
title: "NiKo 终于等到了这个冠军",
|
title: "NiKo 终于等到了这个冠军",
|
||||||
description: "关于 NiKo 与 Falcons 捧起科隆 Major 冠军的一点观赛感想。",
|
description: "关于 NiKo 与 Falcons 捧起科隆 Major 冠军的一点观赛感想。",
|
||||||
images: [
|
content: [
|
||||||
{
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
text: "看到 NiKo 跟 Falcons 捧起科隆 Major 冠军的时候,第一反应不是“他终于证明自己了”,而是“这个故事总算没有一直卡在遗憾那里”。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
src: "/life/niko-major-champion/falcons-trophy.jpg",
|
src: "/life/niko-major-champion/falcons-trophy.jpg",
|
||||||
alt: "Falcons 队员在舞台上举起冠军奖杯",
|
alt: "Falcons 队员在舞台上举起冠军奖杯",
|
||||||
caption: "Falcons 举起冠军奖杯,舞台上的金色纸屑和火花刚好把这个瞬间托住。",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
text: "CS 圈里很少有人会怀疑 NiKo 的个人能力。那些年的精准定位、干拉、残局处理,还有他身上那种很固执的强者气质,早就把他放进了历史级步枪手的讨论里。可越是这样,Major 冠军这个缺口就越刺眼。天赋被看见了,巅峰被看见了,遗憾也被反复看见了。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
src: "/life/niko-major-champion/niko-emotion.jpg",
|
src: "/life/niko-major-champion/niko-emotion.jpg",
|
||||||
alt: "NiKo 站在观众席前情绪激动",
|
alt: "NiKo 站在观众席前情绪激动",
|
||||||
caption: "NiKo 站在观众面前,像是终于从漫长的遗憾里走了出来。",
|
// caption: "NiKo 站在观众面前,像是终于从漫长的遗憾里走了出来。",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
text: "所以这次夺冠最打动我的地方,不只是奖杯本身,而是一个选手在漫长职业生涯里终于把叙事拧回来了。第 17 次 Major,29 岁,换过队友,换过体系,也经历过足够多次“差一点”。这种冠军不是爽文式的轻松圆梦,更像是把很多年没说完的话,终于稳稳地放在了桌面上。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
src: "/life/niko-major-champion/niko-celebration.jpg",
|
src: "/life/niko-major-champion/niko-celebration.jpg",
|
||||||
alt: "NiKo 在比赛席上握拳庆祝",
|
alt: "NiKo 在比赛席上握拳庆祝",
|
||||||
caption: "比赛席上的握拳庆祝,比任何解释都更直接。",
|
},
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
text: "我挺喜欢这样的体育瞬间:它提醒人,很多东西不是只属于最年轻、最锋利、最顺风的时候。只要还在场上,还愿意承担,还能把自己交给比赛,故事就没有真正写完。",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
paragraphs: [
|
|
||||||
"看到 NiKo 跟 Falcons 捧起科隆 Major 冠军的时候,第一反应不是“他终于证明自己了”,而是“这个故事总算没有一直卡在遗憾那里”。",
|
|
||||||
"CS 圈里很少有人会怀疑 NiKo 的个人能力。那些年的精准定位、干拉、残局处理,还有他身上那种很固执的强者气质,早就把他放进了历史级步枪手的讨论里。可越是这样,Major 冠军这个缺口就越刺眼。天赋被看见了,巅峰被看见了,遗憾也被反复看见了。",
|
|
||||||
"所以这次夺冠最打动我的地方,不只是奖杯本身,而是一个选手在漫长职业生涯里终于把叙事拧回来了。第 17 次 Major,29 岁,换过队友,换过体系,也经历过足够多次“差一点”。这种冠军不是爽文式的轻松圆梦,更像是把很多年没说完的话,终于稳稳地放在了桌面上。",
|
|
||||||
"我挺喜欢这样的体育瞬间:它提醒人,很多东西不是只属于最年轻、最锋利、最顺风的时候。只要还在场上,还愿意承担,还能把自己交给比赛,故事就没有真正写完。",
|
|
||||||
],
|
|
||||||
} satisfies LifePost;
|
} satisfies LifePost;
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { nikoMajorChampion } from "./articles/niko-major-champion";
|
||||||
|
import { countLifePostParagraphs, getLifePostImages } from "./content";
|
||||||
|
import type { LifeContentBlock, LifePostImage } from "./types";
|
||||||
|
|
||||||
|
const inlineContent: LifeContentBlock[] = [
|
||||||
|
{ type: "paragraph", text: "first paragraph" },
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
|
src: "/life/demo/step.jpg",
|
||||||
|
alt: "Demo step",
|
||||||
|
caption: "A demo image inside the article.",
|
||||||
|
},
|
||||||
|
{ type: "paragraph", text: "second paragraph" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const images: LifePostImage[] = getLifePostImages(nikoMajorChampion);
|
||||||
|
const paragraphCount: number = countLifePostParagraphs(nikoMajorChampion);
|
||||||
|
const inlineImages: LifePostImage[] = getLifePostImages({
|
||||||
|
...nikoMajorChampion,
|
||||||
|
content: inlineContent,
|
||||||
|
});
|
||||||
|
const inlineParagraphCount: number = countLifePostParagraphs({
|
||||||
|
...nikoMajorChampion,
|
||||||
|
content: inlineContent,
|
||||||
|
});
|
||||||
|
|
||||||
|
void images;
|
||||||
|
void paragraphCount;
|
||||||
|
void inlineImages;
|
||||||
|
void inlineParagraphCount;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { LifePost, LifePostImage } from "./types";
|
||||||
|
|
||||||
|
export function getLifePostImages(post: LifePost): LifePostImage[] {
|
||||||
|
return post.content
|
||||||
|
.filter((block) => block.type === "image")
|
||||||
|
.map(({ src, alt, caption }) => ({ src, alt, caption }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLifePostCoverImage(post: LifePost): LifePostImage | undefined {
|
||||||
|
return getLifePostImages(post)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function countLifePostParagraphs(post: LifePost): number {
|
||||||
|
return post.content.filter((block) => block.type === "paragraph").length;
|
||||||
|
}
|
||||||
@@ -1,12 +1,22 @@
|
|||||||
|
export type LifePostImage = {
|
||||||
|
src: string;
|
||||||
|
alt: string;
|
||||||
|
caption?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LifeContentBlock =
|
||||||
|
| {
|
||||||
|
type: "paragraph";
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
| ({
|
||||||
|
type: "image";
|
||||||
|
} & LifePostImage);
|
||||||
|
|
||||||
export type LifePost = {
|
export type LifePost = {
|
||||||
slug: string;
|
slug: string;
|
||||||
date: string;
|
date: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
images: {
|
content: LifeContentBlock[];
|
||||||
src: string;
|
|
||||||
alt: string;
|
|
||||||
caption: string;
|
|
||||||
}[];
|
|
||||||
paragraphs: string[];
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user