Находки прохода /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
51 lines
2.2 KiB
TypeScript
51 lines
2.2 KiB
TypeScript
// Отображаем время в фиксированном поясе +3 (МСК) независимо от пояса браузера.
|
|
const APP_TZ_OFFSET_MIN = 3 * 60;
|
|
|
|
const p2 = (n: number) => String(n).padStart(2, "0");
|
|
|
|
// iso — корректный момент (бэкенд отдаёт UTC со смещением). Сдвигаем в +3
|
|
// и форматируем по UTC-частям, чтобы получить «настенное» время МСК.
|
|
function shifted(iso: string): Date {
|
|
return new Date(new Date(iso).getTime() + APP_TZ_OFFSET_MIN * 60_000);
|
|
}
|
|
|
|
// Склонение существительного при числе: plural(3, "игрок", "игрока", "игроков").
|
|
export function plural(n: number, one: string, few: string, many: string): string {
|
|
const m10 = n % 10;
|
|
const m100 = n % 100;
|
|
if (m10 === 1 && m100 !== 11) return one;
|
|
if (m10 >= 2 && m10 <= 4 && (m100 < 12 || m100 > 14)) return few;
|
|
return many;
|
|
}
|
|
|
|
export function formatDuration(minutes: number | null | undefined): string {
|
|
if (minutes == null) return "—";
|
|
const h = Math.floor(minutes / 60);
|
|
const m = minutes % 60;
|
|
if (h && m) return `≈ ${h} ч ${m} мин`;
|
|
if (h) return `≈ ${h} ч`;
|
|
return `≈ ${m} мин`;
|
|
}
|
|
|
|
export function formatTime(iso: string | null | undefined): string {
|
|
if (!iso) return "—";
|
|
const d = shifted(iso);
|
|
if (Number.isNaN(d.getTime())) return iso;
|
|
return `${p2(d.getUTCDate())}.${p2(d.getUTCMonth() + 1)} ${p2(d.getUTCHours())}:${p2(d.getUTCMinutes())}`;
|
|
}
|
|
|
|
// «Дата игры» приходит date-only строкой (YYYY-MM-DD) — просто переставляем части,
|
|
// без сдвига пояса (бэкенд уже посчитал дату в поясе приложения).
|
|
export function formatDate(iso: string | null | undefined): string {
|
|
if (!iso) return "—";
|
|
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso);
|
|
return m ? `${m[3]}.${m[2]}.${m[1]}` : iso;
|
|
}
|
|
|
|
export function formatDateTime(iso: string | null | undefined): string {
|
|
if (!iso) return "—";
|
|
const d = shifted(iso);
|
|
if (Number.isNaN(d.getTime())) return iso;
|
|
return `${p2(d.getUTCDate())}.${p2(d.getUTCMonth() + 1)}.${d.getUTCFullYear()} ${p2(d.getUTCHours())}:${p2(d.getUTCMinutes())}`;
|
|
}
|