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([]); const [videosSource, setVideosSource] = useState<'auto'|'manual'>('manual'); const [videosEnabled, setVideosEnabled] = useState(true); const toast = useToast(); // YouTube Scraper API integration state const [channelInput, setChannelInput] = useState(''); const [ytLoading, setYtLoading] = useState(false); const [ytError, setYtError] = useState(''); const [ytVideos, setYtVideos] = useState>([]); const [selectedIds, setSelectedIds] = useState>({}); // Auto source preview state (cached YouTube) const [autoVideos, setAutoVideos] = useState([]); const [autoLoading, setAutoLoading] = useState(false); const [autoError, setAutoError] = useState(''); const [filter, setFilter] = useState(''); // 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 ( Videa (pro titulní stránku) Přidejte až 5 videí (doporučeno). První se zobrazí jako hlavní, další 4 v mřížce. Podporováno YouTube/Vimeo URL. {/* Source toggle */} Zdroj videí: Zobrazit sekci Videa na titulní stránce { 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 }); } }} /> {!hasChannel && items.length === 0 && ( Pro aktivaci sekce vyplňte YouTube kanál nebo přidejte video. )} {videosSource === 'auto' && ( 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. )} {videosSource !== 'auto' && ( Import z YouTube kanálu Použijte Scraper API. Zadejte handle (např. @FotbalKunovice) nebo URL kanálu a načtěte videa z karty „Videa“. Služba: https://youtube.tdvorak.dev/ Kanál (handle nebo URL) setChannelInput(e.target.value)} /> {ytError && ( {ytError} )} {ytLoading && ( Načítám videa… )} {!ytLoading && ytVideos.length > 0 && ( {ytVideos.map((v) => ( {v.title} toggleSelect(v.video_id)}> Vybrat {v.title} {v.length && {v.length}} {v.published_text && {v.published_text}} ))} )} )} {/* Always-visible preview of effective videos */} Náhled: všechna videa (aktivní zdroj) {videosSource === 'auto' && ( setFilter(e.target.value)} width={{ base: '100%', md: '260px' }} /> )} {videosSource === 'auto' ? ( <> {autoError && ( {autoError} )} {autoLoading ? ( Načítám videa… ) : ( <> Počet videí: {autoVideos.filter(v => v.title.toLowerCase().includes(filter.toLowerCase())).length} {autoVideos .filter(v => v.title.toLowerCase().includes(filter.toLowerCase())) .map((v) => ( {v.title} {v.title} {v.published_date && {new Date(v.published_date).toLocaleDateString('cs-CZ')}} ))} {autoVideos.length === 0 && ( Žádná videa v cache. Zkontrolujte YouTube URL v nastavení a použijte „Aktualizovat cache“. )} )} ) : ( <> Počet videí: {items.length} {items.map((it, idx) => ( {it.title {it.title || `Video ${idx+1}`} {it.uploaded_at && {(new Date(it.uploaded_at)).toLocaleDateString('cs-CZ')}} ))} {items.length === 0 && ( Zatím žádná videa. )} )} {loading ? ( Načítání… ) : videosSource === 'auto' ? ( Automatický zdroj videí je aktivní. Pro ruční správu přepněte zdroj na „Ručně“. ) : ( {items.map((it, idx) => ( Video #{idx + 1} URL videa updateField(idx, 'url', e.target.value)} placeholder="https://www.youtube.com/watch?v=..." /> Thumbnail (volitelné) updateField(idx, 'thumbnail_url', e.target.value)} placeholder="https://example.com/thumb.jpg" /> Název (volitelné) updateField(idx, 'title', e.target.value)} placeholder="Titulek videa" /> Délka (volitelné) updateField(idx, 'length', e.target.value)} placeholder="3:45" /> Datum nahrání (volitelné) updateField(idx, 'uploaded_at', e.target.value)} /> } onClick={() => removeItem(idx)} variant="outline" colorScheme="red" /> ))} {items.length === 0 && ( Zatím žádná videa. Použijte tlačítko „Přidat video“. )} )} ); }; export default AdminVideosPage;