Phases 6-8 of docs/website/THEMING_AND_NAV.md. The public header, the admin sidebar and the player portal now read their override row, and /admin/navigation writes them: rename, reorder by drag, hide, and — on the admin sidebar — move a row into another existing section. The merge always runs BEFORE the role and shard-feature filters in the layouts, which are unchanged and remain the boundary. An override is presentation: it cannot introduce a route, cannot touch a `roles` or `feature` gate, and a stored `hidden: false` on a gated item shows nobody anything. The design scoped these phases as client work, but the server had no way to store a nav row: updateSettings validates and stringifies theme_visual and brand_assets and lets everything else through, so a nav object would have been written as "[object Object]" and read as absent for ever. utils/navOverrides.js mirrors utils/brandAssets.js — strict on write with the offending key named, forgiving on read. It validates shape only; whether a `to` exists is settled client-side at merge time, because the base NAV arrays are client constants and a server-side copy would be a second source of truth that drifts. The nav editor cannot be hidden — its own toggle is disabled, the write path drops `hidden` on that one `to`, and AdminLayout strips it again before merging, which also covers a row edited straight in the database. Orders are written only when the sequence actually differs from the code's, and the comparison is restricted to the rows the editing admin can see, so renaming one item does not pin the position of every other one and a role- or feature-gated item missing from their palette is not mistaken for a reorder. Co-Authored-By: Claude <noreply@anthropic.com>
57 lines
1.9 KiB
JavaScript
57 lines
1.9 KiB
JavaScript
import { useEffect, useState } from 'react'
|
|
import { api } from '../api/client.js'
|
|
import { parseJsonSetting } from './settingsJson.js'
|
|
|
|
// The nav overrides for the two authenticated layouts (THEMING_AND_NAV.md §4.2).
|
|
//
|
|
// `nav_public` rides along in the public settings payload, but `nav_admin` and
|
|
// `nav_player` deliberately do not: an anonymous visitor has no use for either,
|
|
// and the admin nav's labels describe the shape of the admin surface. Their
|
|
// owners read them from GET /api/v1/settings/nav, which any signed-in account
|
|
// may call — AdminLayout renders for editors and moderators, who cannot reach
|
|
// GET /admin/settings at all.
|
|
//
|
|
// Failing quiet is the whole posture: a request that errors, a malformed row and
|
|
// "not fetched yet" are the same state to the caller, `{}`, which
|
|
// applyNavOverrides turns into the coded nav. A sidebar must never blink empty
|
|
// because a settings call was slow.
|
|
|
|
// One module-level copy, so the second layout to mount renders the nav it
|
|
// already knows rather than flashing the coded one, and so the nav editor can
|
|
// push its save into the sidebar the admin is looking at without a reload.
|
|
let cache = {}
|
|
const subscribers = new Set()
|
|
|
|
async function load() {
|
|
try {
|
|
const data = await api.navSettings()
|
|
cache = {
|
|
nav_admin: parseJsonSetting(data?.nav_admin),
|
|
nav_player: parseJsonSetting(data?.nav_player),
|
|
}
|
|
subscribers.forEach((fn) => fn(cache))
|
|
} catch {
|
|
/* the coded nav is the fallback, and it is already on screen */
|
|
}
|
|
return cache
|
|
}
|
|
|
|
/** Re-read the rows after a save, so the live sidebar catches up at once. */
|
|
export function refreshNavOverrides() {
|
|
return load()
|
|
}
|
|
|
|
export function useNavOverrides() {
|
|
const [overrides, setOverrides] = useState(cache)
|
|
|
|
useEffect(() => {
|
|
subscribers.add(setOverrides)
|
|
load()
|
|
return () => subscribers.delete(setOverrides)
|
|
}, [])
|
|
|
|
return overrides
|
|
}
|
|
|
|
export default useNavOverrides
|