style: format code with prettier
Local Build and Deploy / deploy (push) Successful in 49s

This commit is contained in:
openhands committed 2026-07-12 21:07:34 +02:00
1 parent e85e4d74ea
commit 2e4ed76121
378 files changed
+22404 -20686

No files matched your search

+159 -177
View File
@@ -1,13 +1,13 @@
'use client'
"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 { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
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 { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
@@ -15,113 +15,102 @@ import {
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'
} 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
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<number, string> = {
1: 'User',
2: 'Bronze',
3: 'Silver',
4: 'Gold',
5: 'Moderator',
6: 'Admin',
7: 'Super Admin',
}
1: "User",
2: "Bronze",
3: "Silver",
4: "Gold",
5: "Moderator",
6: "Admin",
7: "Super Admin",
};
function getColumns(canEdit: boolean): DataTableColumn<UserRow>[] {
const cols: DataTableColumn<UserRow>[] = [
{ key: 'id', label: 'ID', sortable: true },
{ key: "id", label: "ID", sortable: true },
{
key: 'username',
label: 'User',
key: "username",
label: "User",
sortable: true,
render: (_, row) => (
<Link
href={`/admin/users/show/${row.id}`}
className="flex items-center gap-2.5 hover:opacity-80"
>
<Link href={`/admin/users/show/${row.id}`} className="flex items-center gap-2.5 hover:opacity-80">
<img
src={getAvatarUrl(row.look, { size: 's', headOnly: true, headDirection: 2 })}
src={getAvatarUrl(row.look, { size: "s", headOnly: true, headDirection: 2 })}
alt=""
className="h-8 w-8 rounded"
style={{ imageRendering: 'pixelated' }}
style={{ imageRendering: "pixelated" }}
/>
<div className="min-w-0">
<span className="font-medium block">{row.username}</span>
{row.motto && (
<span className="text-xs text-muted-foreground truncate block max-w-[200px]">
{row.motto}
</span>
<span className="text-xs text-muted-foreground truncate block max-w-[200px]">{row.motto}</span>
)}
</div>
</Link>
),
},
{ key: 'mail', label: 'Email', sortable: true },
{ key: "mail", label: "Email", sortable: true },
{
key: 'rank',
label: 'Rank',
key: "rank",
label: "Rank",
sortable: true,
filterKey: 'filter_rank',
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' },
{ 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)
const r = Number(value);
return (
<Badge variant={r >= 7 ? 'default' : r >= 5 ? 'secondary' : 'outline'}>
<Badge variant={r >= 7 ? "default" : r >= 5 ? "secondary" : "outline"}>
{RANK_LABELS[r] || `Rank ${r}`}
</Badge>
)
);
},
},
{
key: 'credits',
label: 'Credits',
key: "credits",
label: "Credits",
sortable: true,
render: (value) => <span className="tabular-nums">{Number(value).toLocaleString()}</span>,
},
{
key: 'online',
label: 'Status',
filterKey: 'filter_online',
key: "online",
label: "Status",
filterKey: "filter_online",
filterOptions: [
{ label: 'Online', value: '1' },
{ label: 'Offline', value: '0' },
{ label: "Online", value: "1" },
{ label: "Offline", value: "0" },
],
render: (value) =>
value === '1' ? (
value === "1" ? (
<span className="flex items-center gap-1.5 text-[var(--admin-success)]">
<Wifi className="h-3.5 w-3.5" /> Online
</span>
@@ -132,26 +121,26 @@ function getColumns(canEdit: boolean): DataTableColumn<UserRow>[] {
),
},
{
key: 'accountCreated',
label: 'Registered',
key: "accountCreated",
label: "Registered",
sortable: true,
render: (value) => {
const ts = Number(value)
if (!ts) return '-'
const ts = Number(value);
if (!ts) return "-";
return (
<span className="text-xs text-muted-foreground">
{new Date(ts * 1000).toLocaleDateString('it-IT', {
day: '2-digit',
month: 'short',
year: 'numeric',
{new Date(ts * 1000).toLocaleDateString("it-IT", {
day: "2-digit",
month: "short",
year: "numeric",
})}
</span>
)
);
},
},
{
key: 'actions',
label: '',
key: "actions",
label: "",
render: (_, row) => (
<div className="flex items-center gap-1">
<Button variant="ghost" size="icon" asChild title="View">
@@ -169,147 +158,147 @@ function getColumns(canEdit: boolean): DataTableColumn<UserRow>[] {
</div>
),
},
]
];
return cols
return cols;
}
interface UsersTableProps {
data: PaginatedResult<UserRow>
canEdit: boolean
canBan: boolean
data: PaginatedResult<UserRow>;
canEdit: boolean;
canBan: boolean;
}
type PendingDialog =
| { kind: 'ban'; ids: number[] }
| { kind: 'give-currency'; ids: number[] }
| { kind: 'give-badge'; ids: number[] }
| null
| { 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<PendingDialog>(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('')
const [pending, setPending] = useState<PendingDialog>(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`)
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))
const numericIds = ids.map((i) => Number(i));
if (action === 'unban') {
setBusy(true)
if (action === "unban") {
setBusy(true);
try {
const result = await bulkUnban({ userIds: numericIds })
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)
const data = result.data as { unbanned: number; total: number };
reportResult("Unbanned", data.unbanned, data.total);
} else toast.error(result.error);
} finally {
setBusy(false)
setBusy(false);
}
return
return;
}
if (action === 'ban') {
setPending({ kind: 'ban', ids: numericIds })
return
if (action === "ban") {
setPending({ kind: "ban", ids: numericIds });
return;
}
if (action === 'give-currency') {
setPending({ kind: 'give-currency', 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
if (action === "give-badge") {
setPending({ kind: "give-badge", ids: numericIds });
return;
}
}
async function confirmBan() {
if (pending?.kind !== 'ban') return
setBusy(true)
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)
const data = result.data as { banned: number };
reportResult("Banned", data.banned, pending.ids.length);
} else toast.error(result.error);
setPending(null);
} finally {
setBusy(false)
setBusy(false);
}
}
async function confirmGiveCurrency() {
if (pending?.kind !== 'give-currency') return
setBusy(true)
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 }[]
}
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)
);
} else toast.error(result.error);
setPending(null);
} finally {
setBusy(false)
setBusy(false);
}
}
async function confirmGiveBadge() {
if (pending?.kind !== 'give-badge') return
setBusy(true)
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)
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)
setBusy(false);
}
}
const bulkActions: { label: string; value: string; variant?: 'destructive' | 'default' }[] = []
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' })
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' })
bulkActions.push({ label: "Give credits", value: "give-currency" });
bulkActions.push({ label: "Give badge", value: "give-badge" });
}
return (
@@ -319,29 +308,25 @@ export function UsersTable({ data, canEdit, canBan }: UsersTableProps) {
columns={getColumns(canEdit)}
searchPlaceholder="Search by username, email or IP..."
presets={[
{ label: 'Online now', params: { filter_online: '1' } },
{ label: 'Staff (rank ≥ 5)', params: { filter_rank: '5' } },
{ label: 'Super admins', params: { filter_rank: '7' } },
{ label: "Online now", params: { filter_online: "1" } },
{ label: "Staff (rank ≥ 5)", params: { filter_rank: "5" } },
{ label: "Super admins", params: { filter_rank: "7" } },
]}
bulkActions={bulkActions.length > 0 ? bulkActions : undefined}
onBulkAction={bulkActions.length > 0 ? handleBulkAction : undefined}
exportUrl={canEdit ? '/api/admin/export?type=users' : undefined}
exportUrl={canEdit ? "/api/admin/export?type=users" : undefined}
/>
<Dialog open={pending?.kind === 'ban'} onOpenChange={(o) => !o && setPending(null)}>
<Dialog open={pending?.kind === "ban"} onOpenChange={(o) => !o && setPending(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Ban {pending?.kind === 'ban' ? pending.ids.length : 0} users</DialogTitle>
<DialogTitle>Ban {pending?.kind === "ban" ? pending.ids.length : 0} users</DialogTitle>
<DialogDescription>Permanent ban: leave duration at -1.</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<div>
<Label htmlFor="ban-reason">Reason</Label>
<Input
id="ban-reason"
value={banReason}
onChange={(e) => setBanReason(e.target.value)}
/>
<Input id="ban-reason" value={banReason} onChange={(e) => setBanReason(e.target.value)} />
</div>
<div>
<Label htmlFor="ban-duration">Duration (seconds, -1 = permanent)</Label>
@@ -364,20 +349,17 @@ export function UsersTable({ data, canEdit, canBan }: UsersTableProps) {
</DialogContent>
</Dialog>
<Dialog open={pending?.kind === 'give-currency'} onOpenChange={(o) => !o && setPending(null)}>
<Dialog open={pending?.kind === "give-currency"} onOpenChange={(o) => !o && setPending(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>
Give currency to {pending?.kind === 'give-currency' ? pending.ids.length : 0} users
Give currency to {pending?.kind === "give-currency" ? pending.ids.length : 0} users
</DialogTitle>
</DialogHeader>
<div className="space-y-3">
<div>
<Label>Type</Label>
<Select
value={currencyType}
onValueChange={(v) => setCurrencyType(v as typeof currencyType)}
>
<Select value={currencyType} onValueChange={(v) => setCurrencyType(v as typeof currencyType)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
@@ -409,11 +391,11 @@ export function UsersTable({ data, canEdit, canBan }: UsersTableProps) {
</DialogContent>
</Dialog>
<Dialog open={pending?.kind === 'give-badge'} onOpenChange={(o) => !o && setPending(null)}>
<Dialog open={pending?.kind === "give-badge"} onOpenChange={(o) => !o && setPending(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>
Give badge to {pending?.kind === 'give-badge' ? pending.ids.length : 0} users
Give badge to {pending?.kind === "give-badge" ? pending.ids.length : 0} users
</DialogTitle>
</DialogHeader>
<div className="space-y-3">
@@ -438,5 +420,5 @@ export function UsersTable({ data, canEdit, canBan }: UsersTableProps) {
</DialogContent>
</Dialog>
</>
)
);
}