64 lines
5.4 KiB
TypeScript
64 lines
5.4 KiB
TypeScript
"use client";
|
|
|
|
import { useCommentLocale } from "./comment-locale";
|
|
|
|
import { useRouter } from "next/navigation";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
import { RefreshCwIcon } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
import { Empty, EmptyHeader, EmptyTitle, EmptyDescription } from "@/components/ui/empty";
|
|
import type { CommentPage } from "@/lib/comments/types";
|
|
import { CommentComposer } from "./comment-composer";
|
|
import { CommentCard, CommentThread } from "./comment-card";
|
|
import { commentSignInHref } from "./client";
|
|
import { CommentSkeleton } from "./comment-skeleton";
|
|
import { useCommentEvents } from "./use-comment-events";
|
|
import { useCommentFeed } from "./use-comment-feed";
|
|
|
|
export function CommentBoard({ target, initial }: { target: string; initial: CommentPage }) {
|
|
const { t } = useCommentLocale();
|
|
const router = useRouter();
|
|
const [sort, setSort] = useState("top");
|
|
const [linkedReply, setLinkedReply] = useState<string | null>(null);
|
|
const [linkedThread, setLinkedThread] = useState<string | null>(null);
|
|
const feed = useCommentFeed(`/api/comments?target=${encodeURIComponent(target)}&sort=${sort}`, initial);
|
|
useCommentEvents({ target }, (changed) => feed.refresh({ silent: true, forceRevision: changed }));
|
|
useEffect(() => {
|
|
const showThread = () => {
|
|
const match = /^#comment-([0-9a-f-]{36})$/i.exec(window.location.hash);
|
|
if (match) {
|
|
setLinkedThread(match[1]);
|
|
const reply = new URLSearchParams(window.location.search).get("reply");
|
|
setLinkedReply(reply && /^[0-9a-f-]{36}$/i.test(reply) ? reply : null);
|
|
}
|
|
};
|
|
showThread();
|
|
window.addEventListener("hashchange", showThread);
|
|
return () => window.removeEventListener("hashchange", showThread);
|
|
}, []);
|
|
return <div className="flex flex-col gap-6 py-2">
|
|
<div className="flex items-start gap-3"><Avatar className="size-10 shrink-0"><AvatarImage src={feed.page.viewer?.image ?? undefined} alt={feed.page.viewer?.name ?? t("โปรไฟล์", "Profile")} /><AvatarFallback>{feed.page.viewer?.name?.slice(0, 2) || t("คุณ", "You")}</AvatarFallback></Avatar><div className="min-w-0 flex-1">{feed.page.viewer ? <CommentComposer target={target} onSaved={() => void feed.refresh()} />
|
|
: <div className="flex flex-col items-start gap-2"><p className="text-sm text-muted-foreground">{t("เข้าสู่ระบบเพื่อแสดงความคิดเห็น ตอบกลับ และกดถูกใจ", "Sign in to comment, reply, and like comments")}</p><Button onClick={() => router.push(commentSignInHref())}>{t("เข้าสู่ระบบ", "Sign in")}</Button></div>}</div></div>
|
|
<div className="order-first flex flex-wrap items-center gap-3"><h2 className="font-heading text-xl font-semibold">{t("ความคิดเห็น", "Comments")}</h2>
|
|
<ToggleGroup value={[sort]} onValueChange={(values) => { if (values[0]) setSort(values[0]); }} variant="outline" aria-label={t("เรียงความคิดเห็น", "Sort comments")}>
|
|
<ToggleGroupItem value="newest">{t("ล่าสุด", "Newest")}</ToggleGroupItem><ToggleGroupItem value="top">{t("ยอดนิยม", "Top")}</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
<Button variant="ghost" className="ml-auto rounded-full" aria-label={t("รีเฟรชความคิดเห็น", "Refresh comments")} size="icon" disabled={feed.busy} onClick={() => void feed.refresh()}><RefreshCwIcon /></Button>
|
|
</div>
|
|
{feed.error && <Alert variant="destructive"><AlertDescription>{feed.error}</AlertDescription></Alert>}
|
|
{feed.page.items.filter((item) => item.id !== linkedThread).map((item) => <CommentCard key={item.id} item={item} viewer={feed.page.viewer} onChange={() => feed.refresh({ silent: true })} revision={feed.revision} />)}
|
|
{!feed.page.items.length && !feed.busy && !feed.error && <Empty><EmptyHeader><EmptyTitle>{t("ยังไม่มีความคิดเห็น", "No comments yet")}</EmptyTitle><EmptyDescription>{t("เป็นคนแรกที่ร่วมพูดคุยเกี่ยวกับไกด์นี้", "Be the first to discuss this guide")}</EmptyDescription></EmptyHeader></Empty>}
|
|
{feed.busy && <CommentSkeleton count={feed.page.items.length ? 1 : 3} />}
|
|
{feed.page.nextCursor && <Button variant="outline" disabled={feed.busy} onClick={() => void feed.more()}>{t("ดูความคิดเห็นเพิ่มเติม", "Load more comments")}</Button>}
|
|
{linkedThread && <section aria-label={t("การสนทนาที่เลือก", "Selected thread")} className="flex flex-col gap-3">
|
|
<div className="flex items-center justify-between gap-2"><h3 className="font-semibold">{t("การสนทนา", "Thread")}</h3><Button variant="ghost" onClick={() => { setLinkedThread(null); setLinkedReply(null); window.history.replaceState(null, "", window.location.pathname + window.location.search); }}>{t("ปิดการสนทนา", "Close thread")}</Button></div>
|
|
<CommentThread rootId={linkedThread} focusReplyId={linkedReply ?? undefined} expectedTarget={target} onChange={() => feed.refresh({ silent: true })} />
|
|
</section>}
|
|
|
|
</div>;
|
|
}
|