Frontend Architecture - StockEase

Overview

The StockEase frontend is a modern React 18 application with TypeScript, built with Vite and styled with Tailwind CSS. It provides a responsive, feature-rich user interface for inventory management with multi-language support and dark mode capabilities.

Tech Stack: React 18 β€’ TypeScript 5.x β€’ Vite 5 β€’ Tailwind CSS 3 β€’ React Router 6
Deployment: Render (https://stockease-frontend.onrender.com)
Environment: Node.js 18+


Component Hierarchy

App (Root Component)
β”‚
β”œβ”€β”€ Layout Components
β”‚   β”œβ”€β”€ Header/Navigation Bar
β”‚   β”‚   β”œβ”€β”€ Logo
β”‚   β”œβ”€β”€ Dark Mode Toggle
β”‚   └── Language Switcher
β”‚
β”œβ”€β”€ Routes (React Router)
β”‚   β”œβ”€β”€ PublicRoutes
β”‚   β”‚   β”œβ”€β”€ LoginPage (/login)
β”‚   β”‚   β”œβ”€β”€ RegisterPage (/register)
β”‚   β”‚   └── NotFoundPage (404)
β”‚   β”‚
β”‚   └── ProtectedRoutes (JWT-required)
β”‚       β”œβ”€β”€ ProductDashboard (/dashboard)
β”‚       β”‚   β”œβ”€β”€ ProductList
β”‚       β”‚   β”‚   β”œβ”€β”€ ProductCard (reusable)
β”‚       β”‚   β”‚   β”œβ”€β”€ ProductFilter
β”‚       β”‚   β”‚   └── ProductPagination
β”‚       β”‚   β”œβ”€β”€ ProductForm (Create/Edit modal)
β”‚       β”‚   β”œβ”€β”€ ProductSearch
β”‚       β”‚   └── StockValueDisplay
β”‚       β”‚
β”‚       β”œβ”€β”€ AdminPanel (/admin) - Admin only
β”‚       β”‚   β”œβ”€β”€ UserManagement
β”‚       β”‚   β”œβ”€β”€ AuditLog
β”‚       β”‚   └── SystemSettings
β”‚       β”‚
β”‚       └── ProfilePage (/profile)
β”‚           β”œβ”€β”€ UserInfo
β”‚           └── PreferencesPanel
β”‚
└── Global Providers
    β”œβ”€β”€ QueryClientProvider (React Query)
    β”œβ”€β”€ i18nextProvider (i18next)
    β”œβ”€β”€ ThemeProvider (Dark Mode)
    └── AuthContext (JWT Management)

Layered Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     React Components & Pages        β”‚ ← UI Layer
β”‚  (Stateless presentational)         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   Custom Hooks & Services Layer     β”‚ ← Logic Layer
β”‚  (useQuery, useMutation, context)   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   API Integration (Axios)           β”‚ ← API Adapter Layer
β”‚  (Configured with interceptors)     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Backend REST API (Spring Boot)     β”‚ ← External
β”‚  Running at :8081                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Project Structure

frontend/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ pages/                        # Route-level components
β”‚   β”‚   β”œβ”€β”€ LoginPage.tsx
β”‚   β”‚   β”œβ”€β”€ RegisterPage.tsx
β”‚   β”‚   β”œβ”€β”€ ProductDashboard.tsx
β”‚   β”‚   β”œβ”€β”€ AdminPanel.tsx
β”‚   β”‚   └── NotFoundPage.tsx
β”‚   β”‚
β”‚   β”œβ”€β”€ components/                   # Reusable UI components
β”‚   β”‚   β”œβ”€β”€ Header/
β”‚   β”‚   β”œβ”€β”€ Navigation/
β”‚   β”‚   β”œβ”€β”€ ProductCard/
β”‚   β”‚   β”œβ”€β”€ ProductForm/
β”‚   β”‚   β”œβ”€β”€ ProductFilter/
β”‚   β”‚   β”œβ”€β”€ ProductPagination/
β”‚   β”‚   β”œβ”€β”€ Button/
β”‚   β”‚   β”œβ”€β”€ Modal/
β”‚   β”‚   └── FormField/
β”‚   β”‚
β”‚   β”œβ”€β”€ api/                          # API layer
β”‚   β”‚   β”œβ”€β”€ auth.ts                   # Auth endpoints
β”‚   β”‚   β”œβ”€β”€ products.ts               # Product endpoints
β”‚   β”‚   β”œβ”€β”€ index.ts                  # Axios config & interceptors
β”‚   β”‚   └── types.ts                  # API response types
β”‚   β”‚
β”‚   β”œβ”€β”€ hooks/                        # Custom React hooks
β”‚   β”‚   β”œβ”€β”€ useAuth.ts                # Auth context hook
β”‚   β”‚   β”œβ”€β”€ useProducts.ts            # Product queries
β”‚   β”‚   └── useLocalStorage.ts        # Persist preferences
β”‚   β”‚
β”‚   β”œβ”€β”€ services/                     # Business logic
β”‚   β”‚   β”œβ”€β”€ authService.ts
β”‚   β”‚   β”œβ”€β”€ productService.ts
β”‚   β”‚   └── storageService.ts
β”‚   β”‚
β”‚   β”œβ”€β”€ types/                        # TypeScript interfaces
β”‚   β”‚   β”œβ”€β”€ auth.types.ts
β”‚   β”‚   β”œβ”€β”€ product.types.ts
β”‚   β”‚   └── api.types.ts
β”‚   β”‚
β”‚   β”œβ”€β”€ styles/                       # Global styles
β”‚   β”‚   β”œβ”€β”€ globals.css
β”‚   β”‚   └── variables.css
β”‚   β”‚
β”‚   β”œβ”€β”€ locales/                      # i18n translations
β”‚   β”‚   β”œβ”€β”€ en.json                   # English
β”‚   β”‚   β”œβ”€β”€ de.json                   # German
β”‚   β”‚   └── index.ts
β”‚   β”‚
β”‚   β”œβ”€β”€ assets/                       # Static assets
β”‚   β”‚   β”œβ”€β”€ images/
β”‚   β”‚   β”œβ”€β”€ icons/
β”‚   β”‚   └── fonts/
β”‚   β”‚
β”‚   β”œβ”€β”€ App.tsx                       # Root component
β”‚   β”œβ”€β”€ main.tsx                      # Entry point
β”‚   β”œβ”€β”€ i18n.ts                       # i18n config
β”‚   └── vite-env.d.ts
β”‚
β”œβ”€β”€ public/                           # Static files (favicons, etc)
β”œβ”€β”€ package.json
β”œβ”€β”€ vite.config.ts
β”œβ”€β”€ tailwind.config.js
β”œβ”€β”€ tsconfig.json
β”œβ”€β”€ tsconfig.app.json
β”œβ”€β”€ tsconfig.node.json
β”œβ”€β”€ eslint.config.js
β”œβ”€β”€ postcss.config.cjs
β”œβ”€β”€ Dockerfile
β”œβ”€β”€ index.html
└── README.md

State Management Strategy

1. Server State (React Query)

Manages data fetched from the backend:

// useProducts hook
const { data: products, isLoading, error } = useQuery({
  queryKey: ['products', page, size],
  queryFn: () => api.getProducts(page, size)
});

Cached data: Products list, user info, pagination state
Refetch triggers: Manual refetch, window focus, interval

2. Client State (React Hooks)

Manages local component state:

const [filterText, setFilterText] = useState('');
const [sortBy, setSortBy] = useState('name');
const [showModal, setShowModal] = useState(false);

3. Global State (Context API)

Manages authentication and theme:

// AuthContext
const { user, token, login, logout, isAuthenticated } = useAuth();

// ThemeContext
const { isDark, toggleDarkMode } = useTheme();

// LanguageContext
const { language, setLanguage } = useI18n();

4. Persistent State (Local Storage)

Stores user preferences: - JWT token - Dark mode preference - Selected language - Recent filters


Authentication Flow

User Input (Username + Password)
     ↓
[Login Form Component]
     ↓
[API Call: POST /api/auth/login]
     ↓
[Backend validates credentials]
     ↓
[JWT Token + User Info returned]
     ↓
[Store token in Local Storage]
     ↓
[Update AuthContext]
     ↓
[Configure Axios Authorization Header]
     ↓
[Redirect to Dashboard]
     ↓
[Protected Routes Check JWT validity]

Token Management

// Store token after login
localStorage.setItem('token', response.token);
localStorage.setItem('user', JSON.stringify(response.user));

// Include in all API requests
api.interceptors.request.use(config => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Clear on logout
localStorage.removeItem('token');
localStorage.removeItem('user');

API Integration

Axios Configuration

// api/index.ts
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL 
  || 'http://localhost:8081/api';

const apiClient = axios.create({
  baseURL: API_BASE_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
});

// Request interceptor - add JWT token
apiClient.interceptors.request.use(config => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Response interceptor - handle errors globally
apiClient.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.status === 401) {
      // Token expired - redirect to login
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

API Service Modules

auth.ts

export const authAPI = {
  register: (username: string, password: string) => 
    apiClient.post('/auth/register', { username, password }),
  
  login: (username: string, password: string) => 
    apiClient.post('/auth/login', { username, password }),
  
  logout: () => {
    localStorage.removeItem('token');
    localStorage.removeItem('user');
  }
};

products.ts

export const productsAPI = {
  getAll: (page?: number, size?: number) =>
    apiClient.get('/products', { params: { page, size } }),
  
  getById: (id: number) =>
    apiClient.get(`/products/${id}`),
  
  create: (product: CreateProductDTO) =>
    apiClient.post('/products', product),
  
  update: (id: number, product: UpdateProductDTO) =>
    apiClient.put(`/products/${id}`, product),
  
  delete: (id: number) =>
    apiClient.delete(`/products/${id}`)
};

Multi-Language Support (i18next)

Configuration

// i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import enMessages from './locales/en.json';
import deMessages from './locales/de.json';

i18n.use(initReactI18next).init({
  resources: {
    en: { translation: enMessages },
    de: { translation: deMessages }
  },
  lng: localStorage.getItem('language') || 'en',
  fallbackLng: 'en',
  interpolation: { escapeValue: false }
});

Usage in Components

import { useTranslation } from 'react-i18next';

export const ProductCard = ({ product }) => {
  const { t, i18n } = useTranslation();
  
  return (
    <div>
      <h3>{product.name}</h3>
      <p>{t('product.quantity')}: {product.quantity}</p>
      <p>{t('product.price')}: ${product.price}</p>
      
      <button onClick={() => i18n.changeLanguage('de')}>
        {t('switch-language')}
      </button>
    </div>
  );
};

Supported Languages

  • English (en) - Default
  • German (de) - Full translation

Adding New Translations

  1. Add key-value pairs to locales/en.json and locales/de.json
  2. Use i18n.t('key.path') in components
  3. Test both languages

Dark Mode Support

Implementation

// hooks/useTheme.ts
export const useTheme = () => {
  const [isDark, setIsDark] = useState(
    localStorage.getItem('theme') === 'dark'
  );
  
  useEffect(() => {
    if (isDark) {
      document.documentElement.classList.add('dark');
    } else {
      document.documentElement.classList.remove('dark');
    }
    localStorage.setItem('theme', isDark ? 'dark' : 'light');
  }, [isDark]);
  
  return {
    isDark,
    toggleDarkMode: () => setIsDark(!isDark)
  };
};

Usage in Components

// Components automatically respond to dark: prefix in Tailwind
<div className="bg-white dark:bg-slate-900 text-black dark:text-white">
  {/* Content adapts to theme */}
</div>

CSS Variables

/* styles/variables.css */
:root {
  --color-primary: #3b82f6;
  --color-secondary: #ef4444;
  --background: #ffffff;
  --foreground: #000000;
}

.dark {
  --background: #1e293b;
  --foreground: #f1f5f9;
}

Responsive Design

Tailwind Breakpoints

  • sm: 640px
  • md: 768px
  • lg: 1024px
  • xl: 1280px
  • 2xl: 1536px

Mobile-First Approach

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
  {products.map(product => (
    <ProductCard key={product.id} product={product} />
  ))}
</div>

Component Responsive Patterns

  • Navigation: Hamburger menu on mobile, full navbar on desktop
  • Tables: Cards on mobile, table on desktop
  • Forms: Single column on mobile, multi-column on desktop
  • Images: Scaled proportionally with max-width constraints

Form Management

React Hook Form + TypeScript

import { useForm } from 'react-hook-form';

interface ProductFormData {
  name: string;
  quantity: number;
  price: number;
}

export const ProductForm = () => {
  const { register, handleSubmit, formState: { errors } } = useForm<ProductFormData>();
  
  const onSubmit = async (data: ProductFormData) => {
    await productsAPI.create(data);
  };
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name', { required: true })} />
      {errors.name && <span>Name is required</span>}
      
      <input {...register('quantity', { min: 1 })} type="number" />
      {errors.quantity && <span>Must be positive</span>}
      
      <button type="submit">Create Product</button>
    </form>
  );
};

Error Handling

Global Error Handler

// services/errorService.ts
export const handleApiError = (error: AxiosError) => {
  if (error.response?.status === 401) {
    return 'Session expired. Please login again.';
  }
  if (error.response?.status === 403) {
    return 'You do not have permission for this action.';
  }
  if (error.response?.status === 404) {
    return 'Resource not found.';
  }
  return error.message || 'An error occurred.';
};

Component Error Boundary

interface ErrorBoundaryState {
  hasError: boolean;
}

export class ErrorBoundary extends React.Component<any, ErrorBoundaryState> {
  componentDidCatch(error: Error) {
    this.setState({ hasError: true });
    console.error('Error caught:', error);
  }
  
  render() {
    if (this.state.hasError) {
      return <div>Something went wrong. Please refresh.</div>;
    }
    return this.props.children;
  }
}

Performance Optimization

1. Code Splitting

Vite automatically splits code by route:

const ProductDashboard = lazy(() => import('./pages/ProductDashboard'));
const AdminPanel = lazy(() => import('./pages/AdminPanel'));

<Suspense fallback={<Loading />}>
  <Routes>
    <Route path="/dashboard" element={<ProductDashboard />} />
    <Route path="/admin" element={<AdminPanel />} />
  </Routes>
</Suspense>

2. React Query Caching

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      cacheTime: 10 * 60 * 1000, // 10 minutes
    }
  }
});

3. Memoization

export const ProductCard = memo(({ product }: Props) => {
  return <div>{product.name}</div>;
}, (prevProps, nextProps) => 
  prevProps.product.id === nextProps.product.id
);

4. Image Optimization

  • Use optimized formats (WebP with fallbacks)
  • Lazy load images
  • Responsive images with srcset

5. Bundle Analysis

npm run build -- --analyze  # If analyzer plugin installed

Deployment

Environment Configuration

# .env.production
VITE_API_BASE_URL=https://stockease-backend-production.koyeb.app/api
VITE_APP_VERSION=1.0.0

Build Process

npm install                # Install dependencies
npm run build              # Build production bundle β†’ dist/
npm run preview            # Preview production build locally

Deployment to Render

  1. Connect GitHub repository to Render
  2. Set build command: npm run build
  3. Set publish directory: dist
  4. Add environment variables: VITE_API_BASE_URL
  5. Deploy on push to main branch

Performance Metrics

  • Bundle Size: ~150-200 KB (gzipped)
  • Build Time: 30-60 seconds
  • Lighthouse Score: 90+
  • Time to Interactive: <2 seconds

Testing

Unit Tests (Vitest)

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

describe('ProductCard', () => {
  it('renders product name', () => {
    const product = { id: 1, name: 'Test', price: 100, quantity: 5 };
    render(<ProductCard product={product} />);
    expect(screen.getByText('Test')).toBeInTheDocument();
  });
});

Integration Tests

  • Test API calls with mock server
  • Test authentication flow
  • Test form submission

E2E Tests

  • Consider Playwright or Cypress for future
  • Test complete user workflows

Browser Support

  • Chrome/Edge: Latest 2 versions
  • Firefox: Latest 2 versions
  • Safari: Latest 2 versions
  • Mobile browsers: Latest versions

Accessibility (a11y)

  • Semantic HTML structure
  • ARIA labels on form inputs
  • Keyboard navigation support
  • Color contrast ratios meet WCAG AA
  • Focus indicators on interactive elements

Main Architecture Topics

Architecture Decisions (ADRs)

Design Patterns & Practices

Infrastructure & Deployment


Frontend Documentation Version: 1.0
Last Updated: October 31, 2025
Status: Production Ready