import type { ComponentType } from "react"; import { lazyWithPreload, type PreloadableComponent } from "../lib/lazyWithPreload"; import type { Page } from "../lib/urlState"; import type { PanelId, PanelLayout } from "../lib/panelLayout"; import Sidebar from "./Sidebar"; import PlaylistsRail from "./PlaylistsRail"; import { Feed, FeedPage, Messages, MessagesPage } from "./modulePages"; // 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 in modulePages (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 . Each // one is preloadable: App fetches the chunks of every page the account can reach once the first page // is on screen (preloadPages below), so switching modules never shows the chunk fallback. const Home = lazyWithPreload(() => import("./Home")); const Channels = lazyWithPreload(() => import("./Channels")); const Playlists = lazyWithPreload(() => import("./Playlists")); const PlexBrowse = lazyWithPreload(() => import("./PlexBrowse")); const PlexSidebar = lazyWithPreload(() => import("./PlexSidebar")); const NotificationsPanel = lazyWithPreload(() => import("./NotificationsPanel")); const DownloadCenter = lazyWithPreload(() => import("./DownloadCenter")); const Stats = lazyWithPreload(() => import("./Stats")); const Scheduler = lazyWithPreload(() => import("./Scheduler")); const ConfigPanel = lazyWithPreload(() => import("./ConfigPanel")); const AdminUsers = lazyWithPreload(() => import("./AdminUsers")); const AuditLog = lazyWithPreload(() => import("./AuditLog")); const AdminLogs = lazyWithPreload(() => import("./AdminLogs")); const SettingsPanel = lazyWithPreload(() => import("./SettingsPanel")); export type PageDef = { // Prop-free by construction: reads `me` + module state from hooks/providers. Preloadable by type, so // a new page cannot silently opt out of preloadPages. Component: PreloadableComponent; // 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" }, // The two wrappers preload the lazy chunk they render. feed: { Component: Object.assign(FeedPage, { preload: Feed.preload }) }, channels: { Component: Channels, maxWidth: "96rem" }, playlists: { Component: Playlists, maxWidth: "64rem" }, plex: { Component: PlexBrowse, maxWidth: "100rem" }, notifications: { Component: NotificationsPanel, maxWidth: "48rem" }, messages: { Component: Object.assign(MessagesPage, { preload: Messages.preload }) }, downloads: { Component: DownloadCenter, maxWidth: "56rem" }, stats: { Component: Stats }, scheduler: { Component: Scheduler }, config: { Component: ConfigPanel }, users: { Component: AdminUsers, maxWidth: "48rem" }, audit: { Component: AuditLog }, logs: { Component: AdminLogs }, 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" }, }; /** Fetch the code of these pages and of their side rails now, ahead of their first visit. Errors are * swallowed: a chunk that fails to preload is simply fetched (and its failure surfaced) on visit. */ export function preloadPages(pages: Page[]): void { for (const p of pages) { PAGE_CONTENT[p].Component.preload().catch(() => {}); const rail = PAGE_RAIL[p]?.Rail as Partial> | undefined; rail?.preload?.().catch(() => {}); } }