"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import TopBar from "@/components/TopBar"; import VideoCard from "@/components/VideoCard"; import FilterPanel from "@/components/FilterPanel"; import { fetchVideos, fetchStatus, fetchConfig, triggerRefresh, type Video, type Filters } from "@/lib/api"; interface FilterState { min_score: number; days: number; } const DEFAULT_FILTERS: FilterState = { min_score: 0, days: 30 }; const PAGE_SIZE = 18; export default function HomePage() { const [videos, setVideos] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [filters, setFilters] = useState(DEFAULT_FILTERS); const [search, setSearch] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(""); const [loading, setLoading] = useState(true); const [initialQueries, setInitialQueries] = useState([]); const [launching, setLaunching] = useState(false); const [activeQueryFilters, setActiveQueryFilters] = useState([]); const [dark, setDark] = useState(false); // Lire la préférence stockée au montage useEffect(() => { const stored = localStorage.getItem("theme"); const isDark = stored === "dark" || (!stored && window.matchMedia("(prefers-color-scheme: dark)").matches); setDark(isDark); document.documentElement.classList.toggle("dark", isDark); }, []); const handleToggleDark = () => { setDark((prev) => { const next = !prev; document.documentElement.classList.toggle("dark", next); localStorage.setItem("theme", next ? "dark" : "light"); return next; }); }; const [status, setStatus] = useState<{ video_count: number; last_updated: string | null; refresh_running: boolean; quota_exceeded: boolean; quota_exceeded_at: string | null }>({ video_count: 0, last_updated: null, refresh_running: false, quota_exceeded: false, quota_exceeded_at: null, }); const pollRef = useRef | null>(null); const loadVideos = useCallback(async (f: FilterState, p: number, q: string, sqFilters: string[]) => { setLoading(true); try { const params: Filters = { q: q || undefined, min_score: f.min_score, days: f.days, page: p, page_size: PAGE_SIZE, source_queries: sqFilters.length > 0 ? sqFilters : undefined, }; const result = await fetchVideos(params); setVideos(result.items); setTotal(result.total); } catch { setVideos([]); } finally { setLoading(false); } }, []); // Charger config + statut au montage useEffect(() => { fetchConfig().then((c) => setInitialQueries(c.queries)).catch(() => {}); fetchStatus().then(setStatus).catch(() => {}); }, []); // Debounce de la recherche texte (300 ms) useEffect(() => { const timer = setTimeout(() => { setDebouncedSearch(search); setPage(1); }, 300); return () => clearTimeout(timer); }, [search]); // Charger vidéos à chaque changement de filtres / page / recherche / chips useEffect(() => { loadVideos(filters, page, debouncedSearch, activeQueryFilters); }, [filters, page, debouncedSearch, activeQueryFilters, loadVideos]); const handleFilterChange = (partial: Partial) => { setFilters((prev) => ({ ...prev, ...partial })); setPage(1); }; const handleQueryFilterToggle = (q: string) => { setActiveQueryFilters((prev) => prev.includes(q) ? prev.filter((x) => x !== q) : [...prev, q] ); setPage(1); }; const handleLaunch = async (queries: string[]) => { setLaunching(true); try { await triggerRefresh(queries); // Polling jusqu'à la fin du refresh pollRef.current = setInterval(async () => { try { const s = await fetchStatus(); setStatus(s); if (!s.refresh_running) { clearInterval(pollRef.current!); pollRef.current = null; setLaunching(false); setPage(1); loadVideos(filters, 1, debouncedSearch, activeQueryFilters); } } catch { clearInterval(pollRef.current!); pollRef.current = null; setLaunching(false); } }, 2000); } catch { setLaunching(false); } }; // Nettoyage du polling au démontage useEffect(() => () => { if (pollRef.current) clearInterval(pollRef.current); }, []); const totalPages = Math.ceil(total / PAGE_SIZE); // Tous les filtres (texte + chips) sont gérés côté backend. const displayedVideos = videos; return (

▶ YTVeille

{total} vidéo{total > 1 ? "s" : ""}
{launching && (
⟳ Recherche YouTube en cours, veuillez patienter...
)} {status.quota_exceeded && !launching && (
⚠️
Quota YouTube API dépassé {status.quota_exceeded_at && ( <> — depuis le {new Intl.DateTimeFormat("fr-FR", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit", }).format(new Date(status.quota_exceeded_at))} )}
Le quota se renouvelle chaque jour à minuit (heure du Pacifique, ~8h–9h UTC). Réessayez demain.
)} {loading ? (
{Array.from({ length: 6 }).map((_, i) => (
))}
) : displayedVideos.length === 0 ? (

Aucune vidéo trouvée.

Configurez vos requêtes et lancez la veille depuis le panneau gauche.

) : (
{displayedVideos.map((v) => ( ))}
)} {/* Pagination */} {totalPages > 1 && ( )}
); }