import { Box, Container, Heading, Image, Spinner, Stack, Text, HStack, Badge, Link, SimpleGrid, Button, AspectRatio } from '@chakra-ui/react'; import { useQuery } from '@tanstack/react-query'; import { useParams, Link as RouterLink } from 'react-router-dom'; import { getArticle, getArticleBySlug, getArticleMatchLink, trackArticleView } from '../services/articles'; import MainLayout from '../components/layout/MainLayout'; import DOMPurify from 'dompurify'; import { Helmet } from 'react-helmet-async'; import NewsletterCTA from '../components/common/NewsletterCTA'; import SponsorsSection from '../components/common/SponsorsSection'; import EmbeddedPoll from '../components/polls/EmbeddedPoll'; import { ExternalLink, ArrowRight, Eye, Clock } from 'lucide-react'; import React from 'react'; import { trackEvent as umamiTrackEvent, trackMatchView as umamiTrackMatchView, trackVideoPlay as umamiTrackVideoPlay, trackArticleView as umamiTrackArticleView } from '../utils/umami'; import { assetUrl } from '../utils/url'; const toText = (html?: string) => { if (!html) return ''; const div = document.createElement('div'); div.innerHTML = html; return (div.textContent || div.innerText || '').replace(/\s+/g, ' ').trim(); }; const ArticleDetailPage: React.FC = () => { const { id, slug } = useParams<{ id?: string; slug?: string }>(); const { data, isLoading, isError } = useQuery({ queryKey: ['article', slug ? `slug:${slug}` : `id:${id}`], queryFn: () => (slug ? getArticleBySlug(slug!) : getArticle(id!)), enabled: Boolean(slug || id), }); // Placeholders; moved tracking effects below to avoid using variables before declaration // Track article view when data is loaded React.useEffect(() => { if (data && (data as any).id) { trackArticleView((data as any).id); // Umami + backend generic event umamiTrackArticleView((data as any).id, (data as any).title); } }, [data]); // Fetch linked match (public) const matchLinkQuery = useQuery({ queryKey: ['article-match-link', (data as any)?.id], queryFn: () => getArticleMatchLink((data as any)?.id), enabled: Boolean((data as any)?.id), staleTime: 60_000, }); // Track match view when resolved React.useEffect(() => { const mid = (matchLinkQuery.data as any)?.external_match_id; if (mid) { umamiTrackMatchView(mid); } }, [(matchLinkQuery.data as any)?.external_match_id]); // From cached FACR club info, resolve the match details const facrMatchQuery = useQuery({ queryKey: ['facr-cached-match', (matchLinkQuery.data as any)?.external_match_id], enabled: Boolean((matchLinkQuery.data as any)?.external_match_id), queryFn: async () => { const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; const origin = new URL(apiUrl).origin; const url = `${origin}/cache/prefetch/facr_club_info.json`; const res = await fetch(url, { cache: 'no-cache' }); if (!res.ok) return null as any; const json = await res.json(); const comps = Array.isArray(json?.competitions) ? json.competitions : []; for (const c of comps) { const matches = Array.isArray(c.matches) ? c.matches : []; for (const m of matches) { const mid = String(m.match_id || m.id); if (mid === String((matchLinkQuery.data as any)?.external_match_id)) { return { ...m, competitionName: c.name }; } } } return null as any; }, staleTime: 60_000, }); // Fetch gallery album if article has one const galleryAlbumQuery = useQuery({ queryKey: ['article-gallery-album', (data as any)?.gallery_album_id], enabled: Boolean((data as any)?.gallery_album_id), queryFn: async () => { const albumId = (data as any)?.gallery_album_id; let photoIds: string[] = []; const raw = (data as any)?.gallery_photo_ids; if (Array.isArray(raw)) { photoIds = raw as string[]; } else if (typeof raw === 'string') { // Try JSON parse, otherwise split by comma try { const parsed = JSON.parse(raw); if (Array.isArray(parsed)) { photoIds = parsed.map(String); } else { photoIds = raw.split(',').map(s => s.trim()).filter(Boolean); } } catch { photoIds = raw.split(',').map(s => s.trim()).filter(Boolean); } } const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; const origin = new URL(apiUrl).origin; // Try to find album in both sources const [profileRes, albumsRes] = await Promise.allSettled([ fetch(`${origin}/cache/prefetch/zonerama_profile.json`, { cache: 'no-cache' }), fetch(`${origin}/cache/prefetch/zonerama_albums.json`, { cache: 'no-cache' }) ]); // Check profile albums first if (profileRes.status === 'fulfilled' && profileRes.value.ok) { const profileData = await profileRes.value.json(); const album = (profileData.albums || []).find((a: any) => a.id === albumId); if (album) { // Filter photos by selected IDs if available const photos = photoIds.length > 0 ? album.photos.filter((p: any) => photoIds.includes(p.id)) : album.photos; return { ...album, photos }; } } // Check blog albums if (albumsRes.status === 'fulfilled' && albumsRes.value.ok) { const albumsData = await albumsRes.value.json(); const blogAlbums = Array.isArray(albumsData) ? albumsData : []; const album = blogAlbums.find((a: any) => a.id === albumId); if (album) { const photos = photoIds.length > 0 ? album.photos.filter((p: any) => photoIds.includes(p.id)) : album.photos; return { ...album, photos }; } } return null; }, staleTime: 60_000, }); // Track gallery section when loaded React.useEffect(() => { const albumId = (data as any)?.gallery_album_id; if (albumId && galleryAlbumQuery.data) { umamiTrackEvent('Gallery Section Shown', { album_id: albumId, photos: galleryAlbumQuery.data?.photos?.length || 0 }); } }, [(data as any)?.gallery_album_id, galleryAlbumQuery.data]); if (isLoading) return ; if (isError || !data) return Článek nenalezen; const title = (data as any).seo_title || data.title; const description = (data as any).seo_description || toText(data.content).slice(0, 160); const ogImageRaw = (data as any).og_image_url || data.image_url || '/logo512.png'; const ogImage = assetUrl(ogImageRaw) || ogImageRaw; const canonical = typeof window !== 'undefined' ? window.location.href : undefined; // Transform content to ensure /uploads URLs resolve against API origin and allow iframes // Memoize the transformation function to prevent infinite loops const toAbsoluteUploads = React.useCallback((html?: string) => { if (!html) return ''; try { const base = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; const origin = new URL(base).origin; // Replace src="/uploads... and href="/uploads... return html .replace(/src=("|')\s*(\/uploads\/[^"']+)("|')/g, (_m, q1, p2, q3) => `src=${q1}${origin}${p2}${q3}`) .replace(/href=("|')\s*(\/uploads\/[^"']+)("|')/g, (_m, q1, p2, q3) => `href=${q1}${origin}${p2}${q3}`); } catch { return html; } }, []); const safeContentHTML = React.useMemo(() => { const transformed = toAbsoluteUploads(data.content); return DOMPurify.sanitize(transformed || '', { USE_PROFILES: { html: true }, ADD_TAGS: ['iframe'], ADD_ATTR: ['target', 'rel', 'allow', 'allowfullscreen'], }); }, [data.content, toAbsoluteUploads]); return ( {title} {canonical && } {data.title} {((data as any).read_time || (data as any).estimated_read_minutes) && ( {(data as any).read_time || (data as any).estimated_read_minutes} min čtení )} {(data as any).view_count !== undefined && (data as any).view_count > 0 && ( {(data as any).view_count} zobrazení )} {/* Featured Image - Top */} {data.image_url && ( {data.title} )} {/* YouTube Video Section - If attached to article */} {(data as any)?.youtube_video_id && ( 🎬 Video k článku umamiTrackEvent('Video Widget Shown', { id: (data as any).youtube_video_id, title: (data as any).youtube_video_title })} onClick={() => umamiTrackVideoPlay((data as any).youtube_video_id, (data as any).youtube_video_title)} /> { (data as any).youtube_video_title ? ( {(data as any).youtube_video_title} ) : null } )} {/* Match Section - After Image */} {(matchLinkQuery.data as any)?.external_match_id && ( Zápas k článku {facrMatchQuery.isLoading ? ( Načítám údaje o zápasu… ) : facrMatchQuery.data ? ( <> {facrMatchQuery.data.competitionName && ( {String(facrMatchQuery.data.competitionName)} )} {String(facrMatchQuery.data.date_time || facrMatchQuery.data.date || '')} {String(facrMatchQuery.data.home || facrMatchQuery.data.home_team || '')} {' '} {String(facrMatchQuery.data.score || (facrMatchQuery.data.result_home!=null && facrMatchQuery.data.result_away!=null ? `${facrMatchQuery.data.result_home}:${facrMatchQuery.data.result_away}` : 'vs'))} {' '} {String(facrMatchQuery.data.away || facrMatchQuery.data.away_team || '')} {facrMatchQuery.data.venue && ( {String(facrMatchQuery.data.venue)} )} {facrMatchQuery.data.report_url && ( Protokol zápasu (fotbal.cz) )} ) : ( Propojeno s FACR ID: {(matchLinkQuery.data as any)?.external_match_id} )} )} {/* Article Content - Main Section */} {/* Gallery Section - At the End */} {(data as any)?.gallery_album_id && ( Fotogalerie k článku {galleryAlbumQuery.isLoading ? ( Načítám fotografie… ) : galleryAlbumQuery.data ? ( <> {galleryAlbumQuery.data.title} {galleryAlbumQuery.data.date && ( {galleryAlbumQuery.data.date} )} {galleryAlbumQuery.data.photos?.length || 0} foto {/* Photo Grid */} {galleryAlbumQuery.data.photos && galleryAlbumQuery.data.photos.length > 0 && ( {galleryAlbumQuery.data.photos.slice(0, 12).map((photo: any) => ( umamiTrackEvent('Gallery Photo Click', { album_id: (data as any).gallery_album_id, photo_id: photo.id })} > {`Fotka ))} )} {/* Zonerama Attribution */} 📸 Fotografie z Zonerama ) : ( Album s ID: {(data as any).gallery_album_id} )} )} {/* Embedded Poll - shows polls related to this article */} {data?.id && } {/* Newsletter CTA */} {/* Sponsors Section */} ); }; export default ArticleDetailPage;