import React, { useState, useRef } from 'react'; import { Box, Button, FormControl, FormLabel, Heading, HStack, IconButton, Image, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Spinner, Table, Tbody, Td, Th, Thead, Tr, useColorModeValue, useDisclosure, useToast, VStack, Select, Text, Switch, Badge, Alert, AlertIcon, AlertTitle, AlertDescription, Divider, Grid, GridItem } from '@chakra-ui/react'; 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 { Banner as AdminBanner, getBanners, createBanner, updateBanner, deleteBanner } from '../../services/banners'; import { uploadFile } from '../../services/articles'; import { assetUrl } from '../../utils/url'; // Banner placement presets with dimensions and descriptions type BannerPreset = { value: string; label: string; description: string; width: number; height: number; aspectRatio: number; position: 'top' | 'middle' | 'sidebar' | 'footer' | 'article' | 'under_table'; }; const BANNER_PRESETS: BannerPreset[] = [ { value: 'homepage_top', label: 'Hlavní banner (Homepage - vrchol)', description: 'Hlavní reklamní plocha nahoře, zobrazena všem návštěvníkům', width: 1200, height: 200, aspectRatio: 6, position: 'top' }, { value: 'homepage_middle', label: 'Střední banner (Homepage - střed)', description: 'Banner ve středu stránky mezi obsahem', width: 970, height: 250, aspectRatio: 3.88, position: 'middle' }, { value: 'homepage_sidebar', label: 'Postranní banner (Homepage - sidebar)', description: 'Menší banner v pravém postranním panelu', width: 300, height: 250, aspectRatio: 1.2, position: 'sidebar' }, { value: 'homepage_footer', label: 'Spodní banner (Homepage - zápatí)', description: 'Banner v dolní části stránky před zápatím', width: 1200, height: 200, aspectRatio: 6, position: 'footer' }, { value: 'article_inline', label: 'Banner v článcích', description: 'Banner zobrazený v textu článků', width: 728, 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' } ]; const BannersAdminPage: React.FC = () => { const cardBg = useColorModeValue('white', 'gray.800'); const borderColor = useColorModeValue('gray.200', 'gray.700'); const inputBg = useColorModeValue('white', 'gray.700'); const toast = useToast(); const qc = useQueryClient(); const { data, isLoading } = useQuery(['admin-banners'], () => getBanners()); const [editing, setEditing] = useState | null>(null); const [imageResolution, setImageResolution] = useState<{ width: number; height: number } | null>(null); const [recommendedPlacements, setRecommendedPlacements] = useState([]); const [uploadingImage, setUploadingImage] = useState(false); const fileInputRef = useRef(null); const { isOpen, onOpen, onClose } = useDisclosure(); // Get preset by value const getPreset = (placement?: string): BannerPreset | undefined => { return BANNER_PRESETS.find(p => p.value === placement); }; // Recommend placements based on image resolution const recommendPlacement = (imgWidth: number, imgHeight: number): BannerPreset[] => { const imgAspectRatio = imgWidth / imgHeight; // Sort presets by how close their aspect ratio is to the image const scored = BANNER_PRESETS.map(preset => { const ratioDiff = Math.abs(preset.aspectRatio - imgAspectRatio); const widthDiff = Math.abs(preset.width - imgWidth) / preset.width; const heightDiff = Math.abs(preset.height - imgHeight) / preset.height; // Lower score is better const score = ratioDiff * 2 + widthDiff + heightDiff; return { preset, score }; }).sort((a, b) => a.score - b.score); // Return top 3 recommendations return scored.slice(0, 3).map(s => s.preset); }; const openCreate = () => { const defaultPreset = BANNER_PRESETS[0]; setEditing({ name: '', is_active: true, placement: defaultPreset.value, width: defaultPreset.width, height: defaultPreset.height } as any); setImageResolution(null); setRecommendedPlacements([]); onOpen(); }; const openEdit = (s: AdminBanner) => { setEditing({ ...s }); setImageResolution(null); setRecommendedPlacements([]); onOpen(); }; const closeModal = () => { setEditing(null); setImageResolution(null); setRecommendedPlacements([]); if (fileInputRef.current) { fileInputRef.current.value = ''; } onClose(); }; const createMut = useMutation({ 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 }) => 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) => 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' }), }); const onSubmit = async () => { if (!editing) return; const payload = { name: editing.name || '', 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, height: (editing as any).height || undefined, }; if ((editing as any).id != null) { await updateMut.mutateAsync({ id: (editing as any).id, payload }); } else { await createMut.mutateAsync(payload); } }; const onUpload = async (file?: File | null) => { if (!file) return; setUploadingImage(true); try { // First, detect image resolution const img = new window.Image(); const imageLoadPromise = new Promise<{ width: number; height: number }>((resolve, reject) => { img.onload = () => resolve({ width: img.width, height: img.height }); img.onerror = reject; img.src = URL.createObjectURL(file); }); const resolution = await imageLoadPromise; setImageResolution(resolution); // Recommend placements based on resolution const recommended = recommendPlacement(resolution.width, resolution.height); setRecommendedPlacements(recommended); // Upload the file const res = await uploadFile(file); // Update editing state with uploaded URL setEditing((prev) => ({ ...(prev || {}), image_url: res.url })); // If no placement selected yet, auto-select the best recommendation if (!editing?.placement && recommended.length > 0) { const bestMatch = recommended[0]; setEditing((prev) => ({ ...(prev || {}), placement: bestMatch.value, width: bestMatch.width, height: bestMatch.height })); toast({ title: 'Obrázek nahrán', description: `Rozlišení: ${resolution.width}×${resolution.height}. Doporučeno umístění: ${bestMatch.label}`, status: 'success', duration: 6000 }); } else { toast({ title: 'Obrázek nahrán', description: `Rozlišení: ${resolution.width}×${resolution.height}`, status: 'success' }); } // Clean up object URL URL.revokeObjectURL(img.src); } catch (e: any) { toast({ title: 'Nahrání selhalo', description: e?.message || 'Zkuste to znovu', status: 'error' }); } finally { setUploadingImage(false); } }; const banners = data || []; return ( Bannery a reklamní plochy Správa bannerů a reklamních ploch zobrazovaných na webu. Můžete přidávat, upravovat a odebírat bannery. {isLoading && ( )} {!isLoading && banners.map((b: AdminBanner) => { const preset = getPreset((b as any).placement); return ( ); })}
Náhled Název Umístění Rozměry Aktivní Akce
Načítání…
{b.name} {b.name} {(b as any).click_url && ( {(b as any).click_url} )} {preset ? ( {preset.label} {preset.position} ) : ( - )} {(b as any).width && (b as any).height ? ( {(b as any).width} × {(b as any).height} ) : '-'} {b.is_active ? 'Ano' : 'Ne'} } onClick={() => openEdit(b)} /> } onClick={() => { if (b.id != null) deleteMut.mutate(b.id); }} />
{(editing as any)?.id ? 'Upravit banner' : 'Nový banner'} Název setEditing((prev) => ({ ...(prev as any), name: e.target.value }))} /> Odkaz (po kliku) setEditing((prev) => ({ ...(prev as any), click_url: e.target.value }))} placeholder="https://partner.cz" /> {/* Image resolution info */} {imageResolution && ( Rozlišení obrázku: {imageResolution.width} × {imageResolution.height} px Poměr stran: {(imageResolution.width / imageResolution.height).toFixed(2)}:1 )} {/* Recommended placements */} {recommendedPlacements.length > 0 && ( Doporučená umístění na základě rozlišení: {recommendedPlacements.map((preset, idx) => ( {idx === 0 ? 'Nejlepší' : `#${idx + 1}`} {preset.label} ({preset.width}×{preset.height}) {editing?.placement !== preset.value && ( )} ))} )} Umístění na webu {editing?.placement && (() => { const preset = getPreset((editing as any).placement); return preset ? ( {preset.description} ) : null; })()} {/* Placement dimensions display (read-only) */} {editing?.placement && (() => { const preset = getPreset((editing as any).placement); return preset ? ( Rozměry banneru: {preset.width} × {preset.height} px Poměr stran: {preset.aspectRatio.toFixed(2)}:1 • Pozice: {preset.position} ) : null; })()} Obrázek banneru {/* Preview */} {(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; return ( Náhled banneru: banner preview {preset && ( Zobrazení v pozici: {preset.label} )} ); })()} {/* Upload button */} {uploadingImage && } {!((editing as any)?.image_url) && ( Nahrajte obrázek pro automatické doporučení umístění )} Aktivní setEditing((prev) => ({ ...(prev as any), is_active: e.target.checked }))} />
); }; export default BannersAdminPage;