Frontend application for HackYourFuture's trainee management system
- React - Modern React with hooks and concurrent features
- TypeScript
- Vite - For development server
- Material-UI (MUI) - Comprehensive component library
- React Router - Client-side routing
- React Query - Server state management and caching
- Axios - HTTP client for API requests
Before you begin, ensure you have the following installed:
Node.js 22 or higher
-
Install dependencies:
npm run setup
-
Set up environment variables: Create a
.envfile in the client directory:# Backend API URL (optional - defaults to http://localhost:7777) VITE_BACKEND_PROXY_TARGET=http://localhost:7777 # Google OAuth Client ID (required for authentication) VITE_GOOGLE_CLIENT_ID=your_google_client_id_here
npm run devThe application will open automatically in your browser at http://localhost:5173.
npm run buildThe build files will be generated in the dist/ directory.
| Variable | Description | Default | Required |
|---|---|---|---|
VITE_BACKEND_PROXY_TARGET |
Backend API URL | http://localhost:7777 |
No |
VITE_GOOGLE_CLIENT_ID |
Google OAuth Client ID | - | Yes |
You need to set up VITE_BACKEND_PROXY_TARGET variable to point to the correct backend URL. If you use the default http://localhost:7777, you need to run the local server first. Read more about local backend development in the server's README.md
The development server automatically proxies API requests:
/api/*→ Backend server/api-docs/*→ Backend API documentation
This eliminates CORS issues during development.
src/: Contains all React components and application logic.assets/: Contains all the assets and images used.components/: Reusable UI components.hooks/: API calls and data fetching logic.models/: Contains TypeScript interfaces, types, and enums.pages/: Main application pages (Login, Dashboard, TraineeProfile, etc.).routes/: Contains Routes and navigating between different pages logic.styles/: CSS and styling files.
The application uses Google OAuth for authentication:
- Users sign in with their Google accounts
- Protected routes require authentication
- User sessions persist across browser refreshes
Make sure you have VITE_GOOGLE_CLIENT_ID set up correctly. Check out the server README.md for more info.
The client communicates with the backend API through:
- Axios for HTTP requests
- React Query for caching and state management
- Automatic retry for failed requests
- Optimistic updates for better UX
This project uses React Query's useQuery and useMutation hooks for data fetching and updates. Here’s how to organize your files and use these hooks, using the strikes feature as an example:
strikes/
├── api/
│ ├── api.ts # API calls (fetch, add, edit, delete)
│ ├── mapper.ts # Maps API types to domain models
│ ├── types.ts # API request/response types
├── data/
│ ├── keys.ts # Query key factory for React Query
│ ├── mutations.ts # React Query mutation hooks
│ ├── strike-queries.ts # React Query query hooks
api.ts: clean calls using axiosmapper.ts: Sometimes we get from the backend more information than we are using in our UI, or the information is arranged differently. Because of this, it's good to separate the business logic that transforms API responses into the shape your UI needs. This file contains functions that map API types (often matching backend structure) to domain models (used in your frontend), ensuring consistency and making it easier to adapt if the backend changes or if you want to refactor your UI. For example, you might convert snake_case fields to camelCase, filter out unused properties, or maybe flatten nested data/types.ts: specifies the request and response type. This way it's very clear to see what data is sent to the backend and what we expect to get back.
-keys.ts: In this file we define the key factory for the queries used in the feature. Query keys are unique identifiers for each query in React Query. They help React Query know which data to cache, refetch, or update.
A key factory is a function or object that generates consistent, structured keys for your queries. This makes it easy to manage cache and invalidation, especially as your feature grows.
mutations.ts: This file contains React Query mutation hooks for creating, updating, or deleting data. Mutations trigger changes on the server and, on success, typically invalidate relevant queries to keep the UI in sync.queries.ts: This file contains React Query query hooks for fetching data from the server. Queries use structured keys to manage caching, loading states, and automatic refetching, making data fetching reliable and efficient.
data/strike-queries.ts:
import { useQuery } from '@tanstack/react-query';
import { getStrikes } from '../api/api';
import { strikeKeys } from './keys';
export const useGetStrikes = (traineeId: string) =>
useQuery({
queryKey: strikeKeys.list(traineeId),
queryFn: () => getStrikes(traineeId),
});data/keys.ts:
export const strikeKeys = {
all: ['strikes'],
list: (traineeId: string) => [...strikeKeys.all, 'list', traineeId],
};💡 Note: This function creates two query keys for us.
- all: the key looks like this: ['strikes']
- list: the key looks like this: ['strikes', 'list',
traineeId]. And when invalidating the cache, if you usequeryKey: strikeKeys.list(traineeId), it invalidates the cache for this specific traineeId. But if you callqueryKey: strikeKeys.all(), it will invalidate all the cache queries that start with 'strikes'. Which is pretty cool :)
api/api.ts:
import axios from 'axios';
import { StrikeResponse } from './types';
import { mapStrikeToDomain } from './mapper';
export const getStrikes = async (traineeId: string) => {
const { data } = await axios.get<StrikeResponse[]>(`/api/trainees/${traineeId}/strikes`);
return data.map((strike) => mapStrikeToDomain(strike));
};As you can see, the reporter details we get from the backend are nested.
export interface StrikeResponse {
id: string;
comments: string;
date: string; // ISO string from backend
reason: StrikeReason;
reporter: ReporterDTO;
}
interface ReporterDTO {
id: string;
name: string;
imageUrl?: string;
}And the strikes model that is used in the frontend is flattend. We are also ignoring the reported id because it is not used in the Strikes component.
// models/strike.ts
export interface Strike {
id: string;
comments: string;
date: Date;
reason: StrikeReason;
reporterName?: string;
reporterImageUrl?: string;
}const { data: strikes, isPending } = useGetStrikes(traineeId);To add, edit, or delete a strike, use a mutation hook from data/mutations.ts:
export const useAddStrike = (traineeId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (strike: Strike) => {
return addStrike(traineeId, strike);
},
onSuccess: async () => await invalidateStrikesQuery(queryClient, traineeId),
});
};It’s important to invalidate the relevant query after a mutation completes. While the mutation is still pending, React Query will keep the loading state active until the operation finishes and the cache is refreshed.
// api.ts
export const addStrike = async (traineeId: string, strike: Strike) => {
const strikeRequest = mapDomainToStrikeRequest(strike);
const { data } = await axios.post<StrikeResponse>(`/api/trainees/${traineeId}/strikes`, strikeRequest);
return mapStrikeToDomain(data);
};And to use it in the component:
import { useAddStrike } from './data/mutations';
const { mutate: addStrike, isPending } = useAddStrike(traineeId);
// Add a new strike
addStrike(newStrike, {
onSuccess: () => {
// Optionally update UI or show a success message
},
onError: (error) => {
// Handle error
},
});Note: On success, the mutation will invalidate the relevant query so the UI stays in sync with the server. This structure keeps your API logic, data fetching, and UI code clean and maintainable. Use this pattern for new features!