Files
Cupcake/app/components/shared/comment/index.tsx
T

189 lines
5.1 KiB
TypeScript

import { useEffect, useRef, useState } from "react";
import { useLocation } from "react-router";
import { add } from "~/components/ui/notice/utils";
import { useHydrated } from "~/hooks/use-hydrated";
import { clsn } from "~/utils";
import CommentForm, { type CommentFormSubmitValues } from "../comment-form";
import CommentItem from "../comment-item";
const { APP_APIURL } = import.meta.env;
interface CommentsProps {
className?: string;
refs: string;
}
export default function Comments({ className, refs }: CommentsProps) {
const wrapperRef = useRef<HTMLDivElement>(null);
const location = useLocation();
const hydrated = useHydrated();
const [list, setList] = useState<API.Comment.ICommentDetailData[]>([]);
const [loading, setLoading] = useState(true);
const prevRefsRef = useRef<string>("");
const scrollToComment = (hash: string) => {
const id = hash.replace(/^#/, "");
if (!id || !id.startsWith("c")) {
return;
}
const commentItem = document.querySelector<HTMLDivElement>(`#${id}`);
if (!commentItem) {
console.log("定位评论失败");
return;
}
console.log("定位评论成功");
commentItem.scrollIntoView();
commentItem.classList.add("animate-scale");
const onAnimationEnd = (ev: AnimationEvent) => {
const el = ev.target as HTMLDivElement;
if (el) {
el.classList.remove("animate-scale");
}
commentItem.removeEventListener("animationend", onAnimationEnd);
};
commentItem.addEventListener("animationend", onAnimationEnd);
};
const getCommentRequest = () => {
fetch(`${APP_APIURL}/api/comment/detail/?ref=${encodeURIComponent(refs)}`)
.then((res) => res.json())
.then((res: API.PageResponse<API.Comment.ICommentDetailData[]>) => {
if (res.status === "Success") {
setList(res.data);
setLoading(false);
}
if (location.hash.match(/^#c/)) {
scrollToComment(location.hash);
}
});
};
useEffect(() => {
if (!hydrated || !wrapperRef.current) {
return;
}
if (location.hash.match(/^#c/)) {
wrapperRef.current.scrollIntoView();
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && prevRefsRef.current !== refs) {
console.log("获取评论");
prevRefsRef.current = refs;
getCommentRequest();
}
});
},
{
threshold: 0.1,
},
);
observer.observe(wrapperRef.current);
return () => {
observer.disconnect();
};
}, [hydrated, refs, location.hash]);
useEffect(() => {
if (hydrated && location.hash.match(/^#c/) && wrapperRef.current) {
scrollToComment(location.hash);
}
}, [hydrated, location.hash]);
const addCommentRequest = (data: CommentFormSubmitValues) => {
return new Promise<void>((resolve, reject) => {
fetch(`${APP_APIURL}/api/comment/add`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...data, ref: refs }),
})
.then((res) => res.json())
.then((res: API.Response<API.Comment.ICommentDetailData>) => {
if (res.status === "Success") {
add({
title: "评论成功",
type: "success",
content: res.msg,
});
if (res.data) {
if (res.data.root) {
setList((prev) => {
const index = prev.findIndex((item) => item.uuid === res.data.root);
if (index > -1) {
const next = [...prev];
next[index] = {
...next[index],
children: [res.data, ...next[index].children],
};
return next;
}
return prev;
});
}
else {
setList((prev) => [{ ...res.data, children: [] }, ...prev]);
}
}
resolve();
}
else {
add({
title: "评论失败",
type: "error",
content: res.msg,
});
reject();
}
})
.catch(() => {
reject();
});
});
};
return (
<div ref={wrapperRef} className={clsn("bg-white rounded-xl border-b-4 border-b-cyan-200 p-5 md:p-8", className)} id="comments">
<h3 className="text-pink-400 text-2xl font-bold mb-6">
评论
</h3>
<CommentForm onSubmit={addCommentRequest} />
{list.length > 0 ? (
<div>
{list.map((item) => (
<CommentItem key={item.uuid} data={item} onSubmit={addCommentRequest} />
))}
</div>
) : (
<div className="text-center opacity-50 py-8">
{loading
? "评论获取中"
: list.length === 0
? "快来成为第一个评论的人吧"
: null}
</div>
)}
</div>
);
}