This commit is contained in:
Tomas Dvorak
2025-10-29 21:20:16 +01:00
parent 823fabee02
commit 16e4533202
61 changed files with 2308 additions and 942 deletions
+29 -20
View File
@@ -3,7 +3,7 @@ import { Box, Button, FormControl, FormLabel, Heading, HStack, IconButton, Image
import { FiPlus, FiEdit2, FiTrash2, FiUpload, FiAlertCircle, FiCheckCircle } from 'react-icons/fi';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import AdminLayout from '../../layouts/AdminLayout';
import { Sponsor, getSponsors, createSponsor, updateSponsor, deleteSponsor } from '../../services/sponsors';
import { Banner as AdminBanner, getBanners, createBanner, updateBanner, deleteBanner } from '../../services/banners';
import { uploadFile } from '../../services/articles';
import { assetUrl } from '../../utils/url';
@@ -15,7 +15,7 @@ type BannerPreset = {
width: number;
height: number;
aspectRatio: number;
position: 'top' | 'middle' | 'sidebar' | 'footer' | 'article';
position: 'top' | 'middle' | 'sidebar' | 'footer' | 'article' | 'under_table';
};
const BANNER_PRESETS: BannerPreset[] = [
@@ -63,6 +63,15 @@ const BANNER_PRESETS: BannerPreset[] = [
height: 90,
aspectRatio: 8.09,
position: 'article'
},
{
value: 'homepage_under_table',
label: 'Pod tabulkou (Homepage)',
description: 'Banner pod sekcí Tabulky na titulní stránce',
width: 970,
height: 90,
aspectRatio: 10.78,
position: 'under_table'
}
];
@@ -72,8 +81,8 @@ const BannersAdminPage: React.FC = () => {
const inputBg = useColorModeValue('white', 'gray.700');
const toast = useToast();
const qc = useQueryClient();
const { data, isLoading } = useQuery({ queryKey: ['admin-banners'], queryFn: getSponsors });
const [editing, setEditing] = useState<Partial<Sponsor> | null>(null);
const { data, isLoading } = useQuery<AdminBanner[]>(['admin-banners'], () => getBanners());
const [editing, setEditing] = useState<Partial<AdminBanner> | null>(null);
const [imageResolution, setImageResolution] = useState<{ width: number; height: number } | null>(null);
const [recommendedPlacements, setRecommendedPlacements] = useState<BannerPreset[]>([]);
const [uploadingImage, setUploadingImage] = useState(false);
@@ -117,7 +126,7 @@ const BannersAdminPage: React.FC = () => {
setRecommendedPlacements([]);
onOpen();
};
const openEdit = (s: Sponsor) => {
const openEdit = (s: AdminBanner) => {
setEditing({ ...s });
setImageResolution(null);
setRecommendedPlacements([]);
@@ -134,17 +143,17 @@ const BannersAdminPage: React.FC = () => {
};
const createMut = useMutation({
mutationFn: (payload: any) => createSponsor(payload),
mutationFn: (payload: any) => createBanner(payload),
onSuccess: () => { toast({ title: 'Banner vytvořen', status: 'success' }); qc.invalidateQueries({ queryKey: ['admin-banners'] }); closeModal(); },
onError: (e: any) => toast({ title: 'Vytvoření selhalo', description: e?.response?.data?.message || 'Chyba', status: 'error' }),
});
const updateMut = useMutation({
mutationFn: ({ id, payload }: { id: number | string; payload: any }) => updateSponsor(id, payload),
mutationFn: ({ id, payload }: { id: number | string; payload: any }) => updateBanner(id, payload),
onSuccess: () => { toast({ title: 'Banner upraven', status: 'success' }); qc.invalidateQueries({ queryKey: ['admin-banners'] }); closeModal(); },
onError: (e: any) => toast({ title: 'Aktualizace selhala', description: e?.response?.data?.message || 'Chyba', status: 'error' }),
});
const deleteMut = useMutation({
mutationFn: (id: number | string) => deleteSponsor(id),
mutationFn: (id: number | string) => deleteBanner(id),
onSuccess: () => { toast({ title: 'Banner smazán', status: 'success' }); qc.invalidateQueries({ queryKey: ['admin-banners'] }); },
onError: (e: any) => toast({ title: 'Smazání selhalo', description: e?.response?.data?.message || 'Chyba', status: 'error' }),
});
@@ -153,8 +162,8 @@ const BannersAdminPage: React.FC = () => {
if (!editing) return;
const payload = {
name: editing.name || '',
logo_url: editing.logo_url,
website_url: editing.website_url,
image_url: (editing as any).image_url,
click_url: (editing as any).click_url,
is_active: editing.is_active ?? true,
placement: (editing as any).placement || '',
width: (editing as any).width || undefined,
@@ -192,7 +201,7 @@ const BannersAdminPage: React.FC = () => {
const res = await uploadFile(file);
// Update editing state with uploaded URL
setEditing((prev) => ({ ...(prev || {}), logo_url: res.url }));
setEditing((prev) => ({ ...(prev || {}), image_url: res.url }));
// If no placement selected yet, auto-select the best recommendation
if (!editing?.placement && recommended.length > 0) {
@@ -265,17 +274,17 @@ const BannersAdminPage: React.FC = () => {
{isLoading && (
<Tr><Td colSpan={6} textAlign="center"><Spinner size="sm" mr={2} />Načítání</Td></Tr>
)}
{!isLoading && banners.map((b) => {
{!isLoading && banners.map((b: AdminBanner) => {
const preset = getPreset((b as any).placement);
return (
<Tr key={b.id}>
<Td>
<Image src={assetUrl(b.logo_url) || '/logo192.png'} alt={b.name} boxSize="56px" objectFit="contain" bg={inputBg} borderRadius="md" />
<Image src={assetUrl((b as any).image_url) || '/logo192.png'} alt={b.name} boxSize="56px" objectFit="contain" bg={inputBg} borderRadius="md" />
</Td>
<Td>
<Text fontWeight="500">{b.name}</Text>
{b.website_url && (
<Text fontSize="xs" color="gray.500" noOfLines={1}>{b.website_url}</Text>
{(b as any).click_url && (
<Text fontSize="xs" color="gray.500" noOfLines={1}>{(b as any).click_url}</Text>
)}
</Td>
<Td>
@@ -323,7 +332,7 @@ const BannersAdminPage: React.FC = () => {
</FormControl>
<FormControl>
<FormLabel>Odkaz (po kliku)</FormLabel>
<Input type="url" value={editing?.website_url || ''} onChange={(e) => setEditing((prev) => ({ ...(prev as any), website_url: e.target.value }))} placeholder="https://partner.cz" />
<Input type="url" value={(editing as any)?.click_url || ''} onChange={(e) => setEditing((prev) => ({ ...(prev as any), click_url: e.target.value }))} placeholder="https://partner.cz" />
</FormControl>
{/* Image resolution info */}
{imageResolution && (
@@ -430,7 +439,7 @@ const BannersAdminPage: React.FC = () => {
<FormLabel>Obrázek banneru</FormLabel>
<VStack align="stretch" spacing={3}>
{/* Preview */}
{editing?.logo_url && (() => {
{(editing as any)?.image_url && (() => {
const preset = getPreset((editing as any)?.placement);
const previewWidth = preset ? Math.min(preset.width, 600) : 300;
const previewHeight = preset ? (previewWidth / preset.aspectRatio) : 150;
@@ -446,7 +455,7 @@ const BannersAdminPage: React.FC = () => {
bg={inputBg}
>
<Image
src={assetUrl(editing?.logo_url) || '/logo192.png'}
src={assetUrl((editing as any)?.image_url) || '/logo192.png'}
alt="banner preview"
width={`${previewWidth}px`}
height={`${previewHeight}px`}
@@ -475,7 +484,7 @@ const BannersAdminPage: React.FC = () => {
isLoading={uploadingImage}
loadingText="Nahrávání..."
>
{editing?.logo_url ? 'Změnit obrázek' : 'Nahrát obrázek'}
{(editing as any)?.image_url ? 'Změnit obrázek' : 'Nahrát obrázek'}
<Input
ref={fileInputRef}
type="file"
@@ -489,7 +498,7 @@ const BannersAdminPage: React.FC = () => {
{uploadingImage && <Spinner size="sm" />}
</HStack>
{!editing?.logo_url && (
{!((editing as any)?.image_url) && (
<Alert status="warning" fontSize="xs">
<AlertIcon boxSize="12px" />
<Text fontSize="xs">Nahrajte obrázek pro automatické doporučení umístění</Text>