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 { ApiError } from "../api/client";
|
||||
import { ConfirmDialog } from "../components/ConfirmDialog";
|
||||
import { MatchMedia } from "../components/MatchMedia";
|
||||
import { Spinner } from "../components/Spinner";
|
||||
import { formatDuration, formatTime } from "../domain/format";
|
||||
@@ -39,6 +40,7 @@ export function MatchDetailPage() {
|
||||
const [winReason, setWinReason] = useState<WinReason>("objectives");
|
||||
const [overall, setOverall] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [confirmRemove, setConfirmRemove] = useState(false);
|
||||
|
||||
if (isLoading) return <Spinner />;
|
||||
if (!match) return <div className="muted">Партия не найдена.</div>;
|
||||
@@ -271,10 +273,32 @@ export function MatchDetailPage() {
|
||||
)}
|
||||
|
||||
{canModify && (
|
||||
<button className="btn btn-danger" style={{ width: "100%" }} onClick={remove}>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
style={{ width: "100%" }}
|
||||
onClick={() => setConfirmRemove(true)}
|
||||
>
|
||||
{inProgress ? "Отменить партию" : "Удалить партию"}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<ConfirmDialog
|
||||
open={confirmRemove}
|
||||
danger
|
||||
title={inProgress ? "Отменить партию?" : "Удалить партию?"}
|
||||
message={
|
||||
inProgress
|
||||
? "Незавершённая партия будет удалена. Действие необратимо."
|
||||
: "Сыгранная партия и её результаты будут удалены. Действие необратимо."
|
||||
}
|
||||
confirmLabel={inProgress ? "Отменить партию" : "Удалить"}
|
||||
cancelLabel="Назад"
|
||||
onCancel={() => setConfirmRemove(false)}
|
||||
onConfirm={() => {
|
||||
setConfirmRemove(false);
|
||||
remove();
|
||||
}}
|
||||
/>
|
||||
</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-ghost { background: transparent; }
|
||||
.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; }
|
||||
|
||||
.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; }
|
||||
|
||||
/* Модальное окно подтверждения */
|
||||
.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 */
|
||||
.faction-spin {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user