From d21d8ebff20013fd0676abd753a7a40bf3e19827 Mon Sep 17 00:00:00 2001 From: Charity Irone Date: Fri, 26 Jun 2026 07:15:44 +0000 Subject: [PATCH] docs: add contributor guide for adding a new page route Closes #426. Adds docs/adding-page-routes.md covering where routes are registered in src/App.tsx, the file naming convention for page components, the recommended pattern for auth-protected routes (with a RequireAuth wrapper example), and a four-step worked example adding a public AboutPage at /about. Adds a one-line pointer in CONTRIBUTING.md under Frontend conventions so contributors can find the guide. --- CONTRIBUTING.md | 1 + docs/adding-page-routes.md | 272 +++++++++++++++++++++++++++++++++++++ 2 files changed, 273 insertions(+) create mode 100644 docs/adding-page-routes.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b606c35..3c8c6f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,6 +51,7 @@ The repository also uses Husky plus `lint-staged` to run lightweight checks on s - Do not reintroduce old template-era pages or branding. - Prefer accessible, keyboard-friendly UI behavior. - Keep new routes focused and incremental until the main marketplace flows land. +- See [docs/adding-page-routes.md](./docs/adding-page-routes.md) for how to register a new page, the file naming convention, and the recommended pattern for auth-protected routes. ## Good first issue guidance diff --git a/docs/adding-page-routes.md b/docs/adding-page-routes.md new file mode 100644 index 0000000..3ae401f --- /dev/null +++ b/docs/adding-page-routes.md @@ -0,0 +1,272 @@ +# Adding a New Page Route + +This guide explains how to add a new route to the Access Layer client. It covers where routes are registered, the file naming convention for page components, and how to mark a route as auth-protected. + +--- + +## Where routes are registered + +Every route in the client is declared in a **single source of truth** at the top of `src/App.tsx`. The router is built once with `createBrowserRouter([...])` and passed to ``. To add a new route, append a `{ path, element }` entry to that array. + +```tsx +// src/App.tsx +import { createBrowserRouter, RouterProvider } from 'react-router'; +import HomePage from './pages/HomePage'; +import NotFoundPage from './pages/NotFoundPage'; +import AboutPage from './pages/AboutPage'; // ← new import + +const router = createBrowserRouter([ + { + path: '/', + element: , + }, + { + path: '/about', // ← new public route + element: , + }, + { + path: '*', // catch-all stays last + element: , + }, +]); +``` + +Key things to know: + +- **Order matters** only for the `*` catch-all — keep it as the **last entry** so it does not shadow real routes. +- **Imports** for new page components live at the top of `src/App.tsx` alongside the existing ones. Use the relative `'./pages/Page'` path shown above, matching the existing imports. +- **Do not** create a second router or wrap the app in another ``. The router configured here is the only one. +- Nested routes for sub-pages (for example `/creators/:handle/keys`) are added the same way — just declare the full pattern on each entry. The current client uses flat routes only. + +--- + +## File naming convention for page components + +| What | Convention | Example | +|---|---|---| +| File location | `src/pages/` | `src/pages/HomePage.tsx` | +| File name | PascalCase + `Page` suffix | `AboutPage.tsx` | +| Exported component | Default export of a function named `Page` | `export default function AboutPage()` | + +The component itself uses `export default function Page()` — not a named export, and not an arrow const. This keeps imports straightforward and matches every existing page in `src/pages/`: + +``` +src/pages/ +├── HomePage.tsx // registered as '/' +├── MarketingPage.tsx // exists on disk, not yet registered +├── LandingPage.tsx // exists on disk, not yet registered +└── NotFoundPage.tsx // registered as '*' (catch-all) +``` + +A few pages exist as files but are not currently wired into the router in `src/App.tsx`. They are kept on disk because they are planned routes waiting on the marketplace flows to land. If you need one of them live, follow this guide to register it like any other page. + +### What a page component looks like + +Top-level page components take **no props**. They own their own layout, fetching, and state. A minimal page is just a function that returns JSX: + +```tsx +// src/pages/AboutPage.tsx +export default function AboutPage() { + return ( +
+

+ About Access Layer +

+

+ Access Layer is a Stellar-native creator keys marketplace. +

+
+ ); +} +``` + +Conventions to follow: + +- **Default export only.** Named exports break the import in `App.tsx`. +- **One component per file.** Don't lump multiple pages into a single file. +- **No props.** Reach for URL params via `useParams()` from `react-router` instead of prop drilling. +- **Accessibility:** wrap content in a single `
` landmark and use semantic headings (`

` for the page title). +- **Match the project styling.** Use the existing `font-grotesque`, `font-jakarta`, and dark-on-blue palette referenced throughout the codebase. Don't introduce new global styles for a single page. +- **Use the `@/` alias for component imports.** The project-wide path alias `@/` maps to `src/` (configured via Vite + TypeScript path mapping). Use it freely from any component file; reserve short relative paths like `'../pages/Page'` for tight sibling-file imports. + +--- + +## Public vs auth-protected routes + +There is **no existing `RequireAuth` / `ProtectedRoute` wrapper** in the codebase yet. Every route registered today is public. When a feature needs auth gating, follow the pattern below — and ship the wrapper component with that feature, since the client doesn't have a standalone auth-guard component yet. + +### Recommended pattern + +1. Create a small wrapper component, conventionally `src/components/auth/RequireAuth.tsx` (create the `auth/` subfolder if it doesn't exist yet). +2. Inside the wrapper, check the appropriate auth state — wagmi's `useAccount` for wallet-gated flows, or `authService.isAuthenticated()` for email/login-gated flows. +3. While the state is resolving, render a lightweight placeholder (the existing `PendingOnboardingPlaceholder` makes a good model). +4. If unauthenticated, render a redirect or a connect-wallet CTA — do **not** render the protected page. +5. Wrap the protected page's `element` with the wrapper inside `App.tsx`. + +```tsx +// src/components/auth/RequireAuth.tsx +import type { ReactNode } from 'react'; +import { useAccount } from 'wagmi'; +import { Navigate, useLocation } from 'react-router'; +import PendingOnboardingPlaceholder from '@/components/common/PendingOnboardingPlaceholder'; + +interface RequireAuthProps { + children: ReactNode; +} + +export default function RequireAuth({ children }: RequireAuthProps) { + const { isConnected, isConnecting } = useAccount(); + const location = useLocation(); + + // Show the placeholder while a fresh wallet connection is in flight + // so the user isn't redirected to "/" mid-connect. Once it resolves, + // isConnected flips to true and we render the protected page below. + // Note: isReconnecting is intentionally NOT in this branch — a + // reconnect of a prior session keeps the user authenticated, so + // rendering the protected page during a reconnect is fine. + if (isConnecting) { + return ; + } + + if (!isConnected) { + // Send unauthenticated users back to the homepage while preserving + // the path they tried to reach so a future flow can deep-link them + // back here after they connect. + return ; + } + + return <>{children}; +} +``` + +Then wire the protection in `src/App.tsx` by wrapping the element rather than registering the page directly: + +```tsx +// src/App.tsx +import RequireAuth from './components/auth/RequireAuth'; +import DashboardPage from './pages/DashboardPage'; + +const router = createBrowserRouter([ + { path: '/', element: }, + { + path: '/dashboard', + // Public route → element: + // Protected route → wrap in : + element: ( + + + + ), + }, + { path: '*', element: }, +]); +``` + +### Choosing which auth check to use + +| Use case | Check | +|---|---| +| The page reads or writes Stellar assets (keys, trades, portfolio) | `useAccount().isConnected` from wagmi | +| The page reads or writes user-profile data via the backend REST API | `authService.isAuthenticated()` | +| Both | Call both. Render the placeholder until both resolve; redirect if either is false. | + +Don't mix the two states in a single component without documenting which is the source of truth for that page — that's the kind of bug that's hard to spot in review. + +### Known repo state (read before adding a protected route) + +Two pre-existing repo facts you should know before shipping a wagmi-based guard: + +1. **`` is not currently mounted above `` in `src/main.tsx`.** As of writing this guide, `main.tsx` renders `` directly inside ``. Any wagmi hook — including `useAccount` inside `RequireAuth` — will throw at runtime because the `WagmiProvider` context is missing. Wiring `` here is an app-level change and should be tracked separately; reference the tracking issue in your route PR description rather than embedding the wiring fix in your route PR. +2. **Wagmi has a transient `isConnecting` state** while a fresh wallet handshake is in flight. During this window `isConnected` is `false`, but redirecting the user mid-connect would bounce them away. The example above handles this by rendering `PendingOnboardingPlaceholder` for the `isConnecting` branch — copy that pattern verbatim. (Note: `isReconnecting` is *not* included in that branch — a reconnect of a prior, already-authenticated session keeps the user authenticated, so rendering the protected page during a reconnect is fine and avoids a UX flash.) + +If your guard only uses `authService.isAuthenticated()` (no wagmi hooks), neither caveat applies — the helper reads `localStorage` directly. + +--- + +## Worked example — adding a new public page + +This walks a contributor end-to-end through adding `AboutPage` at `/about`. The page is public, so no `RequireAuth` wrapper is involved. + +### 1. Create the page component + +Add a new file at `src/pages/AboutPage.tsx`: + +```tsx +// src/pages/AboutPage.tsx +import { Link } from 'react-router'; +import { Button } from '@/components/ui/button'; + +export default function AboutPage() { + return ( +
+

+ About Access Layer +

+

+ Access Layer is a Stellar-native creator keys marketplace built on + the open AccessLayer protocol. +

+ +
+ +
+
+ ); +} +``` + +### 2. Register the route + +Add the import and a new entry to the router array in `src/App.tsx`: + +```tsx +// src/App.tsx +import HomePage from './pages/HomePage'; +import NotFoundPage from './pages/NotFoundPage'; +import AboutPage from './pages/AboutPage'; // ← added + +const router = createBrowserRouter([ + { path: '/', element: }, + { path: '/about', element: }, // ← added + { path: '*', element: }, +]); +``` + +### 3. Link to it from another page + +Open `src/pages/HomePage.tsx`, import `Link`, and add a `` to the new route: + +```tsx +import { Link } from 'react-router'; + +// inside the JSX you return + + About this project + +``` + +### 4. Verify locally + +```bash +pnpm dev # visit http://localhost:5173/about +pnpm lint +pnpm build +``` + +If `pnpm build` succeeds and `/about` renders the page with a working "Back to marketplace" link, you are done. + +--- + +## Key files at a glance + +| File | Purpose | +|---|---| +| `src/App.tsx` | The single source of truth for routing — the only place routes are registered. | +| `src/pages/` | Folder where every page component lives. One file per page, PascalCase + `Page` suffix, default export. | +| `src/components/auth/RequireAuth.tsx` | The recommended wrapper for auth-protected pages. Create it the first time a protected route is added. | +| `src/main.tsx` | Mounts `` inside React's `createRoot`. **Currently does not wrap `` in `Web3Provider`** — must be updated the first time a wagmi-based route guard lands. | +| `src/providers/Web3Provider.tsx` | Provides `WagmiProvider` + `QueryClientProvider`. Any auth wrapper that uses `useAccount` only works after this provider is mounted above the router in `main.tsx`. | +| `CONTRIBUTING.md` | High-level project conventions — read this alongside this guide. | +| `docs/api-layer.md` | Sibling guide covering how to add backend/API endpoints. |