πŸš€ Module 10 Capstone Project: Full-Stack SaaS Platform

Build a production-ready Software-as-a-Service platform that showcases everything you've learned throughout this course. This comprehensive project integrates TypeScript, React, state management, routing, forms, testing, and deploymentβ€”culminating in a portfolio-worthy application!

🎯 Project Overview

You'll build TaskFlow Pro β€” a collaborative project management SaaS platform with team workspaces, real-time updates, advanced analytics, and mocked subscription tiers. This is a deliberately large, integrated build: every concept from the course (typed models, routing, forms and validation, server and client state, data fetching, testing, performance, and deployment) shows up here working together, the way it would in a real product. The goal is not to learn something new, but to prove you can wire the pieces you already know into one coherent, production-shaped application.

Estimated Time: 30-40 hours over 2-3 weeks

Difficulty: Advanced ⭐⭐⭐⭐⭐

πŸŽ“ Skills You'll Demonstrate

This capstone project showcases mastery of:

  • TypeScript advanced patterns and type safety across a large codebase
  • Complex component architecture with reusable patterns
  • Global state management with Redux Toolkit or Zustand
  • Authentication and authorization flows
  • Real-time data synchronization (WebSocket or polling)
  • Advanced routing with protected routes and role-based access
  • Complex form handling with multi-step wizards and validation
  • Data visualization with charts and analytics dashboards
  • Performance optimization for production
  • Comprehensive testing strategy
  • Accessibility (WCAG 2.1 AA compliance)
  • Production deployment with CI/CD
Project Guide

πŸ“‹ Project Requirements

TaskFlow Pro is a modern project management platform designed to help teams organize work, track progress, and collaborate effectively. Think of it as a blend of Trello, Asana, and Linear with your own unique touches.

🎯 What You're Building

πŸ’‘ Project Description

TaskFlow Pro is a collaborative workspace where teams can:

  • Create and manage projects with customizable workflows
  • Organize tasks with boards, lists, and cards (Kanban-style)
  • Assign tasks to team members with due dates and priorities
  • Track project progress with real-time analytics and charts
  • Collaborate with comments, mentions, and notifications
  • Manage team workspaces with role-based permissions
  • Access the platform from any device (fully responsive)

βœ… Minimum Viable Product (MVP) Requirements

⚠️ Must-Have Features

Your application MUST include these core features:

  1. Authentication System
    • User registration with email verification
    • Login/logout functionality
    • Password reset flow
    • Protected routes and authentication guards
  2. Workspace Management
    • Create and manage workspaces
    • Invite team members via email
    • Role-based access control (Owner, Admin, Member, Viewer)
  3. Project & Task Management
    • Create projects within workspaces
    • Kanban board with drag-and-drop task cards
    • Task creation with title, description, assignee, due date, priority
    • Task status management (To Do, In Progress, Done, etc.)
    • Task filtering and search functionality
  4. Real-Time Updates
    • Live task updates when team members make changes
    • Real-time notifications for mentions and assignments
    • Optimistic UI updates for smooth user experience
  5. Analytics Dashboard
    • Project completion rate visualization
    • Team productivity charts
    • Task distribution by status
    • Time tracking and burndown charts
  6. Responsive Design
    • Mobile-first responsive layout
    • Touch-optimized interactions for mobile devices
    • Adaptive navigation for different screen sizes

🌟 Bonus Features (Choose 2-3)

βœ… Nice-to-Have Enhancements

Implement at least 2-3 of these advanced features to showcase your skills:

  • File Attachments: Upload and attach files to tasks (images, documents, etc.)
  • Activity Feed: Timeline showing all workspace activity and changes
  • Custom Labels/Tags: Color-coded labels for task categorization
  • Time Tracking: Track time spent on tasks with start/stop timers
  • Recurring Tasks: Automatically create tasks on a schedule
  • Dark Mode: Complete dark theme implementation with persistence
  • Keyboard Shortcuts: Power-user keyboard navigation (like Linear)
  • Calendar View: Alternative view showing tasks in calendar format
  • Email Integration: Create tasks via email, send digests
  • Export Features: Export projects to CSV/PDF for reporting
  • Advanced Search: Full-text search with filters and autocomplete
  • Webhooks: Integrate with external services (Slack, Discord, etc.)

🚫 What NOT to Build (Keep It Focused)

❌ Out of Scope

To keep the project manageable, do NOT include:

  • Payment processing or subscription billing (you can mock this)
  • Video/audio chat features
  • Advanced AI or machine learning features
  • Mobile native apps (web-only is fine)
  • Backend infrastructure (use Firebase, Supabase, or mock API)

Focus on frontend excellence! Use existing backend services rather than building your own API.

🎨 Core Features Overview

Let's break down each major feature area and what it should include.

πŸ” Authentication & User Management

// Types for authentication
interface User {
  id: string;
  email: string;
  name: string;
  avatar?: string;
  createdAt: Date;
  emailVerified: boolean;
}

interface AuthState {
  user: User | null;
  isAuthenticated: boolean;
  isLoading: boolean;
  error: string | null;
}

// Features to implement:
// - Registration form with validation
// - Email verification flow
// - Login with remember me option
// - Password reset via email
// - Profile management page
// - Avatar upload
// - Account settings

πŸ‘₯ Workspace & Team Management

interface Workspace {
  id: string;
  name: string;
  description?: string;
  ownerId: string;
  members: WorkspaceMember[];
  createdAt: Date;
  updatedAt: Date;
}

interface WorkspaceMember {
  userId: string;
  role: 'owner' | 'admin' | 'member' | 'viewer';
  joinedAt: Date;
}

// Features to implement:
// - Create workspace wizard
// - Workspace settings page
// - Invite members via email
// - Member management table
// - Role-based permissions system
// - Leave/delete workspace functionality

πŸ“Š Project & Board Management

interface Project {
  id: string;
  workspaceId: string;
  name: string;
  description?: string;
  color?: string;
  status: 'active' | 'archived' | 'completed';
  lists: List[];
  createdAt: Date;
  updatedAt: Date;
}

interface List {
  id: string;
  projectId: string;
  title: string;
  position: number;
  tasks: Task[];
}

interface Task {
  id: string;
  listId: string;
  title: string;
  description?: string;
  assigneeId?: string;
  dueDate?: Date;
  priority: 'low' | 'medium' | 'high' | 'urgent';
  status: 'todo' | 'in-progress' | 'review' | 'done';
  labels?: string[];
  position: number;
  createdAt: Date;
  updatedAt: Date;
}

// Features to implement:
// - Kanban board with drag-and-drop
// - Quick task creation
// - Task detail modal/sidebar
// - Task assignment dropdown
// - Due date picker with calendar
// - Priority indicators
// - Progress tracking

πŸ’¬ Comments & Activity

interface Comment {
  id: string;
  taskId: string;
  authorId: string;
  content: string;
  mentions?: string[];  // User IDs mentioned
  createdAt: Date;
  updatedAt: Date;
}

interface Activity {
  id: string;
  workspaceId: string;
  userId: string;
  type: 'task_created' | 'task_updated' | 'comment_added' | 'member_added';
  entityId: string;
  metadata: Record<string, any>;
  createdAt: Date;
}

// Features to implement:
// - Comment thread on tasks
// - @mention functionality
// - Activity feed timeline
// - Notification system
// - Real-time updates

πŸ“ˆ Analytics & Reporting

interface Analytics {
  projectId: string;
  completionRate: number;
  totalTasks: number;
  completedTasks: number;
  overdueTasks: number;
  tasksByStatus: Record<TaskStatus, number>;
  tasksByPriority: Record<TaskPriority, number>;
  memberProductivity: {
    userId: string;
    tasksCompleted: number;
    tasksInProgress: number;
  }[];
}

// Features to implement:
// - Dashboard with key metrics cards
// - Completion rate chart (line/area)
// - Task distribution pie/donut chart
// - Team productivity bar chart
// - Filter by date range
// - Export to CSV
graph TB A[User Lands on App] --> B{"Authenticated?"} B -->|No| C[Landing Page] C --> D["Sign Up / Login"] D --> E[Email Verification] E --> F["Create / Join Workspace"] B -->|Yes| F F --> G[Workspace Dashboard] G --> H[View Projects] H --> I[Kanban Board] I --> J[Manage Tasks] J --> K[View Analytics] I --> L[Add Comments] I --> M[Assign Members] I --> N[Set Due Dates] style A fill:#667eea,stroke:#764ba2,stroke-width:2px,color:#fff style F fill:#48bb78,stroke:#38a169,stroke-width:2px,color:#fff style I fill:#ed8936,stroke:#dd6b20,stroke-width:2px,color:#fff style K fill:#9f7aea,stroke:#805ad5,stroke-width:2px,color:#fff

πŸ—οΈ Technical Architecture

Let's design the technical foundation for your application.

πŸ“¦ Tech Stack Requirements

Category Required Alternatives
Core Framework React 19 with TypeScript 5.x -
Build Tool Vite 6+ (Node 20 or 22) Next.js, TanStack Start
Routing React Router v7 (the react-router package) TanStack Router
Client State Zustand v5 or Redux Toolkit Context API + useReducer
Server State / Data Fetching TanStack Query v5 SWR, custom hooks
Forms React Hook Form v7 + Zod 4 TanStack Form, Formik + Yup
UI Components Tailwind CSS v4 + Headless UI shadcn/ui, Chakra UI, Material UI
Drag & Drop @dnd-kit Pragmatic drag-and-drop
Charts Recharts or Chart.js Victory, Nivo
Testing Vitest + React Testing Library + MSW v2 Playwright (E2E)
Backend/Database Firebase or Supabase Mock API with MSW
Deployment Vercel or Netlify GitHub Pages, Railway

πŸ’‘ Backend Strategy

You have three options for handling backend functionality:

  1. Firebase/Supabase (Recommended): Use a BaaS platform for auth, database, and real-time features
  2. Mock API: Use MSW (Mock Service Worker) to simulate backend responses
  3. Build Your Own: Create a simple Express/Nest.js API (only if you have time)

Recommendation: Use Firebase or Supabase to focus on frontend skills!

πŸ—‚οΈ Folder Structure

taskflow-pro/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ assets/              # Images, icons, fonts
β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”œβ”€β”€ auth/           # Login, Register, ResetPassword
β”‚   β”‚   β”œβ”€β”€ board/          # Kanban board components
β”‚   β”‚   β”œβ”€β”€ common/         # Button, Modal, Input, etc.
β”‚   β”‚   β”œβ”€β”€ dashboard/      # Dashboard widgets
β”‚   β”‚   β”œβ”€β”€ layout/         # Header, Sidebar, Footer
β”‚   β”‚   β”œβ”€β”€ tasks/          # TaskCard, TaskDetail, TaskForm
β”‚   β”‚   └── workspace/      # Workspace components
β”‚   β”œβ”€β”€ features/           # Feature-based organization
β”‚   β”‚   β”œβ”€β”€ auth/
β”‚   β”‚   β”‚   β”œβ”€β”€ authSlice.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ authAPI.ts
β”‚   β”‚   β”‚   └── authHooks.ts
β”‚   β”‚   β”œβ”€β”€ projects/
β”‚   β”‚   β”œβ”€β”€ tasks/
β”‚   β”‚   └── workspaces/
β”‚   β”œβ”€β”€ hooks/              # Custom React hooks
β”‚   β”‚   β”œβ”€β”€ useAuth.ts
β”‚   β”‚   β”œβ”€β”€ useDebounce.ts
β”‚   β”‚   β”œβ”€β”€ useLocalStorage.ts
β”‚   β”‚   └── useRealtime.ts
β”‚   β”œβ”€β”€ lib/                # Third-party configs
β”‚   β”‚   β”œβ”€β”€ firebase.ts
β”‚   β”‚   β”œβ”€β”€ queryClient.ts
β”‚   β”‚   └── store.ts
β”‚   β”œβ”€β”€ pages/              # Page components
β”‚   β”‚   β”œβ”€β”€ Dashboard.tsx
β”‚   β”‚   β”œβ”€β”€ Login.tsx
β”‚   β”‚   β”œβ”€β”€ Project.tsx
β”‚   β”‚   └── Workspace.tsx
β”‚   β”œβ”€β”€ services/           # API services
β”‚   β”‚   β”œβ”€β”€ api.ts
β”‚   β”‚   β”œβ”€β”€ auth.service.ts
β”‚   β”‚   β”œβ”€β”€ project.service.ts
β”‚   β”‚   └── task.service.ts
β”‚   β”œβ”€β”€ types/              # TypeScript types
β”‚   β”‚   β”œβ”€β”€ auth.types.ts
β”‚   β”‚   β”œβ”€β”€ project.types.ts
β”‚   β”‚   └── task.types.ts
β”‚   β”œβ”€β”€ utils/              # Utility functions
β”‚   β”‚   β”œβ”€β”€ date.ts
β”‚   β”‚   β”œβ”€β”€ validation.ts
β”‚   β”‚   └── formatters.ts
β”‚   β”œβ”€β”€ App.tsx
β”‚   β”œβ”€β”€ main.tsx
β”‚   └── router.tsx
β”œβ”€β”€ public/
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ components/
β”‚   β”œβ”€β”€ integration/
β”‚   └── utils/
β”œβ”€β”€ .env.example
β”œβ”€β”€ package.json
β”œβ”€β”€ tsconfig.json
β”œβ”€β”€ vite.config.ts
└── README.md

πŸ”„ Data Flow Architecture

The single most important architectural decision in this app is the split between server state and client state. Server state is anything that lives in your database and is fetched over the network β€” workspaces, projects, tasks, comments. It is asynchronous, shared between users, and can go stale. That is exactly what TanStack Query is built for: it owns caching, background refetching, and mutation lifecycles, so you never hand-roll loading flags or manual cache invalidation. Client state is the purely local UI state that never leaves the browser β€” which modal is open, the active board filter, the collapsed/expanded sidebar. That belongs in Zustand (or Redux Toolkit). Keeping these two worlds separate is what prevents the tangled "everything in one global store" mess that sinks large React apps: components read server data from Query and ephemeral UI flags from Zustand, and neither leaks into the other.

graph LR A[UI Components] -->|User Actions| B["Client State (Zustand / RTK)"] B -->|State Updates| A A -->|API Calls| C["Server State (TanStack Query)"] C -->|"Fetch / Mutate"| D[Backend Services] D -->|Real-time Events| E["WebSocket / Firebase"] E -->|Updates| C C -->|"Cache & State"| B style A fill:#667eea,stroke:#764ba2,stroke-width:2px,color:#fff style B fill:#48bb78,stroke:#38a169,stroke-width:2px,color:#fff style C fill:#ed8936,stroke:#dd6b20,stroke-width:2px,color:#fff style D fill:#4299e1,stroke:#3182ce,stroke-width:2px,color:#fff style E fill:#9f7aea,stroke:#805ad5,stroke-width:2px,color:#fff

πŸ”’ Authentication Flow

sequenceDiagram participant U as User participant A as App participant Auth as Auth Service participant API as Backend U->>A: Visit protected route A->>Auth: Check authentication Auth-->>A: Not authenticated A->>U: Redirect to login U->>A: Submit credentials A->>Auth: Login request Auth->>API: Validate credentials API-->>Auth: Return token + user Auth->>A: Store token A->>U: Redirect to dashboard U->>A: Request data A->>API: Fetch with token API-->>A: Return data A->>U: Display data

πŸ—οΈ Phase 1: Foundation & Setup (4-6 hours)

Start by setting up your development environment and creating the basic project structure.

βœ… Phase 1 Checklist

Tasks to Complete

  1. Initialize project with Vite + React + TypeScript
  2. Set up Tailwind CSS and configure custom theme
  3. Install and configure all required dependencies
  4. Create folder structure following the architecture
  5. Set up React Router with route configuration
  6. Configure Redux Toolkit or Zustand for state management
  7. Set up TanStack Query (React Query)
  8. Create environment variables setup
  9. Initialize Git repository with .gitignore
  10. Set up ESLint and Prettier for code quality
  11. Create basic layout components (Header, Sidebar, Footer)
  12. Implement theme provider (light/dark mode foundation)

πŸ“ Step-by-Step Setup

Step 1: Create Project

# Requires Node 20 or 22 (check with: node --version)

# Create Vite project with React 19 + TypeScript
npm create vite@latest taskflow-pro -- --template react-ts
cd taskflow-pro
npm install

# Routing + server state
npm install react-router @tanstack/react-query

# Client state (pick ONE)
npm install zustand
# npm install @reduxjs/toolkit react-redux

# Forms + validation, drag-and-drop, charts, dates
npm install react-hook-form zod @hookform/resolvers
npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
npm install recharts date-fns
npm install firebase          # OR: npm install @supabase/supabase-js

# Tailwind CSS v4 (Vite plugin β€” no separate init step or PostCSS config)
npm install tailwindcss @tailwindcss/vite

# Dev dependencies: types, tests, mocking
npm install -D @types/node
npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
npm install -D msw   # Mock Service Worker v2 for API mocking

Step 2: Configure Tailwind CSS v4

Tailwind v4 is configured from CSS, not a JavaScript config file. First register the Vite plugin, then declare your theme tokens with @theme in your main stylesheet β€” the utility classes (bg-primary-500, font-sans, etc.) are generated from those tokens automatically.

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

export default defineConfig({
  plugins: [react(), tailwindcss()],
});
/* src/index.css */
@import "tailwindcss";

/* Opt into class-based dark mode: <html class="dark"> toggles it */
@custom-variant dark (&:where(.dark, .dark *));

@theme {
  --color-primary-50: #f0f4ff;
  --color-primary-100: #e0e9ff;
  --color-primary-200: #c7d7fe;
  --color-primary-300: #a5b8fc;
  --color-primary-400: #8192f8;
  --color-primary-500: #667eea; /* Main brand color */
  --color-primary-600: #5b6cd6;
  --color-primary-700: #4f58b3;
  --color-primary-800: #434990;
  --color-primary-900: #3a3d76;

  --font-sans: "Inter", sans-serif;
}

Step 3: Set Up Router

// src/router.tsx
// React Router v7 ships its APIs from the 'react-router' package.
import { createBrowserRouter } from 'react-router';
import { RootLayout } from './components/layout/RootLayout';
import { AuthLayout } from './components/layout/AuthLayout';
import { ProtectedRoute } from './components/auth/ProtectedRoute';

// Page imports
import { LandingPage } from './pages/Landing';
import { LoginPage } from './pages/Login';
import { RegisterPage } from './pages/Register';
import { ResetPasswordPage } from './pages/ResetPassword';
import { DashboardPage } from './pages/Dashboard';
import { WorkspacePage } from './pages/Workspace';
import { ProjectPage } from './pages/Project';
import { AnalyticsPage } from './pages/Analytics';
import { SettingsPage } from './pages/Settings';
import { NotFoundPage } from './pages/NotFound';

export const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    children: [
      {
        index: true,
        element: <LandingPage />,
      },
      {
        path: 'auth',
        element: <AuthLayout />,
        children: [
          { path: 'login', element: <LoginPage /> },
          { path: 'register', element: <RegisterPage /> },
          { path: 'reset-password', element: <ResetPasswordPage /> },
        ],
      },
      {
        path: 'app',
        element: <ProtectedRoute />,
        children: [
          { path: 'dashboard', element: <DashboardPage /> },
          { path: 'workspace/:workspaceId', element: <WorkspacePage /> },
          { path: 'project/:projectId', element: <ProjectPage /> },
          { path: 'analytics', element: <AnalyticsPage /> },
          { path: 'settings', element: <SettingsPage /> },
        ],
      },
      {
        path: '*',
        element: <NotFoundPage />,
      },
    ],
  },
]);

Step 4: Create TypeScript Types

// src/types/index.ts

// User types
export interface User {
  id: string;
  email: string;
  name: string;
  avatar?: string;
  createdAt: Date;
  emailVerified: boolean;
}

// Workspace types
export interface Workspace {
  id: string;
  name: string;
  description?: string;
  ownerId: string;
  members: WorkspaceMember[];
  createdAt: Date;
  updatedAt: Date;
}

export interface WorkspaceMember {
  userId: string;
  role: WorkspaceRole;
  joinedAt: Date;
}

export type WorkspaceRole = 'owner' | 'admin' | 'member' | 'viewer';

// Project types
export interface Project {
  id: string;
  workspaceId: string;
  name: string;
  description?: string;
  color?: string;
  status: ProjectStatus;
  lists: List[];
  createdAt: Date;
  updatedAt: Date;
}

export type ProjectStatus = 'active' | 'archived' | 'completed';

// Task types
export interface Task {
  id: string;
  listId: string;
  title: string;
  description?: string;
  assigneeId?: string;
  dueDate?: Date;
  priority: TaskPriority;
  status: TaskStatus;
  labels?: string[];
  position: number;
  createdAt: Date;
  updatedAt: Date;
}

export type TaskPriority = 'low' | 'medium' | 'high' | 'urgent';
export type TaskStatus = 'todo' | 'in-progress' | 'review' | 'done';

// List types
export interface List {
  id: string;
  projectId: string;
  title: string;
  position: number;
  tasks: Task[];
}

// Comment types
export interface Comment {
  id: string;
  taskId: string;
  authorId: string;
  content: string;
  mentions?: string[];
  createdAt: Date;
  updatedAt: Date;
}

// Activity types
export interface Activity {
  id: string;
  workspaceId: string;
  userId: string;
  type: ActivityType;
  entityId: string;
  metadata: Record<string, any>;
  createdAt: Date;
}

export type ActivityType = 
  | 'task_created'
  | 'task_updated'
  | 'task_completed'
  | 'task_deleted'
  | 'comment_added'
  | 'member_added'
  | 'member_removed'
  | 'project_created';

πŸ’‘ Pro Tip: Environment Variables

Create a .env.example file with all required environment variables:

VITE_FIREBASE_API_KEY=your_api_key_here
VITE_FIREBASE_AUTH_DOMAIN=your_project.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=your_project_id
VITE_FIREBASE_STORAGE_BUCKET=your_project.appspot.com
VITE_FIREBASE_MESSAGING_SENDER_ID=123456789
VITE_FIREBASE_APP_ID=1:123456789:web:abc123

Create a real .env file with your actual credentials and add it to .gitignore!

βœ… Phase 1 Completion Criteria

⚠️ Before Moving to Phase 2

Ensure you have:

  • βœ… Project running locally without errors
  • βœ… Routing working (can navigate between pages)
  • βœ… Tailwind CSS styles applying correctly
  • βœ… TypeScript configured with no errors
  • βœ… All major dependencies installed
  • βœ… Basic layout components created (Header, Footer)
  • βœ… Environment variables configured
  • βœ… Git repository initialized with first commit

πŸ” Phase 2: Authentication & User Management (6-8 hours)

Build a complete authentication system with registration, login, password reset, and protected routes.

βœ… Phase 2 Checklist

Tasks to Complete

  1. Set up Firebase Authentication or Supabase Auth
  2. Create registration form with validation
  3. Create login form with validation
  4. Implement email verification flow
  5. Create password reset flow
  6. Build protected route component
  7. Implement auth state management
  8. Create user profile page
  9. Add avatar upload functionality
  10. Build account settings page
  11. Add logout functionality
  12. Write tests for auth flows

πŸ“ Implementation Guide

Authentication Service

// src/services/auth.service.ts
import { 
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  signOut,
  sendEmailVerification,
  sendPasswordResetEmail,
  updateProfile,
  User as FirebaseUser
} from 'firebase/auth';
import { auth } from '../lib/firebase';
import type { User } from '../types';

class AuthService {
  async register(email: string, password: string, name: string): Promise<User> {
    const userCredential = await createUserWithEmailAndPassword(
      auth,
      email,
      password
    );
    
    await updateProfile(userCredential.user, { displayName: name });
    await sendEmailVerification(userCredential.user);
    
    return this.mapFirebaseUser(userCredential.user);
  }

  async login(email: string, password: string): Promise<User> {
    const userCredential = await signInWithEmailAndPassword(
      auth,
      email,
      password
    );
    return this.mapFirebaseUser(userCredential.user);
  }

  async logout(): Promise<void> {
    await signOut(auth);
  }

  async resetPassword(email: string): Promise<void> {
    await sendPasswordResetEmail(auth, email);
  }

  async verifyEmail(): Promise<void> {
    const user = auth.currentUser;
    if (user) {
      await sendEmailVerification(user);
    }
  }

  getCurrentUser(): User | null {
    const firebaseUser = auth.currentUser;
    return firebaseUser ? this.mapFirebaseUser(firebaseUser) : null;
  }

  private mapFirebaseUser(firebaseUser: FirebaseUser): User {
    return {
      id: firebaseUser.uid,
      email: firebaseUser.email!,
      name: firebaseUser.displayName || '',
      avatar: firebaseUser.photoURL || undefined,
      createdAt: new Date(firebaseUser.metadata.creationTime!),
      emailVerified: firebaseUser.emailVerified,
    };
  }
}

export const authService = new AuthService();

Registration Form Component

// src/components/auth/RegisterForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useNavigate } from 'react-router';
import { authService } from '../../services/auth.service';

const registerSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Invalid email address'),
  password: z.string().min(8, 'Password must be at least 8 characters')
    .regex(/[A-Z]/, 'Password must contain an uppercase letter')
    .regex(/[0-9]/, 'Password must contain a number'),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  path: ['confirmPassword'],
});

type RegisterFormData = z.infer<typeof registerSchema>;

export function RegisterForm() {
  const navigate = useNavigate();
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    setError,
  } = useForm<RegisterFormData>({
    resolver: zodResolver(registerSchema),
  });

  const onSubmit = async (data: RegisterFormData) => {
    try {
      await authService.register(data.email, data.password, data.name);
      // Show success message about email verification
      navigate('/auth/verify-email');
    } catch (error: unknown) {
      const message =
        error instanceof Error ? error.message : 'Failed to create account';
      setError('root', { message });
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
      <div>
        <label htmlFor="name" className="block text-sm font-medium">
          Full Name
        </label>
        <input
          {...register('name')}
          type="text"
          id="name"
          className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
        />
        {errors.name && (
          <p className="mt-1 text-sm text-red-600">{errors.name.message}</p>
        )}
      </div>

      <div>
        <label htmlFor="email" className="block text-sm font-medium">
          Email
        </label>
        <input
          {...register('email')}
          type="email"
          id="email"
          className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
        />
        {errors.email && (
          <p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
        )}
      </div>

      <div>
        <label htmlFor="password" className="block text-sm font-medium">
          Password
        </label>
        <input
          {...register('password')}
          type="password"
          id="password"
          className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
        />
        {errors.password && (
          <p className="mt-1 text-sm text-red-600">{errors.password.message}</p>
        )}
      </div>

      <div>
        <label htmlFor="confirmPassword" className="block text-sm font-medium">
          Confirm Password
        </label>
        <input
          {...register('confirmPassword')}
          type="password"
          id="confirmPassword"
          className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
        />
        {errors.confirmPassword && (
          <p className="mt-1 text-sm text-red-600">
            {errors.confirmPassword.message}
          </p>
        )}
      </div>

      {errors.root && (
        <div className="rounded-md bg-red-50 p-4">
          <p className="text-sm text-red-800">{errors.root.message}</p>
        </div>
      )}

      <button
        type="submit"
        disabled={isSubmitting}
        className="w-full rounded-md bg-primary-600 px-4 py-2 text-white hover:bg-primary-700 disabled:opacity-50"
      >
        {isSubmitting ? 'Creating account...' : 'Create Account'}
      </button>
    </form>
  );
}

Protected Route Component

// src/components/auth/ProtectedRoute.tsx
import { Navigate, Outlet } from 'react-router';
import { useAuth } from '../../hooks/useAuth';
import { LoadingSpinner } from '../common/LoadingSpinner';

export function ProtectedRoute() {
  const { user, isLoading } = useAuth();

  if (isLoading) {
    return (
      <div className="flex h-screen items-center justify-center">
        <LoadingSpinner size="large" />
      </div>
    );
  }

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

  if (!user.emailVerified) {
    return <Navigate to="/auth/verify-email" replace />;
  }

  return <Outlet />;
}

⚠️ Security Best Practices

  • Never store passwords in plain text: Use Firebase/Supabase which handles this
  • Implement rate limiting: Prevent brute force attacks on login
  • Require email verification: Confirm users own the email address
  • Use HTTPS only: Ensure deployment uses SSL/TLS
  • Store tokens securely: Use httpOnly cookies when possible
  • Implement logout everywhere: Allow users to sign out of all devices

βœ… Phase 2 Completion Criteria

Before Moving to Phase 3

  • βœ… Users can register new accounts
  • βœ… Email verification is sent and checked
  • βœ… Users can log in with email/password
  • βœ… Password reset flow works end-to-end
  • βœ… Protected routes redirect unauthenticated users
  • βœ… User profile displays current user info
  • βœ… Users can upload/change avatar
  • βœ… Logout works and clears session
  • βœ… Form validation provides clear error messages
  • βœ… Auth state persists across page refreshes

πŸ“Š Phase 3: Core Features (10-12 hours)

Build the heart of your application: workspaces, projects, and the Kanban board with drag-and-drop functionality.

βœ… Phase 3 Checklist

Tasks to Complete

  1. Create workspace creation wizard/form
  2. Build workspace list and dashboard
  3. Implement workspace member invitation system
  4. Create role-based permission system
  5. Build project creation form
  6. Create project list view with cards
  7. Implement Kanban board layout
  8. Add drag-and-drop for tasks and lists
  9. Create task creation form (quick add + detailed)
  10. Build task detail modal/sidebar
  11. Implement task editing and deletion
  12. Add task assignment functionality
  13. Create due date picker component
  14. Implement priority indicators
  15. Add task filtering and search

🏒 Workspace Management Implementation

Workspace Creation Form

// src/components/workspace/CreateWorkspaceForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { workspaceService } from '../../services/workspace.service';

const workspaceSchema = z.object({
  name: z.string()
    .min(3, 'Name must be at least 3 characters')
    .max(50, 'Name must be less than 50 characters'),
  description: z.string()
    .max(500, 'Description must be less than 500 characters')
    .optional(),
});

type WorkspaceFormData = z.infer<typeof workspaceSchema>;

interface CreateWorkspaceFormProps {
  onSuccess?: (workspaceId: string) => void;
  onCancel?: () => void;
}

export function CreateWorkspaceForm({ onSuccess, onCancel }: CreateWorkspaceFormProps) {
  const queryClient = useQueryClient();
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<WorkspaceFormData>({
    resolver: zodResolver(workspaceSchema),
  });

  const createMutation = useMutation({
    mutationFn: workspaceService.create,
    onSuccess: (workspace) => {
      queryClient.invalidateQueries({ queryKey: ['workspaces'] });
      onSuccess?.(workspace.id);
    },
  });

  const onSubmit = (data: WorkspaceFormData) => {
    createMutation.mutate(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
      <div>
        <label htmlFor="name" className="block text-sm font-medium text-gray-700">
          Workspace Name
        </label>
        <input
          {...register('name')}
          type="text"
          id="name"
          placeholder="e.g., Acme Corporation"
          className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
        />
        {errors.name && (
          <p className="mt-1 text-sm text-red-600">{errors.name.message}</p>
        )}
      </div>

      <div>
        <label htmlFor="description" className="block text-sm font-medium text-gray-700">
          Description (optional)
        </label>
        <textarea
          {...register('description')}
          id="description"
          rows={3}
          placeholder="What's this workspace for?"
          className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
        />
        {errors.description && (
          <p className="mt-1 text-sm text-red-600">{errors.description.message}</p>
        )}
      </div>

      {createMutation.error && (
        <div className="rounded-md bg-red-50 p-4">
          <p className="text-sm text-red-800">
            {createMutation.error.message || 'Failed to create workspace'}
          </p>
        </div>
      )}

      <div className="flex justify-end space-x-3">
        {onCancel && (
          <button
            type="button"
            onClick={onCancel}
            className="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
          >
            Cancel
          </button>
        )}
        <button
          type="submit"
          disabled={createMutation.isPending}
          className="rounded-md bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-700 disabled:opacity-50"
        >
          {createMutation.isPending ? 'Creating...' : 'Create Workspace'}
        </button>
      </div>
    </form>
  );
}

Member Invitation System

// src/components/workspace/InviteMemberForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { workspaceService } from '../../services/workspace.service';
import type { WorkspaceRole } from '../../types';

const inviteSchema = z.object({
  email: z.string().email('Invalid email address'),
  role: z.enum(['admin', 'member', 'viewer']),
});

type InviteFormData = z.infer<typeof inviteSchema>;

interface InviteMemberFormProps {
  workspaceId: string;
  onSuccess?: () => void;
}

export function InviteMemberForm({ workspaceId, onSuccess }: InviteMemberFormProps) {
  const queryClient = useQueryClient();
  const {
    register,
    handleSubmit,
    formState: { errors },
    reset,
  } = useForm<InviteFormData>({
    resolver: zodResolver(inviteSchema),
    defaultValues: { role: 'member' },
  });

  const inviteMutation = useMutation({
    mutationFn: (data: InviteFormData) =>
      workspaceService.inviteMember(workspaceId, data.email, data.role),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['workspace', workspaceId] });
      reset();
      onSuccess?.();
    },
  });

  const onSubmit = (data: InviteFormData) => {
    inviteMutation.mutate(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
      <div>
        <label htmlFor="email" className="block text-sm font-medium text-gray-700">
          Email Address
        </label>
        <input
          {...register('email')}
          type="email"
          id="email"
          placeholder="colleague@example.com"
          className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
        />
        {errors.email && (
          <p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
        )}
      </div>

      <div>
        <label htmlFor="role" className="block text-sm font-medium text-gray-700">
          Role
        </label>
        <select
          {...register('role')}
          id="role"
          className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
        >
          <option value="viewer">Viewer - Can view projects</option>
          <option value="member">Member - Can create and edit</option>
          <option value="admin">Admin - Full permissions</option>
        </select>
      </div>

      <button
        type="submit"
        disabled={inviteMutation.isPending}
        className="w-full rounded-md bg-primary-600 px-4 py-2 text-white hover:bg-primary-700 disabled:opacity-50"
      >
        {inviteMutation.isPending ? 'Sending...' : 'Send Invitation'}
      </button>
    </form>
  );
}

πŸ“‹ Kanban Board Implementation

Board Layout Component

// src/components/board/KanbanBoard.tsx
import { useState } from 'react';
import {
  DndContext,
  DragOverlay,
  closestCorners,
  KeyboardSensor,
  PointerSensor,
  useSensor,
  useSensors,
  DragStartEvent,
  DragOverEvent,
  DragEndEvent,
} from '@dnd-kit/core';
import { arrayMove, sortableKeyboardCoordinates } from '@dnd-kit/sortable';
import { BoardList } from './BoardList';
import { TaskCard } from './TaskCard';
import type { Project, List, Task } from '../../types';

interface KanbanBoardProps {
  project: Project;
  onUpdateProject: (project: Project) => void;
}

export function KanbanBoard({ project, onUpdateProject }: KanbanBoardProps) {
  const [activeTask, setActiveTask] = useState<Task | null>(null);
  
  const sensors = useSensors(
    useSensor(PointerSensor),
    useSensor(KeyboardSensor, {
      coordinateGetter: sortableKeyboardCoordinates,
    })
  );

  const handleDragStart = (event: DragStartEvent) => {
    const { active } = event;
    const task = findTask(active.id as string);
    setActiveTask(task);
  };

  const handleDragOver = (event: DragOverEvent) => {
    const { active, over } = event;
    if (!over) return;

    const activeId = active.id as string;
    const overId = over.id as string;

    if (activeId === overId) return;

    // Find which lists contain the tasks
    const activeList = findListByTaskId(activeId);
    const overList = findListByTaskId(overId) || findListById(overId);

    if (!activeList || !overList) return;

    // Handle moving task between lists
    if (activeList.id !== overList.id) {
      moveTaskBetweenLists(activeId, activeList.id, overList.id);
    }
  };

  const handleDragEnd = (event: DragEndEvent) => {
    const { active, over } = event;
    setActiveTask(null);

    if (!over) return;

    const activeId = active.id as string;
    const overId = over.id as string;

    if (activeId === overId) return;

    const activeList = findListByTaskId(activeId);
    if (!activeList) return;

    // Reorder tasks within the same list
    const oldIndex = activeList.tasks.findIndex((task) => task.id === activeId);
    const newIndex = activeList.tasks.findIndex((task) => task.id === overId);

    if (oldIndex !== -1 && newIndex !== -1) {
      const newTasks = arrayMove(activeList.tasks, oldIndex, newIndex);
      updateListTasks(activeList.id, newTasks);
    }
  };

  const findTask = (taskId: string): Task | null => {
    for (const list of project.lists) {
      const task = list.tasks.find((t) => t.id === taskId);
      if (task) return task;
    }
    return null;
  };

  const findListByTaskId = (taskId: string): List | null => {
    return project.lists.find((list) =>
      list.tasks.some((task) => task.id === taskId)
    ) || null;
  };

  const findListById = (listId: string): List | null => {
    return project.lists.find((list) => list.id === listId) || null;
  };

  const moveTaskBetweenLists = (
    taskId: string,
    fromListId: string,
    toListId: string
  ) => {
    const updatedLists = project.lists.map((list) => {
      if (list.id === fromListId) {
        return {
          ...list,
          tasks: list.tasks.filter((task) => task.id !== taskId),
        };
      }
      if (list.id === toListId) {
        const task = findTask(taskId);
        return task
          ? { ...list, tasks: [...list.tasks, task] }
          : list;
      }
      return list;
    });

    onUpdateProject({ ...project, lists: updatedLists });
  };

  const updateListTasks = (listId: string, newTasks: Task[]) => {
    const updatedLists = project.lists.map((list) =>
      list.id === listId ? { ...list, tasks: newTasks } : list
    );
    onUpdateProject({ ...project, lists: updatedLists });
  };

  return (
    <DndContext
      sensors={sensors}
      collisionDetection={closestCorners}
      onDragStart={handleDragStart}
      onDragOver={handleDragOver}
      onDragEnd={handleDragEnd}
    >
      <div className="flex h-full gap-4 overflow-x-auto p-4">
        {project.lists.map((list) => (
          <BoardList key={list.id} list={list} />
        ))}
      </div>

      <DragOverlay>
        {activeTask ? <TaskCard task={activeTask} /> : null}
      </DragOverlay>
    </DndContext>
  );
}

Task Card Component

// src/components/board/TaskCard.tsx
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { format } from 'date-fns';
import type { Task } from '../../types';

interface TaskCardProps {
  task: Task;
  onClick?: () => void;
}

export function TaskCard({ task, onClick }: TaskCardProps) {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id: task.id });

  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
    opacity: isDragging ? 0.5 : 1,
  };

  const priorityColors = {
    low: 'bg-gray-100 text-gray-800',
    medium: 'bg-blue-100 text-blue-800',
    high: 'bg-orange-100 text-orange-800',
    urgent: 'bg-red-100 text-red-800',
  };

  const isOverdue = task.dueDate && new Date(task.dueDate) < new Date();

  return (
    <div
      ref={setNodeRef}
      style={style}
      {...attributes}
      {...listeners}
      onClick={onClick}
      className="group cursor-pointer rounded-lg border border-gray-200 bg-white p-3 shadow-sm transition-shadow hover:shadow-md"
    >
      <div className="flex items-start justify-between">
        <h4 className="flex-1 text-sm font-medium text-gray-900">
          {task.title}
        </h4>
        <span
          className={`ml-2 rounded-full px-2 py-1 text-xs font-medium ${
            priorityColors[task.priority]
          }`}
        >
          {task.priority}
        </span>
      </div>

      {task.description && (
        <p className="mt-2 line-clamp-2 text-sm text-gray-600">
          {task.description}
        </p>
      )}

      <div className="mt-3 flex items-center justify-between">
        {task.dueDate && (
          <div
            className={`flex items-center text-xs ${
              isOverdue ? 'text-red-600' : 'text-gray-500'
            }`}
          >
            <svg
              className="mr-1 h-4 w-4"
              fill="none"
              stroke="currentColor"
              viewBox="0 0 24 24"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth={2}
                d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
              />
            </svg>
            {format(new Date(task.dueDate), 'MMM d')}
          </div>
        )}

        {task.assigneeId && (
          <div className="flex items-center">
            <div className="h-6 w-6 rounded-full bg-primary-100 flex items-center justify-center text-xs font-medium text-primary-800">
              {/* Replace with actual avatar */}
              JD
            </div>
          </div>
        )}
      </div>

      {task.labels && task.labels.length > 0 && (
        <div className="mt-2 flex flex-wrap gap-1">
          {task.labels.map((label) => (
            <span
              key={label}
              className="rounded-full bg-gray-100 px-2 py-1 text-xs text-gray-700"
            >
              {label}
            </span>
          ))}
        </div>
      )}
    </div>
  );
}

πŸ’‘ Drag & Drop Best Practices

  • Keyboard Accessibility: Ensure keyboard users can reorder tasks
  • Visual Feedback: Show clear drop zones and drag previews
  • Touch Support: Test on mobile devices for touch gestures
  • Optimistic Updates: Update UI immediately, sync with backend after
  • Error Handling: Revert changes if backend sync fails
  • Performance: Virtualize long lists to maintain smooth dragging

βœ… Phase 3 Completion Criteria

Before Moving to Phase 4

  • βœ… Users can create and manage workspaces
  • βœ… Workspace members can be invited via email
  • βœ… Role-based permissions work correctly
  • βœ… Projects can be created within workspaces
  • βœ… Kanban board displays with multiple lists
  • βœ… Tasks can be dragged between lists
  • βœ… Tasks can be reordered within lists
  • βœ… Quick task creation works
  • βœ… Task detail view shows all information
  • βœ… Tasks can be assigned to members
  • βœ… Due dates can be set and displayed
  • βœ… Priority indicators are visible
  • βœ… Task filtering and search work
  • βœ… All CRUD operations persist to backend

⚑ Phase 4: Advanced Features (8-10 hours)

Add real-time updates, comments, notifications, and analytics to create a truly collaborative experience.

βœ… Phase 4 Checklist

Tasks to Complete

  1. Implement real-time task updates (WebSocket or polling)
  2. Create notification system for mentions and assignments
  3. Build comment thread component
  4. Add @mention functionality in comments
  5. Create activity feed showing workspace changes
  6. Build analytics dashboard with charts
  7. Implement completion rate visualization
  8. Create team productivity metrics
  9. Add task distribution charts (by status, priority)
  10. Implement file upload for task attachments
  11. Create custom labels/tags system
  12. Add time tracking features (optional)

πŸ”„ Real-Time Updates Implementation

Real-Time Hook with Firebase

// src/hooks/useRealtime.ts
import { useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { onSnapshot, doc, collection } from 'firebase/firestore';
import { db } from '../lib/firebase';
import type { Task, Project } from '../types';

export function useRealtimeProject(projectId: string) {
  const queryClient = useQueryClient();

  useEffect(() => {
    // Subscribe to project document changes
    const unsubscribe = onSnapshot(
      doc(db, 'projects', projectId),
      (snapshot) => {
        if (snapshot.exists()) {
          const project = {
            id: snapshot.id,
            ...snapshot.data(),
          } as Project;

          // Update React Query cache
          queryClient.setQueryData(['project', projectId], project);
        }
      },
      (error) => {
        console.error('Real-time subscription error:', error);
      }
    );

    // Cleanup subscription on unmount
    return () => unsubscribe();
  }, [projectId, queryClient]);
}

export function useRealtimeTasks(projectId: string) {
  const queryClient = useQueryClient();

  useEffect(() => {
    // Subscribe to tasks collection changes
    const unsubscribe = onSnapshot(
      collection(db, 'projects', projectId, 'tasks'),
      (snapshot) => {
        const tasks: Task[] = [];
        snapshot.forEach((doc) => {
          tasks.push({ id: doc.id, ...doc.data() } as Task);
        });

        // Update cache with new tasks
        queryClient.setQueryData(['tasks', projectId], tasks);

        // Show toast notification for changes made by others
        snapshot.docChanges().forEach((change) => {
          if (change.type === 'added') {
            // Show "New task added" notification
          } else if (change.type === 'modified') {
            // Show "Task updated" notification
          }
        });
      }
    );

    return () => unsubscribe();
  }, [projectId, queryClient]);
}

Notification System

// src/components/notifications/NotificationCenter.tsx
import { useQuery } from '@tanstack/react-query';
import { notificationService } from '../../services/notification.service';
import { formatDistanceToNow } from 'date-fns';
import type { Notification } from '../../types';

export function NotificationCenter() {
  const { data: notifications, isLoading } = useQuery({
    queryKey: ['notifications'],
    queryFn: notificationService.getAll,
  });

  const unreadCount = notifications?.filter((n) => !n.read).length || 0;

  const getNotificationIcon = (type: Notification['type']) => {
    switch (type) {
      case 'task_assigned':
        return 'πŸ“‹';
      case 'mention':
        return '@';
      case 'task_completed':
        return 'βœ…';
      case 'comment':
        return 'πŸ’¬';
      default:
        return 'πŸ””';
    }
  };

  return (
    <div className="relative">
      <button className="relative rounded-lg p-2 hover:bg-gray-100">
        <svg
          className="h-6 w-6"
          fill="none"
          stroke="currentColor"
          viewBox="0 0 24 24"
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth={2}
            d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
          />
        </svg>
        {unreadCount > 0 && (
          <span className="absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full bg-red-600 text-xs text-white">
            {unreadCount}
          </span>
        )}
      </button>

      {/* Notification dropdown */}
      <div className="absolute right-0 mt-2 w-80 rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5">
        <div className="p-4">
          <h3 className="text-lg font-semibold">Notifications</h3>
        </div>
        <div className="max-h-96 overflow-y-auto">
          {isLoading ? (
            <div className="p-4 text-center text-gray-500">Loading...</div>
          ) : notifications?.length === 0 ? (
            <div className="p-4 text-center text-gray-500">
              No notifications
            </div>
          ) : (
            notifications?.map((notification) => (
              <div
                key={notification.id}
                className={`border-b border-gray-100 p-4 hover:bg-gray-50 ${
                  !notification.read ? 'bg-blue-50' : ''
                }`}
              >
                <div className="flex items-start">
                  <span className="text-2xl">
                    {getNotificationIcon(notification.type)}
                  </span>
                  <div className="ml-3 flex-1">
                    <p className="text-sm text-gray-900">
                      {notification.message}
                    </p>
                    <p className="mt-1 text-xs text-gray-500">
                      {formatDistanceToNow(new Date(notification.createdAt), {
                        addSuffix: true,
                      })}
                    </p>
                  </div>
                </div>
              </div>
            ))
          )}
        </div>
      </div>
    </div>
  );
}

πŸ’¬ Comments System

// src/components/task/CommentThread.tsx
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';
import { commentService } from '../../services/comment.service';
import { formatDistanceToNow } from 'date-fns';
import type { Comment } from '../../types';

interface CommentThreadProps {
  taskId: string;
}

export function CommentThread({ taskId }: CommentThreadProps) {
  const queryClient = useQueryClient();
  const [showInput, setShowInput] = useState(false);

  const { data: comments, isLoading } = useQuery({
    queryKey: ['comments', taskId],
    queryFn: () => commentService.getByTaskId(taskId),
  });

  const { register, handleSubmit, reset } = useForm<{ content: string }>();

  const addCommentMutation = useMutation({
    mutationFn: (content: string) =>
      commentService.create(taskId, content),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['comments', taskId] });
      reset();
      setShowInput(false);
    },
  });

  const onSubmit = (data: { content: string }) => {
    addCommentMutation.mutate(data.content);
  };

  // Parse @mentions in comment content
  const renderCommentContent = (content: string) => {
    const mentionRegex = /@(\w+)/g;
    const parts = content.split(mentionRegex);

    return parts.map((part, index) => {
      if (index % 2 === 1) {
        // This is a mention
        return (
          <span key={index} className="text-primary-600 font-medium">
            @{part}
          </span>
        );
      }
      return part;
    });
  };

  return (
    <div className="space-y-4">
      <div className="flex items-center justify-between">
        <h3 className="text-lg font-semibold">
          Comments ({comments?.length || 0})
        </h3>
        {!showInput && (
          <button
            onClick={() => setShowInput(true)}
            className="text-sm text-primary-600 hover:text-primary-700"
          >
            Add Comment
          </button>
        )}
      </div>

      {showInput && (
        <form onSubmit={handleSubmit(onSubmit)} className="space-y-2">
          <textarea
            {...register('content', { required: true })}
            rows={3}
            placeholder="Write a comment... Use @ to mention someone"
            className="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
          />
          <div className="flex justify-end space-x-2">
            <button
              type="button"
              onClick={() => {
                setShowInput(false);
                reset();
              }}
              className="rounded-md px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
            >
              Cancel
            </button>
            <button
              type="submit"
              disabled={addCommentMutation.isPending}
              className="rounded-md bg-primary-600 px-4 py-2 text-sm text-white hover:bg-primary-700 disabled:opacity-50"
            >
              {addCommentMutation.isPending ? 'Posting...' : 'Post Comment'}
            </button>
          </div>
        </form>
      )}

      <div className="space-y-3">
        {isLoading ? (
          <p className="text-center text-gray-500">Loading comments...</p>
        ) : comments?.length === 0 ? (
          <p className="text-center text-gray-500">No comments yet</p>
        ) : (
          comments?.map((comment) => (
            <div key={comment.id} className="rounded-lg bg-gray-50 p-3">
              <div className="flex items-start justify-between">
                <div className="flex items-center space-x-2">
                  <div className="h-8 w-8 rounded-full bg-primary-100 flex items-center justify-center">
                    <span className="text-xs font-medium text-primary-800">
                      {/* Avatar or initials */}
                      JD
                    </span>
                  </div>
                  <div>
                    <p className="text-sm font-medium">John Doe</p>
                    <p className="text-xs text-gray-500">
                      {formatDistanceToNow(new Date(comment.createdAt), {
                        addSuffix: true,
                      })}
                    </p>
                  </div>
                </div>
              </div>
              <p className="mt-2 text-sm text-gray-700">
                {renderCommentContent(comment.content)}
              </p>
            </div>
          ))
        )}
      </div>
    </div>
  );
}

πŸ“Š Analytics Dashboard

// src/components/analytics/AnalyticsDashboard.tsx
import { useQuery } from '@tanstack/react-query';
import {
  LineChart,
  Line,
  PieChart,
  Pie,
  BarChart,
  Bar,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  Legend,
  ResponsiveContainer,
  Cell,
} from 'recharts';
import { analyticsService } from '../../services/analytics.service';

interface AnalyticsDashboardProps {
  workspaceId: string;
  projectId?: string;
}

export function AnalyticsDashboard({ workspaceId, projectId }: AnalyticsDashboardProps) {
  const { data: analytics, isLoading } = useQuery({
    queryKey: ['analytics', workspaceId, projectId],
    queryFn: () => analyticsService.getAnalytics(workspaceId, projectId),
  });

  if (isLoading) {
    return <div>Loading analytics...</div>;
  }

  const COLORS = ['#667eea', '#48bb78', '#ed8936', '#f56565'];

  // Prepare data for charts
  const completionData = analytics?.completionHistory || [];
  const statusData = Object.entries(analytics?.tasksByStatus || {}).map(
    ([status, count]) => ({
      name: status,
      value: count,
    })
  );
  const priorityData = Object.entries(analytics?.tasksByPriority || {}).map(
    ([priority, count]) => ({
      name: priority,
      count,
    })
  );

  return (
    <div className="space-y-6">
      {/* Key Metrics */}
      <div className="grid grid-cols-1 gap-4 md:grid-cols-4">
        <div className="rounded-lg bg-white p-6 shadow">
          <h3 className="text-sm font-medium text-gray-500">Total Tasks</h3>
          <p className="mt-2 text-3xl font-bold text-gray-900">
            {analytics?.totalTasks || 0}
          </p>
        </div>
        <div className="rounded-lg bg-white p-6 shadow">
          <h3 className="text-sm font-medium text-gray-500">Completed</h3>
          <p className="mt-2 text-3xl font-bold text-green-600">
            {analytics?.completedTasks || 0}
          </p>
        </div>
        <div className="rounded-lg bg-white p-6 shadow">
          <h3 className="text-sm font-medium text-gray-500">In Progress</h3>
          <p className="mt-2 text-3xl font-bold text-blue-600">
            {analytics?.inProgressTasks || 0}
          </p>
        </div>
        <div className="rounded-lg bg-white p-6 shadow">
          <h3 className="text-sm font-medium text-gray-500">Completion Rate</h3>
          <p className="mt-2 text-3xl font-bold text-primary-600">
            {analytics?.completionRate || 0}%
          </p>
        </div>
      </div>

      {/* Completion Trend */}
      <div className="rounded-lg bg-white p-6 shadow">
        <h3 className="mb-4 text-lg font-semibold">Completion Trend</h3>
        <ResponsiveContainer width="100%" height={300}>
          <LineChart data={completionData}>
            <CartesianGrid strokeDasharray="3 3" />
            <XAxis dataKey="date" />
            <YAxis />
            <Tooltip />
            <Legend />
            <Line
              type="monotone"
              dataKey="completed"
              stroke="#48bb78"
              strokeWidth={2}
            />
          </LineChart>
        </ResponsiveContainer>
      </div>

      <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
        {/* Task Status Distribution */}
        <div className="rounded-lg bg-white p-6 shadow">
          <h3 className="mb-4 text-lg font-semibold">Tasks by Status</h3>
          <ResponsiveContainer width="100%" height={300}>
            <PieChart>
              <Pie
                data={statusData}
                cx="50%"
                cy="50%"
                labelLine={false}
                label={({ name, percent }) =>
                  `${name}: ${(percent * 100).toFixed(0)}%`
                }
                outerRadius={80}
                fill="#8884d8"
                dataKey="value"
              >
                {statusData.map((entry, index) => (
                  <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
                ))}
              </Pie>
              <Tooltip />
            </PieChart>
          </ResponsiveContainer>
        </div>

        {/* Task Priority Distribution */}
        <div className="rounded-lg bg-white p-6 shadow">
          <h3 className="mb-4 text-lg font-semibold">Tasks by Priority</h3>
          <ResponsiveContainer width="100%" height={300}>
            <BarChart data={priorityData}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="name" />
              <YAxis />
              <Tooltip />
              <Bar dataKey="count" fill="#667eea" />
            </BarChart>
          </ResponsiveContainer>
        </div>
      </div>

      {/* Team Productivity */}
      <div className="rounded-lg bg-white p-6 shadow">
        <h3 className="mb-4 text-lg font-semibold">Team Productivity</h3>
        <div className="space-y-3">
          {analytics?.memberProductivity?.map((member) => (
            <div key={member.userId} className="flex items-center justify-between">
              <div className="flex items-center space-x-3">
                <div className="h-10 w-10 rounded-full bg-primary-100 flex items-center justify-center">
                  <span className="text-sm font-medium text-primary-800">
                    {member.name?.slice(0, 2).toUpperCase()}
                  </span>
                </div>
                <span className="font-medium">{member.name}</span>
              </div>
              <div className="flex space-x-4 text-sm">
                <span className="text-green-600">
                  βœ… {member.tasksCompleted} completed
                </span>
                <span className="text-blue-600">
                  πŸ”„ {member.tasksInProgress} in progress
                </span>
              </div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

πŸ’‘ Real-Time Best Practices

  • Optimistic Updates: Update UI immediately, sync with server after
  • Conflict Resolution: Handle cases where multiple users edit simultaneously
  • Connection Status: Show indicator when connection is lost
  • Debouncing: Don't send updates on every keystroke
  • Presence Indicators: Show who's currently viewing/editing
  • Graceful Degradation: App should work offline with limited functionality

βœ… Phase 4 Completion Criteria

Before Moving to Phase 5

  • βœ… Real-time updates work across multiple browser tabs
  • βœ… Notifications appear for assignments and mentions
  • βœ… Comment system allows threaded discussions
  • βœ… @mentions work and notify users
  • βœ… Activity feed shows recent workspace changes
  • βœ… Analytics dashboard displays key metrics
  • βœ… Charts visualize task distribution and trends
  • βœ… Team productivity metrics are accurate
  • βœ… All bonus features (if implemented) are functional

✨ Phase 5: Polish & Optimization (6-8 hours)

Refine your application with performance optimizations, accessibility improvements, and professional polish.

βœ… Phase 5 Checklist

Tasks to Complete

  1. Implement loading states and skeleton screens
  2. Add error boundaries for graceful error handling
  3. Optimize images and assets
  4. Implement code splitting and lazy loading
  5. Add performance monitoring
  6. Improve accessibility (ARIA labels, keyboard navigation)
  7. Add tooltips and help text throughout
  8. Implement dark mode (if not done earlier)
  9. Add empty states for lists and boards
  10. Create onboarding flow for new users
  11. Add keyboard shortcuts for power users
  12. Polish animations and transitions
  13. Optimize bundle size
  14. Add meta tags for SEO

⚑ Performance Optimization

Code Splitting and Lazy Loading

// src/router.tsx - Updated with lazy loading
import { lazy, Suspense } from 'react';
import { createBrowserRouter } from 'react-router';
import { LoadingSpinner } from './components/common/LoadingSpinner';

// Lazy load heavy components
const DashboardPage = lazy(() => import('./pages/Dashboard'));
const ProjectPage = lazy(() => import('./pages/Project'));
const AnalyticsPage = lazy(() => import('./pages/Analytics'));
const SettingsPage = lazy(() => import('./pages/Settings'));

// Wrapper component for suspense
function LazyPage({ Component }: { Component: React.LazyExoticComponent<any> }) {
  return (
    <Suspense
      fallback={
        <div className="flex h-screen items-center justify-center">
          <LoadingSpinner size="large" />
        </div>
      }
    >
      <Component />
    </Suspense>
  );
}

export const router = createBrowserRouter([
  // ... other routes
  {
    path: 'app',
    element: <ProtectedRoute />,
    children: [
      {
        path: 'dashboard',
        element: <LazyPage Component={DashboardPage} />,
      },
      {
        path: 'project/:projectId',
        element: <LazyPage Component={ProjectPage} />,
      },
      // ... other lazy routes
    ],
  },
]);

React Query Optimization

// src/lib/queryClient.ts
import { QueryClient } from '@tanstack/react-query';

// Note (TanStack Query v5): `cacheTime` was renamed to `gcTime`, and the
// per-query `onSuccess`/`onError`/`onSettled` callbacks were removed from
// defaultOptions. Put side effects in the mutation or in the component's
// own onSuccess handler instead.
export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes: data is "fresh" this long
      gcTime: 10 * 60 * 1000,   // 10 minutes: unused cache is garbage-collected after this
      refetchOnWindowFocus: false,
      retry: 1,
    },
  },
});

Optimistic updates now live on the individual mutation, where you have the specific query keys to touch. The canonical pattern cancels in-flight refetches, snapshots the current cache, writes the optimistic value, and rolls back in onError:

// Optimistic task move, colocated with the mutation that needs it
const moveTask = useMutation({
  mutationFn: taskService.move,
  onMutate: async (moved) => {
    await queryClient.cancelQueries({ queryKey: ['tasks', moved.projectId] });
    const previous = queryClient.getQueryData<Task[]>(['tasks', moved.projectId]);
    queryClient.setQueryData<Task[]>(['tasks', moved.projectId], (old = []) =>
      old.map((t) => (t.id === moved.id ? { ...t, status: moved.status } : t))
    );
    return { previous }; // context passed to onError
  },
  onError: (_err, moved, context) => {
    queryClient.setQueryData(['tasks', moved.projectId], context?.previous);
  },
  onSettled: (_data, _err, moved) => {
    queryClient.invalidateQueries({ queryKey: ['tasks', moved.projectId] });
  },
});

Memoization for Performance

// src/components/board/KanbanBoard.tsx - With memoization
import { useMemo, useCallback, memo } from 'react';

interface KanbanBoardProps {
  project: Project;
  onUpdateProject: (project: Project) => void;
}

export const KanbanBoard = memo(function KanbanBoard({ 
  project, 
  onUpdateProject 
}: KanbanBoardProps) {
  // Memoize expensive computations
  const sortedLists = useMemo(() => {
    return [...project.lists].sort((a, b) => a.position - b.position);
  }, [project.lists]);

  const taskCount = useMemo(() => {
    return project.lists.reduce((sum, list) => sum + list.tasks.length, 0);
  }, [project.lists]);

  // Memoize callbacks to prevent child re-renders
  const handleTaskUpdate = useCallback((taskId: string, updates: Partial<Task>) => {
    // Update logic
  }, [project]);

  return (
    // ... board JSX
  );
});

β™Ώ Accessibility Improvements

// src/components/common/Button.tsx - Accessible button component
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  isLoading?: boolean;
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
}

export function Button({
  children,
  variant = 'primary',
  size = 'md',
  isLoading = false,
  leftIcon,
  rightIcon,
  disabled,
  ...props
}: ButtonProps) {
  return (
    <button
      {...props}
      disabled={disabled || isLoading}
      aria-busy={isLoading}
      aria-disabled={disabled || isLoading}
      className={/* styled classes */}
    >
      {isLoading && (
        <span className="mr-2" aria-hidden="true">
          <LoadingSpinner size="sm" />
        </span>
      )}
      {leftIcon && !isLoading && (
        <span className="mr-2" aria-hidden="true">
          {leftIcon}
        </span>
      )}
      {children}
      {rightIcon && (
        <span className="ml-2" aria-hidden="true">
          {rightIcon}
        </span>
      )}
    </button>
  );
}

Keyboard Shortcuts

// src/hooks/useKeyboardShortcuts.ts
import { useEffect } from 'react';

interface Shortcut {
  key: string;
  ctrl?: boolean;
  shift?: boolean;
  alt?: boolean;
  action: () => void;
  description: string;
}

export function useKeyboardShortcuts(shortcuts: Shortcut[]) {
  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      for (const shortcut of shortcuts) {
        const ctrlMatch = shortcut.ctrl ? event.ctrlKey || event.metaKey : !event.ctrlKey && !event.metaKey;
        const shiftMatch = shortcut.shift ? event.shiftKey : !event.shiftKey;
        const altMatch = shortcut.alt ? event.altKey : !event.altKey;
        const keyMatch = event.key.toLowerCase() === shortcut.key.toLowerCase();

        if (ctrlMatch && shiftMatch && altMatch && keyMatch) {
          event.preventDefault();
          shortcut.action();
          break;
        }
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [shortcuts]);
}

// Usage in component
export function ProjectBoard() {
  const navigate = useNavigate();

  useKeyboardShortcuts([
    {
      key: 'n',
      action: () => openNewTaskModal(),
      description: 'Create new task',
    },
    {
      key: 'k',
      ctrl: true,
      action: () => openCommandPalette(),
      description: 'Open command palette',
    },
    {
      key: '/',
      action: () => focusSearch(),
      description: 'Focus search',
    },
  ]);

  // ... component logic
}

Error Boundaries

// src/components/common/ErrorBoundary.tsx
import { Component, ReactNode } from 'react';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
}

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

export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

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

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('Error caught by boundary:', error, errorInfo);
    // Log to error tracking service (Sentry, LogRocket, etc.)
  }

  render() {
    if (this.state.hasError) {
      return (
        this.props.fallback || (
          <div className="flex min-h-screen items-center justify-center">
            <div className="text-center">
              <h1 className="text-2xl font-bold text-gray-900">
                Something went wrong
              </h1>
              <p className="mt-2 text-gray-600">
                We're sorry for the inconvenience. Please try refreshing the page.
              </p>
              <button
                onClick={() => window.location.reload()}
                className="mt-4 rounded-md bg-primary-600 px-4 py-2 text-white"
              >
                Refresh Page
              </button>
            </div>
          </div>
        )
      );
    }

    return this.props.children;
  }
}

🎨 Empty States and Loading States

// src/components/common/EmptyState.tsx
interface EmptyStateProps {
  icon?: React.ReactNode;
  title: string;
  description: string;
  action?: {
    label: string;
    onClick: () => void;
  };
}

export function EmptyState({ icon, title, description, action }: EmptyStateProps) {
  return (
    <div className="flex flex-col items-center justify-center py-12 text-center">
      {icon && (
        <div className="mb-4 text-6xl opacity-50" aria-hidden="true">
          {icon}
        </div>
      )}
      <h3 className="text-lg font-semibold text-gray-900">{title}</h3>
      <p className="mt-2 max-w-sm text-sm text-gray-600">{description}</p>
      {action && (
        <button
          onClick={action.onClick}
          className="mt-6 rounded-md bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-700"
        >
          {action.label}
        </button>
      )}
    </div>
  );
}

// Usage
<EmptyState
  icon="πŸ“‹"
  title="No tasks yet"
  description="Get started by creating your first task"
  action={{
    label: 'Create Task',
    onClick: () => openTaskModal(),
  }}
/>

⚠️ Performance Checklist

  • βœ… Lighthouse score > 90 in all categories
  • βœ… First Contentful Paint < 1.8s
  • βœ… Time to Interactive < 3.8s
  • βœ… Largest Contentful Paint < 2.5s
  • βœ… Cumulative Layout Shift < 0.1
  • βœ… Bundle size optimized (lazy loading, tree shaking)
  • βœ… Images optimized (WebP, responsive images)
  • βœ… No unnecessary re-renders (use React DevTools)

βœ… Phase 5 Completion Criteria

Before Moving to Phase 6

  • βœ… Loading states display during data fetching
  • βœ… Error boundaries catch and handle errors gracefully
  • βœ… Images are optimized and lazy loaded
  • βœ… Routes are code-split and lazy loaded
  • βœ… Lighthouse performance score > 90
  • βœ… WCAG 2.1 AA accessibility compliance
  • βœ… Keyboard navigation works throughout app
  • βœ… Screen readers can navigate effectively
  • βœ… Dark mode implemented (optional)
  • βœ… Empty states provide clear guidance
  • βœ… Keyboard shortcuts work for common actions
  • βœ… Animations are smooth and purposeful
  • βœ… Bundle size is optimized
  • βœ… App feels polished and professional

πŸ§ͺ Phase 6: Testing & Deployment (6-8 hours)

Ensure your application is production-ready with comprehensive testing, CI/CD setup, and professional deployment.

βœ… Phase 6 Checklist

Tasks to Complete

  1. Write unit tests for utility functions
  2. Write component tests for UI components
  3. Test user interactions and workflows
  4. Test async operations and data fetching
  5. Write integration tests for key features
  6. Set up GitHub Actions for CI/CD
  7. Configure environment variables for production
  8. Deploy to Vercel or Netlify
  9. Set up error tracking (Sentry)
  10. Configure analytics (Google Analytics, Plausible)
  11. Set up monitoring and alerts
  12. Create comprehensive README documentation

πŸ§ͺ Testing Strategy

Follow the testing pyramid: a broad base of fast, cheap unit tests for pure logic (date helpers, validation, reducers); a middle layer of component and integration tests with React Testing Library that exercise real user interactions; and a thin top of end-to-end tests for the handful of critical flows (sign up, create a task, drag it across the board). Mock the network at the boundary with MSW v2 so your tests hit realistic request/response shapes without a live backend. The point is not chasing a coverage number β€” it is buying confidence that the flows a user actually depends on keep working as you refactor.

graph TB E["End-to-End (few) β€” Playwright: critical flows"] I["Integration / Component (some) β€” RTL + MSW"] U["Unit (many) β€” Vitest: pure logic"] E --> I --> U style E fill:#ed8936,stroke:#dd6b20,stroke-width:2px,color:#fff style I fill:#48bb78,stroke:#38a169,stroke-width:2px,color:#fff style U fill:#667eea,stroke:#764ba2,stroke-width:2px,color:#fff

Unit Tests for Utilities

// src/utils/__tests__/date.test.ts
import { describe, it, expect } from 'vitest';
import { formatDate, isOverdue, getDaysUntil } from '../date';

describe('date utilities', () => {
  it('formats dates correctly', () => {
    const date = new Date('2024-01-15');
    expect(formatDate(date)).toBe('Jan 15, 2024');
  });

  it('detects overdue dates', () => {
    const pastDate = new Date('2020-01-01');
    const futureDate = new Date('2030-01-01');
    
    expect(isOverdue(pastDate)).toBe(true);
    expect(isOverdue(futureDate)).toBe(false);
  });

  it('calculates days until date', () => {
    const date = new Date();
    date.setDate(date.getDate() + 5);
    
    expect(getDaysUntil(date)).toBe(5);
  });
});

Component Tests

// src/components/board/__tests__/TaskCard.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { TaskCard } from '../TaskCard';
import type { Task } from '../../../types';

const mockTask: Task = {
  id: '1',
  listId: 'list-1',
  title: 'Test Task',
  description: 'Test description',
  priority: 'high',
  status: 'todo',
  position: 0,
  createdAt: new Date(),
  updatedAt: new Date(),
};

describe('TaskCard', () => {
  it('renders task information', () => {
    render(<TaskCard task={mockTask} />);
    
    expect(screen.getByText('Test Task')).toBeInTheDocument();
    expect(screen.getByText('Test description')).toBeInTheDocument();
    expect(screen.getByText('high')).toBeInTheDocument();
  });

  it('calls onClick when clicked', () => {
    const handleClick = vi.fn();
    render(<TaskCard task={mockTask} onClick={handleClick} />);
    
    fireEvent.click(screen.getByText('Test Task'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('displays overdue indicator for past due dates', () => {
    const overdueTask = {
      ...mockTask,
      dueDate: new Date('2020-01-01'),
    };
    
    render(<TaskCard task={overdueTask} />);
    const dateElement = screen.getByText(/Jan 1/);
    expect(dateElement).toHaveClass('text-red-600');
  });
});

Integration Tests

// src/__tests__/integration/task-workflow.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ProjectBoard } from '../../pages/ProjectBoard';

const createWrapper = () => {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });
  
  return ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
};

describe('Task Workflow', () => {
  it('creates, edits, and completes a task', async () => {
    const user = userEvent.setup();
    render(<ProjectBoard />, { wrapper: createWrapper() });

    // Create task
    const createButton = screen.getByRole('button', { name: /create task/i });
    await user.click(createButton);

    const titleInput = screen.getByLabelText(/title/i);
    await user.type(titleInput, 'New Test Task');

    const submitButton = screen.getByRole('button', { name: /create/i });
    await user.click(submitButton);

    // Verify task appears
    await waitFor(() => {
      expect(screen.getByText('New Test Task')).toBeInTheDocument();
    });

    // Edit task
    const taskCard = screen.getByText('New Test Task');
    await user.click(taskCard);

    const editButton = screen.getByRole('button', { name: /edit/i });
    await user.click(editButton);

    const descriptionInput = screen.getByLabelText(/description/i);
    await user.type(descriptionInput, 'Updated description');

    const saveButton = screen.getByRole('button', { name: /save/i });
    await user.click(saveButton);

    // Complete task
    const completeButton = screen.getByRole('button', { name: /complete/i });
    await user.click(completeButton);

    await waitFor(() => {
      expect(screen.getByText(/completed/i)).toBeInTheDocument();
    });
  });
});

πŸš€ CI/CD Setup

GitHub Actions Workflow

# .github/workflows/ci.yml
name: CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run linter
        run: npm run lint

      - name: Run type check
        run: npm run type-check

      - name: Run tests
        run: npm run test:ci
        env:
          CI: true

      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          files: ./coverage/coverage-final.json

  build:
    needs: test
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build application
        run: npm run build
        env:
          VITE_FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }}
          VITE_FIREBASE_AUTH_DOMAIN: ${{ secrets.FIREBASE_AUTH_DOMAIN }}

      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
      - uses: actions/checkout@v4

      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: '--prod'

Environment Configuration

# .env.production
VITE_APP_ENV=production
VITE_API_URL=https://api.taskflow.com
VITE_FIREBASE_API_KEY=your_production_key
VITE_FIREBASE_AUTH_DOMAIN=taskflow-prod.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=taskflow-prod
VITE_SENTRY_DSN=your_sentry_dsn
VITE_GA_TRACKING_ID=your_ga_id

πŸ“Š Monitoring & Analytics

Error Tracking with Sentry

// src/lib/sentry.ts
// Sentry v8+: integrations are functions, not classes. The old
// `new BrowserTracing()` / `new Sentry.Replay()` API was removed.
import * as Sentry from '@sentry/react';

export function initSentry() {
  if (import.meta.env.PROD) {
    Sentry.init({
      dsn: import.meta.env.VITE_SENTRY_DSN,
      integrations: [
        Sentry.browserTracingIntegration(),
        Sentry.replayIntegration(),
      ],
      tracesSampleRate: 0.1,
      replaysSessionSampleRate: 0.1,
      replaysOnErrorSampleRate: 1.0,
      environment: import.meta.env.VITE_APP_ENV,
    });
  }
}

// Usage in main.tsx
import { initSentry } from './lib/sentry';

initSentry();

πŸ’‘ Deployment Best Practices

  • Environment Variables: Never commit secrets to Git
  • Preview Deployments: Test changes before production
  • Automated Testing: Run tests on every commit
  • Error Tracking: Monitor production errors in real-time
  • Performance Monitoring: Track Core Web Vitals
  • Rollback Plan: Be able to revert deployments quickly

βœ… Phase 6 Completion Criteria

Production Ready Checklist

  • βœ… Unit test coverage > 70%
  • βœ… All critical user flows tested
  • βœ… CI/CD pipeline configured and running
  • βœ… Application deployed to production
  • βœ… Custom domain configured (optional)
  • βœ… SSL/HTTPS enabled
  • βœ… Error tracking active
  • βœ… Analytics configured
  • βœ… Performance monitoring in place
  • βœ… README documentation complete
  • βœ… All environment variables secured
  • βœ… Backup/disaster recovery plan

πŸ’‘ Implementation Guide

Here are additional tips and patterns to help you build your capstone project effectively.

πŸ—‚οΈ State Management Strategy

πŸ’‘ When to Use Which State Management

  • useState: Component-local UI state (modals, form inputs)
  • useReducer: Complex component state with multiple actions
  • Context: Theme, user preferences, auth state
  • Zustand/Redux: Truly global state (workspaces, projects)
  • React Query: All server state (API data, cache)

🎯 Feature Implementation Order

graph TD A["Setup & Auth"] --> B[Workspace CRUD] B --> C[Project CRUD] C --> D[Basic Kanban Board] D --> E["Drag & Drop"] E --> F[Task CRUD] F --> G[Comments] G --> H[Real-time Updates] H --> I[Analytics] I --> J["Polish & Testing"] style A fill:#667eea,stroke:#764ba2,stroke-width:2px,color:#fff style D fill:#48bb78,stroke:#38a169,stroke-width:2px,color:#fff style H fill:#ed8936,stroke:#dd6b20,stroke-width:2px,color:#fff style J fill:#9f7aea,stroke:#805ad5,stroke-width:2px,color:#fff

⚑ Performance Tips

Optimize Component Re-renders
  • Use React.memo() for expensive components
  • Memoize callbacks with useCallback
  • Memoize computed values with useMemo
  • Split large components into smaller ones
  • Use React DevTools Profiler to find bottlenecks
Optimize Data Fetching
  • Implement pagination for large lists
  • Use infinite scroll with React Query
  • Prefetch data for likely next actions
  • Cache aggressively, invalidate precisely
  • Implement optimistic updates for better UX
Optimize Bundle Size
  • Use dynamic imports for heavy libraries
  • Tree-shake unused code
  • Analyze bundle with webpack-bundle-analyzer
  • Use lightweight alternatives (date-fns vs moment)
  • Remove unused dependencies

πŸ› οΈ Debugging Tips

⚠️ Common Issues and Solutions

  • Infinite re-renders: Check useEffect dependencies
  • Stale closures: Update dependency arrays properly
  • Type errors: Ensure proper TypeScript types everywhere
  • State not updating: Don't mutate state directly
  • Tests failing: Mock external dependencies properly
  • Memory leaks: Clean up subscriptions in useEffect

πŸ“š Recommended Resources

Resource Description When to Use
React Documentation Official React docs Understanding core concepts
TanStack Query Docs Data fetching patterns Implementing server state
dnd kit Documentation Drag and drop guides Building Kanban board
Firebase Docs Backend integration Auth and real-time features
Tailwind CSS Docs Utility-first styling Styling components

πŸ“Š Evaluation Criteria

Your capstone project will be evaluated based on the following criteria. Use this as a checklist to ensure you've met all requirements.

🎯 Grading Rubric

Category Weight Criteria
Functionality 30% - All MVP features implemented
- Features work as expected
- No critical bugs
- Edge cases handled
Code Quality 25% - Clean, readable code
- Proper TypeScript usage
- Consistent code style
- Follows React best practices
Architecture 20% - Well-organized folder structure
- Proper separation of concerns
- Reusable components
- Efficient state management
UI/UX 15% - Professional appearance
- Responsive design
- Intuitive user experience
- Accessibility compliance
Testing 10% - Adequate test coverage
- Tests for key features
- Tests actually pass
- Good test practices

πŸ’Ž Excellence Indicators

🌟 What Makes a Portfolio-Worthy Project

  • Professional Polish: Looks and feels like a real product
  • Attention to Detail: Loading states, error handling, empty states
  • Performance: Fast, responsive, optimized
  • Accessibility: Works for everyone, keyboard navigation
  • Documentation: Comprehensive README, code comments
  • Real-World Features: Goes beyond basic CRUD
  • Production Ready: Deployed, monitored, maintained

πŸ“ Submission Requirements

What to Submit

  1. Live Demo URL: Deployed application on Vercel/Netlify
  2. GitHub Repository: Complete source code with README
  3. Demo Video: 3-5 minute walkthrough of key features (optional)
  4. Project Report: Brief document covering:
    • Technical decisions and trade-offs
    • Challenges faced and solutions
    • Features implemented and future plans
    • What you learned from the project

πŸ“‹ README Template

# TaskFlow Pro

A collaborative project management platform built with React, TypeScript, and Firebase.

## πŸš€ Features

- **Workspace Management:** Create and manage team workspaces
- **Kanban Boards:** Drag-and-drop task organization
- **Real-Time Collaboration:** Live updates across team members
- **Analytics Dashboard:** Track project progress and team productivity
- **Comments & Mentions:** Collaborate directly on tasks
- **Role-Based Access:** Owner, Admin, Member, and Viewer roles

## πŸ› οΈ Tech Stack

- **Frontend:** React 19, TypeScript 5.x, Vite
- **Styling:** Tailwind CSS v4
- **Client State:** Zustand v5
- **Server State:** TanStack Query v5
- **Routing:** React Router v7
- **Forms:** React Hook Form v7 + Zod 4
- **Drag & Drop:** @dnd-kit
- **Charts:** Recharts
- **Backend:** Firebase (Auth, Firestore, Storage)
- **Testing:** Vitest, React Testing Library, MSW v2
- **Deployment:** Vercel

## πŸƒ Getting Started

### Prerequisites
- Node.js 20 or 22
- npm or pnpm

### Installation

```bash
git clone https://github.com/yourusername/taskflow-pro.git
cd taskflow-pro
npm install
```

### Environment Setup

Create a `.env` file:

```
VITE_FIREBASE_API_KEY=your_api_key
VITE_FIREBASE_AUTH_DOMAIN=your_domain
VITE_FIREBASE_PROJECT_ID=your_project_id
```

### Run Development Server

```bash
npm run dev
```

Visit http://localhost:5173

## πŸ§ͺ Testing

```bash
npm run test        # Run tests
npm run test:ui     # Open Vitest UI
npm run coverage    # Generate coverage report
```

## πŸ“¦ Building

```bash
npm run build       # Create production build
npm run preview     # Preview production build
```

## 🎯 Key Features Implemented

### Phase 1: Foundation
- [x] Project setup with Vite + TypeScript
- [x] Tailwind CSS configuration
- [x] React Router setup

### Phase 2: Authentication
- [x] User registration and login
- [x] Email verification
- [x] Password reset flow
- [x] Protected routes

### Phase 3: Core Features
- [x] Workspace CRUD operations
- [x] Project management
- [x] Kanban board with drag & drop
- [x] Task management

### Phase 4: Advanced Features
- [x] Real-time updates
- [x] Comment system with @mentions
- [x] Notifications
- [x] Analytics dashboard

### Phase 5: Polish
- [x] Performance optimization
- [x] Accessibility (WCAG 2.1 AA)
- [x] Error boundaries
- [x] Loading states

### Phase 6: Production
- [x] Comprehensive testing
- [x] CI/CD pipeline
- [x] Production deployment
- [x] Error tracking

## πŸ’‘ Challenges & Solutions

**Challenge:** Implementing real-time updates across multiple users
**Solution:** Used Firebase onSnapshot with React Query cache invalidation

**Challenge:** Complex drag-and-drop state management
**Solution:** Leveraged dnd kit with optimistic updates

## πŸ“ˆ What I Learned

- Advanced TypeScript patterns and generics
- Real-time data synchronization strategies
- Production deployment and monitoring
- Writing maintainable, testable React code

## 🚧 Future Enhancements

- [ ] Mobile app with React Native
- [ ] Offline support with service workers
- [ ] Advanced filtering and search
- [ ] Calendar view for tasks
- [ ] Email integrations

## πŸ“„ License

MIT License

## πŸ‘€ Author

Your Name - [GitHub](https://github.com/yourusername)

πŸŽ“ Final Thoughts & Next Steps

πŸŽ‰ Congratulations!

You've reached the capstone projectβ€”the culmination of everything you've learned in this course. This project is your opportunity to showcase your skills, creativity, and problem-solving abilities. Take your time, build something you're proud of, and remember: this is what goes in your portfolio!

πŸ’ͺ Making the Most of Your Capstone

πŸ’‘ Success Strategies

  • Start Early: Give yourself 2-3 weeks to build properly
  • Build Daily: Consistent progress beats cramming
  • Test Continuously: Don't wait until the end to test
  • Git Often: Commit frequently with clear messages
  • Document as You Go: Update README with each feature
  • Deploy Early: Set up deployment in Phase 1, not Phase 6
  • Ask for Feedback: Share your progress with peers
  • Refactor Boldly: Don't be afraid to improve code

🎯 Time Management

Week Focus Deliverables
Week 1 Phases 1-2 Setup complete, auth working, first deployment
Week 2 Phases 3-4 Core features working, real-time updates live
Week 3 Phases 5-6 Polished, tested, documented, production-ready

πŸš€ Going Above and Beyond

Optional: Add Advanced Features

If you finish early and want to challenge yourself further:

  • Webhooks: Integrate with Slack or Discord
  • AI Features: Auto-categorize tasks, smart suggestions
  • Advanced Analytics: Burndown charts, velocity tracking
  • Mobile App: Build React Native version
  • Email Integration: Create tasks via email
  • API Documentation: If you built custom backend

πŸ’Ό Using This in Job Applications

βœ… How to Present Your Capstone

  • Portfolio: Feature it prominently on your portfolio site
  • Resume: List it under "Projects" with 2-3 bullet points
  • LinkedIn: Share it as a project with screenshots
  • GitHub: Pin it to your profile with excellent README
  • Interviews: Use it to demonstrate technical decisions
  • Case Study: Write a blog post about building it

πŸ“š Continue Learning

After completing your capstone, consider exploring:

  • Next.js: Server-side rendering and modern React patterns
  • React Native: Build mobile apps with your React skills
  • Advanced TypeScript: Type gymnastics and advanced patterns
  • System Design: Architecture for large-scale applications
  • DevOps: CI/CD, Docker, Kubernetes
  • Open Source: Contribute to React ecosystem projects

πŸ’¬ Get Help & Support

⚠️ When You Get Stuck

Remember these debugging strategies:

  1. Read the Error: Error messages usually tell you what's wrong
  2. Console.log Strategically: Trace data flow through your app
  3. React DevTools: Inspect component state and props
  4. Check Dependencies: Ensure useEffect dependencies are correct
  5. Isolate the Problem: Create a minimal reproduction
  6. Google Effectively: Include error message and technologies
  7. Ask for Help: StackOverflow, Discord, Reddit with code examples

πŸŽ–οΈ Badge of Completion

πŸ†

React TypeScript Capstone

By completing this capstone project, you demonstrate
mastery of production-level React development with TypeScript.

You are now ready for professional React developer positions!

✨ Final Words

This capstone is more than just a projectβ€”it's proof of your journey from beginner to skilled developer. You've learned TypeScript, mastered React, built complex state management, implemented real-time features, and deployed to production. That's incredible!

Take pride in what you build. Make it uniquely yours. Add features that excite you. Polish it until it shines. This is the project that will open doors for you.

Remember: the best developers aren't those who know everythingβ€”they're those who can figure anything out. You've proven you can do that. Now go build something amazing!

Good luck with your capstone! πŸš€

You've got this! πŸ’ͺ