In this comprehensive final project, you'll build a complete full-stack application that demonstrates mastery of all concepts learned throughout this course. You'll create a professional-grade project that showcases your React development skills, from component architecture to deployment. This capstone project will serve as a portfolio piece demonstrating your ability to build real-world applications.
You'll build a Task Management Dashboard - a comprehensive productivity application that demonstrates modern React development practices. This project will include user authentication, real-time collaboration, advanced UI components, and production-ready features.
Core Features:
Technical Requirements:
Design a scalable architecture for your application:
// Architecture Overview
/*
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Frontend │ │ Backend │ │ Database │
│ (Next.js) │◄──►│ (API Routes) │◄──►│ (Prisma) │
│ │ │ │ │ │
│ • Components │ │ • Authentication│ │ • Users │
│ • State Mgmt │ │ • CRUD Ops │ │ • Tasks │
│ • UI/UX │ │ • Validation │ │ • Projects │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└───────────────────────┼───────────────────────┘
│
┌─────────────────┐
│ External │
│ Services │
│ │
│ • Email │
│ • Storage │
│ • Analytics │
└─────────────────┘
*/
Plan your data structure with relationships:
// Database Schema
interface User {
id: string
email: string
name: string
avatar?: string
createdAt: Date
updatedAt: Date
projects: Project[]
tasks: Task[]
}
interface Project {
id: string
name: string
description?: string
ownerId: string
createdAt: Date
updatedAt: Date
members: ProjectMember[]
tasks: Task[]
}
interface Task {
id: string
title: string
description?: string
status: TaskStatus
priority: TaskPriority
assigneeId?: string
projectId: string
createdBy: string
createdAt: Date
updatedAt: Date
dueDate?: Date
tags: TaskTag[]
}
enum TaskStatus {
TODO = 'TODO',
IN_PROGRESS = 'IN_PROGRESS',
REVIEW = 'REVIEW',
DONE = 'DONE'
}
enum TaskPriority {
LOW = 'LOW',
MEDIUM = 'MEDIUM',
HIGH = 'HIGH',
URGENT = 'URGENT'
}
Design a scalable component structure:
// Component Architecture
/*
src/
├── app/
│ ├── (auth)/
│ │ ├── login/page.tsx
│ │ └── register/page.tsx
│ ├── dashboard/
│ │ ├── page.tsx
│ │ └── layout.tsx
│ ├── projects/
│ │ ├── [id]/
│ │ │ ├── page.tsx
│ │ │ └── tasks/
│ │ └── layout.tsx
│ └── api/
│ ├── auth/
│ ├── projects/
│ └── tasks/
├── components/
│ ├── ui/ // shadcn/ui components
│ ├── forms/ // Form components
│ ├── charts/ // Data visualization
│ └── layout/ // Layout components
├── hooks/ // Custom hooks
├── lib/ // Utilities and configs
├── types/ // TypeScript definitions
└── data-api/ // Database operations
*/
// components/dashboard/DashboardLayout.tsx
interface DashboardLayoutProps {
children: React.ReactNode
}
export function DashboardLayout({ children }: DashboardLayoutProps) {
const { user } = useAuth()
const [sidebarOpen, setSidebarOpen] = useState(false)
return (
<div className="flex h-screen bg-gray-50">
<Sidebar
isOpen={sidebarOpen}
onClose={() => setSidebarOpen(false)}
/>
<div className="flex-1 flex flex-col overflow-hidden">
<Header
user={user}
onMenuClick={() => setSidebarOpen(true)}
/>
<main className="flex-1 overflow-auto p-6">
{children}
</main>
</div>
</div>
)
}
// components/tasks/TaskBoard.tsx
interface TaskBoardProps {
projectId: string
}
export function TaskBoard({ projectId }: TaskBoardProps) {
const { data: tasks, isLoading } = useQuery({
queryKey: ['tasks', projectId],
queryFn: () => getTasksByProject(projectId)
})
const [draggedTask, setDraggedTask] = useState<Task | null>(null)
if (isLoading) return <TaskBoardSkeleton />
return (
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
{Object.values(TaskStatus).map(status => (
<TaskColumn
key={status}
status={status}
tasks={tasks?.filter(task => task.status === status) || []}
onDragStart={setDraggedTask}
onDrop={(task) => handleTaskDrop(task, status)}
/>
))}
</div>
)
}
Implement comprehensive state management:
// context/AppContext.tsx
interface AppState {
user: User | null
currentProject: Project | null
notifications: Notification[]
theme: 'light' | 'dark'
}
interface AppActions {
setUser: (user: User | null) => void
setCurrentProject: (project: Project | null) => void
addNotification: (notification: Notification) => void
toggleTheme: () => void
}
const AppContext = createContext<AppState & AppActions | null>(null)
export function AppProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(appReducer, initialState)
const actions = useMemo(() => ({
setUser: (user) => dispatch({ type: 'SET_USER', payload: user }),
setCurrentProject: (project) => dispatch({ type: 'SET_PROJECT', payload: project }),
addNotification: (notification) => dispatch({ type: 'ADD_NOTIFICATION', payload: notification }),
toggleTheme: () => dispatch({ type: 'TOGGLE_THEME' })
}), [])
return (
<AppContext.Provider value={{ ...state, ...actions }}>
{children}
</AppContext.Provider>
)
}
// hooks/useApp.ts
export function useApp() {
const context = useContext(AppContext)
if (!context) {
throw new Error('useApp must be used within AppProvider')
}
return context
}
Implement efficient data fetching patterns:
// hooks/useTasks.ts
export function useTasks(projectId?: string) {
return useQuery({
queryKey: ['tasks', projectId],
queryFn: () => projectId ? getTasksByProject(projectId) : getAllTasks(),
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
})
}
export function useCreateTask() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: createTask,
onSuccess: (newTask) => {
queryClient.invalidateQueries({ queryKey: ['tasks'] })
queryClient.setQueryData(['task', newTask.id], newTask)
},
onError: (error) => {
toast({
title: "Error",
description: "Failed to create task",
variant: "destructive"
})
}
})
}
export function useUpdateTask() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ id, updates }: { id: string, updates: Partial<Task> }) =>
updateTask(id, updates),
onMutate: async ({ id, updates }) => {
await queryClient.cancelQueries({ queryKey: ['tasks'] })
const previousTasks = queryClient.getQueryData<Task[]>(['tasks'])
queryClient.setQueryData<Task[]>(['tasks'], (old) =>
old?.map(task =>
task.id === id ? { ...task, ...updates } : task
)
)
return { previousTasks }
},
onError: (err, variables, context) => {
queryClient.setQueryData(['tasks'], context?.previousTasks)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] })
}
})
}
Implement real-time features with WebSockets:
// hooks/useRealtime.ts
export function useRealtime(projectId: string) {
const [socket, setSocket] = useState<WebSocket | null>(null)
const { addNotification } = useApp()
useEffect(() => {
const ws = new WebSocket(`${process.env.NEXT_PUBLIC_WS_URL}/projects/${projectId}`)
ws.onopen = () => {
console.log('Connected to real-time updates')
}
ws.onmessage = (event) => {
const message = JSON.parse(event.data)
switch (message.type) {
case 'TASK_UPDATED':
queryClient.invalidateQueries({ queryKey: ['tasks'] })
addNotification({
type: 'info',
title: 'Task Updated',
message: `${message.user.name} updated ${message.task.title}`
})
break
case 'TASK_ASSIGNED':
if (message.assigneeId === user?.id) {
addNotification({
type: 'success',
title: 'New Assignment',
message: `You were assigned to ${message.task.title}`
})
}
break
}
}
setSocket(ws)
return () => {
ws.close()
}
}, [projectId])
const sendUpdate = useCallback((type: string, data: any) => {
if (socket?.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type, ...data }))
}
}, [socket])
return { sendUpdate }
}
Implement sophisticated search functionality:
// hooks/useTaskFilters.ts
interface TaskFilters {
search: string
status: TaskStatus[]
priority: TaskPriority[]
assignee: string[]
dueDate: {
from?: Date
to?: Date
}
tags: string[]
}
export function useTaskFilters(tasks: Task[]) {
const [filters, setFilters] = useState<TaskFilters>({
search: '',
status: [],
priority: [],
assignee: [],
dueDate: {},
tags: []
})
const filteredTasks = useMemo(() => {
return tasks.filter(task => {
// Search filter
if (filters.search && !task.title.toLowerCase().includes(filters.search.toLowerCase()) &&
!task.description?.toLowerCase().includes(filters.search.toLowerCase())) {
return false
}
// Status filter
if (filters.status.length > 0 && !filters.status.includes(task.status)) {
return false
}
// Priority filter
if (filters.priority.length > 0 && !filters.priority.includes(task.priority)) {
return false
}
// Assignee filter
if (filters.assignee.length > 0 && !task.assigneeId ||
!filters.assignee.includes(task.assigneeId)) {
return false
}
// Due date filter
if (filters.dueDate.from && task.dueDate && task.dueDate < filters.dueDate.from) {
return false
}
if (filters.dueDate.to && task.dueDate && task.dueDate > filters.dueDate.to) {
return false
}
return true
})
}, [tasks, filters])
return { filteredTasks, filters, setFilters }
}
Write tests for all components and functionality:
// __tests__/components/TaskBoard.test.tsx
describe('TaskBoard', () => {
const mockTasks = [
{ id: '1', title: 'Task 1', status: TaskStatus.TODO },
{ id: '2', title: 'Task 2', status: TaskStatus.IN_PROGRESS }
]
it('renders task columns correctly', () => {
render(
<TaskBoard projectId="test-project" />
)
Object.values(TaskStatus).forEach(status => {
expect(screen.getByText(status)).toBeInTheDocument()
})
})
it('displays tasks in correct columns', async () => {
jest.spyOn(api, 'getTasksByProject').mockResolvedValue(mockTasks)
render(<TaskBoard projectId="test-project" />)
await waitFor(() => {
expect(screen.getByText('Task 1')).toBeInTheDocument()
expect(screen.getByText('Task 2')).toBeInTheDocument()
})
})
it('handles drag and drop correctly', async () => {
const user = userEvent.setup()
render(<TaskBoard projectId="test-project" />)
const task = screen.getByText('Task 1')
const targetColumn = screen.getByTestId(`column-${TaskStatus.IN_PROGRESS}`)
await user.drag(task, targetColumn)
expect(api.updateTask).toHaveBeenCalledWith('1', {
status: TaskStatus.IN_PROGRESS
})
})
})
// __tests__/hooks/useTasks.test.ts
describe('useTasks', () => {
it('fetches tasks successfully', async () => {
const mockTasks = [{ id: '1', title: 'Test Task' }]
jest.spyOn(api, 'getAllTasks').mockResolvedValue(mockTasks)
const { result } = renderHook(() => useTasks(), {
wrapper: QueryClientProvider
})
await waitFor(() => {
expect(result.current.data).toEqual(mockTasks)
})
})
it('handles create task mutation', async () => {
const newTask = { title: 'New Task', projectId: '1' }
jest.spyOn(api, 'createTask').mockResolvedValue({ id: '2', ...newTask })
const { result } = renderHook(() => useCreateTask(), {
wrapper: QueryClientProvider
})
await act(async () => {
await result.current.mutateAsync(newTask)
})
expect(api.createTask).toHaveBeenCalledWith(newTask)
})
})
Apply all performance techniques learned:
// components/TaskList.tsx
export const TaskList = React.memo(function TaskList({
tasks,
onTaskUpdate
}: TaskListProps) {
const [searchTerm, setSearchTerm] = useState('')
const debouncedSearch = useDebounce(searchTerm, 300)
const filteredTasks = useMemo(() => {
return tasks.filter(task =>
task.title.toLowerCase().includes(debouncedSearch.toLowerCase())
)
}, [tasks, debouncedSearch])
const sortedTasks = useMemo(() => {
return [...filteredTasks].sort((a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
)
}, [filteredTasks])
return (
<div className="space-y-2">
<Input
placeholder="Search tasks..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="mb-4"
/>
<VirtualizedList
items={sortedTasks}
itemHeight={80}
renderItem={({ item }) => (
<TaskItem
key={item.id}
task={item}
onUpdate={onTaskUpdate}
/>
)}
/>
</div>
)
})
// pages/dashboard/page.tsx
export default function DashboardPage() {
return (
<Suspense fallback={<DashboardSkeleton />}>
<DashboardContent />
</Suspense>
)
}
function DashboardContent() {
const { data: projects } = useProjects()
const { data: tasks } = useTasks()
const analytics = useMemo(() => {
return calculateAnalytics(projects, tasks)
}, [projects, tasks])
return (
<div className="space-y-6">
<AnalyticsOverview data={analytics} />
<RecentTasks tasks={tasks?.slice(0, 5) || []} />
<ProjectGrid projects={projects || []} />
</div>
)
}
Set up deployment with all optimizations:
# .github/workflows/deploy.yml
name: Deploy Task Management App
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test -- --coverage
- name: Run E2E tests
run: npm run test:e2e
- name: Upload coverage
uses: codecov/codecov-action@v3
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Build application
run: npm run build
env:
NEXT_PUBLIC_API_URL: ${{ secrets.API_URL }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
vercel-args: '--prod'
Core Functionality:
Technical Requirements:
Project Documentation:
Demo Features:
In this comprehensive final project, you've applied all concepts learned throughout this course to build a professional-grade React application. You've demonstrated mastery of:
Continue Learning:
Portfolio Development:
Congratulations on completing the Modern React Fundamentals course! You now have the skills and experience to build professional React applications and contribute to the modern web development ecosystem.