feat(frontend): redesign race dashboard
Some checks failed
CI / build-and-test (pull_request) Has been cancelled

This commit is contained in:
Anton
2026-04-22 11:47:37 +03:00
parent 7b0267f9ac
commit 0da7454033
21 changed files with 1651 additions and 139 deletions

View File

@@ -0,0 +1,206 @@
import type { Race } from "../api";
export type RaceVisualVariant = "short" | "half" | "marathon" | "trail" | "night";
export type RaceVisualFit = "cover" | "contain";
interface RaceVisual {
variant: RaceVisualVariant;
imageSrc: string;
fallbackSrc: string;
imageFit: RaceVisualFit;
label: string;
}
interface OfficialRaceVisual {
keywords: string[];
imageSrc: string;
imageFit?: RaceVisualFit;
label: string;
}
const FALLBACK_VISUALS: Record<RaceVisualVariant, RaceVisual> = {
short: {
variant: "short",
imageSrc: "/images/race-short.png",
fallbackSrc: "/images/race-short.png",
imageFit: "cover",
label: "Городской темп",
},
half: {
variant: "half",
imageSrc: "/images/race-half.png",
fallbackSrc: "/images/race-half.png",
imageFit: "cover",
label: "Полумарафон",
},
marathon: {
variant: "marathon",
imageSrc: "/images/race-marathon.png",
fallbackSrc: "/images/race-marathon.png",
imageFit: "cover",
label: "Марафон",
},
trail: {
variant: "trail",
imageSrc: "/images/race-trail.png",
fallbackSrc: "/images/race-trail.png",
imageFit: "cover",
label: "Трейл",
},
night: {
variant: "night",
imageSrc: "/images/race-night.png",
fallbackSrc: "/images/race-night.png",
imageFit: "cover",
label: "Ночной старт",
},
};
const OFFICIAL_VISUALS: OfficialRaceVisual[] = [
{
keywords: ["забег апрель"],
imageSrc: "https://aprilrun5km.runc.run/uploads/page_card_photos/AprilRun_photo_1.jpg",
label: "Забег Апрель",
},
{
keywords: ["быстрый пес"],
imageSrc: "https://fastdogxc.runc.run/uploads/page_card_photos/Dog_spring_2026-5.jpg",
label: "Кросс",
},
{
keywords: ["лисья гора"],
imageSrc: "https://foxhillxc.runc.run/uploads/page_card_photos/Fox_Spring_2026-0.jpg",
label: "Кросс",
},
{
keywords: ["казанский марафон"],
imageSrc: "https://static.tildacdn.com/tild3961-6436-4462-b738-356665613039/Frame_2131327895.png",
imageFit: "contain",
label: "Казанский марафон",
},
{
keywords: ["мышкинский полумарафон", "по шести холмам"],
imageSrc: "https://static.tildacdn.com/tild6133-6137-4865-b166-623532313531/photo.jpg",
label: "Золотое кольцо",
},
{
keywords: ["забег.рф", "забег рф"],
imageSrc: "https://xn--80acghh.xn--p1ai/zabeg.jpg",
label: "ЗаБег.РФ",
},
{
keywords: ["переславский марафон", "александровские версты"],
imageSrc: "https://static.tildacdn.com/tild6432-3338-4533-b262-633339353335/photo_1.jpg",
label: "Золотое кольцо",
},
{
keywords: ["красочный забег"],
imageSrc: "https://colorrun5km.runc.run/uploads/page_card_photos/ColorRun2026-1.jpg",
label: "Красочный забег",
},
{
keywords: ["здорово кострома", "здорово, кострома"],
imageSrc: "https://static.tildacdn.com/tild6139-3539-4661-b232-386230336431/kostroma.jpg",
label: "Золотое кольцо",
},
{
keywords: ["ночной забег москва"],
imageSrc: "https://nightrun10km.runc.run/uploads/page_card_photos/NightRun_2026-9.jpg",
label: "Ночной забег",
},
{
keywords: ["белые ночи"],
imageSrc: "https://wnmarathon.runc.run/uploads/page_card_photos/WN_photo_01.jpg",
label: "Белые ночи",
},
{
keywords: ["сергиевым путем", "сергиевым путём"],
imageSrc: "https://static.tildacdn.com/tild6236-3466-4239-b666-393061326338/serg.jpg",
label: "Золотое кольцо",
},
{
keywords: ["ночной забег нижний новгород"],
imageSrc: "https://rrweb.russiarunning.com/-x740/generalimages/0531a1b8-3876-4620-8961-2fa374e474e5.png",
imageFit: "contain",
label: "Ночной забег",
},
{
keywords: ["suvorov extreme"],
imageSrc: "https://goldenultra.ru/grut/files/photos/100.jpg",
label: "Трейл",
},
{
keywords: ["рыбинский полумарафон", "великий хлебный путь"],
imageSrc: "https://static.tildacdn.com/tild6130-3230-4332-b932-366166366633/photo.jpg",
label: "Золотое кольцо",
},
{
keywords: ["ярославский полумарафон", "золотое кольцо"],
imageSrc: "https://static.tildacdn.com/tild6331-6333-4635-b635-376262373361/photo.jpg",
label: "Золотое кольцо",
},
{
keywords: ["моя столица"],
imageSrc: "https://static.tildacdn.com/tild3263-3036-4639-b830-653365663832/-min.jpg",
imageFit: "contain",
label: "Моя столица",
},
];
function normalizeTitle(value: string): string {
return value
.toLowerCase()
.replaceAll("ё", "е")
.replace(/[«»|]/g, " ")
.replace(/[^\p{L}\p{N}.&]+/gu, " ")
.replace(/\s+/g, " ")
.trim();
}
function getFallbackRaceVisual(race: Race): RaceVisual {
const title = normalizeTitle(race.title);
if (title.includes("ночной")) {
return FALLBACK_VISUALS.night;
}
if (
title.includes("trail") ||
title.includes("extreme") ||
title.includes("suvorov") ||
title.includes("трейл") ||
title.includes("экстрим")
) {
return FALLBACK_VISUALS.trail;
}
if (Math.abs(race.distanceKm - 42.2) < 0.8) {
return FALLBACK_VISUALS.marathon;
}
if (Math.abs(race.distanceKm - 21.1) < 0.4) {
return FALLBACK_VISUALS.half;
}
return FALLBACK_VISUALS.short;
}
export function getRaceVisual(race: Race): RaceVisual {
const fallback = getFallbackRaceVisual(race);
const title = normalizeTitle(race.title);
const official = OFFICIAL_VISUALS.find((visual) =>
visual.keywords.some((keyword) => title.includes(normalizeTitle(keyword))),
);
if (!official) {
return fallback;
}
return {
...fallback,
imageSrc: official.imageSrc,
fallbackSrc: fallback.imageSrc,
imageFit: official.imageFit ?? fallback.imageFit,
label: official.label,
};
}