mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-05-11 06:11:15 +00:00
feat: fix channel scanning bugs, add package tags, and kickstarters tab
Bug fixes: - Fix channels not being scanned by paginating TDLib getChats (was only loading first batch, additional channels were unknown to TDLib) - Add per-channel getChat pre-load as safety net before scanning - Fix preview pictures not loading by checking previewData instead of previewMsgId for hasPreview flag - Prevent previewMsgId from being set when preview download fails Package Tags: - Add tags Text[] column to Package with migration backfilling from channel categories - Worker auto-inherits source channel category as initial tag - Tag filter dropdown and Tags column in STL Files table - Server actions for individual and bulk tag editing Kickstarters Tab: - New KickstarterHost, Kickstarter, and KickstarterPackage models - Full CRUD with delivery status, payment status, host management - Package linking (many-to-many with existing packages) - Sidebar entry with Gift icon - Table with search, filters, modal forms Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
187
src/app/(app)/kickstarters/_components/kickstarter-columns.tsx
Normal file
187
src/app/(app)/kickstarters/_components/kickstarter-columns.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
"use client";
|
||||
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import { MoreHorizontal, Pencil, Trash2, ExternalLink } from "lucide-react";
|
||||
import { DataTableColumnHeader } from "@/components/shared/data-table-column-header";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export interface KickstarterRow {
|
||||
id: string;
|
||||
name: string;
|
||||
link: string | null;
|
||||
filesUrl: string | null;
|
||||
deliveryStatus: "NOT_DELIVERED" | "PARTIAL" | "DELIVERED";
|
||||
paymentStatus: "PAID" | "UNPAID";
|
||||
notes: string | null;
|
||||
hostId: string | null;
|
||||
userId: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
host: { id: string; name: string } | null;
|
||||
_count: { packages: number };
|
||||
}
|
||||
|
||||
interface KickstarterColumnsProps {
|
||||
onEdit: (kickstarter: KickstarterRow) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
const deliveryConfig: Record<string, { label: string; className: string }> = {
|
||||
NOT_DELIVERED: {
|
||||
label: "Not Delivered",
|
||||
className: "bg-red-500/15 text-red-400 border-red-500/30",
|
||||
},
|
||||
PARTIAL: {
|
||||
label: "Partial",
|
||||
className: "bg-orange-500/15 text-orange-400 border-orange-500/30",
|
||||
},
|
||||
DELIVERED: {
|
||||
label: "Delivered",
|
||||
className: "bg-emerald-500/15 text-emerald-400 border-emerald-500/30",
|
||||
},
|
||||
};
|
||||
|
||||
const paymentConfig: Record<string, { label: string; className: string }> = {
|
||||
PAID: {
|
||||
label: "Paid",
|
||||
className: "bg-emerald-500/15 text-emerald-400 border-emerald-500/30",
|
||||
},
|
||||
UNPAID: {
|
||||
label: "Unpaid",
|
||||
className: "bg-red-500/15 text-red-400 border-red-500/30",
|
||||
},
|
||||
};
|
||||
|
||||
export function getKickstarterColumns({
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: KickstarterColumnsProps): ColumnDef<KickstarterRow, unknown>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Name" />,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{row.original.name}</span>
|
||||
{row.original.link && (
|
||||
<a
|
||||
href={row.original.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:text-primary/80"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "host",
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Host" />,
|
||||
cell: ({ row }) =>
|
||||
row.original.host ? (
|
||||
<span className="text-sm">{row.original.host.name}</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">--</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "files",
|
||||
header: "Files",
|
||||
cell: ({ row }) =>
|
||||
row.original.filesUrl ? (
|
||||
<a
|
||||
href={row.original.filesUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-sm text-primary hover:underline"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-muted-foreground">--</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "deliveryStatus",
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Delivery" />,
|
||||
cell: ({ row }) => {
|
||||
const config = deliveryConfig[row.original.deliveryStatus];
|
||||
return (
|
||||
<Badge variant="outline" className={`text-[10px] font-medium ${config.className}`}>
|
||||
{config.label}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "paymentStatus",
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Payment" />,
|
||||
cell: ({ row }) => {
|
||||
const config = paymentConfig[row.original.paymentStatus];
|
||||
return (
|
||||
<Badge variant="outline" className={`text-[10px] font-medium ${config.className}`}>
|
||||
{config.label}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "packages",
|
||||
header: "Packages",
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{row.original._count.packages}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Created" />,
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{new Date(row.original.createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => onEdit(row.original)}>
|
||||
<Pencil className="mr-2 h-3.5 w-3.5" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => onDelete(row.original.id)}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<Trash2 className="mr-2 h-3.5 w-3.5" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
),
|
||||
enableHiding: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user