perf : cache
CI / Verify (push) Successful in 1m56s
CI / Build immutable images and deploy (push) Successful in 3m12s

This commit is contained in:
2026-10-08 23:21:34 +07:00 Unverified
parent 36063fc214
commit 9580407a90
58 changed files with 5797 additions and 5336 deletions
+21 -18
View File
@@ -1,7 +1,10 @@
"use client";
import { useCommentLocale } from "./comment-locale";
import { useEffect, useState } from "react";
import Link from "next/link";
import Image from "@/components/ui/resilient-image";
import { RefreshCwIcon } from "lucide-react";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
@@ -22,6 +25,7 @@ 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 { t } = useCommentLocale();
const [target, setTarget] = useState<TargetOption | null>(targets.find((item) => item.value === initialTarget) ?? null);
const [status, setStatus] = useState("all");
const [answered, setAnswered] = useState("all");
@@ -32,46 +36,45 @@ export function AdminCommentInbox({ initial, targets, initialTarget, initialComm
const markRead = useMarkCommentsRead();
useEffect(() => { void markRead(target?.value ?? "all"); }, [markRead, target, feed.revision]);
return <div className="flex min-w-0 flex-col gap-6 max-md:[&_button]:min-h-11 max-md:[&_button]:min-w-11">
{initialComment && initialTarget && <section className="flex min-w-0 flex-col gap-3" aria-label="การสนทนาจากการแจ้งเตือน">
<h2 className="font-heading text-lg font-semibold">การสนทนาจากการแจ้งเตือน</h2>
{initialComment && initialTarget && <section className="flex min-w-0 flex-col gap-3" aria-label={t("การสนทนาจากการแจ้งเตือน", "Thread from notification")}>
<h2 className="font-heading text-lg font-semibold">{t("การสนทนาจากการแจ้งเตือน", "Thread from notification")}</h2>
<CommentThread rootId={initialComment} expectedTarget={initialTarget} onChange={() => feed.refresh({ silent: true })} />
</section>}
<FieldGroup className="lg:grid lg:grid-cols-[repeat(3,minmax(0,1fr))_auto] lg:items-end">
<Field><FieldLabel>ไกด์หรือรอบ Stygian</FieldLabel>
<Field><FieldLabel>{t("ไกด์หรือรอบ Stygian", "Guide or Stygian season")}</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>
<ComboboxInput aria-label={t("ค้นหาไกด์หรือรอบ Stygian", "Search guides or Stygian seasons")} placeholder={t("ทุกไกด์และทุกรอบ", "All guides and seasons")} showClear />
<ComboboxContent><ComboboxEmpty>{t("ไม่พบไกด์ที่ตรงกัน", "No matching guides")}</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 className="max-w-full flex-wrap" 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>
<Field><FieldLabel>{t("การแสดงผล", "Visibility")}</FieldLabel><ToggleGroup className="max-w-full flex-wrap" value={[status]} onValueChange={(values) => { if (values[0]) setStatus(values[0]); }} variant="outline" aria-label={t("กรองการแสดงผลความคิดเห็น", "Filter comment visibility")}>
<ToggleGroupItem value="all">{t("ทั้งหมด", "All")}</ToggleGroupItem><ToggleGroupItem value="visible">{t("แสดงอยู่", "Visible")}</ToggleGroupItem><ToggleGroupItem value="hidden">{t("ซ่อนอยู่", "Hidden")}</ToggleGroupItem>
</ToggleGroup></Field>
<Field><FieldLabel>การตอบกลับจากผู้ดูแล</FieldLabel><ToggleGroup className="max-w-full flex-wrap" value={[answered]} onValueChange={(values) => { if (values[0]) setAnswered(values[0]); }} variant="outline" aria-label="กรองการตอบกลับจากผู้ดูแล">
<ToggleGroupItem value="all">ทุกความคิดเห็น</ToggleGroupItem><ToggleGroupItem value="unanswered">ยังไม่ได้ตอบ</ToggleGroupItem>
<Field><FieldLabel>{t("การตอบกลับจากผู้ดูแล", "Admin replies")}</FieldLabel><ToggleGroup className="max-w-full flex-wrap" value={[answered]} onValueChange={(values) => { if (values[0]) setAnswered(values[0]); }} variant="outline" aria-label={t("กรองการตอบกลับจากผู้ดูแล", "Filter admin replies")}>
<ToggleGroupItem value="all">{t("ทุกความคิดเห็น", "All comments")}</ToggleGroupItem><ToggleGroupItem value="unanswered">{t("ยังไม่ได้ตอบ", "Unanswered")}</ToggleGroupItem>
</ToggleGroup></Field>
<Button variant="outline" className="self-end" disabled={feed.busy} onClick={() => void feed.refresh({ silent: false, forceRevision: true })}><RefreshCwIcon data-icon="inline-start" />รีเฟรช</Button>
<Button variant="outline" className="self-end" disabled={feed.busy} onClick={() => void feed.refresh({ silent: false, forceRevision: true })}><RefreshCwIcon data-icon="inline-start" />{t("รีเฟรช", "Refresh")}</Button>
</FieldGroup>
{feed.error && <Alert variant="destructive"><AlertDescription>{feed.error}</AlertDescription></Alert>}
{feed.page.items.length > 0 && <section aria-label="ความคิดเห็นจากทุกไกด์และทุกรอบ Stygian" className="flex min-w-0 flex-col">
<Table className="table-fixed max-md:block" aria-label="ความคิดเห็นจากทุกไกด์และทุกรอบ Stygian">
{feed.page.items.length > 0 && <section aria-label={t("ความคิดเห็นจากทุกไกด์และทุกรอบ Stygian", "Comments from all guides and Stygian seasons")} className="flex min-w-0 flex-col">
<Table className="table-fixed max-md:block" aria-label={t("ความคิดเห็นจากทุกไกด์และทุกรอบ Stygian", "Comments from all guides and Stygian seasons")}>
<TableHeader className="max-md:sr-only"><TableRow>
<TableHead scope="col" className="px-0">ความคิดเห็น</TableHead>
<TableHead scope="col" className="w-[280px] px-0 text-right">เนื้อหา</TableHead>
<TableHead scope="col" className="px-0">{t("ความคิดเห็น", "Comments")}</TableHead>
<TableHead scope="col" className="w-[280px] px-0 text-right">{t("เนื้อหา", "Content")}</TableHead>
</TableRow></TableHeader>
<TableBody className="max-md:block">
{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}
overview={item.target.startsWith("stygian:") ? <Link href={item.href} prefetch={false} className="min-w-0 break-words rounded-lg py-2 text-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring">
{item.targetName}
</Link> : item.targetImage && <Link href={item.href} prefetch={false} className="shrink-0 rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-ring">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={item.targetImage} alt={`ภาพ Overview ของ ${item.targetName}`} loading="lazy" decoding="async" width={112} height={112} className="size-28 rounded-lg object-contain" />
<Image src={item.targetImage} alt={t(`ภาพ Overview ของ ${item.targetName}`, `Overview image for ${item.targetName}`)} sizes="112px" width={112} height={112} className="size-28 rounded-lg object-contain" />
</Link>} />)}
</TableBody>
</Table>
</section>}
{!feed.page.items.length && !feed.busy && !feed.error && <Empty><EmptyHeader><EmptyTitle>ไม่พบความคิดเห็นที่ตรงกัน</EmptyTitle><EmptyDescription>ความคิดเห็นจากทุกไกด์และทุกรอบ Stygian จะแสดงที่นี่</EmptyDescription></EmptyHeader></Empty>}
{!feed.page.items.length && !feed.busy && !feed.error && <Empty><EmptyHeader><EmptyTitle>{t("ไม่พบความคิดเห็นที่ตรงกัน", "No matching comments")}</EmptyTitle><EmptyDescription>{t("ความคิดเห็นจากทุกไกด์และทุกรอบ Stygian จะแสดงที่นี่", "Comments from every guide and Stygian season will appear here")}</EmptyDescription></EmptyHeader></Empty>}
{feed.busy && <AdminCommentRowsSkeleton count={feed.page.items.length ? 1 : 3} />}
{feed.page.nextCursor && <Button variant="outline" disabled={feed.busy} onClick={() => void feed.more()}>ดูความคิดเห็นเพิ่มเติม</Button>}
{feed.page.nextCursor && <Button variant="outline" disabled={feed.busy} onClick={() => void feed.more()}>{t("ดูความคิดเห็นเพิ่มเติม", "Load more comments")}</Button>}
</div>;
}