PersonalWebApplication/Client/src/components/BlogPage.tsx

233 lines
9.1 KiB
TypeScript
Raw Normal View History

import { motion } from 'framer-motion'
import { useSystemStats } from '../hooks/useSystemStats'
import { fadeUp, stagger } from './motion'
import { AnnouncementBar } from './AnnouncementBar'
import { RainbowSpotlight } from './RainbowSpotlight'
interface BlogPageProps {
/** 当前登录用户名 */
username: string
/** 退出登录 */
onSignOut: () => void
}
interface Post {
title: string
date: string
readingTime: string
excerpt: string
tag: string
}
// 示例文章数据(占位)。将来可换成从后端 /api/web/posts 拉取。
const POSTS: Post[] = [
{
title: 'An Interactive Guide to CSS Gradients',
date: 'Jun 2026',
readingTime: '12 min',
excerpt:
'从线性到锥形渐变,拆解每个参数如何影响最终画面,并顺手做一道流动的彩虹。',
tag: 'CSS',
},
{
title: '用 Rust + axum 写一个会呼吸的监控后端',
date: 'May 2026',
readingTime: '9 min',
excerpt:
'采样与请求解耦、RwLock 快照、CORS 只拦浏览器——把顶部栏那行系统状态讲透。',
tag: 'Rust',
},
{
title: 'framer-motion 里那些恰到好处的微交互',
date: 'Apr 2026',
readingTime: '7 min',
excerpt: '光标跟随、入场编排、spring 手感。动效不是炫技,是把注意力放对地方。',
tag: 'Motion',
},
{
title: '为什么我给只有 20 个用户的系统拒绝了数据库',
date: 'Mar 2026',
readingTime: '6 min',
excerpt: '一个进程内 HashMap 就够了。聊聊在小规模下,简单是怎样跑赢“正确架构”的。',
tag: 'Backend',
},
]
// 两侧图片区的图片:用 Vite 的 import.meta.glob 自动索引 src/assets/sides/ 下的所有图。
// 走 bundler import —— 自带内容哈希、构建期校验、可优化;丢新图进该文件夹即生效,无需改代码。
// 按文件名升序取用:第 1 张 → 左栏,第 2 张 → 右栏。详见 src/assets/sides/README.md。
const sideImages = Object.entries(
import.meta.glob<string>('../assets/sides/*.{jpg,jpeg,png,webp}', {
eager: true,
query: '?url',
import: 'default',
}),
)
.sort(([pathA], [pathB]) => pathA.localeCompare(pathB))
.map(([, url]) => url)
const SIDE_IMAGE_LEFT = sideImages[0]
const SIDE_IMAGE_RIGHT = sideImages[1] ?? sideImages[0]
export function BlogPage({ username, onSignOut }: BlogPageProps) {
// 顶部栏沿用系统状态彩蛋,保持与登录页同一风格。
const stats = useSystemStats()
return (
<div className="relative flex min-h-svh flex-col">
{/* 整页 Lunada 渐变极光背景,固定铺满视口、置于所有内容之下 */}
<div className="aurora" aria-hidden />
<AnnouncementBar text={stats.text} online={stats.online} />
{/* 顶部彩虹栏:通栏,横跨左右图片区之上;底部一条流动彩虹线 */}
<header className="glass-sheet relative z-20 w-full">
<div className="mx-auto flex w-full max-w-6xl items-center justify-between px-6 py-5">
<span className="rainbow-text font-display text-lg font-semibold tracking-tight">
cc.dev
</span>
<div className="flex items-center gap-4 text-sm text-muted">
<span className="hidden sm:inline">
Signed in as <span className="text-text">{username}</span>
</span>
<button
onClick={onSignOut}
className="rounded-full border border-line px-4 py-1.5 text-xs font-medium uppercase tracking-[0.16em] text-muted transition-colors hover:border-accent hover:text-accent"
>
Sign out
</button>
</div>
</div>
<div className="rainbow-gradient h-1 w-full" aria-hidden />
</header>
{/* Hero 通栏:彩虹光标动效 + 大字标题 + 灵感来源,占满一整行(在三栏之上) */}
<section className="glass-sheet relative z-10 w-full overflow-hidden py-20 sm:py-28">
<RainbowSpotlight />
<motion.div
variants={stagger(0.15, 0.12)}
initial="hidden"
animate="show"
className="relative z-10 mx-auto flex w-full max-w-3xl flex-col gap-6 px-6"
>
<motion.span
variants={fadeUp}
className="text-xs font-medium uppercase tracking-[0.3em] text-accent"
>
Personal blog
</motion.span>
<motion.h1
variants={fadeUp}
className="font-display text-5xl font-medium leading-[1.05] text-text sm:text-6xl lg:text-7xl"
>
Hello, I'm <span className="rainbow-text italic">cc</span>
<span className="cursor-bar ml-1 inline-block not-italic text-accent">_</span>
</motion.h1>
<motion.p
variants={fadeUp}
className="max-w-xl text-base leading-relaxed text-muted"
>
Rust
{' '}
<a
href="https://www.joshwcomeau.com/"
target="_blank"
rel="noreferrer"
className="text-accent underline-offset-4 hover:underline"
>
Josh W. Comeau
</a>
</motion.p>
</motion.div>
</section>
{/* · () ·
max-w-3xl flex-1 */}
<div className="relative flex flex-1 justify-center">
{/* ()()
Lunada */}
<aside
className="relative hidden flex-1 overflow-hidden bg-center bg-no-repeat lg:block"
style={{ backgroundImage: `url(${SIDE_IMAGE_LEFT})`, backgroundSize: 'auto 100%' }}
aria-hidden
>
{/* 液态玻璃覆层 */}
<div className="glass-image absolute inset-0" />
</aside>
{/* 中间内容纸(玻璃纸张) */}
<div className="glass-sheet relative flex w-full max-w-3xl flex-col">
<main className="flex-1">
{/* 文章列表 */}
<section className="w-full px-6 py-14">
<h2 className="mb-10 text-xs font-medium uppercase tracking-[0.3em] text-muted">
Latest writing
</h2>
<motion.ul
variants={stagger(0.1, 0.08)}
initial="hidden"
animate="show"
className="flex flex-col"
>
{POSTS.map((post) => (
<motion.li key={post.title} variants={fadeUp}>
<a
href="#"
className="group flex flex-col gap-3 border-b border-line/50 py-7 transition-colors sm:flex-row sm:items-baseline sm:gap-8"
>
{/* 左侧彩虹竖条hover 时出现 */}
<span
aria-hidden
className="rainbow-gradient hidden w-1 self-stretch rounded-full opacity-0 transition-opacity duration-300 group-hover:opacity-100 sm:block"
/>
<div className="w-28 shrink-0 font-mono text-xs uppercase tracking-wider text-muted">
<div>{post.date}</div>
<div className="mt-1 text-accent/70">{post.tag}</div>
</div>
<div className="flex-1">
<h3 className="rainbow-clip font-display text-xl font-medium text-text transition-colors duration-200 group-hover:text-transparent">
{post.title}
</h3>
<p className="mt-2 text-sm leading-relaxed text-muted">
{post.excerpt}
</p>
<span className="mt-2 inline-block text-xs text-muted/70">
{post.readingTime} read
</span>
</div>
</a>
</motion.li>
))}
</motion.ul>
</section>
</main>
{/* 页脚:一条流动彩虹装饰线 */}
<footer className="mt-auto">
<div className="rainbow-gradient h-1 w-full" />
<div className="flex w-full items-center justify-between px-6 py-8 text-xs text-muted">
<span>© {new Date().getFullYear()} cc</span>
<span className="font-mono tracking-wider">built with React · Rust · 🌈</span>
</div>
</footer>
</div>
{/* 右侧图片区(与左侧同样的高度适配 + 居中 + 液态玻璃覆层) */}
<aside
className="relative hidden flex-1 overflow-hidden bg-center bg-no-repeat lg:block"
style={{ backgroundImage: `url(${SIDE_IMAGE_RIGHT})`, backgroundSize: 'auto 100%' }}
aria-hidden
>
{/* 液态玻璃覆层 */}
<div className="glass-image absolute inset-0" />
</aside>
</div>
</div>
)
}