mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 18:52:56 +00:00
upload
This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Heading,
|
||||
Text,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
Button,
|
||||
HStack,
|
||||
VStack,
|
||||
Badge,
|
||||
Spinner,
|
||||
useToast,
|
||||
Image,
|
||||
Link,
|
||||
Alert,
|
||||
AlertIcon,
|
||||
AlertTitle,
|
||||
AlertDescription,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { RefreshCw, ExternalLink, Calendar, Image as ImageIcon, Eye } from 'lucide-react';
|
||||
import AdminLayout from '../../components/layout/AdminLayout';
|
||||
|
||||
interface Album {
|
||||
id: string;
|
||||
title: string;
|
||||
url: string;
|
||||
date: string;
|
||||
photos_count: number;
|
||||
views_count?: number;
|
||||
photos: Array<{
|
||||
id: string;
|
||||
page_url: string;
|
||||
image_1500: 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 GalleryAdminPage: React.FC = () => {
|
||||
const cardBg = useColorModeValue('white', 'gray.800');
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.700');
|
||||
const textSecondary = useColorModeValue('gray.600', 'gray.400');
|
||||
const [albums, setAlbums] = useState<Album[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string>('');
|
||||
const toast = useToast();
|
||||
|
||||
const fetchAlbums = async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
// Try to load from both sources and combine them (similar to frontpage)
|
||||
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 combinedAlbums: Album[] = [];
|
||||
|
||||
// Get profile albums (main source)
|
||||
if (profileRes.status === 'fulfilled' && profileRes.value.ok) {
|
||||
const profileData = await profileRes.value.json();
|
||||
if (profileData.albums && Array.isArray(profileData.albums)) {
|
||||
combinedAlbums = [...profileData.albums];
|
||||
}
|
||||
}
|
||||
|
||||
// Get additional albums (fallback source)
|
||||
if (albumsRes.status === 'fulfilled' && albumsRes.value.ok) {
|
||||
const albumsData = await albumsRes.value.json();
|
||||
const blogAlbums = Array.isArray(albumsData) ? albumsData : [];
|
||||
|
||||
// Filter out empty albums and avoid duplicates
|
||||
const validBlogAlbums = blogAlbums.filter((album: any) =>
|
||||
album.id &&
|
||||
album.title &&
|
||||
!combinedAlbums.some(existing => existing.id === album.id)
|
||||
);
|
||||
|
||||
combinedAlbums = [...combinedAlbums, ...validBlogAlbums];
|
||||
}
|
||||
|
||||
setAlbums(combinedAlbums);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Nepodařilo se načíst alba');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
|
||||
try {
|
||||
const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1';
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
const response = await fetch(`${apiUrl}/admin/gallery/refresh`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Chyba při obnově galerie');
|
||||
}
|
||||
|
||||
toast({
|
||||
title: 'Galerie obnovena',
|
||||
description: 'Data z Zonerama byla úspěšně načtena',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
// Reload albums after refresh
|
||||
await fetchAlbums();
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: 'Chyba',
|
||||
description: err.message || 'Nepodařilo se obnovit galerii',
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
});
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchAlbums();
|
||||
}, []);
|
||||
|
||||
const totalPhotos = albums.reduce((sum, album) => sum + album.photos_count, 0);
|
||||
const totalViews = albums.reduce((sum, album) => sum + (album.views_count || 0), 0);
|
||||
|
||||
return (
|
||||
<AdminLayout>
|
||||
<Container maxW="7xl" py={8}>
|
||||
<VStack align="stretch" spacing={6}>
|
||||
{/* Header */}
|
||||
<HStack justify="space-between" align="center" flexWrap="wrap">
|
||||
<VStack align="start" spacing={1}>
|
||||
<Heading size="xl">Správa galerie</Heading>
|
||||
<Text color="gray.600">
|
||||
Správa alb a fotografií ze Zonerama
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
<Button
|
||||
leftIcon={<RefreshCw size={18} />}
|
||||
colorScheme="blue"
|
||||
onClick={handleRefresh}
|
||||
isLoading={refreshing}
|
||||
loadingText="Obnova..."
|
||||
>
|
||||
Obnovit z Zonerama
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
{/* Zonerama Info */}
|
||||
<Alert status="info" borderRadius="md">
|
||||
<AlertIcon />
|
||||
<Box>
|
||||
<AlertTitle>Zonerama integrace</AlertTitle>
|
||||
<AlertDescription>
|
||||
Alba jsou automaticky načítána ze Zonerama profilu. Klikněte na "Obnovit z Zonerama" pro synchronizaci s nejnovějšími daty.
|
||||
</AlertDescription>
|
||||
</Box>
|
||||
</Alert>
|
||||
|
||||
{/* Statistics */}
|
||||
{!loading && !error && albums.length > 0 && (
|
||||
<HStack spacing={4} flexWrap="wrap">
|
||||
<Badge colorScheme="purple" fontSize="md" p={3} borderRadius="md">
|
||||
{albums.length} alb
|
||||
</Badge>
|
||||
<Badge colorScheme="blue" fontSize="md" p={3} borderRadius="md">
|
||||
{totalPhotos} fotografií
|
||||
</Badge>
|
||||
<Badge colorScheme="green" fontSize="md" p={3} borderRadius="md">
|
||||
{totalViews} zhlédnutí
|
||||
</Badge>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
{/* Loading State */}
|
||||
{loading && (
|
||||
<VStack spacing={4} py={12}>
|
||||
<Spinner size="xl" color="brand.primary" />
|
||||
<Text color="gray.600">Načítám alba...</Text>
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{/* Error State */}
|
||||
{error && !loading && (
|
||||
<Alert status="error" borderRadius="md">
|
||||
<AlertIcon />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{!loading && !error && albums.length === 0 && (
|
||||
<Box
|
||||
bg={cardBg}
|
||||
borderWidth="1px"
|
||||
borderRadius="lg"
|
||||
p={12}
|
||||
textAlign="center"
|
||||
>
|
||||
<VStack spacing={4}>
|
||||
<ImageIcon size={48} color="gray" />
|
||||
<Heading size="md" color="gray.600">
|
||||
Zatím nejsou k dispozici žádná alba
|
||||
</Heading>
|
||||
<Text color="gray.500">
|
||||
Klikněte na tlačítko "Obnovit z Zonerama" pro načtení alb.
|
||||
</Text>
|
||||
<Button
|
||||
leftIcon={<RefreshCw size={18} />}
|
||||
colorScheme="blue"
|
||||
onClick={handleRefresh}
|
||||
isLoading={refreshing}
|
||||
>
|
||||
Obnovit z Zonerama
|
||||
</Button>
|
||||
</VStack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Albums Table */}
|
||||
{!loading && !error && albums.length > 0 && (
|
||||
<Box
|
||||
bg={cardBg}
|
||||
borderWidth="1px"
|
||||
borderRadius="lg"
|
||||
overflow="hidden"
|
||||
boxShadow="sm"
|
||||
>
|
||||
<Table variant="simple">
|
||||
<Thead bg={useColorModeValue('gray.50', 'gray.900')}>
|
||||
<Tr>
|
||||
<Th width="100px">Náhled</Th>
|
||||
<Th>Název</Th>
|
||||
<Th width="120px">Datum</Th>
|
||||
<Th width="100px" isNumeric>Fotky</Th>
|
||||
<Th width="120px" isNumeric>Zhlédnutí</Th>
|
||||
<Th width="180px">Akce</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{albums.map((album) => {
|
||||
const coverPhoto = album.photos && album.photos.length > 0
|
||||
? album.photos[0]
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Tr key={album.id} _hover={{ bg: 'gray.50' }}>
|
||||
<Td>
|
||||
{coverPhoto ? (
|
||||
<Image
|
||||
src={coverPhoto.image_1500}
|
||||
alt={album.title}
|
||||
boxSize="60px"
|
||||
objectFit="cover"
|
||||
borderRadius="md"
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
boxSize="60px"
|
||||
bg="gray.200"
|
||||
borderRadius="md"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<ImageIcon size={24} color="gray" />
|
||||
</Box>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<Text fontWeight="600" color="gray.800" noOfLines={2}>
|
||||
{album.title}
|
||||
</Text>
|
||||
</Td>
|
||||
<Td>
|
||||
<HStack spacing={1} fontSize="sm" color="gray.600">
|
||||
<Calendar size={14} />
|
||||
<Text>{album.date}</Text>
|
||||
</HStack>
|
||||
</Td>
|
||||
<Td isNumeric>
|
||||
<Badge colorScheme="blue">
|
||||
{album.photos_count}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td isNumeric>
|
||||
<HStack spacing={1} justify="flex-end">
|
||||
<Eye size={14} />
|
||||
<Text fontSize="sm">{album.views_count || 0}</Text>
|
||||
</HStack>
|
||||
</Td>
|
||||
<Td>
|
||||
<HStack spacing={2}>
|
||||
<Button
|
||||
as={Link}
|
||||
href={`/galerie/album/${album.id}`}
|
||||
target="_blank"
|
||||
size="sm"
|
||||
colorScheme="purple"
|
||||
variant="outline"
|
||||
>
|
||||
Náhled
|
||||
</Button>
|
||||
<Button
|
||||
as={Link}
|
||||
href={album.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
variant="ghost"
|
||||
rightIcon={<ExternalLink size={14} />}
|
||||
>
|
||||
Zonerama
|
||||
</Button>
|
||||
</HStack>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
</Container>
|
||||
</AdminLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default GalleryAdminPage;
|
||||
Reference in New Issue
Block a user