Replace App's page-render ternary and the three side-rail page conditionals with a data-driven lookup: components/moduleRegistry maps each page to a prop-free content component (+ optional side rail + shell fadeTop), and App renders PAGE_CONTENT[page]/PAGE_RAIL[page] gated by moduleDef(page).gate (an unreachable gated page falls back to the feed, as the old ternary did). Content modules and the two rails are now prop-free: they read `me` via the new useMe() hook and their module-local state from the scoped providers; FeedPage/MessagesPage are the only wrappers (they wire the global feed scope / meId). App.tsx 796->431 lines, zero `page===X` chrome conditionals — closes the Platform epic DoD. (Platform S6b.3)
122 lines
5.1 KiB
TypeScript
122 lines
5.1 KiB
TypeScript
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";
|
|
|
|
// 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 `<Content />` 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 <Feed> 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 <Suspense>.
|
|
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 <Feed> to the app-wide feed filters / view / live-search. (The channel
|
|
// page renders <Feed> 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();
|
|
return (
|
|
<Feed
|
|
filters={filters}
|
|
setFilters={setFilters}
|
|
view={view}
|
|
setView={setView}
|
|
canRead={me.can_read}
|
|
isDemo={me.is_demo}
|
|
ytSearch={ytSearch}
|
|
onYtSearch={enterYtSearch}
|
|
onExitYtSearch={exitYtSearch}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function MessagesPage() {
|
|
const me = useMe();
|
|
return (
|
|
<div className="p-4 max-w-3xl w-full mx-auto">
|
|
<Messages meId={me.id} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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;
|
|
};
|
|
|
|
// page id → the component App renders in the content column (+ its shell chrome config).
|
|
export const PAGE_CONTENT: Record<Page, PageDef> = {
|
|
feed: { Component: FeedPage },
|
|
channels: { Component: Channels, fadeTop: false },
|
|
playlists: { Component: Playlists },
|
|
plex: { Component: PlexBrowse },
|
|
notifications: { Component: NotificationsPanel },
|
|
messages: { Component: MessagesPage },
|
|
downloads: { Component: DownloadCenter },
|
|
stats: { Component: Stats },
|
|
scheduler: { Component: Scheduler },
|
|
config: { Component: ConfigPanel },
|
|
users: { Component: AdminUsers },
|
|
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<RailProps>;
|
|
// 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<Record<Page, RailDef>> = {
|
|
feed: { Rail: Sidebar, panelId: "feed", collapse: "filter" },
|
|
plex: { Rail: PlexSidebar, panelId: "plex", collapse: "filter" },
|
|
playlists: { Rail: PlaylistsRail, panelId: "playlists", collapse: "playlists" },
|
|
};
|