Skip to content

Frontend Developer Guide

Quick Start

Prerequisites

  • Node.js: 20.19.0 or higher (specified in package.json engines)
  • npm: 10.x or higher (comes with Node.js)
  • Code editor: VS Code recommended with extensions:
  • ESLint
  • Prettier
  • Tailwind CSS IntelliSense
  • TypeScript and JavaScript Language Features
  • Basic knowledge: React 18+, TypeScript, Tailwind CSS, React Router

Initial Setup

  1. Navigate to frontend directory

    cd frontend
    

  2. Install dependencies

    npm install
    

  3. Set up environment variables

    # Create .env file in frontend root (if it doesn't exist)
    touch .env
    

Add the following to .env:

# Development (local backend)
VITE_API_URL=http://localhost:8000/api/v1
VITE_ENVIRONMENT=development

# Production (Azure - uncomment when deploying)
# VITE_API_URL=https://lco-function-app-hggkctgbhhhghgef.canadacentral-01.azurewebsites.net/api/v1
# VITE_ENVIRONMENT=production

  1. Start development server
    npm run dev
    

The app will be available at http://localhost:5173 Vite will display the URL in the terminal

  1. Verify backend connection
  2. Ensure the backend API is running on http://localhost:8000
  3. Open browser DevTools (F12)
  4. Check Console tab for API configuration logs
  5. Check Network tab for API requests
  6. Visit http://localhost:8000/api/docs to verify backend is up

Development Workflow

Running the Application

# Development server with hot reload
npm run dev

# Build for production
npm run build

# Preview production build locally
npm run preview

# Lint code
npm run lint

Project Scripts

Command Description
npm run dev Start Vite dev server with HMR on port 5173
npm run build Run TypeScript type check (tsc -b) + Vite production build
npm run lint Run ESLint on codebase (check for errors)
npm run preview Preview production build locally (after npm run build)

Note: All scripts are defined in package.json. The build script runs type checking before building to catch TypeScript errors early.

Directory Structure Guide

/src/components/

UI Components (/ui/) - Reusable, atomic components - Based on Radix UI primitives - Styled with Tailwind CSS - No business logic

Feature Components - Business-specific components - May contain state and logic - Examples: Sidebar.tsx, CrewsTable.tsx, DisciplineCard.tsx

Dialog/Modal Components - Focused workflows (create, edit) - Examples: ServiceDialog.tsx, CreateCrewDialog.tsx

/src/pages/

Route-specific page components: - Data fetching and loading states - Layout composition - Route parameter handling - Navigation logic

/src/services/api/

API integration modules: - One file per domain (projects, services, crews, etc.) - Typed request/response interfaces - Error handling - Uses centralized apiClient from config.ts

/src/types/

TypeScript type definitions: - Match backend data models - Include request/response DTOs - Helper types for UI state

/src/utils/

Pure utility functions: - Business logic calculations - Data transformations - No side effects

Code Style Guidelines

TypeScript Best Practices

  1. Always define types/interfaces

    // Good
    interface Project {
      projectId: string;
      projectName: string;
      // ... other fields
    }
    
    // Avoid
    const project: any = { ... };
    

  2. Use type inference where obvious

    // Good
    const [loading, setLoading] = useState(false); // boolean inferred
    
    // Unnecessary
    const [loading, setLoading] = useState<boolean>(false);
    

  3. Prefer interfaces over types for objects

    // Preferred
    interface CrewFilters {
      search?: string;
      discipline?: string;
    }
    
    // Use type for unions/intersections
    type ServiceType = 'estimation' | 'loan-monitoring' | 'project-controls' | 'claims';
    

Component Structure

Functional Component Pattern:

import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { projectsApi } from '@/services/api/projects';
import type { Project } from '@/types/project';

interface MyComponentProps {
  title: string;
  onSave?: (data: Project) => void;
}

export function MyComponent({ title, onSave }: MyComponentProps) {
  // Hooks first
  const { projectId } = useParams();
  const navigate = useNavigate();
  const [data, setData] = useState<Project | null>(null);

  // Effects
  useEffect(() => {
    // Fetch data
  }, [projectId]);

  // Event handlers
  const handleSave = () => {
    // Logic
  };

  // Render
  return (
    <div>
      {/* JSX */}
    </div>
  );
}

Naming Conventions

Files: - Components: PascalCase.tsx (e.g., ProjectDetailPage.tsx) - Utilities: camelCase.ts (e.g., crewRateCalculator.ts) - Types: camelCase.ts (e.g., project.ts)

Variables & Functions: - camelCase for variables and functions - PascalCase for components and types - UPPER_SNAKE_CASE for constants

Event Handlers: - Prefix with handle: handleClick, handleSave, handleCrewSelect

Boolean Props: - Prefix with is, has, should: isLoading, hasError, shouldRender

Import Organization

// 1. External libraries
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';

// 2. UI components
import { Button } from '@/components/ui/button';
import { Table } from '@/components/ui/table';

// 3. Feature components
import { PageHeader } from '@/components/PageHeader';
import { Sidebar } from '@/components/Sidebar';

// 4. Services/API
import { projectsApi } from '@/services/api/projects';

// 5. Types
import type { Project } from '@/types/project';

// 6. Utilities/Helpers
import { cn } from '@/lib/utils';

// 7. Styles/Assets
import './MyComponent.css';

Working with APIs

API Call Pattern

import { useState, useEffect } from 'react';
import { projectsApi } from '@/services/api/projects';
import { useToast } from '@/hooks/use-toast';

export function MyPage() {
  const [data, setData] = useState<Project[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const { toast } = useToast();

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);
        const response = await projectsApi.getProjects();
        setData(response.data);
      } catch (err: any) {
        console.error('Error fetching projects:', err);
        setError('Failed to load projects');
        toast({
          title: 'Error',
          description: 'Failed to load projects',
          variant: 'destructive',
        });
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [toast]);

  // Render loading, error, or data
}

Adding a New API Endpoint

  1. Define types in /src/types/

    // types/myDomain.ts
    export interface MyEntity {
      id: string;
      name: string;
    }
    
    export interface MyEntityCreate {
      name: string;
    }
    

  2. Create API module in /src/services/api/

    // services/api/myDomain.ts
    import { apiClient } from './config';
    import type { MyEntity, MyEntityCreate } from '@/types/myDomain';
    
    const ENDPOINT = '/my-entities';
    
    export const myDomainApi = {
      async getAll(): Promise<MyEntity[]> {
        const response = await apiClient.get<MyEntity[]>(ENDPOINT);
        return response.data;
      },
    
      async create(data: MyEntityCreate): Promise<MyEntity> {
        const response = await apiClient.post<MyEntity>(ENDPOINT, data);
        return response.data;
      },
    };
    

  3. Use in component

    import { myDomainApi } from '@/services/api/myDomain';
    
    const entities = await myDomainApi.getAll();
    

Creating New Components

UI Component (shadcn/ui style)

// components/ui/my-widget.tsx
import * as React from 'react';
import { cn } from '@/lib/utils';

export interface MyWidgetProps extends React.HTMLAttributes<HTMLDivElement> {
  variant?: 'default' | 'outline';
  size?: 'sm' | 'md' | 'lg';
}

const MyWidget = React.forwardRef<HTMLDivElement, MyWidgetProps>(
  ({ className, variant = 'default', size = 'md', ...props }, ref) => {
    return (
      <div
        ref={ref}
        className={cn(
          'base-styles',
          variant === 'outline' && 'outline-styles',
          size === 'sm' && 'text-sm',
          className
        )}
        {...props}
      />
    );
  }
);

MyWidget.displayName = 'MyWidget';

export { MyWidget };

Feature Component

// components/MyFeature.tsx
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { myDomainApi } from '@/services/api/myDomain';

interface MyFeatureProps {
  projectId: string;
  onComplete?: () => void;
}

export function MyFeature({ projectId, onComplete }: MyFeatureProps) {
  const [isLoading, setIsLoading] = useState(false);

  const handleAction = async () => {
    setIsLoading(true);
    try {
      await myDomainApi.create({ /* data */ });
      onComplete?.();
    } catch (err) {
      console.error(err);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div>
      <Button onClick={handleAction} disabled={isLoading}>
        {isLoading ? 'Loading...' : 'Submit'}
      </Button>
    </div>
  );
}

Adding a New Page/Route

  1. Create page component in /src/pages/

    // pages/MyNewPage.tsx
    import { PageHeader } from '@/components/PageHeader';
    
    export function MyNewPage() {
      const breadcrumbs = [
        { label: 'Home', href: '/' },
        { label: 'My New Page' },
      ];
    
      return (
        <div className="flex flex-col h-full">
          <PageHeader title="My New Page" breadcrumbs={breadcrumbs} />
          <div className="flex-1 bg-neutral-100 p-6">
            {/* Content */}
          </div>
        </div>
      );
    }
    

  2. Add route in App.tsx

    import { MyNewPage } from '@/pages/MyNewPage';
    
    // In <Routes>
    <Route path="/my-new-page" element={<MyNewPage />} />
    

  3. Add navigation link (if needed in Sidebar)

    // components/Sidebar.tsx
    const menuItems = [
      // ... existing items
      {
        label: 'My New Page',
        icon: MyIcon,
        href: '/my-new-page',
      },
    ];
    

State Management Patterns

The application uses a hybrid approach with Zustand for global state and React hooks for local state.

Global State with Zustand

Project Context Store:

import { useProjectStore } from '@/stores/projectStore';

function MyComponent() {
  // Access context
  const { projectId, serviceId, clientId } = useProjectStore();

  // Set context (usually in page components)
  const { setContext } = useProjectStore();
  useEffect(() => {
    setContext({ projectId, serviceId, clientId });
  }, [projectId, serviceId, clientId]);

  // Check if context exists
  const { hasContext } = useProjectStore();
  if (!hasContext()) {
    return <div>No project context</div>;
  }

  // Get query params (non-null values)
  const { getQueryParams } = useProjectStore();
  const params = getQueryParams(); // { projectId: string, serviceId: string, clientId: string } | null
}

Benefits: - Eliminates prop drilling - Redux DevTools integration - Type-safe with TypeScript - Minimal boilerplate

Custom Hooks for Business Logic

The app extensively uses custom hooks to encapsulate reusable logic:

// Data fetching with pagination
import { useCrewMembers } from '@/hooks';

const { crewMembersMap, crewMembers, loading, error } = useCrewMembers({
  country: 'CA',
  province: 'AB',
  enabled: true
});

// Crew rate calculations
import { useCrewRates } from '@/hooks';

const {
  rates,
  indirectCosts,
  updateLabourIndirectCost,
  updateEquipmentIndirectCost
} = useCrewRates(baseLabourRate, baseEquipmentRate, initialIndirectCosts);

// MTO management
import { useMTOManagement } from '@/hooks';

const {
  mtoItems,
  mtoLoading,
  handleCellEdit,
  handleSaveMTOChanges,
  handleBulkEditMTO
} = useMTOManagement({ projectId, disciplineApiEnum, selectedDiscipline, toast });

See src/hooks/README.md for complete documentation of all available hooks.

Component State

For simple, local state:

const [count, setCount] = useState(0);

Derived State

Compute from existing state/props instead of storing:

// Good - derived
const totalPrice = items.reduce((sum, item) => sum + item.price, 0);

// Avoid - redundant state
const [totalPrice, setTotalPrice] = useState(0);

Form State

const [formData, setFormData] = useState({
  name: '',
  email: '',
  description: '',
});

const handleChange = (field: keyof typeof formData) => (
  e: React.ChangeEvent<HTMLInputElement>
) => {
  setFormData(prev => ({ ...prev, [field]: e.target.value }));
};

Session Storage (for temporary persistence)

// Save
sessionStorage.setItem('myKey', JSON.stringify(data));

// Load
const stored = sessionStorage.getItem('myKey');
const data = stored ? JSON.parse(stored) : defaultValue;

// Clear
sessionStorage.removeItem('myKey');
import { useNavigate } from 'react-router-dom';

const navigate = useNavigate();

// With query params
navigate(`/projects/${projectId}?clientId=${clientId}`);

// With state
navigate('/some-page', {
  state: { returnPath: '/previous-page', data: someData }
});

Access Route Params

import { useParams } from 'react-router-dom';

const { projectId, serviceId } = useParams<{
  projectId: string;
  serviceId: string;
}>();

Access Query Params

const urlParams = new URLSearchParams(window.location.search);
const clientId = urlParams.get('clientId');

Access Navigation State

import { useLocation } from 'react-router-dom';

const location = useLocation();
const { activeTab, updatedData } = location.state || {};

Styling with Tailwind CSS

Utility Classes

<div className="flex items-center justify-between p-4 bg-white border border-neutral-200">
  <h1 className="text-lg font-medium text-neutral-950">Title</h1>
  <button className="px-4 py-2 text-sm bg-blue-900 text-white rounded hover:bg-blue-800">
    Action
  </button>
</div>

Conditional Classes with cn()

import { cn } from '@/lib/utils';

<div className={cn(
  'base-class',
  isActive && 'active-class',
  variant === 'primary' && 'primary-variant-class',
  className  // Allow prop override
)} />

Responsive Design

<div className="w-full md:w-1/2 lg:w-1/3">
  {/* Full width on mobile, half on tablet, third on desktop */}
</div>

Custom Theme Colors

Defined in tailwind.config.js:

colors: {
  'blue-900': '#1e3a8a',  // Primary brand color
  'neutral-50': '#fafafa',
  'neutral-950': '#0a0a0a',
}

Working with Advanced Tables

The application uses @tanstack/react-table with custom components for advanced data tables.

DataTable Component

import { DataTable } from '@/components/table/DataTable';
import { ColumnDef } from '@tanstack/react-table';

// Define columns
const columns: ColumnDef<MyDataType>[] = [
  {
    accessorKey: 'name',
    header: 'Name',
    cell: ({ row }) => <span>{row.original.name}</span>,
  },
  // ... more columns
];

function MyTablePage() {
  const [data, setData] = useState<MyDataType[]>([]);
  const [filters, setFilters] = useState<FilterGroup>({ id: 'root', logicalOperator: 'AND', conditions: [] });
  const [sorting, setSorting] = useState<SortingState>([]);
  const [searchTerm, setSearchTerm] = useState('');

  return (
    <DataTable
      data={data}
      columns={columns}
      filters={filters}
      onFiltersChange={setFilters}
      sorting={sorting}
      onSortingChange={setSorting}
      searchTerm={searchTerm}
      onSearchChange={setSearchTerm}
      // Optional features
      enableRowSelection={true}
      enableColumnVisibility={true}
      enableBulkEdit={true}
      onBulkEdit={handleBulkEdit}
      onBulkDelete={handleBulkDelete}
    />
  );
}

Editable Cells

import { EditableCell } from '@/components/table/EditableCell';

const columns: ColumnDef<MyDataType>[] = [
  {
    accessorKey: 'quantity',
    header: 'Quantity',
    cell: ({ row }) => (
      <EditableCell
        value={row.original.quantity}
        type="number"
        onSave={(newValue) => handleCellEdit(row.id, { quantity: newValue })}
      />
    ),
  },
];

Advanced Filtering

Users can build complex filters with AND/OR logic via the filter builder UI. The filter state structure:

interface FilterGroup {
  id: string;
  logicalOperator: 'AND' | 'OR';
  conditions: Array<{
    field: string;
    operator: 'equals' | 'contains' | 'greaterThan' | 'lessThan';
    value: string | number;
  } | FilterGroup>;
}

Column Visibility

// Enable column visibility with drag-and-drop reordering
<DataTable
  columns={columns}
  data={data}
  enableColumnVisibility={true}
/>

Users can show/hide columns and reorder them via the column visibility dialog.

Working with Forms

Controlled Form Example

import { useState } from 'react';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';

export function MyForm() {
  const [formData, setFormData] = useState({
    name: '',
    description: '',
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    // Submit logic
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <div>
        <Label htmlFor="name">Name</Label>
        <Input
          id="name"
          value={formData.name}
          onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
          placeholder="Enter name"
        />
      </div>

      <div>
        <Label htmlFor="description">Description</Label>
        <Input
          id="description"
          value={formData.description}
          onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
        />
      </div>

      <Button type="submit">Save</Button>
    </form>
  );
}

Error Handling

Component-Level Errors

try {
  await api.call();
} catch (err: any) {
  console.error('Detailed error:', err);

  toast({
    title: 'Error',
    description: err.response?.data?.message || 'Something went wrong',
    variant: 'destructive',
  });
}

Loading & Error States

if (loading) {
  return <Loader2 className="h-8 w-8 animate-spin" />;
}

if (error) {
  return (
    <div className="text-red-600">
      <p>{error}</p>
      <button onClick={retry}>Retry</button>
    </div>
  );
}

return <div>{/* Normal content */}</div>;

Toast Notifications

import { useToast } from '@/hooks/use-toast';

const { toast } = useToast();

// Success
toast({
  title: 'Success',
  description: 'Project created successfully',
});

// Error
toast({
  title: 'Error',
  description: 'Failed to save project',
  variant: 'destructive',
});

// Info
toast({
  title: 'Coming Soon',
  description: 'This feature is not yet available',
});

Debugging Tips

React DevTools

Install browser extension for component inspection and state debugging.

Console Logging

// Development logs
if (import.meta.env.DEV) {
  console.log('Loaded crews from sessionStorage:', parsedCrews);
}

// Always log errors
console.error('Error fetching data:', err);

Network Tab

  • Monitor API calls in browser DevTools Network tab
  • Check request/response payloads
  • Verify status codes

Vite Dev Server Logs

  • Check terminal for server errors
  • Hot reload failures
  • Compilation errors

Common Patterns & Solutions

Prevent Memory Leaks with AbortController

useEffect(() => {
  const abortController = new AbortController();

  const fetchData = async () => {
    try {
      const data = await api.get('/endpoint', {
        signal: abortController.signal
      });
      setData(data);
    } catch (err: any) {
      if (err.name === 'AbortError') return; // Ignore abort errors
      console.error(err);
    }
  };

  fetchData();

  return () => {
    abortController.abort(); // Cleanup on unmount
  };
}, []);

Optimistic UI Updates

const handleDelete = async (id: string) => {
  // Optimistically remove from UI
  setItems(items.filter(item => item.id !== id));

  try {
    await api.delete(id);
    toast({ title: 'Deleted successfully' });
  } catch (err) {
    // Revert on error
    setItems(originalItems);
    toast({ title: 'Delete failed', variant: 'destructive' });
  }
};

Batch API Calls

// Fetch all pages
const allItems: Item[] = [];
let skip = 0;
const limit = 100;
let hasMore = true;

while (hasMore) {
  const response = await api.getItems({ skip, limit });
  allItems.push(...response.data);
  hasMore = response.hasMore;
  skip += limit;

  // Safety limit
  if (skip > 10000) break;
}

Performance Best Practices

  1. Memoization:

    import { useMemo } from 'react';
    
    const expensiveValue = useMemo(() => {
      return computeExpensiveValue(data);
    }, [data]);
    

  2. Callback Memoization:

    import { useCallback } from 'react';
    
    const handleClick = useCallback(() => {
      // Handler logic
    }, [dependencies]);
    

  3. Avoid Inline Object/Array Creation:

    // Bad - recreates on every render
    <MyComponent options={[1, 2, 3]} />
    
    // Good - stable reference
    const OPTIONS = [1, 2, 3];
    <MyComponent options={OPTIONS} />
    

  4. Conditional Rendering:

    // Preferred - early return
    if (!data) return <Loading />;
    return <Content data={data} />;
    
    // Avoid nested ternaries
    {data ? <Content /> : loading ? <Loading /> : <Error />}
    

Testing (Future Setup)

Unit Tests (Vitest - planned)

import { describe, it, expect } from 'vitest';
import { calculateCrewLabourRate } from '@/utils/crewRateCalculator';

describe('crewRateCalculator', () => {
  it('should calculate weighted labour rate', () => {
    const result = calculateCrewLabourRate(mockCrew, mockProject, mockMap);
    expect(result).toBe(50.5);
  });
});

Component Tests (React Testing Library - planned)

import { render, screen } from '@testing-library/react';
import { MyComponent } from './MyComponent';

it('renders component with props', () => {
  render(<MyComponent title="Test" />);
  expect(screen.getByText('Test')).toBeInTheDocument();
});

Troubleshooting

Build Errors

Issue: TypeScript errors during build

npm run build
- Check all type errors - Ensure imports are correct - Verify types match backend API

Issue: Module not found - Check import paths - Verify @/ alias in vite.config.ts

Runtime Errors

Issue: API calls failing (CORS, 404) - Check VITE_API_URL in .env - Verify backend is running - Check Network tab in DevTools

Issue: State not updating - Check if state setter is called - Verify dependencies in useEffect - Check for immutability issues

Styling Issues

Issue: Tailwind classes not applying - Check tailwind.config.js content paths - Restart dev server after config changes - Verify class names are correct

Useful Resources

Official Documentation

Internal Resources

Contributing Guidelines

  1. Branch naming:
  2. Features: feature/my-feature-name
  3. Bug fixes: fix/bug-description
  4. Hotfixes: hotfix/critical-issue
  5. Commit messages:
  6. Clear, descriptive commit messages
  7. Follow conventional commits format when possible
  8. Example: "feat: add crew rate calculator", "fix: resolve navigation issue"
  9. Pull requests:
  10. Include clear description of changes
  11. Add screenshots for UI changes
  12. Reference related issues
  13. Ensure CI checks pass
  14. Code review:
  15. Address all review comments
  16. Request re-review after changes
  17. Be responsive to feedback
  18. Testing:
  19. Test all changes locally
  20. Ensure no console errors or warnings
  21. Check cross-browser compatibility (Chrome, Firefox, Safari, Edge)
  22. Verify mobile responsiveness
  23. Code quality:
  24. Run npm run lint before committing
  25. Run npm run build to catch type errors
  26. Follow existing code patterns and conventions

Getting Help

  • Check existing documentation in docs/frontend/
  • Review similar existing components for patterns
  • Consult the codebase's existing implementations
  • Ask team members for clarification on business logic