Developer GuideFrontend DevelopmentApp Router Guide

App Router Guide

The SALLY frontend uses Next.js 15 with the App Router. This page explains how routing works, how layouts are structured, and how to add new pages.

How App Router Works

In Next.js App Router, the file system defines your routes. Every page.tsx file inside apps/web/src/app/ becomes a URL:

src/app/login/page.tsx        →  /login
src/app/dispatcher/page.tsx   →  /dispatcher
src/app/settings/page.tsx     →  /settings

Directories without a page.tsx are not routable — they only serve as organizational groupings.

Route Groups

Route groups use parentheses in the directory name and do not affect the URL. SALLY uses two route groups:

src/app/
├── (dashboard)/              # Dispatcher and driver views
│   ├── drivers/              # /drivers
│   └── users/                # /users
├── (super-admin)/            # Super admin panel
│   └── admin/                # /admin

Route groups are used to apply different layouts to different sections of the app without nesting the URL path.

Layout Hierarchy

Layouts wrap pages and persist across navigation. SALLY uses a nested layout structure:

Root Layout (src/app/layout.tsx)
├── Providers (src/app/providers.tsx)
│   ├── React Query Provider
│   ├── Theme Provider (next-themes)
│   └── Auth Provider
├── Layout Client (src/app/layout-client.tsx)
│   ├── (dashboard) layout → Sidebar + header for dispatchers/drivers
│   ├── (super-admin) layout → Admin sidebar
│   └── Auth pages → No sidebar (login, register)

The root layout at src/app/layout.tsx applies globally. It sets up:

  • HTML lang attribute and metadata
  • Global CSS (globals.css)
  • Font loading
  • Provider wrappers (React Query, theme, auth)

Route group layouts apply only to their children. For example, the dashboard layout adds a sidebar and header that only appear on dashboard pages.

Key Routes

RoutePage FileDescription
/src/app/page.tsxLanding page / redirect
/loginsrc/app/login/page.tsxLogin form
/registersrc/app/register/page.tsxRegistration form
/accept-invitationsrc/app/accept-invitation/page.tsxInvitation acceptance
/dispatchersrc/app/dispatcher/page.tsxDispatcher dashboard home
/dispatcher/overviewsrc/app/dispatcher/overview/Fleet overview
/dispatcher/fleetsrc/app/dispatcher/fleet/page.tsxFleet management (drivers, vehicles)
/dispatcher/alertssrc/app/dispatcher/alerts/page.tsxAlert management
/dispatcher/active-routessrc/app/dispatcher/active-routes/page.tsxActive route monitoring
/dispatcher/create-plansrc/app/dispatcher/create-plan/page.tsxCreate new route plan
/dispatcher/analyticssrc/app/dispatcher/analytics/page.tsxAnalytics dashboards
/dispatcher/invoicingsrc/app/dispatcher/invoicing/page.tsxInvoice management
/dispatcher/settlementssrc/app/dispatcher/settlements/Settlement management
/driver/*src/app/driver/Driver-facing pages
/settings/*src/app/settings/Settings pages
/admin/*src/app/admin/Admin dashboard
/notificationssrc/app/notifications/page.tsxNotification center

Adding a New Page

To add a new page at /dispatcher/reports:

1. Create the Directory and Page File

src/app/dispatcher/reports/page.tsx
import { ReportsDashboard } from '@/features/operations/reports/components/ReportsDashboard';
 
export default function ReportsPage() {
  return <ReportsDashboard />;
}

That is all you need. Next.js automatically registers the route.

2. Add Metadata (Optional)

src/app/dispatcher/reports/page.tsx
import type { Metadata } from 'next';
 
export const metadata: Metadata = {
  title: 'Reports | SALLY',
  description: 'Fleet operations reports and analytics',
};
 
export default function ReportsPage() {
  return <ReportsDashboard />;
}

3. Add a Layout (Optional)

If the page needs a different layout from its parent:

src/app/dispatcher/reports/layout.tsx
export default function ReportsLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="space-y-6">
      <h1 className="text-2xl font-bold text-foreground">Reports</h1>
      {children}
    </div>
  );
}

Page Structure Pattern

Pages in SALLY are thin. They import a feature component and render it:

// Page files are minimal -- they delegate to feature components
import { FleetDashboard } from '@/features/fleet/components/FleetDashboard';
 
export default function FleetPage() {
  return <FleetDashboard />;
}

All the actual UI logic, data fetching, and state management live in feature modules, not in page files. This keeps the routing layer clean and the business logic reusable.

Dynamic Routes

For pages with URL parameters, use square bracket directory names:

src/app/dispatcher/drivers/[driverId]/page.tsx  →  /dispatcher/drivers/:driverId
src/app/dispatcher/drivers/[driverId]/page.tsx
interface Props {
  params: { driverId: string };
}
 
export default function DriverDetailPage({ params }: Props) {
  return <DriverDetail driverId={params.driverId} />;
}

Loading and Error States

Add loading.tsx and error.tsx files alongside page.tsx for built-in loading and error handling:

src/app/dispatcher/reports/loading.tsx
import { Skeleton } from '@/shared/components/ui/skeleton';
 
export default function Loading() {
  return (
    <div className="space-y-4">
      <Skeleton className="h-8 w-48" />
      <Skeleton className="h-64 w-full" />
    </div>
  );
}
src/app/dispatcher/reports/error.tsx
'use client';
 
export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  return (
    <div className="flex flex-col items-center gap-4 py-12">
      <p className="text-muted-foreground">Something went wrong loading reports.</p>
      <Button onClick={reset} variant="outline">Try again</Button>
    </div>
  );
}

Client vs Server Components

By default, all components in the App Router are Server Components. Add the 'use client' directive at the top of files that need:

  • Event handlers (onClick, onChange)
  • React hooks (useState, useEffect, useQuery)
  • Browser APIs (window, localStorage)
'use client';
 
import { useState } from 'react';
 
export function InteractiveComponent() {
  const [count, setCount] = useState(0);
  // ...
}

Pages that import client-side feature components do not need 'use client' themselves — the boundary is set at the component level.