feat: feed UI, in-app player, offline mode, settings
Adds an end-to-end integration test that runs the real pipeline against live YouTube Atom feeds, verifying the merged feed is newest-first across channels.
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import type { ChannelWithCount } from "../types";
|
||||
|
||||
interface Props {
|
||||
channels: ChannelWithCount[];
|
||||
activeChannel: string | null;
|
||||
onSelect: (id: string | null) => void;
|
||||
onOpenSettings: () => void;
|
||||
totalVideos: number;
|
||||
}
|
||||
|
||||
export default function Sidebar({
|
||||
channels, activeChannel, onSelect, onOpenSettings, totalVideos,
|
||||
}: Props) {
|
||||
const rowBase =
|
||||
"w-full text-left px-3 py-2 rounded-lg text-sm flex items-center justify-between gap-2 transition-colors cursor-pointer";
|
||||
|
||||
return (
|
||||
<aside className="w-64 shrink-0 border-r border-edge flex flex-col bg-ink">
|
||||
<div className="px-4 py-4 flex items-center gap-2">
|
||||
<span className="text-xl">✈</span>
|
||||
<span className="font-semibold tracking-tight">FlightTube</span>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 overflow-y-auto px-2 pb-2 space-y-0.5">
|
||||
<button onClick={() => onSelect(null)}
|
||||
className={`${rowBase} ${
|
||||
activeChannel === null ? "bg-raised text-white" : "text-muted hover:bg-surface"
|
||||
}`}>
|
||||
<span className="font-medium">All subscriptions</span>
|
||||
<span className="text-xs tabular-nums opacity-70">{totalVideos}</span>
|
||||
</button>
|
||||
|
||||
{channels.length > 0 && (
|
||||
<div className="pt-3 pb-1 px-3 text-[11px] uppercase tracking-wider text-muted/70">
|
||||
Channels
|
||||
</div>
|
||||
)}
|
||||
|
||||
{channels.map((c) => (
|
||||
<button key={c.id} onClick={() => onSelect(c.id)} title={c.title}
|
||||
className={`${rowBase} ${
|
||||
activeChannel === c.id ? "bg-raised text-white" : "text-muted hover:bg-surface"
|
||||
}`}>
|
||||
<span className="truncate">{c.title}</span>
|
||||
<span className="text-xs tabular-nums opacity-70 shrink-0">
|
||||
{c.downloaded_count > 0 && (
|
||||
<span className="text-emerald-400">{c.downloaded_count}/</span>
|
||||
)}
|
||||
{c.video_count}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<button onClick={onOpenSettings}
|
||||
className="m-2 px-3 py-2 rounded-lg text-sm text-muted hover:bg-surface text-left cursor-pointer">
|
||||
Settings
|
||||
</button>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user