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,523 @@
|
||||
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 = () => {
|
||||
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;
|
||||
});
|
||||
toast({ status: 'success', title: 'Videa přidána', description: `${selected.length} videí bylo přidáno do seznamu.` });
|
||||
};
|
||||
|
||||
const addItem = () => setItems((prev) => [...prev, { ...emptyItem }]);
|
||||
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}>
|
||||
<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">Načíst videa</Button>
|
||||
<Button colorScheme="green" onClick={importSelected} isDisabled={ytVideos.length === 0}>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}>
|
||||
<Input size="sm" placeholder="Filtrovat podle názvu" value={filter} onChange={(e) => setFilter(e.target.value)} />
|
||||
<Button size="sm" onClick={refreshAuto} isLoading={autoLoading} variant="outline">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;
|
||||
Reference in New Issue
Block a user