import { lazy, type ComponentType } from "react"; import type { Page } from "../lib/urlState"; import type { PanelId, PanelLayout } from "../lib/panelLayout"; import { useMe } from "../lib/useMe"; import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider"; import { useFeedView, useFeedViewActions } from "./FeedViewProvider"; import { useNavigation, useNavigationActions } from "./NavigationProvider"; import Sidebar from "./Sidebar"; import PlaylistsRail from "./PlaylistsRail"; import PersonalViewsModal from "./PersonalViewsModal"; import { useSavedViews } from "./SavedViewsProvider"; // The RENDER half of the module registry: which component (and side rail) each page renders. The // metadata half — nav order, label, icon, gate — lives in lib/modules.ts (imported by NavSidebar / // Header / App's gate check). Together they let App drive the whole shell from `page` alone, with no // `page === X` chrome conditionals. // // Every content entry is a PROP-FREE component: it reads `me` from useMe() and its module-local state // from the scoped providers, so App renders `` uniformly. The two that need per-render // wiring get a thin wrapper here (FeedPage supplies the GLOBAL feed scope; the channel page threads // its own isolated scope to directly — see the scope note in FeedFiltersProvider). // Route-level code splitting: each page is its own lazy chunk (admin-only pages never reach // non-admins; the logged-out landing pulls only the shell). Rendered inside App's . const Home = lazy(() => import("./Home")); const Feed = lazy(() => import("./Feed")); const Channels = lazy(() => import("./Channels")); const Playlists = lazy(() => import("./Playlists")); const PlexBrowse = lazy(() => import("./PlexBrowse")); const PlexSidebar = lazy(() => import("./PlexSidebar")); const NotificationsPanel = lazy(() => import("./NotificationsPanel")); const Messages = lazy(() => import("./Messages")); const DownloadCenter = lazy(() => import("./DownloadCenter")); const Stats = lazy(() => import("./Stats")); const Scheduler = lazy(() => import("./Scheduler")); const ConfigPanel = lazy(() => import("./ConfigPanel")); const AdminUsers = lazy(() => import("./AdminUsers")); const AuditLog = lazy(() => import("./AuditLog")); const SettingsPanel = lazy(() => import("./SettingsPanel")); // The GLOBAL feed page: wires to the app-wide feed filters / view / live-search. (The channel // page renders itself with its own channel-scoped filters, so those two never share state.) function FeedPage() { const me = useMe(); const filters = useFeedFilters(); const { setFilters } = useFeedFiltersActions(); const view = useFeedView(); const { setView } = useFeedViewActions(); const { ytSearch } = useNavigation(); const { enterYtSearch, exitYtSearch } = useNavigationActions(); // The Personal Views modal lives at the feed-page level (always mounted while on the feed, unmounts // on navigation) rather than inside the collapsible filter rail — otherwise opening it from the // always-visible toolbar view chip while the rail is unpinned would render nothing and latch open. const { modalOpen } = useSavedViews(); return ( <> {modalOpen && !me.is_demo && } ); } function MessagesPage() { const me = useMe(); return (
); } export type PageDef = { // Prop-free by construction: reads `me` + module state from hooks/providers. Component: ComponentType; // Channels + Audit render a table with a sticky header at the scroller top; the shell's top fade // would sit on it and flip on with the first scroll, so they turn it off. Defaults to on. fadeTop?: boolean; // The content column's max width. The header + toolbar centre on this same width (via --content-mw) // so the floating chrome lines up with the centred content instead of spanning the whole column. // MUST match the max-w the page's own content wrapper uses. Omit for a full-width page (feed). maxWidth?: string; }; // page id → the component App renders in the content column (+ its shell chrome config). export const PAGE_CONTENT: Record = { home: { Component: Home, maxWidth: "80rem" }, feed: { Component: FeedPage }, channels: { Component: Channels, fadeTop: false, maxWidth: "96rem" }, playlists: { Component: Playlists, fadeTop: false, maxWidth: "64rem" }, plex: { Component: PlexBrowse, maxWidth: "1600px" }, notifications: { Component: NotificationsPanel, maxWidth: "48rem" }, messages: { Component: MessagesPage }, downloads: { Component: DownloadCenter, maxWidth: "56rem" }, stats: { Component: Stats }, scheduler: { Component: Scheduler }, config: { Component: ConfigPanel }, users: { Component: AdminUsers, maxWidth: "48rem" }, audit: { Component: AuditLog, fadeTop: false }, settings: { Component: SettingsPanel }, }; // The uniform side-rail contract — shell layout/collapse only; the rail reads its module state from // its provider and `me` from useMe(). type RailProps = { layout: PanelLayout; setLayout: (l: PanelLayout) => void; collapsed: boolean; onToggleCollapse: () => void; }; // Which collapse flag a rail uses (feed + plex share the filter rail's flag; playlists has its own). type RailCollapseKey = "filter" | "playlists"; export type RailDef = { Rail: ComponentType; // The persisted panel-layout record this rail edits (App owns the panelLayouts map). panelId: PanelId; collapse: RailCollapseKey; }; // page id → its side rail (only the three pages that have one). App renders it in the rail slot. export const PAGE_RAIL: Partial> = { feed: { Rail: Sidebar, panelId: "feed", collapse: "filter" }, plex: { Rail: PlexSidebar, panelId: "plex", collapse: "filter" }, playlists: { Rail: PlaylistsRail, panelId: "playlists", collapse: "playlists" }, };