diff --git a/docs/superpowers/plans/2026-07-04-send-all-from-creator.md b/docs/superpowers/plans/2026-07-04-send-all-from-creator.md new file mode 100644 index 0000000..856d843 --- /dev/null +++ b/docs/superpowers/plans/2026-07-04-send-all-from-creator.md @@ -0,0 +1,454 @@ +# Send All From Creator Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a toolbar button on the STL packages page that queues every sendable package from the currently-filtered creator for delivery to the user's Telegram. + +**Architecture:** A new server action `sendAllFromCreatorAction(creatorName)` mirrors the existing `sendAllInGroupAction`: it fetches sendable packages for the creator, dedups against live `BotSendRequest`s, creates a `BotSendRequest` per package, and fires `pg_notify('bot_send', id)`. The bot's existing `send-listener` consumes these unchanged. The client table renders a "Send all from [Creator]" button in the packages-tab toolbar only when `?creator=` is active, wired to the action with a confirm dialog and a count toast. + +**Tech Stack:** Next.js 16 App Router, TypeScript, Prisma v7, server actions, `pg_notify`, sonner toasts, lucide-react icons. + +**Testing note:** This repo has no test framework (`CLAUDE.md`: "Testing is manual"). Verification for each task is `npm run lint` + `npm run build`, plus manual UI checks at the end. Do not scaffold a test harness. + +--- + +### Task 1: Add `sendAllFromCreatorAction` server action + +**Files:** +- Modify: `src/app/(app)/stls/actions.ts` (append new action after `sendAllInGroupAction`, which ends at line 591) + +Reference implementation to mirror: `sendAllInGroupAction` at `src/app/(app)/stls/actions.ts:515-591`. +Existing imports already present in the file (do not re-add): `auth` from `@/lib/auth`, `prisma` from `@/lib/prisma`, `ActionResult` from `@/types/api.types`, `revalidatePath` from `next/cache`. +`ActionResult` is generic: `ActionResult` (`src/types/api.types.ts`). + +- [ ] **Step 1: Append the new action** + +Add this to the end of `src/app/(app)/stls/actions.ts`: + +```ts +export async function sendAllFromCreatorAction( + creatorName: string +): Promise> { + const session = await auth(); + if (!session?.user?.id) return { success: false, error: "Unauthorized" }; + + const creator = creatorName.trim(); + if (!creator) { + return { success: false, error: "No creator specified" }; + } + + try { + const telegramLink = await prisma.telegramLink.findUnique({ + where: { userId: session.user.id }, + }); + + if (!telegramLink) { + return { success: false, error: "No linked Telegram account. Link one in Settings." }; + } + + const sendablePackages = await prisma.package.findMany({ + where: { + creator, + destChannelId: { not: null }, + destMessageId: { not: null }, + }, + select: { id: true }, + }); + + if (sendablePackages.length === 0) { + return { success: false, error: "No uploaded packages found for this creator" }; + } + + let queued = 0; + let skipped = 0; + for (const pkg of sendablePackages) { + // Only create if no existing PENDING/SENDING request for this package+link combo + const existing = await prisma.botSendRequest.findFirst({ + where: { + packageId: pkg.id, + telegramLinkId: telegramLink.id, + status: { in: ["PENDING", "SENDING"] }, + }, + }); + + if (existing) { + skipped++; + continue; + } + + const sendRequest = await prisma.botSendRequest.create({ + data: { + packageId: pkg.id, + telegramLinkId: telegramLink.id, + requestedByUserId: session.user.id, + status: "PENDING", + }, + }); + + // Notify the bot via pg_notify + try { + await prisma.$queryRawUnsafe( + `SELECT pg_notify('bot_send', $1)`, + sendRequest.id + ); + } catch { + // Best-effort — the bot also polls periodically + } + + queued++; + } + + revalidatePath("/stls"); + return { success: true, data: { queued, skipped } }; + } catch { + return { success: false, error: "Failed to send creator packages" }; + } +} +``` + +- [ ] **Step 2: Verify lint + typecheck pass** + +Run: `npm run lint` +Expected: no new errors in `src/app/(app)/stls/actions.ts`. + +- [ ] **Step 3: Commit** + +```bash +git add src/app/\(app\)/stls/actions.ts +git commit -m "feat(stls): add sendAllFromCreatorAction server action" +``` + +--- + +### Task 2: Wire up the toolbar button in the STL table + +**Files:** +- Modify: `src/app/(app)/stls/_components/stl-table.tsx` + - lucide import (line 6) + - actions import block (lines 44-54) + - handler (after `handleSendAllInGroup`, which ends at line 278) + - `activeCreator` derivation (near `activeTag` at line 445) + - toolbar JSX (packages-tab toolbar row starting at line 478) + +- [ ] **Step 1: Add the `Send` icon to the lucide import** + +Change line 6 from: + +```ts +import { Search, Layers, Upload } from "lucide-react"; +``` + +to: + +```ts +import { Search, Layers, Upload, Send } from "lucide-react"; +``` + +- [ ] **Step 2: Import the new action** + +In the actions import block (lines 44-54), add `sendAllFromCreatorAction` next to `sendAllInGroupAction`: + +```ts +import { + updatePackageCreator, + updatePackageTags, + renameGroupAction, + dissolveGroupAction, + createGroupAction, + removeFromGroupAction, + sendAllInGroupAction, + sendAllFromCreatorAction, + updateGroupPreviewAction, + mergeGroupsAction, +} from "../actions"; +``` + +- [ ] **Step 3: Derive the active creator from the URL** + +Immediately after the `activeTag` line (`src/app/(app)/stls/_components/stl-table.tsx:445`): + +```ts + const activeTag = searchParams.get("tag") ?? ""; +``` + +add: + +```ts + const activeCreator = searchParams.get("creator") ?? ""; +``` + +- [ ] **Step 4: Add the click handler** + +After `handleSendAllInGroup` (which ends at line 278), add: + +```ts + const handleSendAllFromCreator = useCallback(() => { + if (!confirm(`Send all packages from "${activeCreator}" to your Telegram?`)) return; + startTransition(async () => { + const result = await sendAllFromCreatorAction(activeCreator); + if (result.success) { + const { queued, skipped } = result.data; + toast.success( + `Queued ${queued} package${queued === 1 ? "" : "s"} from ${activeCreator}` + + (skipped ? ` (${skipped} already queued)` : "") + ); + router.refresh(); + } else { + toast.error(result.error); + } + }); + }, [activeCreator, router]); +``` + +- [ ] **Step 5: Render the toolbar button** + +In the packages-tab toolbar (`flex flex-wrap items-center gap-2` row at line 478), add the button right after the "Upload Files" button (which closes at line 507, before the `selectedPackages.size >= 2` block at line 508): + +```tsx + {activeCreator && ( + + )} +``` + +- [ ] **Step 6: Verify lint + build pass** + +Run: `npm run lint && npm run build` +Expected: no new errors; build completes. + +- [ ] **Step 7: Manual verification** + +1. `npm run dev`, open `/stls`. +2. Click a creator name in the Creator column to apply `?creator=` (or navigate to `/stls?creator=`). +3. Confirm the "Send all from [Creator]" button appears in the toolbar. +4. Remove the creator filter → confirm the button disappears. +5. Click the button → confirm the dialog appears; on confirm, a toast reports the queued count. +6. (If a linked Telegram account + uploaded packages exist) confirm packages arrive via the bot. + +- [ ] **Step 8: Commit** + +```bash +git add src/app/\(app\)/stls/_components/stl-table.tsx +git commit -m "feat(stls): add 'send all from creator' toolbar button" +``` + +--- + +### Task 3: Searchable creator filter combobox (makes the filter reachable) + +**Why:** Discovered during execution — the `?creator=` filter that reveals the Task 2 +button had no UI trigger. The creator table cell click only opens the edit prompt +(`package-columns.tsx:339` → `onSetCreator`). This task adds a searchable +"All Creators" combobox to the packages-tab toolbar (like the existing Tags select, +but type-to-filter since there can be many creators). The creator cell stays +edit-only. + +**Files:** +- Modify: `src/lib/telegram/queries.ts` — add `getAllPackageCreators()`. +- Modify: `src/app/(app)/stls/page.tsx` — fetch + pass `availableCreators`. +- Create: `src/app/(app)/stls/_components/creator-filter.tsx` — the combobox. +- Modify: `src/app/(app)/stls/_components/stl-table.tsx` — new prop, handler, render. + +- [ ] **Step 1: Add the distinct-creators query** + +Append to `src/lib/telegram/queries.ts` (mirrors `getAllPackageTags` at line 530): + +```ts +export async function getAllPackageCreators(): Promise { + const result = await prisma.$queryRaw<{ creator: string }[]>` + SELECT DISTINCT creator FROM packages + WHERE creator IS NOT NULL AND creator <> '' + ORDER BY creator + `; + return result.map((r) => r.creator); +} +``` + +- [ ] **Step 2: Create the combobox component** + +Create `src/app/(app)/stls/_components/creator-filter.tsx` (mirrors the +Popover+Command pattern in `src/components/shared/data-table-faceted-filter.tsx`): + +```tsx +"use client"; + +import { useState } from "react"; +import { Check, ChevronsUpDown } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; + +interface CreatorFilterProps { + creators: string[]; + value: string; // active creator, "" when none + onChange: (creator: string) => void; // "" clears the filter +} + +export function CreatorFilter({ creators, value, onChange }: CreatorFilterProps) { + const [open, setOpen] = useState(false); + + return ( + + + + + + + + + No creators found. + + { + onChange(""); + setOpen(false); + }} + > + + All Creators + + {creators.map((creator) => ( + { + onChange(creator); + setOpen(false); + }} + > + + {creator} + + ))} + + + + + + ); +} +``` + +- [ ] **Step 3: Wire the query through the page** + +In `src/app/(app)/stls/page.tsx`: +- Add `getAllPackageCreators` to the import from `@/lib/telegram/queries` (line 3). +- Add `getAllPackageCreators()` to the `Promise.all` (line 27) and destructure + `availableCreators`: + ```ts + const [result, ingestionStatus, availableTags, availableCreators, skippedCount, ungroupedCount] = + await Promise.all([ + // ...existing entries unchanged... + getIngestionStatus(), + getAllPackageTags(), + getAllPackageCreators(), + countSkippedPackages(), + countUngroupedPackages(), + ]); + ``` + (Insert `getAllPackageCreators()` immediately after `getAllPackageTags()`, and add + `availableCreators` in the matching position of the destructure.) +- Pass the prop to ``: `availableCreators={availableCreators}` (next to + `availableTags={availableTags}`). + +- [ ] **Step 4: Add prop + handler + render in the table** + +In `src/app/(app)/stls/_components/stl-table.tsx`: +- Import the component: `import { CreatorFilter } from "./creator-filter";` +- Add to `StlTableProps` (next to `availableTags: string[];`): `availableCreators: string[];` +- Add to the destructured params (next to `availableTags,`): `availableCreators,` +- Add the handler after `updateTagFilter` (ends ~line 210): + ```ts + const updateCreatorFilter = useCallback( + (value: string) => { + const params = new URLSearchParams(searchParams.toString()); + if (value) { + params.set("creator", value); + params.set("page", "1"); + } else { + params.delete("creator"); + } + router.push(`${pathname}?${params.toString()}`, { scroll: false }); + }, + [router, pathname, searchParams] + ); + ``` +- Render it in the toolbar right after the Tags `Select` block (the + `{availableTags.length > 0 && (...)}` block, ~lines 507-522): + ```tsx + {availableCreators.length > 0 && ( + + )} + ``` + +- [ ] **Step 5: Verify + manual test** + +Run: `npx tsc --noEmit` (filter for the touched files — expect none new). Note: full +`npm run build` fails on a pre-existing stale Prisma client issue in +`src/lib/telegram/*` unrelated to this change; do not attempt to fix it. +Manual: open `/stls`, use the "All Creators" combobox, type to filter, pick a +creator → list filters and the "Send all from [Creator]" button appears; pick +"All Creators" → filter clears and button disappears. + +- [ ] **Step 6: Commit** + +```bash +git add "src/lib/telegram/queries.ts" "src/app/(app)/stls/page.tsx" \ + "src/app/(app)/stls/_components/creator-filter.tsx" \ + "src/app/(app)/stls/_components/stl-table.tsx" +git commit -m "feat(stls): add searchable creator filter combobox to toolbar" +``` + +--- + +## Self-Review + +**Spec coverage:** +- Req 1 (button when filtered by creator) → Task 2 Step 5 (`{activeCreator && ...}`). +- Req 2 (hidden when no filter) → Task 2 Step 5 conditional. +- Req 3 (confirm dialog) → Task 2 Step 4 `confirm(...)`. +- Req 4 (all pages, not current page) → Task 1 queries `prisma.package.findMany` by `creator`, unpaginated. +- Req 5 (skip not-uploaded + dedup live requests) → Task 1 `where` filter on `destChannelId`/`destMessageId` + `existing` check. +- Req 6 (report counts) → Task 1 returns `{ queued, skipped }`; Task 2 Step 4 toast. +- Req 7 (no polling) → Task 2 handler queues + `router.refresh()`, no poll loop. +- Blank-creator guard → Task 1 `creator.trim()` check. + +**Placeholder scan:** No TBD/TODO/placeholder steps; all code shown in full. + +**Type consistency:** Action returns `ActionResult<{ queued: number; skipped: number }>`; handler destructures `result.data.{queued,skipped}` inside the `result.success` branch (where `data` is typed). `sendAllFromCreatorAction` name matches between Task 1 definition, Task 2 import, and Task 2 call site. `activeCreator` defined once (Step 3) and used in Steps 4-5. diff --git a/docs/superpowers/specs/2026-07-04-send-all-from-creator-design.md b/docs/superpowers/specs/2026-07-04-send-all-from-creator-design.md new file mode 100644 index 0000000..3b7535c --- /dev/null +++ b/docs/superpowers/specs/2026-07-04-send-all-from-creator-design.md @@ -0,0 +1,170 @@ +# Send All From Creator — Design + +**Date:** 2026-07-04 +**Status:** Approved for planning + +## Summary + +Add a "Send all from [Creator]" button to the STL packages page that queues every +sendable package from a given creator for delivery to the user's Telegram. The +button appears in the packages-tab toolbar only when the list is filtered by a +single creator (`?creator=`). + +This reuses the existing single-send infrastructure (`BotSendRequest` + +`pg_notify('bot_send', id)` + the bot's `send-listener`) and closely mirrors the +existing `sendAllInGroupAction`. No bot, DB schema, or send-listener changes are +required. + +## Context + +Relevant existing pieces: + +- **Creator field:** `Package.creator` is a nullable, indexed `String` on the + `Package` model (`prisma/schema.prisma:476-524`). Filtering by exact creator is + cheap. +- **Creator filter:** The packages list already supports `?creator=` + (`src/app/(app)/stls/page.tsx:22,38`), rendered by the packages column as a + clickable value (`package-columns.tsx`). +- **Single send:** `send-to-telegram-button.tsx` → `POST /api/telegram/bot/send` + creates a `BotSendRequest` (status `PENDING`) and fires + `pg_notify('bot_send', requestId)`; the button then polls the request to + completion. +- **Existing bulk send (group):** `sendAllInGroupAction(groupId)` in + `src/app/(app)/stls/actions.ts:515-591` fetches the group's packages, filters to + those with `destChannelId` + `destMessageId`, skips any with an existing + `PENDING`/`SENDING` request for the same package + telegram link, creates a + `BotSendRequest` per remaining package, and fires `pg_notify` per package. It is + wired into the table via `handleSendAllInGroup` (`stl-table.tsx:264-278`) with a + `confirm()` dialog and a success toast — no polling. +- **Bot side:** `bot/src/send-listener.ts` processes every `BotSendRequest` + uniformly; nothing there needs to change. + +## Requirements + +1. When the packages tab is filtered by a single creator (`?creator=` is + present), show a toolbar button labeled **"Send all from [Creator]"**. +2. When no creator filter is active, the button is not rendered. +3. Clicking the button shows a confirmation dialog before sending (consistent with + the single-send and group-send flows, since this pushes files to Telegram). +4. On confirm, queue **all** sendable packages from that creator across all pages — + not just the current page. +5. Skip packages that are not yet uploaded (missing `destChannelId` / + `destMessageId`) and packages that already have a `PENDING`/`SENDING` request for + the same package + telegram link (dedup). +6. Report real counts back to the user via toast, e.g. + "Queued 12 packages from [Creator]" and, when applicable, a skipped count. +7. No polling — the bulk action queues and returns; sends process in the background + via the bot listener (same behavior as group send). + +## Design + +### Server action — `sendAllFromCreatorAction(creatorName: string)` + +New action in `src/app/(app)/stls/actions.ts`, mirroring `sendAllInGroupAction`: + +1. Auth check (`auth()`), return `Unauthorized` if no session. +2. Load the user's `TelegramLink`; if none, return + "No linked Telegram account. Link one in Settings." +3. Reject an empty/blank `creatorName` (guard against sending the entire "no + creator" set — see Edge cases). +4. Fetch sendable packages directly with a `where` filter (no in-memory filtering): + ```ts + prisma.package.findMany({ + where: { + creator: creatorName, + destChannelId: { not: null }, + destMessageId: { not: null }, + }, + select: { id: true }, + }) + ``` +5. If none, return "No uploaded packages found for this creator." +6. For each package, skip if an existing `PENDING`/`SENDING` `BotSendRequest` + exists for that package + telegram link; otherwise create a `BotSendRequest` + (status `PENDING`) and fire `pg_notify('bot_send', id)` (best-effort, wrapped in + try/catch — the bot also polls). +7. `revalidatePath("/stls")`. +8. Return counts so the UI can show them. This is a small improvement over + `sendAllInGroupAction`, which currently returns `{ success: true, data: undefined }`. + Shape: + ```ts + { success: true, data: { queued: number, skipped: number } } + ``` + where `skipped` counts packages that already had a live request. (Packages not + yet uploaded are excluded by the query and not counted as skipped.) + +### UI — toolbar button in `stl-table.tsx` + +- Derive the active creator from the URL: `const activeCreator = + searchParams.get("creator") ?? "";` (parallel to the existing + `activeTag` at `stl-table.tsx:445`). +- In the packages-tab toolbar (`stl-table.tsx:478` `flex flex-wrap items-center + gap-2` row), render the button only when `activeCreator` is non-empty: + ```tsx + {activeCreator && ( + + )} + ``` + (Reuse the icon already used by the send action; place near the Upload / Group + buttons.) +- Add `handleSendAllFromCreator`, modeled on `handleSendAllInGroup` + (`stl-table.tsx:264-278`): + ```ts + const handleSendAllFromCreator = useCallback(() => { + if (!confirm(`Send all packages from "${activeCreator}" to your Telegram?`)) return; + startTransition(async () => { + const result = await sendAllFromCreatorAction(activeCreator); + if (result.success) { + const { queued, skipped } = result.data; + toast.success( + `Queued ${queued} package${queued === 1 ? "" : "s"} from ${activeCreator}` + + (skipped ? ` (${skipped} already queued)` : "") + ); + router.refresh(); + } else { + toast.error(result.error); + } + }); + }, [activeCreator, router]); + ``` +- Import `sendAllFromCreatorAction` alongside the existing `sendAllInGroupAction` + import (`stl-table.tsx:51`). + +### Data flow + +``` +User (filtered by creator) → clicks button → confirm dialog + → sendAllFromCreatorAction(creatorName) + → for each sendable, un-queued package: create BotSendRequest + pg_notify + → returns { queued, skipped } → toast + router.refresh() + +(background) bot send-listener consumes each BotSendRequest → delivers to user +``` + +## Edge cases + +- **Blank creator:** The action rejects an empty/whitespace `creatorName` so a + stray `?creator=` never queues every package. The UI already gates the button on + a non-empty `activeCreator`, so this is defense-in-depth. +- **No linked Telegram account:** Returns an error toast, queues nothing. +- **No uploaded packages for creator:** Returns an error toast, queues nothing. +- **All packages already queued:** `queued: 0`, `skipped: N` → toast reflects it. +- **pg_notify failure:** Swallowed per-package (best-effort); the bot's periodic + poll picks the request up. Matches existing behavior. + +## Out of scope / non-goals + +- No per-row "send all from this creator" action (toolbar-only, per decision). +- No progress polling / live completion tracking for the bulk send. +- No changes to the bot, `send-listener`, `BotSendRequest` schema, or API routes. +- No batching/throttling beyond what the bot listener already does. + +## Files touched + +- `src/app/(app)/stls/actions.ts` — add `sendAllFromCreatorAction`. +- `src/app/(app)/stls/_components/stl-table.tsx` — derive `activeCreator`, add + `handleSendAllFromCreator`, render the toolbar button, import the new action. diff --git a/src/app/(app)/stls/_components/creator-filter.tsx b/src/app/(app)/stls/_components/creator-filter.tsx new file mode 100644 index 0000000..e744bc3 --- /dev/null +++ b/src/app/(app)/stls/_components/creator-filter.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { useState } from "react"; +import { Check, ChevronsUpDown } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; + +interface CreatorFilterProps { + creators: string[]; + value: string; // active creator, "" when none + onChange: (creator: string) => void; // "" clears the filter +} + +export function CreatorFilter({ creators, value, onChange }: CreatorFilterProps) { + const [open, setOpen] = useState(false); + + return ( + + + + + + + + + No creators found. + + { + onChange(""); + setOpen(false); + }} + > + + All Creators + + {creators.map((creator) => ( + { + onChange(creator); + setOpen(false); + }} + > + + {creator} + + ))} + + + + + + ); +} diff --git a/src/app/(app)/stls/_components/stl-table.tsx b/src/app/(app)/stls/_components/stl-table.tsx index 9235e4d..d914bb0 100644 --- a/src/app/(app)/stls/_components/stl-table.tsx +++ b/src/app/(app)/stls/_components/stl-table.tsx @@ -3,7 +3,7 @@ import { useState, useCallback, useTransition, useMemo, useRef } from "react"; import { useRouter, usePathname, useSearchParams } from "next/navigation"; import { toast } from "sonner"; -import { Search, Layers, Upload } from "lucide-react"; +import { Search, Layers, Upload, Send } from "lucide-react"; import { UploadDialog } from "./upload-dialog"; import { useDataTable } from "@/hooks/use-data-table"; import { @@ -16,6 +16,7 @@ import { import { PackageFilesDrawer } from "./package-files-drawer"; import { IngestionStatus } from "./ingestion-status"; import { SkippedPackagesTab } from "./skipped-packages-tab"; +import { CreatorFilter } from "./creator-filter"; import { DataTable } from "@/components/shared/data-table"; import { DataTablePagination } from "@/components/shared/data-table-pagination"; import { DataTableViewOptions } from "@/components/shared/data-table-view-options"; @@ -49,6 +50,7 @@ import { createGroupAction, removeFromGroupAction, sendAllInGroupAction, + sendAllFromCreatorAction, updateGroupPreviewAction, mergeGroupsAction, } from "../actions"; @@ -59,6 +61,7 @@ interface StlTableProps { totalCount: number; ingestionStatus: IngestionAccountStatus[]; availableTags: string[]; + availableCreators: string[]; searchTerm: string; skippedData: SkippedRow[]; skippedPageCount: number; @@ -74,6 +77,7 @@ export function StlTable({ totalCount, ingestionStatus, availableTags, + availableCreators, searchTerm, skippedData, skippedPageCount, @@ -85,6 +89,7 @@ export function StlTable({ const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); + const activeCreator = searchParams.get("creator") ?? ""; const [searchValue, setSearchValue] = useState(searchParams.get("search") ?? ""); const [viewPkg, setViewPkg] = useState(null); @@ -207,6 +212,20 @@ export function StlTable({ [router, pathname, searchParams] ); + const updateCreatorFilter = useCallback( + (value: string) => { + const params = new URLSearchParams(searchParams.toString()); + if (value) { + params.set("creator", value); + params.set("page", "1"); + } else { + params.delete("creator"); + } + router.push(`${pathname}?${params.toString()}`, { scroll: false }); + }, + [router, pathname, searchParams] + ); + const activeTab = searchParams.get("tab") ?? "packages"; const updateTab = useCallback( @@ -277,6 +296,23 @@ export function StlTable({ [router] ); + const handleSendAllFromCreator = useCallback(() => { + if (!confirm(`Send all packages from "${activeCreator}" to your Telegram?`)) return; + startTransition(async () => { + const result = await sendAllFromCreatorAction(activeCreator); + if (result.success) { + const { queued, skipped } = result.data; + toast.success( + `Queued ${queued} package${queued === 1 ? "" : "s"} from ${activeCreator}` + + (skipped ? ` (${skipped} already queued)` : "") + ); + router.refresh(); + } else { + toast.error(result.error); + } + }); + }, [activeCreator, router]); + const handleRemoveFromGroup = useCallback( (packageId: string) => { startTransition(async () => { @@ -500,11 +536,29 @@ export function StlTable({ )} + {availableCreators.length > 0 && ( + + )} + {activeCreator && ( + + )} {selectedPackages.size >= 2 && (