mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-07-22 23:12:02 +00:00
Merge feat/send-all-from-creator: send-all-from-creator button + searchable creator filter
This commit is contained in:
82
src/app/(app)/stls/_components/creator-filter.tsx
Normal file
82
src/app/(app)/stls/_components/creator-filter.tsx
Normal file
@@ -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 (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="h-9 w-[200px] justify-between"
|
||||
>
|
||||
<span className="truncate">{value || "All Creators"}</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[240px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search creators..." className="h-9" />
|
||||
<CommandList>
|
||||
<CommandEmpty>No creators found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
value="__all__"
|
||||
onSelect={() => {
|
||||
onChange("");
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn("mr-2 h-4 w-4", value === "" ? "opacity-100" : "opacity-0")}
|
||||
/>
|
||||
All Creators
|
||||
</CommandItem>
|
||||
{creators.map((creator) => (
|
||||
<CommandItem
|
||||
key={creator}
|
||||
value={creator}
|
||||
onSelect={() => {
|
||||
onChange(creator);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
value === creator ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<span className="truncate">{creator}</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -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<PackageRow | null>(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({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
{availableCreators.length > 0 && (
|
||||
<CreatorFilter
|
||||
creators={availableCreators}
|
||||
value={activeCreator}
|
||||
onChange={updateCreatorFilter}
|
||||
/>
|
||||
)}
|
||||
<DataTableViewOptions table={table} />
|
||||
<Button variant="outline" size="sm" className="h-9" onClick={() => setUploadOpen(true)}>
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
Upload Files
|
||||
</Button>
|
||||
{activeCreator && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-9 gap-1.5"
|
||||
onClick={handleSendAllFromCreator}
|
||||
>
|
||||
<Send className="h-3.5 w-3.5" />
|
||||
Send all from {activeCreator}
|
||||
</Button>
|
||||
)}
|
||||
{selectedPackages.size >= 2 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -589,3 +589,82 @@ export async function sendAllInGroupAction(
|
||||
return { success: false, error: "Failed to send group packages" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendAllFromCreatorAction(
|
||||
creatorName: string
|
||||
): Promise<ActionResult<{ queued: number; skipped: number }>> {
|
||||
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" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { auth } from "@/lib/auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { listDisplayItems, searchPackages, getIngestionStatus, getAllPackageTags, listSkippedPackages, countSkippedPackages, listUngroupedPackages, countUngroupedPackages } from "@/lib/telegram/queries";
|
||||
import { listDisplayItems, searchPackages, getIngestionStatus, getAllPackageTags, getAllPackageCreators, listSkippedPackages, countSkippedPackages, listUngroupedPackages, countUngroupedPackages } from "@/lib/telegram/queries";
|
||||
import { StlTable } from "./_components/stl-table";
|
||||
import type { DisplayItem, PackageListItem } from "@/lib/telegram/types";
|
||||
|
||||
@@ -24,7 +24,7 @@ export default async function StlFilesPage({ searchParams }: Props) {
|
||||
const tab = (params.tab as string) ?? "packages";
|
||||
|
||||
// Fetch packages, ingestion status, tags, and skipped count in parallel
|
||||
const [result, ingestionStatus, availableTags, skippedCount, ungroupedCount] = await Promise.all([
|
||||
const [result, ingestionStatus, availableTags, availableCreators, skippedCount, ungroupedCount] = await Promise.all([
|
||||
search
|
||||
? searchPackages({
|
||||
query: search,
|
||||
@@ -42,6 +42,7 @@ export default async function StlFilesPage({ searchParams }: Props) {
|
||||
}),
|
||||
getIngestionStatus(),
|
||||
getAllPackageTags(),
|
||||
getAllPackageCreators(),
|
||||
countSkippedPackages(),
|
||||
countUngroupedPackages(),
|
||||
]);
|
||||
@@ -68,6 +69,7 @@ export default async function StlFilesPage({ searchParams }: Props) {
|
||||
totalCount={result.pagination.total}
|
||||
ingestionStatus={ingestionStatus}
|
||||
availableTags={availableTags}
|
||||
availableCreators={availableCreators}
|
||||
searchTerm={search}
|
||||
skippedData={skippedResult?.items ?? []}
|
||||
skippedPageCount={skippedResult?.pagination.totalPages ?? 0}
|
||||
|
||||
@@ -534,6 +534,15 @@ export async function getAllPackageTags(): Promise<string[]> {
|
||||
return result.map((r) => r.tag);
|
||||
}
|
||||
|
||||
export async function getAllPackageCreators(): Promise<string[]> {
|
||||
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);
|
||||
}
|
||||
|
||||
export async function getIngestionStatus(): Promise<IngestionAccountStatus[]> {
|
||||
const accounts = await prisma.telegramAccount.findMany({
|
||||
orderBy: { createdAt: "asc" },
|
||||
|
||||
Reference in New Issue
Block a user