- Четыре копии «ресайз → FormData → CSRF из cookie → fetch → разбор конверта ошибки» (аватар, фото партии, фото из админки, иконка ачивки) сведены в lib/upload.ts. Иконка по-прежнему уходит оригиналом: ресайз в JPEG убил бы прозрачность герба. - Три копии readCsrfToken и вторая копия resizeImage удалены — берём readCookie из api/client.ts и resizeImage из lib/image.ts с параметром размера. - Ключи, протухающие от партии, собраны в matchAffectedKeys: раньше они были написаны строками мимо реестра qk в двух местах, и переименование ключа сломало бы инвалидацию молча. - Различение «нет сессии» и «нет связи» вынесено в authProbeRetry и применено к обеим пробам. Прошлый заход чинил только игроцкие гварды, и RequireAdmin по-прежнему выкидывал админа на страницу входа при обрыве связи. - ToastContext больше не пересоздаёт значение контекста: провайдер обёрнут вокруг всего приложения, и каждый тост перерисовывал всех потребителей. - PlaceEditor не трогает DOM, пока цель подсветки не изменилась (было querySelectorAll на каждый pointermove). #8 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BoiJK9ux8peeyjLb8TYjFf
36 lines
1.3 KiB
TypeScript
36 lines
1.3 KiB
TypeScript
import { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
|
|
import type { PropsWithChildren } from "react";
|
|
|
|
interface ToastCtx {
|
|
show: (message: string) => void;
|
|
}
|
|
|
|
const Ctx = createContext<ToastCtx>({ show: () => {} });
|
|
|
|
export function ToastProvider({ children }: PropsWithChildren) {
|
|
const [message, setMessage] = useState<string | null>(null);
|
|
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
const show = useCallback((msg: string) => {
|
|
setMessage(msg);
|
|
if (timer.current) clearTimeout(timer.current);
|
|
timer.current = setTimeout(() => setMessage(null), 2800);
|
|
}, []);
|
|
|
|
// Провайдер обёрнут вокруг всего приложения и перерисовывается на каждый тост:
|
|
// без useMemo новое значение контекста заставляло бы перерисоваться и всех
|
|
// потребителей, к самому тосту отношения не имеющих.
|
|
const value = useMemo(() => ({ show }), [show]);
|
|
|
|
return (
|
|
<Ctx.Provider value={value}>
|
|
{children}
|
|
{message && <div className="toast">{message}</div>}
|
|
</Ctx.Provider>
|
|
);
|
|
}
|
|
|
|
export function useToast(): ToastCtx {
|
|
return useContext(Ctx);
|
|
}
|