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,307 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user