mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-26 11:06:23 +00:00
- Replaced static config with dynamic site info context. - Updated color scheme handling in various components to use site info. - Removed deprecated config file and integrated site info fetching. - Enhanced user preference page to allow color scheme selection. - Adjusted blog and console components to reflect new site info structure. - Improved error handling and fallback mechanisms for site info retrieval.
165 lines
6.2 KiB
TypeScript
165 lines
6.2 KiB
TypeScript
"use client"
|
|
|
|
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/blog-sidebar-card";
|
|
import type { Post } from "@/models/post";
|
|
import { listPosts } from "@/api/post";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { motion } from "motion/react";
|
|
import { useTranslations } from "next-intl";
|
|
import { useSearchParams } from "next/navigation";
|
|
import { OrderBy } from "@/models/common";
|
|
import { PaginationController } from "@/components/common/pagination";
|
|
import { QueryKey } from "@/constant";
|
|
import { useStoredState } from "@/hooks/use-storage-state";
|
|
import { parseAsInteger, useQueryState } from "nuqs";
|
|
import { useSiteInfo } from "@/contexts/site-info-context";
|
|
|
|
// 定义排序类型
|
|
enum SortBy {
|
|
Latest = 'latest',
|
|
Hottest = 'hottest',
|
|
}
|
|
|
|
const DEFAULT_SORTBY: SortBy = SortBy.Latest;
|
|
|
|
export default function BlogHome() {
|
|
// 从路由查询参数中获取页码和标签们
|
|
const t = useTranslations("BlogHome");
|
|
const {siteInfo} = useSiteInfo();
|
|
const [labels, setLabels] = useState<string[]>([]);
|
|
const [keywords, setKeywords] = useState<string[]>([]);
|
|
const [page, setPage] = useQueryState("page", parseAsInteger.withDefault(1).withOptions({ history: "replace", clearOnDefault: true }));
|
|
const [posts, setPosts] = useState<Post[]>([]);
|
|
const [totalPosts, setTotalPosts] = useState(0);
|
|
const [loading, setLoading] = useState(false);
|
|
const [sortBy, setSortBy, isSortByLoaded] = useStoredState<SortBy>(QueryKey.SortBy, DEFAULT_SORTBY);
|
|
|
|
useEffect(() => {
|
|
if (!isSortByLoaded) return;
|
|
setLoading(true);
|
|
listPosts(
|
|
{
|
|
page,
|
|
size: siteInfo.postsPerPage || 9,
|
|
orderBy: sortBy === SortBy.Latest ? OrderBy.CreatedAt : OrderBy.Heat,
|
|
desc: true,
|
|
keywords: keywords.join(",") || undefined,
|
|
labels: labels.join(",") || undefined,
|
|
}
|
|
).then(res => {
|
|
setPosts(res.data.posts);
|
|
setTotalPosts(res.data.total);
|
|
setLoading(false);
|
|
}).catch(err => {
|
|
console.error(err);
|
|
setLoading(false);
|
|
});
|
|
}, [keywords, labels, page, sortBy, isSortByLoaded]);
|
|
|
|
const handleSortChange = (type: SortBy) => {
|
|
if (sortBy !== type) {
|
|
setSortBy(type);
|
|
setPage(1);
|
|
}
|
|
};
|
|
|
|
const handlePageChange = (page: number) => {
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
setPage(page);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{/* 主内容区域 */}
|
|
<section className="py-16">
|
|
{/* 容器 - 关键布局 */}
|
|
<div className="">
|
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
|
{/* 主要内容区域 */}
|
|
<motion.div
|
|
className="lg:col-span-3 self-start"
|
|
initial={{ y: 40, opacity: 0 }}
|
|
animate={{ y: 0, opacity: 1 }}
|
|
transition={{ duration: siteInfo.animationDurationSecond, ease: "easeOut" }}>
|
|
{/* 文章列表标题 */}
|
|
<div className="flex items-center justify-between mb-8">
|
|
<h2 className="text-3xl font-bold text-slate-900 dark:text-slate-100">
|
|
{sortBy === 'latest' ? t("latest_posts") : t("hottest_posts")}
|
|
{posts.length > 0 && (
|
|
<span className="text-xl font-normal text-slate-500 ml-2">
|
|
({posts.length})
|
|
</span>
|
|
)}
|
|
</h2>
|
|
{/* 排序按钮组 */}
|
|
{isSortByLoaded && <div className="flex items-center gap-2">
|
|
<Button
|
|
variant={sortBy === SortBy.Latest ? 'default' : 'outline'}
|
|
size="sm"
|
|
onClick={() => handleSortChange(SortBy.Latest)}
|
|
disabled={loading}
|
|
className="transition-all duration-200"
|
|
>
|
|
<Clock className="w-4 h-4 mr-2" />
|
|
{t("latest")}
|
|
</Button>
|
|
<Button
|
|
variant={sortBy === 'hottest' ? 'default' : 'outline'}
|
|
size="sm"
|
|
onClick={() => handleSortChange(SortBy.Hottest)}
|
|
disabled={loading}
|
|
className="transition-all duration-200"
|
|
>
|
|
<TrendingUp className="w-4 h-4 mr-2" />
|
|
{t("hottest")}
|
|
</Button>
|
|
</div>}
|
|
</div>
|
|
{/* 博客卡片网格 */}
|
|
<BlogCardGrid posts={posts} isLoading={loading} showPrivate={true} />
|
|
{/* 分页控制器 */}
|
|
<div className="mt-8">
|
|
{totalPosts > 0 && <PaginationController
|
|
className="pt-4 flex justify-center"
|
|
pageSize={siteInfo.postsPerPage}
|
|
initialPage={page}
|
|
total={totalPosts}
|
|
onPageChange={handlePageChange}
|
|
/>}
|
|
</div>
|
|
{/* 加载状态指示器 */}
|
|
{loading && (
|
|
<div className="text-center py-8">
|
|
<div className="inline-flex items-center gap-2 text-slate-600">
|
|
<div className="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
|
|
<span>{t("loading")}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</motion.div>
|
|
{/* 侧边栏 */}
|
|
<motion.div
|
|
initial={{ x: 80, opacity: 0 }}
|
|
animate={{ x: 0, y: 0, opacity: 1 }}
|
|
transition={{ duration: siteInfo.animationDurationSecond, ease: "easeOut" }}
|
|
>
|
|
<Sidebar
|
|
cards={[
|
|
<SidebarAbout key="about" />,
|
|
posts.length > 0 ? <SidebarHotPosts key="hot" posts={posts} sortType={sortBy} /> : null,
|
|
<SidebarTags key="tags" labels={[]} />,
|
|
<SidebarMisskeyIframe key="misskey" />,
|
|
].filter(Boolean)}
|
|
/>
|
|
</motion.div>
|
|
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</>
|
|
);
|
|
} |