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 && (
)}
{/* 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
}
onClick={() => umamiTrackEvent('Gallery Link Click', { album_id: (data as any).gallery_album_id })}
>
Zobrazit celé album
{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 })}
>
))}
)}
{/* 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;