Merit Journal is a full-stack application that allows users to record and manage meritorious acts or significant things done on a particular day. The frontend is built with React, TypeScript, and Material-UI (MUI).
The frontend follows modern React patterns:
- Component-Based Architecture: UI elements are broken down into reusable components.
- Container-Component Pattern: Separation of components that manage data and those that render UI.
- Redux for State Management: Centralized state management with Redux Toolkit.
- Type-Safe Development: TypeScript is used throughout the application.
- React: UI library
- TypeScript: Type-safe JavaScript
- Redux Toolkit: State management
- Material-UI (MUI): UI component library
- oidc-client-ts: Authentication with OpenID Connect
- Vite: Build tool and development server
src/
Frontend/
public/ # Static assets
src/
app/ # App configuration
store.ts # Redux store setup
components/ # Reusable UI components
features/ # Feature-based modules with their own state
auth/ # Authentication related code
journalEntry/ # Journal entry related code
pages/ # Page components
services/ # API service functions
types/ # TypeScript type definitions
App.tsx # Main application component
index.tsx # Application entry point
theme.ts # Material-UI theme customization
index.html # HTML entry point
package.json # Dependencies and scripts
tsconfig.json # TypeScript configuration
vite.config.ts # Vite configuration
The application uses Redux Toolkit for state management:
{
auth: {
isAuthenticated: boolean,
user: User | null,
token: string | null,
loading: boolean,
error: string | null
},
journalEntries: {
entries: JournalEntry[],
currentEntry: JournalEntry | null,
loading: boolean,
error: string | null
}
}
- authSlice: Manages authentication state
- journalEntrySlice: Manages journal entry data
Redux Toolkit thunks are used for async operations:
loginUser: Initiates OIDC login flowfetchJournalEntries: Gets all journal entries for the current usercreateJournalEntry: Creates a new journal entryupdateJournalEntry: Updates an existing journal entrydeleteJournalEntry: Deletes a journal entry
The application uses OIDC authentication:
- User clicks "Login" which initiates the OIDC flow
- User is redirected to the identity provider (e.g., Google, Microsoft)
- After successful authentication, user is redirected back with tokens
- The access token is stored in Redux state
- The token is sent with each API request in the Authorization header
Services are organized by domain and handle API calls:
// Example of a service function
export const fetchJournalEntries = async (token: string) => {
const response = await fetch(`${API_BASE_URL}/api/journal-entries`, {
headers: {
Authorization: `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch journal entries');
}
return response.json();
};- Base Components: Reusable UI elements like buttons, inputs, cards
- Feature Components: Components specific to a feature, like JournalEntryForm
- Page Components: Top-level components that compose the UI for entire pages
- Layout Components: Components that handle layout structure
Material-UI is used for consistent styling:
- Theme customization in
theme.ts - Component styling using Material-UI's
sxprop andstyledutility - Responsive design using MUI's Grid system and breakpoints
- Forms are built using controlled components
- Validation is handled with a mix of:
- HTML5 validation attributes
- Form-level validation in component state
- Server-side validation responses handled in UI
Images are stored as base64-encoded strings:
- When selecting an image file, it's converted to a base64 string
- This string is sent to the API as part of the journal entry data
- When displaying, the base64 string is used directly in img src attributes
- List View: Grid of journal entry cards, sorted by date
- Detail View: Full journal entry with formatted content and images
- Creation/Editing: Form with rich text editor and image upload
- Tagging: Multi-select component for adding and managing tags
- Search bar that filters journal entries by tag
- Tag cloud showing most used tags
- Create a new file in the appropriate folder (e.g.,
components/,features/) - Import necessary dependencies
- Define the component with proper TypeScript interfaces
- Export the component
Example:
import React from 'react';
import { Box, Typography } from '@mui/material';
interface MyComponentProps {
title: string;
content: string;
}
export const MyComponent: React.FC<MyComponentProps> = ({ title, content }) => {
return (
<Box sx={{ padding: 2 }}>
<Typography variant="h5">{title}</Typography>
<Typography variant="body1">{content}</Typography>
</Box>
);
};- Create a new file in
features/[feature]/[feature]Slice.ts - Define the initial state and action creators
- Create reducers and thunks as needed
- Add the slice to the store in
app/store.ts
- Create or update a file in
services/folder - Define functions for API interactions
- Use fetch API with proper error handling
- Return structured data for use in components or thunks
- Component tests with React Testing Library
- Redux tests for slice reducers and thunks
- Mock API responses for testing async operations
- Development:
npm run dev- Starts Vite dev server - Build:
npm run build- Creates production build indist/ - Preview:
npm run preview- Preview production build locally
-
Authentication Errors:
- Check if token is expired
- Verify OIDC configuration
- Check browser console for CORS errors
-
API Integration Issues:
- Ensure API base URL is correct
- Check that Authorization header is being sent
- Verify request/response data structure matches API expectations
-
State Management Problems:
- Use Redux DevTools to inspect state changes
- Check if the correct actions are being dispatched
- Verify that components are connected to the store properly
-
UI Rendering Issues:
- Check component props
- Verify that conditional rendering logic is correct
- Use React DevTools to inspect component hierarchy
- Use React.memo for components that render frequently
- Implement pagination for large data sets
- Use lazy loading for images and components when possible
- Minimize unnecessary re-renders by optimizing Redux selectors