/** * Comment moderation inbox. * * Status tabs (Pending, Approved, Spam, Trash), search, collection filter, * table with row actions, bulk selection, and detail slide-over. */ import { Badge, Button, Checkbox, Input, Select, Tabs } from "@cloudflare/kumo"; import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import { MagnifyingGlass, Check, Trash, Warning, ChatCircle } from "@phosphor-icons/react"; import * as React from "react"; import type { AdminComment, CommentCounts, CommentStatus, BulkAction, } from "../../lib/api/comments.js"; import { cn } from "../../lib/utils.js"; import { CaretNext, CaretPrev } from "../ArrowIcons.js"; import { ConfirmDialog } from "../ConfirmDialog.js"; import { CommentDetail } from "./CommentDetail.js"; // --------------------------------------------------------------------------- // Props // --------------------------------------------------------------------------- export interface CommentInboxProps { comments: AdminComment[]; counts: CommentCounts; isLoading: boolean; nextCursor?: string; collections: Record; activeStatus: CommentStatus; onStatusChange: (status: CommentStatus) => void; collectionFilter: string; onCollectionFilterChange: (collection: string) => void; searchQuery: string; onSearchChange: (query: string) => void; onCommentStatusChange: (id: string, status: CommentStatus) => Promise; onCommentDelete: (id: string) => Promise; onBulkAction: (ids: string[], action: BulkAction) => Promise; onLoadMore: () => void; isAdmin: boolean; isStatusPending: boolean; deleteError: unknown; onDeleteErrorReset: () => void; } // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- const PAGE_SIZE = 20; export function CommentInbox({ comments, counts, isLoading, nextCursor, collections, activeStatus, onStatusChange, collectionFilter, onCollectionFilterChange, searchQuery, onSearchChange, onCommentStatusChange, onCommentDelete, onBulkAction, onLoadMore, isAdmin, isStatusPending, deleteError, onDeleteErrorReset, }: CommentInboxProps) { const { t } = useLingui(); // Selection state const [selected, setSelected] = React.useState>(new Set()); const [detailComment, setDetailComment] = React.useState(null); const [deleteId, setDeleteId] = React.useState(null); // Pagination (client-side within loaded data) const [page, setPage] = React.useState(0); // Reset selection and page when status tab or filters change React.useEffect(() => { setSelected(new Set()); setPage(0); }, [activeStatus, collectionFilter, searchQuery]); const clearSelection = React.useCallback(() => setSelected(new Set()), []); const totalPages = Math.max(1, Math.ceil(comments.length / PAGE_SIZE)); const paginatedComments = comments.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE); // Bulk select const allOnPageSelected = paginatedComments.length > 0 && paginatedComments.every((c) => selected.has(c.id)); const toggleAll = () => { setSelected((prev) => { const next = new Set(prev); if (allOnPageSelected) { for (const c of paginatedComments) next.delete(c.id); } else { for (const c of paginatedComments) next.add(c.id); } return next; }); }; const toggleOne = (id: string) => { setSelected((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); }; const handleBulk = (action: BulkAction) => { if (selected.size === 0) return; void onBulkAction([...selected], action).then(clearSelection); }; // Collection filter items const collectionItems: Record = { "": t`All collections` }; for (const [slug, config] of Object.entries(collections)) { collectionItems[slug] = config.label; } const total = counts.pending + counts.approved + counts.spam + counts.trash; return (
{/* Header */}

{t`Comments`}

{total > 0 && ( {plural(total, { one: "# total", other: "# total" })} )}
{/* Filters row */}
{/* Search */}
onSearchChange(e.target.value)} className="ps-9" />
{/* Collection filter */} {Object.keys(collections).length > 1 && (