UI: подтверждение перед отменой незавершённой и удалением сыгранной партии (модалка)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
|||||||
|
/** Переиспользуемое модальное подтверждение (без нативного confirm()). */
|
||||||
|
export function ConfirmDialog({
|
||||||
|
open,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
confirmLabel = "Подтвердить",
|
||||||
|
cancelLabel = "Отмена",
|
||||||
|
danger = false,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
title: string;
|
||||||
|
message?: string;
|
||||||
|
confirmLabel?: string;
|
||||||
|
cancelLabel?: string;
|
||||||
|
danger?: boolean;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}) {
|
||||||
|
if (!open) return null;
|
||||||
|
return (
|
||||||
|
<div className="modal-overlay" onClick={onCancel}>
|
||||||
|
<div className="modal" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="modal-title">{title}</div>
|
||||||
|
{message && <p className="modal-msg">{message}</p>}
|
||||||
|
<div className="modal-actions">
|
||||||
|
<button className="btn btn-ghost" onClick={onCancel}>
|
||||||
|
{cancelLabel}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={"btn " + (danger ? "btn-danger-solid" : "btn-primary")}
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
{confirmLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { useState } from "react";
|
|||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
import { ApiError } from "../api/client";
|
import { ApiError } from "../api/client";
|
||||||
|
import { ConfirmDialog } from "../components/ConfirmDialog";
|
||||||
import { MatchMedia } from "../components/MatchMedia";
|
import { MatchMedia } from "../components/MatchMedia";
|
||||||
import { Spinner } from "../components/Spinner";
|
import { Spinner } from "../components/Spinner";
|
||||||
import { formatDuration, formatTime } from "../domain/format";
|
import { formatDuration, formatTime } from "../domain/format";
|
||||||
@@ -39,6 +40,7 @@ export function MatchDetailPage() {
|
|||||||
const [winReason, setWinReason] = useState<WinReason>("objectives");
|
const [winReason, setWinReason] = useState<WinReason>("objectives");
|
||||||
const [overall, setOverall] = useState("");
|
const [overall, setOverall] = useState("");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [confirmRemove, setConfirmRemove] = useState(false);
|
||||||
|
|
||||||
if (isLoading) return <Spinner />;
|
if (isLoading) return <Spinner />;
|
||||||
if (!match) return <div className="muted">Партия не найдена.</div>;
|
if (!match) return <div className="muted">Партия не найдена.</div>;
|
||||||
@@ -271,10 +273,32 @@ export function MatchDetailPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{canModify && (
|
{canModify && (
|
||||||
<button className="btn btn-danger" style={{ width: "100%" }} onClick={remove}>
|
<button
|
||||||
|
className="btn btn-danger"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
onClick={() => setConfirmRemove(true)}
|
||||||
|
>
|
||||||
{inProgress ? "Отменить партию" : "Удалить партию"}
|
{inProgress ? "Отменить партию" : "Удалить партию"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
open={confirmRemove}
|
||||||
|
danger
|
||||||
|
title={inProgress ? "Отменить партию?" : "Удалить партию?"}
|
||||||
|
message={
|
||||||
|
inProgress
|
||||||
|
? "Незавершённая партия будет удалена. Действие необратимо."
|
||||||
|
: "Сыгранная партия и её результаты будут удалены. Действие необратимо."
|
||||||
|
}
|
||||||
|
confirmLabel={inProgress ? "Отменить партию" : "Удалить"}
|
||||||
|
cancelLabel="Назад"
|
||||||
|
onCancel={() => setConfirmRemove(false)}
|
||||||
|
onConfirm={() => {
|
||||||
|
setConfirmRemove(false);
|
||||||
|
remove();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent); }
|
|||||||
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||||||
.btn-ghost { background: transparent; }
|
.btn-ghost { background: transparent; }
|
||||||
.btn-danger { background: transparent; color: var(--danger); border-color: var(--danger); }
|
.btn-danger { background: transparent; color: var(--danger); border-color: var(--danger); }
|
||||||
|
.btn-danger-solid { background: var(--danger); border-color: var(--danger); color: #fff; }
|
||||||
.btn[disabled] { opacity: 0.5; pointer-events: none; }
|
.btn[disabled] { opacity: 0.5; pointer-events: none; }
|
||||||
|
|
||||||
.row { display: flex; align-items: center; gap: 10px; }
|
.row { display: flex; align-items: center; gap: 10px; }
|
||||||
@@ -92,6 +93,30 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent); }
|
|||||||
|
|
||||||
.error-text { color: var(--danger); font-size: 13px; }
|
.error-text { color: var(--danger); font-size: 13px; }
|
||||||
|
|
||||||
|
/* Модальное окно подтверждения */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 300;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.modal {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 360px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 18px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
.modal-title { font-weight: 700; font-size: 16px; margin-bottom: 8px; }
|
||||||
|
.modal-msg { color: var(--text-muted); font-size: 14px; margin: 0 0 16px; }
|
||||||
|
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||||
|
|
||||||
/* «Барабан» рандома фракции — мелькающее название вместо select */
|
/* «Барабан» рандома фракции — мелькающее название вместо select */
|
||||||
.faction-spin {
|
.faction-spin {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user