Files
MyClub/frontend/src/pages/VideosPage.tsx
T
Tomas Dvorak 8762bde4bf dev day #89
2025-11-11 10:29:30 +01:00

394 lines
13 KiB
TypeScript

import React, { useEffect, useMemo, useState } from 'react';
import MainLayout from '../components/layout/MainLayout';
import {
Box,
Container,
Heading,
Text,
SimpleGrid,
AspectRatio,
useColorModeValue,
Spinner,
VStack,
HStack,
Badge,
Button,
Link,
Modal,
ModalOverlay,
ModalContent,
ModalBody,
ModalCloseButton,
useDisclosure,
Icon,
} from '@chakra-ui/react';
import { useClubTheme } from '../contexts/ClubThemeContext';
import { usePublicSettings } from '../hooks/usePublicSettings';
import { getCachedYouTube, YouTubeVideo } from '../services/youtube';
import { FaPlay, FaExternalLinkAlt, FaYoutube } from 'react-icons/fa';
import NewsletterCTA from '../components/common/NewsletterCTA';
import CommentsSection from '../components/comments/CommentsSection';
import { Helmet } from 'react-helmet-async';
type RenderItem = {
key: string;
title: string;
embedUrl: string;
thumbnail?: string;
date?: string; // YYYY-MM-DD
videoId?: string;
};
const toEmbed = (idOrUrl: string): string => {
try {
if (idOrUrl.includes('youtube.com') || idOrUrl.includes('youtu.be')) {
const u = new URL(idOrUrl);
if (u.hostname.includes('youtu.be')) {
const id = u.pathname.replace('/', '');
return `https://www.youtube.com/embed/${id}`;
}
const id = u.searchParams.get('v');
if (id) return `https://www.youtube.com/embed/${id}`;
}
} catch {}
return `https://www.youtube.com/embed/${idOrUrl}`;
};
const extractVideoId = (embedUrl: string): string | undefined => {
if (embedUrl?.includes('/embed/')) {
return embedUrl.split('/embed/')[1]?.split('?')[0];
}
return undefined;
};
const VideosPage: React.FC = () => {
const cardBg = useColorModeValue('white', 'gray.800');
const borderColor = useColorModeValue('gray.200', 'gray.600');
const headingColor = useColorModeValue('gray.800', 'gray.100');
const textSecondary = useColorModeValue('gray.600', 'gray.300');
const textTertiary = useColorModeValue('gray.500', 'gray.400');
const modalBg = useColorModeValue('white', 'gray.800');
const placeholderBg = useColorModeValue('gray.100', 'gray.700');
const placeholderIcon = useColorModeValue('gray.400', 'gray.500');
const videoPrimaryColor = useColorModeValue('brand.primary', 'brand.accent');
const theme = useClubTheme();
const { data: settings, isLoading: settingsLoading } = usePublicSettings();
const [yt, setYt] = useState<YouTubeVideo[]>([]);
const [loading, setLoading] = useState(true);
const { isOpen, onOpen, onClose } = useDisclosure();
const [selectedVideo, setSelectedVideo] = useState<RenderItem | null>(null);
const source = settings?.videos_source || 'auto';
const youtubeUrl = (settings as any)?.youtube_url || (settings as any)?.social_youtube || null;
const titleOverrides: Record<string, string> = (settings as any)?.videos_title_overrides || {};
useEffect(() => {
let canceled = false;
const run = async () => {
if (source !== 'auto') {
setLoading(false);
return;
}
try {
const payload = await getCachedYouTube();
if (!payload) {
setLoading(false);
return;
}
const vids = (payload.videos || [])
.slice()
.sort((a, b) => (Date.parse(b.published_date || '') || 0) - (Date.parse(a.published_date || '') || 0));
if (!canceled) setYt(vids);
} catch (e) {
console.error('Failed to load videos:', e);
} finally {
if (!canceled) setLoading(false);
}
};
run();
return () => {
canceled = true;
};
}, [source]);
const items: RenderItem[] = useMemo(() => {
if (source === 'auto') {
return (yt || []).map((v) => ({
key: v.video_id,
title: (titleOverrides?.[v.video_id]?.trim()) || v.title,
embedUrl: toEmbed(v.video_id),
thumbnail: v.thumbnail_url,
date: v.published_date,
videoId: v.video_id,
}));
}
// Manual fallback from settings
const manual = (settings?.videos_items || []).map((it, i) => {
const embedUrl = toEmbed(it.url);
return {
key: `${i}-${it.url}`,
title: it.title || `Video ${i + 1}`,
embedUrl,
thumbnail: it.thumbnail_url,
date: it.uploaded_at,
videoId: extractVideoId(embedUrl),
};
});
const legacy = ((settings as any)?.videos || []).map((url: string, i: number) => {
const embedUrl = toEmbed(url);
return {
key: `${i}-${url}`,
title: `Video ${i + 1}`,
embedUrl,
videoId: extractVideoId(embedUrl),
};
});
return manual.length ? manual : legacy;
}, [source, yt, settings?.videos_items, settings, titleOverrides]);
const openVideo = (item: RenderItem) => {
setSelectedVideo(item);
onOpen();
};
const VideoCard: React.FC<{ item: RenderItem }> = ({ item }) => {
const thumb =
item.thumbnail ||
(item.videoId ? `https://i.ytimg.com/vi/${item.videoId}/hqdefault.jpg` : undefined);
return (
<Box
bg={cardBg}
borderRadius="xl"
overflow="hidden"
boxShadow="sm"
borderWidth="2px"
borderColor={borderColor}
transition="all 0.3s"
_hover={{
transform: 'translateY(-8px)',
boxShadow: '0 20px 40px rgba(0,0,0,0.15)',
borderColor: 'brand.primary',
}}
>
<AspectRatio ratio={16 / 9}>
<Box position="relative" cursor="pointer" onClick={() => openVideo(item)}>
{/* Thumbnail */}
{thumb ? (
<Box
as="img"
src={thumb}
alt={item.title}
width="100%"
height="100%"
loading="lazy"
decoding="async"
referrerPolicy="origin-when-cross-origin"
style={{ objectFit: 'cover' }}
/>
) : (
<Box bg={placeholderBg} display="flex" alignItems="center" justifyContent="center">
<Icon as={FaPlay} boxSize={12} color={placeholderIcon} />
</Box>
)}
{/* Play overlay */}
<Box
position="absolute"
inset={0}
display="flex"
alignItems="center"
justifyContent="center"
bg="blackAlpha.600"
opacity={0}
_hover={{ opacity: 1 }}
transition="opacity 0.3s"
>
<Box
bg="brand.primary"
color="white"
borderRadius="full"
px={8}
py={4}
fontWeight="bold"
display="flex"
alignItems="center"
gap={2}
transform="scale(1)"
_hover={{ transform: 'scale(1.1)' }}
transition="transform 0.2s"
boxShadow="0 8px 24px rgba(0,0,0,0.3)"
>
<Icon as={FaPlay} boxSize={5} />
<Text fontSize="lg">Přehrát</Text>
</Box>
</Box>
</Box>
</AspectRatio>
<Box p={4} borderTopWidth="2px" borderTopColor={videoPrimaryColor}>
<VStack align="start" spacing={2}>
<Text fontWeight="bold" fontSize="md" color={videoPrimaryColor} noOfLines={2}>
{item.title}
</Text>
<HStack justify="space-between" width="100%">
{item.date && (
<Badge colorScheme="gray" fontSize="0.7rem">
{new Date(item.date).toLocaleDateString('cs-CZ')}
</Badge>
)}
{item.videoId && (
<Link href={`https://www.youtube.com/watch?v=${item.videoId}`} isExternal onClick={(e) => e.stopPropagation()}>
<Button
size="xs"
variant="ghost"
colorScheme="red"
leftIcon={<Icon as={FaYoutube} />}
>
YouTube
</Button>
</Link>
)}
</HStack>
</VStack>
</Box>
</Box>
);
};
if (settingsLoading || loading) {
return (
<MainLayout>
<Container maxW="7xl" py={{ base: 6, md: 10 }}>
<VStack spacing={4}>
<Spinner size="xl" color={theme.accent} />
<Text color={textSecondary}>Načítám videa...</Text>
</VStack>
</Container>
</MainLayout>
);
}
return (
<MainLayout>
<Helmet>
<title>Videa</title>
<meta name="description" content="Sledujte naše nejnovější videa a zápasy." />
</Helmet>
<Container maxW="7xl" py={{ base: 6, md: 10 }}>
<Box mb={6}>
<HStack justify="space-between" mb={2} flexWrap="wrap">
<Heading as="h1" size="xl" color={headingColor}>
Videa
</Heading>
{youtubeUrl && (
<Link href={youtubeUrl} isExternal>
<Button
size="sm"
variant="outline"
colorScheme="red"
rightIcon={<Icon as={FaExternalLinkAlt} />}
>
YouTube kanál
</Button>
</Link>
)}
</HStack>
<VStack align="start" spacing={1}>
<Text color={textSecondary}>Sledujte naše nejnovější videa a zápasy</Text>
<HStack spacing={1} color={textTertiary} fontSize="sm">
<Icon as={FaYoutube} color="red.500" />
<Text>Všechna videa jsou z YouTube</Text>
</HStack>
</VStack>
</Box>
{items.length === 0 ? (
<Box textAlign="center" py={12}>
<Icon as={FaPlay} boxSize={16} color={placeholderIcon} mb={4} />
<Text color={textSecondary} fontSize="lg">
Zatím nejsou k dispozici žádná videa
</Text>
</Box>
) : (
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={6}>
{items.map((item) => (
<VideoCard key={item.key} item={item} />
))}
</SimpleGrid>
)}
</Container>
{/* Video Modal */}
<Modal isOpen={isOpen} onClose={onClose} size="6xl" isCentered>
<ModalOverlay bg="blackAlpha.800" />
<ModalContent bg="transparent" boxShadow="none" maxW="90vw">
<ModalCloseButton
color="white"
bg="blackAlpha.600"
_hover={{ bg: 'blackAlpha.800' }}
size="lg"
top={2}
right={2}
zIndex={2}
/>
<ModalBody p={0}>
{selectedVideo && (
<Box>
<AspectRatio ratio={16 / 9} maxH="80vh">
<iframe
src={`${selectedVideo.embedUrl}?autoplay=1&vq=hd1080&rel=0&modestbranding=1&playsinline=1`}
title={selectedVideo.title}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
loading="lazy"
referrerPolicy="strict-origin-when-cross-origin"
style={{ borderRadius: '8px' }}
/>
</AspectRatio>
<Box bg={modalBg} p={4} borderRadius="md" mt={2}>
<HStack justify="space-between" align="start">
<VStack align="start" flex={1}>
<Text fontWeight="bold" fontSize="lg">
{selectedVideo.title}
</Text>
{selectedVideo.date && (
<Text color={textSecondary} fontSize="sm">
{new Date(selectedVideo.date).toLocaleDateString('cs-CZ', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</Text>
)}
</VStack>
{selectedVideo.videoId && (
<Link href={`https://www.youtube.com/watch?v=${selectedVideo.videoId}`} isExternal>
<Button
size="sm"
colorScheme="red"
leftIcon={<Icon as={FaYoutube} />}
>
Otevřít na YouTube
</Button>
</Link>
)}
</HStack>
{selectedVideo.videoId && (
<Box mt={4}>
<CommentsSection targetType="youtube_video" targetId={selectedVideo.videoId} />
</Box>
)}
</Box>
</Box>
)}
</ModalBody>
</ModalContent>
</Modal>
{/* Newsletter CTA */}
<NewsletterCTA />
</MainLayout>
);
};
export default VideosPage;