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 = { short: { variant: "short", imageSrc: "/images/race-short.jpg", fallbackSrc: "/images/race-short.jpg", imageFit: "cover", label: "Городской темп", }, half: { variant: "half", imageSrc: "/images/race-half.jpg", fallbackSrc: "/images/race-half.jpg", imageFit: "cover", label: "Полумарафон", }, marathon: { variant: "marathon", imageSrc: "/images/race-marathon.jpg", fallbackSrc: "/images/race-marathon.jpg", imageFit: "cover", label: "Марафон", }, trail: { variant: "trail", imageSrc: "/images/race-trail.jpg", fallbackSrc: "/images/race-trail.jpg", imageFit: "cover", label: "Трейл", }, night: { variant: "night", imageSrc: "/images/race-night.jpg", fallbackSrc: "/images/race-night.jpg", 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); if (race.coverImageUrl) { return { ...fallback, imageSrc: race.coverImageUrl, fallbackSrc: fallback.imageSrc, }; } 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, }; }