Content is user-generated and unverified.

React TypeScript Frontend Development Guide

Building Modern Apps with Tailwind CSS & shadcn/ui


🎯 Phase 1: Foundation Setup

Step 1: Environment Setup

bash
# Create new React app with TypeScript
npx create-react-app my-app --template typescript
cd my-app

# Or using Vite (recommended for better performance)
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install

Step 2: Install Core Dependencies

bash
# Install Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

# Install shadcn/ui
npx shadcn-ui@latest init

# Install additional React hooks and utilities
npm install react-router-dom @types/react-router-dom
npm install axios react-query @tanstack/react-query

Step 3: Configure Tailwind CSS

typescript
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/**/*.{js,jsx,ts,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Key Learnings - Phase 1:

  • Vite vs Create React App: Vite offers faster development server and build times
  • TypeScript Benefits: Catch errors at compile time, better IDE support, self-documenting code
  • Tailwind Philosophy: Utility-first CSS for rapid development and consistent design systems

🏗️ Phase 2: Project Structure & TypeScript Setup

Step 4: Create Organized Folder Structure

src/
├── components/
│   ├── ui/           # shadcn/ui components
│   ├── common/       # Reusable components
│   └── forms/        # Form components
├── hooks/            # Custom React hooks
├── context/          # React Context providers
├── types/            # TypeScript type definitions
├── utils/            # Utility functions
├── pages/            # Route components
├── services/         # API calls
└── constants/        # App constants

Step 5: Setup TypeScript Types

typescript
// src/types/index.ts
export interface User {
  id: string;
  name: string;
  email: string;
  avatar?: string;
}

export interface ApiResponse<T> {
  data: T;
  message: string;
  success: boolean;
}

export interface AppState {
  user: User | null;
  isLoading: boolean;
  theme: 'light' | 'dark';
}

Step 6: Configure Path Aliases

typescript
// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@/components/*": ["src/components/*"],
      "@/hooks/*": ["src/hooks/*"],
      "@/types/*": ["src/types/*"]
    }
  }
}

Key Learnings - Phase 2:

  • File Organization: Clear structure improves maintainability and team collaboration
  • TypeScript Interfaces: Define contracts for your data structures
  • Path Aliases: Make imports cleaner and more readable

🔧 Phase 3: React Context & State Management

Step 7: Create App Context Provider

typescript
// src/context/AppContext.tsx
import React, { createContext, useContext, useReducer, ReactNode } from 'react';
import { AppState, User } from '@/types';

interface AppContextType {
  state: AppState;
  dispatch: React.Dispatch<AppAction>;
  login: (user: User) => void;
  logout: () => void;
  toggleTheme: () => void;
}

type AppAction = 
  | { type: 'SET_USER'; payload: User | null }
  | { type: 'SET_LOADING'; payload: boolean }
  | { type: 'TOGGLE_THEME' };

const initialState: AppState = {
  user: null,
  isLoading: false,
  theme: 'light'
};

const AppContext = createContext<AppContextType | undefined>(undefined);

function appReducer(state: AppState, action: AppAction): AppState {
  switch (action.type) {
    case 'SET_USER':
      return { ...state, user: action.payload };
    case 'SET_LOADING':
      return { ...state, isLoading: action.payload };
    case 'TOGGLE_THEME':
      return { ...state, theme: state.theme === 'light' ? 'dark' : 'light' };
    default:
      return state;
  }
}

export function AppProvider({ children }: { children: ReactNode }) {
  const [state, dispatch] = useReducer(appReducer, initialState);

  const login = (user: User) => {
    dispatch({ type: 'SET_USER', payload: user });
  };

  const logout = () => {
    dispatch({ type: 'SET_USER', payload: null });
  };

  const toggleTheme = () => {
    dispatch({ type: 'TOGGLE_THEME' });
  };

  return (
    <AppContext.Provider value={{
      state,
      dispatch,
      login,
      logout,
      toggleTheme
    }}>
      {children}
    </AppContext.Provider>
  );
}

export function useAppContext() {
  const context = useContext(AppContext);
  if (!context) {
    throw new Error('useAppContext must be used within AppProvider');
  }
  return context;
}

Step 8: Setup Theme Context

typescript
// src/context/ThemeContext.tsx
import React, { createContext, useContext, useEffect } from 'react';
import { useAppContext } from './AppContext';

const ThemeContext = createContext<{
  theme: 'light' | 'dark';
  toggleTheme: () => void;
} | undefined>(undefined);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const { state, toggleTheme } = useAppContext();

  useEffect(() => {
    document.documentElement.classList.toggle('dark', state.theme === 'dark');
  }, [state.theme]);

  return (
    <ThemeContext.Provider value={{ theme: state.theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

Key Learnings - Phase 3:

  • Context vs Props: Context prevents prop drilling for deeply nested components
  • useReducer vs useState: useReducer is better for complex state logic
  • Custom Hooks: Extract context logic into reusable hooks
  • TypeScript with Context: Proper typing prevents runtime errors

🪝 Phase 4: Custom React Hooks

Step 9: Create Custom Hooks

typescript
// src/hooks/useApi.ts
import { useState, useEffect } from 'react';
import { ApiResponse } from '@/types';

export function useApi<T>(
  apiFunction: () => Promise<ApiResponse<T>>,
  dependencies: any[] = []
) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);
        const response = await apiFunction();
        setData(response.data);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'An error occurred');
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, dependencies);

  return { data, loading, error, refetch: () => fetchData() };
}
typescript
// src/hooks/useLocalStorage.ts
import { useState, useEffect } from 'react';

export function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T) => void] {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });

  const setValue = (value: T) => {
    try {
      setStoredValue(value);
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error);
    }
  };

  return [storedValue, setValue];
}
typescript
// src/hooks/useDebounce.ts
import { useState, useEffect } from 'react';

export function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

Key Learnings - Phase 4:

  • Custom Hooks: Extract component logic into reusable functions
  • Generic Types: Make hooks reusable with different data types
  • Effect Dependencies: Properly manage when effects run
  • Error Handling: Always handle potential errors in async operations

🎨 Phase 5: shadcn/ui Components

Step 10: Install and Setup shadcn/ui Components

bash
# Install common components
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add input
npx shadcn-ui@latest add form
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add toast

Step 11: Create Composite Components

typescript
// src/components/ui/LoadingSpinner.tsx
import { cn } from '@/lib/utils';

interface LoadingSpinnerProps {
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

export function LoadingSpinner({ size = 'md', className }: LoadingSpinnerProps) {
  const sizeClasses = {
    sm: 'h-4 w-4',
    md: 'h-6 w-6',
    lg: 'h-8 w-8'
  };

  return (
    <div
      className={cn(
        'animate-spin rounded-full border-2 border-gray-300 border-t-blue-600',
        sizeClasses[size],
        className
      )}
    />
  );
}
typescript
// src/components/common/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';

interface Props {
  children: ReactNode;
}

interface State {
  hasError: boolean;
  error?: Error;
}

export class ErrorBoundary extends Component<Props, State> {
  public state: State = {
    hasError: false
  };

  public static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('Uncaught error:', error, errorInfo);
  }

  public render() {
    if (this.state.hasError) {
      return (
        <div className="flex items-center justify-center min-h-screen">
          <div className="text-center">
            <h2 className="text-2xl font-bold text-red-600">Something went wrong!</h2>
            <p className="mt-2 text-gray-600">{this.state.error?.message}</p>
            <button
              className="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
              onClick={() => this.setState({ hasError: false })}
            >
              Try again
            </button>
          </div>
        </div>
      );
    }

    return this.props.children;
  }
}

Key Learnings - Phase 5:

  • shadcn/ui Philosophy: Copy components into your project for full customization
  • Compound Components: Build complex UIs from simple, reusable pieces
  • Error Boundaries: Catch JavaScript errors in component tree
  • Conditional Rendering: Handle different UI states elegantly

🔄 Phase 6: Forms & Validation

Step 12: Setup Form Handling

bash
npm install react-hook-form @hookform/resolvers zod
typescript
// src/components/forms/ContactForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';

const contactSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Invalid email address'),
  message: z.string().min(10, 'Message must be at least 10 characters')
});

type ContactFormData = z.infer<typeof contactSchema>;

export function ContactForm() {
  const form = useForm<ContactFormData>({
    resolver: zodResolver(contactSchema),
    defaultValues: {
      name: '',
      email: '',
      message: ''
    }
  });

  const onSubmit = async (data: ContactFormData) => {
    try {
      // Handle form submission
      console.log('Form data:', data);
      // Reset form after successful submission
      form.reset();
    } catch (error) {
      console.error('Form submission error:', error);
    }
  };

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <FormField
          control={form.control}
          name="name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Name</FormLabel>
              <FormControl>
                <Input placeholder="Enter your name" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl>
                <Input type="email" placeholder="Enter your email" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        
        <FormField
          control={form.control}
          name="message"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Message</FormLabel>
              <FormControl>
                <textarea
                  className="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
                  placeholder="Enter your message"
                  {...field}
                />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        
        <Button type="submit" disabled={form.formState.isSubmitting}>
          {form.formState.isSubmitting ? 'Sending...' : 'Send Message'}
        </Button>
      </form>
    </Form>
  );
}

Key Learnings - Phase 6:

  • React Hook Form: Efficient form handling with minimal re-renders
  • Zod Validation: Type-safe runtime validation
  • Form State Management: Handle loading, errors, and success states
  • Accessibility: Proper form labels and error messages

🌐 Phase 7: API Integration & Data Fetching

Step 13: Setup API Layer

typescript
// src/services/api.ts
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import { ApiResponse } from '@/types';

class ApiClient {
  private client: AxiosInstance;

  constructor() {
    this.client = axios.create({
      baseURL: process.env.REACT_APP_API_URL || 'https://api.example.com',
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json'
      }
    });

    this.setupInterceptors();
  }

  private setupInterceptors() {
    // Request interceptor
    this.client.interceptors.request.use(
      (config) => {
        const token = localStorage.getItem('authToken');
        if (token) {
          config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
      },
      (error) => Promise.reject(error)
    );

    // Response interceptor
    this.client.interceptors.response.use(
      (response: AxiosResponse) => response,
      (error) => {
        if (error.response?.status === 401) {
          // Handle unauthorized access
          localStorage.removeItem('authToken');
          window.location.href = '/login';
        }
        return Promise.reject(error);
      }
    );
  }

  async get<T>(url: string): Promise<ApiResponse<T>> {
    const response = await this.client.get<ApiResponse<T>>(url);
    return response.data;
  }

  async post<T>(url: string, data: any): Promise<ApiResponse<T>> {
    const response = await this.client.post<ApiResponse<T>>(url, data);
    return response.data;
  }

  async put<T>(url: string, data: any): Promise<ApiResponse<T>> {
    const response = await this.client.put<ApiResponse<T>>(url, data);
    return response.data;
  }

  async delete<T>(url: string): Promise<ApiResponse<T>> {
    const response = await this.client.delete<ApiResponse<T>>(url);
    return response.data;
  }
}

export const apiClient = new ApiClient();

Step 14: Create Data Fetching Hooks

typescript
// src/hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '@/services/api';
import { User } from '@/types';

export function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: () => apiClient.get<User[]>('/users'),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
}

export function useCreateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (userData: Omit<User, 'id'>) => 
      apiClient.post<User>('/users', userData),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
}

Key Learnings - Phase 7:

  • API Architecture: Centralized API client with interceptors
  • React Query: Powerful data fetching with caching and synchronization
  • Error Handling: Proper error boundaries and user feedback
  • Authentication: Token management and automatic logout

🎨 Phase 8: Styling & Theming

Step 15: Advanced Tailwind Configuration

javascript
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  darkMode: 'class',
  content: [
    "./src/**/*.{js,jsx,ts,tsx}",
  ],
  theme: {
    extend: {
      colors: {
        border: "hsl(var(--border))",
        input: "hsl(var(--input))",
        ring: "hsl(var(--ring))",
        background: "hsl(var(--background))",
        foreground: "hsl(var(--foreground))",
        primary: {
          DEFAULT: "hsl(var(--primary))",
          foreground: "hsl(var(--primary-foreground))",
        },
        secondary: {
          DEFAULT: "hsl(var(--secondary))",
          foreground: "hsl(var(--secondary-foreground))",
        },
        destructive: {
          DEFAULT: "hsl(var(--destructive))",
          foreground: "hsl(var(--destructive-foreground))",
        },
        muted: {
          DEFAULT: "hsl(var(--muted))",
          foreground: "hsl(var(--muted-foreground))",
        },
        accent: {
          DEFAULT: "hsl(var(--accent))",
          foreground: "hsl(var(--accent-foreground))",
        },
        popover: {
          DEFAULT: "hsl(var(--popover))",
          foreground: "hsl(var(--popover-foreground))",
        },
        card: {
          DEFAULT: "hsl(var(--card))",
          foreground: "hsl(var(--card-foreground))",
        },
      },
      borderRadius: {
        lg: "var(--radius)",
        md: "calc(var(--radius) - 2px)",
        sm: "calc(var(--radius) - 4px)",
      },
      animation: {
        "accordion-down": "accordion-down 0.2s ease-out",
        "accordion-up": "accordion-up 0.2s ease-out",
      },
    },
  },
  plugins: [require("tailwindcss-animate")],
}

Step 16: CSS Custom Properties

css
/* src/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;
    --card: 0 0% 100%;
    --card-foreground: 222.2 84% 4.9%;
    --popover: 0 0% 100%;
    --popover-foreground: 222.2 84% 4.9%;
    --primary: 222.2 47.4% 11.2%;
    --primary-foreground: 210 40% 98%;
    --secondary: 210 40% 96%;
    --secondary-foreground: 222.2 47.4% 11.2%;
    --muted: 210 40% 96%;
    --muted-foreground: 215.4 16.3% 46.9%;
    --accent: 210 40% 96%;
    --accent-foreground: 222.2 47.4% 11.2%;
    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 210 40% 98%;
    --border: 214.3 31.8% 91.4%;
    --input: 214.3 31.8% 91.4%;
    --ring: 222.2 84% 4.9%;
    --radius: 0.5rem;
  }

  .dark {
    --background: 222.2 84% 4.9%;
    --foreground: 210 40% 98%;
    --card: 222.2 84% 4.9%;
    --card-foreground: 210 40% 98%;
    --popover: 222.2 84% 4.9%;
    --popover-foreground: 210 40% 98%;
    --primary: 210 40% 98%;
    --primary-foreground: 222.2 47.4% 11.2%;
    --secondary: 217.2 32.6% 17.5%;
    --secondary-foreground: 210 40% 98%;
    --muted: 217.2 32.6% 17.5%;
    --muted-foreground: 215 20.2% 65.1%;
    --accent: 217.2 32.6% 17.5%;
    --accent-foreground: 210 40% 98%;
    --destructive: 0 62.8% 30.6%;
    --destructive-foreground: 210 40% 98%;
    --border: 217.2 32.6% 17.5%;
    --input: 217.2 32.6% 17.5%;
    --ring: 212.7 26.8% 83.9%;
  }
}

@layer base {
  * {
    @apply border-border;
  }
  body {
    @apply bg-background text-foreground;
  }
}

Key Learnings - Phase 8:

  • CSS Custom Properties: Enable dynamic theming
  • Dark Mode: Implement system-wide theme switching
  • Design Tokens: Consistent spacing, colors, and typography
  • Component Styling: Balance utility classes with maintainability

🚀 Phase 9: Performance & Optimization

Step 17: Implement Code Splitting

typescript
// src/pages/LazyPages.tsx
import { lazy } from 'react';

export const HomePage = lazy(() => import('./HomePage'));
export const AboutPage = lazy(() => import('./AboutPage'));
export const ContactPage = lazy(() => import('./ContactPage'));
export const UserProfile = lazy(() => import('./UserProfile'));

Step 18: Optimize with React.memo and useMemo

typescript
// src/components/UserCard.tsx
import React, { memo } from 'react';
import { User } from '@/types';

interface UserCardProps {
  user: User;
  onEdit: (userId: string) => void;
  onDelete: (userId: string) => void;
}

export const UserCard = memo(({ user, onEdit, onDelete }: UserCardProps) => {
  return (
    <div className="p-4 border rounded-lg">
      <h3 className="font-semibold">{user.name}</h3>
      <p className="text-gray-600">{user.email}</p>
      <div className="mt-2 space-x-2">
        <button
          onClick={() => onEdit(user.id)}
          className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
        >
          Edit
        </button>
        <button
          onClick={() => onDelete(user.id)}
          className="px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600"
        >
          Delete
        </button>
      </div>
    </div>
  );
});

UserCard.displayName = 'UserCard';

Step 19: Setup Error Tracking

typescript
// src/utils/errorTracking.ts
export function logError(error: Error, context?: string) {
  // In production, you'd send this to a service like Sentry
  console.error('Error:', error);
  if (context) {
    console.error('Context:', context);
  }
  
  // Example: Send to error tracking service
  // Sentry.captureException(error, { extra: { context } });
}

Key Learnings - Phase 9:

  • Code Splitting: Reduce initial bundle size with lazy loading
  • React.memo: Prevent unnecessary re-renders of expensive components
  • useMemo/useCallback: Optimize expensive calculations and function references
  • Error Tracking: Monitor and fix production issues

🧪 Phase 10: Testing & Quality Assurance

Step 20: Setup Testing Environment

bash
npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event
typescript
// src/components/__tests__/UserCard.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { UserCard } from '../UserCard';
import { User } from '@/types';

const mockUser: User = {
  id: '1',
  name: 'John Doe',
  email: 'john@example.com'
};

describe('UserCard', () => {
  it('renders user information', () => {
    const mockOnEdit = jest.fn();
    const mockOnDelete = jest.fn();

    render(
      <UserCard
        user={mockUser}
        onEdit={mockOnEdit}
        onDelete={mockOnDelete}
      />
    );

    expect(screen.getByText('John Doe')).toBeInTheDocument();
    expect(screen.getByText('john@example.com')).toBeInTheDocument();
  });

  it('calls onEdit when edit button is clicked', () => {
    const mockOnEdit = jest.fn();
    const mockOnDelete = jest.fn();

    render(
      <UserCard
        user={mockUser}
        onEdit={mockOnEdit}
        onDelete={mockOnDelete}
      />
    );

    fireEvent.click(screen.getByText('Edit'));
    expect(mockOnEdit).toHaveBeenCalledWith('1');
  });
});

Step 21: Setup ESLint and Prettier

bash
npm install -D eslint prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser
json
// .eslintrc.json
{
  "extends": [
    "react-app",
    "react-app/jest",
    "@typescript-eslint/recommended"
  ],
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint"],
  "rules": {
    "@typescript-eslint/no-unused-vars": "error",
    "@typescript-eslint/no-explicit-any": "warn",
    "react-hooks/exhaustive-deps": "warn"
  }
}

Key Learnings - Phase 10:

  • Testing Strategy: Unit tests for components, integration tests for features
  • Code Quality: ESLint and Prettier ensure consistent code style
  • Test-Driven Development: Write tests before implementing features
  • Coverage: Aim for meaningful tests, not just high coverage numbers

🔧 Phase 11: Advanced Patterns & Optimization

Step 22: Implement Advanced React Patterns

typescript
// src/patterns/RenderProps.tsx
interface RenderPropsExample<T> {
  data: T;
  loading: boolean;
  error: string | null;
  children: (props: { data: T; loading: boolean; error: string | null }) => React.ReactNode;
}

export function DataProvider<T>({ data, loading, error, children }: RenderPropsExample<T>) {
  return <>{children({ data, loading, error })}</>;
}

// Usage
function UserList() {
  const { data: users, loading, error } = useUsers();
  
  return (
    <DataProvider data={users} loading={loading} error={error}>
      {({ data, loading, error }) => {
        if (loading) return <LoadingSpinner />;
        if (error) return <div>Error: {error}</div>;
        return (
          <div>
            {data?.map(user => (
              <UserCard key={user.id} user={user} />
            ))}
          </div>
        );
      }}
    </DataProvider>
  );
}

Step 23: Higher-Order Components (HOCs)

typescript
// src/hocs/withAuth.tsx
import React, { ComponentType } from 'react';
import { useAppContext } from '@/context/AppContext';
import { Navigate } from 'react-router-dom';

interface WithAuthProps {
  // Any additional props the HOC might need
}

export function withAuth<P extends object>(
  WrappedComponent: ComponentType<P>
) {
  const AuthenticatedComponent = (props: P & WithAuthProps) => {
    const { state } = useAppContext();

    if (!state.user) {
      return <Navigate to="/login" replace />;
    }

    return <WrappedComponent {...props} />;
  };

  AuthenticatedComponent.displayName = `withAuth(${WrappedComponent.displayName || WrappedComponent.name})`;

  return AuthenticatedComponent;
}

// Usage
export const ProtectedDashboard = withAuth(Dashboard);

Step 24: Custom Hook Composition

typescript
// src/hooks/useUserActions.ts
import { useCallback } from 'react';
import { useAppContext } from '@/context/AppContext';
import { useCreateUser, useUpdateUser, useDeleteUser } from './useUsers';
import { User } from '@/types';

export function useUserActions() {
  const { state } = useAppContext();
  const createUserMutation = useCreateUser();
  const updateUserMutation = useUpdateUser();
  const deleteUserMutation = useDeleteUser();

  const createUser = useCallback(async (userData: Omit<User, 'id'>) => {
    try {
      await createUserMutation.mutateAsync(userData);
      // Handle success (show toast, etc.)
    } catch (error) {
      // Handle error
      console.error('Failed to create user:', error);
    }
  }, [createUserMutation]);

  const updateUser = useCallback(async (userId: string, userData: Partial<User>) => {
    try {
      await updateUserMutation.mutateAsync({ id: userId, ...userData });
    } catch (error) {
      console.error('Failed to update user:', error);
    }
  }, [updateUserMutation]);

  const deleteUser = useCallback(async (userId: string) => {
    if (window.confirm('Are you sure you want to delete this user?')) {
      try {
        await deleteUserMutation.mutateAsync(userId);
      } catch (error) {
        console.error('Failed to delete user:', error);
      }
    }
  }, [deleteUserMutation]);

  return {
    createUser,
    updateUser,
    deleteUser,
    isLoading: createUserMutation.isPending || updateUserMutation.isPending || deleteUserMutation.isPending,
    currentUser: state.user
  };
}

Step 25: Implement Virtual Scrolling for Large Lists

typescript
// src/components/VirtualList.tsx
import React, { useState, useMemo, useCallback } from 'react';
import { FixedSizeList as List } from 'react-window';

interface VirtualListProps<T> {
  items: T[];
  height: number;
  itemHeight: number;
  renderItem: (item: T, index: number) => React.ReactNode;
}

export function VirtualList<T>({ 
  items, 
  height, 
  itemHeight, 
  renderItem 
}: VirtualListProps<T>) {
  const Row = useCallback(({ index, style }: { index: number; style: React.CSSProperties }) => (
    <div style={style}>
      {renderItem(items[index], index)}
    </div>
  ), [items, renderItem]);

  return (
    <List
      height={height}
      itemCount={items.length}
      itemSize={itemHeight}
      width="100%"
    >
      {Row}
    </List>
  );
}

Key Learnings - Phase 11:

  • Render Props: Share stateful logic between components
  • HOCs: Add cross-cutting concerns like authentication
  • Hook Composition: Combine multiple hooks for complex functionality
  • Virtual Scrolling: Handle large datasets efficiently

🌐 Phase 12: Routing & Navigation

Step 26: Setup React Router with TypeScript

typescript
// src/types/routes.ts
export interface RouteParams {
  userId?: string;
  postId?: string;
}

export interface LocationState {
  from?: string;
  message?: string;
}
typescript
// src/router/AppRouter.tsx
import React, { Suspense } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { ErrorBoundary } from '@/components/common/ErrorBoundary';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { Layout } from '@/components/layout/Layout';
import { HomePage, AboutPage, ContactPage, UserProfile } from '@/pages/LazyPages';
import { withAuth } from '@/hocs/withAuth';

const ProtectedUserProfile = withAuth(UserProfile);

export function AppRouter() {
  return (
    <BrowserRouter>
      <ErrorBoundary>
        <Routes>
          <Route path="/" element={<Layout />}>
            <Route index element={
              <Suspense fallback={<LoadingSpinner />}>
                <HomePage />
              </Suspense>
            } />
            <Route path="about" element={
              <Suspense fallback={<LoadingSpinner />}>
                <AboutPage />
              </Suspense>
            } />
            <Route path="contact" element={
              <Suspense fallback={<LoadingSpinner />}>
                <ContactPage />
              </Suspense>
            } />
            <Route path="users/:userId" element={
              <Suspense fallback={<LoadingSpinner />}>
                <ProtectedUserProfile />
              </Suspense>
            } />
            <Route path="*" element={<Navigate to="/" replace />} />
          </Route>
        </Routes>
      </ErrorBoundary>
    </BrowserRouter>
  );
}

Step 27: Create Layout Components

typescript
// src/components/layout/Layout.tsx
import React from 'react';
import { Outlet } from 'react-router-dom';
import { Header } from './Header';
import { Footer } from './Footer';
import { Sidebar } from './Sidebar';

export function Layout() {
  return (
    <div className="min-h-screen flex flex-col">
      <Header />
      <div className="flex flex-1">
        <Sidebar />
        <main className="flex-1 p-6">
          <Outlet />
        </main>
      </div>
      <Footer />
    </div>
  );
}
typescript
// src/components/layout/Header.tsx
import React from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { useAppContext } from '@/context/AppContext';
import { useTheme } from '@/context/ThemeContext';

export function Header() {
  const { state, logout } = useAppContext();
  const { theme, toggleTheme } = useTheme();
  const navigate = useNavigate();

  const handleLogout = () => {
    logout();
    navigate('/');
  };

  return (
    <header className="bg-white dark:bg-gray-800 shadow-md">
      <div className="container mx-auto px-4 py-4 flex justify-between items-center">
        <Link to="/" className="text-2xl font-bold text-primary">
          MyApp
        </Link>
        
        <nav className="hidden md:flex space-x-6">
          <Link to="/" className="hover:text-primary transition-colors">
            Home
          </Link>
          <Link to="/about" className="hover:text-primary transition-colors">
            About
          </Link>
          <Link to="/contact" className="hover:text-primary transition-colors">
            Contact
          </Link>
        </nav>

        <div className="flex items-center space-x-4">
          <Button
            variant="ghost"
            size="sm"
            onClick={toggleTheme}
            className="p-2"
          >
            {theme === 'light' ? '🌙' : '☀️'}
          </Button>
          
          {state.user ? (
            <div className="flex items-center space-x-2">
              <span className="text-sm">Hello, {state.user.name}</span>
              <Button variant="outline" size="sm" onClick={handleLogout}>
                Logout
              </Button>
            </div>
          ) : (
            <Link to="/login">
              <Button size="sm">Login</Button>
            </Link>
          )}
        </div>
      </div>
    </header>
  );
}

Key Learnings - Phase 12:

  • Route Protection: Combine HOCs with routing for authentication
  • Lazy Loading: Improve performance with code splitting
  • Layout Patterns: Consistent UI structure across pages
  • Navigation State: Manage active states and transitions

🔒 Phase 13: Security & Best Practices

Step 28: Input Validation & Sanitization

typescript
// src/utils/validation.ts
import DOMPurify from 'dompurify';

export function sanitizeInput(input: string): string {
  return DOMPurify.sanitize(input, { ALLOWED_TAGS: [] });
}

export function validateEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

export function validatePassword(password: string): {
  isValid: boolean;
  errors: string[];
} {
  const errors: string[] = [];
  
  if (password.length < 8) {
    errors.push('Password must be at least 8 characters long');
  }
  
  if (!/(?=.*[a-z])/.test(password)) {
    errors.push('Password must contain at least one lowercase letter');
  }
  
  if (!/(?=.*[A-Z])/.test(password)) {
    errors.push('Password must contain at least one uppercase letter');
  }
  
  if (!/(?=.*\d)/.test(password)) {
    errors.push('Password must contain at least one number');
  }
  
  if (!/(?=.*[@$!%*?&])/.test(password)) {
    errors.push('Password must contain at least one special character');
  }

  return {
    isValid: errors.length === 0,
    errors
  };
}

Step 29: Environment Configuration

typescript
// src/config/environment.ts
interface Config {
  apiUrl: string;
  environment: 'development' | 'staging' | 'production';
  enableDebug: boolean;
  features: {
    analytics: boolean;
    errorTracking: boolean;
  };
}

function getConfig(): Config {
  const environment = (process.env.NODE_ENV as Config['environment']) || 'development';
  
  return {
    apiUrl: process.env.REACT_APP_API_URL || 'http://localhost:3001',
    environment,
    enableDebug: environment === 'development',
    features: {
      analytics: process.env.REACT_APP_ENABLE_ANALYTICS === 'true',
      errorTracking: process.env.REACT_APP_ENABLE_ERROR_TRACKING === 'true'
    }
  };
}

export const config = getConfig();

Step 30: Content Security Policy

typescript
// src/utils/security.ts
export function setupCSP() {
  // In a real app, this would be set via HTTP headers
  const meta = document.createElement('meta');
  meta.httpEquiv = 'Content-Security-Policy';
  meta.content = [
    "default-src 'self'",
    "script-src 'self' 'unsafe-inline'",
    "style-src 'self' 'unsafe-inline'",
    "img-src 'self' data: https:",
    "connect-src 'self' https://api.example.com",
    "font-src 'self'"
  ].join('; ');
  
  document.head.appendChild(meta);
}

Key Learnings - Phase 13:

  • Input Sanitization: Prevent XSS attacks with proper validation
  • Environment Management: Separate configurations for different environments
  • Security Headers: Implement CSP and other security measures
  • Authentication Security: Secure token storage and transmission

📱 Phase 14: Responsive Design & Accessibility

Step 31: Responsive Design Patterns

typescript
// src/hooks/useMediaQuery.ts
import { useState, useEffect } from 'react';

export function useMediaQuery(query: string): boolean {
  const [matches, setMatches] = useState(false);

  useEffect(() => {
    const media = window.matchMedia(query);
    
    if (media.matches !== matches) {
      setMatches(media.matches);
    }
    
    const listener = () => setMatches(media.matches);
    media.addEventListener('change', listener);
    
    return () => media.removeEventListener('change', listener);
  }, [matches, query]);

  return matches;
}

// Usage
export function ResponsiveComponent() {
  const isMobile = useMediaQuery('(max-width: 768px)');
  const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
  const isDesktop = useMediaQuery('(min-width: 1025px)');

  return (
    <div className={`
      ${isMobile ? 'px-4' : ''}
      ${isTablet ? 'px-8' : ''}
      ${isDesktop ? 'px-12' : ''}
    `}>
      {isMobile && <MobileView />}
      {isTablet && <TabletView />}
      {isDesktop && <DesktopView />}
    </div>
  );
}

Step 32: Accessibility Features

typescript
// src/components/accessible/SkipLink.tsx
import React from 'react';

export function SkipLink() {
  return (
    <a
      href="#main-content"
      className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 
                 bg-primary text-primary-foreground px-4 py-2 rounded-md z-50"
    >
      Skip to main content
    </a>
  );
}
typescript
// src/hooks/useAnnouncement.ts
import { useCallback } from 'react';

export function useAnnouncement() {
  const announce = useCallback((message: string, priority: 'polite' | 'assertive' = 'polite') => {
    const announcement = document.createElement('div');
    announcement.setAttribute('aria-live', priority);
    announcement.setAttribute('aria-atomic', 'true');
    announcement.className = 'sr-only';
    announcement.textContent = message;
    
    document.body.appendChild(announcement);
    
    setTimeout(() => {
      document.body.removeChild(announcement);
    }, 1000);
  }, []);

  return { announce };
}

Step 33: Focus Management

typescript
// src/hooks/useFocusTrap.ts
import { useEffect, useRef } from 'react';

export function useFocusTrap(isActive: boolean) {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!isActive) return;

    const container = containerRef.current;
    if (!container) return;

    const focusableElements = container.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    
    const firstElement = focusableElements[0] as HTMLElement;
    const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;

    const handleTabKeyPress = (event: KeyboardEvent) => {
      if (event.key === 'Tab') {
        if (event.shiftKey) {
          if (document.activeElement === firstElement) {
            lastElement.focus();
            event.preventDefault();
          }
        } else {
          if (document.activeElement === lastElement) {
            firstElement.focus();
            event.preventDefault();
          }
        }
      }
    };

    document.addEventListener('keydown', handleTabKeyPress);
    firstElement?.focus();

    return () => {
      document.removeEventListener('keydown', handleTabKeyPress);
    };
  }, [isActive]);

  return containerRef;
}

Key Learnings - Phase 14:

  • Responsive Design: Use CSS Grid, Flexbox, and media queries effectively
  • Accessibility: Implement WCAG guidelines for inclusive design
  • Focus Management: Handle keyboard navigation properly
  • Screen Reader Support: Use ARIA labels and live regions

🚀 Phase 15: Deployment & Production

Step 34: Build Optimization

json
// package.json - Add build scripts
{
  "scripts": {
    "build": "react-scripts build",
    "build:analyze": "npm run build && npx bundle-analyzer build/static/js/*.js",
    "build:staging": "REACT_APP_ENV=staging npm run build",
    "build:production": "REACT_APP_ENV=production npm run build"
  }
}

Step 35: Docker Configuration

dockerfile
# Dockerfile
FROM node:18-alpine as builder

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nginx
# nginx.conf
server {
    listen 80;
    location / {
        root /usr/share/nginx/html;
        index index.html index.htm;
        try_files $uri $uri/ /index.html;
    }
}

Step 36: CI/CD Pipeline (GitHub Actions)

yaml
# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - run: npm ci
      - run: npm run test -- --coverage --watchAll=false
      - run: npm run build

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
      - uses: actions/checkout@v3
      - name: Deploy to production
        run: |
          # Your deployment commands here
          echo "Deploying to production..."

Key Learnings - Phase 15:

  • Build Optimization: Analyze bundle size and optimize imports
  • Containerization: Use Docker for consistent deployments
  • CI/CD: Automate testing and deployment processes
  • Environment Management: Handle different environments properly

🎓 Final Integration & Best Practices

Step 37: App.tsx - Putting It All Together

typescript
// src/App.tsx
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { AppProvider } from '@/context/AppContext';
import { ThemeProvider } from '@/context/ThemeContext';
import { AppRouter } from '@/router/AppRouter';
import { Toaster } from '@/components/ui/toaster';
import { SkipLink } from '@/components/accessible/SkipLink';
import { config } from '@/config/environment';
import './globals.css';

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

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <AppProvider>
        <ThemeProvider>
          <SkipLink />
          <AppRouter />
          <Toaster />
          {config.enableDebug && <ReactQueryDevtools />}
        </ThemeProvider>
      </AppProvider>
    </QueryClientProvider>
  );
}

export default App;

🎯 Summary of Key Architectural Decisions:

  1. State Management: React Context + useReducer for global state, React Query for server state
  2. Styling: Tailwind CSS with custom design system and dark mode support
  3. Type Safety: Comprehensive TypeScript integration throughout the application
  4. Performance: Code splitting, memoization, and virtual scrolling for large datasets
  5. Security: Input validation, sanitization, and proper authentication handling
  6. Accessibility: WCAG compliance with proper ARIA labels and keyboard navigation
  7. Testing: Unit tests with React Testing Library and comprehensive error boundaries
  8. Developer Experience: ESLint, Prettier, and hot reloading for rapid development

🚀 Next Steps for Advanced Development:

  • Micro-frontends: Split large applications into smaller, manageable pieces
  • Server-Side Rendering: Implement Next.js for better SEO and performance
  • Progressive Web App: Add service workers and offline capabilities
  • Advanced Analytics: Implement user behavior tracking and performance monitoring
  • Internationalization: Add multi-language support with react-i18next
  • Real-time Features: Integrate WebSockets for live updates

📚 Additional Resources for Continued Learning:

  • React Documentation: Official React docs for latest patterns
  • TypeScript Handbook: Deep dive into advanced TypeScript features
  • Tailwind CSS: Explore advanced utility patterns and plugins
  • React Query: Master server state management and caching strategies
  • Testing Library: Learn effective testing strategies for React applications

💰 Phase 16: Expense Management System Integration

Step 38: API Integration Setup for Expense Management

typescript
// src/types/expense.ts
export interface User {
  id: number;
  email: string;
  full_name: string;
  department: string;
  created_at: string;
  updated_at: string;
}

export interface Category {
  id: number;
  name: string;
  description: string;
  created_at: string;
  updated_at: string;
}

export interface Expense {
  id: number;
  amount: number;
  description: string;
  expense_date: string;
  merchant_name?: string;
  receipt_number?: string;
  category_id: number;
  user_id: number;
  created_at: string;
  updated_at: string;
  category?: Category;
}

export interface Budget {
  id: number;
  amount: number;
  month: number;
  year: number;
  category_id: number;
  user_id: number;
  category?: Category;
}

export interface ExpenseCreate {
  amount: number;
  description: string;
  expense_date: string;
  merchant_name?: string;
  receipt_number?: string;
  category_id: number;
}

export interface BudgetCreate {
  amount: number;
  month: number;
  year: number;
  category_id: number;
}

export interface MonthlySummary {
  month: number;
  year: number;
  total_amount: number;
  total_expenses: number;
  category_breakdown: Record<number, {
    category_name: string;
    total: number;
    count: number;
  }>;
}

export interface BudgetSummary {
  month: number;
  year: number;
  total_budget: number;
  total_spent: number;
  remaining: number;
  is_over_budget: boolean;
  categories: Array<{
    category_id: number;
    category_name: string;
    budget_amount: number;
    spent_amount: number;
    remaining: number;
    percentage_used: number;
    is_over_budget: boolean;
  }>;
}

Step 39: Enhanced API Client for Expense Management

typescript
// src/services/expenseApi.ts
import { apiClient } from './api';
import { 
  Expense, 
  ExpenseCreate, 
  Category, 
  Budget, 
  BudgetCreate, 
  MonthlySummary, 
  BudgetSummary 
} from '@/types/expense';

export class ExpenseAPI {
  // Expense operations
  static async createExpense(data: ExpenseCreate): Promise<Expense> {
    const response = await apiClient.post<Expense>('/expenses/', data);
    return response.data;
  }

  static async getExpenses(params?: {
    skip?: number;
    limit?: number;
    category_id?: number;
    start_date?: string;
    end_date?: string;
  }): Promise<Expense[]> {
    const queryParams = new URLSearchParams();
    if (params?.skip) queryParams.append('skip', params.skip.toString());
    if (params?.limit) queryParams.append('limit', params.limit.toString());
    if (params?.category_id) queryParams.append('category_id', params.category_id.toString());
    if (params?.start_date) queryParams.append('start_date', params.start_date);
    if (params?.end_date) queryParams.append('end_date', params.end_date);

    const response = await apiClient.get<Expense[]>(`/expenses/?${queryParams}`);
    return response.data;
  }

  static async getExpense(id: number): Promise<Expense> {
    const response = await apiClient.get<Expense>(`/expenses/${id}`);
    return response.data;
  }

  static async updateExpense(id: number, data: Partial<ExpenseCreate>): Promise<Expense> {
    const response = await apiClient.put<Expense>(`/expenses/${id}`, data);
    return response.data;
  }

  static async deleteExpense(id: number): Promise<void> {
    await apiClient.delete(`/expenses/${id}`);
  }

  static async getMonthlySummary(month: number, year: number): Promise<MonthlySummary> {
    const response = await apiClient.get<MonthlySummary>(
      `/expenses/summary/monthly?month=${month}&year=${year}`
    );
    return response.data;
  }

  // Category operations
  static async getCategories(): Promise<Category[]> {
    const response = await apiClient.get<Category[]>('/categories/');
    return response.data;
  }

  static async createCategory(data: { name: string; description: string }): Promise<Category> {
    const response = await apiClient.post<Category>('/categories/', data);
    return response.data;
  }

  // Budget operations
  static async createBudget(data: BudgetCreate): Promise<Budget> {
    const response = await apiClient.post<Budget>('/budgets/', data);
    return response.data;
  }

  static async getBudgetSummary(month: number, year: number): Promise<BudgetSummary> {
    const response = await apiClient.get<BudgetSummary>(
      `/budgets/summary?month=${month}&year=${year}`
    );
    return response.data;
  }

  static async updateBudget(id: number, amount: number): Promise<Budget> {
    const response = await apiClient.put<Budget>(`/budgets/${id}?amount=${amount}`, {});
    return response.data;
  }

  static async deleteBudget(id: number): Promise<void> {
    await apiClient.delete(`/budgets/${id}`);
  }
}

Key Learnings - Phase 16:

  • API Design Integration: Structure TypeScript types to match backend API schemas
  • Query Parameter Handling: Build flexible API clients with optional filters
  • Error Response Handling: Handle FastAPI validation errors and business logic exceptions
  • Date Handling: Proper ISO date string formatting for API communication

📊 Phase 17: Data Fetching Hooks for Expense Management

Step 40: Custom Hooks for Expense Operations

typescript
// src/hooks/useExpenses.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { ExpenseAPI } from '@/services/expenseApi';
import { Expense, ExpenseCreate } from '@/types/expense';
import { useToast } from '@/components/ui/use-toast';

export function useExpenses(filters?: {
  category_id?: number;
  start_date?: string;
  end_date?: string;
  skip?: number;
  limit?: number;
}) {
  return useQuery({
    queryKey: ['expenses', filters],
    queryFn: () => ExpenseAPI.getExpenses(filters),
    staleTime: 30 * 1000, // 30 seconds
  });
}

export function useExpense(id: number) {
  return useQuery({
    queryKey: ['expense', id],
    queryFn: () => ExpenseAPI.getExpense(id),
    enabled: !!id,
  });
}

export function useCreateExpense() {
  const queryClient = useQueryClient();
  const { toast } = useToast();

  return useMutation({
    mutationFn: (data: ExpenseCreate) => ExpenseAPI.createExpense(data),
    onSuccess: (newExpense) => {
      queryClient.invalidateQueries({ queryKey: ['expenses'] });
      queryClient.invalidateQueries({ queryKey: ['monthly-summary'] });
      queryClient.invalidateQueries({ queryKey: ['budget-summary'] });
      
      toast({
        title: "Success",
        description: "Expense created successfully",
      });
    },
    onError: (error: any) => {
      toast({
        title: "Error",
        description: error.response?.data?.detail || "Failed to create expense",
        variant: "destructive",
      });
    },
  });
}

export function useUpdateExpense() {
  const queryClient = useQueryClient();
  const { toast } = useToast();

  return useMutation({
    mutationFn: ({ id, data }: { id: number; data: Partial<ExpenseCreate> }) =>
      ExpenseAPI.updateExpense(id, data),
    onSuccess: (updatedExpense) => {
      queryClient.invalidateQueries({ queryKey: ['expenses'] });
      queryClient.invalidateQueries({ queryKey: ['expense', updatedExpense.id] });
      queryClient.invalidateQueries({ queryKey: ['monthly-summary'] });
      
      toast({
        title: "Success",
        description: "Expense updated successfully",
      });
    },
    onError: (error: any) => {
      toast({
        title: "Error",
        description: error.response?.data?.detail || "Failed to update expense",
        variant: "destructive",
      });
    },
  });
}

export function useDeleteExpense() {
  const queryClient = useQueryClient();
  const { toast } = useToast();

  return useMutation({
    mutationFn: (id: number) => ExpenseAPI.deleteExpense(id),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['expenses'] });
      queryClient.invalidateQueries({ queryKey: ['monthly-summary'] });
      
      toast({
        title: "Success",
        description: "Expense deleted successfully",
      });
    },
    onError: (error: any) => {
      toast({
        title: "Error",
        description: error.response?.data?.detail || "Failed to delete expense",
        variant: "destructive",
      });
    },
  });
}

export function useMonthlySummary(month: number, year: number) {
  return useQuery({
    queryKey: ['monthly-summary', month, year],
    queryFn: () => ExpenseAPI.getMonthlySummary(month, year),
    enabled: !!month && !!year,
  });
}

Step 41: Budget and Category Hooks

typescript
// src/hooks/useBudgets.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { ExpenseAPI } from '@/services/expenseApi';
import { BudgetCreate } from '@/types/expense';
import { useToast } from '@/components/ui/use-toast';

export function useBudgetSummary(month: number, year: number) {
  return useQuery({
    queryKey: ['budget-summary', month, year],
    queryFn: () => ExpenseAPI.getBudgetSummary(month, year),
    enabled: !!month && !!year,
  });
}

export function useCreateBudget() {
  const queryClient = useQueryClient();
  const { toast } = useToast();

  return useMutation({
    mutationFn: (data: BudgetCreate) => ExpenseAPI.createBudget(data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['budget-summary'] });
      toast({
        title: "Success",
        description: "Budget created successfully",
      });
    },
    onError: (error: any) => {
      toast({
        title: "Error",
        description: error.response?.data?.detail || "Failed to create budget",
        variant: "destructive",
      });
    },
  });
}

// src/hooks/useCategories.ts
export function useCategories() {
  return useQuery({
    queryKey: ['categories'],
    queryFn: () => ExpenseAPI.getCategories(),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
}

export function useCreateCategory() {
  const queryClient = useQueryClient();
  const { toast } = useToast();

  return useMutation({
    mutationFn: (data: { name: string; description: string }) =>
      ExpenseAPI.createCategory(data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['categories'] });
      toast({
        title: "Success",
        description: "Category created successfully",
      });
    },
    onError: (error: any) => {
      toast({
        title: "Error",
        description: error.response?.data?.detail || "Failed to create category",
        variant: "destructive",
      });
    },
  });
}

Key Learnings - Phase 17:

  • React Query Integration: Proper cache invalidation for related data
  • Error Handling: User-friendly error messages with toast notifications
  • Optimistic Updates: Immediate UI feedback with server synchronization
  • Query Dependencies: Enable/disable queries based on required parameters

💳 Phase 18: Expense Management Components

Step 42: Expense Form Component

typescript
// src/components/expenses/ExpenseForm.tsx
import React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { format } from 'date-fns';
import { CalendarIcon } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Calendar } from '@/components/ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { useCategories } from '@/hooks/useCategories';
import { ExpenseCreate, Expense } from '@/types/expense';
import { cn } from '@/lib/utils';

const expenseSchema = z.object({
  amount: z.number().min(0.01, 'Amount must be greater than 0'),
  description: z.string().min(1, 'Description is required'),
  expense_date: z.date({ required_error: 'Expense date is required' }),
  merchant_name: z.string().optional(),
  receipt_number: z.string().optional(),
  category_id: z.number({ required_error: 'Category is required' }),
});

type ExpenseFormData = z.infer<typeof expenseSchema>;

interface ExpenseFormProps {
  initialData?: Expense;
  onSubmit: (data: ExpenseCreate) => void;
  isLoading?: boolean;
}

export function ExpenseForm({ initialData, onSubmit, isLoading }: ExpenseFormProps) {
  const { data: categories, isLoading: categoriesLoading } = useCategories();

  const form = useForm<ExpenseFormData>({
    resolver: zodResolver(expenseSchema),
    defaultValues: {
      amount: initialData?.amount || 0,
      description: initialData?.description || '',
      expense_date: initialData ? new Date(initialData.expense_date) : new Date(),
      merchant_name: initialData?.merchant_name || '',
      receipt_number: initialData?.receipt_number || '',
      category_id: initialData?.category_id,
    },
  });

  const handleSubmit = (data: ExpenseFormData) => {
    const submitData: ExpenseCreate = {
      ...data,
      expense_date: data.expense_date.toISOString(),
    };
    onSubmit(submitData);
  };

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
          <FormField
            control={form.control}
            name="amount"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Amount *</FormLabel>
                <FormControl>
                  <Input
                    type="number"
                    step="0.01"
                    placeholder="0.00"
                    {...field}
                    onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />

          <FormField
            control={form.control}
            name="category_id"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Category *</FormLabel>
                <Select
                  onValueChange={(value) => field.onChange(parseInt(value))}
                  defaultValue={field.value?.toString()}
                  disabled={categoriesLoading}
                >
                  <FormControl>
                    <SelectTrigger>
                      <SelectValue placeholder="Select a category" />
                    </SelectTrigger>
                  </FormControl>
                  <SelectContent>
                    {categories?.map((category) => (
                      <SelectItem key={category.id} value={category.id.toString()}>
                        {category.name}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
                <FormMessage />
              </FormItem>
            )}
          />
        </div>

        <FormField
          control={form.control}
          name="description"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Description *</FormLabel>
              <FormControl>
                <Textarea
                  placeholder="Describe the expense..."
                  className="min-h-[80px]"
                  {...field}
                />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
          <FormField
            control={form.control}
            name="expense_date"
            render={({ field }) => (
              <FormItem className="flex flex-col">
                <FormLabel>Date *</FormLabel>
                <Popover>
                  <PopoverTrigger asChild>
                    <FormControl>
                      <Button
                        variant="outline"
                        className={cn(
                          'w-full pl-3 text-left font-normal',
                          !field.value && 'text-muted-foreground'
                        )}
                      >
                        {field.value ? (
                          format(field.value, 'PPP')
                        ) : (
                          <span>Pick a date</span>
                        )}
                        <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
                      </Button>
                    </FormControl>
                  </PopoverTrigger>
                  <PopoverContent className="w-auto p-0" align="start">
                    <Calendar
                      mode="single"
                      selected={field.value}
                      onSelect={field.onChange}
                      disabled={(date) =>
                        date > new Date() || date < new Date('1900-01-01')
                      }
                      initialFocus
                    />
                  </PopoverContent>
                </Popover>
                <FormMessage />
              </FormItem>
            )}
          />

          <FormField
            control={form.control}
            name="merchant_name"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Merchant</FormLabel>
                <FormControl>
                  <Input placeholder="Store/Restaurant name" {...field} />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />

          <FormField
            control={form.control}
            name="receipt_number"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Receipt #</FormLabel>
                <FormControl>
                  <Input placeholder="Receipt number" {...field} />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
        </div>

        <div className="flex justify-end space-x-4">
          <Button type="submit" disabled={isLoading}>
            {isLoading ? 'Saving...' : initialData ? 'Update Expense' : 'Create Expense'}
          </Button>
        </div>
      </form>
    </Form>
  );
}

Step 43: Expense List Component with Filtering

typescript
// src/components/expenses/ExpenseList.tsx
import React, { useState } from 'react';
import { format } from 'date-fns';
import { MoreHorizontal, Edit, Trash2, Filter } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Input } from '@/components/ui/input';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { useExpenses, useDeleteExpense } from '@/hooks/useExpenses';
import { useCategories } from '@/hooks/useCategories';
import { LoadingSpinner } from '@/components/ui/loading-spinner';
import { Expense } from '@/types/expense';

interface ExpenseListProps {
  onEdit: (expense: Expense) => void;
}

export function ExpenseList({ onEdit }: ExpenseListProps) {
  const [filters, setFilters] = useState({
    category_id: undefined as number | undefined,
    start_date: '',
    end_date: '',
    search: '',
  });
  const [showFilters, setShowFilters] = useState(false);

  const { data: expenses, isLoading, error } = useExpenses(filters);
  const { data: categories } = useCategories();
  const deleteExpenseMutation = useDeleteExpense();

  const handleDelete = async (id: number) => {
    if (window.confirm('Are you sure you want to delete this expense?')) {
      deleteExpenseMutation.mutate(id);
    }
  };

  const filteredExpenses = expenses?.filter(expense => {
    if (!filters.search) return true;
    return expense.description.toLowerCase().includes(filters.search.toLowerCase()) ||
           expense.merchant_name?.toLowerCase().includes(filters.search.toLowerCase());
  });

  if (isLoading) return <LoadingSpinner />;
  if (error) return <div>Error loading expenses</div>;

  return (
    <div className="space-y-4">
      <div className="flex justify-between items-center">
        <h2 className="text-2xl font-bold">Expenses</h2>
        <Button
          variant="outline"
          onClick={() => setShowFilters(!showFilters)}
          className="flex items-center gap-2"
        >
          <Filter className="h-4 w-4" />
          Filters
        </Button>
      </div>

      <Collapsible open={showFilters} onOpenChange={setShowFilters}>
        <CollapsibleContent className="space-y-4">
          <Card>
            <CardHeader>
              <CardTitle>Filter Expenses</CardTitle>
            </CardHeader>
            <CardContent className="grid grid-cols-1 md:grid-cols-4 gap-4">
              <div>
                <label className="text-sm font-medium">Category</label>
                <Select
                  value={filters.category_id?.toString() || ''}
                  onValueChange={(value) =>
                    setFilters(prev => ({
                      ...prev,
                      category_id: value ? parseInt(value) : undefined
                    }))
                  }
                >
                  <SelectTrigger>
                    <SelectValue placeholder="All categories" />
                  </SelectTrigger>
                  <SelectContent>
                    <SelectItem value="">All categories</SelectItem>
                    {categories?.map((category) => (
                      <SelectItem key={category.id} value={category.id.toString()}>
                        {category.name}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </div>

              <div>
                <label className="text-sm font-medium">Start Date</label>
                <Input
                  type="date"
                  value={filters.start_date}
                  onChange={(e) =>
                    setFilters(prev => ({ ...prev, start_date: e.target.value }))
                  }
                />
              </div>

              <div>
                <label className="text-sm font-medium">End Date</label>
                <Input
                  type="date"
                  value={filters.end_date}
                  onChange={(e) =>
                    setFilters(prev => ({ ...prev, end_date: e.target.value }))
                  }
                />
              </div>

              <div>
                <label className="text-sm font-medium">Search</label>
                <Input
                  placeholder="Search description or merchant..."
                  value={filters.search}
                  onChange={(e) =>
                    setFilters(prev => ({ ...prev, search: e.target.value }))
                  }
                />
              </div>
            </CardContent>
          </Card>
        </CollapsibleContent>
      </Collapsible>

      <div className="grid gap-4">
        {filteredExpenses?.map((expense) => (
          <Card key={expense.id} className="hover:shadow-md transition-shadow">
            <CardContent className="p-4">
              <div className="flex justify-between items-start">
                <div className="flex-1">
                  <div className="flex items-center gap-2 mb-2">
                    <h3 className="font-medium">{expense.description}</h3>
                    <Badge variant="secondary">
                      {categories?.find(c => c.id === expense.category_id)?.name}
                    </Badge>
                  </div>
                  
                  <div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm text-gray-600">
                    <div>
                      <span className="font-medium">Amount:</span> ${expense.amount.toFixed(2)}
                    </div>
                    <div>
                      <span className="font-medium">Date:</span>{' '}
                      {format(new Date(expense.expense_date), 'MMM dd, yyyy')}
                    </div>
                    {expense.merchant_name && (
                      <div>
                        <span className="font-medium">Merchant:</span> {expense.merchant_name}
                      </div>
                    )}
                    {expense.receipt_number && (
                      <div>
                        <span className="font-medium">Receipt:</span> {expense.receipt_number}
                      </div>
                    )}
                  </div>
                </div>

                <DropdownMenu>
                  <DropdownMenuTrigger asChild>
                    <Button variant="ghost" className="h-8 w-8 p-0">
                      <MoreHorizontal className="h-4 w-4" />
                    </Button>
                  </DropdownMenuTrigger>
                  <DropdownMenuContent align="end">
                    <DropdownMenuItem onClick={() => onEdit(expense)}>
                      <Edit className="mr-2 h-4 w-4" />
                      Edit
                    </DropdownMenuItem>
                    <DropdownMenuItem
                      onClick={() => handleDelete(expense.id)}
                      className="text-red-600"
                    >
                      <Trash2 className="mr-2 h-4 w-4" />
                      Delete
                    </DropdownMenuItem>
                  </DropdownMenuContent>
                </DropdownMenu>
              </div>
            </CardContent>
          </Card>
        ))}
      </div>

      {filteredExpenses?.length === 0 && (
        <Card>
          <CardContent className="p-8 text-center">
            <p className="text-gray-500">No expenses found matching your criteria.</p>
          </CardContent>
        </Card>
      )}
    </div>
  );
}

Key Learnings - Phase 18:

  • Form Validation: Comprehensive validation with Zod and React Hook Form
  • Date Handling: Proper date input with calendar component
  • Filtering & Search: Client-side and server-side filtering strategies
  • Optimistic UI: Immediate feedback for user actions

📈 Phase 19: Dashboard and Analytics Components

Step 44: Dashboard Overview Component

typescript
// src/components/dashboard/Dashboard.tsx
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useMonthlySummary } from '@/hooks/useExpenses';
import { useBudgetSummary } from '@/hooks/useBudgets';
import { ExpenseChart } from './ExpenseChart';
import { BudgetProgress } from './BudgetProgress';
import { QuickStats } from './QuickStats';
import { RecentExpenses } from './RecentExpenses';
import { LoadingSpinner } from '@/components/ui/loading-spinner';

export function Dashboard() {
  const currentDate = new Date();
  const [selectedMonth, setSelectedMonth] = useState(currentDate.getMonth() + 1);
  const [selectedYear, setSelectedYear] = useState(currentDate.getFullYear());

  const { data: monthlySummary, isLoading: summaryLoading } = useMonthlySummary(
    selectedMonth,
    selectedYear
  );
  const { data: budgetSummary, isLoading: budgetLoading } = useBudgetSummary(
    selectedMonth,
    selectedYear
  );

  const months = [
    { value: 1, label: 'January' },
    { value: 2, label: 'February' },
    { value: 3, label: 'March' },
    { value: 4, label: 'April' },
    { value: 5, label: 'May' },
    { value: 6, label: 'June' },
    { value: 7, label: 'July' },
    { value: 8, label: 'August' },
    { value: 9, label: 'September' },
    { value: 10, label: 'October' },
    { value: 11, label: 'November' },
    { value: 12, label: 'December' },
  ];

  const years = Array.from({ length: 5 }, (_, i) => currentDate.getFullYear() - 2 + i);

  if (summaryLoading || budgetLoading) {
    return <LoadingSpinner />;
  }

  return (
    <div className="space-y-6">
      <div className="flex justify-between items-center">
        <h1 className="text-3xl font-bold">Dashboard</h1>
        <div className="flex gap-4">
          <Select
            value={selectedMonth.toString()}
            onValueChange={(value) => setSelectedMonth(parseInt(value))}
          >
            <SelectTrigger className="w-[150px]">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              {months.map((month) => (
                <SelectItem key={month.value} value={month.value.toString()}>
                  {month.label}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>

          <Select
            value={selectedYear.toString()}
            onValueChange={(value) => setSelectedYear(parseInt(value))}
          >
            <SelectTrigger className="w-[120px]">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              {years.map((year) => (
                <SelectItem key={year} value={year.toString()}>
                  {year}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        </div>
      </div>

      <QuickStats
        monthlySummary={monthlySummary}
        budgetSummary={budgetSummary}
        month={selectedMonth}
        year={selectedYear}
      />

      <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
        <div className="lg:col-span-2">
          <ExpenseChart
            categoryBreakdown={monthlySummary?.category_breakdown}
            month={selectedMonth}
            year={selectedYear}
          />
        </div>
        <div>
          <BudgetProgress budgetSummary={budgetSummary} />
        </div>
      </div>

      <RecentExpenses />
    </div>
  );
}

Step 45: Quick Stats Component

typescript
// src/components/dashboard/QuickStats.tsx
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { TrendingUp, TrendingDown, DollarSign, CreditCard } from 'lucide-react';
import { MonthlySummary, BudgetSummary } from '@/types/expense';

interface QuickStatsProps {
  monthlySummary?: MonthlySummary;
  budgetSummary?: BudgetSummary;
  month: number;
  year: number;
}

export function QuickStats({ monthlySummary, budgetSummary, month, year }: QuickStatsProps) {
  const totalSpent = monthlySummary?.total_amount || 0;
  const totalBudget = budgetSummary?.total_budget || 0;
  const remaining = budgetSummary?.remaining || 0;
  const isOverBudget = budgetSummary?.is_over_budget || false;

  const stats = [
    {
      title: 'Total Spent',
      value: `${totalSpent.toFixed(2)}`,
      icon: DollarSign,
      trend: isOverBudget ? 'down' : 'up',
      color: isOverBudget ? 'text-red-600' : 'text-green-600',
    },
    {
      title: 'Total Budget',
      value: `${totalBudget.toFixed(2)}`,
      icon: CreditCard,
      trend: 'neutral',
      color: 'text-blue-600',
    },
    {
      title: 'Remaining',
      value: `${remaining.toFixed(2)}`,
      icon: remaining >= 0 ? TrendingUp : TrendingDown,
      trend: remaining >= 0 ? 'up' : 'down',
      color: remaining >= 0 ? 'text-green-600' : 'text-red-600',
    },
    {
      title: 'Expenses Count',
      value: monthlySummary?.total_expenses?.toString() || '0',
      icon: CreditCard,
      trend: 'neutral',
      color: 'text-gray-600',
    },
  ];

  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
      {stats.map((stat) => (
        <Card key={stat.title}>
          <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
            <CardTitle className="text-sm font-medium">{stat.title}</CardTitle>
            <stat.icon className={`h-4 w-4 ${stat.color}`} />
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">{stat.value}</div>
            {stat.trend !== 'neutral' && (
              <p className={`text-xs ${stat.color} flex items-center mt-1`}>
                {stat.trend === 'up' ? (
                  <TrendingUp className="mr-1 h-3 w-3" />
                ) : (
                  <TrendingDown className="mr-1 h-3 w-3" />
                )}
                {stat.trend === 'up' ? 'Within budget' : 'Over budget'}
              </p>
            )}
          </CardContent>
        </Card>
      ))}
    </div>
  );
}

Step 46: Expense Chart Component with Recharts

typescript
// src/components/dashboard/ExpenseChart.tsx
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { PieChart, Pie, Cell, ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts';

interface ExpenseChartProps {
  categoryBreakdown?: Record<number, {
    category_name: string;
    total: number;
    count: number;
  }>;
  month: number;
  year: number;
}

const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8', '#82CA9D'];

export function ExpenseChart({ categoryBreakdown, month, year }: ExpenseChartProps) {
  const chartData = categoryBreakdown 
    ? Object.entries(categoryBreakdown).map(([categoryId, data]) => ({
        name: data.category_name,
        amount: data.total,
        count: data.count,
        categoryId: parseInt(categoryId),
      }))
    : [];

  return (
    <div className="space-y-6">
      <Card>
        <CardHeader>
          <CardTitle>Expenses by Category</CardTitle>
        </CardHeader>
        <CardContent>
          {chartData.length > 0 ? (
            <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
              {/* Pie Chart */}
              <div className="h-[300px]">
                <ResponsiveContainer width="100%" height="100%">
                  <PieChart>
                    <Pie
                      data={chartData}
                      cx="50%"
                      cy="50%"
                      labelLine={false}
                      label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
                      outerRadius={80}
                      fill="#8884d8"
                      dataKey="amount"
                    >
                      {chartData.map((entry, index) => (
                        <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
                      ))}
                    </Pie>
                    <Tooltip formatter={(value) => [`${Number(value).toFixed(2)}`, 'Amount']} />
                  </PieChart>
                </ResponsiveContainer>
              </div>

              {/* Bar Chart */}
              <div className="h-[300px]">
                <ResponsiveContainer width="100%" height="100%">
                  <BarChart data={chartData}>
                    <CartesianGrid strokeDasharray="3 3" />
                    <XAxis 
                      dataKey="name" 
                      angle={-45}
                      textAnchor="end"
                      height={80}
                    />
                    <YAxis />
                    <Tooltip formatter={(value) => [`${Number(value).toFixed(2)}`, 'Amount']} />
                    <Bar dataKey="amount" fill="#8884d8" />
                  </BarChart>
                </ResponsiveContainer>
              </div>
            </div>
          ) : (
            <div className="text-center py-8 text-gray-500">
              No expense data available for this period
            </div>
          )}
        </CardContent>
      </Card>

      {/* Category Details Table */}
      {chartData.length > 0 && (
        <Card>
          <CardHeader>
            <CardTitle>Category Breakdown</CardTitle>
          </CardHeader>
          <CardContent>
            <div className="overflow-x-auto">
              <table className="w-full">
                <thead>
                  <tr className="border-b">
                    <th className="text-left p-2">Category</th>
                    <th className="text-right p-2">Amount</th>
                    <th className="text-right p-2">Count</th>
                    <th className="text-right p-2">Avg per Expense</th>
                  </tr>
                </thead>
                <tbody>
                  {chartData.map((item, index) => (
                    <tr key={item.categoryId} className="border-b">
                      <td className="p-2 flex items-center">
                        <div 
                          className="w-4 h-4 rounded mr-2" 
                          style={{ backgroundColor: COLORS[index % COLORS.length] }}
                        />
                        {item.name}
                      </td>
                      <td className="text-right p-2">${item.amount.toFixed(2)}</td>
                      <td className="text-right p-2">{item.count}</td>
                      <td className="text-right p-2">
                        ${(item.amount / item.count).toFixed(2)}
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </CardContent>
        </Card>
      )}

      {viewType === 'category' && (
        <Card>
          <CardHeader>
            <CardTitle>Category Breakdown</CardTitle>
          </CardHeader>
          <CardContent>
            <div className="h-[400px]">
              <ResponsiveContainer width="100%" height="100%">
                <BarChart data={categoryData}>
                  <CartesianGrid strokeDasharray="3 3" />
                  <XAxis 
                    dataKey="category" 
                    angle={-45}
                    textAnchor="end"
                    height={100}
                  />
                  <YAxis />
                  <Tooltip formatter={(value) => [`${Number(value).toFixed(2)}`, 'Amount']} />
                  <Bar dataKey="total" fill="#8884d8" name="Total Spent" />
                </BarChart>
              </ResponsiveContainer>
            </div>
          </CardContent>
        </Card>
      )}

      {/* Summary Statistics */}
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
        <Card>
          <CardContent className="p-4">
            <div className="text-2xl font-bold">
              ${expenses?.reduce((sum, exp) => sum + exp.amount, 0).toFixed(2) || '0.00'}
            </div>
            <div className="text-sm text-gray-600">Total Spent</div>
          </CardContent>
        </Card>
        
        <Card>
          <CardContent className="p-4">
            <div className="text-2xl font-bold">{expenses?.length || 0}</div>
            <div className="text-sm text-gray-600">Total Expenses</div>
          </CardContent>
        </Card>
        
        <Card>
          <CardContent className="p-4">
            <div className="text-2xl font-bold">
              ${expenses?.length ? (expenses.reduce((sum, exp) => sum + exp.amount, 0) / expenses.length).toFixed(2) : '0.00'}
            </div>
            <div className="text-sm text-gray-600">Average per Expense</div>
          </CardContent>
        </Card>
        
        <Card>
          <CardContent className="p-4">
            <div className="text-2xl font-bold">
              {new Set(expenses?.map(exp => exp.category_id)).size || 0}
            </div>
            <div className="text-sm text-gray-600">Categories Used</div>
          </CardContent>
        </Card>
      </div>
    </div>
  );
}

Key Learnings - Phase 20:

  • Advanced Data Processing: Complex data aggregation and analysis
  • Export Functionality: CSV export for external analysis
  • Multiple Chart Types: Different visualizations for different insights
  • Date Range Filtering: Flexible time period selection

🔄 Phase 21: Real-time Features and Notifications

Step 50: Toast Notification System

typescript
// src/hooks/useNotifications.ts
import { useToast } from '@/components/ui/use-toast';
import { useCallback } from 'react';

export function useNotifications() {
  const { toast } = useToast();

  const notifyBudgetWarning = useCallback((categoryName: string, remaining: number) => {
    toast({
      title: "Budget Warning",
      description: `You're close to your budget limit for ${categoryName}. ${remaining.toFixed(2)} remaining.`,
      variant: "destructive",
    });
  }, [toast]);

  const notifyBudgetExceeded = useCallback((categoryName: string, overage: number) => {
    toast({
      title: "Budget Exceeded",
      description: `You've exceeded your budget for ${categoryName} by ${overage.toFixed(2)}.`,
      variant: "destructive",
    });
  }, [toast]);

  const notifyExpenseCreated = useCallback((amount: number) => {
    toast({
      title: "Expense Added",
      description: `Expense of ${amount.toFixed(2)} has been recorded.`,
    });
  }, [toast]);

  const notifyBudgetAlert = useCallback((summary: any) => {
    // Check for budget alerts
    summary.categories?.forEach((category: any) => {
      if (category.is_over_budget) {
        notifyBudgetExceeded(category.category_name, Math.abs(category.remaining));
      } else if (category.percentage_used > 80) {
        notifyBudgetWarning(category.category_name, category.remaining);
      }
    });
  }, [notifyBudgetWarning, notifyBudgetExceeded]);

  return {
    notifyBudgetWarning,
    notifyBudgetExceeded,
    notifyExpenseCreated,
    notifyBudgetAlert,
  };
}

Step 51: Enhanced Error Handling

typescript
// src/utils/errorHandling.ts
import { AxiosError } from 'axios';

export interface APIError {
  message: string;
  status: number;
  details?: any;
}

export function handleAPIError(error: unknown): APIError {
  if (error instanceof AxiosError) {
    const status = error.response?.status || 500;
    const data = error.response?.data;

    // Handle FastAPI validation errors
    if (status === 422 && data?.detail) {
      if (Array.isArray(data.detail)) {
        const messages = data.detail.map((err: any) => 
          `${err.loc?.join(' → ') || 'Field'}: ${err.msg}`
        ).join(', ');
        return {
          message: `Validation Error: ${messages}`,
          status,
          details: data.detail,
        };
      }
    }

    // Handle business logic errors
    if (data?.detail) {
      return {
        message: data.detail,
        status,
        details: data,
      };
    }

    // Generic HTTP errors
    switch (status) {
      case 400:
        return { message: 'Bad request. Please check your input.', status };
      case 401:
        return { message: 'Unauthorized. Please log in again.', status };
      case 403:
        return { message: 'Forbidden. You don\'t have permission for this action.', status };
      case 404:
        return { message: 'Resource not found.', status };
      case 500:
        return { message: 'Internal server error. Please try again later.', status };
      default:
        return { message: `HTTP Error ${status}`, status };
    }
  }

  // Handle network errors
  if (error instanceof Error) {
    return {
      message: error.message || 'An unexpected error occurred',
      status: 0,
    };
  }

  return {
    message: 'An unknown error occurred',
    status: 0,
  };
}

// Enhanced API client with better error handling
// src/services/enhancedApi.ts
import { apiClient } from './api';
import { handleAPIError } from '@/utils/errorHandling';

export class EnhancedAPI {
  static async safeRequest<T>(
    requestFn: () => Promise<any>,
    options?: {
      successMessage?: string;
      onSuccess?: (data: T) => void;
      onError?: (error: APIError) => void;
    }
  ): Promise<{ data?: T; error?: APIError }> {
    try {
      const response = await requestFn();
      const data = response.data;
      
      if (options?.onSuccess) {
        options.onSuccess(data);
      }
      
      return { data };
    } catch (error) {
      const apiError = handleAPIError(error);
      
      if (options?.onError) {
        options.onError(apiError);
      }
      
      return { error: apiError };
    }
  }
}

Step 52: Offline Support with Service Worker

typescript
// src/utils/serviceWorker.ts
const isLocalhost = Boolean(
  window.location.hostname === 'localhost' ||
  window.location.hostname === '[::1]' ||
  window.location.hostname.match(
    /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
  )
);

export function registerServiceWorker() {
  if ('serviceWorker' in navigator) {
    const publicUrl = new URL(process.env.PUBLIC_URL!, window.location.href);
    if (publicUrl.origin !== window.location.origin) {
      return;
    }

    window.addEventListener('load', () => {
      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;

      if (isLocalhost) {
        checkValidServiceWorker(swUrl);
      } else {
        registerValidSW(swUrl);
      }
    });
  }
}

function registerValidSW(swUrl: string) {
  navigator.serviceWorker
    .register(swUrl)
    .then(registration => {
      console.log('SW registered: ', registration);
    })
    .catch(registrationError => {
      console.log('SW registration failed: ', registrationError);
    });
}

function checkValidServiceWorker(swUrl: string) {
  fetch(swUrl, {
    headers: { 'Service-Worker': 'script' },
  })
    .then(response => {
      const contentType = response.headers.get('content-type');
      if (
        response.status === 404 ||
        (contentType != null && contentType.indexOf('javascript') === -1)
      ) {
        navigator.serviceWorker.ready.then(registration => {
          registration.unregister().then(() => {
            window.location.reload();
          });
        });
      } else {
        registerValidSW(swUrl);
      }
    })
    .catch(() => {
      console.log('No internet connection found. App is running in offline mode.');
    });
}

// src/hooks/useOfflineSync.ts
import { useState, useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query';

interface OfflineAction {
  id: string;
  type: 'CREATE_EXPENSE' | 'UPDATE_EXPENSE' | 'DELETE_EXPENSE';
  data: any;
  timestamp: number;
}

export function useOfflineSync() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);
  const [pendingActions, setPendingActions] = useState<OfflineAction[]>([]);
  const queryClient = useQueryClient();

  useEffect(() => {
    const handleOnline = () => {
      setIsOnline(true);
      syncPendingActions();
    };

    const handleOffline = () => {
      setIsOnline(false);
    };

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    // Load pending actions from localStorage
    const stored = localStorage.getItem('pendingActions');
    if (stored) {
      setPendingActions(JSON.parse(stored));
    }

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  const addPendingAction = (action: Omit<OfflineAction, 'id' | 'timestamp'>) => {
    const newAction: OfflineAction = {
      ...action,
      id: crypto.randomUUID(),
      timestamp: Date.now(),
    };

    const updated = [...pendingActions, newAction];
    setPendingActions(updated);
    localStorage.setItem('pendingActions', JSON.stringify(updated));
  };

  const syncPendingActions = async () => {
    if (pendingActions.length === 0) return;

    try {
      // Process pending actions
      for (const action of pendingActions) {
        await processPendingAction(action);
      }

      // Clear pending actions on success
      setPendingActions([]);
      localStorage.removeItem('pendingActions');
      
      // Invalidate queries to refresh data
      queryClient.invalidateQueries();
    } catch (error) {
      console.error('Failed to sync pending actions:', error);
    }
  };

  const processPendingAction = async (action: OfflineAction) => {
    // Implement actual API calls here
    switch (action.type) {
      case 'CREATE_EXPENSE':
        // await ExpenseAPI.createExpense(action.data);
        break;
      case 'UPDATE_EXPENSE':
        // await ExpenseAPI.updateExpense(action.data.id, action.data);
        break;
      case 'DELETE_EXPENSE':
        // await ExpenseAPI.deleteExpense(action.data.id);
        break;
    }
  };

  return {
    isOnline,
    pendingActions,
    addPendingAction,
    syncPendingActions,
  };
}

Key Learnings - Phase 21:

  • Error Handling: Comprehensive error management for better UX
  • Offline Support: PWA capabilities for unreliable networks
  • Notifications: Real-time feedback for user actions
  • Data Synchronization: Handling offline-to-online transitions

🧪 Phase 22: Advanced Testing Strategies

Step 53: Integration Testing with MSW

typescript
// src/test/mocks/handlers.ts
import { rest } from 'msw';
import { Expense, Category, Budget } from '@/types/expense';

const mockCategories: Category[] = [
  { id: 1, name: 'Food & Dining', description: 'Restaurant and food expenses', created_at: '2024-01-01', updated_at: '2024-01-01' },
  { id: 2, name: 'Transportation', description: 'Car and travel expenses', created_at: '2024-01-01', updated_at: '2024-01-01' },
];

const mockExpenses: Expense[] = [
  {
    id: 1,
    amount: 25.50,
    description: 'Lunch at restaurant',
    expense_date: '2024-03-15T12:30:00',
    merchant_name: 'Pizza Palace',
    receipt_number: 'R12345',
    category_id: 1,
    user_id: 1,
    created_at: '2024-03-15T12:30:00',
    updated_at: '2024-03-15T12:30:00',
  },
];

export const handlers = [
  // Categories
  rest.get('/api/categories/', (req, res, ctx) => {
    return res(ctx.json({ data: mockCategories }));
  }),

  rest.post('/api/categories/', (req, res, ctx) => {
    const newCategory = {
      id: mockCategories.length + 1,
      created_at: new Date().toISOString(),
      updated_at: new Date().toISOString(),
      ...(req.body as any),
    };
    mockCategories.push(newCategory);
    return res(ctx.status(201), ctx.json({ data: newCategory }));
  }),

  // Expenses
  rest.get('/api/expenses/', (req, res, ctx) => {
    const categoryId = req.url.searchParams.get('category_id');
    let filteredExpenses = mockExpenses;

    if (categoryId) {
      filteredExpenses = mockExpenses.filter(
        exp => exp.category_id === parseInt(categoryId)
      );
    }

    return res(ctx.json({ data: filteredExpenses }));
  }),

  rest.post('/api/expenses/', (req, res, ctx) => {
    const newExpense = {
      id: mockExpenses.length + 1,
      user_id: 1,
      created_at: new Date().toISOString(),
      updated_at: new Date().toISOString(),
      ...(req.body as any),
    };
    mockExpenses.push(newExpense);
    return res(ctx.status(201), ctx.json({ data: newExpense }));
  }),

  rest.put('/api/expenses/:id', (req, res, ctx) => {
    const { id } = req.params;
    const index = mockExpenses.findIndex(exp => exp.id === parseInt(id as string));
    
    if (index === -1) {
      return res(ctx.status(404), ctx.json({ detail: 'Expense not found' }));
    }

    mockExpenses[index] = {
      ...mockExpenses[index],
      ...(req.body as any),
      updated_at: new Date().toISOString(),
    };

    return res(ctx.json({ data: mockExpenses[index] }));
  }),

  rest.delete('/api/expenses/:id', (req, res, ctx) => {
    const { id } = req.params;
    const index = mockExpenses.findIndex(exp => exp.id === parseInt(id as string));
    
    if (index === -1) {
      return res(ctx.status(404), ctx.json({ detail: 'Expense not found' }));
    }

    mockExpenses.splice(index, 1);
    return res(ctx.status(204));
  }),

  // Budget endpoints
  rest.get('/api/budgets/summary', (req, res, ctx) => {
    const month = req.url.searchParams.get('month');
    const year = req.url.searchParams.get('year');

    const mockBudgetSummary = {
      month: parseInt(month || '3'),
      year: parseInt(year || '2024'),
      total_budget: 1000.0,
      total_spent: 450.0,
      remaining: 550.0,
      is_over_budget: false,
      categories: [
        {
          category_id: 1,
          category_name: 'Food & Dining',
          budget_amount: 500.0,
          spent_amount: 250.0,
          remaining: 250.0,
          percentage_used: 50.0,
          is_over_budget: false,
        },
      ],
    };

    return res(ctx.json({ data: mockBudgetSummary }));
  }),

  // Error scenarios
  rest.post('/api/expenses/error', (req, res, ctx) => {
    return res(
      ctx.status(400),
      ctx.json({
        detail: 'Expense exceeds monthly budget limit. Remaining: $50.00',
        error_type: 'budget_exceeded',
        remaining_budget: 50.0,
      })
    );
  }),
];

// src/test/setup.ts
import { setupServer } from 'msw/node';
import { handlers } from './mocks/handlers';

export const server = setupServer(...handlers);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Step 54: Component Integration Tests

typescript
// src/components/__tests__/ExpenseForm.integration.test.tsx
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ExpenseForm } from '../expenses/ExpenseForm';
import { server } from '@/test/setup';

const createWrapper = () => {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: { retry: false },
      mutations: { retry: false },
    },
  });

  return ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
};

describe('ExpenseForm Integration', () => {
  const mockOnSubmit = jest.fn();

  beforeEach(() => {
    mockOnSubmit.mockClear();
  });

  it('creates expense with all fields', async () => {
    const user = userEvent.setup();
    
    render(
      <ExpenseForm onSubmit={mockOnSubmit} />,
      { wrapper: createWrapper() }
    );

    // Wait for categories to load
    await waitFor(() => {
      expect(screen.getByText('Food & Dining')).toBeInTheDocument();
    });

    // Fill out form
    await user.type(screen.getByPlaceholderText('0.00'), '99.99');
    await user.selectOptions(screen.getByRole('combobox'), ['1']);
    await user.type(screen.getByPlaceholderText('Describe the expense...'), 'Business lunch');
    await user.type(screen.getByPlaceholderText('Store/Restaurant name'), 'Fine Dining');
    await user.type(screen.getByPlaceholderText('Receipt number'), 'R001');

    // Submit form
    await user.click(screen.getByText('Create Expense'));

    await waitFor(() => {
      expect(mockOnSubmit).toHaveBeenCalledWith({
        amount: 99.99,
        description: 'Business lunch',
        merchant_name: 'Fine Dining',
        receipt_number: 'R001',
        category_id: 1,
        expense_date: expect.any(String),
      });
    });
  });

  it('shows validation errors for invalid input', async () => {
    const user = userEvent.setup();
    
    render(
      <ExpenseForm onSubmit={mockOnSubmit} />,
      { wrapper: createWrapper() }
    );

    // Try to submit without required fields
    await user.click(screen.getByText('Create Expense'));

    await waitFor(() => {
      expect(screen.getByText('Amount must be greater than 0')).toBeInTheDocument();
      expect(screen.getByText('Description is required')).toBeInTheDocument();
      expect(screen.getByText('Category is required')).toBeInTheDocument();
    });

    expect(mockOnSubmit).not.toHaveBeenCalled();
  });

  it('handles server validation errors', async () => {
    // Mock server error response
    server.use(
      rest.post('/api/expenses/', (req, res, ctx) => {
        return res(
          ctx.status(422),
          ctx.json({
            detail: [
              {
                loc: ['body', 'amount'],
                msg: 'Amount must be positive',
                type: 'value_error',
              },
            ],
          })
        );
      })
    );

    const user = userEvent.setup();
    
    render(
      <ExpenseForm onSubmit={mockOnSubmit} />,
      { wrapper: createWrapper() }
    );

    // Fill and submit form
    await user.type(screen.getByPlaceholderText('0.00'), '-10');
    await user.type(screen.getByPlaceholderText('Describe the expense...'), 'Invalid expense');
    await user.click(screen.getByText('Create Expense'));

    // Should show server validation error
    await waitFor(() => {
      expect(screen.getByText(/Validation Error/)).toBeInTheDocument();
    });
  });
});

Step 55: E2E Testing with Playwright

typescript
// tests/e2e/expense-management.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Expense Management Flow', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/');
    // Mock authentication if needed
  });

  test('complete expense creation workflow', async ({ page }) => {
    // Navigate to expenses page
    await page.click('[data-testid="expenses-nav"]');
    
    // Open create expense dialog
    await page.click('[data-testid="create-expense-btn"]');
    
    // Fill out expense form
    await page.fill('[data-testid="amount-input"]', '125.50');
    await page.selectOption('[data-testid="category-select"]', '1');
    await page.fill('[data-testid="description-input"]', 'Team lunch meeting');
    await page.fill('[data-testid="merchant-input"]', 'Downtown Bistro');
    
    // Submit form
    await page.click('[data-testid="submit-expense-btn"]');
    
    // Verify success notification
    await expect(page.locator('[data-testid="toast-success"]')).toBeVisible();
    
    // Verify expense appears in list
    await expect(page.locator('[data-testid="expense-list"]')).toContainText('Team lunch meeting');
    await expect(page.locator('[data-testid="expense-list"]')).toContainText('$125.50');
  });

  test('budget warning workflow', async ({ page }) => {
    // Set a low budget
    await page.click('[data-testid="budgets-nav"]');
    await page.click('[data-testid="create-budget-btn"]');
    await page.fill('[data-testid="budget-amount"]', '100');
    await page.selectOption('[data-testid="budget-category"]', '1');
    await page.click('[data-testid="submit-budget-btn"]');
    
    // Create expense that approaches budget limit
    await page.click('[data-testid="expenses-nav"]');
    await page.click('[data-testid="create-expense-btn"]');
    await page.fill('[data-testid="amount-input"]', '85');
    await page.selectOption('[data-testid="category-select"]', '1');
    await page.fill('[data-testid="description-input"]', 'Large expense');
    await page.click('[data-testid="submit-expense-btn"]');
    
    // Should show budget warning
    await expect(page.locator('[data-testid="toast-warning"]')).toBeVisible();
    await expect(page.locator('[data-testid="toast-warning"]')).toContainText('close to your budget limit');
  });

  test('expense filtering and search', async ({ page }) => {
    await page.click('[data-testid="expenses-nav"]');
    
    // Open filters
    await page.click('[data-testid="filters-toggle"]');
    
    // Filter by category
    await page.selectOption('[data-testid="category-filter"]', '1');
    
    // Search by description
    await page.fill('[data-testid="search-input"]', 'lunch');
    
    // Verify filtered results
    const expenseCards = page.locator('[data-testid="expense-card"]');
    await expect(expenseCards).toHaveCount(1);
    await expect(expenseCards.first()).toContainText('lunch');
  });

  test('responsive design on mobile', async ({ page }) => {
    await page.setViewportSize({ width: 375, height: 667 });
    
    await page.click('[data-testid="expenses-nav"]');
    
    // Mobile menu should be visible
    await expect(page.locator('[data-testid="mobile-menu"]')).toBeVisible();
    
    // Cards should stack vertically
    const expenseCards = page.locator('[data-testid="expense-card"]');
    const firstCard = expenseCards.first();
    const secondCard = expenseCards.nth(1);
    
    if (await expenseCards.count() > 1) {
      const firstBox = await firstCard.boundingBox();
      const secondBox = await secondCard.boundingBox();
      
      // Second card should be below first card (stacked)
      expect(secondBox?.y).toBeGreaterThan(firstBox?.y! + firstBox?.height!);
    }
  });
});

Key Learnings - Phase 22:

  • MSW Integration: Mock API responses for consistent testing
  • Component Integration: Test component interactions with real API calls
  • E2E Testing: Verify complete user workflows
  • Mobile Testing: Ensure responsive design works across devices

🚀 Final Integration: Complete Expense Management System

Step 56: Main Application Assembly

typescript
// src/App.tsx - Final Complete Application
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { BrowserRouter } from 'react-router-dom';
import { AppProvider } from '@/context/AppContext';
import { ThemeProvider } from '@/context/ThemeContext';
import { AppRouter } from '@/router/AppRouter';
import { Toaster } from '@/components/ui/toaster';
import { SkipLink } from '@/components/accessible/SkipLink';
import { ErrorBoundary } from '@/components/common/ErrorBoundary';
import { config } from '@/config/environment';
import { registerServiceWorker } from '@/utils/serviceWorker';
import './globals.css';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      retry: (failureCount, error: any) => {
        if (error?.response?.status === 404) return false;
        return failureCount < 3;
      },
    },
    mutations: {
      retry: 1,
    },
  },
});

// Register service worker for PWA capabilities
if (process.env.NODE_ENV === 'production') {
  registerServiceWorker();
}

function App() {
  return (
    <ErrorBoundary>
      <QueryClientProvider client={queryClient}>
        <BrowserRouter>
          <AppProvider>
            <ThemeProvider>
              <SkipLink />
              <AppRouter />
              <Toaster />
              {config.enableDebug && <ReactQueryDevtools />}
            </ThemeProvider>
          </AppProvider>
        </BrowserRouter>
      </QueryClientProvider>
    </ErrorBoundary>
  );
}

export default App;

Step 57: Complete Router Configuration

typescript
// src/router/AppRouter.tsx - Enhanced with all routes
import React, { Suspense } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { Layout } from '@/components/layout/Layout';
import { LoadingSpinner } from '@/components/ui/loading-spinner';

// Lazy load all pages
const Dashboard = React.lazy(() => import('@/pages/Dashboard'));
const ExpensesPage = React.lazy(() => import('@/pages/ExpensesPage'));
const BudgetsPage = React.lazy(() => import('@/pages/BudgetsPage'));
const AnalyticsPage = React.lazy(() => import('@/pages/AnalyticsPage'));
const CategoriesPage = React.lazy(() => import('@/pages/CategoriesPage'));
const SettingsPage = React.lazy(() => import('@/pages/SettingsPage'));
const NotFoundPage = React.lazy(() => import('@/pages/NotFoundPage'));

export function AppRouter() {
  return (
    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={
          <Suspense fallback={<LoadingSpinner />}>
            <Dashboard />
          </Suspense>
        } />
        
        <Route path="expenses" element={
          <Suspense fallback={<LoadingSpinner />}>
            <ExpensesPage />
          </Suspense>
        } />
        
        <Route path="budgets" element={
          <Suspense fallback={<LoadingSpinner />}>
            <BudgetsPage />dContent>
        </Card>
      )}
    </div>
  );
}

Step 47: Budget Progress Component

typescript
// src/components/dashboard/BudgetProgress.tsx
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { AlertTriangle, CheckCircle } from 'lucide-react';
import { BudgetSummary } from '@/types/expense';

interface BudgetProgressProps {
  budgetSummary?: BudgetSummary;
}

export function BudgetProgress({ budgetSummary }: BudgetProgressProps) {
  if (!budgetSummary || budgetSummary.categories.length === 0) {
    return (
      <Card>
        <CardHeader>
          <CardTitle>Budget Progress</CardTitle>
        </CardHeader>
        <CardContent>
          <p className="text-gray-500 text-center py-4">
            No budgets set for this period
          </p>
        </CardContent>
      </Card>
    );
  }

  return (
    <Card>
      <CardHeader>
        <CardTitle className="flex items-center justify-between">
          Budget Progress
          {budgetSummary.is_over_budget ? (
            <AlertTriangle className="h-5 w-5 text-red-500" />
          ) : (
            <CheckCircle className="h-5 w-5 text-green-500" />
          )}
        </CardTitle>
      </CardHeader>
      <CardContent className="space-y-4">
        {/* Overall Progress */}
        <div className="p-4 bg-gray-50 rounded-lg">
          <div className="flex justify-between items-center mb-2">
            <span className="text-sm font-medium">Overall Budget</span>
            <span className="text-sm text-gray-600">
              ${budgetSummary.total_spent.toFixed(2)} / ${budgetSummary.total_budget.toFixed(2)}
            </span>
          </div>
          <Progress 
            value={Math.min((budgetSummary.total_spent / budgetSummary.total_budget) * 100, 100)}
            className="h-2"
          />
          <div className="mt-1 text-xs text-gray-600">
            {budgetSummary.remaining >= 0 
              ? `${budgetSummary.remaining.toFixed(2)} remaining`
              : `${Math.abs(budgetSummary.remaining).toFixed(2)} over budget`
            }
          </div>
        </div>

        {/* Category Progress */}
        <div className="space-y-3">
          {budgetSummary.categories.map((category) => (
            <div key={category.category_id} className="space-y-2">
              <div className="flex justify-between items-center">
                <span className="text-sm font-medium">{category.category_name}</span>
                <span className="text-xs text-gray-600">
                  ${category.spent_amount.toFixed(2)} / ${category.budget_amount.toFixed(2)}
                </span>
              </div>
              
              <Progress 
                value={Math.min(category.percentage_used, 100)}
                className={`h-2 ${category.is_over_budget ? 'bg-red-100' : ''}`}
              />
              
              <div className="flex justify-between items-center text-xs">
                <span className={category.is_over_budget ? 'text-red-600' : 'text-gray-600'}>
                  {category.percentage_used.toFixed(1)}% used
                </span>
                <span className={category.remaining >= 0 ? 'text-green-600' : 'text-red-600'}>
                  {category.remaining >= 0 
                    ? `${category.remaining.toFixed(2)} left`
                    : `${Math.abs(category.remaining).toFixed(2)} over`
                  }
                </span>
              </div>
            </div>
          ))}
        </div>
      </CardContent>
    </Card>
  );
}

Key Learnings - Phase 19:

  • Data Visualization: Effective use of charts for financial data
  • Progress Indicators: Visual feedback for budget vs spending
  • Responsive Design: Charts that work on different screen sizes
  • Color Coding: Visual cues for budget status and alerts

🏗️ Phase 20: Advanced Expense Management Features

Step 48: Budget Management Interface

typescript
// src/components/budgets/BudgetManager.tsx
import React, { useState } from 'react';
import { Plus, Edit, Trash2 } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { BudgetForm } from './BudgetForm';
import { useBudgetSummary, useCreateBudget } from '@/hooks/useBudgets';
import { useCategories } from '@/hooks/useCategories';
import { BudgetCreate } from '@/types/expense';

export function BudgetManager() {
  const [isDialogOpen, setIsDialogOpen] = useState(false);
  const currentDate = new Date();
  const [selectedMonth, setSelectedMonth] = useState(currentDate.getMonth() + 1);
  const [selectedYear, setSelectedYear] = useState(currentDate.getFullYear());

  const { data: budgetSummary, isLoading } = useBudgetSummary(selectedMonth, selectedYear);
  const { data: categories } = useCategories();
  const createBudgetMutation = useCreateBudget();

  const handleCreateBudget = async (data: BudgetCreate) => {
    try {
      await createBudgetMutation.mutateAsync(data);
      setIsDialogOpen(false);
    } catch (error) {
      // Error handled by the hook
    }
  };

  const availableCategories = categories?.filter(category => 
    !budgetSummary?.categories.some(budget => budget.category_id === category.id)
  ) || [];

  return (
    <div className="space-y-6">
      <div className="flex justify-between items-center">
        <h1 className="text-3xl font-bold">Budget Management</h1>
        <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
          <DialogTrigger asChild>
            <Button disabled={availableCategories.length === 0}>
              <Plus className="h-4 w-4 mr-2" />
              Add Budget
            </Button>
          </DialogTrigger>
          <DialogContent>
            <DialogHeader>
              <DialogTitle>Create New Budget</DialogTitle>
            </DialogHeader>
            <BudgetForm
              availableCategories={availableCategories}
              defaultMonth={selectedMonth}
              defaultYear={selectedYear}
              onSubmit={handleCreateBudget}
              isLoading={createBudgetMutation.isPending}
            />
          </DialogContent>
        </Dialog>
      </div>

      {/* Period Selector */}
      <Card>
        <CardHeader>
          <CardTitle>Budget Period</CardTitle>
        </CardHeader>
        <CardContent>
          <div className="flex gap-4">
            <select
              value={selectedMonth}
              onChange={(e) => setSelectedMonth(parseInt(e.target.value))}
              className="border rounded px-3 py-2"
            >
              {Array.from({ length: 12 }, (_, i) => (
                <option key={i + 1} value={i + 1}>
                  {new Date(2024, i).toLocaleString('default', { month: 'long' })}
                </option>
              ))}
            </select>
            <select
              value={selectedYear}
              onChange={(e) => setSelectedYear(parseInt(e.target.value))}
              className="border rounded px-3 py-2"
            >
              {Array.from({ length: 5 }, (_, i) => (
                <option key={currentDate.getFullYear() - 2 + i} value={currentDate.getFullYear() - 2 + i}>
                  {currentDate.getFullYear() - 2 + i}
                </option>
              ))}
            </select>
          </div>
        </CardContent>
      </Card>

      {/* Budget Summary */}
      {budgetSummary && (
        <Card>
          <CardHeader>
            <CardTitle>
              Budget Summary - {new Date(selectedYear, selectedMonth - 1).toLocaleString('default', { month: 'long', year: 'numeric' })}
            </CardTitle>
          </CardHeader>
          <CardContent>
            <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
              <div className="text-center p-4 bg-blue-50 rounded">
                <div className="text-2xl font-bold text-blue-600">
                  ${budgetSummary.total_budget.toFixed(2)}
                </div>
                <div className="text-sm text-gray-600">Total Budget</div>
              </div>
              <div className="text-center p-4 bg-green-50 rounded">
                <div className="text-2xl font-bold text-green-600">
                  ${budgetSummary.total_spent.toFixed(2)}
                </div>
                <div className="text-sm text-gray-600">Total Spent</div>
              </div>
              <div className={`text-center p-4 rounded ${
                budgetSummary.remaining >= 0 ? 'bg-green-50' : 'bg-red-50'
              }`}>
                <div className={`text-2xl font-bold ${
                  budgetSummary.remaining >= 0 ? 'text-green-600' : 'text-red-600'
                }`}>
                  ${Math.abs(budgetSummary.remaining).toFixed(2)}
                </div>
                <div className="text-sm text-gray-600">
                  {budgetSummary.remaining >= 0 ? 'Remaining' : 'Over Budget'}
                </div>
              </div>
            </div>

            {/* Category Budgets */}
            <div className="space-y-4">
              <h3 className="text-lg font-semibold">Category Budgets</h3>
              {budgetSummary.categories.map((category) => (
                <Card key={category.category_id} className="p-4">
                  <div className="flex justify-between items-start">
                    <div className="flex-1">
                      <h4 className="font-medium">{category.category_name}</h4>
                      <div className="mt-2 space-y-1">
                        <div className="flex justify-between text-sm">
                          <span>Budget: ${category.budget_amount.toFixed(2)}</span>
                          <span>Spent: ${category.spent_amount.toFixed(2)}</span>
                        </div>
                        <div className="w-full bg-gray-200 rounded-full h-2">
                          <div
                            className={`h-2 rounded-full ${
                              category.is_over_budget ? 'bg-red-500' : 'bg-green-500'
                            }`}
                            style={{ width: `${Math.min(category.percentage_used, 100)}%` }}
                          />
                        </div>
                        <div className="flex justify-between text-xs text-gray-600">
                          <span>{category.percentage_used.toFixed(1)}% used</span>
                          <span className={category.remaining >= 0 ? 'text-green-600' : 'text-red-600'}>
                            {category.remaining >= 0 
                              ? `${category.remaining.toFixed(2)} left`
                              : `${Math.abs(category.remaining).toFixed(2)} over`
                            }
                          </span>
                        </div>
                      </div>
                    </div>
                    <div className="flex gap-2 ml-4">
                      <Button size="sm" variant="outline">
                        <Edit className="h-3 w-3" />
                      </Button>
                      <Button size="sm" variant="outline">
                        <Trash2 className="h-3 w-3" />
                      </Button>
                    </div>
                  </div>
                </Card>
              ))}
            </div>
          </CardContent>
        </Card>
      )}
    </div>
  );
}

Step 49: Expense Analytics and Reports

typescript
// src/components/analytics/ExpenseAnalytics.tsx
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { DatePickerWithRange } from '@/components/ui/date-range-picker';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts';
import { useExpenses } from '@/hooks/useExpenses';
import { useCategories } from '@/hooks/useCategories';
import { format, startOfMonth, endOfMonth, eachMonthOfInterval, subMonths } from 'date-fns';
import { DateRange } from 'react-day-picker';

export function ExpenseAnalytics() {
  const [dateRange, setDateRange] = useState<DateRange | undefined>({
    from: subMonths(new Date(), 5),
    to: new Date(),
  });
  const [selectedCategory, setSelectedCategory] = useState<string>('all');
  const [viewType, setViewType] = useState<'monthly' | 'category' | 'trends'>('monthly');

  const { data: categories } = useCategories();
  const { data: expenses } = useExpenses({
    start_date: dateRange?.from ? format(dateRange.from, 'yyyy-MM-dd') : undefined,
    end_date: dateRange?.to ? format(dateRange.to, 'yyyy-MM-dd') : undefined,
    category_id: selectedCategory !== 'all' ? parseInt(selectedCategory) : undefined,
  });

  // Process data for monthly trends
  const monthlyData = React.useMemo(() => {
    if (!expenses || !dateRange?.from || !dateRange?.to) return [];

    const months = eachMonthOfInterval({
      start: startOfMonth(dateRange.from),
      end: endOfMonth(dateRange.to),
    });

    return months.map(month => {
      const monthExpenses = expenses.filter(expense => {
        const expenseDate = new Date(expense.expense_date);
        return expenseDate.getMonth() === month.getMonth() && 
               expenseDate.getFullYear() === month.getFullYear();
      });

      const total = monthExpenses.reduce((sum, expense) => sum + expense.amount, 0);
      const count = monthExpenses.length;

      return {
        month: format(month, 'MMM yyyy'),
        total,
        count,
        average: count > 0 ? total / count : 0,
      };
    });
  }, [expenses, dateRange]);

  // Process data for category breakdown
  const categoryData = React.useMemo(() => {
    if (!expenses || !categories) return [];

    const categoryTotals = expenses.reduce((acc, expense) => {
      const category = categories.find(c => c.id === expense.category_id);
      const categoryName = category?.name || 'Unknown';
      
      if (!acc[categoryName]) {
        acc[categoryName] = { total: 0, count: 0 };
      }
      
      acc[categoryName].total += expense.amount;
      acc[categoryName].count += 1;
      
      return acc;
    }, {} as Record<string, { total: number; count: number }>);

    return Object.entries(categoryTotals).map(([name, data]) => ({
      category: name,
      total: data.total,
      count: data.count,
      average: data.total / data.count,
    }));
  }, [expenses, categories]);

  const exportToCSV = () => {
    if (!expenses) return;

    const csvContent = [
      ['Date', 'Amount', 'Description', 'Category', 'Merchant', 'Receipt'],
      ...expenses.map(expense => [
        format(new Date(expense.expense_date), 'yyyy-MM-dd'),
        expense.amount.toString(),
        expense.description,
        categories?.find(c => c.id === expense.category_id)?.name || '',
        expense.merchant_name || '',
        expense.receipt_number || '',
      ])
    ].map(row => row.join(',')).join('\n');

    const blob = new Blob([csvContent], { type: 'text/csv' });
    const url = window.URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `expenses-${format(new Date(), 'yyyy-MM-dd')}.csv`;
    a.click();
    window.URL.revokeObjectURL(url);
  };

  return (
    <div className="space-y-6">
      <div className="flex justify-between items-center">
        <h1 className="text-3xl font-bold">Expense Analytics</h1>
        <Button onClick={exportToCSV} disabled={!expenses?.length}>
          Export CSV
        </Button>
      </div>

      {/* Filters */}
      <Card>
        <CardHeader>
          <CardTitle>Filters</CardTitle>
        </CardHeader>
        <CardContent className="flex flex-wrap gap-4">
          <div className="flex-1 min-w-[200px]">
            <label className="text-sm font-medium">Date Range</label>
            <DatePickerWithRange
              date={dateRange}
              onDateChange={setDateRange}
            />
          </div>

          <div className="min-w-[150px]">
            <label className="text-sm font-medium">Category</label>
            <Select value={selectedCategory} onValueChange={setSelectedCategory}>
              <SelectTrigger>
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="all">All Categories</SelectItem>
                {categories?.map(category => (
                  <SelectItem key={category.id} value={category.id.toString()}>
                    {category.name}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          </div>

          <div className="min-w-[150px]">
            <label className="text-sm font-medium">View Type</label>
            <Select value={viewType} onValueChange={(value: any) => setViewType(value)}>
              <SelectTrigger>
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="monthly">Monthly Trends</SelectItem>
                <SelectItem value="category">Category Breakdown</SelectItem>
                <SelectItem value="trends">Spending Trends</SelectItem>
              </SelectContent>
            </Select>
          </div>
        </CardContent>
      </Card>

      {/* Charts */}
      {viewType === 'monthly' && (
        <Card>
          <CardHeader>
            <CardTitle>Monthly Spending Trends</CardTitle>
          </CardHeader>
          <CardContent>
            <div className="h-[400px]">
              <ResponsiveContainer width="100%" height="100%">
                <LineChart data={monthlyData}>
                  <CartesianGrid strokeDasharray="3 3" />
                  <XAxis dataKey="month" />
                  <YAxis />
                  <Tooltip formatter={(value) => [`${Number(value).toFixed(2)}`, 'Amount']} />
                  <Line 
                    type="monotone" 
                    dataKey="total" 
                    stroke="#8884d8" 
                    strokeWidth={2}
                    name="Total Spent"
                  />
                  <Line 
                    type="monotone" 
                    dataKey="count" 
                    stroke="#82ca9d" 
                    strokeWidth={2}
                    name="Number of Expenses"
                    yAxisId="right"
                  />
                </LineChart>
              </ResponsiveContainer>
            </div>
          </Car
Content is user-generated and unverified.
    React TypeScript Frontend Development Guide | Claude