import { useState, useMemo } from "react"; import { useParams } from "react-router-dom"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { getChannel, getChannelVideos, followChannel, unfollowChannel, indexChannel, downloadChannel } from "../api"; import VideoCard from "../components/VideoCard"; import SortPicker from "../components/SortPicker"; 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); } const VIDEO_SORTS = [ { value: "newest", label: "Newest" }, { value: "oldest", label: "Oldest" }, { value: "title", label: "Title A–Z" }, { value: "unwatched", label: "Unwatched first" }, ]; function sortVideos(items, sort) { const arr = [...items]; if (sort === "oldest") return arr.sort((a, b) => new Date(a.published_at ?? 0) - new Date(b.published_at ?? 0)); if (sort === "title") return arr.sort((a, b) => (a.title ?? "").localeCompare(b.title ?? "")); if (sort === "unwatched") return arr.sort((a, b) => Number(a.is_watched) - Number(b.is_watched)); return arr.sort((a, b) => new Date(b.published_at ?? 0) - new Date(a.published_at ?? 0)); } export default function ChannelPage() { const { id } = useParams(); const qc = useQueryClient(); const { data: channel, isLoading: loadingChannel } = useQuery({ queryKey: ["channel", id], queryFn: () => getChannel(id).then((r) => r.data), }); const { data: videos, isLoading: loadingVideos } = useQuery({ queryKey: ["channel-videos", id], queryFn: () => getChannelVideos(id).then((r) => r.data), }); const followMut = useMutation({ mutationFn: () => channel?.status === "followed" ? unfollowChannel(id) : followChannel(id), onSuccess: () => { qc.invalidateQueries({ queryKey: ["channel", id] }); qc.invalidateQueries({ queryKey: ["channels"] }); }, }); const indexMut = useMutation({ mutationFn: () => indexChannel(id), onSuccess: () => setTimeout(() => qc.invalidateQueries({ queryKey: ["channel-videos", id] }), 4000), }); const [dlResult, setDlResult] = useState(null); const [videoSort, setVideoSort] = useState("newest"); const dlMut = useMutation({ mutationFn: () => downloadChannel(id), onSuccess: (res) => { setDlResult(res.data.queued); qc.invalidateQueries({ queryKey: ["downloads"] }); }, }); if (loadingChannel) { return (
); } if (!channel) return

Channel not found.

; const isFollowed = channel.status === "followed"; return (
{/* Channel header — banner with overlay, or plain if no banner */}
{channel.banner_url && ( )} {/* Gradient overlay */}
{/* Info row sits at the bottom of the banner */}
{/* Avatar */} {channel.thumbnail_url ? ( {channel.name} ) : (
{channel.name?.[0]?.toUpperCase()}
)} {/* Name + meta */}

{channel.name}

{[ formatSubs(channel.subscriber_count) && `${formatSubs(channel.subscriber_count)} subscribers`, `${channel.video_count} videos indexed`, ].filter(Boolean).join(" · ")}

{/* Action buttons */}
{dlResult != null && ( {dlResult === 0 ? "Already up to date" : `${dlResult} queued`} )}
{/* Description below banner */} {channel.description && (

{channel.description}

)} {/* Video grid */} {loadingVideos ? (
) : videos?.length ? ( <>
{sortVideos(videos, videoSort).map((v) => ( ))}
) : (

No videos indexed yet. Hit Re-index to fetch them.

)}
); }