Files
buzz-sheet/components/comments/comment-board.tsx
T
gunshiz 9580407a90
CI / Verify (push) Successful in 1m56s
CI / Build immutable images and deploy (push) Successful in 3m12s
perf : cache
2026-10-08 23:21:34 +07:00

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>;
}