61 lines
5.0 KiB
TypeScript
61 lines
5.0 KiB
TypeScript
"use client";
|
|
|
|
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 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 ?? "โปรไฟล์"} /><AvatarFallback>{feed.page.viewer?.name?.slice(0, 2) || "คุณ"}</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">เข้าสู่ระบบเพื่อแสดงความคิดเห็น ตอบกลับ และกดถูกใจ</p><Button onClick={() => router.push(commentSignInHref())}>เข้าสู่ระบบ</Button></div>}</div></div>
|
|
<div className="order-first flex flex-wrap items-center gap-3"><h2 className="font-heading text-xl font-semibold">ความคิดเห็น</h2>
|
|
<ToggleGroup value={[sort]} onValueChange={(values) => { if (values[0]) setSort(values[0]); }} variant="outline" aria-label="เรียงความคิดเห็น">
|
|
<ToggleGroupItem value="newest">ล่าสุด</ToggleGroupItem><ToggleGroupItem value="top">ยอดนิยม</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
<Button variant="ghost" className="ml-auto rounded-full" aria-label="รีเฟรชความคิดเห็น" 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>ยังไม่มีความคิดเห็น</EmptyTitle><EmptyDescription>เป็นคนแรกที่ร่วมพูดคุยเกี่ยวกับไกด์นี้</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()}>ดูความคิดเห็นเพิ่มเติม</Button>}
|
|
{linkedThread && <section aria-label="การสนทนาที่เลือก" className="flex flex-col gap-3">
|
|
<div className="flex items-center justify-between gap-2"><h3 className="font-semibold">การสนทนา</h3><Button variant="ghost" onClick={() => { setLinkedThread(null); setLinkedReply(null); window.history.replaceState(null, "", window.location.pathname + window.location.search); }}>ปิดการสนทนา</Button></div>
|
|
<CommentThread rootId={linkedThread} focusReplyId={linkedReply ?? undefined} expectedTarget={target} onChange={() => feed.refresh({ silent: true })} />
|
|
</section>}
|
|
|
|
</div>;
|
|
}
|