Многие тосты повторяли то, что пользователь и так видит сразу после действия: новую фракцию или аватар в профиле, переход на страницу партии, исчезнувшую карточку приглашения, закрывшийся редактор. Такой шум приучает не читать всплывашки, в том числе ошибки. Убраны 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
64 lines
2.2 KiB
TypeScript
64 lines
2.2 KiB
TypeScript
import { useState } from "react";
|
|
|
|
import { ApiError } from "../api/client";
|
|
import { useCreateGroup } from "../hooks/groups";
|
|
import { useExpansions } from "../hooks/reference";
|
|
import { Switch } from "./Switch";
|
|
|
|
export function CreateGroupForm({ onCreated }: { onCreated?: () => void }) {
|
|
const { data: expansions } = useExpansions();
|
|
const createGroup = useCreateGroup();
|
|
const [name, setName] = useState("");
|
|
const [selected, setSelected] = useState<Set<number>>(new Set());
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const toggle = (id: number) =>
|
|
setSelected((s) => {
|
|
const next = new Set(s);
|
|
next.has(id) ? next.delete(id) : next.add(id);
|
|
return next;
|
|
});
|
|
|
|
const submit = async () => {
|
|
setError(null);
|
|
try {
|
|
await createGroup.mutateAsync({ name: name.trim(), expansion_ids: [...selected] });
|
|
onCreated?.();
|
|
} catch (e) {
|
|
setError(e instanceof ApiError ? e.message : "Не удалось создать группу");
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="card">
|
|
<h3>Новая группа</h3>
|
|
<div className="field">
|
|
<label className="label">Название</label>
|
|
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="Например: Вечер 40k" />
|
|
</div>
|
|
<div className="field">
|
|
<label className="label">Дополнения группы</label>
|
|
<div className="stack">
|
|
{(expansions ?? []).map((e) => (
|
|
<label key={e.id} className="row" style={{ justifyContent: "space-between" }}>
|
|
<span>
|
|
{e.name_ru}
|
|
{e.is_base && <span className="muted small"> (всегда включено)</span>}
|
|
</span>
|
|
<Switch
|
|
disabled={e.is_base}
|
|
checked={e.is_base || selected.has(e.id)}
|
|
onChange={() => toggle(e.id)}
|
|
/>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
{error && <p className="error-text">{error}</p>}
|
|
<button className="btn btn-primary btn-block" onClick={submit} disabled={createGroup.isPending}>
|
|
Создать
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|