refactor(community): extract comment store and post/comment API adapters

This commit is contained in:
zetaloop
2026-02-23 11:04:40 +08:00
parent 8e62b15403
commit 77d23d0c9d
9 changed files with 201 additions and 63 deletions
+6 -9
View File
@@ -1,14 +1,15 @@
import { ArrowLeft, MessageCircle, Pin, Star } from "lucide-react" import { ArrowLeft, Pin, Star } from "lucide-react"
import Image from "next/image" import Image from "next/image"
import Link from "next/link" import Link from "next/link"
import { notFound } from "next/navigation" import { notFound } from "next/navigation"
import { PostCommentCount } from "@/components/post-comment-count"
import { PostComments } from "@/components/post-comments" import { PostComments } from "@/components/post-comments"
import { PostLikeButton } from "@/components/post-like-button" import { PostLikeButton } from "@/components/post-like-button"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader } from "@/components/ui/card" import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator"
import { getOrderById, getPlayerById, getPostById, listCommentsByPost } from "@/lib/api" import { getOrderById, getPlayerById, getPostById } from "@/lib/api"
import { roleLabels } from "@/lib/constants" import { roleLabels } from "@/lib/constants"
export default async function PostDetailPage({ params }: { params: Promise<{ id: string }> }) { export default async function PostDetailPage({ params }: { params: Promise<{ id: string }> }) {
@@ -16,7 +17,6 @@ export default async function PostDetailPage({ params }: { params: Promise<{ id:
const post = getPostById(id) const post = getPostById(id)
if (!post) notFound() if (!post) notFound()
const comments = listCommentsByPost(id)
const linkedOrder = post.linkedOrderId ? getOrderById(post.linkedOrderId) : null const linkedOrder = post.linkedOrderId ? getOrderById(post.linkedOrderId) : null
const linkedPlayer = linkedOrder ? getPlayerById(linkedOrder.playerId) : null const linkedPlayer = linkedOrder ? getPlayerById(linkedOrder.playerId) : null
@@ -91,18 +91,15 @@ export default async function PostDetailPage({ params }: { params: Promise<{ id:
)} )}
<div className="flex items-center gap-4 text-sm text-muted-foreground pt-2"> <div className="flex items-center gap-4 text-sm text-muted-foreground pt-2">
<PostLikeButton initialLiked={post.liked} initialCount={post.likeCount} /> <PostLikeButton postId={post.id} />
<span className="flex items-center gap-1"> <PostCommentCount postId={post.id} />
<MessageCircle className="h-4 w-4" />
{post.commentCount}
</span>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Separator className="my-6" /> <Separator className="my-6" />
<PostComments initialComments={comments} postId={id} /> <PostComments postId={id} />
</div> </div>
) )
} }
+21
View File
@@ -0,0 +1,21 @@
"use client"
import { MessageCircle } from "lucide-react"
import { useCommentStore } from "@/store/comments"
interface PostCommentCountProps {
postId: string
}
export function PostCommentCount({ postId }: PostCommentCountProps) {
const count = useCommentStore(
(state) => state.comments.filter((comment) => comment.postId === postId).length,
)
return (
<span className="flex items-center gap-1">
<MessageCircle className="h-4 w-4" />
{count}
</span>
)
}
+12 -35
View File
@@ -1,25 +1,26 @@
"use client" "use client"
import { Heart } from "lucide-react" import { Heart } from "lucide-react"
import { useState } from "react" import { useMemo, useState } from "react"
import { generateId } from "@/lib/id" import { addComment, toggleCommentLike } from "@/lib/api/comments"
import type { Comment } from "@/lib/types"
import { useRequireAuth } from "@/lib/use-require-auth" import { useRequireAuth } from "@/lib/use-require-auth"
import { useAuthStore } from "@/store/auth" import { useCommentStore } from "@/store/comments"
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar" import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"
import { Button } from "./ui/button" import { Button } from "./ui/button"
import { Textarea } from "./ui/textarea" import { Textarea } from "./ui/textarea"
interface PostCommentsProps { interface PostCommentsProps {
initialComments: Comment[]
postId: string postId: string
} }
export function PostComments({ initialComments, postId }: PostCommentsProps) { export function PostComments({ postId }: PostCommentsProps) {
const [comments, setComments] = useState(initialComments) const allComments = useCommentStore((state) => state.comments)
const comments = useMemo(
() => allComments.filter((comment) => comment.postId === postId),
[allComments, postId],
)
const [content, setContent] = useState("") const [content, setContent] = useState("")
const { requireAuth } = useRequireAuth() const { requireAuth } = useRequireAuth()
const user = useAuthStore((s) => s.user)
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@@ -32,20 +33,8 @@ export function PostComments({ initialComments, postId }: PostCommentsProps) {
requireAuth(() => { requireAuth(() => {
const nextContent = content.trim() const nextContent = content.trim()
if (!nextContent) return if (!nextContent) return
if (!user) return const decision = addComment(postId, nextContent)
const createdAt = new Date().toISOString() if (!decision.ok) return
setComments((prev) => [
...prev,
{
id: generateId(`comment-${postId}`),
postId,
author: user,
content: nextContent,
likeCount: 0,
liked: false,
createdAt,
},
])
setContent("") setContent("")
}) })
}} }}
@@ -85,19 +74,7 @@ export function PostComments({ initialComments, postId }: PostCommentsProps) {
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground mt-1 transition-colors" className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground mt-1 transition-colors"
onClick={() => onClick={() =>
requireAuth(() => { requireAuth(() => {
setComments((prev) => toggleCommentLike(comment.id)
prev.map((item) => {
if (item.id !== comment.id) return item
const nextLiked = !item.liked
return {
...item,
liked: nextLiked,
likeCount: nextLiked
? item.likeCount + 1
: Math.max(0, item.likeCount - 1),
}
}),
)
}) })
} }
> >
+12 -13
View File
@@ -1,18 +1,21 @@
"use client" "use client"
import { Heart } from "lucide-react" import { Heart } from "lucide-react"
import { useState } from "react" import { togglePostLike } from "@/lib/api/posts"
import { useRequireAuth } from "@/lib/use-require-auth" import { useRequireAuth } from "@/lib/use-require-auth"
import { usePostStore } from "@/store/posts"
interface PostLikeButtonProps { interface PostLikeButtonProps {
initialLiked: boolean postId: string
initialCount: number
} }
export function PostLikeButton({ initialLiked, initialCount }: PostLikeButtonProps) { export function PostLikeButton({ postId }: PostLikeButtonProps) {
const { requireAuth } = useRequireAuth() const { requireAuth } = useRequireAuth()
const [liked, setLiked] = useState(initialLiked) const post = usePostStore((state) => state.posts.find((item) => item.id === postId))
const [count, setCount] = useState(initialCount)
if (!post) {
return null
}
return ( return (
<button <button
@@ -20,16 +23,12 @@ export function PostLikeButton({ initialLiked, initialCount }: PostLikeButtonPro
className="flex items-center gap-1 hover:text-foreground transition-colors" className="flex items-center gap-1 hover:text-foreground transition-colors"
onClick={() => onClick={() =>
requireAuth(() => { requireAuth(() => {
setLiked((prevLiked) => { togglePostLike(postId)
const nextLiked = !prevLiked
setCount((prevCount) => (nextLiked ? prevCount + 1 : Math.max(0, prevCount - 1)))
return nextLiked
})
}) })
} }
> >
<Heart className={`h-4 w-4 ${liked ? "fill-red-500 text-red-500" : ""}`} /> <Heart className={`h-4 w-4 ${post.liked ? "fill-red-500 text-red-500" : ""}`} />
{count} {post.likeCount}
</button> </button>
) )
} }
+50 -3
View File
@@ -1,9 +1,56 @@
import { mockComments } from "@/lib/mock" import { allow, deny } from "@/lib/policy/assert"
import { addNotification } from "@/lib/api/notifications"
import { useAuthStore } from "@/store/auth"
import { useCommentStore } from "@/store/comments"
import { usePostStore } from "@/store/posts"
export function listComments() { export function listComments() {
return mockComments return useCommentStore.getState().comments
} }
export function listCommentsByPost(postId: string) { export function listCommentsByPost(postId: string) {
return mockComments.filter((comment) => comment.postId === postId) return useCommentStore.getState().comments.filter((comment) => comment.postId === postId)
}
export function addComment(postId: string, content: string) {
const user = useAuthStore.getState().user
if (!user) {
return deny("AUTH_REQUIRED", "请先登录")
}
const post = usePostStore.getState().posts.find((item) => item.id === postId)
if (!post) {
return deny("NOT_FOUND", "帖子不存在")
}
const comment = useCommentStore.getState().addComment(postId, user, content)
if (!comment) {
return deny("VALIDATION_FAILED", "评论内容不能为空")
}
usePostStore.getState().incrementCommentCount(postId)
addNotification({
type: "community",
title: "帖子收到新评论",
content: `${post.title}》有新的评论`,
link: `/post/${post.id}`,
})
return allow()
}
export function toggleCommentLike(commentId: string) {
const user = useAuthStore.getState().user
if (!user) {
return deny("AUTH_REQUIRED", "请先登录")
}
const comment = useCommentStore.getState().comments.find((item) => item.id === commentId)
if (!comment) {
return deny("NOT_FOUND", "评论不存在")
}
useCommentStore.getState().toggleCommentLike(commentId)
return allow()
} }
+3 -3
View File
@@ -1,13 +1,13 @@
export { getChatSessionById, listChatMessages, listChatSessions } from "./chat" export { getChatSessionById, listChatMessages, listChatSessions } from "./chat"
export { requestWithAuth } from "./client" export { requestWithAuth } from "./client"
export { listComments, listCommentsByPost } from "./comments" export { addComment, listComments, listCommentsByPost, toggleCommentLike } from "./comments"
export { getDisputeByOrderId, listDisputes } from "./disputes" export { getDisputeByOrderId, listDisputes } from "./disputes"
export { isFavorited, listFavorites, listFavoritesByUser } from "./favorites" export { isFavorited, listFavorites, listFavoritesByUser } from "./favorites"
export { getGameById, listGames } from "./games" export { getGameById, listGames } from "./games"
export { listNotifications } from "./notifications" export { addNotification, listNotifications, markNotificationAsRead } from "./notifications"
export { getOrderById, listOrders, listOrdersByConsumer } from "./orders" export { getOrderById, listOrders, listOrdersByConsumer } from "./orders"
export { getPlayerById, listPlayers, listPlayersByShop } from "./players" export { getPlayerById, listPlayers, listPlayersByShop } from "./players"
export { getPostById, listPosts, listPostsByAuthor } from "./posts" export { getPostById, listPosts, listPostsByAuthor, togglePostLike } from "./posts"
export { listReviews, listReviewsByOrder, listReviewsByTargetUser } from "./reviews" export { listReviews, listReviewsByOrder, listReviewsByTargetUser } from "./reviews"
export { getServiceById, listServices, listServicesByPlayer } from "./services" export { getServiceById, listServices, listServicesByPlayer } from "./services"
export { getShopById, getShopByOwnerId, listShops } from "./shops" export { getShopById, getShopByOwnerId, listShops } from "./shops"
+30
View File
@@ -1,3 +1,6 @@
import { allow, deny } from "@/lib/policy/assert"
import { addNotification } from "@/lib/api/notifications"
import { useAuthStore } from "@/store/auth"
import { usePostStore } from "@/store/posts" import { usePostStore } from "@/store/posts"
export function listPosts() { export function listPosts() {
@@ -11,3 +14,30 @@ export function getPostById(postId: string) {
export function listPostsByAuthor(userId: string) { export function listPostsByAuthor(userId: string) {
return usePostStore.getState().posts.filter((post) => post.author.id === userId) return usePostStore.getState().posts.filter((post) => post.author.id === userId)
} }
export function togglePostLike(postId: string) {
const user = useAuthStore.getState().user
if (!user) {
return deny("AUTH_REQUIRED", "请先登录")
}
const post = usePostStore.getState().posts.find((item) => item.id === postId)
if (!post) {
return deny("NOT_FOUND", "帖子不存在")
}
const shouldNotify = !post.liked
usePostStore.getState().togglePostLike(postId)
if (shouldNotify) {
addNotification({
type: "community",
title: "帖子收到点赞",
content: `${post.title}》有新的点赞`,
link: `/post/${post.id}`,
})
}
return allow()
}
+47
View File
@@ -0,0 +1,47 @@
import { create } from "zustand"
import { generateId } from "@/lib/id"
import { mockComments } from "@/lib/mock"
import type { Comment, User } from "@/lib/types"
interface CommentState {
comments: Comment[]
addComment: (postId: string, author: User, content: string) => Comment | null
toggleCommentLike: (commentId: string) => void
}
export const useCommentStore = create<CommentState>((set) => ({
comments: mockComments,
addComment: (postId, author, content) => {
const normalizedContent = content.trim()
if (!normalizedContent) return null
const comment: Comment = {
id: generateId("comment"),
postId,
author,
content: normalizedContent,
likeCount: 0,
liked: false,
createdAt: new Date().toISOString(),
}
set((state) => ({
comments: [...state.comments, comment],
}))
return comment
},
toggleCommentLike: (commentId) => {
set((state) => ({
comments: state.comments.map((comment) => {
if (comment.id !== commentId) return comment
const liked = !comment.liked
return {
...comment,
liked,
likeCount: liked ? comment.likeCount + 1 : Math.max(0, comment.likeCount - 1),
}
}),
}))
},
}))
+20
View File
@@ -17,6 +17,8 @@ interface CreatePostInput {
interface PostState { interface PostState {
posts: Post[] posts: Post[]
createPost: (input: CreatePostInput) => Post createPost: (input: CreatePostInput) => Post
togglePostLike: (postId: string) => void
incrementCommentCount: (postId: string) => void
} }
export const usePostStore = create<PostState>((set) => ({ export const usePostStore = create<PostState>((set) => ({
@@ -45,4 +47,22 @@ export const usePostStore = create<PostState>((set) => ({
return post return post
}, },
togglePostLike: (postId) =>
set((state) => ({
posts: state.posts.map((post) => {
if (post.id !== postId) return post
const liked = !post.liked
return {
...post,
liked,
likeCount: liked ? post.likeCount + 1 : Math.max(0, post.likeCount - 1),
}
}),
})),
incrementCommentCount: (postId) =>
set((state) => ({
posts: state.posts.map((post) =>
post.id === postId ? { ...post, commentCount: post.commentCount + 1 } : post,
),
})),
})) }))