import { Box, Button, Heading, HStack, IconButton, Image, Input, Link, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Table, Tbody, Td, Th, Thead, Tr, FormControl, FormLabel, Switch, useDisclosure, useToast, VStack, useColorModeValue, Text, Select, NumberInput, NumberInputField, Badge, } from '@chakra-ui/react'; import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { FiEdit2, FiPlus, FiTrash2, FiUpload, FiExternalLink } from 'react-icons/fi'; import AdminLayout from '../../layouts/AdminLayout'; import { Sponsor, getSponsors, createSponsor, updateSponsor, deleteSponsor } from '../../services/sponsors'; import { uploadFile } from '../../services/articles'; const SponsorsAdminPage: React.FC = () => { const cardBg = useColorModeValue('white', 'gray.800'); const borderColor = useColorModeValue('gray.200', 'gray.700'); const inputBg = useColorModeValue('white', 'gray.700'); const normalizeImageUrl = (url?: string) => { if (!url || url === '') return '/logo192.png'; if (/^https?:\/\//i.test(url)) return url; const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; const origin = new URL(apiUrl).origin; if (url.startsWith('/uploads/')) return `${origin}${url}`; return `${origin}${url.startsWith('/') ? '' : '/'}${url}`; }; const toast = useToast(); const qc = useQueryClient(); const { data, isLoading } = useQuery({ queryKey: ['admin-sponsors'], queryFn: getSponsors }); const [editing, setEditing] = useState | null>(null); const { isOpen, onOpen, onClose } = useDisclosure(); const openCreate = () => { setEditing({ name: '', is_active: true, tier: 'standard', display_order: 0 }); onOpen(); }; const openEdit = (s: Sponsor) => { setEditing({ ...s }); onOpen(); }; const closeModal = () => { setEditing(null); onClose(); }; const createMut = useMutation({ mutationFn: (payload: any) => createSponsor(payload), onSuccess: (created: any) => { // If backend returned the created sponsor, merge into cache for immediate UI update try { qc.setQueryData(['admin-sponsors'], (old: any) => { const list = Array.isArray(old) ? old : (old?.data || []); const newList = [created, ...list]; // if cache shape is { data, total, page }, attempt to preserve if (old && old.data) return { ...old, data: newList }; return newList; }); } catch (e) { // ignore cache update errors } toast({ title: 'Sponzor vytvořen', status: 'success' }); qc.invalidateQueries({ queryKey: ['admin-sponsors'] }); closeModal(); }, onError: (e: any) => toast({ title: 'Vytvoření selhalo', description: e?.response?.data?.chyba || 'Chyba', status: 'error' }), }); const updateMut = useMutation({ mutationFn: ({ id, payload }: { id: number | string; payload: any }) => updateSponsor(id, payload), onSuccess: (updated: any) => { // Optimistically merge returned sponsor into cache to avoid transient undefined fields try { qc.setQueryData(['admin-sponsors'], (old: any) => { const list = Array.isArray(old) ? old : (old?.data || []); const newList = (list || []).map((it: any) => (it?.id === updated?.id ? { ...it, ...updated } : it)); if (old && old.data) return { ...old, data: newList }; return newList; }); } catch {} toast({ title: 'Sponzor aktualizován', status: 'success' }); qc.invalidateQueries({ queryKey: ['admin-sponsors'] }); closeModal(); }, onError: (e: any) => toast({ title: 'Aktualizace selhala', description: e?.response?.data?.chyba || 'Chyba', status: 'error' }), }); const deleteMut = useMutation({ mutationFn: (id: number) => deleteSponsor(id), onSuccess: () => { toast({ title: 'Sponzor smazán', status: 'success' }); qc.invalidateQueries({ queryKey: ['admin-sponsors'] }); }, onError: (e: any) => toast({ title: 'Smazání selhalo', description: e?.response?.data?.chyba || 'Chyba', status: 'error' }), }); const onSubmit = async () => { if (!editing) return; const payload = { name: editing.name || '', website_url: editing.website_url || '', logo_url: editing.logo_url || '', is_active: editing.is_active ?? true, tier: editing.tier || 'standard', display_order: editing.display_order ?? 0, }; 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; try { const res = await uploadFile(file); const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; const apiOrigin = new URL(apiUrl).origin; // If backend returned an absolute URL pointing to the dev host (same origin as app), rewrite to API origin let url = res.url || ''; try { const parsed = new URL(url, window.location.origin); const appOrigin = window.location.origin; if (parsed.origin === appOrigin) { // replace with API origin while keeping path url = apiOrigin + parsed.pathname + parsed.search + parsed.hash; } } catch (e) { // ignore } setEditing((prev) => ({ ...(prev || {}), logo_url: url })); toast({ title: 'Logo nahráno', status: 'success' }); } catch (err) { toast({ title: 'Nahrání loga selhalo', status: 'error' }); } }; const toggleActive = async (s: Sponsor) => { if (s.id == null) return; try { await updateMut.mutateAsync({ id: s.id, payload: { is_active: !s.is_active } }); toast({ title: 'Stav sponzora aktualizován', status: 'success' }); } catch (err) { toast({ title: 'Aktualizace selhala', status: 'error' }); } }; return ( Správa sponzorů Správa sponzorů a partnerů klubu. Můžete přidávat, upravovat a odebírat sponzory, kteří se zobrazují na webu. {isLoading && ()} {!isLoading && (data || []).map((s) => ( ))}
Logo Název Úroveň Pořadí Web Aktivní Akce
Načítám...
{s.name} {s.name} {s.tier === 'general' ? 'Hlavní partner' : 'Partner'} {s.display_order ?? 0} {s.website_url ? ( {s.website_url} ) : '-'} toggleActive(s)} /> } size="sm" onClick={() => openEdit(s)} /> } size="sm" colorScheme="red" variant="outline" onClick={async () => { if (!window.confirm(`Opravdu chcete smazat sponzora "${s.name}"?`)) return; try { await deleteMut.mutateAsync(s.id as number); toast({ title: 'Sponzor smazán', status: 'success' }); } catch (err) { toast({ title: 'Smazání sponzora selhalo', status: 'error' }); } }} />
{(editing as any)?.id ? 'Upravit sponzora' : 'Nový sponzor'} Název setEditing((p) => ({ ...(p as any), name: e.target.value }))} /> Web (URL) setEditing((p) => ({ ...(p as any), website_url: e.target.value }))} /> Logo logo Úroveň partnera Pořadí zobrazení setEditing((p) => ({ ...(p as any), display_order: parseInt(val) || 0 }))} min={0}> Menší číslo = vyšší pozice Aktivní setEditing((p) => ({ ...(p as any), is_active: e.target.checked }))} />
); }; export default SponsorsAdminPage;