62 lines
6.3 KiB
TypeScript
62 lines
6.3 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { RefreshCwIcon } from "lucide-react";
|
|
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
|
import { Button } from "@/components/ui/button";
|
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
|
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
|
|
import { FuzzyCombobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList } from "@/components/ui/combobox";
|
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
import { Empty, EmptyHeader, EmptyTitle, EmptyDescription } from "@/components/ui/empty";
|
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
import type { CommentPage } from "@/lib/comments/types";
|
|
import { characterRarityGradientClass } from "@/lib/catalog/rarity";
|
|
import { cn } from "@/lib/utils";
|
|
import { useMarkCommentsRead } from "./admin-unread-comments";
|
|
import { CommentCard, CommentThread } from "./comment-card";
|
|
import { CommentSkeleton } from "./comment-skeleton";
|
|
import { useCommentEvents } from "./use-comment-events";
|
|
import { useCommentFeed } from "./use-comment-feed";
|
|
|
|
type TargetOption = { value: string; label: string; image: string | null; rarity: number | null; characterKey: string | null };
|
|
export function AdminCommentInbox({ initial, targets, initialTarget, initialComment }: { initial: CommentPage; targets: TargetOption[]; initialTarget?: string; initialComment?: string }) {
|
|
const [notificationOpen, setNotificationOpen] = useState(Boolean(initialComment));
|
|
const [target, setTarget] = useState<TargetOption | null>(targets.find((item) => item.value === initialTarget) ?? null);
|
|
const [status, setStatus] = useState("all");
|
|
const [answered, setAnswered] = useState("all");
|
|
const query = new URLSearchParams({ view: "inbox", grouped: "true", status, unanswered: String(answered === "unanswered") });
|
|
if (target) query.set("target", target.value);
|
|
const feed = useCommentFeed(`/api/admin/comments?${query}`, initial);
|
|
useCommentEvents({ admin: true }, () => feed.refresh({ silent: true, forceRevision: true }));
|
|
const markRead = useMarkCommentsRead();
|
|
useEffect(() => { void markRead(target?.value ?? "all"); }, [markRead, target, feed.revision]);
|
|
return <div className="flex flex-col gap-6">
|
|
{initialComment && initialTarget && <Dialog open={notificationOpen} onOpenChange={setNotificationOpen}><DialogContent className="max-h-[90svh] overflow-y-auto sm:max-w-4xl max-sm:top-0 max-sm:left-0 max-sm:h-svh max-sm:max-h-svh max-sm:max-w-none max-sm:translate-x-0 max-sm:translate-y-0 max-sm:rounded-none max-sm:content-start">
|
|
<DialogHeader className="pr-8"><DialogTitle>การสนทนา</DialogTitle><DialogDescription>ความคิดเห็นจากการแจ้งเตือนและคำตอบกลับ</DialogDescription></DialogHeader>
|
|
{notificationOpen && <CommentThread rootId={initialComment} expectedTarget={initialTarget} onChange={() => feed.refresh({ silent: true })} />}
|
|
</DialogContent></Dialog>}
|
|
<FieldGroup className="md:grid md:grid-cols-3">
|
|
<Field><FieldLabel>ไกด์หรือรอบ Stygian</FieldLabel>
|
|
<FuzzyCombobox items={targets} value={target} onValueChange={setTarget} itemToStringLabel={(item) => item.label} itemToStringValue={(item) => item.value}
|
|
searchNames={(item) => [item.label]} autoHighlight>
|
|
<ComboboxInput aria-label="ค้นหาไกด์หรือรอบ Stygian" placeholder="ทุกไกด์และทุกรอบ" showClear />
|
|
<ComboboxContent><ComboboxEmpty>ไม่พบไกด์ที่ตรงกัน</ComboboxEmpty><ComboboxList>{(item: TargetOption) => <ComboboxItem key={item.value} value={item}><Avatar className={cn("size-9 shrink-0", characterRarityGradientClass(item.characterKey, item.rarity))}><AvatarImage src={item.image ?? undefined} alt="" /><AvatarFallback>{item.label.slice(0, 2)}</AvatarFallback></Avatar><span className="min-w-0 flex-1 truncate">{item.label}</span></ComboboxItem>}</ComboboxList></ComboboxContent>
|
|
</FuzzyCombobox>
|
|
</Field>
|
|
<Field><FieldLabel>การแสดงผล</FieldLabel><ToggleGroup value={[status]} onValueChange={(values) => { if (values[0]) setStatus(values[0]); }} variant="outline" aria-label="กรองการแสดงผลความคิดเห็น">
|
|
<ToggleGroupItem value="all">ทั้งหมด</ToggleGroupItem><ToggleGroupItem value="visible">แสดงอยู่</ToggleGroupItem><ToggleGroupItem value="hidden">ซ่อนอยู่</ToggleGroupItem>
|
|
</ToggleGroup></Field>
|
|
<Field><FieldLabel>การตอบกลับจากผู้ดูแล</FieldLabel><ToggleGroup value={[answered]} onValueChange={(values) => { if (values[0]) setAnswered(values[0]); }} variant="outline" aria-label="กรองการตอบกลับจากผู้ดูแล">
|
|
<ToggleGroupItem value="all">ทุกความคิดเห็น</ToggleGroupItem><ToggleGroupItem value="unanswered">ยังไม่ได้ตอบ</ToggleGroupItem>
|
|
</ToggleGroup></Field>
|
|
</FieldGroup>
|
|
<Button variant="outline" className="self-end" disabled={feed.busy} onClick={() => void feed.refresh({ silent: false, forceRevision: true })}><RefreshCwIcon data-icon="inline-start" />รีเฟรช</Button>
|
|
{feed.error && <Alert variant="destructive"><AlertDescription>{feed.error}</AlertDescription></Alert>}
|
|
{feed.page.items.map((item) => <CommentCard key={item.id} item={item} viewer={feed.page.viewer} inbox onChange={() => feed.refresh({ silent: true, forceRevision: true })} revision={feed.revision} />)}
|
|
{!feed.page.items.length && !feed.busy && !feed.error && <Empty><EmptyHeader><EmptyTitle>ไม่พบความคิดเห็นที่ตรงกัน</EmptyTitle><EmptyDescription>ความคิดเห็นจากทุกไกด์และทุกรอบ Stygian จะแสดงที่นี่</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>}
|
|
</div>;
|
|
}
|