Frontend Applications
- 1. Overview
- 2. Frontend Architecture
- 3. admin-ui
- 4. member-ui
- 5. Shared Patterns
- 6. Testing
- 7. Related Documentation
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.
3. admin-ui
Administrative interface for event organizers and system administrators to manage events, participants, memberships, and financial operations.
Repository: https://github.com/christhonie/admin-ui
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.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.
Repository: https://github.com/christhonie/member-ui
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.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;
}
}
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();
});
});