import { Activity, BarChart3, Bell, Clapperboard, Download, Home, ListVideo, MessageSquare, ScrollText, FileSearch, Settings, SlidersHorizontal, Tv, Users, Youtube, type LucideIcon, } from "lucide-react"; import type { Me } from "./api"; import type { Page } from "./urlState"; // ── The module registry ─────────────────────────────────────────────────────────────────────── // One ordered source of truth for every navigable module: its nav label, icon, admin/system // placement, whether it carries a live badge, and its availability gate. The nav rail, the header's // ◀/▶ stepper, and (as the registry grows) the page render all read from here, so a new or newly // gated module shows up everywhere at once — nothing hard-codes the sequence or duplicates the icon. export type ModuleDef = { id: Page; // i18n key for the SHORT display name — the same label the nav rail and the header pill show. labelKey: string; // The SHORT name under the icon when the user turns the rail labels on (B7): the rail is 3.75rem // wide with labels or without, so it has to fit there ("Alerts", "Listák"). The full `labelKey` // stays the tooltip and the accessible name. railKey: string; // i18n key for the LONGER, descriptive browser-tab title (e.g. "Channel manager" vs the rail's // "Channels"). Absent → the tab title falls back to `labelKey`. Read via `pageTitleKey`. titleKey?: string; icon: LucideIcon; // Admin/system modules render in their own section (below a divider) in the nav rail. system?: boolean; // Carries a live unread/active count in the rail (the number itself is computed in NavSidebar, // which owns the polling queries; this only flags which modules have one). hasBadge?: boolean; // Availability for THIS account, derived from language-independent account state (plex toggle, // demo restrictions, admin role). Absent = always available. Drives both the reachable module // set (nav + stepper) and, later, the render gate. gate?: (me: Me) => boolean; }; const isAdmin = (me: Me) => me.role === "admin"; const notDemo = (me: Me) => !me.is_demo; // Ordered in nav-rail order — this array IS the sequence the rail and the stepper walk. const MODULES: readonly ModuleDef[] = [ // Home leads: it is the landing (see navigationInit.loadInitialPage), and the module order IS the // nav order and the header stepper's sequence. The feed takes the YouTube mark its own toolbar // already uses, so the two are apart by shape and not just by position. { id: "home", labelKey: "home.navLabel", railKey: "nav.rail.home", titleKey: "home.title", icon: Home, }, { id: "feed", labelKey: "header.account.feed", railKey: "nav.rail.feed", icon: Youtube }, { id: "channels", labelKey: "header.account.channels", railKey: "nav.rail.channels", titleKey: "header.channelManager", icon: Tv, }, { id: "playlists", labelKey: "header.account.playlists", railKey: "nav.rail.playlists", icon: ListVideo, }, { id: "plex", labelKey: "plex.navLabel", railKey: "nav.rail.plex", icon: Clapperboard, gate: (me) => me.plex_enabled, }, { id: "notifications", labelKey: "inbox.navLabel", railKey: "nav.rail.notifications", icon: Bell, hasBadge: true, }, { id: "messages", labelKey: "messages.navLabel", railKey: "nav.rail.messages", icon: MessageSquare, hasBadge: true, gate: notDemo, }, { id: "downloads", labelKey: "downloads.navLabel", railKey: "nav.rail.downloads", icon: Download, hasBadge: true, gate: notDemo, }, { id: "stats", labelKey: "header.account.stats", railKey: "nav.rail.stats", titleKey: "header.usageStats", icon: BarChart3, }, { id: "scheduler", labelKey: "header.account.scheduler", railKey: "nav.rail.scheduler", titleKey: "header.scheduler", icon: Activity, system: true, gate: isAdmin, }, { id: "config", labelKey: "header.account.config", railKey: "nav.rail.config", titleKey: "header.configuration", icon: SlidersHorizontal, system: true, gate: isAdmin, }, { id: "users", labelKey: "header.account.users", railKey: "nav.rail.users", titleKey: "header.users", icon: Users, system: true, gate: isAdmin, }, { id: "audit", labelKey: "header.account.audit", railKey: "nav.rail.audit", titleKey: "audit.title", icon: ScrollText, system: true, gate: isAdmin, }, { id: "logs", labelKey: "header.account.logs", railKey: "nav.rail.logs", titleKey: "adminLogs.title", icon: FileSearch, system: true, gate: isAdmin, }, ]; // Settings is reachable (its own rail button + the ◀/▶ wrap target) but is not a primary module in // the ordered nav list, so it lives beside the array rather than in it. const SETTINGS_MODULE: ModuleDef = { id: "settings", labelKey: "header.account.settings", railKey: "nav.rail.settings", titleKey: "settings.title", icon: Settings, }; // Registering a NEW Page means adding it to MODULES (or SETTINGS_MODULE) above — that's the single // place. If a Page is left unregistered, moduleDef()/moduleLabelKey[] return undefined and NavSidebar // throws on `.icon`; the coverage isn't compiler-enforced here because the ordered array can't also // be an exhaustive Page map, so treat the array as the checklist when the Page union grows. const BY_ID = Object.fromEntries([...MODULES, SETTINGS_MODULE].map((m) => [m.id, m])) as Record< Page, ModuleDef >; // Look up a module's definition by page id (every Page is covered, incl. settings). export function moduleDef(id: Page): ModuleDef { return BY_ID[id]; } // The i18n key for each module's SHORT display name. Single source so the nav rail and the header's // ModuleName pill always read identically (the longer, descriptive `pageTitleKey` names are kept // only for the browser tab title). export const moduleLabelKey: Record = Object.fromEntries( [...MODULES, SETTINGS_MODULE].map((m) => [m.id, m.labelKey]), ) as Record; export const moduleRailKey: Record = Object.fromEntries( [...MODULES, SETTINGS_MODULE].map((m) => [m.id, m.railKey]), ) as Record; // Admin/system modules — rendered in their own section (below a divider) in the nav rail. export const SYSTEM_PAGES: readonly Page[] = MODULES.filter((m) => m.system).map((m) => m.id); // The ordered list of primary module pages available to THIS user, in nav-rail order. Single // source of truth for both the nav rail (NavSidebar) and the header's ◀/▶ module stepper, so a // new module (or a newly-gated one) shows up in both at once. Availability is derived from the // account via each module's `gate`, so the reachable set changes with the account state, not the UI. export function moduleOrder(me: Me): Page[] { return MODULES.filter((m) => !m.gate || m.gate(me)).map((m) => m.id); }