Ревью: корректность фронтенда

Находки прохода /code-review high по frontend/src:

- CreateMatchPage: барабан рандома, таймеры и строки адресовались по индексу,
  а удаление строки индексы сдвигает — фракция могла записаться соседу или
  потеряться. Строки получили стабильный id.
- AdminAccountsPage: переключение «активен» глотало ошибку и всё равно
  показывало «Сохранено».
- useServerEvents/useFinishMatch: завершение чужой партии не инвалидировало
  историю игр и публичные профили — открытый профиль показывал состав до
  завершения.
- guards/useMe: обрыв связи не отличался от «нет сессии», и мигание сети
  выкидывало авторизованного пользователя на /login. Транспортные ошибки
  повторяем, гварды показывают сообщение вместо редиректа.
- format: fallback на нераспознанную дату не работал (new Date не бросает
  исключение), и в интерфейс попадало «NaN.NaN NaN:NaN».
- PlaceEditor: без onPointerCancel прерванный перенос оставлял блок с классом
  dragging и сдвигом, которые React не снимает — они выставлены в обход него.
- AdminFactionsPage: refetch после сохранения одной фракции затирал
  несохранённый ввод в остальных.
- MatchDetailPage: nav(-1) после удаления уводил из приложения при открытии
  партии по прямой ссылке; id из URL мог быть NaN и уходил в запрос.

#8

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186Fk74jkkszahEHSjBzTjD
This commit is contained in:
2026-09-09 15:30:56 +03:00
co-authored by Claude Opus 5
parent 62d75ea176
commit c4b72cca39
10 changed files with 105 additions and 57 deletions
+6 -1
View File
@@ -12,7 +12,9 @@ function readCsrfToken(): string | null {
export function useMatch(matchId: number | null) {
return useQuery({
queryKey: matchId ? qk.match(matchId) : ["match", "none"],
// Ключ и enabled должны сходиться: иначе запрос без валидного id кэшировал бы
// свою ошибку под общим ключом-заглушкой.
queryKey: matchId != null ? qk.match(matchId) : ["match", "none"],
enabled: matchId != null,
queryFn: async () =>
unwrap(
@@ -63,6 +65,9 @@ export function useFinishMatch() {
qc.invalidateQueries({ queryKey: qk.leaderboard });
qc.invalidateQueries({ queryKey: qk.home });
qc.invalidateQueries({ queryKey: qk.myStats });
// Завершённая партия попадает в историю игр и в публичные профили участников.
qc.invalidateQueries({ queryKey: ["userMatches"] });
qc.invalidateQueries({ queryKey: ["publicProfile"] });
},
});
}