Тосты: убрать лишние, остальные показывать как уведомления сверху
Многие тосты повторяли то, что пользователь и так видит сразу после действия: новую фракцию или аватар в профиле, переход на страницу партии, исчезнувшую карточку приглашения, закрывшийся редактор. Такой шум приучает не читать всплывашки, в том числе ошибки. Убраны 19 таких тостов в профиле, группах, партиях и админке. Остались ошибки, предупреждения (лимит фото, повторы фракций, конфликт правок, «вы не в группе») и сообщения о действиях без видимого следа: приглашение отправлено, дополнения сохранены, вступление в группу, пароль задан. Сохранение текстового поля, где заметна только погасшая кнопка, тоже осталось. По просьбе владельца оставшиеся тосты показываются не нижней «таблеткой», а всплывашкой сверху, как входящие уведомления. ToastProvider держит одну стопку на всё приложение: show — обычное сообщение, error — ошибка с красной полосой (держится 5 с), notify — уведомление с переходом по клику. Одинаковое сообщение не копится, а заменяет прежнее; клик по карточке закрывает её. NotificationToaster больше не рисует свою стопку, а отдаёт новые уведомления в общую, поэтому они не наезжают на сообщения. Все вызовы с ApiError и отказы (лимит фото, партия изменилась на другом устройстве) переведены на toast.error. Нижний класс .toast удалён. #41 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { ApiError } from "../api/client";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useCreateGroup } from "../hooks/groups";
|
||||
import { useExpansions } from "../hooks/reference";
|
||||
import { Switch } from "./Switch";
|
||||
@@ -9,7 +8,6 @@ import { Switch } from "./Switch";
|
||||
export function CreateGroupForm({ onCreated }: { onCreated?: () => void }) {
|
||||
const { data: expansions } = useExpansions();
|
||||
const createGroup = useCreateGroup();
|
||||
const toast = useToast();
|
||||
const [name, setName] = useState("");
|
||||
const [selected, setSelected] = useState<Set<number>>(new Set());
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -25,7 +23,6 @@ export function CreateGroupForm({ onCreated }: { onCreated?: () => void }) {
|
||||
setError(null);
|
||||
try {
|
||||
await createGroup.mutateAsync({ name: name.trim(), expansion_ids: [...selected] });
|
||||
toast.show("Группа создана");
|
||||
onCreated?.();
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? e.message : "Не удалось создать группу");
|
||||
|
||||
@@ -15,16 +15,15 @@ export function GroupInvitations() {
|
||||
await accept.mutateAsync(id);
|
||||
toast.show("Вы вступили в группу");
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Ошибка");
|
||||
toast.error(e instanceof ApiError ? e.message : "Ошибка");
|
||||
}
|
||||
};
|
||||
|
||||
const onDecline = async (id: number) => {
|
||||
try {
|
||||
await decline.mutateAsync(id);
|
||||
toast.show("Приглашение отклонено");
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Ошибка");
|
||||
toast.error(e instanceof ApiError ? e.message : "Ошибка");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -35,14 +35,14 @@ export function MatchMedia({
|
||||
let slots = max - attachments.length;
|
||||
for (const f of files) {
|
||||
if (slots <= 0) {
|
||||
toast.show(`Максимум ${max} фото`);
|
||||
toast.error(`Максимум ${max} фото`);
|
||||
break;
|
||||
}
|
||||
try {
|
||||
await onUpload(f);
|
||||
slots--;
|
||||
} catch (err) {
|
||||
toast.show(err instanceof ApiError ? err.message : "Не удалось загрузить");
|
||||
toast.error(err instanceof ApiError ? err.message : "Не удалось загрузить");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ export function MatchMedia({
|
||||
try {
|
||||
await onDelete(id);
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Не удалось удалить");
|
||||
toast.error(e instanceof ApiError ? e.message : "Не удалось удалить");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useNotifications, type Notification } from "../hooks/notifications";
|
||||
|
||||
const TOAST_TTL_MS = 5000;
|
||||
const MAX_VISIBLE = 4;
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { useNotifications } from "../hooks/notifications";
|
||||
|
||||
/** Всплывашки сверху экрана в момент прихода новых уведомлений (push-to-invalidate → pull).
|
||||
* Первый снимок списка не всплывает (чтобы не спамить при загрузке) — только новые id. */
|
||||
* Первый снимок списка не всплывает (чтобы не спамить при загрузке) — только новые id.
|
||||
* Показывает их общая стопка ToastProvider — там же, где сообщения приложения. */
|
||||
export function NotificationToaster({ enabled }: { enabled: boolean }) {
|
||||
const { data } = useNotifications(enabled);
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
const seen = useRef<Set<number> | null>(null);
|
||||
const [toasts, setToasts] = useState<Notification[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
@@ -22,29 +21,16 @@ export function NotificationToaster({ enabled }: { enabled: boolean }) {
|
||||
return;
|
||||
}
|
||||
const fresh = items.filter((n) => !seen.current!.has(n.id));
|
||||
if (fresh.length === 0) return;
|
||||
fresh.forEach((n) => seen.current!.add(n.id));
|
||||
setToasts((prev) => [...fresh, ...prev].slice(0, MAX_VISIBLE));
|
||||
fresh.forEach((n) => {
|
||||
setTimeout(() => setToasts((prev) => prev.filter((t) => t.id !== n.id)), TOAST_TTL_MS);
|
||||
});
|
||||
}, [data]);
|
||||
// Стопка добавляет новое наверх, поэтому идём с конца: первое в списке окажется сверху.
|
||||
[...fresh].reverse().forEach((n) =>
|
||||
toast.notify({
|
||||
title: n.title,
|
||||
body: n.body,
|
||||
onClick: n.link ? () => navigate(n.link!) : undefined,
|
||||
}),
|
||||
);
|
||||
}, [data, toast, navigate]);
|
||||
|
||||
if (!enabled || toasts.length === 0) return null;
|
||||
|
||||
const click = (n: Notification) => {
|
||||
setToasts((prev) => prev.filter((t) => t.id !== n.id));
|
||||
if (n.link) navigate(n.link);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="toast-top-wrap">
|
||||
{toasts.map((n) => (
|
||||
<button key={n.id} className="toast-top" onClick={() => click(n)}>
|
||||
<div className="toast-top-title">{n.title}</div>
|
||||
{n.body && <div className="toast-top-body">{n.body}</div>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,31 +1,97 @@
|
||||
import { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
interface ToastCtx {
|
||||
show: (message: string) => void;
|
||||
// Сообщения приложения и входящие уведомления — одна стопка всплывашек сверху экрана.
|
||||
type ToastKind = "info" | "error" | "notification";
|
||||
|
||||
interface ToastItem {
|
||||
id: number;
|
||||
kind: ToastKind;
|
||||
title: string;
|
||||
body?: string | null;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const Ctx = createContext<ToastCtx>({ show: () => {} });
|
||||
interface ToastCtx {
|
||||
/** Нейтральное сообщение: действие прошло, но на экране это не видно. */
|
||||
show: (message: string) => void;
|
||||
/** Ошибка или отказ — красная полоса, держится дольше. */
|
||||
error: (message: string) => void;
|
||||
/** Входящее уведомление (заголовок, текст, переход по клику). */
|
||||
notify: (n: { title: string; body?: string | null; onClick?: () => void }) => void;
|
||||
}
|
||||
|
||||
const TTL_MS: Record<ToastKind, number> = { info: 3000, error: 5000, notification: 5000 };
|
||||
const MAX_VISIBLE = 4;
|
||||
|
||||
const Ctx = createContext<ToastCtx>({ show: () => {}, error: () => {}, notify: () => {} });
|
||||
|
||||
export function ToastProvider({ children }: PropsWithChildren) {
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [items, setItems] = useState<ToastItem[]>([]);
|
||||
const nextId = useRef(1);
|
||||
const timers = useRef(new Map<number, ReturnType<typeof setTimeout>>());
|
||||
|
||||
const show = useCallback((msg: string) => {
|
||||
setMessage(msg);
|
||||
if (timer.current) clearTimeout(timer.current);
|
||||
timer.current = setTimeout(() => setMessage(null), 2800);
|
||||
useEffect(() => {
|
||||
const pending = timers.current;
|
||||
return () => pending.forEach(clearTimeout);
|
||||
}, []);
|
||||
|
||||
// Провайдер обёрнут вокруг всего приложения и перерисовывается на каждый тост:
|
||||
const dismiss = useCallback((id: number) => {
|
||||
clearTimeout(timers.current.get(id));
|
||||
timers.current.delete(id);
|
||||
setItems((prev) => prev.filter((t) => t.id !== id));
|
||||
}, []);
|
||||
|
||||
const push = useCallback(
|
||||
(item: Omit<ToastItem, "id">) => {
|
||||
const id = nextId.current++;
|
||||
setItems((prev) => {
|
||||
// Тот же текст уже на экране (двойной клик, повтор ошибки) — заменяем, а не копим.
|
||||
// Уведомления не схлопываем: одинаковый заголовок бывает у разных событий.
|
||||
const rest =
|
||||
item.kind === "notification"
|
||||
? prev
|
||||
: prev.filter((t) => !(t.kind === item.kind && t.title === item.title));
|
||||
return [{ ...item, id }, ...rest].slice(0, MAX_VISIBLE);
|
||||
});
|
||||
timers.current.set(id, setTimeout(() => dismiss(id), TTL_MS[item.kind]));
|
||||
},
|
||||
[dismiss],
|
||||
);
|
||||
|
||||
// Провайдер обёрнут вокруг всего приложения и перерисовывается на каждую всплывашку:
|
||||
// без useMemo новое значение контекста заставляло бы перерисоваться и всех
|
||||
// потребителей, к самому тосту отношения не имеющих.
|
||||
const value = useMemo(() => ({ show }), [show]);
|
||||
// потребителей, к самим всплывашкам отношения не имеющих.
|
||||
const value = useMemo<ToastCtx>(
|
||||
() => ({
|
||||
show: (message) => push({ kind: "info", title: message }),
|
||||
error: (message) => push({ kind: "error", title: message }),
|
||||
notify: ({ title, body, onClick }) => push({ kind: "notification", title, body, onClick }),
|
||||
}),
|
||||
[push],
|
||||
);
|
||||
|
||||
return (
|
||||
<Ctx.Provider value={value}>
|
||||
{children}
|
||||
{message && <div className="toast">{message}</div>}
|
||||
{/* Контейнер есть всегда: aria-live объявляет только то, что появилось внутри уже
|
||||
существующей области. */}
|
||||
<div className="toast-top-wrap" aria-live="polite">
|
||||
{items.map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
type="button"
|
||||
className={"toast-top" + (t.kind === "error" ? " toast-top-error" : "")}
|
||||
onClick={() => {
|
||||
dismiss(t.id);
|
||||
t.onClick?.();
|
||||
}}
|
||||
>
|
||||
<div className="toast-top-title">{t.title}</div>
|
||||
{t.body && <div className="toast-top-body">{t.body}</div>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Ctx.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,9 +65,8 @@ export function AccountPage() {
|
||||
const saveFavorite = async (factionId: number | null) => {
|
||||
try {
|
||||
await updateFavorite.mutateAsync(factionId);
|
||||
toast.show(factionId == null ? "Любимая фракция убрана" : "Любимая фракция сохранена");
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Ошибка");
|
||||
toast.error(e instanceof ApiError ? e.message : "Ошибка");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -75,7 +74,7 @@ export function AccountPage() {
|
||||
try {
|
||||
await updateHistory.mutateAsync(prefs);
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Ошибка");
|
||||
toast.error(e instanceof ApiError ? e.message : "Ошибка");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -85,9 +84,8 @@ export function AccountPage() {
|
||||
try {
|
||||
await updateNick.mutateAsync(nick);
|
||||
setNickname("");
|
||||
toast.show("Никнейм обновлён");
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Ошибка");
|
||||
toast.error(e instanceof ApiError ? e.message : "Ошибка");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -96,7 +94,7 @@ export function AccountPage() {
|
||||
await updateBio.mutateAsync(bioValue.trim());
|
||||
toast.show("Сохранено");
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Ошибка");
|
||||
toast.error(e instanceof ApiError ? e.message : "Ошибка");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -106,18 +104,16 @@ export function AccountPage() {
|
||||
if (!file) return;
|
||||
try {
|
||||
await uploadAvatar.mutateAsync(file);
|
||||
toast.show("Аватар обновлён");
|
||||
} catch (err) {
|
||||
toast.show(err instanceof ApiError ? err.message : "Не удалось загрузить");
|
||||
toast.error(err instanceof ApiError ? err.message : "Не удалось загрузить");
|
||||
}
|
||||
};
|
||||
|
||||
const removeAvatar = async () => {
|
||||
try {
|
||||
await deleteAvatar.mutateAsync();
|
||||
toast.show("Аватар удалён");
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Ошибка");
|
||||
toast.error(e instanceof ApiError ? e.message : "Ошибка");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -307,9 +303,8 @@ function LoginSettingsCard({ me }: { me: Me }) {
|
||||
const onTelegram = async (payload: Record<string, unknown>) => {
|
||||
try {
|
||||
await linkTelegram.mutateAsync(payload);
|
||||
toast.show("Telegram привязан");
|
||||
} catch (err) {
|
||||
toast.show(err instanceof ApiError ? err.message : "Не удалось привязать Telegram");
|
||||
toast.error(err instanceof ApiError ? err.message : "Не удалось привязать Telegram");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ export function CreateMatchPage() {
|
||||
} catch (e) {
|
||||
if (reelTimers.current[id]) clearTimeout(reelTimers.current[id]);
|
||||
setSpin((s) => ({ ...s, [id]: null }));
|
||||
toast.show(e instanceof ApiError ? e.message : "Ошибка рандома");
|
||||
toast.error(e instanceof ApiError ? e.message : "Ошибка рандома");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -176,7 +176,6 @@ export function CreateMatchPage() {
|
||||
was_random: r.was_random,
|
||||
})),
|
||||
});
|
||||
toast.show("Партия начата");
|
||||
navigate(`/match/${match.id}`, { replace: true });
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? e.message : "Не удалось начать партию");
|
||||
|
||||
@@ -53,7 +53,7 @@ export function GroupPage() {
|
||||
setNewNick("");
|
||||
toast.show("Приглашение отправлено");
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Ошибка");
|
||||
toast.error(e instanceof ApiError ? e.message : "Ошибка");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export function GroupSettingsPage() {
|
||||
setName(null);
|
||||
toast.show("Название сохранено");
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Ошибка");
|
||||
toast.error(e instanceof ApiError ? e.message : "Ошибка");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -68,7 +68,7 @@ export function GroupSettingsPage() {
|
||||
await setExpansions.mutateAsync([...selected]);
|
||||
toast.show("Дополнения сохранены");
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Ошибка");
|
||||
toast.error(e instanceof ApiError ? e.message : "Ошибка");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -76,7 +76,7 @@ export function GroupSettingsPage() {
|
||||
try {
|
||||
await removeMember.mutateAsync(userId);
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Ошибка");
|
||||
toast.error(e instanceof ApiError ? e.message : "Ошибка");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -165,10 +165,9 @@ export function MatchDetailPage() {
|
||||
expected_version: match.version,
|
||||
},
|
||||
});
|
||||
toast.show("Партия завершена");
|
||||
} catch (e) {
|
||||
if (isStale(e)) {
|
||||
toast.show("Партия изменилась на другом устройстве — обновлено");
|
||||
toast.error("Партия изменилась на другом устройстве — обновлено");
|
||||
refetch();
|
||||
return;
|
||||
}
|
||||
@@ -234,11 +233,10 @@ export function MatchDetailPage() {
|
||||
expected_version: match.version,
|
||||
},
|
||||
});
|
||||
toast.show("Партия обновлена");
|
||||
cancelEditing();
|
||||
} catch (e) {
|
||||
if (isStale(e)) {
|
||||
toast.show("Партия изменилась на другом устройстве — обновлено");
|
||||
toast.error("Партия изменилась на другом устройстве — обновлено");
|
||||
refetch();
|
||||
cancelEditing();
|
||||
return;
|
||||
@@ -257,11 +255,11 @@ export function MatchDetailPage() {
|
||||
navigate("/group", { replace: true });
|
||||
} catch (e) {
|
||||
if (isStale(e)) {
|
||||
toast.show("Партия изменилась на другом устройстве — обновлено");
|
||||
toast.error("Партия изменилась на другом устройстве — обновлено");
|
||||
refetch();
|
||||
return;
|
||||
}
|
||||
toast.show(e instanceof ApiError ? e.message : "Не удалось выполнить");
|
||||
toast.error(e instanceof ApiError ? e.message : "Не удалось выполнить");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ export function PublicProfilePage() {
|
||||
await invite.mutateAsync({ groupId, nickname: data.nickname });
|
||||
toast.show("Приглашение отправлено");
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Не удалось пригласить");
|
||||
toast.error(e instanceof ApiError ? e.message : "Не удалось пригласить");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ export function AdminAccountsPage() {
|
||||
setPasswordId(null);
|
||||
setNewPassword("");
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Не удалось задать пароль");
|
||||
toast.error(e instanceof ApiError ? e.message : "Не удалось задать пароль");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -51,19 +51,17 @@ export function AdminAccountsPage() {
|
||||
if (editId == null || !name) return;
|
||||
try {
|
||||
await update.mutateAsync({ userId: editId, nickname: name });
|
||||
toast.show("Ник обновлён");
|
||||
setEditId(null);
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Не удалось переименовать");
|
||||
toast.error(e instanceof ApiError ? e.message : "Не удалось переименовать");
|
||||
}
|
||||
};
|
||||
|
||||
const toggleActive = async (id: number, isActive: boolean) => {
|
||||
try {
|
||||
await update.mutateAsync({ userId: id, is_active: !isActive });
|
||||
toast.show("Сохранено");
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Не удалось сохранить");
|
||||
toast.error(e instanceof ApiError ? e.message : "Не удалось сохранить");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -35,9 +35,8 @@ export function AdminAchievementsPage() {
|
||||
setName("");
|
||||
setDescription("");
|
||||
setCondition("");
|
||||
toast.show("Ачивка создана");
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Ошибка");
|
||||
toast.error(e instanceof ApiError ? e.message : "Ошибка");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -100,9 +99,8 @@ function AchievementCard({ ach }: { ach: AchievementRead }) {
|
||||
body: { name: name.trim(), description, condition },
|
||||
});
|
||||
setEditing(false);
|
||||
toast.show("Сохранено");
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Ошибка");
|
||||
toast.error(e instanceof ApiError ? e.message : "Ошибка");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -112,18 +110,16 @@ function AchievementCard({ ach }: { ach: AchievementRead }) {
|
||||
if (!file) return;
|
||||
try {
|
||||
await uploadIcon.mutateAsync({ slug: ach.slug, file });
|
||||
toast.show("Иконка обновлена");
|
||||
} catch (err) {
|
||||
toast.show(err instanceof ApiError ? err.message : "Не удалось загрузить");
|
||||
toast.error(err instanceof ApiError ? err.message : "Не удалось загрузить");
|
||||
}
|
||||
};
|
||||
|
||||
const remove = async () => {
|
||||
try {
|
||||
await del.mutateAsync(ach.slug);
|
||||
toast.show("Удалено");
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Ошибка");
|
||||
toast.error(e instanceof ApiError ? e.message : "Ошибка");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export function AdminFactionsPage() {
|
||||
await rename.mutateAsync({ factionId: id, name_ru: names[id] });
|
||||
toast.show("Название обновлено во всей системе");
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Ошибка");
|
||||
toast.error(e instanceof ApiError ? e.message : "Ошибка");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { MatchMedia } from "../../components/MatchMedia";
|
||||
import { Spinner } from "../../components/Spinner";
|
||||
import { Switch } from "../../components/Switch";
|
||||
import { WIN_REASONS, type WinReason } from "../../domain/winReasons";
|
||||
import { useToast } from "../../context/ToastContext";
|
||||
import {
|
||||
useAdminDeleteAttachment,
|
||||
useAdminFactions,
|
||||
@@ -33,7 +32,6 @@ export function AdminMatchEdit({ matchId, onClose }: { matchId: number; onClose:
|
||||
const update = useAdminUpdateMatch();
|
||||
const uploadAtt = useAdminUploadAttachment(matchId);
|
||||
const deleteAtt = useAdminDeleteAttachment(matchId);
|
||||
const toast = useToast();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [rows, setRows] = useState<Row[]>([]);
|
||||
@@ -83,7 +81,6 @@ export function AdminMatchEdit({ matchId, onClose }: { matchId: number; onClose:
|
||||
})),
|
||||
},
|
||||
});
|
||||
toast.show("Партия обновлена");
|
||||
onClose();
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? e.message : "Не удалось сохранить");
|
||||
|
||||
@@ -149,9 +149,8 @@ export function AdminRecordsPage() {
|
||||
setDeleteTarget(null);
|
||||
try {
|
||||
await del.mutateAsync(deleteTarget.id);
|
||||
toast.show("Удалено");
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Не удалось удалить");
|
||||
toast.error(e instanceof ApiError ? e.message : "Не удалось удалить");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -57,9 +57,8 @@ export function DevDeleteAccountButton({
|
||||
setConfirmOpen(false);
|
||||
try {
|
||||
await del.mutateAsync();
|
||||
toast.show("Аккаунт удалён");
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Не удалось удалить");
|
||||
toast.error(e instanceof ApiError ? e.message : "Не удалось удалить");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -193,22 +193,6 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent); }
|
||||
.switch input:checked + .switch-track .switch-thumb { transform: translateX(18px); background: #fff; }
|
||||
.switch input:disabled + .switch-track { opacity: 0.5; }
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: calc(var(--bottombar-height) + 16px);
|
||||
transform: translateX(-50%);
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
padding: 10px 16px;
|
||||
border-radius: 999px;
|
||||
z-index: 100;
|
||||
max-width: 90%;
|
||||
text-align: center;
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* ─── Уведомления ─────────────────────────────────────────────────────────── */
|
||||
|
||||
/* Зоны верхней панели: слева back/заголовок, по центру название, справа колокольчик */
|
||||
@@ -471,5 +455,6 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent); }
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.toast-top-error { border-left-color: var(--danger); }
|
||||
.toast-top-title { font-weight: 700; }
|
||||
.toast-top-body { margin-top: 2px; font-size: 13px; color: var(--text-muted); }
|
||||
|
||||
Reference in New Issue
Block a user