mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 10:42:57 +00:00
308 lines
9.3 KiB
TypeScript
308 lines
9.3 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { useParams, Link as RouterLink } from 'react-router-dom';
|
|
import {
|
|
Box,
|
|
Container,
|
|
Heading,
|
|
Text,
|
|
SimpleGrid,
|
|
Image,
|
|
Spinner,
|
|
VStack,
|
|
HStack,
|
|
Button,
|
|
Breadcrumb,
|
|
BreadcrumbItem,
|
|
BreadcrumbLink,
|
|
useDisclosure,
|
|
Badge,
|
|
useColorModeValue,
|
|
} from '@chakra-ui/react';
|
|
import { ChevronRight, ExternalLink, Calendar, Image as ImageIcon } from 'lucide-react';
|
|
import MainLayout from '../components/layout/MainLayout';
|
|
import PhotoModal from '../components/gallery/PhotoModal';
|
|
|
|
interface Photo {
|
|
id: string;
|
|
page_url: string;
|
|
image_1500: string;
|
|
}
|
|
|
|
interface Album {
|
|
id: string;
|
|
title: string;
|
|
url: string;
|
|
date: string;
|
|
photos_count: number;
|
|
views_count?: number;
|
|
photos: Photo[];
|
|
fetched_at?: string;
|
|
}
|
|
|
|
const resolveBackendUrl = (path: string) => {
|
|
try {
|
|
if (/^https?:\/\//i.test(path)) return path;
|
|
if (path.startsWith('/cache') || path.startsWith('/uploads') || path.startsWith('/api/')) {
|
|
const base = (process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1');
|
|
const b = new URL(base);
|
|
const abs = new URL(path, `${b.protocol}//${b.host}`);
|
|
return abs.toString();
|
|
}
|
|
return path;
|
|
} catch {
|
|
return path;
|
|
}
|
|
};
|
|
|
|
const AlbumDetailPage: React.FC = () => {
|
|
const { id } = useParams<{ id: string }>();
|
|
const [album, setAlbum] = useState<Album | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string>('');
|
|
const [selectedPhoto, setSelectedPhoto] = useState<Photo | null>(null);
|
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
|
|
|
// Dark mode colors
|
|
const bgColor = useColorModeValue('#f8f9fb', '#0f1115');
|
|
const cardBg = useColorModeValue('white', '#1a1d29');
|
|
const borderColor = useColorModeValue('#e5e7eb', '#2a2e3a');
|
|
const headingColor = useColorModeValue('gray.800', 'gray.100');
|
|
const textSecondary = useColorModeValue('gray.600', 'gray.300');
|
|
const infoBg = useColorModeValue('blue.50', 'blue.900');
|
|
const infoBorder = useColorModeValue('blue.200', 'blue.700');
|
|
const infoText = useColorModeValue('blue.800', 'blue.200');
|
|
|
|
useEffect(() => {
|
|
const fetchAlbum = async () => {
|
|
if (!id) return;
|
|
|
|
setLoading(true);
|
|
setError('');
|
|
|
|
try {
|
|
// Check both sources for the album
|
|
const [profileRes, albumsRes] = await Promise.allSettled([
|
|
fetch(resolveBackendUrl('/cache/prefetch/zonerama_profile.json'), { cache: 'no-cache' }),
|
|
fetch(resolveBackendUrl('/cache/prefetch/zonerama_albums.json'), { cache: 'no-cache' })
|
|
]);
|
|
|
|
let foundAlbum: Album | null = null;
|
|
|
|
// Try profile albums first (newest/main source)
|
|
if (profileRes.status === 'fulfilled' && profileRes.value.ok) {
|
|
const profileData = await profileRes.value.json();
|
|
const albums = profileData.albums || [];
|
|
foundAlbum = albums.find((a: Album) => a.id === id);
|
|
}
|
|
|
|
// If not found, try blog-related albums
|
|
if (!foundAlbum && albumsRes.status === 'fulfilled' && albumsRes.value.ok) {
|
|
const albumsData = await albumsRes.value.json();
|
|
const blogAlbums = Array.isArray(albumsData) ? albumsData : [];
|
|
foundAlbum = blogAlbums.find((a: Album) => a.id === id);
|
|
}
|
|
|
|
if (!foundAlbum) {
|
|
throw new Error('Album nenalezen');
|
|
}
|
|
|
|
setAlbum(foundAlbum);
|
|
} catch (err: any) {
|
|
setError(err.message || 'Chyba při načítání alba');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchAlbum();
|
|
}, [id]);
|
|
|
|
const handlePhotoClick = (photo: Photo) => {
|
|
setSelectedPhoto(photo);
|
|
onOpen();
|
|
};
|
|
|
|
const handleCloseModal = () => {
|
|
onClose();
|
|
setSelectedPhoto(null);
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<MainLayout>
|
|
<Container maxW="7xl" py={8}>
|
|
<VStack spacing={4}>
|
|
<Spinner size="xl" color="brand.primary" />
|
|
<Text color={textSecondary}>Načítám album...</Text>
|
|
</VStack>
|
|
</Container>
|
|
</MainLayout>
|
|
);
|
|
}
|
|
|
|
if (error || !album) {
|
|
return (
|
|
<MainLayout>
|
|
<Container maxW="7xl" py={8}>
|
|
<VStack spacing={4}>
|
|
<Text color="red.500" fontSize="lg">
|
|
{error || 'Album nenalezeno'}
|
|
</Text>
|
|
<Button as={RouterLink} to="/galerie" colorScheme="blue">
|
|
Zpět na galerii
|
|
</Button>
|
|
</VStack>
|
|
</Container>
|
|
</MainLayout>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<MainLayout>
|
|
<Box bg={bgColor} minH="100vh" py={8}>
|
|
<Container maxW="7xl">
|
|
{/* Breadcrumbs */}
|
|
<Breadcrumb
|
|
spacing={2}
|
|
separator={<ChevronRight size={16} color="gray" />}
|
|
mb={6}
|
|
fontSize="sm"
|
|
>
|
|
<BreadcrumbItem>
|
|
<BreadcrumbLink as={RouterLink} to="/">
|
|
Domů
|
|
</BreadcrumbLink>
|
|
</BreadcrumbItem>
|
|
<BreadcrumbItem>
|
|
<BreadcrumbLink as={RouterLink} to="/galerie">
|
|
Galerie
|
|
</BreadcrumbLink>
|
|
</BreadcrumbItem>
|
|
<BreadcrumbItem isCurrentPage>
|
|
<BreadcrumbLink>{album.title}</BreadcrumbLink>
|
|
</BreadcrumbItem>
|
|
</Breadcrumb>
|
|
|
|
{/* Album Header */}
|
|
<VStack align="stretch" spacing={4} mb={8}>
|
|
<HStack justify="space-between" align="start" flexWrap="wrap" gap={4}>
|
|
<VStack align="start" spacing={2} flex={1}>
|
|
<Heading size="xl" color={headingColor}>
|
|
{album.title}
|
|
</Heading>
|
|
<HStack spacing={4} flexWrap="wrap" fontSize="sm" color={textSecondary}>
|
|
{album.date && (
|
|
<HStack spacing={1}>
|
|
<Calendar size={16} />
|
|
<Text>{album.date}</Text>
|
|
</HStack>
|
|
)}
|
|
<HStack spacing={1}>
|
|
<ImageIcon size={16} />
|
|
<Text>{album.photos_count} fotografií</Text>
|
|
</HStack>
|
|
{album.views_count !== undefined && album.views_count > 0 && (
|
|
<Badge colorScheme="purple">{album.views_count} zhlédnutí</Badge>
|
|
)}
|
|
</HStack>
|
|
</VStack>
|
|
|
|
<Button
|
|
as="a"
|
|
href={album.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
rightIcon={<ExternalLink size={18} />}
|
|
colorScheme="purple"
|
|
size="md"
|
|
>
|
|
Zobrazit na Zonerama
|
|
</Button>
|
|
</HStack>
|
|
|
|
{/* Zonerama Attribution */}
|
|
<Box
|
|
bg={infoBg}
|
|
borderWidth="1px"
|
|
borderColor={infoBorder}
|
|
borderRadius="md"
|
|
p={3}
|
|
>
|
|
<Text fontSize="sm" color={infoText}>
|
|
📸 Všechny fotografie jsou z platformy{' '}
|
|
<Text
|
|
as="a"
|
|
href="https://zonerama.com"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
fontWeight="600"
|
|
color="blue.600"
|
|
_hover={{ textDecoration: 'underline' }}
|
|
>
|
|
Zonerama
|
|
</Text>
|
|
</Text>
|
|
</Box>
|
|
</VStack>
|
|
|
|
{/* Photo Grid */}
|
|
{album.photos && album.photos.length > 0 ? (
|
|
<SimpleGrid columns={{ base: 2, md: 3, lg: 4, xl: 5 }} spacing={4}>
|
|
{album.photos.map((photo) => (
|
|
<Box
|
|
key={photo.id}
|
|
cursor="pointer"
|
|
onClick={() => handlePhotoClick(photo)}
|
|
borderRadius="lg"
|
|
overflow="hidden"
|
|
boxShadow="md"
|
|
borderWidth="1px"
|
|
borderColor={borderColor}
|
|
transition="all 0.2s"
|
|
_hover={{ transform: 'translateY(-4px)', boxShadow: 'xl', borderColor: useColorModeValue('gray.300', 'gray.600') }}
|
|
bg={cardBg}
|
|
>
|
|
<Image
|
|
src={resolveBackendUrl(photo.image_1500)}
|
|
alt={`Fotka ${photo.id}`}
|
|
w="100%"
|
|
h="200px"
|
|
objectFit="cover"
|
|
loading="lazy"
|
|
/>
|
|
</Box>
|
|
))}
|
|
</SimpleGrid>
|
|
) : (
|
|
<Box
|
|
bg="bg.card"
|
|
borderWidth="1px"
|
|
borderColor="border.subtle"
|
|
borderRadius="lg"
|
|
p={8}
|
|
textAlign="center"
|
|
>
|
|
<Text color="gray.500">
|
|
V tomto albu nejsou žádné fotografie.
|
|
</Text>
|
|
</Box>
|
|
)}
|
|
</Container>
|
|
</Box>
|
|
|
|
{/* Photo Modal */}
|
|
{selectedPhoto && (
|
|
<PhotoModal
|
|
isOpen={isOpen}
|
|
onClose={handleCloseModal}
|
|
photoUrl={resolveBackendUrl(selectedPhoto.image_1500)}
|
|
pageUrl={selectedPhoto.page_url}
|
|
albumTitle={album.title}
|
|
/>
|
|
)}
|
|
</MainLayout>
|
|
);
|
|
};
|
|
|
|
export default AlbumDetailPage;
|