Files
runners-calendar/frontend/src/lib/raceMetrics.ts
Vaka.pro e0ed0b6435
Some checks failed
CI / build-and-test (pull_request) Has been cancelled
fix: прод — CORS, версия API, ошибки клиента и подсказка по прошедшим стартам
- CORS_ORIGIN: несколько origin через запятую; комментарии в .env.example
- Версия бэкенда: APP_VERSION, безопасное чтение package.json, футер при пустой версии
- Сообщения API: unknown_error и ответы 401/403/404 без JSON; отладочный лог при !ok
- Статус «внесите результат» для прошедшей даты + блок на карточке старта и стили
2026-04-08 01:21:11 +03:00

163 lines
4.5 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.
import type { Race } from "../api";
const MS_IN_DAY = 24 * 60 * 60 * 1000;
/** API date: YYYY-MM-DD или ISO-строка от сериализации (не склеивать с «T00:00:00» повторно). */
export function parseRaceDate(date: string): Date {
const ymd = date.slice(0, 10);
if (/^\d{4}-\d{2}-\d{2}$/.test(ymd)) {
return new Date(`${ymd}T00:00:00`);
}
const parsed = new Date(date);
return parsed;
}
export function parseFinishTimeToSeconds(value: string | null): number | null {
if (!value) {
return null;
}
const parts = value.split(":").map((part) => Number(part));
if (parts.some((part) => Number.isNaN(part) || part < 0)) {
return null;
}
if (parts.length === 2) {
const [minutes, seconds] = parts;
return minutes * 60 + seconds;
}
if (parts.length === 3) {
const [hours, minutes, seconds] = parts;
return hours * 3600 + minutes * 60 + seconds;
}
return null;
}
export function formatDistance(distanceKm: number): string {
return `${distanceKm.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} км`;
}
export function formatRaceDate(date: string): string {
return parseRaceDate(date).toLocaleDateString("ru-RU", {
day: "2-digit",
month: "long",
year: "numeric",
});
}
export function sortByDateAsc(races: Race[]): Race[] {
return [...races].sort((left, right) => parseRaceDate(left.date).getTime() - parseRaceDate(right.date).getTime());
}
export function sortByDateDesc(races: Race[]): Race[] {
return [...races].sort((left, right) => parseRaceDate(right.date).getTime() - parseRaceDate(left.date).getTime());
}
export function splitRacesByDate(races: Race[], now: Date = new Date()): { upcoming: Race[]; past: Race[] } {
const today = new Date(now);
today.setHours(0, 0, 0, 0);
const upcoming: Race[] = [];
const past: Race[] = [];
for (const race of races) {
if (parseRaceDate(race.date).getTime() >= today.getTime()) {
upcoming.push(race);
} else {
past.push(race);
}
}
return {
upcoming: sortByDateAsc(upcoming),
past: sortByDateDesc(past),
};
}
function pluralizeDays(n: number): string {
const mod10 = n % 10;
const mod100 = n % 100;
if (mod100 >= 11 && mod100 <= 19) {
return "дней";
}
if (mod10 === 1) {
return "день";
}
if (mod10 >= 2 && mod10 <= 4) {
return "дня";
}
return "дней";
}
export function getRaceCountdownLabel(date: string, now: Date = new Date()): string {
const today = new Date(now);
today.setHours(0, 0, 0, 0);
const target = parseRaceDate(date);
const days = Math.ceil((target.getTime() - today.getTime()) / MS_IN_DAY);
if (days <= 0) {
return "сегодня";
}
return `через ${days} ${pluralizeDays(days)}`;
}
export function isCloseDistance(left: number, right: number): boolean {
return Math.abs(left - right) < 0.05;
}
export function getPaceLabel(finishTime: string | null, distanceKm: number): string | null {
const totalSeconds = parseFinishTimeToSeconds(finishTime);
if (!totalSeconds || distanceKm <= 0) {
return null;
}
const paceSeconds = Math.round(totalSeconds / distanceKm);
const paceMinutes = Math.floor(paceSeconds / 60);
const paceRemainder = paceSeconds % 60;
return `${String(paceMinutes).padStart(2, "0")}:${String(paceRemainder).padStart(2, "0")} /км`;
}
function isPastDateNeedingResult(status: Race["status"], raceDate: string): boolean {
if (status !== "planned" && status !== "registered") {
return false;
}
const today = new Date();
today.setHours(0, 0, 0, 0);
return parseRaceDate(raceDate).getTime() < today.getTime();
}
export function raceNeedsResultEntry(race: Race): boolean {
return isPastDateNeedingResult(race.status, race.date);
}
export function getRaceStatusClassName(status: Race["status"], raceDate?: string): string {
const base = "race-card__status";
let tier = `${base}--planned`;
if (status === "completed") {
tier = `${base}--completed`;
} else if (status === "registered") {
tier = `${base}--registered`;
}
const needs =
raceDate && isPastDateNeedingResult(status, raceDate) ? ` ${base}--needs-result` : "";
return `${base} ${tier}${needs}`;
}
export function getRaceStatusLabel(status: Race["status"], raceDate?: string): string {
if (raceDate && isPastDateNeedingResult(status, raceDate)) {
return "внесите результат";
}
if (status === "completed") {
return "пробежал";
}
if (status === "registered") {
return "зарегистрирован";
}
return "планирую";
}