Тосты: убрать лишние, остальные показывать как уведомления сверху

Многие тосты повторяли то, что пользователь и так видит сразу после действия:
новую фракцию или аватар в профиле, переход на страницу партии, исчезнувшую
карточку приглашения, закрывшийся редактор. Такой шум приучает не читать
всплывашки, в том числе ошибки. Убраны 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:
2026-09-13 15:45:34 +03:00
co-authored by Claude Opus 5
parent 0cbee9d866
commit 3587fc8af2
18 changed files with 129 additions and 115 deletions
+7 -12
View File
@@ -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");
}
};
+1 -2
View File
@@ -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 : "Не удалось начать партию");
+1 -1
View File
@@ -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 : "Ошибка");
}
};
+3 -3
View File
@@ -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 : "Ошибка");
}
};
+4 -6
View File
@@ -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 : "Не удалось выполнить");
}
};
+1 -1
View File
@@ -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 : "Не удалось удалить");
}
}}
/>