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
82 lines
4.1 KiB
TypeScript
82 lines
4.1 KiB
TypeScript
// Время показываем в поясе приложения — одном для всех, независимо от пояса браузера.
|
||
// Смещение задаёт 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())}`;
|
||
}
|