Frontend Applications

1. Overview

The frontend applications provide web-based user interfaces for different user roles. Both applications are built using React and TypeScript, consuming the REST API provided by the backend services.

2. Frontend Architecture

frontend-architecture

3. admin-ui

Administrative interface for event organizers and system administrators to manage events, participants, memberships, and financial operations.

NPM Package:

{
  "name": "@idealogic/admin-ui",
  "version": "1.2.0",
  "private": true
}

3.1. Technology Stack

Core Framework:

  • React 18 with functional components

  • TypeScript for type safety

  • React Router for navigation

UI Library:

  • Material-UI (MUI) v5

  • MUI DataGrid for tables

  • MUI Date Pickers

State Management:

  • React Context API

  • React Query for server state

  • Local state with useState/useReducer

API Communication:

  • Axios for HTTP requests

  • Axios interceptors for auth tokens

  • React Query for caching and synchronization

Form Handling:

  • React Hook Form

  • Yup for validation schemas

Build Tools:

  • Vite for fast development

  • ESLint for code quality

  • Prettier for code formatting

3.2. Project Structure

admin-ui/
├── src/
│   ├── components/
│   │   ├── common/
│   │   │   ├── Button/
│   │   │   ├── TextField/
│   │   │   ├── DataTable/
│   │   │   └── Dialog/
│   │   ├── event/
│   │   │   ├── EventList/
│   │   │   ├── EventForm/
│   │   │   ├── EventDetails/
│   │   │   └── RaceMatrix/
│   │   ├── participant/
│   │   │   ├── ParticipantList/
│   │   │   ├── ParticipantForm/
│   │   │   └── ParticipantResults/
│   │   ├── membership/
│   │   │   ├── MembershipList/
│   │   │   └── MembershipForm/
│   │   └── layout/
│   │       ├── AppBar/
│   │       ├── Sidebar/
│   │       └── Footer/
│   ├── pages/
│   │   ├── Dashboard/
│   │   ├── Events/
│   │   ├── Participants/
│   │   ├── Races/
│   │   ├── Memberships/
│   │   ├── Financial/
│   │   ├── Reports/
│   │   └── Settings/
│   ├── services/
│   │   ├── api/
│   │   │   ├── eventApi.ts
│   │   │   ├── participantApi.ts
│   │   │   ├── raceApi.ts
│   │   │   └── membershipApi.ts
│   │   ├── auth/
│   │   │   ├── authService.ts
│   │   │   └── tokenService.ts
│   │   └── http/
│   │       └── httpClient.ts
│   ├── context/
│   │   ├── AuthContext.tsx
│   │   ├── OrganisationContext.tsx
│   │   └── ThemeContext.tsx
│   ├── hooks/
│   │   ├── useEvents.ts
│   │   ├── useParticipants.ts
│   │   ├── useAuth.ts
│   │   └── useOrganisation.ts
│   ├── types/
│   │   ├── event.types.ts
│   │   ├── participant.types.ts
│   │   ├── race.types.ts
│   │   └── api.types.ts
│   ├── utils/
│   │   ├── dateUtils.ts
│   │   ├── formatters.ts
│   │   └── validators.ts
│   ├── App.tsx
│   ├── main.tsx
│   └── routes.tsx
├── public/
│   ├── index.html
│   └── favicon.ico
├── package.json
├── tsconfig.json
├── vite.config.ts
└── .env.example

3.3. Key Features

3.3.1. Event Management

  • Create and configure events

  • Define event categories and race types

  • Generate race matrix

  • Configure start groups

  • Manage event participants

3.3.2. Participant Management

  • Register participants for events

  • Assign participants to races

  • Assign race numbers

  • Track registration status

  • View participant results

3.3.3. Race Administration

  • Configure race details

  • Manage start groups

  • Assign participants to start groups

  • Enter race results

  • Publish results

3.3.4. Membership Management

  • Create and renew memberships

  • Configure membership types

  • Track membership periods

  • Manage membership criteria

3.3.5. Financial Operations

  • Create and process orders

  • Manage order line items

  • Process payments

  • Generate invoices

  • Financial reporting

3.3.6. Reporting

  • Event summary reports

  • Participant lists

  • Race results

  • Financial reports

  • Custom report builder

3.4. React Components

Event List Component:

import React from 'react';
import { useQuery } from 'react-query';
import { DataGrid, GridColDef } from '@mui/x-data-grid';
import { Button, Box } from '@mui/material';
import { useNavigate } from 'react-router-dom';
import { eventApi } from '../../services/api/eventApi';
import { Event } from '../../types/event.types';

const columns: GridColDef[] = [
  { field: 'id', headerName: 'ID', width: 90 },
  { field: 'name', headerName: 'Event Name', width: 250 },
  { field: 'startDate', headerName: 'Start Date', width: 150 },
  { field: 'endDate', headerName: 'End Date', width: 150 },
  { field: 'status', headerName: 'Status', width: 120 },
  {
    field: 'actions',
    headerName: 'Actions',
    width: 200,
    renderCell: (params) => (
      <Box>
        <Button
          size="small"
          onClick={() => navigate(`/events/${params.row.id}`)}
        >
          View
        </Button>
        <Button
          size="small"
          onClick={() => navigate(`/events/${params.row.id}/edit`)}
        >
          Edit
        </Button>
      </Box>
    ),
  },
];

export const EventList: React.FC = () => {
  const navigate = useNavigate();

  const { data, isLoading, error } = useQuery(
    'events',
    () => eventApi.getEvents()
  );

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error loading events</div>;

  return (
    <Box sx={{ height: 600, width: '100%' }}>
      <Box sx={{ mb: 2 }}>
        <Button
          variant="contained"
          onClick={() => navigate('/events/new')}
        >
          Create Event
        </Button>
      </Box>
      <DataGrid
        rows={data?.content || []}
        columns={columns}
        pageSize={10}
        rowsPerPageOptions={[10, 25, 50]}
        checkboxSelection
        disableSelectionOnClick
      />
    </Box>
  );
};

Event Form Component:

import React from 'react';
import { useForm, Controller } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import { TextField, Button, Box, Grid } from '@mui/material';
import { DatePicker } from '@mui/x-date-pickers';
import { useMutation, useQueryClient } from 'react-query';
import { eventApi } from '../../services/api/eventApi';
import { EventCreateRequest } from '../../types/event.types';

const schema = yup.object({
  name: yup.string().required('Event name is required').max(200),
  startDate: yup.date().required('Start date is required'),
  endDate: yup
    .date()
    .required('End date is required')
    .min(yup.ref('startDate'), 'End date must be after start date'),
  location: yup.string().required('Location is required'),
  description: yup.string(),
}).required();

interface EventFormProps {
  onSuccess?: () => void;
}

export const EventForm: React.FC<EventFormProps> = ({ onSuccess }) => {
  const queryClient = useQueryClient();

  const { control, handleSubmit, formState: { errors } } = useForm<EventCreateRequest>({
    resolver: yupResolver(schema),
  });

  const createMutation = useMutation(
    (data: EventCreateRequest) => eventApi.createEvent(data),
    {
      onSuccess: () => {
        queryClient.invalidateQueries('events');
        onSuccess?.();
      },
    }
  );

  const onSubmit = (data: EventCreateRequest) => {
    createMutation.mutate(data);
  };

  return (
    <Box component="form" onSubmit={handleSubmit(onSubmit)}>
      <Grid container spacing={2}>
        <Grid item xs={12}>
          <Controller
            name="name"
            control={control}
            render={({ field }) => (
              <TextField
                {...field}
                label="Event Name"
                fullWidth
                error={!!errors.name}
                helperText={errors.name?.message}
              />
            )}
          />
        </Grid>

        <Grid item xs={12} md={6}>
          <Controller
            name="startDate"
            control={control}
            render={({ field }) => (
              <DatePicker
                {...field}
                label="Start Date"
                renderInput={(params) => (
                  <TextField
                    {...params}
                    fullWidth
                    error={!!errors.startDate}
                    helperText={errors.startDate?.message}
                  />
                )}
              />
            )}
          />
        </Grid>

        <Grid item xs={12} md={6}>
          <Controller
            name="endDate"
            control={control}
            render={({ field }) => (
              <DatePicker
                {...field}
                label="End Date"
                renderInput={(params) => (
                  <TextField
                    {...params}
                    fullWidth
                    error={!!errors.endDate}
                    helperText={errors.endDate?.message}
                  />
                )}
              />
            )}
          />
        </Grid>

        <Grid item xs={12}>
          <Controller
            name="location"
            control={control}
            render={({ field }) => (
              <TextField
                {...field}
                label="Location"
                fullWidth
                error={!!errors.location}
                helperText={errors.location?.message}
              />
            )}
          />
        </Grid>

        <Grid item xs={12}>
          <Controller
            name="description"
            control={control}
            render={({ field }) => (
              <TextField
                {...field}
                label="Description"
                fullWidth
                multiline
                rows={4}
                error={!!errors.description}
                helperText={errors.description?.message}
              />
            )}
          />
        </Grid>

        <Grid item xs={12}>
          <Button
            type="submit"
            variant="contained"
            disabled={createMutation.isLoading}
          >
            {createMutation.isLoading ? 'Creating...' : 'Create Event'}
          </Button>
        </Grid>
      </Grid>
    </Box>
  );
};

3.5. API Service Layer

import { httpClient } from '../http/httpClient';
import { Event, EventCreateRequest, PageResponse } from '../../types/event.types';

export const eventApi = {
  getEvents: async (params?: {
    organisationId?: number;
    status?: string;
    page?: number;
    size?: number;
  }): Promise<PageResponse<Event>> => {
    const response = await httpClient.get('/events', { params });
    return response.data;
  },

  getEvent: async (id: number): Promise<Event> => {
    const response = await httpClient.get(`/events/${id}`);
    return response.data;
  },

  createEvent: async (data: EventCreateRequest): Promise<Event> => {
    const response = await httpClient.post('/events', data);
    return response.data;
  },

  updateEvent: async (id: number, data: EventCreateRequest): Promise<Event> => {
    const response = await httpClient.put(`/events/${id}`, data);
    return response.data;
  },

  deleteEvent: async (id: number): Promise<void> => {
    await httpClient.delete(`/events/${id}`);
  },

  generateRaces: async (id: number): Promise<any[]> => {
    const response = await httpClient.post(`/events/${id}/races/generate`);
    return response.data;
  },
};

3.6. HTTP Client with Interceptors

import axios from 'axios';
import { tokenService } from '../auth/tokenService';

export const httpClient = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api',
  timeout: 30000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Request interceptor to add auth token
httpClient.interceptors.request.use(
  (config) => {
    const token = tokenService.getToken();
    if (token && config.headers) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// Response interceptor to handle errors
httpClient.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Token expired or invalid
      tokenService.removeToken();
      window.location.href = '/login';
    }

    if (error.response?.status === 403) {
      // Forbidden - insufficient permissions
      console.error('Access denied:', error.response.data);
    }

    return Promise.reject(error);
  }
);

3.7. Configuration

package.json:

{
  "name": "@idealogic/admin-ui",
  "version": "1.2.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "format": "prettier --write \"src/**/*.{ts,tsx,json,css,md}\""
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.14.0",
    "@mui/material": "^5.14.0",
    "@mui/x-data-grid": "^6.10.0",
    "@mui/x-date-pickers": "^6.10.0",
    "@emotion/react": "^11.11.1",
    "@emotion/styled": "^11.11.0",
    "axios": "^1.4.0",
    "react-query": "^3.39.3",
    "react-hook-form": "^7.45.2",
    "yup": "^1.2.0",
    "@hookform/resolvers": "^3.1.1",
    "date-fns": "^2.30.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.15",
    "@types/react-dom": "^18.2.7",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "@vitejs/plugin-react": "^4.0.3",
    "eslint": "^8.45.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.3",
    "prettier": "^3.0.0",
    "typescript": "^5.0.2",
    "vite": "^4.4.5"
  }
}

.env.example:

VITE_API_BASE_URL=http://localhost:8080/api
VITE_APP_TITLE=Event Management Admin

3.8. Build and Deployment

# Install dependencies
npm install

# Run development server
npm run dev

# Build for production
npm run build

# Preview production build
npm run preview

The production build creates static files in the dist/ directory that can be served by any web server (Nginx, Apache, CDN).

4. member-ui

Member portal for participants to register for events, view their race schedules, and access results.

NPM Package:

{
  "name": "@idealogic/member-ui",
  "version": "1.2.0",
  "private": true
}

4.1. Technology Stack

Same technology stack as admin-ui:

  • React 18 + TypeScript

  • Material-UI v5

  • React Router

  • React Query

  • Axios

4.2. Key Features

4.2.1. Event Discovery

  • Browse upcoming events

  • View event details

  • Check event categories

  • View event schedule

4.2.2. Registration

  • Register for events

  • Select race categories

  • Pay registration fees

  • Download receipts

4.2.3. Member Dashboard

  • View registered events

  • See race schedule

  • Check race numbers

  • View start groups

4.2.4. Results

  • View personal results

  • Compare with other participants

  • Download result certificates

  • View series standings

4.2.5. Profile Management

  • Update personal information

  • Manage membership

  • View order history

  • Update preferences

4.3. Project Structure

Similar to admin-ui but focused on member-facing features:

member-ui/
├── src/
│   ├── components/
│   │   ├── event/
│   │   │   ├── EventCard/
│   │   │   ├── EventDetails/
│   │   │   └── RegistrationForm/
│   │   ├── race/
│   │   │   ├── RaceSchedule/
│   │   │   ├── RaceResults/
│   │   │   └── StartGroupInfo/
│   │   ├── profile/
│   │   │   ├── ProfileForm/
│   │   │   ├── MembershipCard/
│   │   │   └── OrderHistory/
│   │   └── layout/
│   ├── pages/
│   │   ├── Home/
│   │   ├── Events/
│   │   ├── Registration/
│   │   ├── Dashboard/
│   │   ├── Results/
│   │   └── Profile/
│   ├── services/
│   └── ...

4.4. Deployment

Both frontend applications are deployed as static sites:

Hosting Options:

  • Nginx - Traditional web server

  • CDN - CloudFront, Cloudflare, etc.

  • GitHub Pages - For public-facing sites

  • Netlify/Vercel - Modern hosting platforms

See Deployment Documentation for deployment procedures.

5. Shared Patterns

Both applications follow common patterns:

5.1. Component Structure

ComponentName/
├── ComponentName.tsx        # Main component file
├── ComponentName.styles.ts  # Styled components (if needed)
├── ComponentName.test.tsx   # Unit tests
└── index.ts                 # Export barrel

5.2. Custom Hooks

export const useEvents = (organisationId?: number) => {
  return useQuery(
    ['events', organisationId],
    () => eventApi.getEvents({ organisationId }),
    {
      staleTime: 5 * 60 * 1000, // 5 minutes
      cacheTime: 10 * 60 * 1000, // 10 minutes
    }
  );
};

5.3. Error Boundaries

export class ErrorBoundary extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('Error caught by boundary:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <Box sx={{ p: 3 }}>
          <Typography variant="h5">Something went wrong</Typography>
          <Typography>{this.state.error?.message}</Typography>
        </Box>
      );
    }

    return this.props.children;
  }
}

5.4. Loading States

export const LoadingSpinner: React.FC = () => (
  <Box
    sx={{
      display: 'flex',
      justifyContent: 'center',
      alignItems: 'center',
      minHeight: 400,
    }}
  >
    <CircularProgress />
  </Box>
);

6. Testing

6.1. Unit Tests with Jest

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

describe('EventCard', () => {
  it('renders event name', () => {
    const event = {
      id: 1,
      name: 'Test Event',
      startDate: '2024-06-01',
      endDate: '2024-06-02',
    };

    render(<EventCard event={event} />);

    expect(screen.getByText('Test Event')).toBeInTheDocument();
  });
});

6.2. Integration Tests

import { renderWithProviders } from '../../test/utils';
import { EventList } from './EventList';

describe('EventList Integration', () => {
  it('loads and displays events', async () => {
    const { findByText } = renderWithProviders(<EventList />);

    expect(await findByText('Test Event')).toBeInTheDocument();
  });
});