Files
neo-blog/web/src/components/comment/index.tsx

170 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { User } from "@/models/user";
import { useTranslations } from "next-intl";
import { Suspense, useEffect, useState } from "react";
import { toast } from "sonner";
import { Comment } from "@/models/comment";
import { createComment, deleteComment, getComment, listComments } from "@/api/comment";
import { TargetType } from "@/models/types";
import { OrderBy } from "@/models/common";
import { Separator } from "@/components/ui/separator";
import { getLoginUser } from "@/api/user";
import { Skeleton } from "@/components/ui/skeleton";
import { CommentInput } from "./comment-input";
import { CommentItem } from "./comment-item";
import config from "@/config";
import "./style.css";
export function CommentSection(
{
targetType,
targetId,
totalCount = 0
}: {
targetType: TargetType,
targetId: number,
totalCount?: number
}
) {
const t = useTranslations('Comment')
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [comments, setComments] = useState<Comment[]>([]);
const [activeInput, setActiveInput] = useState<{ id: number; type: 'reply' | 'edit' } | null>(null);
const [page, setPage] = useState(1); // 当前页码
const [totalCommentCount, setTotalCommentCount] = useState(totalCount); // 评论总数
const [needLoadMore, setNeedLoadMore] = useState(true); // 是否需要加载更多当最后一次获取的评论数小于分页大小时设为false
// 获取当前登录用户
useEffect(() => {
getLoginUser()
.then(response => {
setCurrentUser(response.data);
})
}, [])
// 加载0/顶层评论
useEffect(() => {
listComments({
targetType,
targetId,
depth: 0,
orderBy: OrderBy.CreatedAt,
desc: true,
page: page,
size: config.commentsPerPage,
commentId: 0
}).then(response => {
setComments(response.data.comments);
});
}, [])
const onCommentSubmitted = ({ commentContent, isPrivate }: { commentContent: string, isPrivate: boolean }) => {
createComment({
targetType,
targetId,
content: commentContent,
replyId: null,
isPrivate,
}).then(res => {
toast.success(t("comment_success"));
setTotalCommentCount(c => c + 1);
getComment({ id: res.data.id }).then(response => {
console.log("New comment fetched:", response.data);
setComments(prevComments => [response.data, ...prevComments]);
});
setActiveInput(null);
})
}
const onReplySubmitted = ({ }: { commentContent: string, isPrivate: boolean }) => {
setTotalCommentCount(c => c + 1);
}
const onCommentDelete = ({ commentId }: { commentId: number }) => {
deleteComment({ id: commentId }).then(() => {
toast.success(t("delete_success"));
setComments(prevComments => prevComments.filter(comment => comment.id !== commentId));
setTotalCommentCount(c => c - 1);
}).catch(error => {
toast.error(t("delete_failed") + ": " + error.message);
});
}
const handleLoadMore = () => {
const nextPage = page + 1;
listComments({
targetType,
targetId,
depth: 0,
orderBy: OrderBy.CreatedAt,
desc: true,
page: nextPage,
size: config.commentsPerPage,
commentId: 0
}).then(response => {
if (response.data.comments.length < config.commentsPerPage) {
setNeedLoadMore(false);
}
setComments(prevComments => [...prevComments, ...response.data.comments]);
setPage(nextPage);
});
}
return (
<div>
<Separator className="my-16" />
<div className="font-bold text-2xl">{t("comment")} ({totalCommentCount})</div>
<CommentInput
user={currentUser}
onCommentSubmitted={onCommentSubmitted}
/>
<div className="mt-4">
<Suspense fallback={<CommentLoading />}>
{comments.map((comment, idx) => (
<div key={comment.id} className="" style={{ animationDelay: `${idx * 60}ms` }}>
<Separator className="my-2" />
<CommentItem
user={currentUser}
comment={comment}
parentComment={null}
onCommentDelete={onCommentDelete}
activeInput={activeInput}
setActiveInputId={setActiveInput}
onReplySubmitted={onReplySubmitted}
/>
</div>
))}
</Suspense>
{needLoadMore ?
<p onClick={handleLoadMore} className="text-center text-sm text-gray-500 my-4 cursor-pointer hover:underline">
{t("load_more")}
</p>
:
<p className="text-center text-sm text-gray-500 my-4">
{t("no_more")}
</p>
}
</div>
</div>
)
}
function CommentLoading() {
return (
<div className="space-y-6 py-8">
{[...Array(3)].map((_, i) => (
<div key={i} className="flex gap-3 fade-in-up" style={{ animationDelay: `${i * 80}ms` }}>
<Skeleton className="w-10 h-10 rounded-full fade-in" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-1/4 fade-in" />
<Skeleton className="h-4 w-3/4 fade-in" />
<Skeleton className="h-4 w-2/3 fade-in" />
</div>
</div>
))}
</div>
);
}