Skip to main content

Sidebar Route Bundles

The application sidebar is composed from one component per section. Each component lives under src/components/routes/**, reads permissionKeys from TeamsContext, and renders a single <NavProjects /> group with the items the current user is allowed to see.

This pattern keeps the sidebar declarative and removes the need for ad-hoc visibility checks inside app-sidebar.tsx.

Files

src/components/routes/
├── accounting-routes.tsx # Turn-Ins, Rates, Transactions (campaign-scoped fallbacks supported)
├── campaign-admin-routes.tsx # Manage Campaign, Petition Formats, Sub-teams, Regions
├── campaign-routes.tsx # My Dashboard, Petitions, Validators, Circulators, Signatures, Households
├── circulator-routes.tsx # Circulator dashboards (by campaign / by turn-in date)
├── common-routes.tsx # Voter Search, Admin Voter Search
├── super-admin-routes.tsx # Credentials, All Teams, Roles, Responsibilities, Permissions
├── team-management-routes.tsx # Team Dashboard, Campaigns, Members
└── user-settings-route.tsx # Profile (always rendered)

Composition

src/components/common/app-sidebar.tsx decides which bundles to render based on the current layout:

<SidebarContent>
{activeCampaign && <CampaignRoutes />}
{activeCampaign && <CampaignAdminRoutes />}
{layout !== "root" && <CirculatorRoutes />}
{layout !== "root" && <TeamManagementRoutes />}
{layout !== "root" && <AccountingRoutes />}
{layout !== "root" && <SuperAdminRoutes />}
{layout !== "root" && <CommonRoutes />}
<UserSettingsRoute />
</SidebarContent>

layout comes from the parent route layout and is one of "root" | "teams" | "campaign" | "circulator".

Bundle pattern

Every bundle follows the same shape:

const SomeRoutes = () => {
const params = useParams<{ slug: string; primaryTeam: string }>();
const { permissionKeys, superAdminTeamId } = useTeams();

const items = [
...(permissionKeys?.includes("some-perm-key") ||
params.primaryTeam === superAdminTeamId
? [{
name: "Some Page",
url: `/${params.primaryTeam}/some-page`,
icon: SomeIcon,
}]
: []),
// ... more items
];

return items.length > 0 ? (
<NavProjects projects={items} label="Section Title" />
) : null;
};

Notes:

  • permissionKeys is provided by TeamsContext, hydrated from getRoutePermissions({ teamId }) in the team server layout.
  • The super-admin team (SUPER_ADMIN_TEAM_ID, exposed via superAdminTeamId) bypasses key checks so super-admins always see every section.
  • If no items survive the gating, the bundle returns null so no empty section appears.
  • accounting-routes.tsx switches between team-scoped (/${primaryTeam}/all-turn-ins) and campaign-scoped (/${primaryTeam}/campaign/${slug}/turn-in) URLs based on whether params.slug is defined — letting the same bundle be reused inside both layouts.

Adding a new sidebar item

  1. Confirm or add the underlying permission key in App/src/lib/data/permissions/standard-permissions.ts.
  2. Confirm the page itself calls requireAccess({ teamId, key: "<your-key>" }).
  3. Add the item to the appropriate bundle, using the OR-with-super-admin pattern above.
  4. (Optional) If the item must appear in a brand-new section, create a new file under src/components/routes/ and add it to app-sidebar.tsx.

Why bundles instead of one big config?

  • Keeps each section's gating + icon set co-located with its routes.
  • Makes the diff for "permission key X now also gates page Y" tiny.
  • Avoids passing huge config arrays through React tree props.
  • Matches how requireAccess enforces gates server-side — the sidebar is just a mirror of the same key set.