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

321 lines
9.8 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 { API_URL } from '../services/api';
import PhotoModal from '../components/gallery/PhotoModal';
import CommentsSection from '../components/comments/CommentsSection';
import { Helmet } from 'react-helmet-async';
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 origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin;
const abs = new URL(path, origin);
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>
<Helmet>
<title>{album.title} | Fotogalerie</title>
<meta name="description" content={`Fotogalerie: ${album.title}.`} />
</Helmet>
<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={album.url || '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>
)}
{/* Comments */}
{album.id && (
<Box mt={6}>
<CommentsSection targetType="gallery_album" targetId={String(album.id)} />
</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;