mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-07-22 23:12:02 +00:00
feat(stls): add searchable creator filter combobox to toolbar
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>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
@@ -60,6 +61,7 @@ interface StlTableProps {
|
||||
totalCount: number;
|
||||
ingestionStatus: IngestionAccountStatus[];
|
||||
availableTags: string[];
|
||||
availableCreators: string[];
|
||||
searchTerm: string;
|
||||
skippedData: SkippedRow[];
|
||||
skippedPageCount: number;
|
||||
@@ -75,6 +77,7 @@ export function StlTable({
|
||||
totalCount,
|
||||
ingestionStatus,
|
||||
availableTags,
|
||||
availableCreators,
|
||||
searchTerm,
|
||||
skippedData,
|
||||
skippedPageCount,
|
||||
@@ -209,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(
|
||||
@@ -519,6 +536,13 @@ 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" />
|
||||
|
||||
@@ -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