This commit is contained in:
Tomas Dvorak
2025-10-28 22:38:27 +01:00
parent 3d621e2187
commit 823fabee02
106 changed files with 9011 additions and 3930 deletions
+258 -156
View File
@@ -1,4 +1,4 @@
import { Box, Container, Heading, Image, Spinner, Stack, Text, HStack, Badge, Link, SimpleGrid, Button, AspectRatio } from '@chakra-ui/react';
import { Box, Container, Heading, Image, Spinner, Stack, Text, HStack, Badge, Link, SimpleGrid, Button, AspectRatio, useColorModeValue, Flex, VStack, Tag } 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';
@@ -13,6 +13,11 @@ 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';
import { API_URL } from '../services/api';
import TeamLogo from '../components/common/TeamLogo';
import { extractPalette } from '../utils/colors';
import { getTeamLogo } from '../utils/sportLogosAPI';
import FilePreview from '../components/common/FilePreview';
import { usePublicSettings } from '../hooks/usePublicSettings';
const toText = (html?: string) => {
if (!html) return '';
@@ -29,6 +34,22 @@ const ArticleDetailPage: React.FC = () => {
enabled: Boolean(slug || id),
});
// UI colors and public settings
const { data: publicSettings } = usePublicSettings();
const cardBg = useColorModeValue('white','gray.900');
const videoBg = useColorModeValue('gray.50','gray.800');
const textMuted = useColorModeValue('gray.600','gray.400');
// Hoist all color mode values to top-level to avoid conditional hook calls
const videoTitleColor = useColorModeValue('gray.700','gray.300');
const galleryBg = useColorModeValue('blue.50','blue.900');
const galleryBorder = useColorModeValue('blue.200','blue.700');
const attachmentsBg = useColorModeValue('gray.50','gray.800');
// Derive opponent color (for right edge fade) from team logo
const [opponentColor, setOpponentColor] = React.useState<string | null>(null);
// Placeholders; moved tracking effects below to avoid using variables before declaration
// Track article view when data is loaded
@@ -40,6 +61,27 @@ const ArticleDetailPage: React.FC = () => {
}
}, [data]);
// Delegated click tracking for normal links inside content
const contentRef = React.useRef<HTMLDivElement | null>(null);
React.useEffect(() => {
const el = contentRef.current;
if (!el) return;
const handler = (e: MouseEvent) => {
let target = e.target as HTMLElement | null;
if (!target) return;
// Find nearest anchor
const anchor = (target.closest ? target.closest('a') : null) as HTMLAnchorElement | null;
if (anchor && anchor.href) {
try {
const href = anchor.getAttribute('href') || anchor.href;
umamiTrackEvent('Link Click', { href, page: window.location.pathname, context: 'article_content' });
} catch {}
}
};
el.addEventListener('click', handler);
return () => { el.removeEventListener('click', handler); };
}, [contentRef.current]);
// Fetch linked match (public)
const matchLinkQuery = useQuery({
queryKey: ['article-match-link', (data as any)?.id],
@@ -81,10 +123,10 @@ const ArticleDetailPage: React.FC = () => {
staleTime: 60_000,
});
// Fetch gallery album if article has one
// Fetch gallery album if article has one (fallback to URL when ID is missing)
const galleryAlbumQuery = useQuery({
queryKey: ['article-gallery-album', (data as any)?.gallery_album_id],
enabled: Boolean((data as any)?.gallery_album_id),
queryKey: ['article-gallery-album', (data as any)?.gallery_album_id || (data as any)?.gallery_album_url],
enabled: Boolean((data as any)?.gallery_album_id || (data as any)?.gallery_album_url),
queryFn: async () => {
const albumId = (data as any)?.gallery_album_id;
let photoIds: string[] = [];
@@ -139,6 +181,24 @@ const ArticleDetailPage: React.FC = () => {
}
}
// Fallback: load by URL via proxy endpoint
const albumUrl = (data as any)?.gallery_album_url;
if (albumUrl) {
const params = new URLSearchParams({ link: albumUrl, photo_limit: '12', rendered: 'true' });
const resp = await fetch(`${API_URL}/zonerama-album?${params.toString()}`);
if (resp.ok) {
const payload = await resp.json();
let photos: any[] = [];
if (Array.isArray(payload?.albums) && payload.albums.length > 0) {
photos = payload.albums[0]?.photos || [];
} else if (Array.isArray(payload?.photos)) {
photos = payload.photos;
}
if (photoIds.length > 0) photos = photos.filter((p: any) => photoIds.includes(p.id));
return { id: albumUrl, title: 'Album', date: '', photos } as any;
}
}
return null;
},
staleTime: 60_000,
@@ -152,15 +212,6 @@ const ArticleDetailPage: React.FC = () => {
}
}, [(data as any)?.gallery_album_id, galleryAlbumQuery.data]);
if (isLoading) return <Spinner />;
if (isError || !data) return <Text color="red.500">Článek nenalezen</Text>;
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) => {
@@ -177,13 +228,27 @@ const ArticleDetailPage: React.FC = () => {
}, []);
const safeContentHTML = React.useMemo(() => {
const transformed = toAbsoluteUploads(data.content);
const transformed = toAbsoluteUploads((data as any)?.content);
return DOMPurify.sanitize(transformed || '', {
USE_PROFILES: { html: true },
ADD_TAGS: ['iframe'],
ADD_ATTR: ['target', 'rel', 'allow', 'allowfullscreen'],
});
}, [data.content, toAbsoluteUploads]);
}, [(data as any)?.content, toAbsoluteUploads]);
if (isLoading) return <Spinner />;
if (isError || !data) return <Text color="red.500">Článek nenalezen</Text>;
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;
const publishedAt = (data as any).published_at || (data as any).created_at;
const monthParam = (() => {
if (!publishedAt) return '';
try { const d = new Date(publishedAt); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}`; } catch { return ''; }
})();
return (
<MainLayout>
@@ -243,201 +308,238 @@ const ArticleDetailPage: React.FC = () => {
})}
</script>
</Helmet>
<Box bg="transparent" color="inherit" py={{ base: 6, md: 8 }} mb={4}>
<Box bg="transparent" color="inherit" py={{ base: 6, md: 8 }} mb={2}>
<Container maxW="7xl">
<Heading as="h1" size={{ base: 'xl', md: '2xl' }} mb={3}>{data.title}</Heading>
<HStack spacing={4} fontSize="sm" color="gray.600">
{((data as any).read_time || (data as any).estimated_read_minutes) && (
<Heading as="h1" size={{ base: 'xl', md: '2xl' }} mb={2}>{data.title}</Heading>
<HStack spacing={2} rowGap={2} wrap="wrap" fontSize="sm" color={textMuted}>
{((data as any).read_time || (data as any).estimated_read_minutes) ? (
<HStack spacing={1}>
<Clock size={16} />
<Text>{(data as any).read_time || (data as any).estimated_read_minutes} min čtení</Text>
</HStack>
) : null}
{publishedAt && (
<Tag as={RouterLink} to={`/news?month=${monthParam}`} size="sm" variant="subtle">{new Date(publishedAt).toLocaleDateString('cs-CZ')}</Tag>
)}
{(data as any).view_count !== undefined && (data as any).view_count > 0 && (
<HStack spacing={1}>
{(data as any)?.category?.id && (
<Tag as={RouterLink} to={`/news?category_id=${(data as any).category.id}`} size="sm" variant="subtle">{(data as any).category.name || 'Kategorie'}</Tag>
)}
{(matchLinkQuery.data as any)?.external_match_id && (
<Tag as={RouterLink} to={`/news?match_id=${(matchLinkQuery.data as any).external_match_id}`} size="sm" variant="subtle">Zápas</Tag>
)}
{(data as any).view_count ? (
<HStack spacing={1} ml={{ base: 0, md: 2 }}>
<Eye size={16} />
<Text>{(data as any).view_count} zobrazení</Text>
</HStack>
)}
) : null}
</HStack>
</Container>
</Box>
<Container maxW="7xl">
<Stack spacing={6}>
{/* Featured Image - Top */}
{/* Featured Image - smaller with subtle overlay */}
{data.image_url && (
<Image src={assetUrl(data.image_url) || data.image_url} alt={data.title} borderRadius="lg" />
<Box position="relative" borderRadius="xl" overflow="hidden">
<Image src={assetUrl(data.image_url) || data.image_url} alt={data.title} w="100%" h={{ base: '220px', md: '360px' }} objectFit="cover" />
<Box position="absolute" inset={0} bg="brand.primary" opacity={0.08} pointerEvents="none" />
<Box position="absolute" inset={0} bgGradient="linear(to-b, rgba(0,0,0,0.12), rgba(0,0,0,0.02))" pointerEvents="none" />
</Box>
)}
{/* YouTube Video Section - If attached to article */}
{/* YouTube Video Section - smaller and rounded */}
{(data as any)?.youtube_video_id && (
<Box borderWidth="1px" borderRadius="lg" p={{ base: 3, md: 4 }} bg="gray.50">
<Box borderWidth="1px" borderRadius="lg" p={{ base: 3, md: 4 }} bg={videoBg}>
<Heading as="h3" size="md" mb={2}>🎬 Video k článku</Heading>
<AspectRatio ratio={16 / 9}>
<Box
as="iframe"
src={`https://www.youtube-nocookie.com/embed/${(data as any).youtube_video_id}`}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
title={(data as any).youtube_video_title || 'YouTube video'}
onLoad={() => 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)}
/>
</AspectRatio>
{ (data as any).youtube_video_title ? (
<Text mt={2} color="gray.700">{(data as any).youtube_video_title}</Text>
) : null }
<Box maxW="3xl" mx="auto" borderRadius="lg" overflow="hidden">
<AspectRatio ratio={16 / 9}>
<Box
as="iframe"
src={`https://www.youtube-nocookie.com/embed/${(data as any).youtube_video_id}`}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
title={(data as any).youtube_video_title || 'YouTube video'}
onLoad={() => 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)}
/>
</AspectRatio>
</Box>
{(data as any).youtube_video_title ? (
<Text mt={2} color={videoTitleColor}>{(data as any).youtube_video_title}</Text>
) : null}
</Box>
)}
{/* Match Section - After Image */}
{/* Match Section - Card with logos, score/countdown, venue/date */}
{(matchLinkQuery.data as any)?.external_match_id && (
<Box borderWidth="1px" borderRadius="lg" p={{ base: 3, md: 4 }} bg="gray.50">
<HStack justify="space-between" align="start">
<Box>
<Heading as="h3" size="md" mb={1}>Zápas k článku</Heading>
{facrMatchQuery.isLoading ? (
<Text color="gray.600">Načítám údaje o zápasu</Text>
) : facrMatchQuery.data ? (
<>
<HStack spacing={2} wrap="wrap">
{facrMatchQuery.data.competitionName && (
<Badge colorScheme="blue">{String(facrMatchQuery.data.competitionName)}</Badge>
)}
<Badge>{String(facrMatchQuery.data.date_time || facrMatchQuery.data.date || '')}</Badge>
</HStack>
<Text mt={2} fontWeight="600">
{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 || '')}
</Text>
{facrMatchQuery.data.venue && (
<Text color="gray.600">{String(facrMatchQuery.data.venue)}</Text>
)}
{facrMatchQuery.data.report_url && (
<Box mt={2}>
<Link href={String(facrMatchQuery.data.report_url)} isExternal color="blue.600">Protokol zápasu (fotbal.cz)</Link>
</Box>
)}
</>
) : (
<Text color="gray.600">Propojeno s FACR ID: {(matchLinkQuery.data as any)?.external_match_id}</Text>
<Box position="relative" borderWidth="1px" borderRadius="lg" p={{ base: 4, md: 5 }} bg={cardBg} overflow="hidden">
{/* Edge fades */}
<Box position="absolute" top={0} left={0} bottom={0} w={{ base: '6px', md: '12px' }} bgGradient={`linear(to-r, var(--club-primary, #0b5cff), transparent)`} pointerEvents="none" />
{opponentColor && (
<Box position="absolute" top={0} right={0} bottom={0} w={{ base: '6px', md: '12px' }} bgGradient={`linear(to-l, ${opponentColor}, transparent)`} pointerEvents="none" />
)}
<Heading as="h3" size="md" mb={3}>Zápas k článku</Heading>
{facrMatchQuery.isLoading ? (
<Text color={textMuted}>Načítám údaje o zápasu</Text>
) : facrMatchQuery.data ? (
<>
<HStack spacing={2} wrap="wrap" mb={3}>
{facrMatchQuery.data.competitionName && (
<Badge colorScheme="blue">{String(facrMatchQuery.data.competitionName)}</Badge>
)}
<Badge>{String((facrMatchQuery.data as any).date_time || (facrMatchQuery.data as any).date || '')}</Badge>
</HStack>
<Flex align="center" justify="space-between" gap={4}>
<VStack flex={1} spacing={2} minW="0">
<TeamLogo size="custom" style={{ width: 64, height: 64 }} teamId={String((facrMatchQuery.data as any).home_team_id || (facrMatchQuery.data as any).home_id || '')} teamName={String((facrMatchQuery.data as any).home || (facrMatchQuery.data as any).home_team || '')} />
<Text fontWeight="600" noOfLines={2} textAlign="center">{String((facrMatchQuery.data as any).home || (facrMatchQuery.data as any).home_team || '')}</Text>
</VStack>
<VStack minW={{ base: '100px', md: '140px' }}>
{(() => {
const dRaw = String((facrMatchQuery.data as any).date_time || (facrMatchQuery.data as any).date || '');
const d = new Date(dRaw);
const hasScore = ((facrMatchQuery.data as any).result_home != null && (facrMatchQuery.data as any).result_away != null) || Boolean((facrMatchQuery.data as any).score && (facrMatchQuery.data as any).score !== 'vs');
if (hasScore) {
const score = String((facrMatchQuery.data as any).score || `${(facrMatchQuery.data as any).result_home}:${(facrMatchQuery.data as any).result_away}`);
return (<Heading size="2xl">{score}</Heading>);
}
const now = Date.now();
const ms = d.getTime() - now;
const days = Math.max(0, Math.floor(ms / (1000*60*60*24)));
const hours = Math.max(0, Math.floor((ms % (1000*60*60*24))/(1000*60*60)));
const mins = Math.max(0, Math.floor((ms % (1000*60*60))/(1000*60)));
return (<Text fontSize="lg" fontWeight="700">Za {days} d {hours} h {mins} min</Text>);
})()}
{(facrMatchQuery.data as any).venue && <Text fontSize="sm" color={textMuted}>{String((facrMatchQuery.data as any).venue)}</Text>}
{(() => {
const dRaw = String((facrMatchQuery.data as any).date_time || (facrMatchQuery.data as any).date || '');
const d = new Date(dRaw);
return <Text fontSize="sm" color={textMuted}>{d.toLocaleDateString('cs-CZ')} {d.toLocaleTimeString('cs-CZ', { hour: '2-digit', minute: '2-digit' })}</Text>;
})()}
</VStack>
<VStack flex={1} spacing={2} minW="0">
<TeamLogo size="custom" style={{ width: 64, height: 64 }} teamId={String((facrMatchQuery.data as any).away_team_id || (facrMatchQuery.data as any).away_id || '')} teamName={String((facrMatchQuery.data as any).away || (facrMatchQuery.data as any).away_team || '')} />
<Text fontWeight="600" noOfLines={2} textAlign="center">{String((facrMatchQuery.data as any).away || (facrMatchQuery.data as any).away_team || '')}</Text>
</VStack>
</Flex>
{(facrMatchQuery.data as any).report_url && (
<Box mt={3}>
<Link href={String((facrMatchQuery.data as any).report_url)} isExternal color="blue.600">Protokol zápasu (fotbal.cz)</Link>
</Box>
)}
</Box>
</HStack>
</>
) : (
<Text color={textMuted}>Propojeno s FACR ID: {(matchLinkQuery.data as any)?.external_match_id}</Text>
)}
</Box>
)}
{/* Article Content - Main Section */}
{/* Article Content - Main Section with editor-like lists */}
<Box
className="article-content"
bg="white"
bg={useColorModeValue('white','gray.900')}
borderRadius="lg"
p={{ base: 4, md: 6 }}
ref={contentRef}
sx={{ 'ul, ol': { pl: 6, listStylePosition: 'outside' }, 'ul': { listStyleType: 'disc' }, 'ol': { listStyleType: 'decimal' }, 'li': { mb: 2 } }}
dangerouslySetInnerHTML={{ __html: safeContentHTML }}
/>
{/* Gallery Section - At the End */}
{(data as any)?.gallery_album_id && (
<Box borderWidth="1px" borderRadius="lg" p={{ base: 3, md: 4 }} bg="blue.50" borderColor="blue.200">
{/* Gallery Section - Mosaic of 5 images with grayscale + hover color */}
{((data as any)?.gallery_album_id || (data as any)?.gallery_album_url) && (
<Box borderWidth="1px" borderRadius="lg" p={{ base: 3, md: 4 }} bg={galleryBg} borderColor={galleryBorder}>
<Box mb={3}>
<HStack justify="space-between" align="center" mb={2}>
<Heading as="h3" size="md">Fotogalerie k článku</Heading>
<Button
as={RouterLink}
to={`/galerie/album/${(data as any).gallery_album_id}`}
to={(data as any).gallery_album_id ? `/galerie/album/${(data as any).gallery_album_id}` : '#'}
size="sm"
colorScheme="blue"
variant="outline"
rightIcon={<ArrowRight size={16} />}
onClick={() => umamiTrackEvent('Gallery Link Click', { album_id: (data as any).gallery_album_id })}
>
Zobrazit celé album
Zobrazit galerii
</Button>
</HStack>
{galleryAlbumQuery.isLoading ? (
<Text color="gray.600">Načítám fotografie</Text>
) : galleryAlbumQuery.data ? (
<>
<HStack spacing={2} mb={3}>
<Badge colorScheme="purple">
{galleryAlbumQuery.data.title}
</Badge>
{galleryAlbumQuery.data.date && (
<Badge>{galleryAlbumQuery.data.date}</Badge>
)}
<Badge colorScheme="blue">
{galleryAlbumQuery.data.photos?.length || 0} foto
</Badge>
</HStack>
{/* Photo Grid */}
{galleryAlbumQuery.data.photos && galleryAlbumQuery.data.photos.length > 0 && (
<SimpleGrid columns={{ base: 2, sm: 3, md: 4, lg: 6 }} spacing={2}>
{galleryAlbumQuery.data.photos.slice(0, 12).map((photo: any) => (
<Box
key={photo.id}
as={RouterLink}
to={`/galerie/album/${(data as any).gallery_album_id}`}
position="relative"
borderRadius="md"
overflow="hidden"
cursor="pointer"
transition="all 0.2s"
_hover={{ transform: 'scale(1.05)', boxShadow: 'lg' }}
onClick={() => umamiTrackEvent('Gallery Photo Click', { album_id: (data as any).gallery_album_id, photo_id: photo.id })}
>
<Image
src={photo.image_1500}
alt={`Fotka ${photo.id}`}
w="100%"
h="120px"
objectFit="cover"
/>
</Box>
{/* Custom 5-image mosaic */}
{galleryAlbumQuery.data.photos && galleryAlbumQuery.data.photos.length > 0 && (() => {
const photos = galleryAlbumQuery.data.photos.slice(0, 5);
if (photos.length < 5) {
return (
<SimpleGrid columns={{ base: 2, sm: 3 }} spacing={2}>
{photos.map((p: any) => (
<Image key={p.id} src={p.image_1500} alt={String(p.id)} w="100%" h="140px" objectFit="cover" filter="grayscale(100%)" _hover={{ filter: 'grayscale(0%)' }} />
))}
</SimpleGrid>
)}
{/* Zonerama Attribution */}
<HStack mt={3} spacing={1} fontSize="xs" color="blue.700">
<Text>📸 Fotografie z</Text>
<Link
href={(data as any).gallery_album_url || `https://zonerama.com`}
isExternal
fontWeight="600"
color="blue.600"
display="inline-flex"
alignItems="center"
gap={1}
>
Zonerama
<ExternalLink size={12} />
</Link>
</HStack>
</>
) : (
<Text color="gray.600">Album s ID: {(data as any).gallery_album_id}</Text>
)}
);
}
return (
<Box position="relative" sx={{
display: 'grid',
gridTemplateColumns: '1fr 1.2fr 1fr',
gridTemplateRows: 'repeat(2, 140px)',
gap: '8px'
}}>
<Image src={photos[0].image_1500} alt={String(photos[0].id)} sx={{ gridColumn: 1, gridRow: 1 }} objectFit="cover" w="100%" h="100%" filter="grayscale(100%)" _hover={{ filter: 'grayscale(0%)' }} borderRadius="md" />
<Image src={photos[1].image_1500} alt={String(photos[1].id)} sx={{ gridColumn: 1, gridRow: 2 }} objectFit="cover" w="100%" h="100%" filter="grayscale(100%)" _hover={{ filter: 'grayscale(0%)' }} borderRadius="md" />
<Image src={photos[2].image_1500} alt={String(photos[2].id)} sx={{ gridColumn: 2, gridRow: '1 / span 2' }} objectFit="cover" w="100%" h="100%" filter="grayscale(100%)" _hover={{ filter: 'grayscale(0%)' }} borderRadius="md" />
<Image src={photos[3].image_1500} alt={String(photos[3].id)} sx={{ gridColumn: 3, gridRow: 1 }} objectFit="cover" w="100%" h="100%" filter="grayscale(100%)" _hover={{ filter: 'grayscale(0%)' }} borderRadius="md" />
<Image src={photos[4].image_1500} alt={String(photos[4].id)} sx={{ gridColumn: 3, gridRow: 2 }} objectFit="cover" w="100%" h="100%" filter="grayscale(100%)" _hover={{ filter: 'grayscale(0%)' }} borderRadius="md" />
<Button as={RouterLink} to={(data as any).gallery_album_id ? `/galerie/album/${(data as any).gallery_album_id}` : '#'} size="sm" colorScheme="blue" position="absolute" top="50%" left="50%" transform="translate(-50%, -50%)" onClick={() => umamiTrackEvent('Gallery Link Click', { album_id: (data as any).gallery_album_id })}>Zobrazit galerii</Button>
</Box>
);
})()}
{/* Zonerama Attribution */}
<HStack mt={3} spacing={1} fontSize="xs" color="blue.700">
<Text>📸 Fotografie z</Text>
<Link
href={(data as any).gallery_album_url || `https://zonerama.com`}
isExternal
fontWeight="600"
color="blue.600"
display="inline-flex"
alignItems="center"
gap={1}
>
Zonerama
<ExternalLink size={12} />
</Link>
</HStack>
</Box>
</Box>
)}
{/* Embedded Poll - directly under content/gallery */}
{data?.id && <EmbeddedPoll articleId={(data as any).id} maxPolls={3} />}
</Stack>
</Container>
</Box>
{/* Embedded Poll - shows polls related to this article */}
{data?.id && <EmbeddedPoll articleId={data.id} />}
{/* Newsletter CTA */}
<NewsletterCTA />
{/* Sponsors Section */}
<SponsorsSection />
</MainLayout>
);
{/* Attachments - bottom above CTA */}
{Array.isArray((data as any)?.attachments) && (data as any).attachments.length > 0 && (
<Container maxW="7xl" mt={4}>
<Box borderWidth="1px" borderRadius="lg" p={{ base: 3, md: 4 }} bg={attachmentsBg}>
<Heading as="h3" size="md" mb={2}>Přílohy</Heading>
<Stack spacing={2}>
{(data as any).attachments.map((f: any, idx: number) => (
<HStack key={idx} justify="space-between">
<Text noOfLines={1}>{f.name || f.url}</Text>
<FilePreview url={assetUrl(f.url) || f.url} name={f.name || ''} mimeType={f.mime_type || ''} size={f.size} />
</HStack>
))}
</Stack>
</Box>
</Container>
)}
{/* Newsletter CTA */}
<NewsletterCTA />
{/* Sponsors Section */}
<SponsorsSection />
</MainLayout>
);
};
export default ArticleDetailPage;