Sidebar using Next.js Link + usePathname active state + per-module links + search + marketplace-store refresh subscription
usePathname
from
to
via
pattern
apps/web/src/components/layout/sidebar.tsx
/modules/active
fetch on mount and on marketplace-store sidebarRefreshKey change
modules/active
from
to
via
pattern
apps/web/src/components/layout/sidebar.tsx
marketplace-store sidebarRefreshKey
useEffect dependency triggers re-fetch
sidebarRefreshKey
from
to
via
pattern
apps/web/src/components/layout/sidebar.tsx
next/navigation usePathname
active-state comparison against link href
usePathname
Enhance the existing sidebar so it becomes the live navigation surface for activated modules: migrate all links from raw `` to Next.js ``, add `usePathname`-based active highlighting, render an individual link per activated module under each category, add a sidebar search field, and subscribe to the marketplace-store refresh signal so newly activated/deactivated modules appear without a page reload.
Purpose: Delivers PRTAL-02 (sidebar shows categories + activated modules), PRTAL-03 (selected module opens in main area + active highlight), PRTAL-05 (sidebar search/filter), and MRKT-03 (only activated modules appear).
Output: Modified sidebar.tsx, new sidebar-search.tsx, and unit tests.
Phase Goal
As a Tessera user, I want to see my tenant's activated modules in the sidebar, search them, and click one to open it, so that I can navigate directly to the tools I use without leaving the portal.
@.planning/PROJECT.md
@.planning/phases/04-marketplace-portal-navigation/04-CONTEXT.md
@.planning/phases/04-marketplace-portal-navigation/04-RESEARCH.md
@.planning/phases/04-marketplace-portal-navigation/04-PATTERNS.md
@.planning/phases/04-marketplace-portal-navigation/04-UI-SPEC.md
@.planning/phases/04-marketplace-portal-navigation/04-01-SUMMARY.md
Task 1: Build SidebarSearch and migrate sidebar to Next.js Link + usePathname active state
- apps/web/src/components/layout/sidebar.tsx (the file being modified — current raw `` links, hardcoded Dashboard active state, categories accordion with per-module `` links at lines 144-169, admin section)
- apps/web/src/app/(portal)/admin/tenants/page.tsx (input styling for the search field)
- .planning/phases/04-marketplace-portal-navigation/04-PATTERNS.md (sidebar.tsx MODIFY section: Link/usePathname insertion points, active CSS classes; sidebar-search no-analog guidance)
- .planning/phases/04-marketplace-portal-navigation/04-UI-SPEC.md (Sidebar Enhancement: SidebarSearch 36px height, placement between main nav and categories, collapsed-mode rule, active classes)
- .planning/phases/04-marketplace-portal-navigation/04-RESEARCH.md (Pitfall 1 raw `` reloads, Pitfall 3 active state, Pitfall 5 accordion state reset)
- apps/web/src/messages/de.json (sidebar.search + sidebar.noResults added in Plan 01)
- Test 1: Sidebar renders the Dashboard and Marketplace links as Next.js Link elements (anchor with client-side href, no full reload semantics) — assert hrefs "/" and "/marketplace"
- Test 2: Given pathname "/marketplace", the Marketplace link has the active class (`bg-sidebar-accent`) and Dashboard does not
- Test 3: SidebarSearch renders an input with the `sidebar.search` placeholder and calls onChange with typed value
- Test 4: With a search term that matches no module, the sidebar renders the `sidebar.noResults` text
Create `apps/web/src/components/layout/sidebar-search.tsx`: `'use client'` controlled ``, props `{ value: string; onChange: (v: string) => void }`, placeholder from `useTranslations('sidebar')('search')`, height 36px, `border border-border rounded-md`, focus `focus:ring-2 focus:ring-ring focus:border-transparent`, `px-3` within the sidebar nav padding. Include an accessible label (`aria-label` = placeholder). No debounce needed (client-side, tiny dataset) — filter immediately.
Modify `apps/web/src/components/layout/sidebar.tsx`:
1. Add imports `import Link from 'next/link'` and `import { usePathname } from 'next/navigation'`. Add `const pathname = usePathname()` and an `isActive(href)` helper: for "/" return `pathname === '/'`, otherwise `pathname.startsWith(href)`.
2. Replace EVERY `` (Dashboard, Marketplace, category links, per-module links, all admin links) with ``. Remove the hardcoded Dashboard active class; instead apply active classes conditionally via `isActive(href)`: active = `bg-sidebar-accent text-sidebar-accent-foreground font-medium`, inactive = `text-sidebar-foreground hover:bg-muted` (for top-level) / muted variants for nested module links. Per-module links use `/modules/{category}/{slug}` and get active highlight when `isActive` matches that path.
3. Insert `` between the main-nav `
Sidebar fetches the tenant's active modules (GET /modules/active)
STRIDE Threat Register
Threat ID
Category
Component
Disposition
Mitigation Plan
T-04-08
Information Disclosure
Sidebar /modules/active list
mitigate
GET /modules/active is tenant-scoped server-side (TenantMiddleware + RLS, Phase 2/3). Sidebar renders whatever the backend returns for the authenticated tenant — no client-side tenant selection here.
T-04-09
Tampering
Module name rendered in sidebar link
mitigate
Rendered as React text content; auto-escaped. Module slug used in href is a registry-controlled slug, not free user input.
T-04-10
Denial of Service
Sidebar re-fetch loop
mitigate
Re-fetch keyed only on mount + explicit sidebarRefreshKey bump; pathname deliberately excluded from deps to avoid per-navigation fetch storms (anti-pattern guarded).
</threat_model>
- `cd apps/web && pnpm vitest run` exits 0 (all sidebar tests green)
- `cd apps/web && pnpm type-check` exits 0
- Manual: activate a module in marketplace → it appears in sidebar without page reload; click it → opens in main area and sidebar highlights it; clear-then-type in sidebar search keeps expanded categories
<success_criteria>
Sidebar shows only activated modules grouped by category with per-module links (PRTAL-02, MRKT-03)
Clicking a module opens it in the main area client-side with active highlight (PRTAL-03)
Sidebar search filters modules/categories in real time (PRTAL-05)
Sidebar refreshes on marketplace activation without full reload; no per-navigation re-fetch
All raw <a> links migrated to Next.js <Link>
</success_criteria>
Create `.planning/phases/04-marketplace-portal-navigation/04-03-SUMMARY.md` when done