mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-05 03:02:56 +00:00
dev day #77
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
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';
|
||||
import { getArticle, getArticleBySlug, getArticleMatchLink, trackArticleView, getArticles } from '../services/articles';
|
||||
import MainLayout from '../components/layout/MainLayout';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
@@ -18,6 +18,11 @@ import { extractPalette } from '../utils/colors';
|
||||
import { getTeamLogo } from '../utils/sportLogosAPI';
|
||||
import FilePreview from '../components/common/FilePreview';
|
||||
import { usePublicSettings } from '../hooks/usePublicSettings';
|
||||
import InstagramGeneratorButton from '../components/admin/InstagramGeneratorButton';
|
||||
import { MatchSnapshot } from '../services/instagram';
|
||||
import { Widget } from '../components/widgets/Widget';
|
||||
import { MatchesWidget } from '../components/widgets/MatchesWidget';
|
||||
import { getUpcomingEvents } from '../services/eventService';
|
||||
|
||||
const toText = (html?: string) => {
|
||||
if (!html) return '';
|
||||
@@ -123,6 +128,38 @@ const ArticleDetailPage: React.FC = () => {
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
// Build a snapshot usable for sharing if available (FACR data or article fallback)
|
||||
const matchSnapshot: MatchSnapshot | null = React.useMemo(() => {
|
||||
const m: any = facrMatchQuery?.data as any;
|
||||
if (m) {
|
||||
let score = '';
|
||||
if (m?.score && m.score !== 'vs') score = String(m.score);
|
||||
else if (m?.result_home != null && m?.result_away != null) score = `${m.result_home}:${m.result_away}`;
|
||||
return {
|
||||
external_match_id: String((matchLinkQuery.data as any)?.external_match_id || ''),
|
||||
competition: String(m.competitionName || ''),
|
||||
date_time: String(m.date_time || m.date || ''),
|
||||
venue: m.venue ? String(m.venue) : undefined,
|
||||
home: String(m.home || m.home_team || ''),
|
||||
away: String(m.away || m.away_team || ''),
|
||||
score,
|
||||
};
|
||||
}
|
||||
const snap: any = (data as any)?.match_snapshot;
|
||||
if (snap) {
|
||||
return {
|
||||
external_match_id: snap.external_match_id,
|
||||
competition: snap.competition || snap.competitionName,
|
||||
date_time: snap.date_time || snap.date,
|
||||
venue: snap.venue,
|
||||
home: snap.home,
|
||||
away: snap.away,
|
||||
score: snap.score,
|
||||
} as MatchSnapshot;
|
||||
}
|
||||
return null;
|
||||
}, [facrMatchQuery?.data, (matchLinkQuery.data as any)?.external_match_id, (data as any)?.match_snapshot]);
|
||||
|
||||
// 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 || (data as any)?.gallery_album_url],
|
||||
@@ -239,6 +276,24 @@ const ArticleDetailPage: React.FC = () => {
|
||||
});
|
||||
}, [(data as any)?.content, toAbsoluteUploads]);
|
||||
|
||||
const relatedArticlesQuery = useQuery({
|
||||
queryKey: ['related-articles', (data as any)?.category?.id || 'none', (data as any)?.id],
|
||||
enabled: Boolean((data as any)?.id),
|
||||
queryFn: () => getArticles({
|
||||
page: 1,
|
||||
page_size: 6,
|
||||
published: true,
|
||||
...(((data as any)?.category?.id) ? { category_id: (data as any).category.id } : {}),
|
||||
}),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
const upcomingEventsQuery = useQuery({
|
||||
queryKey: ['upcoming-events-sidebar'],
|
||||
queryFn: getUpcomingEvents,
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
if (isLoading) return <Spinner />;
|
||||
if (isError || !data) return <Text color="red.500">Článek nenalezen</Text>;
|
||||
|
||||
@@ -256,6 +311,13 @@ const ArticleDetailPage: React.FC = () => {
|
||||
return (
|
||||
<MainLayout>
|
||||
<Box>
|
||||
<InstagramGeneratorButton
|
||||
article={data as any}
|
||||
match={matchSnapshot}
|
||||
targetUrl={typeof window !== 'undefined' ? window.location.href : undefined}
|
||||
placement="fixed"
|
||||
size="md"
|
||||
/>
|
||||
<Helmet>
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
@@ -340,183 +402,243 @@ const ArticleDetailPage: React.FC = () => {
|
||||
</Container>
|
||||
</Box>
|
||||
<Container maxW="7xl">
|
||||
<Stack spacing={6}>
|
||||
{/* Featured Image - smaller with subtle overlay */}
|
||||
{data.image_url && (
|
||||
<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 - smaller and rounded */}
|
||||
{(data as any)?.youtube_video_id && (
|
||||
<Box borderWidth="1px" borderRadius="lg" p={{ base: 3, md: 4 }} bg={videoBg}>
|
||||
<Heading as="h3" size="md" mb={2}>🎬 Video k článku</Heading>
|
||||
<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 - Card with logos, score/countdown, venue/date */}
|
||||
{(matchLinkQuery.data as any)?.external_match_id && (
|
||||
<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" />
|
||||
<SimpleGrid columns={{ base: 1, lg: 12 }} spacing={6}>
|
||||
<Box gridColumn={{ base: '1 / -1', lg: 'span 8' }}>
|
||||
<Stack spacing={6}>
|
||||
{/* Featured Image - smaller with subtle overlay */}
|
||||
{data.image_url && (
|
||||
<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>
|
||||
)}
|
||||
<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>
|
||||
{/* YouTube Video Section - smaller and rounded */}
|
||||
{(data as any)?.youtube_video_id && (
|
||||
<Box borderWidth="1px" borderRadius="lg" p={{ base: 3, md: 4 }} bg={videoBg}>
|
||||
<Heading as="h3" size="md" mb={2}>🎬 Video k článku</Heading>
|
||||
<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 - Card with logos, score/countdown, venue/date */}
|
||||
{(matchLinkQuery.data as any)?.external_match_id && (
|
||||
<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" />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Text color={textMuted}>Propojeno s FACR ID: {(matchLinkQuery.data as any)?.external_match_id}</Text>
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Text color={textMuted}>Propojeno s FACR ID: {(matchLinkQuery.data as any)?.external_match_id}</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Article Content - Main Section with editor-like lists */}
|
||||
<Box
|
||||
className="article-content"
|
||||
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 }}
|
||||
/>
|
||||
{/* Article Content - Main Section with editor-like lists */}
|
||||
<Box
|
||||
className="article-content"
|
||||
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 - 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={(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 galerii
|
||||
</Button>
|
||||
</HStack>
|
||||
{/* Custom 5-image mosaic */}
|
||||
{Array.isArray(galleryAlbumQuery.data?.photos) && (galleryAlbumQuery.data?.photos?.length || 0) > 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>
|
||||
);
|
||||
}
|
||||
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>
|
||||
)}
|
||||
{/* 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={(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 galerii
|
||||
</Button>
|
||||
</HStack>
|
||||
{/* Custom 5-image mosaic */}
|
||||
{Array.isArray(galleryAlbumQuery.data?.photos) && (galleryAlbumQuery.data?.photos?.length || 0) > 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>
|
||||
);
|
||||
}
|
||||
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>
|
||||
{/* Embedded Poll - directly under content/gallery */}
|
||||
{data?.id && <EmbeddedPoll articleId={(data as any).id} maxPolls={3} />}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<VStack align="stretch" spacing={6} gridColumn={{ base: '1 / -1', lg: 'span 4' }}>
|
||||
<Widget title="Podobné články">
|
||||
{relatedArticlesQuery.isLoading ? (
|
||||
<Text color={textMuted}>Načítám…</Text>
|
||||
) : (() => {
|
||||
const list = ((relatedArticlesQuery.data as any)?.data || [])
|
||||
.filter((a: any) => a?.id !== (data as any)?.id)
|
||||
.slice(0, 4);
|
||||
if (!list.length) return <Text color={textMuted}>Žádné související články</Text>;
|
||||
return (
|
||||
<VStack spacing={3} align="stretch">
|
||||
{list.map((a: any) => {
|
||||
const link = a.slug ? `/news/${a.slug}` : `/articles/${a.id}`;
|
||||
return (
|
||||
<HStack key={a.id} align="flex-start" spacing={3} as={RouterLink} to={link} _hover={{ textDecoration: 'none' }}>
|
||||
<Image src={assetUrl(a.image_url) || '/stadium-placeholder.jpg'} alt={a.title} boxSize="64px" objectFit="cover" borderRadius="md" />
|
||||
<VStack align="start" spacing={1} flex={1} minW={0}>
|
||||
<Text fontWeight="600" noOfLines={2}>{a.title}</Text>
|
||||
{a.published_at && (
|
||||
<Text fontSize="sm" color={textMuted}>{new Date(a.published_at).toLocaleDateString('cs-CZ')}</Text>
|
||||
)}
|
||||
</VStack>
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</VStack>
|
||||
);
|
||||
})()}
|
||||
</Widget>
|
||||
|
||||
<MatchesWidget />
|
||||
|
||||
<Widget title="Nejbližší aktivity">
|
||||
{upcomingEventsQuery.isLoading ? (
|
||||
<Text color={textMuted}>Načítám…</Text>
|
||||
) : (() => {
|
||||
const items = Array.isArray(upcomingEventsQuery.data) ? (upcomingEventsQuery.data as any[]).slice(0, 3) : [];
|
||||
if (!items.length) return <Text color={textMuted}>Žádné plánované aktivity</Text>;
|
||||
return (
|
||||
<VStack spacing={3} align="stretch">
|
||||
{items.map((ev: any) => (
|
||||
<HStack key={ev.id} as={RouterLink} to={`/aktivita/${ev.id}`} _hover={{ textDecoration: 'none' }} align="flex-start" spacing={3}>
|
||||
<Box flex={1} minW={0}>
|
||||
<Text fontWeight="600" noOfLines={2}>{ev.title}</Text>
|
||||
<Text fontSize="sm" color={textMuted}>
|
||||
{(() => { try { const d = new Date(ev.start_time); return d.toLocaleDateString('cs-CZ') + (ev.location ? ` • ${ev.location}` : ''); } catch { return ev.start_time; } })()}
|
||||
</Text>
|
||||
</Box>
|
||||
</HStack>
|
||||
))}
|
||||
</VStack>
|
||||
);
|
||||
})()}
|
||||
</Widget>
|
||||
</VStack>
|
||||
</SimpleGrid>
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user