From 20853367480165832dada9aec917880283a4bdf3 Mon Sep 17 00:00:00 2001 From: neodk2004 Date: Wed, 12 Aug 2026 20:48:41 +0200 Subject: [PATCH] feat(admin): comma-separated bulk add on the Blacklist page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The add-entry input took the whole field as one literal value, so typing "word1, word2" created a single entry matching that exact phrase instead of two separate words. Splits on comma client-side and calls the existing single-value createBlacklistEntry mutation once per value (dedup'd), with one consolidated toast instead of one per word. No backend change — reuses the existing retroactive-scan mutation as-is. --- src/app/p/[secret]/blacklist/page.tsx | 78 ++++++++++++++++++--------- 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/src/app/p/[secret]/blacklist/page.tsx b/src/app/p/[secret]/blacklist/page.tsx index e76e74b..474c838 100644 --- a/src/app/p/[secret]/blacklist/page.tsx +++ b/src/app/p/[secret]/blacklist/page.tsx @@ -40,22 +40,8 @@ export default function AdminBlacklistPage() { ) ); - const createMutation = useMutation( - trpc.admin.createBlacklistEntry.mutationOptions({ - onSuccess: ({ hiddenCount }) => { - void qc.invalidateQueries({ queryKey: trpc.admin.listBlacklist.queryKey() }); - setNewValue(""); - if (hiddenCount > 0) { - toast.success(`Added — ${hiddenCount} existing post${hiddenCount !== 1 ? "s" : ""} hidden retroactively.`); - } else { - toast.success("Blacklist entry added."); - } - }, - onError: (err) => { - toast.error(`Failed to add entry: ${err.message}`); - }, - }) - ); + const createMutation = useMutation(trpc.admin.createBlacklistEntry.mutationOptions()); + const [isBulkAdding, setIsBulkAdding] = useState(false); const deleteMutation = useMutation( trpc.admin.deleteBlacklistEntry.mutationOptions({ @@ -66,10 +52,45 @@ export default function AdminBlacklistPage() { }) ); - function handleCreate() { - const value = newValue.trim(); - if (!value) return; - createMutation.mutate({ type: newType, value }); + /** + * Comma-separated bulk add — splits on "," so pasting "word1, word2, word3" + * creates one entry per word instead of a single literal-phrase entry. + * Reuses the existing single-value createBlacklistEntry mutation (with its + * retroactive scan) once per word rather than a new batch endpoint. + */ + async function handleCreate() { + const values = [...new Set(newValue.split(",").map((v) => v.trim()).filter(Boolean))]; + if (values.length === 0) return; + + setIsBulkAdding(true); + let addedCount = 0; + let hiddenTotal = 0; + let failedCount = 0; + + for (const value of values) { + try { + const { hiddenCount } = await createMutation.mutateAsync({ type: newType, value }); + addedCount++; + hiddenTotal += hiddenCount; + } catch { + failedCount++; + } + } + + setIsBulkAdding(false); + void qc.invalidateQueries({ queryKey: trpc.admin.listBlacklist.queryKey() }); + setNewValue(""); + + if (addedCount > 0) { + const suffix = + hiddenTotal > 0 + ? ` — ${hiddenTotal} existing post${hiddenTotal !== 1 ? "s" : ""} hidden retroactively.` + : ""; + toast.success(`${addedCount} ${addedCount !== 1 ? "entries" : "entry"} added.${suffix}`); + } + if (failedCount > 0) { + toast.error(`${failedCount} ${failedCount !== 1 ? "entries" : "entry"} failed to add.`); + } } return ( @@ -79,6 +100,7 @@ export default function AdminBlacklistPage() {

Words, links, and hashtags that auto-hide matching posts — new posts are checked on creation, and adding an entry here retroactively scans + hides existing matches. + Separate multiple values with a comma to add them all at once.

@@ -96,19 +118,25 @@ export default function AdminBlacklistPage() { setNewValue(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleCreate()} + onKeyDown={(e) => { + if (e.key === "Enter") void handleCreate(); + }} className="flex-1 min-w-[200px] bg-zinc-800 border-zinc-700 text-zinc-100 placeholder:text-zinc-500" />