mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-05 03:02:56 +00:00
upload
This commit is contained in:
@@ -0,0 +1,381 @@
|
||||
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 SponsorsSection from '../components/common/SponsorsSection';
|
||||
import NewsletterCTA from '../components/common/NewsletterCTA';
|
||||
|
||||
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;
|
||||
|
||||
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: 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]);
|
||||
|
||||
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%"
|
||||
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>
|
||||
<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
|
||||
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>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
{/* Newsletter CTA */}
|
||||
<NewsletterCTA />
|
||||
|
||||
{/* Sponsors Section */}
|
||||
<SponsorsSection />
|
||||
</MainLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideosPage;
|
||||
Reference in New Issue
Block a user