feat: add channel categories and improved creator detection

- Add category field to TelegramChannel (filterable tag like STL, PDF, D&D)
- Category column in channels table with edit via dropdown menu
- Improved creator extraction: filename patterns + channel title fallback
- extractCreatorFromChannelTitle strips [Completed], (Paid), emoji, etc.
- Fix ArchiveType in PackageListItem and PackageRow for new types
- Add Prisma migration for category column
This commit is contained in:
admin
2026-03-21 20:37:44 +01:00
parent 53a76a8136
commit 36a7e3d5f4
10 changed files with 126 additions and 14 deletions

View File

@@ -12,7 +12,7 @@ export interface PackageRow {
fileName: string;
fileSize: string;
contentHash: string;
archiveType: "ZIP" | "RAR";
archiveType: "ZIP" | "RAR" | "SEVEN_Z" | "DOCUMENT";
fileCount: number;
isMultipart: boolean;
hasPreview: boolean;

View File

@@ -8,6 +8,7 @@ import {
ArrowDownToLine,
ArrowUpFromLine,
RefreshCcw,
Tag,
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -25,6 +26,7 @@ interface ChannelColumnsProps {
onDelete: (id: string) => void;
onSetType: (id: string, type: "SOURCE" | "DESTINATION") => void;
onRescan: (id: string) => void;
onSetCategory: (id: string, category: string | null) => void;
}
export function getChannelColumns({
@@ -32,6 +34,7 @@ export function getChannelColumns({
onDelete,
onSetType,
onRescan,
onSetCategory,
}: ChannelColumnsProps): ColumnDef<ChannelRow, unknown>[] {
return [
{
@@ -63,6 +66,18 @@ export function getChannelColumns({
</Badge>
),
},
{
accessorKey: "category",
header: "Category",
cell: ({ row }) => {
const category = row.original.category;
return category ? (
<Badge variant="outline">{category}</Badge>
) : (
<span className="text-xs text-muted-foreground"></span>
);
},
},
{
accessorKey: "isActive",
header: "Status",
@@ -132,6 +147,15 @@ export function getChannelColumns({
Rescan Channel
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => {
const cat = prompt("Enter category (e.g. STL, PDF, D&D, Cosplay):", row.original.category ?? "");
if (cat !== null) onSetCategory(row.original.id, cat || null);
}}
>
<Tag className="mr-2 h-3.5 w-3.5" />
Set Category
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onToggleActive(row.original.id)}
>

View File

@@ -10,6 +10,7 @@ import {
deleteChannel,
toggleChannelActive,
setChannelType,
setChannelCategory,
rescanChannel,
} from "../actions";
import { DataTable } from "@/components/shared/data-table";
@@ -50,6 +51,13 @@ export function ChannelsTab({ channels, globalDestination, accounts }: ChannelsT
});
},
onRescan: (id) => setRescanId(id),
onSetCategory: (id, category) => {
startTransition(async () => {
const result = await setChannelCategory(id, category);
if (result.success) toast.success(category ? `Category set to "${category}"` : "Category removed");
else toast.error(result.error);
});
},
});
const { table } = useDataTable({

View File

@@ -259,6 +259,25 @@ export async function deleteChannel(id: string): Promise<ActionResult> {
}
}
export async function setChannelCategory(
id: string,
category: string | null
): Promise<ActionResult> {
const admin = await requireAdmin();
if (!admin.success) return admin;
try {
await prisma.telegramChannel.update({
where: { id },
data: { category: category?.trim() || null },
});
revalidatePath("/telegram");
return { success: true, data: undefined };
} catch {
return { success: false, error: "Failed to update category" };
}
}
export async function setChannelType(
id: string,
type: "SOURCE" | "DESTINATION"