From 92c2a58e801546c4808296d1fe8c898a93bf7691 Mon Sep 17 00:00:00 2001 From: Snowykami Date: Wed, 30 Jul 2025 00:18:32 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Implement=20blog=20home=20a?= =?UTF-8?q?nd=20post=20components=20with=20sidebar=20and=20pagination?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added BlogHome component for displaying posts with sorting options (latest/popular). - Integrated Sidebar with About, Hot Posts, Tags, and Misskey Iframe components. - Created BlogPost component to render individual posts with metadata and content. - Developed GravatarAvatar component for user avatars. - Implemented Markdown rendering with syntax highlighting and custom code blocks. - Added pagination component for navigating through posts. - Enhanced login form with OpenID Connect options and email/password authentication. - Utility functions for generating post URLs and calculating reading time. --- web/src/app/(main)/layout.tsx | 4 +- web/src/app/(main)/p/[id]/page.tsx | 2 +- web/src/app/(main)/page.tsx | 2 +- web/src/app/globals.css | 2 +- web/src/app/login/page.tsx | 2 +- .../{ => blog-comment}/blog-comment.tsx | 0 .../blog-home-card.tsx} | 7 +- .../components/{ => blog-home}/blog-home.tsx | 5 +- .../components/{ => blog-post}/blog-post.tsx | 10 +- .../blog-sidebar-card.tsx} | 21 +-- web/src/components/{ => common}/gravatar.tsx | 0 .../{ => common}/markdown-codeblock.tsx | 0 web/src/components/{ => common}/markdown.tsx | 2 +- web/src/components/{ => layout}/footer.tsx | 0 web/src/components/{ => layout}/navbar.tsx | 8 +- web/src/components/{ => login}/login-form.tsx | 0 web/src/components/post-page.tsx | 0 web/src/components/scroll-to-top.client.tsx | 11 -- web/src/components/sidebar.tsx | 0 web/src/components/ui/button.tsx | 43 +++--- web/src/components/ui/pagination.tsx | 127 ++++++++++++++++++ web/src/config.ts | 1 + web/src/utils/common/post.ts | 12 ++ 23 files changed, 196 insertions(+), 63 deletions(-) rename web/src/components/{ => blog-comment}/blog-comment.tsx (100%) rename web/src/components/{blog-card.tsx => blog-home/blog-home-card.tsx} (98%) rename web/src/components/{ => blog-home}/blog-home.tsx (97%) rename web/src/components/{ => blog-post}/blog-post.tsx (93%) rename web/src/components/{blog-home-sidebar.tsx => blog/blog-sidebar-card.tsx} (85%) rename web/src/components/{ => common}/gravatar.tsx (100%) rename web/src/components/{ => common}/markdown-codeblock.tsx (100%) rename web/src/components/{ => common}/markdown.tsx (97%) rename web/src/components/{ => layout}/footer.tsx (100%) rename web/src/components/{ => layout}/navbar.tsx (96%) rename web/src/components/{ => login}/login-form.tsx (100%) delete mode 100644 web/src/components/post-page.tsx delete mode 100644 web/src/components/scroll-to-top.client.tsx delete mode 100644 web/src/components/sidebar.tsx create mode 100644 web/src/components/ui/pagination.tsx create mode 100644 web/src/utils/common/post.ts diff --git a/web/src/app/(main)/layout.tsx b/web/src/app/(main)/layout.tsx index 193cf07..374a8bf 100644 --- a/web/src/app/(main)/layout.tsx +++ b/web/src/app/(main)/layout.tsx @@ -2,9 +2,9 @@ import { motion } from 'framer-motion' import { usePathname } from 'next/navigation' -import { Navbar } from '@/components/navbar' +import { Navbar } from '@/components/layout/navbar' import { BackgroundProvider } from '@/contexts/background-context' -import Footer from '@/components/footer' +import Footer from '@/components/layout/footer' export default function RootLayout({ children, diff --git a/web/src/app/(main)/p/[id]/page.tsx b/web/src/app/(main)/p/[id]/page.tsx index 576326d..c744132 100644 --- a/web/src/app/(main)/p/[id]/page.tsx +++ b/web/src/app/(main)/p/[id]/page.tsx @@ -1,6 +1,6 @@ import { getPostById } from '@/api/post' import { cookies } from 'next/headers' -import BlogPost from '@/components/blog-post' +import BlogPost from '@/components/blog-post/blog-post' interface Props { params: Promise<{ id: string }> diff --git a/web/src/app/(main)/page.tsx b/web/src/app/(main)/page.tsx index 2a48e70..fb25ce7 100644 --- a/web/src/app/(main)/page.tsx +++ b/web/src/app/(main)/page.tsx @@ -1,6 +1,6 @@ "use client"; -import BlogHome from "@/components/blog-home"; +import BlogHome from "@/components/blog-home/blog-home"; export default function Page(){ diff --git a/web/src/app/globals.css b/web/src/app/globals.css index e1a6217..ae9755e 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -122,7 +122,7 @@ } html, body { - transition: background-color 0.3s, color 0.3s !important; + transition: background-color 0.2s !important; } .sonner-toast { diff --git a/web/src/app/login/page.tsx b/web/src/app/login/page.tsx index 67b66fc..562f99a 100644 --- a/web/src/app/login/page.tsx +++ b/web/src/app/login/page.tsx @@ -1,6 +1,6 @@ import Image from 'next/image' import { Suspense } from 'react' -import { LoginForm } from '@/components/login-form' +import { LoginForm } from '@/components/login/login-form' import config from '@/config' function LoginPageContent() { diff --git a/web/src/components/blog-comment.tsx b/web/src/components/blog-comment/blog-comment.tsx similarity index 100% rename from web/src/components/blog-comment.tsx rename to web/src/components/blog-comment/blog-comment.tsx diff --git a/web/src/components/blog-card.tsx b/web/src/components/blog-home/blog-home-card.tsx similarity index 98% rename from web/src/components/blog-card.tsx rename to web/src/components/blog-home/blog-home-card.tsx index a7af090..a1da558 100644 --- a/web/src/components/blog-card.tsx +++ b/web/src/components/blog-home/blog-home-card.tsx @@ -6,6 +6,7 @@ import { Badge } from '@/components/ui/badge' import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' import config from '@/config' import { cn } from '@/lib/utils' +import { getPostHref } from '@/utils/common/post' interface BlogCardProps { post: Post @@ -13,7 +14,6 @@ interface BlogCardProps { } export function BlogCard({ post, className }: BlogCardProps) { - // 格式化日期 const formatDate = (dateString: string) => { const date = new Date(dateString) return date.toLocaleDateString('zh-CN', { @@ -22,9 +22,6 @@ export function BlogCard({ post, className }: BlogCardProps) { day: '2-digit', }) } - - // TODO: 阅读时间估计 - return ( {filteredPosts.map(post => ( - + ))} diff --git a/web/src/components/blog-home.tsx b/web/src/components/blog-home/blog-home.tsx similarity index 97% rename from web/src/components/blog-home.tsx rename to web/src/components/blog-home/blog-home.tsx index be27d4d..ebaff47 100644 --- a/web/src/components/blog-home.tsx +++ b/web/src/components/blog-home/blog-home.tsx @@ -1,9 +1,9 @@ "use client"; -import { BlogCardGrid } from "@/components/blog-card"; +import { BlogCardGrid } from "@/components/blog-home/blog-home-card"; import { Button } from "@/components/ui/button"; import { TrendingUp, Clock, } from "lucide-react"; -import Sidebar, { SidebarAbout, SidebarHotPosts, SidebarMisskeyIframe, SidebarTags } from "./blog-home-sidebar"; +import Sidebar, { SidebarAbout, SidebarHotPosts, SidebarMisskeyIframe, SidebarTags } from "../blog/blog-sidebar-card"; import config from '@/config'; import type { Label } from "@/models/label"; import type { Post } from "@/models/post"; @@ -22,7 +22,6 @@ export default function BlogHome() { const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(false); const [sortType, setSortType, sortTypeLoaded] = useStoredState(POST_SORT_TYPE, 'latest'); - // 根据排序类型和防抖后的搜索关键词获取文章 useEffect(() => { if (!sortTypeLoaded) return; const fetchPosts = async () => { diff --git a/web/src/components/blog-post.tsx b/web/src/components/blog-post/blog-post.tsx similarity index 93% rename from web/src/components/blog-post.tsx rename to web/src/components/blog-post/blog-post.tsx index b9dd520..9f38f52 100644 --- a/web/src/components/blog-post.tsx +++ b/web/src/components/blog-post/blog-post.tsx @@ -1,9 +1,10 @@ import { Suspense } from "react"; import type { Post } from "@/models/post"; import { Calendar, Clock, FileText, Flame, Heart, MessageCircle, PenLine, SquarePen } from "lucide-react"; -import ScrollToTop from "@/components/scroll-to-top.client"; -import { RenderMarkdown } from "@/components/markdown"; +import { RenderMarkdown } from "@/components/common/markdown"; import { isMobileByUA } from "@/utils/server/device"; +import { calculateReadingTime } from "@/utils/common/post"; +import Link from "next/link"; function PostMeta({ post }: { post: Post }) { return ( @@ -21,7 +22,7 @@ function PostMeta({ post }: { post: Post }) { {/* 阅读时间 */} - {Math.ceil(post.content.length / 400 || 1)} 分钟 + {calculateReadingTime(post.content)} 分钟 {/* 发布时间 */} @@ -74,6 +75,9 @@ async function PostHeader({ post }: { post: Post }) { {post.isOriginal && ( 原创 + + 查看 + )} {(post.labels || []).map(label => ( diff --git a/web/src/components/blog-home-sidebar.tsx b/web/src/components/blog/blog-sidebar-card.tsx similarity index 85% rename from web/src/components/blog-home-sidebar.tsx rename to web/src/components/blog/blog-sidebar-card.tsx index 1829053..8cbe5f2 100644 --- a/web/src/components/blog-home-sidebar.tsx +++ b/web/src/components/blog/blog-sidebar-card.tsx @@ -1,12 +1,14 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Heart, TrendingUp, Eye } from "lucide-react"; -import GravatarAvatar from "./gravatar"; +import GravatarAvatar from "@/components/common/gravatar"; import { Badge } from "@/components/ui/badge"; import type { Label } from "@/models/label"; import type { Post } from "@/models/post"; import type configType from '@/config'; import { useEffect, useState } from "react"; import { useTranslations } from "next-intl"; +import Link from "next/link"; +import { getPostHref } from "@/utils/common/post"; // 侧边栏父组件,接收卡片组件列表 export default function Sidebar({ cards }: { cards: React.ReactNode[] }) { @@ -57,12 +59,13 @@ export function SidebarHotPosts({ posts, sortType }: { posts: Post[], sortType: {posts.slice(0, 3).map((post, index) => ( -
- - {index + 1} - -
-

+ +
+ + {index + 1} + +
+

{post.title}

@@ -77,7 +80,9 @@ export function SidebarHotPosts({ posts, sortType }: { posts: Post[], sortType:
- ))} + + + ))} ); diff --git a/web/src/components/gravatar.tsx b/web/src/components/common/gravatar.tsx similarity index 100% rename from web/src/components/gravatar.tsx rename to web/src/components/common/gravatar.tsx diff --git a/web/src/components/markdown-codeblock.tsx b/web/src/components/common/markdown-codeblock.tsx similarity index 100% rename from web/src/components/markdown-codeblock.tsx rename to web/src/components/common/markdown-codeblock.tsx diff --git a/web/src/components/markdown.tsx b/web/src/components/common/markdown.tsx similarity index 97% rename from web/src/components/markdown.tsx rename to web/src/components/common/markdown.tsx index 30fca9c..4abbc47 100644 --- a/web/src/components/markdown.tsx +++ b/web/src/components/common/markdown.tsx @@ -3,7 +3,7 @@ import { MDXRemote, MDXRemoteProps } from "next-mdx-remote-client/rsc"; import { Suspense } from "react"; import rehypeHighlight from "rehype-highlight"; import "highlight.js/styles/github.css"; // 你可以换成喜欢的主题 -import CodeBlock from "@/components/markdown-codeblock"; +import CodeBlock from "@/components/common/markdown-codeblock"; export const markdownComponents = { h1: (props: React.ComponentPropsWithoutRef<"h1">) => ( diff --git a/web/src/components/footer.tsx b/web/src/components/layout/footer.tsx similarity index 100% rename from web/src/components/footer.tsx rename to web/src/components/layout/footer.tsx diff --git a/web/src/components/navbar.tsx b/web/src/components/layout/navbar.tsx similarity index 96% rename from web/src/components/navbar.tsx rename to web/src/components/layout/navbar.tsx index 81bfad1..9b90906 100644 --- a/web/src/components/navbar.tsx +++ b/web/src/components/layout/navbar.tsx @@ -12,13 +12,13 @@ import { NavigationMenuTrigger, navigationMenuTriggerStyle, } from "@/components/ui/navigation-menu" -import GravatarAvatar from "@/components/gravatar" +import GravatarAvatar from "@/components/common/gravatar" import { useDevice } from "@/contexts/device-context" import config from "@/config" import { useState } from "react" import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "@/components/ui/sheet" import { Menu } from "lucide-react" -import { Switch } from "./ui/switch" +import { Switch } from "../ui/switch" const navbarMenuComponents = [ { @@ -72,13 +72,13 @@ function NavMenuCenter() { {item.href ? ( - {item.title} + {item.title} ) : item.children ? ( <> - {item.title} + {item.title}