monorepo/web/maplepress-frontend/docs/FRONTEND_ARCHITECTURE.md

2058 lines
52 KiB
Markdown

# React Frontend Architecture Guide
## Building Enterprise Applications with Three-Layer Service Architecture
**Version:** 2.0.0
**React:** 19.1.0
**Build Tool:** Vite 7.1.2
**Styling:** Tailwind CSS 4.1.12
---
## Table of Contents
1. [Project Overview](#project-overview)
2. [Technology Stack](#technology-stack)
3. [Project Structure](#project-structure)
4. [Three-Layer Service Architecture](#three-layer-service-architecture)
5. [Dependency Injection System](#dependency-injection-system)
6. [Constants Management](#constants-management)
7. [Component Architecture](#component-architecture)
8. [Authentication & Authorization](#authentication--authorization)
9. [Routing & Navigation](#routing--navigation)
10. [State Management & Caching](#state-management--caching)
11. [Styling & Theming](#styling--theming)
12. [Build Configuration](#build-configuration)
13. [Complete Setup Guide](#complete-setup-guide)
14. [Best Practices & Patterns](#best-practices--patterns)
---
## Project Overview
This architecture guide documents a production-grade React application built for enterprise use. The application follows a strict **three-layer service architecture** (API, Storage, Manager) with custom dependency injection, comprehensive constants management, and zero tolerance for magic values.
### Core Principles
1. **No Magic Values**: Every number, string, or configuration value must be defined as a constant
2. **Service Layer Separation**: Clear boundaries between API, Storage, and Manager layers
3. **Dependency Injection**: Custom DI system without third-party libraries
4. **Component Reusability**: Extensive component library organized by purpose
5. **Type Safety**: PropTypes for runtime type checking
6. **Performance**: Code splitting, caching, and optimization strategies
---
## Technology Stack
### Core Dependencies
```json
{
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.8.0",
"vite": "^7.1.2",
"tailwindcss": "^4.1.12"
}
```
### Key Libraries
- **HTTP Client**: `axios` (^1.11.0) - API communication with interceptors
- **Date Handling**: `luxon` (^3.7.1) - Date manipulation and formatting
- **Case Conversion**: `humps` (^2.0.1) - camelCase ↔ snake_case conversion
- **Icons**: `@heroicons/react` (^2.2.0) - SVG icon library
- **QR Codes**: `qrcode.react` (^4.2.0) - 2FA QR code generation
- **Utility**: `clsx` (^2.1.1) - Conditional className utility
### Development Tools
- **Linting**: ESLint 9 with React plugins
- **Build Optimization**: Terser for minification
- **CSS Processing**: PostCSS with Autoprefixer
---
## Project Structure
```
web/frontend/
├── index.html # Entry HTML file
├── package.json # Dependencies and scripts
├── vite.config.js # Vite build configuration
├── tailwind.config.js # Tailwind CSS configuration
├── eslint.config.js # ESLint configuration
├── public/ # Static assets
│ ├── favicon.ico
│ └── assets/
└── src/
├── main.jsx # Application entry point
├── AppRouter.jsx # Main routing configuration
├── components/ # Reusable components
│ ├── UI/ # Pure UI components
│ │ ├── Button/
│ │ ├── Input/
│ │ ├── Modal/
│ │ ├── Table/
│ │ └── ...
│ ├── UIX/ # Enhanced UI components
│ │ ├── EntityListPage/
│ │ ├── EntityDetailPage/
│ │ └── ...
│ ├── business/ # Domain-aware components
│ │ ├── CustomerSelect/
│ │ ├── OrderDisplay/
│ │ └── ...
│ └── Layout/ # Layout components
│ ├── Sidebar.jsx
│ ├── Header.jsx
│ └── Footer.jsx
├── pages/ # Page components (route handlers)
│ ├── Admin/
│ │ ├── Customer/
│ │ │ ├── List/
│ │ │ │ └── Page.jsx
│ │ │ ├── Detail/
│ │ │ │ └── Page.jsx
│ │ │ ├── Create/
│ │ │ │ └── Page.jsx
│ │ │ └── Update/
│ │ │ └── Page.jsx
│ │ └── ...
│ ├── Dashboard/
│ └── Auth/
├── services/ # Service layer (CRITICAL)
│ ├── Services.jsx # DI container
│ ├── Config/
│ │ └── APIConfig.js
│ ├── API/ # HTTP communication layer
│ │ ├── CustomerAPI.js
│ │ ├── AuthAPI.js
│ │ └── ...
│ ├── Storage/ # Local persistence layer
│ │ ├── CustomerStorage.js
│ │ ├── TokenStorage.js
│ │ └── ...
│ ├── Manager/ # Business logic layer
│ │ ├── CustomerManager.js
│ │ ├── AuthManager.js
│ │ └── ...
│ └── Helpers/
│ └── AuthenticatedAxios.js
├── constants/ # Constants (NO MAGIC VALUES!)
│ ├── Customer.js
│ ├── Authentication.js
│ ├── UI.js
│ ├── Roles.js
│ └── ...
├── utils/ # Utility functions
│ └── serviceUtils.js
├── styles/ # Global styles
│ └── app.css
└── assets/ # Static assets (images, fonts)
```
---
## Three-Layer Service Architecture
The most critical aspect of this architecture is the **three-layer service pattern**. This pattern ensures:
- Clear separation of concerns
- Testability
- Maintainability
- Consistent error handling
- Caching strategies
### Layer 1: API Layer (`src/services/API/`)
**Purpose**: HTTP communication with backend services
**Responsibilities**:
- Making HTTP requests using Axios
- Request/response transformation (camelCase ↔ snake_case)
- Error handling and formatting
- Endpoint configuration
**Example: CustomerAPI.js**
```javascript
// src/services/API/CustomerAPI.js
import { camelizeKeys, decamelizeKeys, decamelize } from "humps";
import { createAuthenticatedAxios } from "../Helpers/AuthenticatedAxios";
import { DateTime } from "luxon";
export class CustomerAPI {
constructor(baseURL, endpoints, tokenStorage) {
this.baseURL = baseURL;
this.endpoints = endpoints;
this.tokenStorage = tokenStorage;
}
/**
* Gets list of customers with filtering and pagination
*/
async getCustomers(params = {}, onUnauthorizedCallback = null) {
try {
// Create authenticated axios instance
const authenticatedAxios = createAuthenticatedAxios(
this.baseURL,
this.tokenStorage,
onUnauthorizedCallback
);
// Build query parameters
const queryParams = new URLSearchParams();
if (params.page) queryParams.append("page", params.page);
if (params.limit) queryParams.append("page_size", params.limit);
if (params.search) queryParams.append("search", params.search);
// Make API call
const response = await authenticatedAxios.get(
`${this.endpoints.CUSTOMERS}?${queryParams.toString()}`
);
// Convert snake_case to camelCase
const data = camelizeKeys(response.data);
// Format dates
if (data.results) {
data.results.forEach((item) => {
if (item.createdAt) {
item.createdAt = DateTime.fromISO(item.createdAt)
.toLocaleString(DateTime.DATETIME_MED);
}
});
}
return data;
} catch (error) {
throw this._formatError(error);
}
}
/**
* Creates a new customer
*/
async createCustomer(customerData, onUnauthorizedCallback = null) {
try {
const authenticatedAxios = createAuthenticatedAxios(
this.baseURL,
this.tokenStorage,
onUnauthorizedCallback
);
// Convert camelCase to snake_case for backend
let decamelizedData = decamelizeKeys(customerData);
const response = await authenticatedAxios.post(
this.endpoints.CUSTOMERS,
decamelizedData
);
return camelizeKeys(response.data);
} catch (error) {
throw this._formatError(error);
}
}
/**
* Gets customer detail by ID
*/
async getCustomerDetail(customerId, onUnauthorizedCallback = null) {
try {
if (!customerId) {
throw { customerId: "Valid customer ID is required" };
}
const authenticatedAxios = createAuthenticatedAxios(
this.baseURL,
this.tokenStorage,
onUnauthorizedCallback
);
const url = this.endpoints.CUSTOMER_DETAIL.replace("{id}", customerId);
const response = await authenticatedAxios.get(url);
return camelizeKeys(response.data);
} catch (error) {
throw this._formatError(error);
}
}
/**
* Updates a customer
*/
async updateCustomer(customerId, customerData, onUnauthorizedCallback = null) {
try {
const authenticatedAxios = createAuthenticatedAxios(
this.baseURL,
this.tokenStorage,
onUnauthorizedCallback
);
let decamelizedData = decamelizeKeys(customerData);
decamelizedData.id = customerId;
const url = this.endpoints.CUSTOMER_DETAIL.replace("{id}", customerId);
const response = await authenticatedAxios.put(url, decamelizedData);
return camelizeKeys(response.data);
} catch (error) {
throw this._formatError(error);
}
}
/**
* Deletes a customer
*/
async deleteCustomer(customerId, onUnauthorizedCallback = null) {
try {
const authenticatedAxios = createAuthenticatedAxios(
this.baseURL,
this.tokenStorage,
onUnauthorizedCallback
);
const url = this.endpoints.CUSTOMER_DETAIL.replace("{id}", customerId);
const response = await authenticatedAxios.delete(url);
return camelizeKeys(response.data);
} catch (error) {
throw this._formatError(error);
}
}
/**
* Formats errors consistently
*/
_formatError(error) {
let errorData = error.response?.data || error.response || error;
return camelizeKeys(errorData);
}
}
```
### Layer 2: Storage Layer (`src/services/Storage/`)
**Purpose**: Local data persistence, caching, and state management
**Responsibilities**:
- localStorage operations
- sessionStorage operations
- In-memory caching
- Cache invalidation
- Data persistence strategies
**Example: CustomerStorage.js**
```javascript
// src/services/Storage/CustomerStorage.js
export class CustomerStorage {
constructor() {
this.CUSTOMERS_CACHE_KEY = "MAPLEPRESS_CUSTOMERS_CACHE";
this.CUSTOMERS_TIMESTAMP_KEY = "MAPLEPRESS_CUSTOMERS_TIMESTAMP";
this.DEFAULT_CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
// In-memory cache for current session
this.memoryCache = {
customers: null,
customersTimestamp: null,
isCustomersLoading: false,
};
}
/**
* Gets customers from cache (memory first, then localStorage)
*/
getCustomersFromCache(maxAge = this.DEFAULT_CACHE_DURATION) {
// Check memory cache first (fastest)
if (this._isMemoryCacheValid(maxAge)) {
console.log("Using memory cache for customers");
return this.memoryCache.customers;
}
// Check localStorage cache
try {
const cachedCustomers = localStorage.getItem(this.CUSTOMERS_CACHE_KEY);
const cachedTimestamp = localStorage.getItem(this.CUSTOMERS_TIMESTAMP_KEY);
if (cachedCustomers && cachedTimestamp) {
const timestamp = parseInt(cachedTimestamp);
const age = Date.now() - timestamp;
if (age < maxAge) {
const customersData = JSON.parse(cachedCustomers);
// Update memory cache
this.memoryCache.customers = customersData;
this.memoryCache.customersTimestamp = timestamp;
console.log("Using localStorage cache for customers");
return customersData;
}
}
} catch (error) {
console.error("Error reading from localStorage", error);
this._clearLocalStorageCache();
}
return null;
}
/**
* Saves customers to cache (both memory and localStorage)
*/
saveCustomersToCache(customersData) {
if (!customersData) {
console.warn("Attempted to save null/undefined data");
return;
}
const timestamp = Date.now();
// Save to memory cache
this.memoryCache.customers = customersData;
this.memoryCache.customersTimestamp = timestamp;
this.memoryCache.isCustomersLoading = false;
// Save to localStorage
try {
localStorage.setItem(
this.CUSTOMERS_CACHE_KEY,
JSON.stringify(customersData)
);
localStorage.setItem(
this.CUSTOMERS_TIMESTAMP_KEY,
timestamp.toString()
);
console.log("Customers cached successfully", {
timestamp: new Date(timestamp).toISOString(),
count: customersData.results ? customersData.results.length : 0,
});
} catch (error) {
console.error("Error saving to localStorage", error);
}
}
/**
* Clears customers cache
*/
clearCustomersCache() {
this.memoryCache.customers = null;
this.memoryCache.customersTimestamp = null;
this.memoryCache.isCustomersLoading = false;
this._clearLocalStorageCache();
console.log("Customers cache cleared");
}
/**
* Sets loading state
*/
setCustomersCacheLoading(isLoading) {
this.memoryCache.isCustomersLoading = isLoading;
}
/**
* Gets loading state
*/
isCustomersCacheLoading() {
return this.memoryCache.isCustomersLoading;
}
/**
* Private helper methods
*/
_isMemoryCacheValid(maxAge) {
if (!this.memoryCache.customers || !this.memoryCache.customersTimestamp) {
return false;
}
const age = Date.now() - this.memoryCache.customersTimestamp;
return age < maxAge;
}
_clearLocalStorageCache() {
localStorage.removeItem(this.CUSTOMERS_CACHE_KEY);
localStorage.removeItem(this.CUSTOMERS_TIMESTAMP_KEY);
}
}
```
### Layer 3: Manager Layer (`src/services/Manager/`)
**Purpose**: Business logic and orchestration
**Responsibilities**:
- Combining API and Storage services
- Business rule implementation
- Validation logic
- Cache coordination
- Providing clean interface to components
**Example: CustomerManager.js**
```javascript
// src/services/Manager/CustomerManager.js
export class CustomerManager {
constructor(customerAPI, customerStorage) {
this.customerAPI = customerAPI;
this.customerStorage = customerStorage;
}
/**
* Gets list of customers with caching
*/
async getCustomers(
params = {},
onUnauthorizedCallback = null,
forceRefresh = false
) {
try {
// Check cache first unless force refresh
if (!forceRefresh) {
const cachedCustomers = this.customerStorage.getCustomersFromCache();
if (cachedCustomers) {
return cachedCustomers;
}
}
// Prevent multiple simultaneous requests
if (this.customerStorage.isCustomersCacheLoading()) {
console.log("Customers request already in progress");
return this._waitForCurrentRequest();
}
this.customerStorage.setCustomersCacheLoading(true);
try {
// Validate parameters
const validatedParams = this._validateCustomersParams(params);
// Fetch from API
const customersData = await this.customerAPI.getCustomers(
validatedParams,
onUnauthorizedCallback
);
// Save to cache
this.customerStorage.saveCustomersToCache(customersData);
return customersData;
} finally {
this.customerStorage.setCustomersCacheLoading(false);
}
} catch (error) {
this.customerStorage.setCustomersCacheLoading(false);
console.error("Failed to get customers", error);
throw error;
}
}
/**
* Creates a new customer with validation
*/
async createCustomer(customerData, onUnauthorizedCallback = null) {
try {
// Validate data
const validationErrors = this._validateCustomerData(customerData, true);
if (Object.keys(validationErrors).length > 0) {
throw validationErrors;
}
// Call API
const createdCustomer = await this.customerAPI.createCustomer(
customerData,
onUnauthorizedCallback
);
// Clear cache since data changed
this.customerStorage.clearCustomersCache();
return createdCustomer;
} catch (error) {
console.error("Failed to create customer", error);
throw error;
}
}
/**
* Gets customer detail
*/
async getCustomerDetail(customerId, onUnauthorizedCallback = null) {
try {
const validationError = this._validateCustomerId(customerId);
if (validationError) {
throw validationError;
}
const customerData = await this.customerAPI.getCustomerDetail(
customerId,
onUnauthorizedCallback
);
return customerData;
} catch (error) {
console.error("Failed to get customer detail", error);
throw error;
}
}
/**
* Updates a customer
*/
async updateCustomer(
customerId,
customerData,
onUnauthorizedCallback = null
) {
try {
// Validate ID
const idError = this._validateCustomerId(customerId);
if (idError) throw idError;
// Validate data
const validationErrors = this._validateCustomerData(customerData);
if (Object.keys(validationErrors).length > 0) {
throw validationErrors;
}
// Call API
const updatedCustomer = await this.customerAPI.updateCustomer(
customerId,
customerData,
onUnauthorizedCallback
);
// Clear cache
this.customerStorage.clearCustomersCache();
return updatedCustomer;
} catch (error) {
console.error("Failed to update customer", error);
throw error;
}
}
/**
* Deletes a customer
*/
async deleteCustomer(customerId, onUnauthorizedCallback = null) {
try {
const validationError = this._validateCustomerId(customerId);
if (validationError) throw validationError;
const deleteResponse = await this.customerAPI.deleteCustomer(
customerId,
onUnauthorizedCallback
);
// Clear cache
this.customerStorage.clearCustomersCache();
return deleteResponse;
} catch (error) {
console.error("Failed to delete customer", error);
throw error;
}
}
/**
* Clears cache
*/
clearCustomersCache() {
this.customerStorage.clearCustomersCache();
}
/**
* Private validation methods
*/
_validateCustomerId(customerId) {
if (!customerId ||
(typeof customerId !== "string" && typeof customerId !== "number")) {
return { customerId: "Valid customer ID is required" };
}
return null;
}
_validateCustomersParams(params) {
const validatedParams = {};
// Validate pagination
if (params.page && typeof params.page === "number" && params.page > 0) {
validatedParams.page = params.page;
}
if (params.limit && typeof params.limit === "number" &&
params.limit > 0 && params.limit <= 1000) {
validatedParams.limit = params.limit;
}
// Validate search
if (params.search && typeof params.search === "string" &&
params.search.trim()) {
validatedParams.search = params.search.trim();
}
return validatedParams;
}
_validateCustomerData(customerData, isCreate = false) {
const errors = {};
if (!customerData || typeof customerData !== "object") {
errors.general = "Customer data is required";
return errors;
}
// Validate first name
if (!customerData.firstName || !customerData.firstName.trim()) {
errors.firstName = "First name is required";
}
// Validate last name
if (!customerData.lastName || !customerData.lastName.trim()) {
errors.lastName = "Last name is required";
}
// Validate email
if (!customerData.email || !customerData.email.trim()) {
errors.email = "Email is required";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(customerData.email)) {
errors.email = "Invalid email format";
}
return errors;
}
_waitForCurrentRequest() {
return new Promise((resolve, reject) => {
const checkInterval = setInterval(() => {
if (!this.customerStorage.isCustomersCacheLoading()) {
clearInterval(checkInterval);
const cachedData = this.customerStorage.getCustomersFromCache();
if (cachedData) {
resolve(cachedData);
} else {
reject(new Error("Request failed"));
}
}
}, 100);
setTimeout(() => {
clearInterval(checkInterval);
reject(new Error("Request timeout"));
}, 30000);
});
}
}
```
---
## Dependency Injection System
The application uses a custom dependency injection container without third-party libraries.
### Service Registration (`src/services/Services.jsx`)
```javascript
// src/services/Services.jsx
import React, { createContext, useContext, useMemo } from "react";
import { getAPIBaseURL, API_ENDPOINTS } from "./Config/APIConfig";
// Import all Storage services
import { TokenStorage } from "./Storage/TokenStorage";
import { CustomerStorage } from "./Storage/CustomerStorage";
// Import all API services
import { AuthAPI } from "./API/AuthAPI";
import { CustomerAPI } from "./API/CustomerAPI";
// Import all Manager services
import { AuthManager } from "./Manager/AuthManager";
import { CustomerManager } from "./Manager/CustomerManager";
/**
* Service container class that manages all service instances
*/
class Services {
constructor() {
const baseURL = getAPIBaseURL();
// Storage Services
this.tokenStorage = new TokenStorage();
this.customerStorage = new CustomerStorage();
// API Services (depend on tokenStorage)
this.authAPI = new AuthAPI(baseURL, API_ENDPOINTS, this.tokenStorage);
this.customerAPI = new CustomerAPI(
baseURL,
API_ENDPOINTS,
this.tokenStorage
);
// Manager Services (depend on API and Storage)
this.authManager = new AuthManager(this.authAPI, this.tokenStorage);
this.customerManager = new CustomerManager(
this.customerAPI,
this.customerStorage
);
}
}
// Create React Context for DI
const ServiceContext = createContext(null);
/**
* ServiceProvider component wraps the application
*/
export function ServiceProvider({ children }) {
const services = useMemo(() => new Services(), []);
return (
<ServiceContext.Provider value={services}>
{children}
</ServiceContext.Provider>
);
}
/**
* Hook to access services in components
*/
export function useServices() {
const context = useContext(ServiceContext);
if (!context) {
throw new Error("useServices must be used within ServiceProvider");
}
return context;
}
```
### Using Services in Components
```javascript
// Example: CustomerListPage.jsx
import React, { useState, useEffect } from "react";
import { useServices } from "../services/Services";
function CustomerListPage() {
const { customerManager } = useServices();
const [customers, setCustomers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchCustomers();
}, []);
async function fetchCustomers() {
try {
setLoading(true);
const data = await customerManager.getCustomers(
{ page: 1, limit: 20 },
handleUnauthorized
);
setCustomers(data.results);
} catch (error) {
console.error("Failed to fetch customers:", error);
} finally {
setLoading(false);
}
}
function handleUnauthorized() {
// Handle unauthorized access (redirect to login)
window.location.href = "/login";
}
return (
<div>
<h1>Customers</h1>
{loading ? (
<p>Loading...</p>
) : (
<ul>
{customers.map((customer) => (
<li key={customer.id}>
{customer.firstName} {customer.lastName}
</li>
))}
</ul>
)}
</div>
);
}
```
---
## Constants Management
**CRITICAL RULE**: The application has ZERO TOLERANCE for magic numbers or strings. Every value must be defined as a constant.
### Constants Structure
```
src/constants/
├── Customer.js # Customer-related constants
├── Authentication.js # Auth constants
├── Roles.js # User role constants
├── UI.js # UI-related constants
├── Order.js # Order constants
├── Event.js # Event constants
├── Financial.js # Financial constants
├── Storage.js # Storage key constants
└── ...
```
### Example: Customer Constants (`src/constants/Customer.js`)
```javascript
// src/constants/Customer.js
// Customer Status
export const CUSTOMER_STATUS = {
ACTIVE: 1,
ARCHIVED: 2,
};
// Customer Status Labels
export const CUSTOMER_STATUS_LABELS = {
[CUSTOMER_STATUS.ACTIVE]: "Active",
[CUSTOMER_STATUS.ARCHIVED]: "Archived",
};
// Customer Types
export const CUSTOMER_TYPE_VALUES = {
EDUCATIONAL: "educational",
CORPORATE: "corporate",
NON_PROFIT: "non_profit",
GOVERNMENT: "government",
};
// Customer Type Labels
export const CUSTOMER_TYPES = {
EDUCATIONAL: "Educational",
CORPORATE: "Corporate",
NON_PROFIT: "Non-Profit",
GOVERNMENT: "Government",
};
// Customer Type Options for Dropdowns
export const CUSTOMER_TYPE_OPTIONS = [
{ value: CUSTOMER_TYPE_VALUES.EDUCATIONAL, label: CUSTOMER_TYPES.EDUCATIONAL },
{ value: CUSTOMER_TYPE_VALUES.CORPORATE, label: CUSTOMER_TYPES.CORPORATE },
{ value: CUSTOMER_TYPE_VALUES.NON_PROFIT, label: CUSTOMER_TYPES.NON_PROFIT },
{ value: CUSTOMER_TYPE_VALUES.GOVERNMENT, label: CUSTOMER_TYPES.GOVERNMENT },
];
// Phone Types
export const CUSTOMER_PHONE_TYPES = {
MOBILE: 1,
WORK: 2,
HOME: 3,
};
export const CUSTOMER_PHONE_TYPE_LABELS = {
[CUSTOMER_PHONE_TYPES.MOBILE]: "Mobile",
[CUSTOMER_PHONE_TYPES.WORK]: "Work",
[CUSTOMER_PHONE_TYPES.HOME]: "Home",
};
// Pagination
export const CUSTOMERS_PER_PAGE = 20;
export const DEFAULT_PAGE = 1;
// Form Validation
export const CUSTOMER_VALIDATION = {
NAME_MIN_LENGTH: 2,
NAME_MAX_LENGTH: 100,
EMAIL_MAX_LENGTH: 255,
PHONE_MAX_LENGTH: 20,
ADDRESS_MAX_LENGTH: 500,
COMMENT_MAX_LENGTH: 638,
};
// Cache Configuration
export const CUSTOMER_CACHE_EXPIRY = {
LIST: 5 * 60 * 1000, // 5 minutes
DETAIL: 10 * 60 * 1000, // 10 minutes
SEARCH: 3 * 60 * 1000, // 3 minutes
};
// Countries
export const CUSTOMER_COUNTRIES = {
CA: "CA",
US: "US",
MX: "MX",
};
export const CUSTOMER_COUNTRY_LABELS = {
[CUSTOMER_COUNTRIES.CA]: "Canada",
[CUSTOMER_COUNTRIES.US]: "United States",
[CUSTOMER_COUNTRIES.MX]: "Mexico",
};
```
### Example: Authentication Constants (`src/constants/Authentication.js`)
```javascript
// src/constants/Authentication.js
// HTTP Status Codes
export const HTTP_STATUS = {
OK: 200,
CREATED: 201,
NO_CONTENT: 204,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
INTERNAL_SERVER_ERROR: 500,
};
// Token Types
export const AUTH_TOKEN_TYPE = {
JWT: "JWT",
BEARER: "Bearer",
};
// HTTP Headers
export const HTTP_HEADERS = {
CONTENT_TYPE_JSON: "application/json",
ACCEPT_JSON: "application/json",
CONTENT_TYPE_MULTIPART: "multipart/form-data",
};
// Token Storage Keys
export const TOKEN_STORAGE_KEYS = {
ACCESS_TOKEN: "MAPLEPRESS_ACCESS_TOKEN",
REFRESH_TOKEN: "MAPLEPRESS_REFRESH_TOKEN",
USER_DATA: "MAPLEPRESS_USER_DATA",
};
// Session Timeout
export const SESSION_TIMEOUT = {
WARNING: 5 * 60 * 1000, // 5 minutes warning
LOGOUT: 15 * 60 * 1000, // 15 minutes auto logout
};
```
### Using Constants in Components
```javascript
import { CUSTOMER_STATUS, CUSTOMER_STATUS_LABELS } from "@constants/Customer";
function CustomerBadge({ status }) {
const label = CUSTOMER_STATUS_LABELS[status];
const isActive = status === CUSTOMER_STATUS.ACTIVE;
return (
<span className={isActive ? "badge-green" : "badge-gray"}>
{label}
</span>
);
}
```
---
## Component Architecture
Components are organized into three main categories:
### 1. UI Components (`src/components/UI/`)
Pure, stateless, reusable components with no business logic.
```
UI/
├── Button/
│ └── Button.jsx
├── Input/
│ └── Input.jsx
├── Modal/
│ └── Modal.jsx
├── Table/
│ └── Table.jsx
├── Card/
│ └── Card.jsx
├── Alert/
│ └── Alert.jsx
└── ...
```
**Example: Button Component**
```javascript
// src/components/UI/Button/Button.jsx
import React from "react";
import PropTypes from "prop-types";
import clsx from "clsx";
export function Button({
children,
variant = "primary",
size = "md",
disabled = false,
loading = false,
onClick,
type = "button",
className,
...rest
}) {
const baseStyles = "inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2";
const variantStyles = {
primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
secondary: "bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500",
danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
outline: "border-2 border-gray-300 text-gray-700 hover:bg-gray-50",
};
const sizeStyles = {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-base",
lg: "px-6 py-3 text-lg",
};
const disabledStyles = "opacity-50 cursor-not-allowed";
return (
<button
type={type}
disabled={disabled || loading}
onClick={onClick}
className={clsx(
baseStyles,
variantStyles[variant],
sizeStyles[size],
(disabled || loading) && disabledStyles,
className
)}
{...rest}
>
{loading && <Spinner className="mr-2" />}
{children}
</button>
);
}
Button.propTypes = {
children: PropTypes.node.isRequired,
variant: PropTypes.oneOf(["primary", "secondary", "danger", "outline"]),
size: PropTypes.oneOf(["sm", "md", "lg"]),
disabled: PropTypes.bool,
loading: PropTypes.bool,
onClick: PropTypes.func,
type: PropTypes.oneOf(["button", "submit", "reset"]),
className: PropTypes.string,
};
```
### 2. Business Components (`src/components/business/`)
Domain-aware components with service integration.
```javascript
// src/components/business/CustomerSelect.jsx
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { useServices } from "@services/Services";
export function CustomerSelect({ value, onChange, placeholder = "Select customer" }) {
const { customerManager } = useServices();
const [customers, setCustomers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchCustomers();
}, []);
async function fetchCustomers() {
try {
setLoading(true);
const data = await customerManager.getCustomers({ limit: 100 });
setCustomers(data.results || []);
} catch (error) {
console.error("Failed to fetch customers:", error);
} finally {
setLoading(false);
}
}
return (
<select
value={value}
onChange={(e) => onChange(e.target.value)}
className="form-select"
disabled={loading}
>
<option value="">{loading ? "Loading..." : placeholder}</option>
{customers.map((customer) => (
<option key={customer.id} value={customer.id}>
{customer.firstName} {customer.lastName}
</option>
))}
</select>
);
}
CustomerSelect.propTypes = {
value: PropTypes.string,
onChange: PropTypes.func.isRequired,
placeholder: PropTypes.string,
};
```
### 3. Layout Components (`src/components/Layout/`)
Application structure components.
```javascript
// src/components/Layout/Sidebar.jsx
import React from "react";
import { Link, useLocation } from "react-router-dom";
import { HomeIcon, UsersIcon, DocumentIcon } from "@heroicons/react/24/outline";
export function Sidebar() {
const location = useLocation();
const navigation = [
{ name: "Dashboard", href: "/dashboard", icon: HomeIcon },
{ name: "Customers", href: "/customers", icon: UsersIcon },
{ name: "Orders", href: "/orders", icon: DocumentIcon },
];
return (
<aside className="w-64 bg-gray-800 text-white min-h-screen">
<nav className="p-4 space-y-2">
{navigation.map((item) => {
const isActive = location.pathname.startsWith(item.href);
return (
<Link
key={item.name}
to={item.href}
className={clsx(
"flex items-center px-4 py-2 rounded-md",
isActive
? "bg-gray-900 text-white"
: "text-gray-300 hover:bg-gray-700"
)}
>
<item.icon className="w-5 h-5 mr-3" />
{item.name}
</Link>
);
})}
</nav>
</aside>
);
}
```
---
## Authentication & Authorization
### Authenticated Axios Instance
```javascript
// src/services/Helpers/AuthenticatedAxios.js
import axios from "axios";
import { camelizeKeys } from "humps";
import { API_ENDPOINTS } from "../Config/APIConfig";
import {
AUTH_TOKEN_TYPE,
HTTP_HEADERS,
HTTP_STATUS,
} from "@constants/Authentication";
export function createAuthenticatedAxios(
baseURL,
tokenStorage,
onUnauthorizedCallback = null
) {
const accessToken = tokenStorage.getAccessToken();
const authenticatedAxios = axios.create({
baseURL: baseURL,
headers: {
Authorization: `${AUTH_TOKEN_TYPE.JWT} ${accessToken}`,
"Content-Type": HTTP_HEADERS.CONTENT_TYPE_JSON,
Accept: HTTP_HEADERS.ACCEPT_JSON,
},
});
// Add response interceptor for automatic token refresh
authenticatedAxios.interceptors.response.use(
(response) => response,
async (error) => {
const originalConfig = error.config;
// Handle 401 unauthorized errors
if (error.response?.status === HTTP_STATUS.UNAUTHORIZED) {
const refreshToken = tokenStorage.getRefreshToken();
if (refreshToken) {
try {
// Attempt token refresh
const refreshResponse = await handleTokenRefresh(
baseURL,
refreshToken
);
if (refreshResponse?.status === HTTP_STATUS.OK) {
// Save new tokens
const newAccessToken = refreshResponse.data.access_token;
const newRefreshToken = refreshResponse.data.refresh_token;
tokenStorage.setAccessToken(newAccessToken);
tokenStorage.setRefreshToken(newRefreshToken);
// Retry original request with new token
const retryConfig = {
...originalConfig,
headers: {
...originalConfig.headers,
Authorization: `${AUTH_TOKEN_TYPE.JWT} ${newAccessToken}`,
},
};
return authenticatedAxios(retryConfig);
}
} catch (refreshError) {
console.error("Token refresh failed:", refreshError);
if (onUnauthorizedCallback) {
onUnauthorizedCallback();
}
}
} else if (onUnauthorizedCallback) {
onUnauthorizedCallback();
}
}
return Promise.reject(error.response?.data || error);
}
);
return authenticatedAxios;
}
async function handleTokenRefresh(baseURL, refreshToken) {
const refreshAxios = axios.create({
baseURL: baseURL,
headers: {
"Content-Type": HTTP_HEADERS.CONTENT_TYPE_JSON,
Accept: HTTP_HEADERS.ACCEPT_JSON,
Authorization: `${AUTH_TOKEN_TYPE.BEARER} ${refreshToken}`,
},
});
const refreshData = { value: refreshToken };
return await refreshAxios.post(API_ENDPOINTS.REFRESH_TOKEN, refreshData);
}
```
### Token Storage
```javascript
// src/services/Storage/TokenStorage.js
import { TOKEN_STORAGE_KEYS } from "@constants/Authentication";
export class TokenStorage {
/**
* Gets access token from localStorage
*/
getAccessToken() {
return localStorage.getItem(TOKEN_STORAGE_KEYS.ACCESS_TOKEN);
}
/**
* Sets access token in localStorage
*/
setAccessToken(token) {
if (token) {
localStorage.setItem(TOKEN_STORAGE_KEYS.ACCESS_TOKEN, token);
} else {
localStorage.removeItem(TOKEN_STORAGE_KEYS.ACCESS_TOKEN);
}
}
/**
* Gets refresh token from localStorage
*/
getRefreshToken() {
return localStorage.getItem(TOKEN_STORAGE_KEYS.REFRESH_TOKEN);
}
/**
* Sets refresh token in localStorage
*/
setRefreshToken(token) {
if (token) {
localStorage.setItem(TOKEN_STORAGE_KEYS.REFRESH_TOKEN, token);
} else {
localStorage.removeItem(TOKEN_STORAGE_KEYS.REFRESH_TOKEN);
}
}
/**
* Clears all tokens
*/
clearTokens() {
localStorage.removeItem(TOKEN_STORAGE_KEYS.ACCESS_TOKEN);
localStorage.removeItem(TOKEN_STORAGE_KEYS.REFRESH_TOKEN);
localStorage.removeItem(TOKEN_STORAGE_KEYS.USER_DATA);
}
/**
* Checks if user is authenticated
*/
isAuthenticated() {
return !!this.getAccessToken();
}
}
```
---
## Routing & Navigation
### Main Router (`src/AppRouter.jsx`)
```javascript
// src/AppRouter.jsx
import React from "react";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { useServices } from "./services/Services";
// Pages
import LoginPage from "./pages/Auth/Login/Page";
import DashboardPage from "./pages/Dashboard/Page";
import CustomerListPage from "./pages/Admin/Customer/List/Page";
import CustomerDetailPage from "./pages/Admin/Customer/Detail/Page";
import CustomerCreatePage from "./pages/Admin/Customer/Create/Page";
import CustomerUpdatePage from "./pages/Admin/Customer/Update/Page";
function AppRouter() {
return (
<BrowserRouter>
<Routes>
{/* Public routes */}
<Route path="/login" element={<LoginPage />} />
{/* Protected routes */}
<Route
path="/dashboard"
element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
}
/>
{/* Customer routes */}
<Route
path="/customers"
element={
<ProtectedRoute>
<CustomerListPage />
</ProtectedRoute>
}
/>
<Route
path="/customers/create"
element={
<ProtectedRoute>
<CustomerCreatePage />
</ProtectedRoute>
}
/>
<Route
path="/customers/:id"
element={
<ProtectedRoute>
<CustomerDetailPage />
</ProtectedRoute>
}
/>
<Route
path="/customers/:id/update"
element={
<ProtectedRoute>
<CustomerUpdatePage />
</ProtectedRoute>
}
/>
{/* Default redirect */}
<Route path="/" element={<Navigate to="/dashboard" replace />} />
</Routes>
</BrowserRouter>
);
}
function ProtectedRoute({ children }) {
const { authManager } = useServices();
if (!authManager.isAuthenticated()) {
return <Navigate to="/login" replace />;
}
return children;
}
export default AppRouter;
```
---
## State Management & Caching
### Caching Strategy
The application uses a two-tier caching strategy:
1. **Memory Cache**: Fastest, cleared on page reload
2. **localStorage Cache**: Persistent across sessions
### Example: List Page with Caching
```javascript
// src/pages/Admin/Customer/List/Page.jsx
import React, { useState, useEffect } from "react";
import { useServices } from "@services/Services";
import { CUSTOMERS_PER_PAGE } from "@constants/Customer";
export default function CustomerListPage() {
const { customerManager } = useServices();
const [customers, setCustomers] = useState([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [totalCount, setTotalCount] = useState(0);
useEffect(() => {
fetchCustomers();
}, [page]);
async function fetchCustomers(forceRefresh = false) {
try {
setLoading(true);
const data = await customerManager.getCustomers(
{
page: page,
limit: CUSTOMERS_PER_PAGE,
},
handleUnauthorized,
forceRefresh
);
setCustomers(data.results || []);
setTotalCount(data.count || 0);
} catch (error) {
console.error("Failed to fetch customers:", error);
// Show error toast
} finally {
setLoading(false);
}
}
function handleUnauthorized() {
window.location.href = "/login";
}
function handleRefresh() {
fetchCustomers(true); // Force refresh
}
return (
<div className="container mx-auto p-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold">Customers</h1>
<button
onClick={handleRefresh}
className="btn-secondary"
>
Refresh
</button>
</div>
{loading ? (
<LoadingSpinner />
) : (
<>
<CustomerTable customers={customers} />
<Pagination
currentPage={page}
totalCount={totalCount}
pageSize={CUSTOMERS_PER_PAGE}
onPageChange={setPage}
/>
</>
)}
</div>
);
}
```
---
## Styling & Theming
### Tailwind CSS Configuration
```javascript
// tailwind.config.js
export default {
content: [
"./index.html",
"./src/**/*.{js,jsx}",
],
theme: {
extend: {
screens: {
'xs': '400px',
},
colors: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
},
},
},
},
plugins: [],
}
```
### Global Styles (`src/styles/app.css`)
```css
@import "tailwindcss";
/* Base styles */
body {
@apply bg-gray-50 text-gray-900;
}
/* Utility classes */
.btn {
@apply px-4 py-2 rounded-md font-medium transition-colors;
}
.btn-primary {
@apply btn bg-blue-600 text-white hover:bg-blue-700;
}
.btn-secondary {
@apply btn bg-gray-600 text-white hover:bg-gray-700;
}
.form-input {
@apply block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm;
@apply focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent;
}
.form-select {
@apply form-input;
}
```
---
## Build Configuration
### Vite Configuration (`vite.config.js`)
```javascript
// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import path from "path";
export default defineConfig(({ mode }) => ({
plugins: [
react(),
tailwindcss(),
],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
"@components": path.resolve(__dirname, "./src/components"),
"@pages": path.resolve(__dirname, "./src/pages"),
"@services": path.resolve(__dirname, "./src/services"),
"@constants": path.resolve(__dirname, "./src/constants"),
"@utils": path.resolve(__dirname, "./src/utils"),
"@styles": path.resolve(__dirname, "./src/styles"),
},
},
server: {
port: 3000,
open: true,
cors: true,
proxy: {
"/api": {
target: process.env.VITE_API_URL || "http://localhost:8080",
changeOrigin: true,
secure: false,
},
},
},
build: {
outDir: "dist",
sourcemap: mode === "development" || mode === "staging",
minify: "terser",
terserOptions: {
compress: {
drop_console: mode === "production" || mode === "staging",
drop_debugger: true,
},
},
rollupOptions: {
output: {
manualChunks: {
"react-vendor": ["react", "react-dom", "react-router-dom"],
"ui-vendor": ["@heroicons/react", "clsx"],
"utils-vendor": ["axios", "luxon", "humps"],
},
},
},
chunkSizeWarningLimit: 1000,
},
optimizeDeps: {
include: [
"react",
"react-dom",
"react-router-dom",
"@heroicons/react/24/outline",
"@heroicons/react/24/solid",
],
},
}));
```
---
## Complete Setup Guide
### Step 1: Initialize Project
```bash
# Create new React project with Vite
npm create vite@latest my-app -- --template react
cd my-app
# Install dependencies
npm install react@^19.1.0 react-dom@^19.1.0
npm install react-router-dom@^7.8.0
npm install axios@^1.11.0
npm install luxon@^3.7.1
npm install humps@^2.0.1
npm install @heroicons/react@^2.2.0
npm install clsx@^2.1.1
npm install prop-types@^15.8.1
# Install dev dependencies
npm install -D tailwindcss@^4.1.12
npm install -D @tailwindcss/vite@^4.1.11
npm install -D @tailwindcss/postcss@^4.1.12
npm install -D vite@^7.1.2
npm install -D @vitejs/plugin-react@^4.7.0
npm install -D terser@^5.44.0
npm install -D eslint@^9.30.1
npm install -D @eslint/js@^9.30.1
npm install -D eslint-plugin-react-hooks@^5.2.0
```
### Step 2: Configure Vite
Create `vite.config.js` as shown in Build Configuration section.
### Step 3: Configure Tailwind
Create `tailwind.config.js` as shown in Styling section.
### Step 4: Create Directory Structure
```bash
mkdir -p src/{components/{UI,UIX,business,Layout},pages,services/{API,Storage,Manager,Config,Helpers},constants,utils,styles,assets}
```
### Step 5: Create Service Layer
1. **Create API Config**
```javascript
// src/services/Config/APIConfig.js
export function getAPIBaseURL() {
return import.meta.env.VITE_API_URL || "http://localhost:8080";
}
export const API_ENDPOINTS = {
// Auth
LOGIN: "/api/v1/auth/login",
LOGOUT: "/api/v1/auth/logout",
REFRESH_TOKEN: "/api/v1/auth/refresh",
// Customers
CUSTOMERS: "/api/v1/customers",
CUSTOMER_DETAIL: "/api/v1/customers/{id}",
CUSTOMER_COUNT: "/api/v1/customers/count",
};
export const ENV_CONFIG = {
API_URL: import.meta.env.VITE_API_URL,
ENV: import.meta.env.MODE,
};
```
2. **Create Storage Services** (TokenStorage, CustomerStorage, etc.)
3. **Create API Services** (AuthAPI, CustomerAPI, etc.)
4. **Create Manager Services** (AuthManager, CustomerManager, etc.)
5. **Create Services Container** (`src/services/Services.jsx`)
### Step 6: Create Constants
Create constant files for each domain (Customer, Auth, UI, etc.) as shown in Constants Management section.
### Step 7: Create Components
1. Create UI components (Button, Input, Modal, etc.)
2. Create Layout components (Sidebar, Header, Footer)
3. Create business components (CustomerSelect, etc.)
### Step 8: Create Pages
Create page components following the structure:
```
pages/
└── Admin/
└── Customer/
├── List/
│ └── Page.jsx
├── Detail/
│ └── Page.jsx
├── Create/
│ └── Page.jsx
└── Update/
└── Page.jsx
```
### Step 9: Setup Router
Create `AppRouter.jsx` as shown in Routing section.
### Step 10: Setup Main Entry
```javascript
// src/main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./AppRouter";
import { ServiceProvider } from "./services/Services";
import "./styles/app.css";
// Error Boundary
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error("Error:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="bg-white p-6 rounded-lg shadow-lg">
<h1 className="text-2xl font-bold text-red-600 mb-4">
Something went wrong
</h1>
<button
onClick={() => window.location.reload()}
className="btn-primary"
>
Reload Page
</button>
</div>
</div>
);
}
return this.props.children;
}
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<ErrorBoundary>
<ServiceProvider>
<App />
</ServiceProvider>
</ErrorBoundary>
</React.StrictMode>
);
```
### Step 11: Environment Variables
Create `.env` files:
```bash
# .env.development
VITE_API_URL=http://localhost:8080
# .env.production
VITE_API_URL=https://api.production.com
# .env.staging
VITE_API_URL=https://api.staging.com
```
### Step 12: Package.json Scripts
```json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"build:production": "vite build --mode production",
"build:staging": "vite build --mode staging",
"preview": "vite preview",
"lint": "eslint .",
"clean": "rm -rf node_modules dist",
"clean:install": "npm run clean && npm install"
}
}
```
---
## Best Practices & Patterns
### 1. Service Layer Rules
- **Components ONLY interact with Manager services**
- **Never call API services directly from components**
- **Never access Storage services directly from components**
- **Managers orchestrate API and Storage layers**
### 2. Constants Rules
- **ZERO TOLERANCE for magic values**
- **Every number, string, or config value must be a constant**
- **Check `src/constants/` before writing ANY code**
- **Create new constants if they don't exist**
### 3. Component Rules
- **UI components are pure and stateless**
- **Business components use the service layer**
- **Page components handle routing logic**
- **Always use PropTypes**
### 4. Error Handling
```javascript
// Consistent error handling pattern
try {
const data = await manager.getData();
setData(data);
} catch (error) {
console.error("Operation failed:", error);
// Show user-friendly error message
showToast("Failed to load data", "error");
} finally {
setLoading(false);
}
```
### 5. Caching Strategy
- **Use memory cache first (fastest)**
- **Fall back to localStorage**
- **Clear cache on mutations**
- **Implement loading flags to prevent duplicate requests**
### 6. Code Organization
```javascript
// Component structure order:
// 1. Imports
// 2. Component definition
// 3. State hooks
// 4. Effect hooks
// 5. Event handlers
// 6. Helper functions
// 7. Render logic
// 8. PropTypes
```
### 7. Naming Conventions
- **Components**: PascalCase (CustomerList)
- **Functions**: camelCase (fetchCustomers)
- **Constants**: UPPER_SNAKE_CASE (CUSTOMER_STATUS)
- **CSS Classes**: kebab-case (customer-list)
- **Files**: PascalCase for components, camelCase for utilities
### 8. Testing Checklist
Before deployment, verify:
- [ ] All magic values replaced with constants
- [ ] Components use Manager services only
- [ ] Error handling implemented
- [ ] Loading states handled
- [ ] Caching working correctly
- [ ] Authentication flow tested
- [ ] Token refresh working
- [ ] PropTypes defined
- [ ] Console logs removed (in production)
- [ ] Build succeeds without warnings
---
## Conclusion
This architecture provides:
- **Scalability**: Clear separation of concerns allows teams to work independently
- **Maintainability**: Consistent patterns make code easy to understand and modify
- **Testability**: Service layer isolation enables comprehensive testing
- **Performance**: Multi-tier caching reduces API calls
- **Reliability**: Automatic token refresh and error handling
- **Developer Experience**: Path aliases, constants, and clear patterns
Follow these patterns strictly to maintain consistency and quality across the entire application.
---
**Document Version**: 1.0.0
**Last Updated**: 2025-01-30
**Maintained By**: Development Team