feat(comments): migrate to backend API
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
"use client"
|
||||
|
||||
import { addComment, toggleCommentLike } from "@/lib/api/comments"
|
||||
import { addComment, listCommentsByPost, toggleCommentLike } from "@/lib/api/comments"
|
||||
import { toApiError } from "@/lib/errors"
|
||||
import { notifyInfo } from "@/lib/toast"
|
||||
import type { Comment } from "@/lib/types"
|
||||
import { useRequireAuth } from "@/lib/use-require-auth"
|
||||
import { useCommentStore } from "@/store/comments"
|
||||
import { Heart } from "lucide-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"
|
||||
import { Button } from "./ui/button"
|
||||
import { Textarea } from "./ui/textarea"
|
||||
@@ -14,28 +16,58 @@ interface PostCommentsProps {
|
||||
}
|
||||
|
||||
export function PostComments({ postId }: PostCommentsProps) {
|
||||
const allComments = useCommentStore((state) => state.comments)
|
||||
const comments = useMemo(
|
||||
() => allComments.filter((comment) => comment.postId === postId),
|
||||
[allComments, postId],
|
||||
)
|
||||
const [comments, setComments] = useState<Comment[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [loadError, setLoadError] = useState<string | null>(null)
|
||||
const [content, setContent] = useState("")
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [pendingLike, setPendingLike] = useState<Record<string, boolean>>({})
|
||||
const { requireAuth } = useRequireAuth()
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setLoadError(null)
|
||||
try {
|
||||
const items = await listCommentsByPost(postId)
|
||||
setComments(items)
|
||||
} catch (err: unknown) {
|
||||
setLoadError(toApiError(err).msg)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [postId])
|
||||
|
||||
useEffect(() => {
|
||||
void refresh()
|
||||
}, [refresh])
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h2 className="font-semibold">评论 ({comments.length})</h2>
|
||||
<h2 className="font-semibold">
|
||||
评论 ({loading && comments.length === 0 ? "..." : comments.length})
|
||||
</h2>
|
||||
|
||||
<form
|
||||
className="flex gap-3"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
requireAuth(() => {
|
||||
requireAuth(async () => {
|
||||
if (submitting) return
|
||||
|
||||
const nextContent = content.trim()
|
||||
if (!nextContent) return
|
||||
const decision = addComment(postId, nextContent)
|
||||
if (!decision.ok) return
|
||||
|
||||
setSubmitting(true)
|
||||
const decision = await addComment(postId, nextContent)
|
||||
if (!decision.ok) {
|
||||
notifyInfo(decision.error.msg)
|
||||
setSubmitting(false)
|
||||
return
|
||||
}
|
||||
|
||||
setContent("")
|
||||
await refresh()
|
||||
setSubmitting(false)
|
||||
})
|
||||
}}
|
||||
>
|
||||
@@ -46,12 +78,16 @@ export function PostComments({ postId }: PostCommentsProps) {
|
||||
value={content}
|
||||
onChange={(event) => setContent(event.target.value)}
|
||||
/>
|
||||
<Button className="self-end" disabled={!content.trim()}>
|
||||
<Button className="self-end" disabled={!content.trim() || submitting}>
|
||||
发送
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{comments.length === 0 ? (
|
||||
{loading && comments.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-8">加载中...</p>
|
||||
) : loadError ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-8">加载失败:{loadError}</p>
|
||||
) : comments.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-8">还没有评论</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
@@ -71,10 +107,50 @@ export function PostComments({ postId }: PostCommentsProps) {
|
||||
<p className="text-sm">{comment.content}</p>
|
||||
<button
|
||||
type="button"
|
||||
disabled={pendingLike[comment.id]}
|
||||
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground mt-1 transition-colors"
|
||||
onClick={() =>
|
||||
requireAuth(() => {
|
||||
toggleCommentLike(comment.id)
|
||||
if (pendingLike[comment.id]) return
|
||||
|
||||
const prevLiked = comment.liked
|
||||
const prevCount = comment.likeCount
|
||||
const nextLiked = !prevLiked
|
||||
|
||||
setComments((current) =>
|
||||
current.map((item) => {
|
||||
if (item.id !== comment.id) return item
|
||||
return {
|
||||
...item,
|
||||
liked: nextLiked,
|
||||
likeCount: Math.max(0, prevCount + (nextLiked ? 1 : -1)),
|
||||
}
|
||||
}),
|
||||
)
|
||||
setPendingLike((current) => ({ ...current, [comment.id]: true }))
|
||||
|
||||
toggleCommentLike(comment.id, prevLiked)
|
||||
.then((decision) => {
|
||||
if (!decision.ok) {
|
||||
throw decision.error
|
||||
}
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
setComments((current) =>
|
||||
current.map((item) => {
|
||||
if (item.id !== comment.id) return item
|
||||
return {
|
||||
...item,
|
||||
liked: prevLiked,
|
||||
likeCount: prevCount,
|
||||
}
|
||||
}),
|
||||
)
|
||||
notifyInfo(toApiError(err).msg)
|
||||
})
|
||||
.finally(() => {
|
||||
setPendingLike((current) => ({ ...current, [comment.id]: false }))
|
||||
})
|
||||
})
|
||||
}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user