refactor(community): extract comment store and post/comment API adapters
This commit is contained in:
@@ -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 Link from "next/link"
|
||||
import { notFound } from "next/navigation"
|
||||
import { PostCommentCount } from "@/components/post-comment-count"
|
||||
import { PostComments } from "@/components/post-comments"
|
||||
import { PostLikeButton } from "@/components/post-like-button"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card"
|
||||
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"
|
||||
|
||||
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)
|
||||
if (!post) notFound()
|
||||
|
||||
const comments = listCommentsByPost(id)
|
||||
const linkedOrder = post.linkedOrderId ? getOrderById(post.linkedOrderId) : 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">
|
||||
<PostLikeButton initialLiked={post.liked} initialCount={post.likeCount} />
|
||||
<span className="flex items-center gap-1">
|
||||
<MessageCircle className="h-4 w-4" />
|
||||
{post.commentCount}
|
||||
</span>
|
||||
<PostLikeButton postId={post.id} />
|
||||
<PostCommentCount postId={post.id} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Separator className="my-6" />
|
||||
|
||||
<PostComments initialComments={comments} postId={id} />
|
||||
<PostComments postId={id} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,25 +1,26 @@
|
||||
"use client"
|
||||
|
||||
import { Heart } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { generateId } from "@/lib/id"
|
||||
import type { Comment } from "@/lib/types"
|
||||
import { useMemo, useState } from "react"
|
||||
import { addComment, toggleCommentLike } from "@/lib/api/comments"
|
||||
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 { Button } from "./ui/button"
|
||||
import { Textarea } from "./ui/textarea"
|
||||
|
||||
interface PostCommentsProps {
|
||||
initialComments: Comment[]
|
||||
postId: string
|
||||
}
|
||||
|
||||
export function PostComments({ initialComments, postId }: PostCommentsProps) {
|
||||
const [comments, setComments] = useState(initialComments)
|
||||
export function PostComments({ postId }: PostCommentsProps) {
|
||||
const allComments = useCommentStore((state) => state.comments)
|
||||
const comments = useMemo(
|
||||
() => allComments.filter((comment) => comment.postId === postId),
|
||||
[allComments, postId],
|
||||
)
|
||||
const [content, setContent] = useState("")
|
||||
const { requireAuth } = useRequireAuth()
|
||||
const user = useAuthStore((s) => s.user)
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -32,20 +33,8 @@ export function PostComments({ initialComments, postId }: PostCommentsProps) {
|
||||
requireAuth(() => {
|
||||
const nextContent = content.trim()
|
||||
if (!nextContent) return
|
||||
if (!user) return
|
||||
const createdAt = new Date().toISOString()
|
||||
setComments((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: generateId(`comment-${postId}`),
|
||||
postId,
|
||||
author: user,
|
||||
content: nextContent,
|
||||
likeCount: 0,
|
||||
liked: false,
|
||||
createdAt,
|
||||
},
|
||||
])
|
||||
const decision = addComment(postId, nextContent)
|
||||
if (!decision.ok) return
|
||||
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"
|
||||
onClick={() =>
|
||||
requireAuth(() => {
|
||||
setComments((prev) =>
|
||||
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),
|
||||
}
|
||||
}),
|
||||
)
|
||||
toggleCommentLike(comment.id)
|
||||
})
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
"use client"
|
||||
|
||||
import { Heart } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { togglePostLike } from "@/lib/api/posts"
|
||||
import { useRequireAuth } from "@/lib/use-require-auth"
|
||||
import { usePostStore } from "@/store/posts"
|
||||
|
||||
interface PostLikeButtonProps {
|
||||
initialLiked: boolean
|
||||
initialCount: number
|
||||
postId: string
|
||||
}
|
||||
|
||||
export function PostLikeButton({ initialLiked, initialCount }: PostLikeButtonProps) {
|
||||
export function PostLikeButton({ postId }: PostLikeButtonProps) {
|
||||
const { requireAuth } = useRequireAuth()
|
||||
const [liked, setLiked] = useState(initialLiked)
|
||||
const [count, setCount] = useState(initialCount)
|
||||
const post = usePostStore((state) => state.posts.find((item) => item.id === postId))
|
||||
|
||||
if (!post) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -20,16 +23,12 @@ export function PostLikeButton({ initialLiked, initialCount }: PostLikeButtonPro
|
||||
className="flex items-center gap-1 hover:text-foreground transition-colors"
|
||||
onClick={() =>
|
||||
requireAuth(() => {
|
||||
setLiked((prevLiked) => {
|
||||
const nextLiked = !prevLiked
|
||||
setCount((prevCount) => (nextLiked ? prevCount + 1 : Math.max(0, prevCount - 1)))
|
||||
return nextLiked
|
||||
})
|
||||
togglePostLike(postId)
|
||||
})
|
||||
}
|
||||
>
|
||||
<Heart className={`h-4 w-4 ${liked ? "fill-red-500 text-red-500" : ""}`} />
|
||||
{count}
|
||||
<Heart className={`h-4 w-4 ${post.liked ? "fill-red-500 text-red-500" : ""}`} />
|
||||
{post.likeCount}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
+50
-3
@@ -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() {
|
||||
return mockComments
|
||||
return useCommentStore.getState().comments
|
||||
}
|
||||
|
||||
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
@@ -1,13 +1,13 @@
|
||||
export { getChatSessionById, listChatMessages, listChatSessions } from "./chat"
|
||||
export { requestWithAuth } from "./client"
|
||||
export { listComments, listCommentsByPost } from "./comments"
|
||||
export { addComment, listComments, listCommentsByPost, toggleCommentLike } from "./comments"
|
||||
export { getDisputeByOrderId, listDisputes } from "./disputes"
|
||||
export { isFavorited, listFavorites, listFavoritesByUser } from "./favorites"
|
||||
export { getGameById, listGames } from "./games"
|
||||
export { listNotifications } from "./notifications"
|
||||
export { addNotification, listNotifications, markNotificationAsRead } from "./notifications"
|
||||
export { getOrderById, listOrders, listOrdersByConsumer } from "./orders"
|
||||
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 { getServiceById, listServices, listServicesByPlayer } from "./services"
|
||||
export { getShopById, getShopByOwnerId, listShops } from "./shops"
|
||||
|
||||
@@ -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"
|
||||
|
||||
export function listPosts() {
|
||||
@@ -11,3 +14,30 @@ export function getPostById(postId: string) {
|
||||
export function listPostsByAuthor(userId: string) {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}),
|
||||
}))
|
||||
},
|
||||
}))
|
||||
@@ -17,6 +17,8 @@ interface CreatePostInput {
|
||||
interface PostState {
|
||||
posts: Post[]
|
||||
createPost: (input: CreatePostInput) => Post
|
||||
togglePostLike: (postId: string) => void
|
||||
incrementCommentCount: (postId: string) => void
|
||||
}
|
||||
|
||||
export const usePostStore = create<PostState>((set) => ({
|
||||
@@ -45,4 +47,22 @@ export const usePostStore = create<PostState>((set) => ({
|
||||
|
||||
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,
|
||||
),
|
||||
})),
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user