import React, { useEffect, useState } from 'react'; import { Image, ImageProps, Skeleton } from '@chakra-ui/react'; import { getTeamLogo } from '../../utils/sportLogosAPI'; import { getLogoStyle, getLogoClassName } from '../../utils/logoUtils'; import '../../styles/logos.css'; import { usePublicSettings } from '../../hooks/usePublicSettings'; import { assetUrl } from '../../utils/url'; import { useIntersectionObserver } from '../../hooks/useIntersectionObserver'; // Lightweight cached overrides loader let __teamOverridesCache: { ts: number; data: { by_id?: Record; by_name?: Record } } | null = null; const loadTeamOverrides = async (): Promise<{ by_id?: Record; by_name?: Record }> => { const now = Date.now(); const TTL = 5_000; if (__teamOverridesCache && now - __teamOverridesCache.ts < TTL) { return __teamOverridesCache.data || {}; } // Try fresh public endpoint first try { const res = await fetch(`/api/v1/public/team-logo-overrides?t=${now}`, { cache: 'no-cache' }); if (res.ok) { const json = await res.json(); __teamOverridesCache = { ts: now, data: json || {} }; return json || {}; } } catch {} // Fallback to static cache snapshot try { const res2 = await fetch('/cache/prefetch/team_logo_overrides.json', { cache: 'no-cache' }); if (res2.ok) { const json = await res2.json(); __teamOverridesCache = { ts: now, data: json || {} }; return json || {}; } } catch {} // Final fallback: previously cached data or empty if (__teamOverridesCache) return __teamOverridesCache.data || {}; __teamOverridesCache = { ts: now, data: {} }; return {}; }; // Normalization helpers for name-based matching const __normalize = (s?: string) => { let out = String(s || ''); out = out .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .toLowerCase(); out = out.replace(/[\u2012\u2013\u2014\u2015\u2212]/g, '-'); out = out.replace(/\bn\.?\b/g, ' nad '); out = out.replace(/\bp\.?\b/g, ' pod '); out = out.replace(/[.,!;:()\[\]{}]/g, ' '); // Remove legal suffixes often appended to Czech organizations out = out.replace(/[\s,]*(z\.?\s*s\.?|o\.?\s*s\.?)\s*$/g, ''); // Remove common organization phrases const orgPhrases = [ 'fotbalovy klub', 'sportovni klub', 'telovychovna jednota', 'skolni sportovni klub', 'spolek', 'fotbal', 'futsal', ]; for (const phrase of orgPhrases) { const re = new RegExp('(^|\\b)'+ phrase + '(\\b|$)', 'g'); out = out.replace(re, ' '); } // Remove common short prefixes/tokens (FK, FC, MFK, TJ, SK, SFC, AFK, BFK, HFK, etc.) out = out.replace(/\b(1\.)?\s*(sfc|afc|fc|fk|mfk|tj|sk|afk|bfk|hfk)\b\.?/g, ' '); out = out.replace(/\s+/g, ' ').trim(); return out; }; interface TeamLogoProps extends Omit { teamId?: string; teamName?: string; facrLogo?: string; size?: 'small' | 'medium' | 'large' | 'custom'; fallbackIcon?: React.ReactElement; } /** * TeamLogo component with automatic logoapi.sportcreative.eu integration * Features: * - Fetches from logoapi first (with local caching) * - Falls back to FACR logo if logoapi doesn't have it * - Properly centers and formats logos * - Handles SVG optimization */ export const TeamLogo: React.FC = ({ teamId, teamName, facrLogo, size = 'medium', fallbackIcon, alt, ...imageProps }) => { const [logoUrl, setLogoUrl] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const { data: publicSettings } = usePublicSettings(); const [observeRef, inView] = useIntersectionObserver({ threshold: 0.01, rootMargin: '150px 0px', freezeOnceVisible: true }); useEffect(() => { if (!inView) return; // defer fetching until visible let mounted = true; const fetchLogo = async () => { try { setLoading(true); setError(false); // Load admin overrides (cached) let overrides: { by_id?: Record; by_name?: Record } = {} as any; try { overrides = await loadTeamOverrides(); } catch {} // Prefer local club logo for own team when IDs match if ( teamId && publicSettings?.club_id && String(teamId) === String(publicSettings.club_id) && publicSettings?.club_logo_url ) { if (mounted) { setLogoUrl(assetUrl(publicSettings.club_logo_url) || publicSettings.club_logo_url); } } else if (teamId && overrides?.by_id?.[teamId]?.logo_url) { const v = overrides.by_id[teamId]!.logo_url as string; if (mounted) { if (typeof v === 'string' && v.startsWith('/')) { setLogoUrl(assetUrl(v) || v); } else { setLogoUrl(v); } } } else { // Try name-based override first if ID override not found let appliedByName = false; try { const byName: Record = (overrides as any)?.by_name || {}; if (teamName && byName && Object.keys(byName).length > 0) { const normMap: Record = {}; for (const k of Object.keys(byName)) { normMap[__normalize(k)] = byName[k]; } const normTeam = __normalize(teamName); let candidate = byName[teamName] || normMap[normTeam]; if (!candidate) { // Suffix/containment match after normalization to handle sponsors/affixes const entries = Object.keys(byName).map((k) => ({ keyNorm: __normalize(k), url: byName[k] })); for (const { keyNorm, url } of entries) { if (!keyNorm) continue; if (normTeam.endsWith(keyNorm) || keyNorm.endsWith(normTeam)) { candidate = url; break; } } } if (!candidate) { const t1 = normTeam.split(' ')[0]; if (t1 && t1.length >= 5) { for (const { keyNorm, url } of Object.keys(byName).map((k) => ({ keyNorm: __normalize(k), url: byName[k] }))) { const k1 = String(keyNorm).split(' ')[0]; if (k1 === t1) { candidate = url; break; } } } } if (candidate) { appliedByName = true; if (mounted) { if (typeof candidate === 'string' && candidate.startsWith('/')) { setLogoUrl(assetUrl(candidate) || candidate); } else { setLogoUrl(candidate); } } } } } catch {} if (!appliedByName) { const url = await getTeamLogo(teamId, teamName, facrLogo); if (mounted) { setLogoUrl(url); } } } } catch (e) { console.error('Failed to fetch logo:', e); if (mounted) { setError(true); // Fallback to FACR or placeholder setLogoUrl(facrLogo || '/logo192.png'); } } finally { if (mounted) { setLoading(false); } } }; fetchLogo(); return () => { mounted = false; }; }, [teamId, teamName, facrLogo, publicSettings?.club_id, publicSettings?.club_logo_url, inView]); // Size mapping const sizeMap = { small: { boxSize: '24px' }, medium: { boxSize: '32px' }, large: { boxSize: '48px' }, custom: {}, }; const sizeProps = size !== 'custom' ? sizeMap[size] : {}; // Class name based on size const className = `match-logo-${size} ${imageProps.className || ''}`.trim(); if (loading) { return (
); } // Check if this is a circular container const isCircular = imageProps.borderRadius === 'full' || imageProps.style?.borderRadius === '50%'; // Get appropriate styling and className using utility functions // Only pass size to utils if it's not 'custom' (utils only accept standard sizes) const utilSize = size !== 'custom' ? size : 'medium'; const logoStyle = getLogoStyle(logoUrl, isCircular, utilSize); const logoClassName = getLogoClassName(logoUrl, isCircular, utilSize); return (
{alt { if (!error) { setError(true); setLogoUrl(facrLogo || '/logo192.png'); } }} />
); }; export default TeamLogo;