fix(comments) : group admin comments by guide with inline replies
CI / Verify (push) Successful in 1m54s
CI / Build immutable images and deploy (push) Successful in 2m33s

This commit is contained in:
2026-10-08 13:44:57 +07:00 Unverified
parent fdea6c94be
commit 34c73e2f03
2 changed files with 27 additions and 28 deletions
+22 -8
View File
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { RefreshCwIcon } from "lucide-react";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
@@ -9,8 +10,7 @@ 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 type { CommentItem, CommentPage } from "@/lib/comments/types";
import { characterRarityGradientClass } from "@/lib/catalog/rarity";
import { cn } from "@/lib/utils";
import { useMarkCommentsRead } from "./admin-unread-comments";
@@ -21,7 +21,6 @@ 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");
@@ -31,11 +30,17 @@ export function AdminCommentInbox({ initial, targets, initialTarget, initialComm
useCommentEvents({ admin: true }, () => feed.refresh({ silent: true, forceRevision: true }));
const markRead = useMarkCommentsRead();
useEffect(() => { void markRead(target?.value ?? "all"); }, [markRead, target, feed.revision]);
const groups = new Map<string, CommentItem[]>();
for (const item of feed.page.items) {
const comments = groups.get(item.target) ?? [];
comments.push(item);
groups.set(item.target, comments);
}
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>}
{initialComment && initialTarget && <section className="flex min-w-0 flex-col gap-3" aria-label="การสนทนาจากการแจ้งเตือน">
<h2 className="font-heading text-lg font-semibold">การสนทนาจากการแจ้งเตือน</h2>
<CommentThread rootId={initialComment} expectedTarget={initialTarget} onChange={() => feed.refresh({ silent: true })} />
</section>}
<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}
@@ -53,7 +58,16 @@ export function AdminCommentInbox({ initial, targets, initialTarget, initialComm
</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} />)}
{Array.from(groups, ([targetKey, items]) => <section key={targetKey} className="flex min-w-0 flex-col gap-4" aria-label={items[0].targetName}>
<Link href={items[0].href} prefetch={false} className="flex min-w-0 flex-col gap-3">
{items[0].targetImage && <>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={items[0].targetImage} alt={`ภาพ Overview ของ ${items[0].targetName}`} loading="lazy" className="aspect-video w-full rounded-lg object-cover sm:max-w-md" />
</>}
<h2 className="font-heading text-xl font-semibold">{items[0].targetName}</h2>
</Link>
{items.map((item) => <CommentCard key={item.id} item={item} viewer={feed.page.viewer} inbox onChange={() => feed.refresh({ silent: true, forceRevision: true })} revision={feed.revision} />)}
</section>)}
{!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>}