Files
ForbiddenStarsApp/frontend/src/domain/format.ts
T
NotBigGhostandClaude Opus 5 8d4f7a95df Время: один пояс приложения из .env и на сервере, и на фронте
APP_TZ_OFFSET_HOURS влиял только на «дату игры», а фронт показывал время в
захардкоженных +3 — при другом значении дата и время партии противоречили друг
другу. Теперь /api/auth/config отдаёт tz_offset_hours, App.tsx выставляет его в
format.ts до первой отрисовки страниц, и в нём же показывается всё время и
вводятся даты объявлений (запасное значение +3, если конфиг недоступен). Пояс
устройства не учитывается — решение владельца. Валидатор ограничивает смещение
−12..14. #68

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
2026-09-19 03:05:30 +03:00

82 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Время показываем в поясе приложения — одном для всех, независимо от пояса браузера.
// Смещение задаёт APP_TZ_OFFSET_HOURS на бэкенде (в нём же считается «дата игры»);
// App.tsx получает его из /api/auth/config до первой отрисовки страниц (#68).
// 3 ч (МСК) — запасное значение, если конфиг недоступен.
let appTzOffsetMin = 3 * 60;
export function setAppTzOffsetHours(hours: number): void {
appTzOffsetMin = hours * 60;
}
const p2 = (n: number) => String(n).padStart(2, "0");
// iso — корректный момент (бэкенд отдаёт UTC со смещением). Сдвигаем в пояс приложения
// и форматируем по UTC-частям, чтобы получить «настенное» время этого пояса.
function shifted(iso: string): Date {
return new Date(new Date(iso).getTime() + appTzOffsetMin * 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;
}
// Изменение рейтинга за партию: всегда со знаком и одним знаком после запятой
// («+12.3», «−4.1», «0.0»); минус — типографский, как в справке.
export function formatRatingDelta(delta: number): string {
const abs = Math.abs(delta).toFixed(1);
if (abs === "0.0") return "0.0";
return `${delta > 0 ? "+" : "−"}${abs}`;
}
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;
}
// Значение <input type="datetime-local"> — «настенное» время в том же поясе приложения, что
// и всё отображение: админ вводит время показа объявления в нём, где бы ни был браузер.
export function toAppLocalInput(iso: string): string {
const d = shifted(iso);
return `${d.getUTCFullYear()}-${p2(d.getUTCMonth() + 1)}-${p2(d.getUTCDate())}T${p2(d.getUTCHours())}:${p2(d.getUTCMinutes())}`;
}
// Обратно: «настенное» время пояса приложения из datetime-local → момент ISO (UTC).
// null — поле пустое.
export function fromAppLocalInput(value: string): string | null {
const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/.exec(value);
if (!m) return null;
const ms = Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5]) - appTzOffsetMin * 60_000;
return new Date(ms).toISOString();
}
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())}`;
}