mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
544 lines
24 KiB
TypeScript
544 lines
24 KiB
TypeScript
import React, { useEffect, useMemo, useState } from 'react';
|
||
import AdminLayout from '../../layouts/AdminLayout';
|
||
import { Box, Heading, Button, SimpleGrid, FormControl, FormLabel, Input, HStack, VStack, Text, IconButton, useToast, Divider, Alert, AlertIcon, Badge, Tooltip, Checkbox, Image, Spinner, Link, Switch, ButtonGroup } from '@chakra-ui/react';
|
||
import { getAdminSettings, updateAdminSettings, AdminSettings } from '../../services/settings';
|
||
import { FiPlus, FiTrash2, FiSave } from 'react-icons/fi';
|
||
import { triggerPrefetch } from '../../services/admin/prefetch';
|
||
import { getCachedYouTube, YouTubeVideo } from '../../services/youtube';
|
||
|
||
export type AdminVideoItem = {
|
||
url: string;
|
||
title?: string;
|
||
length?: string;
|
||
uploaded_at?: string;
|
||
thumbnail_url?: string;
|
||
};
|
||
|
||
const emptyItem: AdminVideoItem = { url: '' };
|
||
|
||
const AdminVideosPage: React.FC = () => {
|
||
const [loading, setLoading] = useState(true);
|
||
const [saving, setSaving] = useState(false);
|
||
const [items, setItems] = useState<AdminVideoItem[]>([]);
|
||
const [videosSource, setVideosSource] = useState<'auto'|'manual'>('manual');
|
||
const [videosEnabled, setVideosEnabled] = useState<boolean>(true);
|
||
const toast = useToast();
|
||
|
||
// YouTube Scraper API integration state
|
||
const [channelInput, setChannelInput] = useState<string>('');
|
||
const [ytLoading, setYtLoading] = useState<boolean>(false);
|
||
const [ytError, setYtError] = useState<string>('');
|
||
const [ytVideos, setYtVideos] = useState<Array<{
|
||
video_id: string;
|
||
title: string;
|
||
length?: string;
|
||
thumbnail_url?: string;
|
||
views?: number;
|
||
views_text?: string;
|
||
published_text?: string;
|
||
published_date?: string;
|
||
}>>([]);
|
||
const [selectedIds, setSelectedIds] = useState<Record<string, boolean>>({});
|
||
|
||
// Auto source preview state (cached YouTube)
|
||
const [autoVideos, setAutoVideos] = useState<YouTubeVideo[]>([]);
|
||
const [autoLoading, setAutoLoading] = useState<boolean>(false);
|
||
const [autoError, setAutoError] = useState<string>('');
|
||
const [filter, setFilter] = useState<string>('');
|
||
|
||
// Derived flags
|
||
const hasChannel = useMemo(() => (channelInput || '').trim().length > 0, [channelInput]);
|
||
|
||
useEffect(() => {
|
||
let mounted = true;
|
||
(async () => {
|
||
try {
|
||
const s: AdminSettings = await getAdminSettings();
|
||
if (!mounted) return;
|
||
const vids = Array.isArray((s as any).videos_items) ? (s as any).videos_items as AdminVideoItem[] : [];
|
||
const legacy = Array.isArray((s as any).videos) ? ((s as any).videos as string[]).map((url) => ({ url })) : [];
|
||
setItems(vids.length ? vids : legacy);
|
||
const src = (s as any).videos_source;
|
||
if (src === 'auto' || src === 'manual') setVideosSource(src);
|
||
// Default enable if not explicitly set and there are any videos configured
|
||
const explicit = (s as any).videos_module_enabled;
|
||
const hasAny = (vids.length + legacy.length) > 0;
|
||
setVideosEnabled(typeof explicit === 'boolean' ? Boolean(explicit) : hasAny);
|
||
// Prefill channel handle from settings if available (social/youtube_url)
|
||
const ytUrl = (s as any).youtube_url || (s as any).social_youtube || '';
|
||
if (ytUrl) setChannelInput(ytUrl);
|
||
} catch (e) {
|
||
// ignore
|
||
} finally {
|
||
if (mounted) setLoading(false);
|
||
}
|
||
})();
|
||
return () => { mounted = false; };
|
||
}, []);
|
||
|
||
// Load cached YouTube videos for preview when auto source is active
|
||
useEffect(() => {
|
||
let mounted = true;
|
||
const run = async () => {
|
||
if (loading) return;
|
||
if (videosSource !== 'auto') return;
|
||
setAutoError('');
|
||
setAutoLoading(true);
|
||
try {
|
||
const payload = await getCachedYouTube();
|
||
if (!mounted) return;
|
||
setAutoVideos(payload?.videos || []);
|
||
} catch (err) {
|
||
if (!mounted) return;
|
||
setAutoError('Nepodařilo se načíst cache videí. Zkuste Aktualizovat.');
|
||
} finally {
|
||
if (mounted) setAutoLoading(false);
|
||
}
|
||
};
|
||
run();
|
||
return () => { mounted = false; };
|
||
}, [loading, videosSource]);
|
||
|
||
// Auto-disable videos module if there is neither channel nor manual items configured
|
||
useEffect(() => {
|
||
if (loading) return;
|
||
if (!hasChannel && items.length === 0 && videosEnabled) {
|
||
setVideosEnabled(false);
|
||
}
|
||
}, [loading, hasChannel, items.length, videosEnabled]);
|
||
|
||
// Auto-trigger backend prefetch of YouTube cache at most once per ~24h
|
||
useEffect(() => {
|
||
if (loading) return;
|
||
if (videosSource !== 'auto') return;
|
||
const channel = (channelInput || '').trim();
|
||
if (!channel) return;
|
||
const KEY = 'youtube_autoload_last';
|
||
let last = 0;
|
||
try { last = Number(localStorage.getItem(KEY) || '0'); } catch {}
|
||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||
const due = !last || (Date.now() - last) > (23 * 60 * 60 * 1000); // ~23h to allow slight drift
|
||
if (!due) return;
|
||
(async () => {
|
||
try {
|
||
// Ask backend to refresh cached files; it will update youtube_channel.json opportunistically
|
||
await triggerPrefetch();
|
||
try { localStorage.setItem(KEY, String(Date.now())); } catch {}
|
||
toast({ status: 'info', title: 'Aktualizace videí', description: 'Na pozadí se aktualizuje cache videí z YouTube.', duration: 3000 });
|
||
} catch {
|
||
// silent
|
||
}
|
||
})();
|
||
}, [loading, videosSource, channelInput, toast]);
|
||
|
||
const refreshAuto = async () => {
|
||
setAutoError('');
|
||
setAutoLoading(true);
|
||
try {
|
||
await triggerPrefetch();
|
||
const payload = await getCachedYouTube();
|
||
setAutoVideos(payload?.videos || []);
|
||
toast({ status: 'success', title: 'Aktualizováno', description: 'Cache videí byla obnovena.', duration: 3000 });
|
||
} catch (e) {
|
||
setAutoError('Aktualizace cache selhala.');
|
||
} finally {
|
||
setAutoLoading(false);
|
||
}
|
||
};
|
||
|
||
const fetchChannelVideos = async () => {
|
||
const channel = channelInput?.trim();
|
||
if (!channel) {
|
||
toast({ status: 'warning', title: 'Zadejte kanál', description: 'Zadejte YouTube handle nebo URL kanálu.' });
|
||
return;
|
||
}
|
||
setYtError('');
|
||
setYtLoading(true);
|
||
setYtVideos([]);
|
||
setSelectedIds({});
|
||
try {
|
||
const url = `https://youtube.tdvorak.dev/channel_videos?channel=${encodeURIComponent(channel)}`;
|
||
const res = await fetch(url, { method: 'GET' });
|
||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
const data = await res.json();
|
||
const vids = Array.isArray(data?.videos) ? data.videos : [];
|
||
setYtVideos(vids);
|
||
if (vids.length === 0) {
|
||
setYtError('Na kanálu nebyla nalezena žádná videa.');
|
||
}
|
||
} catch (err: any) {
|
||
console.error(err);
|
||
setYtError('Nepodařilo se načíst videa z API. Zkontrolujte dostupnost služby na https://youtube.tdvorak.dev/ a CORS.');
|
||
} finally {
|
||
setYtLoading(false);
|
||
}
|
||
};
|
||
|
||
const toggleSelect = (id: string) => {
|
||
setSelectedIds((prev) => ({ ...prev, [id]: !prev[id] }));
|
||
};
|
||
|
||
const importSelected = async () => {
|
||
const selected = ytVideos.filter((v) => selectedIds[v.video_id]);
|
||
if (selected.length === 0) {
|
||
toast({ status: 'info', title: 'Nic k importu', description: 'Vyberte alespoň jedno video.' });
|
||
return;
|
||
}
|
||
const newItems: AdminVideoItem[] = selected.map((v) => ({
|
||
url: `https://www.youtube.com/watch?v=${v.video_id}`,
|
||
title: v.title,
|
||
length: v.length,
|
||
uploaded_at: (v.published_date || '').slice(0,10),
|
||
thumbnail_url: v.thumbnail_url,
|
||
}));
|
||
// Avoid duplicates by URL
|
||
setItems((prev) => {
|
||
const urls = new Set(prev.map((p) => p.url));
|
||
const merged = [...prev];
|
||
for (const it of newItems) {
|
||
if (!urls.has(it.url)) {
|
||
merged.push(it);
|
||
urls.add(it.url);
|
||
}
|
||
}
|
||
return merged;
|
||
});
|
||
// If currently in auto mode, switch to manual so the preview reflects newly added items
|
||
if (videosSource !== 'manual') {
|
||
setVideosSource('manual');
|
||
try {
|
||
await updateAdminSettings({ videos_source: 'manual' });
|
||
toast({ status: 'info', title: 'Přepnuto na ruční správu', description: 'Nově přidaná videa se budou používat. Nezapomeňte uložit seznam.', duration: 3500 });
|
||
} catch {
|
||
// ignore
|
||
}
|
||
}
|
||
toast({ status: 'success', title: 'Videa přidána', description: `${selected.length} videí bylo přidáno do seznamu.` });
|
||
};
|
||
|
||
const addItem = async () => {
|
||
setItems((prev) => [...prev, { ...emptyItem }]);
|
||
if (videosSource !== 'manual') {
|
||
setVideosSource('manual');
|
||
try {
|
||
await updateAdminSettings({ videos_source: 'manual' });
|
||
} catch {
|
||
// ignore
|
||
}
|
||
}
|
||
};
|
||
const removeItem = (idx: number) => setItems((prev) => prev.filter((_, i) => i !== idx));
|
||
const updateField = (idx: number, key: keyof AdminVideoItem, val: string) => {
|
||
setItems((prev) => prev.map((it, i) => i === idx ? { ...it, [key]: val } : it));
|
||
};
|
||
|
||
const save = async () => {
|
||
setSaving(true);
|
||
try {
|
||
const clean = items.filter((it) => it.url && it.url.trim().length > 0);
|
||
await updateAdminSettings({ videos_items: clean, videos_module_enabled: videosEnabled });
|
||
toast({ status: 'success', title: 'Uloženo', description: 'Seznam videí byl uložen.' });
|
||
} catch (e) {
|
||
toast({ status: 'error', title: 'Chyba', description: 'Nepodařilo se uložit nastavení.' });
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const setDateQuick = (idx: number, daysAgo: number) => {
|
||
const d = new Date();
|
||
d.setDate(d.getDate() - daysAgo);
|
||
const iso = d.toISOString().slice(0,10);
|
||
setItems((prev) => prev.map((it, i) => i === idx ? { ...it, uploaded_at: iso } : it));
|
||
};
|
||
|
||
// Helper: derive YouTube thumbnail safely from a URL (supports youtube.com and youtu.be)
|
||
const getThumbFromUrl = (raw: string): string | undefined => {
|
||
try {
|
||
const u = raw.trim();
|
||
if (!u) return undefined;
|
||
if (u.includes('youtu.be/')) {
|
||
const id = u.split('youtu.be/')[1]?.split(/[?&#]/)[0];
|
||
return id ? `https://i.ytimg.com/vi/${id}/hqdefault.jpg` : undefined;
|
||
}
|
||
if (u.includes('youtube.com')) {
|
||
// Try URL API first
|
||
try {
|
||
const url = new URL(u);
|
||
const id = url.searchParams.get('v') || '';
|
||
if (id) return `https://i.ytimg.com/vi/${id}/hqdefault.jpg`;
|
||
} catch {}
|
||
// Fallback regex
|
||
const m = u.match(/[?&]v=([^&#]+)/);
|
||
const id = m?.[1];
|
||
return id ? `https://i.ytimg.com/vi/${id}/hqdefault.jpg` : undefined;
|
||
}
|
||
} catch {}
|
||
return undefined;
|
||
};
|
||
|
||
return (
|
||
<AdminLayout>
|
||
<Box>
|
||
<Heading size="md" mb={2}>Videa (pro titulní stránku)</Heading>
|
||
<Text fontSize="sm" color="gray.600" mb={2}>Přidejte až 5 videí (doporučeno). První se zobrazí jako hlavní, další 4 v mřížce. Podporováno YouTube/Vimeo URL.</Text>
|
||
|
||
{/* Source toggle */}
|
||
<HStack justify="space-between" mb={3} flexWrap="wrap">
|
||
<HStack>
|
||
<Text fontWeight="semibold">Zdroj videí:</Text>
|
||
<ButtonGroup size="sm" isAttached>
|
||
<Button
|
||
variant={videosSource === 'auto' ? 'solid' : 'outline'}
|
||
onClick={async () => {
|
||
if (videosSource === 'auto') return;
|
||
setVideosSource('auto');
|
||
try {
|
||
await updateAdminSettings({ videos_source: 'auto' });
|
||
toast({ status: 'success', title: 'Zdroj nastaven', description: 'Videa se načítají automaticky z YouTube.', duration: 2500 });
|
||
} catch {
|
||
toast({ status: 'error', title: 'Chyba', description: 'Nelze uložit zdroj videí.', duration: 3000 });
|
||
}
|
||
}}
|
||
>Automaticky</Button>
|
||
<Button
|
||
variant={videosSource === 'manual' ? 'solid' : 'outline'}
|
||
onClick={async () => {
|
||
if (videosSource === 'manual') return;
|
||
setVideosSource('manual');
|
||
try {
|
||
await updateAdminSettings({ videos_source: 'manual' });
|
||
toast({ status: 'success', title: 'Zdroj nastaven', description: 'Videa spravujete ručně.', duration: 2500 });
|
||
} catch {
|
||
toast({ status: 'error', title: 'Chyba', description: 'Nelze uložit zdroj videí.', duration: 3000 });
|
||
}
|
||
}}
|
||
>Ručně</Button>
|
||
</ButtonGroup>
|
||
</HStack>
|
||
<FormControl display="flex" alignItems="center" w="auto">
|
||
<FormLabel mb={0}>Zobrazit sekci Videa na titulní stránce</FormLabel>
|
||
<Switch
|
||
isChecked={videosEnabled}
|
||
isDisabled={loading || saving}
|
||
onChange={async (e) => {
|
||
const next = e.target.checked;
|
||
// Require either channel or at least one manual video to enable
|
||
if (next && !hasChannel && items.length === 0) {
|
||
setVideosEnabled(false);
|
||
toast({ status: 'warning', title: 'Doplňte kanál nebo videa', description: 'Pro zobrazení sekce vyplňte YouTube kanál nebo přidejte alespoň jedno video.', duration: 4000 });
|
||
// Try to focus channel input
|
||
setTimeout(() => {
|
||
const el = document.getElementById('admin-videos-channel-input') as HTMLInputElement | null;
|
||
if (el) el.focus();
|
||
}, 0);
|
||
return;
|
||
}
|
||
setVideosEnabled(next);
|
||
try {
|
||
await updateAdminSettings({ videos_module_enabled: next });
|
||
} catch {
|
||
toast({ status: 'error', title: 'Uložení selhalo', description: 'Nepodařilo se uložit změnu zobrazení sekce.', duration: 3000 });
|
||
}
|
||
}}
|
||
/>
|
||
</FormControl>
|
||
</HStack>
|
||
{!hasChannel && items.length === 0 && (
|
||
<Text fontSize="sm" color="orange.600" mb={2}>Pro aktivaci sekce vyplňte YouTube kanál nebo přidejte video.</Text>
|
||
)}
|
||
{videosSource === 'auto' && (
|
||
<Alert status="info" mb={3} borderRadius="md">
|
||
<AlertIcon />
|
||
Automatický režim je zapnutý. Videa se načítají z YouTube kanálu z Nastavení → Sociální sítě (YouTube URL) a správy „Videa (YouTube modul)“. Manuální seznam je v tomto režimu skryt.
|
||
</Alert>
|
||
)}
|
||
|
||
{videosSource !== 'auto' && (
|
||
<Box borderWidth="1px" borderRadius="md" p={3} mb={4}>
|
||
<Heading size="sm" mb={2}>Import z YouTube kanálu</Heading>
|
||
<Text fontSize="sm" color="gray.600" mb={3}>
|
||
Použijte Scraper API. Zadejte handle (např. <code>@FotbalKunovice</code>) nebo URL kanálu a načtěte videa z karty „Videa“.
|
||
Služba: <Link href="https://youtube.tdvorak.dev/" isExternal color="blue.500">https://youtube.tdvorak.dev/</Link>
|
||
</Text>
|
||
<HStack align="start" spacing={3} flexWrap="wrap">
|
||
<FormControl maxW={{ base: '100%', md: '400px' }}>
|
||
<FormLabel>Kanál (handle nebo URL)</FormLabel>
|
||
<Input id="admin-videos-channel-input" placeholder="@FCBizoniUH nebo https://www.youtube.com/@FCBizoniUH/videos" value={channelInput} onChange={(e) => setChannelInput(e.target.value)} />
|
||
</FormControl>
|
||
<Button onClick={fetchChannelVideos} isLoading={ytLoading} variant="outline" flexShrink={0} minW="max-content">Načíst videa</Button>
|
||
<Button colorScheme="green" onClick={importSelected} isDisabled={ytVideos.length === 0} flexShrink={0} minW="max-content">Přidat vybraná</Button>
|
||
</HStack>
|
||
{ytError && (
|
||
<Alert status="error" mt={3} borderRadius="md">
|
||
<AlertIcon />
|
||
{ytError}
|
||
</Alert>
|
||
)}
|
||
{ytLoading && (
|
||
<HStack mt={3} color="gray.600"><Spinner size="sm" /><Text>Načítám videa…</Text></HStack>
|
||
)}
|
||
{!ytLoading && ytVideos.length > 0 && (
|
||
<SimpleGrid columns={{ base: 1, sm: 2, md: 3, lg: 4 }} spacing={3} mt={3}>
|
||
{ytVideos.map((v) => (
|
||
<Box key={v.video_id} borderWidth="1px" borderRadius="md" p={2}>
|
||
<VStack align="stretch" spacing={2}>
|
||
<Image src={v.thumbnail_url} alt={v.title} borderRadius="md" />
|
||
<Checkbox isChecked={!!selectedIds[v.video_id]} onChange={() => toggleSelect(v.video_id)}>
|
||
Vybrat
|
||
</Checkbox>
|
||
<Box>
|
||
<Text fontWeight="semibold" noOfLines={2}>{v.title}</Text>
|
||
<HStack spacing={2} color="gray.600" fontSize="sm">
|
||
{v.length && <Badge>{v.length}</Badge>}
|
||
{v.published_text && <Text>{v.published_text}</Text>}
|
||
</HStack>
|
||
</Box>
|
||
</VStack>
|
||
</Box>
|
||
))}
|
||
</SimpleGrid>
|
||
)}
|
||
</Box>
|
||
)}
|
||
|
||
{/* Always-visible preview of effective videos */}
|
||
<Box borderWidth="1px" borderRadius="md" p={3} mb={4}>
|
||
<HStack justify="space-between" align="center" mb={2} flexWrap="wrap">
|
||
<Heading size="sm">Náhled: všechna videa (aktivní zdroj)</Heading>
|
||
{videosSource === 'auto' && (
|
||
<HStack spacing={2} flexWrap="wrap">
|
||
<Input size="sm" placeholder="Filtrovat podle názvu" value={filter} onChange={(e) => setFilter(e.target.value)} width={{ base: '100%', md: '260px' }} />
|
||
<Button size="sm" onClick={refreshAuto} isLoading={autoLoading} variant="outline" flexShrink={0} minW="max-content">Aktualizovat cache</Button>
|
||
</HStack>
|
||
)}
|
||
</HStack>
|
||
{videosSource === 'auto' ? (
|
||
<>
|
||
{autoError && (
|
||
<Alert status="error" mb={2} borderRadius="md"><AlertIcon />{autoError}</Alert>
|
||
)}
|
||
{autoLoading ? (
|
||
<HStack color="gray.600"><Spinner size="sm" /><Text>Načítám videa…</Text></HStack>
|
||
) : (
|
||
<>
|
||
<Text fontSize="sm" color="gray.600" mb={2}>Počet videí: {autoVideos.filter(v => v.title.toLowerCase().includes(filter.toLowerCase())).length}</Text>
|
||
<SimpleGrid columns={{ base: 1, sm: 2, md: 3, lg: 4 }} spacing={3}>
|
||
{autoVideos
|
||
.filter(v => v.title.toLowerCase().includes(filter.toLowerCase()))
|
||
.map((v) => (
|
||
<Box key={v.video_id} borderWidth="1px" borderRadius="md" p={2}>
|
||
<VStack align="stretch" spacing={2}>
|
||
<Image src={v.thumbnail_url} alt={v.title} borderRadius="md" />
|
||
<Box>
|
||
<Text fontWeight="semibold" noOfLines={2}>{v.title}</Text>
|
||
<HStack spacing={2} color="gray.600" fontSize="sm">
|
||
{v.published_date && <Badge>{new Date(v.published_date).toLocaleDateString('cs-CZ')}</Badge>}
|
||
</HStack>
|
||
</Box>
|
||
</VStack>
|
||
</Box>
|
||
))}
|
||
</SimpleGrid>
|
||
{autoVideos.length === 0 && (
|
||
<Text color="gray.600">Žádná videa v cache. Zkontrolujte YouTube URL v nastavení a použijte „Aktualizovat cache“.</Text>
|
||
)}
|
||
</>
|
||
)}
|
||
</>
|
||
) : (
|
||
<>
|
||
<Text fontSize="sm" color="gray.600" mb={2}>Počet videí: {items.length}</Text>
|
||
<SimpleGrid columns={{ base: 1, sm: 2, md: 3, lg: 4 }} spacing={3}>
|
||
{items.map((it, idx) => (
|
||
<Box key={`${idx}-${it.url}`} borderWidth="1px" borderRadius="md" p={2}>
|
||
<VStack align="stretch" spacing={2}>
|
||
<Image src={it.thumbnail_url || getThumbFromUrl(it.url)} alt={it.title || `Video ${idx+1}`} borderRadius="md" />
|
||
<Box>
|
||
<Text fontWeight="semibold" noOfLines={2}>{it.title || `Video ${idx+1}`}</Text>
|
||
<HStack spacing={2} color="gray.600" fontSize="sm">
|
||
{it.uploaded_at && <Badge>{(new Date(it.uploaded_at)).toLocaleDateString('cs-CZ')}</Badge>}
|
||
</HStack>
|
||
</Box>
|
||
</VStack>
|
||
</Box>
|
||
))}
|
||
</SimpleGrid>
|
||
{items.length === 0 && (
|
||
<Text color="gray.600">Zatím žádná videa.</Text>
|
||
)}
|
||
</>
|
||
)}
|
||
</Box>
|
||
|
||
<HStack justify="space-between" mb={3}>
|
||
<Button leftIcon={<FiPlus />} onClick={addItem}>Přidat video</Button>
|
||
<Button colorScheme="blue" leftIcon={<FiSave />} onClick={save} isLoading={saving}>Uložit</Button>
|
||
</HStack>
|
||
|
||
<Divider my={3} />
|
||
|
||
{loading ? (
|
||
<Text>Načítání…</Text>
|
||
) : videosSource === 'auto' ? (
|
||
<Text color="gray.600">Automatický zdroj videí je aktivní. Pro ruční správu přepněte zdroj na „Ručně“.</Text>
|
||
) : (
|
||
<VStack align="stretch" spacing={4}>
|
||
{items.map((it, idx) => (
|
||
<Box key={idx} borderWidth="1px" borderRadius="md" p={3}>
|
||
<HStack justify="space-between">
|
||
<Heading size="sm">Video #{idx + 1}</Heading>
|
||
</HStack>
|
||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={3} mt={3}>
|
||
<FormControl>
|
||
<FormLabel>URL videa</FormLabel>
|
||
<Input value={it.url} onChange={(e) => updateField(idx, 'url', e.target.value)} placeholder="https://www.youtube.com/watch?v=..." />
|
||
</FormControl>
|
||
<FormControl>
|
||
<FormLabel>Thumbnail (volitelné)</FormLabel>
|
||
<Input value={it.thumbnail_url || ''} onChange={(e) => updateField(idx, 'thumbnail_url', e.target.value)} placeholder="https://example.com/thumb.jpg" />
|
||
</FormControl>
|
||
<FormControl>
|
||
<FormLabel>Název (volitelné)</FormLabel>
|
||
<Input value={it.title || ''} onChange={(e) => updateField(idx, 'title', e.target.value)} placeholder="Titulek videa" />
|
||
</FormControl>
|
||
<FormControl>
|
||
<FormLabel>Délka (volitelné)</FormLabel>
|
||
<Input value={it.length || ''} onChange={(e) => updateField(idx, 'length', e.target.value)} placeholder="3:45" />
|
||
</FormControl>
|
||
<FormControl>
|
||
<FormLabel>Datum nahrání (volitelné)</FormLabel>
|
||
<HStack>
|
||
<Input type="date" value={(it.uploaded_at || '').slice(0,10)} onChange={(e) => updateField(idx, 'uploaded_at', e.target.value)} />
|
||
<Tooltip label="Dnes">
|
||
<Button size="sm" variant="outline" onClick={() => setDateQuick(idx, 0)}>Dnes</Button>
|
||
</Tooltip>
|
||
<Tooltip label="Včera">
|
||
<Button size="sm" variant="outline" onClick={() => setDateQuick(idx, 1)}>Včera</Button>
|
||
</Tooltip>
|
||
<Tooltip label="Před týdnem">
|
||
<Button size="sm" variant="outline" onClick={() => setDateQuick(idx, 7)}>−7 dní</Button>
|
||
</Tooltip>
|
||
<Tooltip label="Vymazat datum">
|
||
<Button size="sm" variant="ghost" onClick={() => updateField(idx, 'uploaded_at', '')}>Vymazat</Button>
|
||
</Tooltip>
|
||
</HStack>
|
||
</FormControl>
|
||
</SimpleGrid>
|
||
<HStack justify="flex-end" mt={2}>
|
||
<IconButton aria-label="Smazat" icon={<FiTrash2 />} onClick={() => removeItem(idx)} variant="outline" colorScheme="red" />
|
||
</HStack>
|
||
</Box>
|
||
))}
|
||
{items.length === 0 && (
|
||
<Text color="gray.600">Zatím žádná videa. Použijte tlačítko „Přidat video“.</Text>
|
||
)}
|
||
</VStack>
|
||
)}
|
||
</Box>
|
||
</AdminLayout>
|
||
);
|
||
};
|
||
|
||
export default AdminVideosPage;
|