refactor: add SubNav layout and per-subsite nav placeholders; switch Base to use SubNav

This commit is contained in:
2025-11-28 09:07:55 -05:00
parent de50889b2c
commit d8d6c5ec21
26 changed files with 1227 additions and 122 deletions

View File

@@ -1,4 +1,4 @@
import React, { Suspense, lazy } from 'react';
import React, { Suspense, lazy, useState, useMemo, useEffect } from 'react';
import Memes from './Memes.jsx';
import Lighting from './Lighting.jsx';
import { toast } from 'react-toastify';
@@ -14,13 +14,69 @@ const LoginPage = lazy(() => import('./Login.jsx'));
const LyricSearch = lazy(() => import('./LyricSearch'));
const MediaRequestForm = lazy(() => import('./TRip/MediaRequestForm.jsx'));
const RequestManagement = lazy(() => import('./TRip/RequestManagement.jsx'));
const Player = lazy(() => import('./AudioPlayer.jsx'));
// NOTE: Player is intentionally NOT imported at module initialization.
// We create the lazy import inside the component at render-time only when
// we are on the main site and the Player island should be rendered. This
// prevents bundling the player island into pages that are explicitly
// identified as subsites.
const ReqForm = lazy(() => import('./req/ReqForm.jsx'));
export default function Root({ child, user = undefined, ...props }) {
window.toast = toast;
const theme = document.documentElement.getAttribute("data-theme")
const loggedIn = props.loggedIn ?? Boolean(user);
usePrimeReactThemeSwitcher(theme);
// Avoid adding the Player island for subsite requests. We expose a
// runtime flag `window.__IS_SUBSITE` from the server layout so pages
// don't need to pass guards.
const isSubsite = typeof document !== 'undefined' && document.documentElement.getAttribute('data-subsite') === 'true';
// Helpful runtime debugging: only log when child changes so we don't spam
// the console on every render. Use an effect so output is stable.
useEffect(() => {
try {
if (typeof console !== 'undefined' && typeof document !== 'undefined') {
console.debug(`[AppLayout] child=${String(child)}, data-subsite=${document.documentElement.getAttribute('data-subsite')}`);
}
} catch (e) {
// no-op
}
}, [child]);
// Only initialize the lazy player when this is NOT a subsite and the
// active child is the Player island. Placing the lazy() call here
// avoids creating a static dependency at module load time.
// Create the lazy component only when we actually need it. Using
// `useMemo` ensures we don't re-create the lazy factory on every render
// which would create a new component identity and cause mount/unmount
// loops and repeated log messages.
const wantPlayer = !isSubsite && child === "Player";
// Use dynamic import+state on the client to avoid React.lazy identity
// churn and to surface any import-time errors. Since Root is used via
// client:only, this code runs in the browser and can safely import.
const [PlayerComp, setPlayerComp] = useState(null);
useEffect(() => {
let mounted = true;
if (wantPlayer) {
try { console.debug('[AppLayout] dynamic-import: requesting AudioPlayer'); } catch (e) { }
import('./AudioPlayer.jsx')
.then((mod) => {
if (!mounted) return;
// set the component factory
setPlayerComp(() => mod.default ?? null);
try { console.debug('[AppLayout] AudioPlayer import succeeded'); } catch (e) { }
})
.catch((err) => {
console.error('[AppLayout] AudioPlayer import failed', err);
if (mounted) setPlayerComp(() => null);
});
} else {
// unload if we no longer want the player
setPlayerComp(() => null);
}
return () => { mounted = false; };
}, [wantPlayer]);
return (
<PrimeReactProvider>
<CustomToastContainer
@@ -37,10 +93,15 @@ export default function Root({ child, user = undefined, ...props }) {
</Alert> */}
{child == "LoginPage" && (<LoginPage {...props} loggedIn={loggedIn} />)}
{child == "LyricSearch" && (<LyricSearch {...props} client:only="react" />)}
{child == "Player" && (<Player client:only="react" user={user} />)}
{child == "Player" && !isSubsite && PlayerComp && (
<Suspense fallback={<div data-testid="player-fallback" className="p-4 text-center">Loading player...</div>}>
<PlayerComp client:only="react" user={user} />
</Suspense>
)}
{child == "Memes" && <Memes client:only="react" />}
{child == "qs2.MediaRequestForm" && <MediaRequestForm client:only="react" />}
{child == "qs2.RequestManagement" && <RequestManagement client:only="react" />}
{child == "ReqForm" && <ReqForm client:only="react" />}
{child == "Lighting" && <Lighting key={window.location.pathname + Math.random()} client:only="react" />}
</JoyUIRootIsland>
</PrimeReactProvider>