Skip to content

Frontend Architecture

The Goals Tracker frontend is a modern React application built with TypeScript and Vite.

Technology Stack

  • Framework: React 19.2
  • Language: TypeScript
  • Build Tool: Vite
  • Routing: React Router
  • UI Components: shadcn/ui
  • State Management: React Context / Hooks
  • HTTP Client: Axios / Fetch
  • Styling: Tailwind CSS
  • Icons: Lucide React

Architecture Overview

The frontend follows a component-based architecture:

┌─────────────────────────────────────┐
│           Pages/Views               │  ← Route Components
├─────────────────────────────────────┤
│        Feature Components           │  ← Business Logic
├─────────────────────────────────────┤
│       Shared Components             │  ← Reusable UI
├─────────────────────────────────────┤
│     Services / API Layer            │  ← HTTP Requests
├─────────────────────────────────────┤
│           Backend API               │  ← REST API
└─────────────────────────────────────┘

Project Structure

src/
├── assets/              # Static assets (images, fonts)
├── components/          # Reusable components
│   ├── ui/             # Base UI components (shadcn)
│   │   ├── button.tsx
│   │   ├── card.tsx
│   │   ├── dialog.tsx
│   │   └── input.tsx
│   ├── layout/         # Layout components
│   │   ├── Header.tsx
│   │   ├── Sidebar.tsx
│   │   └── Footer.tsx
│   └── common/         # Common components
│       ├── Loading.tsx
│       ├── ErrorBoundary.tsx
│       └── ProtectedRoute.tsx
├── features/           # Feature modules
│   ├── auth/
│   │   ├── components/
│   │   │   ├── LoginForm.tsx
│   │   │   └── RegisterForm.tsx
│   │   ├── hooks/
│   │   │   └── useAuth.ts
│   │   └── services/
│   │       └── authService.ts
│   ├── goals/
│   │   ├── components/
│   │   │   ├── GoalList.tsx
│   │   │   ├── GoalCard.tsx
│   │   │   ├── GoalForm.tsx
│   │   │   └── GoalDetails.tsx
│   │   ├── hooks/
│   │   │   └── useGoals.ts
│   │   └── services/
│   │       └── goalService.ts
│   └── habits/
│       ├── components/
│       │   ├── HabitList.tsx
│       │   ├── HabitCard.tsx
│       │   ├── HabitForm.tsx
│       │   └── HabitTracker.tsx
│       ├── hooks/
│       │   └── useHabits.ts
│       └── services/
│           └── habitService.ts
├── pages/              # Page components
│   ├── Dashboard.tsx
│   ├── Goals.tsx
│   ├── Habits.tsx
│   ├── Login.tsx
│   ├── Register.tsx
│   └── Profile.tsx
├── contexts/           # React Context providers
│   ├── AuthContext.tsx
│   └── ThemeContext.tsx
├── hooks/             # Custom hooks
│   ├── useApi.ts
│   ├── useLocalStorage.ts
│   └── useDebounce.ts
├── lib/               # Utility libraries
│   ├── api.ts        # API client configuration
│   └── utils.ts      # Helper functions
├── types/            # TypeScript type definitions
│   ├── api.ts
│   ├── models.ts
│   └── index.ts
├── styles/           # Global styles
│   └── globals.css
├── App.tsx           # Main app component
├── main.tsx          # Entry point
└── router.tsx        # Route configuration

Core Concepts

1. Component Architecture

Components are organized by feature and responsibility:

Feature Components - Specific to a feature (Goals, Habits)

// features/goals/components/GoalCard.tsx
interface GoalCardProps {
  goal: Goal;
  onEdit: (goal: Goal) => void;
  onDelete: (id: number) => void;
}

export const GoalCard: React.FC<GoalCardProps> = ({ goal, onEdit, onDelete }) => {
  return (
    <Card>
      <CardHeader>
        <CardTitle>{goal.title}</CardTitle>
        <Badge variant={getPriorityVariant(goal.priority)}>
          {goal.priority}
        </Badge>
      </CardHeader>
      <CardContent>
        <p>{goal.description}</p>
        <Progress value={goal.progress} />
      </CardContent>
      <CardFooter>
        <Button onClick={() => onEdit(goal)}>Edit</Button>
        <Button variant="destructive" onClick={() => onDelete(goal.id)}>
          Delete
        </Button>
      </CardFooter>
    </Card>
  );
};

Shared Components - Reusable across features

// components/common/Loading.tsx
export const Loading: React.FC = () => {
  return (
    <div className="flex items-center justify-center h-screen">
      <Spinner className="w-8 h-8 animate-spin" />
    </div>
  );
};

2. Custom Hooks

Encapsulate reusable logic and side effects:

// features/goals/hooks/useGoals.ts
export const useGoals = () => {
  const [goals, setGoals] = useState<Goal[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const fetchGoals = async () => {
    setLoading(true);
    try {
      const data = await goalService.getAll();
      setGoals(data);
      setError(null);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  const createGoal = async (goalData: CreateGoalRequest) => {
    const newGoal = await goalService.create(goalData);
    setGoals([...goals, newGoal]);
    return newGoal;
  };

  useEffect(() => {
    fetchGoals();
  }, []);

  return { goals, loading, error, fetchGoals, createGoal };
};

3. Service Layer

API communication abstraction:

// features/goals/services/goalService.ts
import { api } from '@/lib/api';
import { Goal, CreateGoalRequest } from '@/types/models';

export const goalService = {
  async getAll(): Promise<Goal[]> {
    const response = await api.get('/api/goals');
    return response.data;
  },

  async getById(id: number): Promise<Goal> {
    const response = await api.get(`/api/goals/${id}`);
    return response.data;
  },

  async create(data: CreateGoalRequest): Promise<Goal> {
    const response = await api.post('/api/goals', data);
    return response.data;
  },

  async update(id: number, data: Partial<Goal>): Promise<Goal> {
    const response = await api.put(`/api/goals/${id}`, data);
    return response.data;
  },

  async delete(id: number): Promise<void> {
    await api.delete(`/api/goals/${id}`);
  },
};

4. API Client Configuration

// lib/api.ts
import axios from 'axios';

export const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8080',
  headers: {
    'Content-Type': 'application/json',
  },
});

// Request interceptor - Add auth token
api.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// Response interceptor - Handle errors
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Redirect to login
      localStorage.removeItem('token');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

Routing

Route Configuration

// router.tsx
import { createBrowserRouter } from 'react-router-dom';

export const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      {
        index: true,
        element: <Dashboard />,
      },
      {
        path: 'goals',
        element: <GoalsPage />,
      },
      {
        path: 'goals/:id',
        element: <GoalDetails />,
      },
      {
        path: 'habits',
        element: <HabitsPage />,
      },
      {
        path: 'profile',
        element: <ProfilePage />,
      },
    ],
  },
  {
    path: '/login',
    element: <LoginPage />,
  },
  {
    path: '/register',
    element: <RegisterPage />,
  },
]);

Protected Routes

// components/common/ProtectedRoute.tsx
export const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const { isAuthenticated } = useAuth();

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

  return <>{children}</>;
};

State Management

Authentication Context

// contexts/AuthContext.tsx
interface AuthContextType {
  user: User | null;
  isAuthenticated: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  register: (data: RegisterData) => Promise<void>;
}

export const AuthContext = createContext<AuthContextType | null>(null);

export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [user, setUser] = useState<User | null>(null);

  const login = async (email: string, password: string) => {
    const response = await authService.login(email, password);
    localStorage.setItem('token', response.token);
    setUser(response.user);
  };

  const logout = () => {
    localStorage.removeItem('token');
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ user, isAuthenticated: !!user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
};

TypeScript Types

Model Definitions

// types/models.ts
export enum Priority {
  LOW = 'LOW',
  MEDIUM = 'MEDIUM',
  HIGH = 'HIGH',
}

export enum GoalStatus {
  IN_PROGRESS = 'IN_PROGRESS',
  COMPLETED = 'COMPLETED',
  ABANDONED = 'ABANDONED',
}

export interface Goal {
  id: number;
  title: string;
  description?: string;
  priority: Priority;
  status: GoalStatus;
  category?: string;
  startDate?: string;
  dueDate?: string;
  progress: number;
  createdAt: string;
  updatedAt: string;
}

export interface CreateGoalRequest {
  title: string;
  description?: string;
  priority: Priority;
  category?: string;
  dueDate?: string;
}

export interface Habit {
  id: number;
  name: string;
  description?: string;
  frequency: 'DAILY' | 'WEEKLY';
  timesPerWeek?: number;
  category?: string;
  startDate: string;
  active: boolean;
  currentStreak: number;
  bestStreak: number;
}

Styling

Tailwind CSS Configuration

// tailwind.config.js
export default {
  content: [
    './index.html',
    './src/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {
      colors: {
        primary: {
          50: '#f0f9ff',
          500: '#3b82f6',
          600: '#2563eb',
        },
      },
    },
  },
  plugins: [],
};

Component Styling

// Using Tailwind classes
<div className="flex items-center justify-between p-4 bg-white rounded-lg shadow-md">
  <h2 className="text-xl font-bold text-gray-900">{goal.title}</h2>
  <Badge className="bg-blue-500 text-white">{goal.status}</Badge>
</div>

Build Configuration

Vite Configuration

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  server: {
    port: 5173,
    host: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    },
  },
  build: {
    outDir: 'dist',
    sourcemap: true,
  },
});

Testing

Component Testing

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

describe('GoalCard', () => {
  const mockGoal: Goal = {
    id: 1,
    title: 'Test Goal',
    priority: Priority.HIGH,
    status: GoalStatus.IN_PROGRESS,
    progress: 50,
  };

  it('renders goal title', () => {
    render(<GoalCard goal={mockGoal} />);
    expect(screen.getByText('Test Goal')).toBeInTheDocument();
  });

  it('displays progress', () => {
    render(<GoalCard goal={mockGoal} />);
    expect(screen.getByText('50%')).toBeInTheDocument();
  });
});

Performance Optimization

Code Splitting

// Lazy load routes
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Goals = lazy(() => import('./pages/Goals'));

<Suspense fallback={<Loading />}>
  <Routes>
    <Route path="/" element={<Dashboard />} />
    <Route path="/goals" element={<Goals />} />
  </Routes>
</Suspense>

Memoization

const GoalList = memo(({ goals }: { goals: Goal[] }) => {
  return goals.map(goal => <GoalCard key={goal.id} goal={goal} />);
});

const filteredGoals = useMemo(() => {
  return goals.filter(goal => goal.status === 'IN_PROGRESS');
}, [goals]);

Environment Variables

# .env
VITE_API_URL=http://localhost:8080
VITE_APP_NAME=Goals Tracker
VITE_ENABLE_ANALYTICS=false

Access in code:

const apiUrl = import.meta.env.VITE_API_URL;

Next Steps