"use client"; import { Eye, Pencil, Wifi, WifiOff } from "lucide-react"; import Link from "next/link"; import { useState } from "react"; import { toast } from "sonner"; import { bulkBan, bulkGiveBadge, bulkGiveCurrency, bulkUnban, } from "@/actions/bulk-users"; import { DataTable } from "@/components/admin/data-table"; import { CurrencyIcon } from "@/components/shared/currency-icon"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { getAvatarUrl } from "@/lib/imager"; import type { DataTableColumn, PaginatedResult } from "@/types"; interface UserRow { id: number; username: string; mail: string; rank: number; credits: number; pixels: number; online: string; motto: string; look: string; ipRegister: string; accountCreated: number; lastLogin: number | null; } const RANK_LABELS: Record = { 1: "User", 2: "Bronze", 3: "Silver", 4: "Gold", 5: "Moderator", 6: "Admin", 7: "Super Admin", }; function getColumns(canEdit: boolean): DataTableColumn[] { const cols: DataTableColumn[] = [ { key: "id", label: "ID", sortable: true }, { key: "username", label: "User", sortable: true, render: (_, row) => (
{row.username} {row.motto && ( {row.motto} )}
), }, { key: "mail", label: "Email", sortable: true }, { key: "rank", label: "Rank", sortable: true, filterKey: "filter_rank", filterOptions: [ { label: "User (1)", value: "1" }, { label: "Bronze (2)", value: "2" }, { label: "Silver (3)", value: "3" }, { label: "Gold (4)", value: "4" }, { label: "Moderator (5)", value: "5" }, { label: "Admin (6)", value: "6" }, { label: "Super Admin (7+)", value: "7" }, ], render: (value) => { const r = Number(value); return ( = 7 ? "default" : r >= 5 ? "secondary" : "outline"} > {RANK_LABELS[r] || `Rank ${r}`} ); }, }, { key: "credits", label: "Credits", sortable: true, render: (value) => ( {Number(value).toLocaleString()} ), }, { key: "online", label: "Status", filterKey: "filter_online", filterOptions: [ { label: "Online", value: "1" }, { label: "Offline", value: "0" }, ], render: (value) => value === "1" ? ( Online ) : ( Offline ), }, { key: "accountCreated", label: "Registered", sortable: true, render: (value) => { const ts = Number(value); if (!ts) return "-"; return ( {new Date(ts * 1000).toLocaleDateString("it-IT", { day: "2-digit", month: "short", year: "numeric", })} ); }, }, { key: "actions", label: "", render: (_, row) => (
{canEdit && ( )}
), }, ]; return cols; } interface UsersTableProps { data: PaginatedResult; canEdit: boolean; canBan: boolean; } type PendingDialog = | { kind: "ban"; ids: number[] } | { kind: "give-currency"; ids: number[] } | { kind: "give-badge"; ids: number[] } | null; export function UsersTable({ data, canEdit, canBan }: UsersTableProps) { const [pending, setPending] = useState(null); const [busy, setBusy] = useState(false); const [banReason, setBanReason] = useState("Bulk ban"); const [banDuration, setBanDuration] = useState("-1"); const [currencyType, setCurrencyType] = useState< "credits" | "pixels" | "points" >("credits"); const [currencyAmount, setCurrencyAmount] = useState("100"); const [badgeCode, setBadgeCode] = useState(""); function reportResult( label: string, ok: number, total: number, failures = 0, ) { if (ok > 0) toast.success( `${label}: ${ok}/${total}${failures ? ` · ${failures} failed` : ""}`, ); if (failures > 0 && ok === 0) toast.error(`${label} failed for ${failures} users`); } async function handleBulkAction(action: string, ids: (number | string)[]) { const numericIds = ids.map((i) => Number(i)); if (action === "unban") { setBusy(true); try { const result = await bulkUnban({ userIds: numericIds }); if (result.ok) { const data = result.data as { unbanned: number; total: number }; reportResult("Unbanned", data.unbanned, data.total); } else toast.error(result.error); } finally { setBusy(false); } return; } if (action === "ban") { setPending({ kind: "ban", ids: numericIds }); return; } if (action === "give-currency") { setPending({ kind: "give-currency", ids: numericIds }); return; } if (action === "give-badge") { setPending({ kind: "give-badge", ids: numericIds }); return; } } async function confirmBan() { if (pending?.kind !== "ban") return; setBusy(true); try { const result = await bulkBan({ userIds: pending.ids, reason: banReason, duration: Number(banDuration), }); if (result.ok) { const data = result.data as { banned: number }; reportResult("Banned", data.banned, pending.ids.length); } else toast.error(result.error); setPending(null); } finally { setBusy(false); } } async function confirmGiveCurrency() { if (pending?.kind !== "give-currency") return; setBusy(true); try { const result = await bulkGiveCurrency({ userIds: pending.ids, amount: Number(currencyAmount), type: currencyType, }); if (result.ok) { const data = result.data as { given: number; total: number; failedIds: { userId: number; reason: string }[]; }; reportResult( `Given ${currencyAmount} ${currencyType}`, data.given, data.total, data.failedIds.length, ); } else toast.error(result.error); setPending(null); } finally { setBusy(false); } } async function confirmGiveBadge() { if (pending?.kind !== "give-badge") return; setBusy(true); try { const result = await bulkGiveBadge({ userIds: pending.ids, badgeCode: badgeCode.trim().toUpperCase(), }); if (result.ok) { const data = result.data as { given: number; total: number; failedIds: { userId: number; reason: string }[]; }; reportResult( `Given badge ${badgeCode}`, data.given, data.total, data.failedIds.length, ); } else toast.error(result.error); setPending(null); } finally { setBusy(false); } } const bulkActions: { label: string; value: string; variant?: "destructive" | "default"; }[] = []; if (canBan) { bulkActions.push({ label: "Ban", value: "ban", variant: "destructive" }); bulkActions.push({ label: "Unban", value: "unban" }); } if (canEdit) { bulkActions.push({ label: "Give credits", value: "give-currency" }); bulkActions.push({ label: "Give badge", value: "give-badge" }); } return ( <> 0 ? bulkActions : undefined} onBulkAction={bulkActions.length > 0 ? handleBulkAction : undefined} exportUrl={canEdit ? "/api/admin/export?type=users" : undefined} /> !o && setPending(null)} > Ban {pending?.kind === "ban" ? pending.ids.length : 0} users Permanent ban: leave duration at -1.
setBanReason(e.target.value)} />
setBanDuration(e.target.value)} />
!o && setPending(null)} > Give currency to{" "} {pending?.kind === "give-currency" ? pending.ids.length : 0} users
setCurrencyAmount(e.target.value)} />
!o && setPending(null)} > Give badge to{" "} {pending?.kind === "give-badge" ? pending.ids.length : 0} users
setBadgeCode(e.target.value)} placeholder="ACH_*, etc." />
); }