[Frontend] Add a persistent nav bar with real destinations #378

Closed
opened 2026-08-25 20:42:17 +00:00 by claude-bot · 2 comments
Contributor

Impact: HIGH

Found in the August 2026 session lifecycle review (#319).

What the user experiences

The only persistent navigation in the entire product is a corner strip with a display name, Profile, Admin (for admins), a theme toggle, and Sign out. There is no global link to campaigns, no "my next session", no search, and no command palette outside a hidden Ctrl-K shortcut buried inside the Session Shelf. Every cross-page move depends on a page-specific "← Campaigns" / "← Campaign" / "← Session" breadcrumb, so navigation is a strict tree walk: getting from a wiki article to a different campaign's session requires walking back up through the wiki, which itself opens in a new browser tab and destroys the back button for that entire subtree.

Evidence

  • webapp/frontend/src/components/NavBar.jsx:16-40 — contains only display name, Profile, Admin, theme toggle, Sign out; no dashboard link, no campaign switcher, no search.
  • webapp/frontend/src/pages/CampaignDetail.jsx:2587-2605 — the "Wiki" tab (TabLink) is styled identically to the adjacent no-op "Campaign Journal" tab but is actually a target="_blank" link (:2594, also :2785), so clicking it opens a new tab instead of swapping the panel — the user's expectation of an in-place tab switch is violated, and it breaks back-button navigation for the whole wiki subtree.

Why it matters for a hosted product

Without a persistent nav spine, the Workbench, the Quote Board, and the wiki each depend on a single buried link surviving every redesign, and every cross-campaign move is a manual multi-click tree walk. This compounds every other buried-entry-point finding in this milestone.

Proposed fix

Add a campaign switcher, a "Quest Board" home link, and — when inside a campaign — direct links to Sessions / Prep / Wiki / Quotes to NavBar.jsx. Drop target="_blank" from CampaignDetail.jsx:2594 and :2785 at the same time so the wiki opens in-place. This is the audit's P2.

Acceptance criteria

  • NavBar shows a persistent link back to the dashboard from every page.
  • NavBar provides a campaign switcher when the user is a member of more than one campaign.
  • When inside a campaign, NavBar (or an equivalent persistent element) links directly to Sessions, Prep, Wiki, and Quotes for that campaign.
  • CampaignDetail.jsx:2594 and :2785 no longer open the wiki in a new tab.
**Impact: HIGH** Found in the August 2026 session lifecycle review (#319). ## What the user experiences The only persistent navigation in the entire product is a corner strip with a display name, Profile, Admin (for admins), a theme toggle, and Sign out. There is no global link to campaigns, no "my next session", no search, and no command palette outside a hidden `Ctrl-K` shortcut buried inside the Session Shelf. Every cross-page move depends on a page-specific "← Campaigns" / "← Campaign" / "← Session" breadcrumb, so navigation is a strict tree walk: getting from a wiki article to a different campaign's session requires walking back up through the wiki, which itself opens in a new browser tab and destroys the back button for that entire subtree. ## Evidence - `webapp/frontend/src/components/NavBar.jsx:16-40` — contains only display name, Profile, Admin, theme toggle, Sign out; no dashboard link, no campaign switcher, no search. - `webapp/frontend/src/pages/CampaignDetail.jsx:2587-2605` — the "Wiki" tab (`TabLink`) is styled identically to the adjacent no-op "Campaign Journal" tab but is actually a `target="_blank"` link (`:2594`, also `:2785`), so clicking it opens a new tab instead of swapping the panel — the user's expectation of an in-place tab switch is violated, and it breaks back-button navigation for the whole wiki subtree. ## Why it matters for a hosted product Without a persistent nav spine, the Workbench, the Quote Board, and the wiki each depend on a single buried link surviving every redesign, and every cross-campaign move is a manual multi-click tree walk. This compounds every other buried-entry-point finding in this milestone. ## Proposed fix Add a campaign switcher, a "Quest Board" home link, and — when inside a campaign — direct links to Sessions / Prep / Wiki / Quotes to `NavBar.jsx`. Drop `target="_blank"` from `CampaignDetail.jsx:2594` and `:2785` at the same time so the wiki opens in-place. This is the audit's P2. ## Acceptance criteria - [ ] `NavBar` shows a persistent link back to the dashboard from every page. - [ ] `NavBar` provides a campaign switcher when the user is a member of more than one campaign. - [ ] When inside a campaign, `NavBar` (or an equivalent persistent element) links directly to Sessions, Prep, Wiki, and Quotes for that campaign. - [ ] `CampaignDetail.jsx:2594` and `:2785` no longer open the wiki in a new tab.
Author
Contributor

Picking this up as v4.3.0 phase 2 (#514), the spine, on one lane with #379 so the nav links carry the final names from the start. Decisions: one nav component rendered from the app layout; "inside a campaign" comes from the route match with a small campaign context as fallback for session pages; the switcher reads the campaign list the dashboard already fetches; labels are Sessions / Prep / Wiki / Highlights; route paths do not change; per-page breadcrumbs stay until #374 and #386 reduce them.

Picking this up as v4.3.0 phase 2 (#514), the spine, on one lane with #379 so the nav links carry the final names from the start. Decisions: one nav component rendered from the app layout; "inside a campaign" comes from the route match with a small campaign context as fallback for session pages; the switcher reads the campaign list the dashboard already fetches; labels are Sessions / Prep / Wiki / Highlights; route paths do not change; per-page breadcrumbs stay until #374 and #386 reduce them.
Author
Contributor

Done in PR #519 (merged); on dev now, prod with v4.3.0.

NavBar is now the whole bar rather than the right-hand cluster of one, and App.jsx renders it once from a layout route (AppLayout) instead of each page pasting a copy into its own header. Left to right: the Quest Board wordmark (always home), a campaign switcher, and, inside a campaign, Sessions / Prep / Wiki / Highlights for it. The existing right-hand cluster (name, Profile, Admin, theme, Sign out) is unchanged. On a phone the bar is one wrapping flex row, so the campaign links drop to a second line rather than overflow; no new dependency. Folding the guard into the layout also removed 16 copies of <AuthGuard>.

"Inside a campaign" is read from the route on /campaigns/:id/*, and on the session surfaces (whose URL has no campaign id) from a declaration the page makes once it has loaded its session (SessionDetail, RecordingDashboard, SessionShelf, via the new CampaignContext.jsx). Declarations are tagged with the pathname they were made on, so a stale campaign cannot linger after you navigate away, with no clear-on-unmount to flicker the links between two sessions of the same campaign.

Switcher data is the campaign list the dashboard already fetches, requested once in the bar's mount effect; the layout outlives every page, so that is one request per app load, and the shared client de-duplicates it against the dashboard's call. It appears only when you are in more than one campaign; with exactly one, the bar shows that campaign's name instead. Prep is GM-only: /planning already bounces everyone else, and a link that exists to reject you is worse than no link.

Labels use #379's vocabulary from the start (Sessions / Prep / Wiki / Highlights, not "Quotes").

Also: the wiki tab and "Open wiki" button on the campaign page dropped target="_blank" (the tab was styled identically to the no-op journal tab and broke the back button for the whole wiki subtree), and the shelf's "Draft created — Review" link is now an in-app <Link>. Every remaining _blank in the frontend points at an external URL.

One layout knock-on worth knowing: pages each carried min-h-screen, which under an in-flow bar put a permanent scrollbar on every page. The shell owns viewport height now and the page roots dropped it; the two wiki layouts that hard-coded min-h-[calc(100vh-61px)] use flex-1. Per-page breadcrumbs are untouched; #374 and #386 reduce them. SessionTable (the player kiosk) declares no campaign and so shows no campaign links; it fetches only reveals and has no campaign id, which #386 can revisit.

8 new tests in NavBar.test.jsx; suite 523 passed (48 files), eslint clean apart from the pre-existing CampaignDetail warning, vite build green.

Done in PR #519 (merged); on dev now, prod with v4.3.0. `NavBar` is now the whole bar rather than the right-hand cluster of one, and `App.jsx` renders it **once** from a layout route (`AppLayout`) instead of each page pasting a copy into its own header. Left to right: the **Quest Board** wordmark (always home), a campaign switcher, and, inside a campaign, **Sessions / Prep / Wiki / Highlights** for it. The existing right-hand cluster (name, Profile, Admin, theme, Sign out) is unchanged. On a phone the bar is one wrapping flex row, so the campaign links drop to a second line rather than overflow; no new dependency. Folding the guard into the layout also removed 16 copies of `<AuthGuard>`. **"Inside a campaign"** is read from the route on `/campaigns/:id/*`, and on the session surfaces (whose URL has no campaign id) from a declaration the page makes once it has loaded its session (`SessionDetail`, `RecordingDashboard`, `SessionShelf`, via the new `CampaignContext.jsx`). Declarations are tagged with the pathname they were made on, so a stale campaign cannot linger after you navigate away, with no clear-on-unmount to flicker the links between two sessions of the same campaign. **Switcher data** is the campaign list the dashboard already fetches, requested once in the bar's mount effect; the layout outlives every page, so that is one request per app load, and the shared client de-duplicates it against the dashboard's call. It appears only when you are in more than one campaign; with exactly one, the bar shows that campaign's name instead. **Prep is GM-only**: `/planning` already bounces everyone else, and a link that exists to reject you is worse than no link. Labels use #379's vocabulary from the start (Sessions / Prep / Wiki / **Highlights**, not "Quotes"). Also: the wiki tab and "Open wiki" button on the campaign page dropped `target="_blank"` (the tab was styled identically to the no-op journal tab and broke the back button for the whole wiki subtree), and the shelf's "Draft created — Review" link is now an in-app `<Link>`. Every remaining `_blank` in the frontend points at an external URL. One layout knock-on worth knowing: pages each carried `min-h-screen`, which under an in-flow bar put a permanent scrollbar on every page. The shell owns viewport height now and the page roots dropped it; the two wiki layouts that hard-coded `min-h-[calc(100vh-61px)]` use `flex-1`. Per-page breadcrumbs are untouched; #374 and #386 reduce them. `SessionTable` (the player kiosk) declares no campaign and so shows no campaign links; it fetches only reveals and has no campaign id, which #386 can revisit. 8 new tests in `NavBar.test.jsx`; suite 523 passed (48 files), eslint clean apart from the pre-existing `CampaignDetail` warning, `vite build` green.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#378
No description provided.