Files
2026-07-12 10:17:17 -04:00

2.4 KiB

Adding a New Advisor-Only Page to SPQ v2

Five files to touch. Follow the exact pattern below.

Step-by-step

1. Create the page component

src/pages/advisor/<PageName>.tsx

Use export default function PageName() — lazy import expects a default export.

Page shell pattern:

import { useCallback, useEffect, useState } from 'react';
import { pb } from '../../lib/pocketbase';

export default function PageName() {
  const userId = pb.authStore.model?.id as string | undefined;
  // ... state, effects, return JSX using the same Tailwind classes
  // as other advisor pages (AdvisorTeam as canonical reference)
}

2. src/App.tsx — lazy import + route

const PageName = lazy(() => import('./pages/advisor/PageName'));

Route inside <Route element={<OwnerLayout />}>:

<Route path="/route-path" element={<AdvisorOnly><PageName /></AdvisorOnly>} />

AdvisorOnly restricts access to advisor role only.

3. src/components/Layout.tsx — nav item

Add the icon to the lucide-react import, then add a nav item in the navItems array:

{ to: "/route-path", label: "Page Label", icon: IconName },

Also add preload import and wire handlers:

import { preloadSettingsPage, preloadTimeCards } from "../lib/routePreload";
// In the NavLink loop:
onMouseEnter={item.to === "/route-path" ? preloadPageName : ...}

4. src/components/mobile/MobileLayout.tsx — same nav pattern

Mirror the import, nav item, and preload wiring from step 3.

5. src/lib/routePreload.ts — preload helpers

export const loadPageName = () => import('../pages/advisor/PageName');
export function preloadPageName() {
  void loadPageName();
}

Common page components available

  • ListSkeleton / ListError — from ../../components/ui/
  • StatusBadge — define inline or import from existing pages
  • Loading/error/empty pattern: loading ? <ListSkeleton /> : error ? <ListError /> : actualContent

Verification

npm run build && npm run lint && npm run test

Pitfalls

  • Lazy import path: Must point to the .tsx file directly. The lazy(() => import(...)) wrapper uses dynamic import — Vite code-splits it.
  • Don't forget MobileLayout — every new page needs a nav entry in both layouts or mobile users can't reach it.
  • Preload wiring: Both onMouseEnter and onFocus need the preload call in both Layout.tsx and MobileLayout.tsx. Missing either means no prefetch.