630 lines
25 KiB
TypeScript
630 lines
25 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
|
import { useExportLabels } from "@/hooks/use-export-labels";
|
|
import { useTranslation } from "react-i18next";
|
|
import { toast } from "sonner";
|
|
|
|
import {
|
|
deleteAdminPlayer,
|
|
getAdminPlayers,
|
|
postAdminPlayer,
|
|
postAdminPlayerFreeze,
|
|
postAdminPlayerUnfreeze,
|
|
putAdminPlayer,
|
|
} from "@/api/admin-player";
|
|
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
|
|
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
|
|
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
|
import { resolvePlayerStatusTone } from "@/lib/admin-status-tone";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
|
import { PRD_PLAYER_FREEZE_MANAGE, PRD_USERS_MANAGE } from "@/lib/admin-prd";
|
|
import { useAdminProfile } from "@/stores/admin-session";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
|
import { formatAdminMinorUnits } from "@/lib/money";
|
|
import { LotteryApiBizError } from "@/types/api/errors";
|
|
import type { AdminPlayerRow } from "@/types/api/admin-player";
|
|
|
|
function playerStatusLabelT(status: number, t: (key: string) => string): string {
|
|
if (status === 0) return t("statusNormal");
|
|
if (status === 1) return t("statusFrozen");
|
|
if (status === 2) return t("statusBanned");
|
|
return String(status);
|
|
}
|
|
|
|
const PLAYER_STATUS_OPTIONS = [
|
|
{ value: 0, label: "statusNormal" },
|
|
{ value: 1, label: "statusFrozen" },
|
|
{ value: 2, label: "statusBanned" },
|
|
];
|
|
|
|
export function PlayersConsole(): React.ReactElement {
|
|
const { t } = useTranslation(["players", "common"]);
|
|
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
|
|
const exportLabels = useExportLabels("players");
|
|
const profile = useAdminProfile();
|
|
useAdminCurrencyCatalog();
|
|
const canManagePlayers = adminHasAnyPermission(profile?.permissions, [PRD_USERS_MANAGE]);
|
|
const canFreezePlayers = adminHasAnyPermission(profile?.permissions, [PRD_PLAYER_FREEZE_MANAGE]);
|
|
const [page, setPage] = useState(1);
|
|
const [perPage, setPerPage] = useState(10);
|
|
const [keyword, setKeyword] = useState("");
|
|
const [query, setQuery] = useState("");
|
|
|
|
const [items, setItems] = useState<AdminPlayerRow[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [lastPage, setLastPage] = useState(1);
|
|
const [loading, setLoading] = useState(true);
|
|
const [err, setErr] = useState<string | null>(null);
|
|
|
|
const [accountOpen, setAccountOpen] = useState(false);
|
|
const [accountMode, setAccountMode] = useState<"create" | "edit">("create");
|
|
const [accountSaving, setAccountSaving] = useState(false);
|
|
const [editingAccountId, setEditingAccountId] = useState<number | null>(null);
|
|
const [formSiteCode, setFormSiteCode] = useState("");
|
|
const [formSitePlayerId, setFormSitePlayerId] = useState("");
|
|
const [formUsername, setFormUsername] = useState("");
|
|
const [formNickname, setFormNickname] = useState("");
|
|
const [formDefaultCurrency, setFormDefaultCurrency] = useState("NPR");
|
|
const [formStatus, setFormStatus] = useState(0);
|
|
|
|
const [deleteTarget, setDeleteTarget] = useState<AdminPlayerRow | null>(null);
|
|
const [deleteBusy, setDeleteBusy] = useState(false);
|
|
const [freezeBusyId, setFreezeBusyId] = useState<number | null>(null);
|
|
|
|
const editingPlayer = useMemo(
|
|
() => items.find((p) => p.id === editingAccountId) ?? null,
|
|
[items, editingAccountId],
|
|
);
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true);
|
|
setErr(null);
|
|
try {
|
|
const data = await getAdminPlayers({
|
|
page,
|
|
per_page: perPage,
|
|
keyword: query.trim() || undefined,
|
|
});
|
|
setItems(data.items);
|
|
setTotal(data.meta.total);
|
|
setLastPage(Math.max(1, data.meta.last_page));
|
|
} catch (e) {
|
|
const msg = e instanceof LotteryApiBizError ? e.message : t("loadFailed");
|
|
setErr(msg);
|
|
setItems([]);
|
|
setTotal(0);
|
|
setLastPage(1);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [page, perPage, query, t]);
|
|
|
|
useEffect(() => {
|
|
queueMicrotask(() => {
|
|
void load();
|
|
});
|
|
}, [load]);
|
|
|
|
function openCreateAccount(): void {
|
|
setAccountMode("create");
|
|
setEditingAccountId(null);
|
|
setFormSiteCode("");
|
|
setFormSitePlayerId("");
|
|
setFormUsername("");
|
|
setFormNickname("");
|
|
setFormDefaultCurrency("NPR");
|
|
setFormStatus(0);
|
|
setAccountOpen(true);
|
|
}
|
|
|
|
function openEditAccount(row: AdminPlayerRow): void {
|
|
setAccountMode("edit");
|
|
setEditingAccountId(row.id);
|
|
setFormSiteCode(row.site_code);
|
|
setFormSitePlayerId(row.site_player_id);
|
|
setFormUsername(row.username ?? "");
|
|
setFormNickname(row.nickname ?? "");
|
|
setFormDefaultCurrency(row.default_currency);
|
|
setFormStatus(row.status);
|
|
setAccountOpen(true);
|
|
}
|
|
|
|
function handleAccountDialogOpenChange(open: boolean): void {
|
|
setAccountOpen(open);
|
|
if (!open) {
|
|
setEditingAccountId(null);
|
|
}
|
|
}
|
|
|
|
async function submitAccount(): Promise<void> {
|
|
if (accountMode === "create") {
|
|
if (formSiteCode.trim() === "") {
|
|
toast.error(t("siteCodeRequired"));
|
|
return;
|
|
}
|
|
if (formSitePlayerId.trim() === "") {
|
|
toast.error(t("sitePlayerIdRequired"));
|
|
return;
|
|
}
|
|
setAccountSaving(true);
|
|
try {
|
|
const created = await postAdminPlayer({
|
|
site_code: formSiteCode.trim(),
|
|
site_player_id: formSitePlayerId.trim(),
|
|
username: formUsername.trim() || null,
|
|
nickname: formNickname.trim() || null,
|
|
default_currency: formDefaultCurrency,
|
|
status: formStatus,
|
|
});
|
|
setItems((prev) => [created, ...prev]);
|
|
setTotal((t) => t + 1);
|
|
toast.success(t("createSuccess", { name: created.username ?? created.site_player_id }));
|
|
handleAccountDialogOpenChange(false);
|
|
} catch (e) {
|
|
const msg = e instanceof LotteryApiBizError ? e.message : t("createFailed");
|
|
toast.error(msg);
|
|
} finally {
|
|
setAccountSaving(false);
|
|
}
|
|
} else {
|
|
const id = editingAccountId;
|
|
if (id === null) return;
|
|
|
|
const body: Parameters<typeof putAdminPlayer>[1] = {};
|
|
if (formUsername.trim() !== "") {
|
|
body.username = formUsername.trim();
|
|
}
|
|
if (formNickname !== editingPlayer?.nickname) {
|
|
body.nickname = formNickname.trim() || null;
|
|
}
|
|
if (formDefaultCurrency !== editingPlayer?.default_currency) {
|
|
body.default_currency = formDefaultCurrency.trim().toUpperCase();
|
|
}
|
|
if (formStatus !== editingPlayer?.status) {
|
|
body.status = formStatus;
|
|
}
|
|
|
|
if (Object.keys(body).length === 0) {
|
|
toast.success(t("noChanges"));
|
|
handleAccountDialogOpenChange(false);
|
|
return;
|
|
}
|
|
|
|
setAccountSaving(true);
|
|
try {
|
|
const updated = await putAdminPlayer(id, body);
|
|
setItems((prev) => prev.map((row) => (row.id === updated.id ? updated : row)));
|
|
toast.success(t("updateSuccess", { name: updated.username ?? updated.site_player_id }));
|
|
handleAccountDialogOpenChange(false);
|
|
} catch (e) {
|
|
const msg = e instanceof LotteryApiBizError ? e.message : t("updateFailed");
|
|
toast.error(msg);
|
|
} finally {
|
|
setAccountSaving(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function toggleFreeze(row: AdminPlayerRow, freeze: boolean): Promise<void> {
|
|
setFreezeBusyId(row.id);
|
|
try {
|
|
const updated = freeze
|
|
? await postAdminPlayerFreeze(row.id)
|
|
: await postAdminPlayerUnfreeze(row.id);
|
|
setItems((prev) => prev.map((r) => (r.id === updated.id ? updated : r)));
|
|
const name = updated.username ?? updated.site_player_id;
|
|
toast.success(freeze ? t("freezeSuccess", { name }) : t("unfreezeSuccess", { name }));
|
|
} catch (e) {
|
|
const msg =
|
|
e instanceof LotteryApiBizError
|
|
? e.message
|
|
: freeze
|
|
? t("freezeFailed")
|
|
: t("unfreezeFailed");
|
|
toast.error(msg);
|
|
} finally {
|
|
setFreezeBusyId(null);
|
|
}
|
|
}
|
|
|
|
async function confirmDelete(): Promise<void> {
|
|
if (!deleteTarget) return;
|
|
setDeleteBusy(true);
|
|
try {
|
|
await deleteAdminPlayer(deleteTarget.id);
|
|
setItems((prev) => prev.filter((r) => r.id !== deleteTarget.id));
|
|
setTotal((t) => Math.max(0, t - 1));
|
|
toast.success(t("deleteSuccess", { name: deleteTarget.username ?? deleteTarget.site_player_id }));
|
|
setDeleteTarget(null);
|
|
} catch (e) {
|
|
const msg = e instanceof LotteryApiBizError ? e.message : t("deleteFailed");
|
|
toast.error(msg);
|
|
} finally {
|
|
setDeleteBusy(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="flex w-full max-w-none flex-col gap-6">
|
|
<Card className="admin-list-card">
|
|
<CardHeader className="admin-list-header flex flex-col gap-4">
|
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
|
|
<CardTitle className="admin-list-title">{t("listTitle")}</CardTitle>
|
|
{canManagePlayers ? (
|
|
<Button type="button" size="sm" onClick={() => openCreateAccount()}>
|
|
{t("createPlayer")}
|
|
</Button>
|
|
) : null}
|
|
</div>
|
|
<div className="admin-list-toolbar">
|
|
<div className="admin-list-field xl:min-w-0">
|
|
<Label htmlFor="player-search" className="sm:w-20 sm:shrink-0">
|
|
{t("search")}
|
|
</Label>
|
|
<Input
|
|
id="player-search"
|
|
value={keyword}
|
|
className="w-full sm:w-[18rem] xl:w-[24rem]"
|
|
placeholder={t("searchPlaceholder")}
|
|
onChange={(e) => setKeyword(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") {
|
|
setPage(1);
|
|
setQuery(keyword.trim());
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="admin-list-actions">
|
|
<AdminTableExportButton
|
|
tableId="players-table"
|
|
filename={exportLabels.filename}
|
|
sheetName={exportLabels.sheetName}
|
|
/>
|
|
<Button
|
|
type="button"
|
|
onClick={() => {
|
|
setPage(1);
|
|
setQuery(keyword.trim());
|
|
}}
|
|
>
|
|
{t("search")}
|
|
</Button>
|
|
<Button type="button" variant="secondary" onClick={() => void load()}>
|
|
{t("refresh")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="admin-list-content">
|
|
{err ? <p className="text-sm text-red-600 dark:text-red-400">{err}</p> : null}
|
|
{loading && items.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">{t("states.loading", { ns: "common" })}</p>
|
|
) : null}
|
|
<div className="admin-table-shell">
|
|
<Table id="players-table">
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-16">{t("table.id", { ns: "common" })}</TableHead>
|
|
<TableHead>{t("site")}</TableHead>
|
|
<TableHead>{t("sitePlayerId")}</TableHead>
|
|
<TableHead>{t("username")}</TableHead>
|
|
<TableHead>{t("nickname")}</TableHead>
|
|
<TableHead className="whitespace-nowrap">{t("currency")}</TableHead>
|
|
<TableHead className="whitespace-nowrap text-right">{t("balance")}</TableHead>
|
|
<TableHead className="whitespace-nowrap text-right">{t("available")}</TableHead>
|
|
<TableHead className="w-20 whitespace-nowrap">{t("status")}</TableHead>
|
|
<TableHead className="whitespace-nowrap">{t("lastLogin")}</TableHead>
|
|
<TableHead className="min-w-[10rem]">{t("actions")}</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{items.length === 0 && !loading ? (
|
|
<TableRow>
|
|
<TableCell colSpan={11} className="text-muted-foreground">
|
|
{t("states.noData", { ns: "common" })}
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
items.map((row) => (
|
|
<TableRow key={row.id}>
|
|
<TableCell className="tabular-nums">#{row.id}</TableCell>
|
|
<TableCell>
|
|
<span className="font-mono text-xs">{row.site_code}</span>
|
|
</TableCell>
|
|
<TableCell>
|
|
<span className="font-mono text-xs">{row.site_player_id}</span>
|
|
</TableCell>
|
|
<TableCell>{row.username ?? "—"}</TableCell>
|
|
<TableCell>{row.nickname ?? "—"}</TableCell>
|
|
<TableCell>{row.default_currency}</TableCell>
|
|
<TableCell className="whitespace-nowrap text-right tabular-nums text-xs">
|
|
{row.wallets.length > 0
|
|
? formatAdminMinorUnits(row.wallets[0].balance, row.wallets[0].currency_code)
|
|
: "—"}
|
|
</TableCell>
|
|
<TableCell className="whitespace-nowrap text-right tabular-nums text-xs">
|
|
{row.wallets.length > 0
|
|
? formatAdminMinorUnits(row.wallets[0].available_balance, row.wallets[0].currency_code)
|
|
: "—"}
|
|
</TableCell>
|
|
<TableCell>
|
|
<AdminStatusBadge status={row.status} tone={resolvePlayerStatusTone(row.status)}>
|
|
{playerStatusLabelT(row.status, t)}
|
|
</AdminStatusBadge>
|
|
</TableCell>
|
|
<TableCell className="whitespace-nowrap text-xs text-muted-foreground">
|
|
{row.last_login_at
|
|
? new Date(row.last_login_at).toLocaleString("zh-CN", {
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
})
|
|
: "—"}
|
|
</TableCell>
|
|
<TableCell>
|
|
{canManagePlayers || canFreezePlayers ? (
|
|
<div className="flex flex-wrap gap-1">
|
|
{canFreezePlayers && row.status === 0 ? (
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
variant="outline"
|
|
disabled={freezeBusyId === row.id}
|
|
onClick={() => {
|
|
const name = row.username ?? row.site_player_id;
|
|
requestConfirm({
|
|
title: t("confirmFreezeTitle"),
|
|
description: t("confirmFreezeDescription", { name }),
|
|
onConfirm: () => toggleFreeze(row, true),
|
|
});
|
|
}}
|
|
>
|
|
{freezeBusyId === row.id ? t("saving") : t("freeze")}
|
|
</Button>
|
|
) : null}
|
|
{canFreezePlayers && row.status === 1 ? (
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
variant="outline"
|
|
disabled={freezeBusyId === row.id}
|
|
onClick={() => {
|
|
const name = row.username ?? row.site_player_id;
|
|
requestConfirm({
|
|
title: t("confirmUnfreezeTitle"),
|
|
description: t("confirmUnfreezeDescription", { name }),
|
|
onConfirm: () => toggleFreeze(row, false),
|
|
});
|
|
}}
|
|
>
|
|
{freezeBusyId === row.id ? t("saving") : t("unfreeze")}
|
|
</Button>
|
|
) : null}
|
|
{canManagePlayers ? (
|
|
<>
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
variant={
|
|
accountOpen && editingAccountId === row.id ? "secondary" : "outline"
|
|
}
|
|
onClick={() => openEditAccount(row)}
|
|
>
|
|
{t("edit")}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
variant="destructive"
|
|
onClick={() => setDeleteTarget(row)}
|
|
>
|
|
{t("delete")}
|
|
</Button>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
) : (
|
|
<span className="text-xs text-muted-foreground">—</span>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
<AdminListPaginationFooter
|
|
selectId="admin-players-per-page"
|
|
total={total}
|
|
page={page}
|
|
lastPage={lastPage}
|
|
perPage={perPage}
|
|
loading={loading}
|
|
onPerPageChange={(n) => {
|
|
setPerPage(n);
|
|
setPage(1);
|
|
}}
|
|
onPageChange={setPage}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Dialog open={accountOpen} onOpenChange={handleAccountDialogOpenChange}>
|
|
<DialogContent showCloseButton className="max-h-[90vh] max-w-lg gap-4 overflow-y-auto sm:max-w-xl">
|
|
<DialogHeader>
|
|
<DialogTitle>{accountMode === "create" ? t("createDialogTitle") : t("editDialogTitle")}</DialogTitle>
|
|
<DialogDescription>
|
|
{accountMode === "create" ? t("createDialogDesc") : t("editDialogDesc")}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-3">
|
|
{accountMode === "create" && (
|
|
<>
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="player-site-code">{t("siteCode")}</Label>
|
|
<Input
|
|
id="player-site-code"
|
|
value={formSiteCode}
|
|
placeholder={t("siteCodePlaceholder")}
|
|
onChange={(e) => setFormSiteCode(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="player-site-id">{t("sitePlayerIdLabel")}</Label>
|
|
<Input
|
|
id="player-site-id"
|
|
value={formSitePlayerId}
|
|
placeholder={t("sitePlayerIdPlaceholder")}
|
|
onChange={(e) => setFormSitePlayerId(e.target.value)}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="player-username">{t("username")}</Label>
|
|
<Input
|
|
id="player-username"
|
|
value={formUsername}
|
|
disabled={accountMode === "edit"}
|
|
placeholder={accountMode === "create" ? t("usernamePlaceholderOptional") : ""}
|
|
onChange={(e) => setFormUsername(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="player-nickname">{t("nickname")}</Label>
|
|
<Input
|
|
id="player-nickname"
|
|
value={formNickname}
|
|
placeholder={t("nicknamePlaceholderOptional")}
|
|
onChange={(e) => setFormNickname(e.target.value)}
|
|
/>
|
|
</div>
|
|
{accountMode === "create" && (
|
|
<>
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="player-currency">{t("defaultCurrency")}</Label>
|
|
<Input
|
|
id="player-currency"
|
|
value={formDefaultCurrency}
|
|
placeholder="NPR"
|
|
onChange={(e) => setFormDefaultCurrency(e.target.value.toUpperCase())}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="player-status">{t("status")}</Label>
|
|
<Select
|
|
value={String(formStatus)}
|
|
onValueChange={(v) => setFormStatus(Number(v))}
|
|
>
|
|
<SelectTrigger id="player-status">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{PLAYER_STATUS_OPTIONS.map((o) => (
|
|
<SelectItem key={o.value} value={String(o.value)}>
|
|
{t(o.label)}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</>
|
|
)}
|
|
{accountMode === "edit" && (
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="player-edit-status">{t("status")}</Label>
|
|
<Select
|
|
value={String(formStatus)}
|
|
onValueChange={(v) => setFormStatus(Number(v))}
|
|
>
|
|
<SelectTrigger id="player-edit-status">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{PLAYER_STATUS_OPTIONS.map((o) => (
|
|
<SelectItem key={o.value} value={String(o.value)}>
|
|
{t(o.label)}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex justify-end gap-2">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => handleAccountDialogOpenChange(false)}
|
|
>
|
|
{t("cancel")}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
disabled={accountSaving}
|
|
onClick={() => void submitAccount()}
|
|
>
|
|
{accountSaving ? t("saving") : t("save")}
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<Dialog open={deleteTarget !== null} onOpenChange={(open) => !open && setDeleteTarget(null)}>
|
|
<DialogContent showCloseButton className="max-w-sm">
|
|
<DialogHeader>
|
|
<DialogTitle>{t("confirmDelete")}</DialogTitle>
|
|
<DialogDescription>
|
|
{deleteTarget
|
|
? t("confirmDeleteDesc", {
|
|
name: deleteTarget.username ?? deleteTarget.site_player_id,
|
|
})
|
|
: null}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="flex justify-end gap-2">
|
|
<Button type="button" variant="outline" onClick={() => setDeleteTarget(null)}>
|
|
{t("cancel")}
|
|
</Button>
|
|
<Button type="button" variant="destructive" disabled={deleteBusy} onClick={() => void confirmDelete()}>
|
|
{deleteBusy ? t("actions.submitting", { ns: "common" }) : t("delete")}
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
<ConfirmDialog />
|
|
</div>
|
|
);
|
|
}
|