Skip to main content

Frontend Component Architecture

RoundTrip Web — Canonical Directory Structure​

Version: 1.0 | Date: March 2026 | Status: Approved

This document is the authoritative reference for where every type of file lives in the RoundTrip React frontend. All new feature work must follow this structure.


The Rule System​

Three rules govern where any file lives:

Rule 1 — Feature scoping: If a component, hook, or type is only used within one feature, it lives inside that feature's folder. If it crosses feature boundaries, it lives in a shared location.

Rule 2 — Hook proximity: Hooks live in a hooks/ subfolder within their feature (src/features/tickets/hooks/useTicketList.ts). Hooks shared across multiple features live in src/hooks/.

Rule 3 — Component layers: Dependencies only flow downward — features import from components/common/, never the reverse. components/common/ imports from components/ui/ (shadcn/ui primitives), never from features.


Directory Structure​

src/
├── app/ # Router, providers, App.tsx, global error boundary
│
├── components/
│ ├── ui/ # shadcn/ui copy-paste primitives
│ │ └── (Button, Dialog, etc.) # Customised with RoundTrip Command CSS variables
│ └── common/ # Shared app-specific components
│ ├── RoundTripLogo.tsx # The real SVG logo mark, dark/light variants
│ ├── CountBadge.tsx # Numeric pill count
│ ├── StatusBadge.tsx # Ticket status semantic color pill
│ ├── PriorityBadge.tsx # Ticket priority semantic color pill
│ ├── SectionHeader.tsx # Panel header row — icon, title, CountBadge, action slot
│ ├── SurfacePanel.tsx # White card wrapper with correct radius, border, shadow
│ ├── PageHeader.tsx # Barlow Condensed page title + subtitle + action slot
│ ├── FieldLabel.tsx # Form field label with icon mark and required indicator
│ └── MetaRow.tsx # Icon tile + label + value row for detail sidebars
│
├── features/
│ ├── dispatch/
│ │ ├── hooks/
│ │ │ └── useDispatchBoard.ts # TanStack Query — dispatch board data
│ │ └── DispatchBoardPage.tsx # Two-column dispatch board
│ │
│ ├── tickets/
│ │ ├── hooks/
│ │ │ ├── useTicketList.ts # TanStack Query — paginated ticket list
│ │ │ ├── useTicketDetail.ts # TanStack Query — single ticket detail
│ │ │ └── useCreateTicket.ts # Mutation + client search + technician queries
│ │ ├── TicketListPage.tsx # Paginated list with filters
│ │ ├── TicketDetailPage.tsx # Full detail with notes, parts, attachments
│ │ └── CreateTicketModal.tsx # Slide-over create form
│ │
│ ├── clients/ # M8 — client list, detail, create/edit
│ │ └── hooks/
│ │
│ ├── inventory/ # M8 — inventory list, stock adjustment
│ │ └── hooks/
│ │
│ ├── billing/ # M8 — invoice list, detail, generate, send
│ │ └── hooks/
│ │
│ └── technician/ # M9 — PWA views, today's jobs, ticket updates
│ └── hooks/
│
├── hooks/ # Shared hooks (cross-feature)
│ └── useDebounce.ts # Delays value updates — used in search inputs
│
├── layouts/
│ ├── DispatchLayout.tsx # Dark sidebar + command bar + Outlet
│ └── TechnicianLayout.tsx # M9 — mobile layout with bottom tab nav
│
├── lib/
│ └── api/
│ ├── axiosInstance.ts # Single Axios instance with Bearer token interceptor
│ ├── dispatchApi.ts # GET /v1/dispatch/board
│ ├── ticketApi.ts # GET /v1/tickets (list)
│ ├── ticketDetailApi.ts # GET /v1/tickets/:id (detail)
│ └── createTicketApi.ts # POST /v1/tickets + client/tech lookups
│
├── stores/
│ ├── authStore.ts # Zustand — auth state, JWT, role, display name
│ ├── uiStore.ts # Zustand — sidebar collapsed, theme
│ └── dispatchStore.ts # Zustand — SignalR status, optimistic map updates
│
└── index.css # RoundTrip Command design tokens

Shared Component Reference​

CountBadge​

Numeric pill. Used in SectionHeader and nav items.

<CountBadge count={7} />
<CountBadge count={0} showZero={false} />

StatusBadge​

Semantic color pill for ticket status. Correct colors: InProgress=Amber, Assigned=Blue, Completed=Green, Cancelled=Steel, OnHold=Amber, Open=Steel.

<StatusBadge status="InProgress" />
<StatusBadge status={ticket.status} size="md" />

PriorityBadge​

Semantic color pill for ticket priority. Also exports priorityBorderColor(priority) for left-border accent on table rows.

<PriorityBadge priority="Urgent" />
// Table row left-border:
borderLeft: `3px solid ${priorityBorderColor(ticket.priority)}`

SectionHeader​

Panel header row with icon, uppercase title, optional count badge, and optional right-aligned action slot.

<SectionHeader icon={Ticket} title="Active Tickets" count={7} />
<SectionHeader icon={Note} title="Notes" count={3} action={<AddNoteButton />} />

SurfacePanel​

Standard white card wrapper — correct border, radius (--radius-lg), shadow.

<SurfacePanel>
<SectionHeader ... />
<div>content</div>
</SurfacePanel>

Barlow Condensed page title + Inter subtitle. Optional right-aligned action slot.

<PageHeader title="Service Tickets" subtitle="42 tickets" action={<NewTicketButton />} />

FieldLabel​

Form field label — icon mark + uppercase text + optional required asterisk. Accepts htmlFor for accessibility.

<FieldLabel icon={User} label="Client" required htmlFor="client-search" />

MetaRow​

Sidebar metadata row — icon tile + label + value. Renders "Not set" for null values.

<MetaRow icon={User} label="Technician" value={ticket.technicianName} />
<MetaRow icon={CalendarBlank} label="Created" value={formatDate(ticket.createdAt)} mono />

Hook Decision Tree​

When creating a new hook, ask:

  1. Is it a server state hook (TanStack Query)? → Lives in src/features/<feature>/hooks/
  2. Is it a mutation hook? → Lives in src/features/<feature>/hooks/
  3. Is it used by more than one feature? → Lives in src/hooks/
  4. Is it a utility hook with no API dependency? → Lives in src/hooks/

Current shared hooks:

  • useDebounce — delays value updates, used in any search input across all features

API Module Convention​

Every API module in src/lib/api/ follows this pattern:

  • Named export object: export const ticketApi = { ... }
  • All types (request/response) co-located in the same file
  • No direct fetch calls — always through apiClient (the Axios instance)
  • Types are manually maintained until openapi-typescript generation is wired up (M10)

shadcn/ui Usage​

shadcn/ui primitives live in src/components/ui/. The copy-paste components are modified to use RoundTrip Command CSS variables rather than Tailwind color classes. When adding a new shadcn/ui component:

  1. Copy the component from shadcn/ui
  2. Replace any hardcoded colors with the appropriate CSS variable from index.css
  3. Ensure it uses var(--font-ui) or var(--font-body) rather than the default font stack

Current shadcn/ui components in use: (to be documented as they are added)


What NOT to do​

  • ❌ Never import from a feature inside components/common/ or components/ui/
  • ❌ Never put a hook directly at the feature root — always in hooks/ subfolder
  • ❌ Never inline a shared component (badge, header, label) inside a feature file — extract it
  • ❌ Never use hardcoded hex colors in component files — always CSS variables
  • ❌ Never create a pages/ directory — features live in features/, not pages/
  • ❌ Never create a new API module for data used by only one hook — co-locate types in the API module file