Support comment reply.

This commit is contained in:
2025-07-04 15:24:23 +08:00
parent 1f22367cc4
commit 54ab93ea7b
11 changed files with 243 additions and 92 deletions

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { network } from "../network/network";
import showToast from "../components/toast";
import { useNavigate, useParams } from "react-router";
@@ -7,6 +7,10 @@ import { CommentWithRef, Resource } from "../network/models";
import Loading from "../components/loading";
import Markdown from "react-markdown";
import Badge from "../components/badge";
import { CommentInput } from "../components/comment_input";
import { CommentTile } from "../components/comment_tile";
import { Comment } from "../network/models";
import Pagination from "../components/pagination";
export default function CommentPage() {
const params = useParams();
@@ -16,6 +20,7 @@ export default function CommentPage() {
const navigate = useNavigate();
useEffect(() => {
setComment(null);
const id = parseInt(commentId || "0");
if (isNaN(id) || id <= 0) {
showToast({
@@ -34,7 +39,7 @@ export default function CommentPage() {
});
}
});
}, []);
}, [commentId]);
useEffect(() => {
document.title = t("Comment Details");
@@ -46,28 +51,36 @@ export default function CommentPage() {
return (
<div className="p-4">
<h1 className="text-2xl font-bold my-2">{t("Comment")}</h1>
<button
onClick={() => {
navigate(`/user/${encodeURIComponent(comment.user.username)}`);
}}
className="border-b-2 py-1 cursor-pointer border-transparent hover:border-primary transition-colors duration-200 ease-in-out"
>
<div className="flex items-center">
<div className="avatar">
<div className="w-6 rounded-full">
<img src={network.getUserAvatar(comment.user)} alt={"avatar"} />
</div>
</div>
<div className="w-2"></div>
<div className="text-sm">{comment.user.username}</div>
</div>
</button>
{comment.resource && <ResourceCard resource={comment.resource} />}
<div className="flex"></div>
<div className="flex items-center mt-4">
<button
onClick={() => {
navigate(`/user/${encodeURIComponent(comment.user.username)}`);
}}
className="border-b-2 py-1 cursor-pointer border-transparent hover:border-primary transition-colors duration-200 ease-in-out"
>
<div className="flex items-center">
<div className="avatar">
<div className="w-6 rounded-full">
<img src={network.getUserAvatar(comment.user)} alt={"avatar"} />
</div>
</div>
<div className="w-2"></div>
<div className="text-sm">{comment.user.username}</div>
</div>
</button>
<span className="text-xs text-base-content/80 ml-2">
{t("Commented on")}
{new Date(comment.created_at).toLocaleDateString()}
</span>
</div>
<article>
<CommentContent content={comment.content} />
</article>
<div className="h-4" />
<div className="border-t border-base-300" />
<div className="h-4" />
<CommentReply comment={comment} />
</div>
);
}
@@ -99,7 +112,7 @@ function ResourceCard({ resource }: { resource: Resource }) {
return (
<a
href="link"
className="flex flex-row w-full card bg-base-200 shadow overflow-clip my-2"
className="flex flex-row w-full card bg-base-200 shadow-xs hover:shadow overflow-clip my-2"
onClick={(e) => {
e.preventDefault();
navigate(link);
@@ -115,7 +128,7 @@ function ResourceCard({ resource }: { resource: Resource }) {
<div className="flex flex-col p-4 flex-1">
<h2 className="card-title w-full break-all">{resource.title}</h2>
<div className="h-2"></div>
<p>
<p className="mb-2">
{tags.map((tag) => {
return (
<Badge key={tag.id} className={"m-0.5"}>
@@ -138,3 +151,82 @@ function ResourceCard({ resource }: { resource: Resource }) {
</a>
);
}
function CommentReply({ comment }: { comment: CommentWithRef }) {
const { t } = useTranslation();
const [page, setPage] = useState(1);
const [maxPage, setMaxPage] = useState(0);
const [listKey, setListKey] = useState(0);
const reload = useCallback(() => {
setPage(1);
setMaxPage(0);
setListKey((prev) => prev + 1);
}, []);
return <>
<h2 className="text-xl font-bold my-2">{t("Replies")}</h2>
<CommentInput replyTo={comment.id} reload={reload} />
<CommentsList
commentId={comment.id}
page={page}
maxPageCallback={(maxPage: number) => {
setMaxPage(maxPage);
}}
key={listKey}
reload={reload}
/>
{maxPage ? (
<div className={"w-full flex justify-center"}>
<Pagination page={page} setPage={setPage} totalPages={maxPage} />
</div>
) : null}
</>
}
function CommentsList({
commentId,
page,
maxPageCallback,
reload,
}: {
commentId: number;
page: number;
maxPageCallback: (maxPage: number) => void;
reload: () => void;
}) {
const [comments, setComments] = useState<Comment[] | null>(null);
useEffect(() => {
network.listCommentReplies(commentId, page).then((res) => {
if (res.success) {
setComments(res.data!);
maxPageCallback(res.totalPages || 1);
} else {
showToast({
message: res.message,
type: "error",
});
}
});
}, [maxPageCallback, page, commentId]);
if (comments == null) {
return (
<div className={"w-full"}>
<Loading />
</div>
);
}
return (
<>
{comments.map((comment) => {
return (
<CommentTile elevation="high" comment={comment} key={comment.id} onUpdated={reload} />
);
})}
</>
);
}

View File

@@ -295,7 +295,7 @@ export default function EditResourcePage() {
/>
<div className={"flex items-center py-1 "}>
<MdOutlineInfo className={"inline mr-1"} />
<span className={"text-sm"}>{t("Use Markdown format")}</span>
<span className={"text-sm"}>{t("Use markdown format")}</span>
</div>
<div className={"h-4"}></div>
<p className={"my-1"}>{t("Images")}</p>

View File

@@ -306,7 +306,7 @@ export default function PublishPage() {
/>
<div className={"flex items-center py-1 "}>
<MdOutlineInfo className={"inline mr-1"} />
<span className={"text-sm"}>{t("Use Markdown format")}</span>
<span className={"text-sm"}>{t("Use markdown format")}</span>
</div>
<div className={"h-4"}></div>
<p className={"my-1"}>{t("Images")}</p>