164 lines
7.2 KiB
TypeScript
164 lines
7.2 KiB
TypeScript
import { prisma } from "@/lib/prisma";
|
|
import { getServerSession } from "next-auth";
|
|
import { authOptions } from "@/lib/auth";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
async function getHotTopics() {
|
|
return prisma.forumTopic.findMany({
|
|
where: {},
|
|
include: {
|
|
category: { select: { name: true, slug: true, icon: true } },
|
|
user: { select: { id: true, name: true, email: true, avatarUrl: true, level: true, points: true } },
|
|
_count: { select: { posts: true } },
|
|
},
|
|
orderBy: [{ isPinned: "desc" }, { createdAt: "desc" }],
|
|
take: 5,
|
|
});
|
|
}
|
|
|
|
async function getActiveUsers() {
|
|
return prisma.user.findMany({
|
|
where: {
|
|
OR: [
|
|
{ forumTopics: { some: {} } },
|
|
{ forumPosts: { some: {} } },
|
|
],
|
|
},
|
|
select: { id: true, name: true, email: true, avatarUrl: true, level: true, points: true },
|
|
take: 5,
|
|
});
|
|
}
|
|
|
|
export default async function CommunityPage() {
|
|
const categories = await prisma.forumCategory.findMany({
|
|
where: { parentId: null },
|
|
include: {
|
|
children: {
|
|
orderBy: [{ sortOrder: "asc" }],
|
|
},
|
|
},
|
|
orderBy: [{ sortOrder: "asc" }, { createdAt: "desc" }],
|
|
});
|
|
|
|
const [hotTopics, activeUsers] = await Promise.all([getHotTopics(), getActiveUsers()]);
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#f8fafc]">
|
|
<div className="max-w-[1440px] mx-auto px-4 py-8">
|
|
<div className="mb-8">
|
|
<h1 className="text-2xl font-bold">追光会</h1>
|
|
<p className="mt-1 text-sm text-muted-foreground">AI爱好者交流社区</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<div className="lg:col-span-2 space-y-6">
|
|
<div>
|
|
<h2 className="text-lg font-semibold mb-4">板块</h2>
|
|
<div className="space-y-4">
|
|
{categories.map((cat) => (
|
|
<div key={cat.id}>
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/5 text-lg">
|
|
{cat.icon || "📌"}
|
|
</div>
|
|
<h3 className="text-sm font-semibold">{cat.name}</h3>
|
|
{cat.description && (
|
|
<span className="text-xs text-muted-foreground/40">{cat.description}</span>
|
|
)}
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 ml-11">
|
|
{cat.children.map((child) => (
|
|
<div
|
|
key={child.id}
|
|
className="flex items-center gap-3 p-3 rounded-lg border border-border/30 bg-white/60 transition-all hover:border-primary/30 hover:bg-white cursor-pointer group"
|
|
onClick={() => (window.location.href = `/community/${child.slug}`)}
|
|
>
|
|
<span className="text-lg">{child.icon || "📌"}</span>
|
|
<div className="min-w-0 flex-1">
|
|
<h4 className="text-xs font-medium group-hover:text-primary transition-colors truncate">{child.name}</h4>
|
|
{child.description && (
|
|
<p className="text-[10px] text-muted-foreground/50 truncate">{child.description}</p>
|
|
)}
|
|
</div>
|
|
<span className="text-[10px] text-muted-foreground/30">{child.topicCount}话题</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h2 className="text-lg font-semibold mb-4">最新话题</h2>
|
|
<div className="space-y-3">
|
|
{hotTopics.map((topic) => {
|
|
const initial = topic.user.name?.charAt(0).toUpperCase() || "?";
|
|
return (
|
|
<div
|
|
key={topic.id}
|
|
className="flex items-start gap-3 p-4 rounded-xl border border-border/40 bg-white transition-all hover:border-border/60 cursor-pointer"
|
|
onClick={() => (window.location.href = `/community/topic/${topic.id}`)}
|
|
>
|
|
{topic.user.avatarUrl ? (
|
|
<img src={topic.user.avatarUrl} alt="" className="h-8 w-8 rounded-full border border-border/40 object-cover" />
|
|
) : (
|
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-xs font-semibold text-primary">
|
|
{initial}
|
|
</div>
|
|
)}
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-center gap-2">
|
|
{topic.isPinned && (
|
|
<span className="text-[10px] font-medium text-primary bg-primary/10 px-1.5 py-0.5 rounded-full">置顶</span>
|
|
)}
|
|
<h3 className="text-sm font-medium hover:text-primary transition-colors truncate">
|
|
{topic.title}
|
|
</h3>
|
|
</div>
|
|
<div className="mt-1 flex items-center gap-3 text-xs text-muted-foreground/50">
|
|
<span>{topic.user.name || topic.user.email.split("@")[0]}</span>
|
|
<span>{topic.category.name}</span>
|
|
<span>{topic._count.posts}回复</span>
|
|
<span>{topic.viewCount}浏览</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
<div className="rounded-xl border border-border/50 bg-white p-5 shadow-[0_1px_2px_rgba(0,0,0,0.04)]">
|
|
<h3 className="text-sm font-semibold mb-4">活跃用户</h3>
|
|
<div className="space-y-3">
|
|
{activeUsers.map((user) => {
|
|
const initial = user.name?.charAt(0).toUpperCase() || "?";
|
|
return (
|
|
<div key={user.id} className="flex items-center gap-3">
|
|
{user.avatarUrl ? (
|
|
<img src={user.avatarUrl} alt="" className="h-8 w-8 rounded-full border border-border/40 object-cover" />
|
|
) : (
|
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-xs font-semibold text-primary">
|
|
{initial}
|
|
</div>
|
|
)}
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-sm font-medium truncate">{user.name || user.email.split("@")[0]}</p>
|
|
<p className="text-xs text-muted-foreground/50">{user.points}分</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|