import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { getDiscovery, getDiscoveryVideos, followDiscovery, dismissDiscovery, dismissDiscoveryVideo, refreshDiscovery, getDiscoveryStatus, } from "../api"; import VideoCard from "../components/VideoCard"; import { scrollToTop } from "../utils/scroll"; const PAGE_SIZE = 50; const SOURCE_LABELS = { search: "Based on your channels", graph: "Related to channels you follow", community: "Popular with other users", category: "Similar category", liked: "Related to videos you liked", }; function formatSubs(n) { if (!n) return null; if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n >= 10_000_000 ? 0 : 1)}M`; if (n >= 1_000) return `${Math.round(n / 1_000)}K`; return String(n); } function avatarColor(name) { if (!name) return "#52525b"; let h = 0; for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) % 360; return `hsl(${h}, 55%, 42%)`; } function ChannelCard({ item }) { const navigate = useNavigate(); const qc = useQueryClient(); const [gone, setGone] = useState(false); const followMut = useMutation({ mutationFn: () => followDiscovery(item.channel_id), onSuccess: () => { qc.invalidateQueries({ queryKey: ["channels"] }); setTimeout(() => { setGone(true); qc.invalidateQueries({ queryKey: ["discovery"] }); qc.invalidateQueries({ queryKey: ["discovery-videos"] }); }, 600); }, }); const dismissMut = useMutation({ mutationFn: () => dismissDiscovery(item.channel_id), onSuccess: () => { setTimeout(() => { setGone(true); qc.invalidateQueries({ queryKey: ["discovery"] }); qc.invalidateQueries({ queryKey: ["discovery-videos"] }); }, 300); }, }); const [featured, ...rest] = item.preview_videos ?? []; const subs = formatSubs(item.subscriber_count); const busy = followMut.isPending || dismissMut.isPending; if (gone) return null; return (
{/* Featured thumbnail */} {featured ? (
navigate(`/channels/${item.channel_id}`)} > {featured.title}
{item.thumbnail_url ? ( {item.name} ) : (
{item.name?.[0]?.toUpperCase()}
)}
) : (
navigate(`/channels/${item.channel_id}`)} > {item.thumbnail_url ? ( {item.name} ) : (
{item.name?.[0]?.toUpperCase()}
)}
)} {/* Info */}

{[subs && `${subs} subscribers`, SOURCE_LABELS[item.source] ?? "Recommended"].filter(Boolean).join(" · ")}

{!followMut.isSuccess && ( )}
{rest.length > 0 && (
    {rest.slice(0, 2).map((v, i) => (
  • · {v.title}
  • ))}
)} {!featured && item.description && (

{item.description}

)}
{followMut.isSuccess ? (

Following ✓

) : ( )}
); } function Tab({ active, onClick, children, count }) { return ( ); } export default function DiscoveryPage() { const qc = useQueryClient(); const [tab, setTab] = useState("channels"); const [channelPage, setChannelPage] = useState(0); const [videoPage, setVideoPage] = useState(0); const [dismissedVideos, setDismissedVideos] = useState(new Set()); const { data: channels = [], isLoading: loadingChannels } = useQuery({ queryKey: ["discovery", channelPage], queryFn: () => getDiscovery(channelPage * PAGE_SIZE, PAGE_SIZE).then((r) => r.data), staleTime: 0, placeholderData: (prev) => prev, }); const { data: videos = [], isLoading: loadingVideos } = useQuery({ queryKey: ["discovery-videos", videoPage], queryFn: () => getDiscoveryVideos(videoPage * PAGE_SIZE, PAGE_SIZE).then((r) => r.data), staleTime: 0, placeholderData: (prev) => prev, }); const { data: discStatus } = useQuery({ queryKey: ["discovery-status"], queryFn: () => getDiscoveryStatus().then(r => r.data), staleTime: 10_000, refetchInterval: (query) => (query.state.data?.progress?.running ? 10_000 : 60_000), }); const refreshMut = useMutation({ mutationFn: refreshDiscovery, onSuccess: () => { // Discovery runs as a background job and takes several minutes. // Invalidate status immediately so the "queued" state shows, then // re-check every 2 minutes until results land. qc.invalidateQueries({ queryKey: ["discovery-status"] }); }, }); const handleDismissVideo = (video) => { setDismissedVideos(prev => new Set([...prev, video.youtube_video_id])); dismissDiscoveryVideo(video.youtube_video_id).then(() => { qc.invalidateQueries({ queryKey: ["discovery-videos"] }); }); }; const visibleVideos = videos.filter(v => !dismissedVideos.has(v.youtube_video_id)); const isEmpty = tab === "channels" ? channels.length === 0 : visibleVideos.length === 0; const isLoading = tab === "channels" ? loadingChannels : loadingVideos; const hasNextChannelPage = channels.length === PAGE_SIZE; const hasNextVideoPage = videos.length === PAGE_SIZE; return (
{/* Header */}

Discover

{discStatus && (

{discStatus.pending_count > 0 ? `${discStatus.pending_count} channel${discStatus.pending_count !== 1 ? "s" : ""} queued` : "Queue empty"} {discStatus.last_run ? ` · last refreshed ${new Date(discStatus.last_run + "Z").toLocaleDateString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })}` : " · never refreshed"}

)}
{refreshMut.isSuccess && !discStatus?.progress?.running && (
Discovery is running — progress shows in the top bar. Searches are spaced out over ~20 minutes. Runs automatically every day.
)} {/* Tabs */}
setTab("channels")} count={0}> Channels setTab("videos")} count={0}> Videos
{/* Content */} {isLoading ? (
) : isEmpty ? (

Nothing here yet

Follow a few channels first, then hit "Find more" to discover similar ones.

) : tab === "channels" ? ( <>
{channels.map((item) => ( ))}
Page {channelPage + 1}
) : ( <>
{visibleVideos.map((v) => ( ))}
Page {videoPage + 1}
)}
); }