️ feat: Refactor API client to support server-side and client-side configurations

fix: Update post fetching logic to use dynamic ID instead of hardcoded value

feat: Enhance layout with animated transitions using framer-motion

refactor: Remove old post and user page implementations, introduce new structure

feat: Implement sidebar components for blog home with dynamic content

feat: Create blog post component with wave header and metadata display

feat: Add responsive sidebar menu for navigation on mobile devices

chore: Introduce reusable sheet component for modal-like functionality
This commit is contained in:
2025-07-25 06:18:24 +08:00
parent a76f03038c
commit c565b5b5ef
17 changed files with 824 additions and 241 deletions

View File

@ -1,18 +1,35 @@
import { Navbar } from "@/components/navbar";
"use client";
import { Navbar } from "@/components/navbar";
import { AnimatePresence, motion } from "framer-motion";
import { usePathname } from "next/navigation";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const pathname = usePathname();
return (
<>
<header className="flex justify-center">
<header className="fixed top-0 left-0 w-full z-50 bg-white/80 dark:bg-slate-900/80 backdrop-blur flex justify-center border-b border-slate-200 dark:border-slate-800">
<Navbar />
</header>
<main>
{children}
</main>
<AnimatePresence mode="wait">
<motion.main
key={pathname}
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 16 }}
transition={{
type: "tween",
ease: "easeOut",
duration: 0.18
}}
className="pt-16"
>
{children}
</motion.main>
</AnimatePresence>
</>
);
}

View File

@ -0,0 +1,13 @@
import { getPostById } from "@/api/post";
import BlogPost from "@/components/blog-post";
interface Props {
params: Promise<{ id: string }>
}
export default async function PostPage({ params }: Props) {
const { id } = await params;
const post = await getPostById(id);
if (!post) return <div></div>;
return <BlogPost post={post} />;
}

View File

@ -0,0 +1,11 @@
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<>
{children}
</>
);
}

View File

@ -1,82 +0,0 @@
import { getPostById } from "@/api/post";
import { notFound } from "next/navigation";
interface Props {
params: Promise<{
id: string;
}>
}
export default async function PostPage({ params }: Props) {
const { id } = await params;
try {
const post = await getPostById(id);
if (!post) {
notFound();
}
return (
<article className="max-w-4xl mx-auto px-4 py-8">
<header className="mb-8">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="text-gray-600 mb-4">
<time>{new Date(post.createdAt).toLocaleDateString()}</time>
<span className="mx-2">·</span>
<span>: {post.viewCount || 0}</span>
</div>
</header>
<div className="prose max-w-none">
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</div>
{post.labels && post.labels.length > 0 && (
<footer className="mt-8 pt-8 border-t">
<div className="flex flex-wrap gap-2">
{post.labels.map((label) => (
<span key={label.id} className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm">
#{label.key}
</span>
))}
</div>
</footer>
)}
</article>
);
} catch (error) {
console.error("Failed to fetch post:", error);
notFound();
}
}
// 生成元数据
export async function generateMetadata({ params }: Props) {
const { id } = await params;
try {
const post = await getPostById(id);
if (!post) {
return {
title: '文章未找到',
};
}
return {
title: post.title,
description: post.content?.substring(0, 160),
openGraph: {
title: post.title,
description: post.content?.substring(0, 160),
type: 'article',
publishedTime: post.createdAt,
},
};
} catch (error) {
return {
title: '文章未找到',
};
}
}