import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { Check, ChevronDown, Eye, EyeOff, GripVertical, Pencil, RotateCcw, X, } from "lucide-react"; import { closestCenter, DndContext, PointerSensor, useSensor, useSensors, type DragEndEvent, } from "@dnd-kit/core"; import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { api, type FeedFilters, type Tag } from "../lib/api"; import { DEFAULT_LAYOUT, WIDGET_TITLES, type SidebarLayout, type WidgetId, } from "../lib/sidebarLayout"; const SORTS = [ { id: "newest", label: "Newest" }, { id: "oldest", label: "Oldest" }, { id: "views", label: "Most viewed" }, { id: "duration_desc", label: "Longest" }, { id: "duration_asc", label: "Shortest" }, { id: "title", label: "Name (A–Z)" }, { id: "subscribers", label: "Channel subscribers" }, { id: "shuffle", label: "Surprise me" }, ]; const SHOWS = [ { id: "unwatched", label: "Unwatched" }, { id: "all", label: "All" }, { id: "watched", label: "Watched" }, { id: "saved", label: "Saved" }, { id: "hidden", label: "Hidden" }, ]; const DATE_PRESETS: { days: number; label: string }[] = [ { days: 1, label: "24h" }, { days: 7, label: "1 week" }, { days: 30, label: "1 month" }, { days: 180, label: "6 months" }, { days: 365, label: "1 year" }, ]; // Filter values owned by the sidebar (everything except the header search `q`). const DEFAULT_SIDEBAR_FILTERS: Omit = { tags: [], tagMode: "or", sort: "newest", includeNormal: true, includeShorts: false, includeLive: false, show: "unwatched", }; function TagChip({ tag, active, onClick, }: { tag: Tag; active: boolean; onClick: () => void; }) { return ( ); } export default function Sidebar({ filters, setFilters, layout, setLayout, }: { filters: FeedFilters; setFilters: (f: FeedFilters) => void; layout: SidebarLayout; setLayout: (l: SidebarLayout) => void; }) { const tagsQuery = useQuery({ queryKey: ["tags"], queryFn: api.tags }); const tags = tagsQuery.data ?? []; const languages = tags.filter((t) => t.category === "language"); const topics = tags.filter((t) => t.category === "topic"); const [customDates, setCustomDates] = useState(false); const [editing, setEditing] = useState(false); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 4 } }) ); function toggleTag(id: number) { const has = filters.tags.includes(id); setFilters({ ...filters, tags: has ? filters.tags.filter((t) => t !== id) : [...filters.tags, id], }); } const dateActive = !!filters.maxAgeDays || !!filters.dateFrom || !!filters.dateTo; const contentChanged = !filters.includeNormal || filters.includeShorts || filters.includeLive; const activeCount = filters.tags.length + (filters.show !== "unwatched" ? 1 : 0) + (filters.sort !== "newest" ? 1 : 0) + (contentChanged ? 1 : 0) + (filters.channelId ? 1 : 0) + (dateActive ? 1 : 0); const active = activeCount > 0; function clearAll() { setCustomDates(false); setFilters({ ...DEFAULT_SIDEBAR_FILTERS, q: filters.q }); } const available: Record = { show: true, sort: true, date: true, content: true, language: languages.length > 0, topic: topics.length > 0, }; function toggleCollapse(id: WidgetId) { setLayout({ ...layout, collapsed: { ...layout.collapsed, [id]: !layout.collapsed[id] } }); } function toggleHidden(id: WidgetId) { setLayout({ ...layout, hidden: { ...layout.hidden, [id]: !layout.hidden[id] } }); } function onDragEnd(e: DragEndEvent) { const { active: a, over } = e; if (!over || a.id === over.id) return; const oldIndex = layout.order.indexOf(a.id as WidgetId); const newIndex = layout.order.indexOf(over.id as WidgetId); if (oldIndex < 0 || newIndex < 0) return; setLayout({ ...layout, order: arrayMove(layout.order, oldIndex, newIndex) }); } function widgetBody(id: WidgetId): React.ReactNode { switch (id) { case "show": return (
{SHOWS.map((s) => ( ))}
); case "sort": return ( ); case "date": return ( <>
{DATE_PRESETS.map((p) => { const activePreset = filters.maxAgeDays === p.days && !customDates; return ( ); })}
{(customDates || filters.dateFrom || filters.dateTo) && (
{(filters.dateFrom || filters.dateTo) && ( )}
)} ); case "content": return ( <> setFilters({ ...filters, includeNormal: v })} /> setFilters({ ...filters, includeShorts: v })} /> setFilters({ ...filters, includeLive: v })} /> ); case "language": return (
{languages.map((t) => ( toggleTag(t.id)} /> ))}
); case "topic": return ( <>
{topics.map((t) => ( toggleTag(t.id)} /> ))}
); } } const orderedAvailable = layout.order.filter((id) => available[id]); const renderedIds = editing ? orderedAvailable : orderedAvailable.filter((id) => !layout.hidden[id]); return ( ); } function WidgetCard({ id, title, editing, collapsed, hidden, onToggleCollapse, onToggleHidden, children, }: { id: WidgetId; title: string; editing: boolean; collapsed: boolean; hidden: boolean; onToggleCollapse: () => void; onToggleHidden: () => void; children: React.ReactNode; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id, disabled: !editing, }); const style: React.CSSProperties = { transform: CSS.Transform.toString(transform), transition, }; const showBody = !editing && !collapsed; return (
{editing && ( )}
{title}
{editing ? ( ) : ( )}
{showBody &&
{children}
}
); } function Toggle({ label, checked, onChange, }: { label: string; checked: boolean; onChange: (v: boolean) => void; }) { return ( ); }