UI: единое подтверждение удаления через ConfirmDialog
- админка, записи: удаление партии раньше срабатывало сразу по клику на иконку и показывало «Удалено» даже при ошибке — теперь модалка подтверждения и честная обработка ошибок; - ачивки и dev-удаление аккаунта: window.confirm заменён на ConfirmDialog (в приложении принят именно он); - настройки группы: удаление участника — с подтверждением. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { Trash2, UserPlus } from "lucide-react";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { ApiError } from "../api/client";
|
import { ApiError } from "../api/client";
|
||||||
|
import { ConfirmDialog } from "../components/ConfirmDialog";
|
||||||
import { PlayerLink } from "../components/PlayerLink";
|
import { PlayerLink } from "../components/PlayerLink";
|
||||||
import { Spinner } from "../components/Spinner";
|
import { Spinner } from "../components/Spinner";
|
||||||
import { Switch } from "../components/Switch";
|
import { Switch } from "../components/Switch";
|
||||||
@@ -24,6 +25,7 @@ export function GroupSettingsPage() {
|
|||||||
|
|
||||||
const [selected, setSelected] = useState<Set<number>>(new Set());
|
const [selected, setSelected] = useState<Set<number>>(new Set());
|
||||||
const [newNick, setNewNick] = useState("");
|
const [newNick, setNewNick] = useState("");
|
||||||
|
const [removeTarget, setRemoveTarget] = useState<{ userId: number; nickname: string } | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (group) setSelected(new Set(group.expansion_ids));
|
if (group) setSelected(new Set(group.expansion_ids));
|
||||||
@@ -121,7 +123,10 @@ export function GroupSettingsPage() {
|
|||||||
{m.nickname} <span className="muted small">· {m.role}</span>
|
{m.nickname} <span className="muted small">· {m.role}</span>
|
||||||
</PlayerLink>
|
</PlayerLink>
|
||||||
{isOwner && m.role !== "owner" && (
|
{isOwner && m.role !== "owner" && (
|
||||||
<button className="btn btn-ghost btn-danger small" onClick={() => remove(m.user_id)}>
|
<button
|
||||||
|
className="btn btn-ghost btn-danger small"
|
||||||
|
onClick={() => setRemoveTarget({ userId: m.user_id, nickname: m.nickname })}
|
||||||
|
>
|
||||||
<Trash2 size={16} />
|
<Trash2 size={16} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -129,6 +134,21 @@ export function GroupSettingsPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
open={removeTarget != null}
|
||||||
|
danger
|
||||||
|
title={`Удалить участника «${removeTarget?.nickname}»?`}
|
||||||
|
message="Игрок будет удалён из группы. Его сыгранные партии сохранятся."
|
||||||
|
confirmLabel="Удалить"
|
||||||
|
onCancel={() => setRemoveTarget(null)}
|
||||||
|
onConfirm={() => {
|
||||||
|
if (!removeTarget) return;
|
||||||
|
const userId = removeTarget.userId;
|
||||||
|
setRemoveTarget(null);
|
||||||
|
remove(userId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useRef, useState } from "react";
|
|||||||
|
|
||||||
import { ApiError } from "../../api/client";
|
import { ApiError } from "../../api/client";
|
||||||
import { Avatar } from "../../components/Avatar";
|
import { Avatar } from "../../components/Avatar";
|
||||||
|
import { ConfirmDialog } from "../../components/ConfirmDialog";
|
||||||
import { Spinner } from "../../components/Spinner";
|
import { Spinner } from "../../components/Spinner";
|
||||||
import { useToast } from "../../context/ToastContext";
|
import { useToast } from "../../context/ToastContext";
|
||||||
import {
|
import {
|
||||||
@@ -87,6 +88,7 @@ function AchievementCard({ ach }: { ach: AchievementRead }) {
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const fileRef = useRef<HTMLInputElement>(null);
|
const fileRef = useRef<HTMLInputElement>(null);
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
|
const [confirmRemove, setConfirmRemove] = useState(false);
|
||||||
const [name, setName] = useState(ach.name);
|
const [name, setName] = useState(ach.name);
|
||||||
const [description, setDescription] = useState(ach.description);
|
const [description, setDescription] = useState(ach.description);
|
||||||
const [condition, setCondition] = useState(ach.condition);
|
const [condition, setCondition] = useState(ach.condition);
|
||||||
@@ -117,7 +119,6 @@ function AchievementCard({ ach }: { ach: AchievementRead }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const remove = async () => {
|
const remove = async () => {
|
||||||
if (!window.confirm(`Удалить ачивку «${ach.name}»?`)) return;
|
|
||||||
try {
|
try {
|
||||||
await del.mutateAsync(ach.slug);
|
await del.mutateAsync(ach.slug);
|
||||||
toast.show("Удалено");
|
toast.show("Удалено");
|
||||||
@@ -175,7 +176,11 @@ function AchievementCard({ ach }: { ach: AchievementRead }) {
|
|||||||
<button className="btn small" onClick={() => setEditing(true)}>
|
<button className="btn small" onClick={() => setEditing(true)}>
|
||||||
Изменить
|
Изменить
|
||||||
</button>
|
</button>
|
||||||
<button className="btn btn-ghost btn-danger small" onClick={remove} disabled={del.isPending}>
|
<button
|
||||||
|
className="btn btn-ghost btn-danger small"
|
||||||
|
onClick={() => setConfirmRemove(true)}
|
||||||
|
disabled={del.isPending}
|
||||||
|
>
|
||||||
Удалить
|
Удалить
|
||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
@@ -187,6 +192,19 @@ function AchievementCard({ ach }: { ach: AchievementRead }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
open={confirmRemove}
|
||||||
|
danger
|
||||||
|
title={`Удалить ачивку «${ach.name}»?`}
|
||||||
|
message="Ачивка и её иконка будут удалены. Действие необратимо."
|
||||||
|
confirmLabel="Удалить"
|
||||||
|
onCancel={() => setConfirmRemove(false)}
|
||||||
|
onConfirm={() => {
|
||||||
|
setConfirmRemove(false);
|
||||||
|
remove();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Pencil, Trash2 } from "lucide-react";
|
import { Pencil, Trash2 } from "lucide-react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { ApiError } from "../../api/client";
|
||||||
|
import { ConfirmDialog } from "../../components/ConfirmDialog";
|
||||||
import { Spinner } from "../../components/Spinner";
|
import { Spinner } from "../../components/Spinner";
|
||||||
import { Switch } from "../../components/Switch";
|
import { Switch } from "../../components/Switch";
|
||||||
import { useToast } from "../../context/ToastContext";
|
import { useToast } from "../../context/ToastContext";
|
||||||
@@ -20,6 +22,7 @@ export function AdminRecordsPage() {
|
|||||||
const [factionFilter, setFactionFilter] = useState<number | null>(null);
|
const [factionFilter, setFactionFilter] = useState<number | null>(null);
|
||||||
const [grouped, setGrouped] = useState(true);
|
const [grouped, setGrouped] = useState(true);
|
||||||
const [editId, setEditId] = useState<number | null>(null);
|
const [editId, setEditId] = useState<number | null>(null);
|
||||||
|
const [deleteTarget, setDeleteTarget] = useState<AdminMatchRead | null>(null);
|
||||||
|
|
||||||
const { data: groups } = useAdminGroups();
|
const { data: groups } = useAdminGroups();
|
||||||
const { data: users } = useAdminUsers();
|
const { data: users } = useAdminUsers();
|
||||||
@@ -61,13 +64,7 @@ export function AdminRecordsPage() {
|
|||||||
<button className="btn btn-ghost small" onClick={() => setEditId(m.id)}>
|
<button className="btn btn-ghost small" onClick={() => setEditId(m.id)}>
|
||||||
<Pencil size={16} />
|
<Pencil size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button className="btn btn-ghost btn-danger small" onClick={() => setDeleteTarget(m)}>
|
||||||
className="btn btn-ghost btn-danger small"
|
|
||||||
onClick={async () => {
|
|
||||||
await del.mutateAsync(m.id).catch(() => {});
|
|
||||||
toast.show("Удалено");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trash2 size={16} />
|
<Trash2 size={16} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,6 +134,25 @@ export function AdminRecordsPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{editId != null && <AdminMatchEdit matchId={editId} onClose={() => setEditId(null)} />}
|
{editId != null && <AdminMatchEdit matchId={editId} onClose={() => setEditId(null)} />}
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
open={deleteTarget != null}
|
||||||
|
danger
|
||||||
|
title={`Удалить партию #${deleteTarget?.id}?`}
|
||||||
|
message="Партия и её результаты будут удалены. Действие необратимо."
|
||||||
|
confirmLabel="Удалить"
|
||||||
|
onCancel={() => setDeleteTarget(null)}
|
||||||
|
onConfirm={async () => {
|
||||||
|
if (!deleteTarget) return;
|
||||||
|
setDeleteTarget(null);
|
||||||
|
try {
|
||||||
|
await del.mutateAsync(deleteTarget.id);
|
||||||
|
toast.show("Удалено");
|
||||||
|
} catch (e) {
|
||||||
|
toast.show(e instanceof ApiError ? e.message : "Не удалось удалить");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
import { ApiError, api, unwrap } from "../../api/client";
|
import { ApiError, api, unwrap } from "../../api/client";
|
||||||
import { qk } from "../../api/queryKeys";
|
import { qk } from "../../api/queryKeys";
|
||||||
|
import { ConfirmDialog } from "../../components/ConfirmDialog";
|
||||||
import { useToast } from "../../context/ToastContext";
|
import { useToast } from "../../context/ToastContext";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,6 +23,7 @@ export function DevDeleteAccountButton({
|
|||||||
}) {
|
}) {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||||
const del = useMutation({
|
const del = useMutation({
|
||||||
mutationFn: async () =>
|
mutationFn: async () =>
|
||||||
unwrap(
|
unwrap(
|
||||||
@@ -32,20 +35,26 @@ export function DevDeleteAccountButton({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<button
|
<button
|
||||||
className="btn small"
|
className="btn small"
|
||||||
// Яркая (сплошная) красная кнопка. Стиль инлайном, а не классом в общем CSS,
|
// Яркая (сплошная) красная кнопка. Стиль инлайном, а не классом в общем CSS,
|
||||||
// чтобы в прод/тест-стили не попало ничего, связанного с удалением.
|
// чтобы в прод/тест-стили не попало ничего, связанного с удалением.
|
||||||
style={{ background: "var(--danger)", borderColor: "var(--danger)", color: "#fff" }}
|
style={{ background: "var(--danger)", borderColor: "var(--danger)", color: "#fff" }}
|
||||||
disabled={del.isPending}
|
disabled={del.isPending}
|
||||||
onClick={async () => {
|
onClick={() => setConfirmOpen(true)}
|
||||||
if (
|
>
|
||||||
!window.confirm(
|
Удалить
|
||||||
`Удалить аккаунт «${nickname}»? Партии сохранятся, но игрок будет ` +
|
</button>
|
||||||
`вычеркнут из них. Действие необратимо.`,
|
<ConfirmDialog
|
||||||
)
|
open={confirmOpen}
|
||||||
)
|
danger
|
||||||
return;
|
title={`Удалить аккаунт «${nickname}»?`}
|
||||||
|
message="Партии сохранятся, но игрок будет вычеркнут из них. Действие необратимо."
|
||||||
|
confirmLabel="Удалить"
|
||||||
|
onCancel={() => setConfirmOpen(false)}
|
||||||
|
onConfirm={async () => {
|
||||||
|
setConfirmOpen(false);
|
||||||
try {
|
try {
|
||||||
await del.mutateAsync();
|
await del.mutateAsync();
|
||||||
toast.show("Аккаунт удалён");
|
toast.show("Аккаунт удалён");
|
||||||
@@ -53,8 +62,7 @@ export function DevDeleteAccountButton({
|
|||||||
toast.show(e instanceof ApiError ? e.message : "Не удалось удалить");
|
toast.show(e instanceof ApiError ? e.message : "Не удалось удалить");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
Удалить
|
</>
|
||||||
</button>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user