Build and deploy a complete production application integrating all ecosystem tools and best practices from this course.
Build and deploy a complete production application integrating all ecosystem tools and best practices from this course.
Welcome to the final lesson of the React Ecosystem & Production course! This comprehensive project will integrate everything you've learned throughout the course into a complete, production-ready application.
You'll build TaskMaster Pro - an enterprise-grade task management application that demonstrates all the production practices covered in this course.
TaskMaster Pro includes:
1┌─────────────────────────────────────────────────────────────┐
2│ TaskMaster Pro │
3├─────────────────────────────────────────────────────────────┤
4│ Frontend (Next.js 14 + TypeScript) │
5│ ├── App Router with layouts │
6│ ├── Server & Client Components │
7│ ├── Real-time updates (WebSockets) │
8│ └── Progressive Web App features │
9├─────────────────────────────────────────────────────────────┤
10│ Component Library (Storybook) │
11│ ├── Design system components │
12│ ├── Interactive documentation │
13│ └── Visual testing │
14├─────────────────────────────────────────────────────────────┤
15│ Testing Strategy │
16│ ├── Unit tests (Jest + React Testing Library) │
17│ ├── Integration tests (API testing) │
18│ ├── E2E tests (Playwright) │
19│ └── Visual regression tests │
20├─────────────────────────────────────────────────────────────┤
21│ Production Infrastructure │
22│ ├── Vercel deployment with edge functions │
23│ ├── Database (PostgreSQL with Prisma) │
24│ ├── Redis for caching and sessions │
25│ ├── CDN for asset delivery │
26│ └── Monitoring and analytics │
27├─────────────────────────────────────────────────────────────┤
28│ DevOps & Monitoring │
29│ ├── GitHub Actions CI/CD │
30│ ├── Automated testing and deployment │
31│ ├── Performance monitoring (Lighthouse CI) │
32│ ├── Error tracking (Sentry) │
33│ ├── User analytics (PostHog) │
34│ └── Uptime monitoring │
35└─────────────────────────────────────────────────────────────┘This project demonstrates the complete React ecosystem:
1{
2 "framework": "Next.js 14 (App Router)",
3 "language": "TypeScript",
4 "styling": "Tailwind CSS + CSS Modules",
5 "components": "Custom component library",
6 "state": "Zustand + React Query",
7 "forms": "React Hook Form + Zod validation",
8 "animations": "Framer Motion",
9 "icons": "Lucide React"
10}1{
2 "testing": "Jest + React Testing Library + Playwright",
3 "storybook": "Component documentation and testing",
4 "linting": "ESLint + Prettier + TypeScript strict mode",
5 "bundling": "Next.js built-in with custom webpack config",
6 "development": "Hot reload + error boundaries + dev tools"
7}1{
2 "hosting": "Vercel with edge functions",
3 "database": "PostgreSQL with Prisma ORM",
4 "caching": "Redis + Next.js built-in caching",
5 "storage": "AWS S3 for file uploads",
6 "cdn": "Vercel Edge Network",
7 "monitoring": "Sentry + PostHog + Vercel Analytics"
8}By completing this project, you'll demonstrate mastery of:
This project is designed to be completed over 4-5 hours:
Phase 1: Foundation (60 minutes)
Phase 2: Core Features (90 minutes)
Phase 3: Production Ready (60 minutes)
Phase 4: Deployment (30 minutes)
Your completed project should achieve:
Let's begin building your production-ready React application!
Let's start by setting up the foundation for our enterprise-grade task management application.
1# Create the project with our production template
2npx create-next-app@latest taskmaster-pro \
3 --typescript \
4 --tailwind \
5 --eslint \
6 --app \
7 --src-dir \
8 --import-alias "@/*"
9
10cd taskmaster-pro1# Core dependencies
2npm install \
3 @prisma/client \
4 @auth0/nextjs-auth0 \
5 @tanstack/react-query \
6 zustand \
7 react-hook-form \
8 @hookform/resolvers \
9 zod \
10 framer-motion \
11 lucide-react \
12 date-fns \
13 clsx \
14 tailwind-merge \
15 @radix-ui/react-dialog \
16 @radix-ui/react-dropdown-menu \
17 @radix-ui/react-toast
18
19# Development dependencies
20npm install -D \
21 prisma \
22 @types/node \
23 @typescript-eslint/eslint-plugin \
24 @typescript-eslint/parser \
25 eslint-config-prettier \
26 prettier \
27 jest \
28 @testing-library/react \
29 @testing-library/jest-dom \
30 @playwright/test \
31 @storybook/react \
32 @storybook/addon-essentials \
33 @sentry/nextjs \
34 webpack-bundle-analyzerCreate the complete folder structure:
1src/
2├── app/ # Next.js App Router
3│ ├── (auth)/ # Authentication routes
4│ │ ├── login/
5│ │ └── register/
6│ ├── (dashboard)/ # Main application
7│ │ ├── dashboard/
8│ │ ├── projects/
9│ │ ├── tasks/
10│ │ └── settings/
11│ ├── api/ # API routes
12│ │ ├── auth/
13│ │ ├── tasks/
14│ │ ├── projects/
15│ │ └── users/
16│ ├── globals.css
17│ ├── layout.tsx
18│ └── page.tsx
19├── components/ # Component library
20│ ├── ui/ # Base components
21│ │ ├── button.tsx
22│ │ ├── input.tsx
23│ │ ├── modal.tsx
24│ │ ├── toast.tsx
25│ │ └── data-table.tsx
26│ ├── forms/ # Form components
27│ │ ├── task-form.tsx
28│ │ ├── project-form.tsx
29│ │ └── user-form.tsx
30│ ├── layouts/ # Layout components
31│ │ ├── app-layout.tsx
32│ │ ├── dashboard-layout.tsx
33│ │ └── auth-layout.tsx
34│ └── features/ # Feature components
35│ ├── tasks/
36│ ├── projects/
37│ ├── dashboard/
38│ └── auth/
39├── lib/ # Utilities and configurations
40│ ├── prisma.ts
41│ ├── auth.ts
42│ ├── api.ts
43│ ├── validations.ts
44│ ├── utils.ts
45│ └── constants.ts
46├── hooks/ # Custom React hooks
47│ ├── use-tasks.ts
48│ ├── use-projects.ts
49│ ├── use-auth.ts
50│ └── use-local-storage.ts
51├── stores/ # State management
52│ ├── auth-store.ts
53│ ├── tasks-store.ts
54│ └── ui-store.ts
55├── types/ # TypeScript definitions
56│ ├── api.ts
57│ ├── auth.ts
58│ ├── tasks.ts
59│ └── global.ts
60└── styles/ # Stylesheets
61 ├── globals.css
62 └── components.cssprisma/schema.prisma)1generator client {
2 provider = "prisma-client-js"
3}
4
5datasource db {
6 provider = "postgresql"
7 url = env("DATABASE_URL")
8}
9
10model User {
11 id String @id @default(cuid())
12 email String @unique
13 name String
14 avatar String?
15 role Role @default(MEMBER)
16 createdAt DateTime @default(now())
17 updatedAt DateTime @updatedAt
18
19 // Relations
20 tasks Task[]
21 projects ProjectMember[]
22 assignedTasks Task[] @relation("TaskAssignee")
23 comments Comment[]
24
25 @@map("users")
26}
27
28model Project {
29 id String @id @default(cuid())
30 name String
31 description String?
32 color String @default("#3B82F6")
33 status ProjectStatus @default(ACTIVE)
34 createdAt DateTime @default(now())
35 updatedAt DateTime @updatedAt
36
37 // Relations
38 tasks Task[]
39 members ProjectMember[]
40
41 @@map("projects")
42}
43
44model ProjectMember {
45 id String @id @default(cuid())
46 userId String
47 projectId String
48 role ProjectRole @default(MEMBER)
49 joinedAt DateTime @default(now())
50
51 // Relations
52 user User @relation(fields: [userId], references: [id], onDelete: Cascade)
53 project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
54
55 @@unique([userId, projectId])
56 @@map("project_members")
57}
58
59model Task {
60 id String @id @default(cuid())
61 title String
62 description String?
63 status TaskStatus @default(TODO)
64 priority Priority @default(MEDIUM)
65 dueDate DateTime?
66 createdAt DateTime @default(now())
67 updatedAt DateTime @updatedAt
68
69 // Foreign keys
70 projectId String
71 createdById String
72 assigneeId String?
73
74 // Relations
75 project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
76 createdBy User @relation(fields: [createdById], references: [id])
77 assignee User? @relation("TaskAssignee", fields: [assigneeId], references: [id])
78 comments Comment[]
79 attachments Attachment[]
80
81 @@map("tasks")
82}
83
84model Comment {
85 id String @id @default(cuid())
86 content String
87 createdAt DateTime @default(now())
88 updatedAt DateTime @updatedAt
89
90 // Foreign keys
91 taskId String
92 userId String
93
94 // Relations
95 task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
96 user User @relation(fields: [userId], references: [id], onDelete: Cascade)
97
98 @@map("comments")
99}
100
101model Attachment {
102 id String @id @default(cuid())
103 filename String
104 url String
105 size Int
106 mimeType String
107
108 // Foreign keys
109 taskId String
110
111 // Relations
112 task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
113
114 @@map("attachments")
115}
116
117// Enums
118enum Role {
119 ADMIN
120 MANAGER
121 MEMBER
122}
123
124enum ProjectRole {
125 OWNER
126 ADMIN
127 MEMBER
128}
129
130enum ProjectStatus {
131 ACTIVE
132 ARCHIVED
133 COMPLETED
134}
135
136enum TaskStatus {
137 TODO
138 IN_PROGRESS
139 IN_REVIEW
140 COMPLETED
141}
142
143enum Priority {
144 LOW
145 MEDIUM
146 HIGH
147 URGENT
148}.env.local)1# Database
2DATABASE_URL="postgresql://username:password@localhost:5432/taskmaster"
3
4# Authentication (Auth0)
5AUTH0_SECRET='your-auth0-secret'
6AUTH0_BASE_URL='http://localhost:3000'
7AUTH0_ISSUER_BASE_URL='https://your-domain.auth0.com'
8AUTH0_CLIENT_ID='your-client-id'
9AUTH0_CLIENT_SECRET='your-client-secret'
10
11# External Services
12SENTRY_DSN="your-sentry-dsn"
13POSTHOG_KEY="your-posthog-key"
14UPLOADTHING_SECRET="your-uploadthing-secret"
15UPLOADTHING_APP_ID="your-uploadthing-app-id"
16
17# Features
18NEXT_PUBLIC_APP_URL="http://localhost:3000"
19NEXT_PUBLIC_WS_URL="ws://localhost:3001"Update tsconfig.json for enterprise development:
1{
2 "compilerOptions": {
3 "target": "es5",
4 "lib": ["dom", "dom.iterable", "es6"],
5 "allowJs": true,
6 "skipLibCheck": true,
7 "strict": true,
8 "noEmit": true,
9 "esModuleInterop": true,
10 "module": "esnext",
11 "moduleResolution": "bundler",
12 "resolveJsonModule": true,
13 "isolatedModules": true,
14 "jsx": "preserve",
15 "incremental": true,
16 "plugins": [{"name": "next"}],
17
18 // Strict mode for production
19 "noUnusedLocals": true,
20 "noUnusedParameters": true,
21 "noImplicitReturns": true,
22 "noFallthroughCasesInSwitch": true,
23 "exactOptionalPropertyTypes": true,
24
25 // Path mapping
26 "baseUrl": ".",
27 "paths": {
28 "@/*": ["./src/*"],
29 "@/components/*": ["./src/components/*"],
30 "@/lib/*": ["./src/lib/*"],
31 "@/hooks/*": ["./src/hooks/*"],
32 "@/stores/*": ["./src/stores/*"],
33 "@/types/*": ["./src/types/*"],
34 "@/styles/*": ["./src/styles/*"]
35 }
36 },
37 "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
38 "exclude": ["node_modules"]
39}Enhanced next.config.js for production:
1const { withSentryConfig } = require('@sentry/nextjs');
2
3/** @type {import('next').NextConfig} */
4const nextConfig = {
5 experimental: {
6 typedRoutes: true,
7 },
8
9 images: {
10 domains: ['images.unsplash.com', 'utfs.io'],
11 formats: ['image/avif', 'image/webp'],
12 },
13
14 async headers() {
15 return [
16 {
17 source: '/(.*)',
18 headers: [
19 { key: 'X-Frame-Options', value: 'DENY' },
20 { key: 'X-Content-Type-Options', value: 'nosniff' },
21 { key: 'Referrer-Policy', value: 'origin-when-cross-origin' },
22 ],
23 },
24 ];
25 },
26
27 webpack: (config, { dev, isServer }) => {
28 if (!dev && !isServer) {
29 // Bundle analysis
30 if (process.env.ANALYZE === 'true') {
31 const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
32 config.plugins.push(
33 new BundleAnalyzerPlugin({
34 analyzerMode: 'static',
35 openAnalyzer: false,
36 })
37 );
38 }
39 }
40 return config;
41 },
42};
43
44module.exports = withSentryConfig(
45 nextConfig,
46 {
47 silent: true,
48 org: 'your-org',
49 project: 'taskmaster-pro',
50 },
51 {
52 widenClientFileUpload: true,
53 transpileClientSDK: true,
54 tunnelRoute: '/monitoring',
55 hideSourceMaps: true,
56 disableLogger: true,
57 }
58);src/components/ui/button.tsx)1import { ButtonHTMLAttributes, forwardRef } from 'react';
2import { Slot } from '@radix-ui/react-slot';
3import { cva, type VariantProps } from 'class-variance-authority';
4import { cn } from '@/lib/utils';
5
6const buttonVariants = cva(
7 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
8 {
9 variants: {
10 variant: {
11 default: 'bg-primary text-primary-foreground hover:bg-primary/90',
12 destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
13 outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
14 secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
15 ghost: 'hover:bg-accent hover:text-accent-foreground',
16 link: 'text-primary underline-offset-4 hover:underline',
17 },
18 size: {
19 default: 'h-10 px-4 py-2',
20 sm: 'h-9 rounded-md px-3',
21 lg: 'h-11 rounded-md px-8',
22 icon: 'h-10 w-10',
23 },
24 },
25 defaultVariants: {
26 variant: 'default',
27 size: 'default',
28 },
29 }
30);
31
32export interface ButtonProps
33 extends ButtonHTMLAttributes<HTMLButtonElement>,
34 VariantProps<typeof buttonVariants> {
35 asChild?: boolean;
36 isLoading?: boolean;
37}
38
39const Button = forwardRef<HTMLButtonElement, ButtonProps>(
40 ({ className, variant, size, asChild = false, isLoading, children, ...props }, ref) => {
41 const Comp = asChild ? Slot : 'button';
42 return (
43 <Comp
44 className={cn(buttonVariants({ variant, size, className }))}
45 ref={ref}
46 disabled={isLoading}
47 {...props}
48 >
49 {isLoading && (
50 <svg className="mr-2 h-4 w-4 animate-spin" viewBox="0 0 24 24">
51 <circle
52 className="opacity-25"
53 cx="12"
54 cy="12"
55 r="10"
56 stroke="currentColor"
57 strokeWidth="4"
58 />
59 <path
60 className="opacity-75"
61 fill="currentColor"
62 d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
63 />
64 </svg>
65 )}
66 {children}
67 </Comp>
68 );
69 }
70);
71
72Button.displayName = 'Button';
73
74export { Button, buttonVariants };Your project foundation is now ready for enterprise-scale development!
Now let's implement the core functionality of TaskMaster Pro, demonstrating production-ready React patterns.
Create comprehensive types (src/types/tasks.ts):
1export interface Task {
2 id: string;
3 title: string;
4 description?: string;
5 status: TaskStatus;
6 priority: Priority;
7 dueDate?: Date;
8 createdAt: Date;
9 updatedAt: Date;
10 projectId: string;
11 createdById: string;
12 assigneeId?: string;
13
14 // Relations
15 project: Project;
16 createdBy: User;
17 assignee?: User;
18 comments: Comment[];
19 attachments: Attachment[];
20}
21
22export interface CreateTaskData {
23 title: string;
24 description?: string;
25 priority: Priority;
26 dueDate?: Date;
27 projectId: string;
28 assigneeId?: string;
29}
30
31export interface UpdateTaskData extends Partial<CreateTaskData> {
32 status?: TaskStatus;
33}
34
35export enum TaskStatus {
36 TODO = 'TODO',
37 IN_PROGRESS = 'IN_PROGRESS',
38 IN_REVIEW = 'IN_REVIEW',
39 COMPLETED = 'COMPLETED',
40}
41
42export enum Priority {
43 LOW = 'LOW',
44 MEDIUM = 'MEDIUM',
45 HIGH = 'HIGH',
46 URGENT = 'URGENT',
47}
48
49// Form validation schemas
50export const createTaskSchema = z.object({
51 title: z.string().min(1, 'Title is required').max(100),
52 description: z.string().max(1000).optional(),
53 priority: z.nativeEnum(Priority),
54 dueDate: z.date().optional(),
55 projectId: z.string().min(1, 'Project is required'),
56 assigneeId: z.string().optional(),
57});
58
59export const updateTaskSchema = createTaskSchema.partial().extend({
60 status: z.nativeEnum(TaskStatus).optional(),
61});Create API client (src/lib/api/tasks.ts):
1import { Task, CreateTaskData, UpdateTaskData } from '@/types/tasks';
2
3class TasksAPI {
4 private baseURL = '/api/tasks';
5
6 async getTasks(projectId?: string): Promise<Task[]> {
7 const url = projectId
8 ? `${this.baseURL}?projectId=${projectId}`
9 : this.baseURL;
10
11 const response = await fetch(url);
12 if (!response.ok) throw new Error('Failed to fetch tasks');
13 return response.json();
14 }
15
16 async getTask(id: string): Promise<Task> {
17 const response = await fetch(`${this.baseURL}/${id}`);
18 if (!response.ok) throw new Error('Failed to fetch task');
19 return response.json();
20 }
21
22 async createTask(data: CreateTaskData): Promise<Task> {
23 const response = await fetch(this.baseURL, {
24 method: 'POST',
25 headers: { 'Content-Type': 'application/json' },
26 body: JSON.stringify(data),
27 });
28 if (!response.ok) throw new Error('Failed to create task');
29 return response.json();
30 }
31
32 async updateTask(id: string, data: UpdateTaskData): Promise<Task> {
33 const response = await fetch(`${this.baseURL}/${id}`, {
34 method: 'PATCH',
35 headers: { 'Content-Type': 'application/json' },
36 body: JSON.stringify(data),
37 });
38 if (!response.ok) throw new Error('Failed to update task');
39 return response.json();
40 }
41
42 async deleteTask(id: string): Promise<void> {
43 const response = await fetch(`${this.baseURL}/${id}`, {
44 method: 'DELETE',
45 });
46 if (!response.ok) throw new Error('Failed to delete task');
47 }
48}
49
50export const tasksAPI = new TasksAPI();Create data fetching hooks (src/hooks/use-tasks.ts):
1import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
2import { tasksAPI } from '@/lib/api/tasks';
3import { CreateTaskData, UpdateTaskData } from '@/types/tasks';
4import { toast } from '@/components/ui/toast';
5
6export function useTasks(projectId?: string) {
7 return useQuery({
8 queryKey: ['tasks', projectId],
9 queryFn: () => tasksAPI.getTasks(projectId),
10 staleTime: 5 * 60 * 1000, // 5 minutes
11 });
12}
13
14export function useTask(id: string) {
15 return useQuery({
16 queryKey: ['tasks', id],
17 queryFn: () => tasksAPI.getTask(id),
18 enabled: !!id,
19 });
20}
21
22export function useCreateTask() {
23 const queryClient = useQueryClient();
24
25 return useMutation({
26 mutationFn: (data: CreateTaskData) => tasksAPI.createTask(data),
27 onSuccess: (newTask) => {
28 // Update tasks list
29 queryClient.setQueryData(['tasks', newTask.projectId], (old: Task[] = []) => [
30 ...old,
31 newTask,
32 ]);
33
34 // Update all tasks list
35 queryClient.setQueryData(['tasks'], (old: Task[] = []) => [
36 ...old,
37 newTask,
38 ]);
39
40 toast.success('Task created successfully');
41 },
42 onError: (error) => {
43 toast.error(`Failed to create task: ${error.message}`);
44 },
45 });
46}
47
48export function useUpdateTask() {
49 const queryClient = useQueryClient();
50
51 return useMutation({
52 mutationFn: ({ id, data }: { id: string; data: UpdateTaskData }) =>
53 tasksAPI.updateTask(id, data),
54 onSuccess: (updatedTask) => {
55 // Update specific task
56 queryClient.setQueryData(['tasks', updatedTask.id], updatedTask);
57
58 // Update tasks lists
59 ['tasks', ['tasks', updatedTask.projectId]].forEach((queryKey) => {
60 queryClient.setQueryData(queryKey, (old: Task[] = []) =>
61 old.map((task) => task.id === updatedTask.id ? updatedTask : task)
62 );
63 });
64
65 toast.success('Task updated successfully');
66 },
67 onError: (error) => {
68 toast.error(`Failed to update task: ${error.message}`);
69 },
70 });
71}
72
73export function useDeleteTask() {
74 const queryClient = useQueryClient();
75
76 return useMutation({
77 mutationFn: (id: string) => tasksAPI.deleteTask(id),
78 onSuccess: (_, deletedId) => {
79 // Remove from all task lists
80 queryClient.setQueriesData({ queryKey: ['tasks'] }, (old: Task[] = []) =>
81 old.filter((task) => task.id !== deletedId)
82 );
83
84 toast.success('Task deleted successfully');
85 },
86 onError: (error) => {
87 toast.error(`Failed to delete task: ${error.message}`);
88 },
89 });
90}Create a comprehensive task form (src/components/features/tasks/task-form.tsx):
1'use client';
2
3import { useForm } from 'react-hook-form';
4import { zodResolver } from '@hookform/resolvers/zod';
5import { Button } from '@/components/ui/button';
6import { Input } from '@/components/ui/input';
7import { Textarea } from '@/components/ui/textarea';
8import { Select } from '@/components/ui/select';
9import { DatePicker } from '@/components/ui/date-picker';
10import { createTaskSchema, type CreateTaskData, Priority } from '@/types/tasks';
11import { useCreateTask, useUpdateTask } from '@/hooks/use-tasks';
12import { useProjects } from '@/hooks/use-projects';
13import { useUsers } from '@/hooks/use-users';
14
15interface TaskFormProps {
16 task?: Task;
17 projectId?: string;
18 onSuccess?: () => void;
19 onCancel?: () => void;
20}
21
22export function TaskForm({ task, projectId, onSuccess, onCancel }: TaskFormProps) {
23 const isEditing = !!task;
24 const createTask = useCreateTask();
25 const updateTask = useUpdateTask();
26 const { data: projects } = useProjects();
27 const { data: users } = useUsers();
28
29 const form = useForm<CreateTaskData>({
30 resolver: zodResolver(createTaskSchema),
31 defaultValues: {
32 title: task?.title || '',
33 description: task?.description || '',
34 priority: task?.priority || Priority.MEDIUM,
35 dueDate: task?.dueDate,
36 projectId: task?.projectId || projectId || '',
37 assigneeId: task?.assigneeId || '',
38 },
39 });
40
41 const onSubmit = async (data: CreateTaskData) => {
42 try {
43 if (isEditing) {
44 await updateTask.mutateAsync({ id: task.id, data });
45 } else {
46 await createTask.mutateAsync(data);
47 }
48 onSuccess?.();
49 } catch (error) {
50 // Error handling is done in the mutation hooks
51 }
52 };
53
54 const isLoading = createTask.isPending || updateTask.isPending;
55
56 return (
57 <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
58 <div className="space-y-4">
59 <div>
60 <label htmlFor="title" className="block text-sm font-medium mb-1">
61 Task Title *
62 </label>
63 <Input
64 id="title"
65 {...form.register('title')}
66 placeholder="Enter task title"
67 error={form.formState.errors.title?.message}
68 />
69 </div>
70
71 <div>
72 <label htmlFor="description" className="block text-sm font-medium mb-1">
73 Description
74 </label>
75 <Textarea
76 id="description"
77 {...form.register('description')}
78 placeholder="Enter task description"
79 rows={3}
80 error={form.formState.errors.description?.message}
81 />
82 </div>
83
84 <div className="grid grid-cols-2 gap-4">
85 <div>
86 <label htmlFor="priority" className="block text-sm font-medium mb-1">
87 Priority *
88 </label>
89 <Select
90 {...form.register('priority')}
91 error={form.formState.errors.priority?.message}
92 >
93 <option value={Priority.LOW}>Low</option>
94 <option value={Priority.MEDIUM}>Medium</option>
95 <option value={Priority.HIGH}>High</option>
96 <option value={Priority.URGENT}>Urgent</option>
97 </Select>
98 </div>
99
100 <div>
101 <label htmlFor="dueDate" className="block text-sm font-medium mb-1">
102 Due Date
103 </label>
104 <DatePicker
105 {...form.register('dueDate')}
106 error={form.formState.errors.dueDate?.message}
107 />
108 </div>
109 </div>
110
111 <div className="grid grid-cols-2 gap-4">
112 <div>
113 <label htmlFor="projectId" className="block text-sm font-medium mb-1">
114 Project *
115 </label>
116 <Select
117 {...form.register('projectId')}
118 error={form.formState.errors.projectId?.message}
119 >
120 <option value="">Select project</option>
121 {projects?.map((project) => (
122 <option key={project.id} value={project.id}>
123 {project.name}
124 </option>
125 ))}
126 </Select>
127 </div>
128
129 <div>
130 <label htmlFor="assigneeId" className="block text-sm font-medium mb-1">
131 Assignee
132 </label>
133 <Select
134 {...form.register('assigneeId')}
135 error={form.formState.errors.assigneeId?.message}
136 >
137 <option value="">Unassigned</option>
138 {users?.map((user) => (
139 <option key={user.id} value={user.id}>
140 {user.name}
141 </option>
142 ))}
143 </Select>
144 </div>
145 </div>
146 </div>
147
148 <div className="flex justify-end gap-3">
149 <Button
150 type="button"
151 variant="outline"
152 onClick={onCancel}
153 disabled={isLoading}
154 >
155 Cancel
156 </Button>
157 <Button type="submit" isLoading={isLoading}>
158 {isEditing ? 'Update Task' : 'Create Task'}
159 </Button>
160 </div>
161 </form>
162 );
163}Create a sophisticated task list (src/components/features/tasks/task-list.tsx):
1'use client';
2
3import { useState } from 'react';
4import { Task, TaskStatus, Priority } from '@/types/tasks';
5import { useTasks, useUpdateTask, useDeleteTask } from '@/hooks/use-tasks';
6import { Button } from '@/components/ui/button';
7import { Badge } from '@/components/ui/badge';
8import { DropdownMenu } from '@/components/ui/dropdown-menu';
9import { Input } from '@/components/ui/input';
10import { Select } from '@/components/ui/select';
11import { TaskCard } from './task-card';
12import { TaskForm } from './task-form';
13import { Modal } from '@/components/ui/modal';
14import { Plus, Search, Filter } from 'lucide-react';
15
16interface TaskListProps {
17 projectId?: string;
18}
19
20interface Filters {
21 search: string;
22 status: TaskStatus | 'all';
23 priority: Priority | 'all';
24 assignee: string | 'all';
25}
26
27export function TaskList({ projectId }: TaskListProps) {
28 const { data: tasks, isLoading, error } = useTasks(projectId);
29 const updateTask = useUpdateTask();
30 const deleteTask = useDeleteTask();
31
32 const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
33 const [editingTask, setEditingTask] = useState<Task | null>(null);
34 const [filters, setFilters] = useState<Filters>({
35 search: '',
36 status: 'all',
37 priority: 'all',
38 assignee: 'all',
39 });
40
41 // Filter tasks based on current filters
42 const filteredTasks = tasks?.filter((task) => {
43 const matchesSearch = task.title.toLowerCase().includes(filters.search.toLowerCase()) ||
44 task.description?.toLowerCase().includes(filters.search.toLowerCase());
45
46 const matchesStatus = filters.status === 'all' || task.status === filters.status;
47 const matchesPriority = filters.priority === 'all' || task.priority === filters.priority;
48 const matchesAssignee = filters.assignee === 'all' || task.assigneeId === filters.assignee;
49
50 return matchesSearch && matchesStatus && matchesPriority && matchesAssignee;
51 }) || [];
52
53 // Group tasks by status for kanban view
54 const tasksByStatus = {
55 [TaskStatus.TODO]: filteredTasks.filter(task => task.status === TaskStatus.TODO),
56 [TaskStatus.IN_PROGRESS]: filteredTasks.filter(task => task.status === TaskStatus.IN_PROGRESS),
57 [TaskStatus.IN_REVIEW]: filteredTasks.filter(task => task.status === TaskStatus.IN_REVIEW),
58 [TaskStatus.COMPLETED]: filteredTasks.filter(task => task.status === TaskStatus.COMPLETED),
59 };
60
61 const handleStatusChange = async (taskId: string, status: TaskStatus) => {
62 await updateTask.mutateAsync({ id: taskId, data: { status } });
63 };
64
65 const handleDeleteTask = async (taskId: string) => {
66 if (confirm('Are you sure you want to delete this task?')) {
67 await deleteTask.mutateAsync(taskId);
68 }
69 };
70
71 if (isLoading) {
72 return <TaskListSkeleton />;
73 }
74
75 if (error) {
76 return (
77 <div className="text-center py-8">
78 <p className="text-red-600">Failed to load tasks</p>
79 <Button onClick={() => window.location.reload()} className="mt-4">
80 Retry
81 </Button>
82 </div>
83 );
84 }
85
86 return (
87 <div className="space-y-6">
88 {/* Header */}
89 <div className="flex items-center justify-between">
90 <h1 className="text-2xl font-bold">Tasks</h1>
91 <Button onClick={() => setIsCreateModalOpen(true)}>
92 <Plus className="h-4 w-4 mr-2" />
93 New Task
94 </Button>
95 </div>
96
97 {/* Filters */}
98 <div className="flex items-center gap-4 p-4 bg-gray-50 rounded-lg">
99 <div className="flex-1">
100 <div className="relative">
101 <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
102 <Input
103 placeholder="Search tasks..."
104 value={filters.search}
105 onChange={(e) => setFilters({ ...filters, search: e.target.value })}
106 className="pl-10"
107 />
108 </div>
109 </div>
110
111 <Select
112 value={filters.status}
113 onChange={(value) => setFilters({ ...filters, status: value as TaskStatus | 'all' })}
114 >
115 <option value="all">All Status</option>
116 <option value={TaskStatus.TODO}>To Do</option>
117 <option value={TaskStatus.IN_PROGRESS}>In Progress</option>
118 <option value={TaskStatus.IN_REVIEW}>In Review</option>
119 <option value={TaskStatus.COMPLETED}>Completed</option>
120 </Select>
121
122 <Select
123 value={filters.priority}
124 onChange={(value) => setFilters({ ...filters, priority: value as Priority | 'all' })}
125 >
126 <option value="all">All Priorities</option>
127 <option value={Priority.LOW}>Low</option>
128 <option value={Priority.MEDIUM}>Medium</option>
129 <option value={Priority.HIGH}>High</option>
130 <option value={Priority.URGENT}>Urgent</option>
131 </Select>
132 </div>
133
134 {/* Kanban Board */}
135 <div className="grid grid-cols-4 gap-6">
136 {Object.entries(tasksByStatus).map(([status, statusTasks]) => (
137 <div key={status} className="space-y-3">
138 <div className="flex items-center justify-between">
139 <h3 className="font-semibold text-sm uppercase tracking-wide text-gray-600">
140 {status.replace('_', ' ')}
141 </h3>
142 <Badge variant="secondary">
143 {statusTasks.length}
144 </Badge>
145 </div>
146
147 <div className="space-y-3">
148 {statusTasks.map((task) => (
149 <TaskCard
150 key={task.id}
151 task={task}
152 onEdit={() => setEditingTask(task)}
153 onDelete={() => handleDeleteTask(task.id)}
154 onStatusChange={(newStatus) => handleStatusChange(task.id, newStatus)}
155 />
156 ))}
157 </div>
158 </div>
159 ))}
160 </div>
161
162 {/* Create Task Modal */}
163 <Modal
164 isOpen={isCreateModalOpen}
165 onClose={() => setIsCreateModalOpen(false)}
166 title="Create New Task"
167 >
168 <TaskForm
169 projectId={projectId}
170 onSuccess={() => setIsCreateModalOpen(false)}
171 onCancel={() => setIsCreateModalOpen(false)}
172 />
173 </Modal>
174
175 {/* Edit Task Modal */}
176 <Modal
177 isOpen={!!editingTask}
178 onClose={() => setEditingTask(null)}
179 title="Edit Task"
180 >
181 {editingTask && (
182 <TaskForm
183 task={editingTask}
184 onSuccess={() => setEditingTask(null)}
185 onCancel={() => setEditingTask(null)}
186 />
187 )}
188 </Modal>
189 </div>
190 );
191}
192
193function TaskListSkeleton() {
194 return (
195 <div className="grid grid-cols-4 gap-6">
196 {Array.from({ length: 4 }).map((_, i) => (
197 <div key={i} className="space-y-3">
198 <div className="h-6 bg-gray-200 rounded animate-pulse" />
199 {Array.from({ length: 3 }).map((_, j) => (
200 <div key={j} className="h-32 bg-gray-100 rounded animate-pulse" />
201 ))}
202 </div>
203 ))}
204 </div>
205 );
206}This implementation demonstrates enterprise-grade React patterns including proper error handling, optimistic updates, real-time synchronization, and comprehensive user experience.
Testing is crucial for production applications. Let's implement a complete testing suite for TaskMaster Pro.
Create jest.config.js:
1const nextJest = require('next/jest');
2
3const createJestConfig = nextJest({
4 // Provide the path to your Next.js app to load next.config.js and .env files
5 dir: './',
6});
7
8// Add any custom config to be passed to Jest
9const customJestConfig = {
10 setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
11 moduleNameMapping: {
12 // Handle module aliases
13 '^@/components/(.*)$': '<rootDir>/src/components/$1',
14 '^@/lib/(.*)$': '<rootDir>/src/lib/$1',
15 '^@/hooks/(.*)$': '<rootDir>/src/hooks/$1',
16 '^@/types/(.*)$': '<rootDir>/src/types/$1',
17 },
18 testEnvironment: 'jest-environment-jsdom',
19 collectCoverageFrom: [
20 'src/**/*.{js,jsx,ts,tsx}',
21 '!src/**/*.d.ts',
22 '!src/**/*.stories.{js,jsx,ts,tsx}',
23 '!src/**/index.{js,jsx,ts,tsx}',
24 ],
25 coverageThreshold: {
26 global: {
27 branches: 80,
28 functions: 80,
29 lines: 80,
30 statements: 80,
31 },
32 },
33 testMatch: [
34 '<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
35 '<rootDir>/src/**/*.{test,spec}.{js,jsx,ts,tsx}',
36 ],
37};
38
39// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
40module.exports = createJestConfig(customJestConfig);Create jest.setup.js:
1import '@testing-library/jest-dom';
2import { server } from './src/__mocks__/server';
3
4// Mock Next.js router
5jest.mock('next/navigation', () => ({
6 useRouter() {
7 return {
8 push: jest.fn(),
9 replace: jest.fn(),
10 refresh: jest.fn(),
11 back: jest.fn(),
12 forward: jest.fn(),
13 prefetch: jest.fn(),
14 };
15 },
16 useSearchParams() {
17 return new URLSearchParams();
18 },
19 usePathname() {
20 return '/';
21 },
22}));
23
24// Mock environment variables
25process.env = {
26 ...process.env,
27 AUTH0_SECRET: 'test-secret',
28 AUTH0_BASE_URL: 'http://localhost:3000',
29 AUTH0_ISSUER_BASE_URL: 'https://test.auth0.com',
30 AUTH0_CLIENT_ID: 'test-client-id',
31 AUTH0_CLIENT_SECRET: 'test-client-secret',
32};
33
34// Setup MSW
35beforeAll(() => server.listen());
36afterEach(() => server.resetHandlers());
37afterAll(() => server.close());
38
39// Mock IntersectionObserver
40global.IntersectionObserver = jest.fn().mockImplementation(() => ({
41 observe: jest.fn(),
42 unobserve: jest.fn(),
43 disconnect: jest.fn(),
44}));
45
46// Mock ResizeObserver
47global.ResizeObserver = jest.fn().mockImplementation(() => ({
48 observe: jest.fn(),
49 unobserve: jest.fn(),
50 disconnect: jest.fn(),
51}));Create API mocks (src/__mocks__/server.ts):
1import { setupServer } from 'msw/node';
2import { http, HttpResponse } from 'msw';
3import { Task, TaskStatus, Priority } from '@/types/tasks';
4
5const mockTasks: Task[] = [
6 {
7 id: '1',
8 title: 'Setup project',
9 description: 'Initialize the TaskMaster Pro project',
10 status: TaskStatus.COMPLETED,
11 priority: Priority.HIGH,
12 dueDate: new Date('2024-01-15'),
13 createdAt: new Date('2024-01-01'),
14 updatedAt: new Date('2024-01-10'),
15 projectId: 'project-1',
16 createdById: 'user-1',
17 assigneeId: 'user-1',
18 project: {
19 id: 'project-1',
20 name: 'TaskMaster Pro',
21 description: 'Task management application',
22 color: '#3B82F6',
23 status: 'ACTIVE',
24 createdAt: new Date('2024-01-01'),
25 updatedAt: new Date('2024-01-01'),
26 },
27 createdBy: {
28 id: 'user-1',
29 email: '[email protected]',
30 name: 'John Doe',
31 role: 'ADMIN',
32 createdAt: new Date('2024-01-01'),
33 updatedAt: new Date('2024-01-01'),
34 },
35 assignee: {
36 id: 'user-1',
37 email: '[email protected]',
38 name: 'John Doe',
39 role: 'ADMIN',
40 createdAt: new Date('2024-01-01'),
41 updatedAt: new Date('2024-01-01'),
42 },
43 comments: [],
44 attachments: [],
45 },
46];
47
48export const handlers = [
49 // Get tasks
50 http.get('/api/tasks', ({ request }) => {
51 const url = new URL(request.url);
52 const projectId = url.searchParams.get('projectId');
53
54 let filteredTasks = mockTasks;
55 if (projectId) {
56 filteredTasks = mockTasks.filter(task => task.projectId === projectId);
57 }
58
59 return HttpResponse.json(filteredTasks);
60 }),
61
62 // Get single task
63 http.get('/api/tasks/:id', ({ params }) => {
64 const task = mockTasks.find(t => t.id === params.id);
65 if (!task) {
66 return new HttpResponse(null, { status: 404 });
67 }
68 return HttpResponse.json(task);
69 }),
70
71 // Create task
72 http.post('/api/tasks', async ({ request }) => {
73 const data = await request.json() as any;
74 const newTask: Task = {
75 id: String(Date.now()),
76 ...data,
77 status: TaskStatus.TODO,
78 createdAt: new Date(),
79 updatedAt: new Date(),
80 project: mockTasks[0].project,
81 createdBy: mockTasks[0].createdBy,
82 assignee: data.assigneeId ? mockTasks[0].assignee : undefined,
83 comments: [],
84 attachments: [],
85 };
86 mockTasks.push(newTask);
87 return HttpResponse.json(newTask, { status: 201 });
88 }),
89
90 // Update task
91 http.patch('/api/tasks/:id', async ({ params, request }) => {
92 const data = await request.json() as any;
93 const taskIndex = mockTasks.findIndex(t => t.id === params.id);
94
95 if (taskIndex === -1) {
96 return new HttpResponse(null, { status: 404 });
97 }
98
99 mockTasks[taskIndex] = {
100 ...mockTasks[taskIndex],
101 ...data,
102 updatedAt: new Date(),
103 };
104
105 return HttpResponse.json(mockTasks[taskIndex]);
106 }),
107
108 // Delete task
109 http.delete('/api/tasks/:id', ({ params }) => {
110 const taskIndex = mockTasks.findIndex(t => t.id === params.id);
111
112 if (taskIndex === -1) {
113 return new HttpResponse(null, { status: 404 });
114 }
115
116 mockTasks.splice(taskIndex, 1);
117 return new HttpResponse(null, { status: 204 });
118 }),
119];
120
121export const server = setupServer(...handlers);Create comprehensive component tests (src/components/features/tasks/__tests__/task-form.test.tsx):
1import { render, screen, fireEvent, waitFor } from '@testing-library/react';
2import userEvent from '@testing-library/user-event';
3import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4import { TaskForm } from '../task-form';
5import { Priority } from '@/types/tasks';
6
7// Test wrapper with required providers
8function TestWrapper({ children }: { children: React.ReactNode }) {
9 const queryClient = new QueryClient({
10 defaultOptions: {
11 queries: { retry: false },
12 mutations: { retry: false },
13 },
14 });
15
16 return (
17 <QueryClientProvider client={queryClient}>
18 {children}
19 </QueryClientProvider>
20 );
21}
22
23const mockProjects = [
24 { id: 'project-1', name: 'Test Project' },
25 { id: 'project-2', name: 'Another Project' },
26];
27
28const mockUsers = [
29 { id: 'user-1', name: 'John Doe', email: '[email protected]' },
30 { id: 'user-2', name: 'Jane Smith', email: '[email protected]' },
31];
32
33// Mock the hooks
34jest.mock('@/hooks/use-projects', () => ({
35 useProjects: () => ({ data: mockProjects }),
36}));
37
38jest.mock('@/hooks/use-users', () => ({
39 useUsers: () => ({ data: mockUsers }),
40}));
41
42jest.mock('@/hooks/use-tasks', () => ({
43 useCreateTask: () => ({
44 mutateAsync: jest.fn(),
45 isPending: false,
46 }),
47 useUpdateTask: () => ({
48 mutateAsync: jest.fn(),
49 isPending: false,
50 }),
51}));
52
53describe('TaskForm', () => {
54 const user = userEvent.setup();
55
56 it('renders create task form with all fields', () => {
57 render(
58 <TestWrapper>
59 <TaskForm />
60 </TestWrapper>
61 );
62
63 expect(screen.getByLabelText(/task title/i)).toBeInTheDocument();
64 expect(screen.getByLabelText(/description/i)).toBeInTheDocument();
65 expect(screen.getByLabelText(/priority/i)).toBeInTheDocument();
66 expect(screen.getByLabelText(/due date/i)).toBeInTheDocument();
67 expect(screen.getByLabelText(/project/i)).toBeInTheDocument();
68 expect(screen.getByLabelText(/assignee/i)).toBeInTheDocument();
69 expect(screen.getByRole('button', { name: /create task/i })).toBeInTheDocument();
70 });
71
72 it('validates required fields', async () => {
73 const onSuccess = jest.fn();
74
75 render(
76 <TestWrapper>
77 <TaskForm onSuccess={onSuccess} />
78 </TestWrapper>
79 );
80
81 const submitButton = screen.getByRole('button', { name: /create task/i });
82 await user.click(submitButton);
83
84 await waitFor(() => {
85 expect(screen.getByText(/title is required/i)).toBeInTheDocument();
86 expect(screen.getByText(/project is required/i)).toBeInTheDocument();
87 });
88
89 expect(onSuccess).not.toHaveBeenCalled();
90 });
91
92 it('submits form with valid data', async () => {
93 const onSuccess = jest.fn();
94 const { useCreateTask } = require('@/hooks/use-tasks');
95 const mockMutateAsync = jest.fn().mockResolvedValue({});
96 useCreateTask.mockReturnValue({
97 mutateAsync: mockMutateAsync,
98 isPending: false,
99 });
100
101 render(
102 <TestWrapper>
103 <TaskForm onSuccess={onSuccess} />
104 </TestWrapper>
105 );
106
107 // Fill out the form
108 await user.type(screen.getByLabelText(/task title/i), 'Test Task');
109 await user.type(screen.getByLabelText(/description/i), 'Test Description');
110 await user.selectOptions(screen.getByLabelText(/priority/i), Priority.HIGH);
111 await user.selectOptions(screen.getByLabelText(/project/i), 'project-1');
112 await user.selectOptions(screen.getByLabelText(/assignee/i), 'user-1');
113
114 // Submit the form
115 await user.click(screen.getByRole('button', { name: /create task/i }));
116
117 await waitFor(() => {
118 expect(mockMutateAsync).toHaveBeenCalledWith({
119 title: 'Test Task',
120 description: 'Test Description',
121 priority: Priority.HIGH,
122 projectId: 'project-1',
123 assigneeId: 'user-1',
124 dueDate: undefined,
125 });
126 });
127
128 expect(onSuccess).toHaveBeenCalled();
129 });
130
131 it('renders edit form with pre-filled data', () => {
132 const mockTask = {
133 id: '1',
134 title: 'Existing Task',
135 description: 'Existing Description',
136 priority: Priority.HIGH,
137 projectId: 'project-1',
138 assigneeId: 'user-1',
139 status: 'TODO',
140 createdAt: new Date(),
141 updatedAt: new Date(),
142 createdById: 'user-1',
143 };
144
145 render(
146 <TestWrapper>
147 <TaskForm task={mockTask} />
148 </TestWrapper>
149 );
150
151 expect(screen.getByDisplayValue('Existing Task')).toBeInTheDocument();
152 expect(screen.getByDisplayValue('Existing Description')).toBeInTheDocument();
153 expect(screen.getByRole('button', { name: /update task/i })).toBeInTheDocument();
154 });
155
156 it('handles form cancellation', async () => {
157 const onCancel = jest.fn();
158
159 render(
160 <TestWrapper>
161 <TaskForm onCancel={onCancel} />
162 </TestWrapper>
163 );
164
165 await user.click(screen.getByRole('button', { name: /cancel/i }));
166 expect(onCancel).toHaveBeenCalled();
167 });
168
169 it('shows loading state during submission', () => {
170 const { useCreateTask } = require('@/hooks/use-tasks');
171 useCreateTask.mockReturnValue({
172 mutateAsync: jest.fn(),
173 isPending: true,
174 });
175
176 render(
177 <TestWrapper>
178 <TaskForm />
179 </TestWrapper>
180 );
181
182 const submitButton = screen.getByRole('button', { name: /create task/i });
183 expect(submitButton).toBeDisabled();
184 });
185});Create integration tests (src/__tests__/integration/task-management.test.tsx):
1import { render, screen, waitFor } from '@testing-library/react';
2import userEvent from '@testing-library/user-event';
3import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4import { TaskList } from '@/components/features/tasks/task-list';
5
6function TestWrapper({ children }: { children: React.ReactNode }) {
7 const queryClient = new QueryClient({
8 defaultOptions: {
9 queries: { retry: false, staleTime: 0 },
10 mutations: { retry: false },
11 },
12 });
13
14 return (
15 <QueryClientProvider client={queryClient}>
16 {children}
17 </QueryClientProvider>
18 );
19}
20
21describe('Task Management Integration', () => {
22 const user = userEvent.setup();
23
24 beforeEach(() => {
25 // Reset the mock tasks for each test
26 jest.clearAllMocks();
27 });
28
29 it('loads and displays tasks', async () => {
30 render(
31 <TestWrapper>
32 <TaskList />
33 </TestWrapper>
34 );
35
36 // Wait for tasks to load
37 await waitFor(() => {
38 expect(screen.getByText('Setup project')).toBeInTheDocument();
39 });
40
41 // Check that the task appears in the correct column
42 expect(screen.getByText('COMPLETED')).toBeInTheDocument();
43 });
44
45 it('creates a new task through the full flow', async () => {
46 render(
47 <TestWrapper>
48 <TaskList />
49 </TestWrapper>
50 );
51
52 // Wait for initial load
53 await waitFor(() => {
54 expect(screen.getByRole('button', { name: /new task/i })).toBeInTheDocument();
55 });
56
57 // Open create task modal
58 await user.click(screen.getByRole('button', { name: /new task/i }));
59
60 // Wait for modal to open
61 await waitFor(() => {
62 expect(screen.getByText('Create New Task')).toBeInTheDocument();
63 });
64
65 // Fill out the form
66 await user.type(screen.getByLabelText(/task title/i), 'Integration Test Task');
67 await user.type(screen.getByLabelText(/description/i), 'Created during integration test');
68 await user.selectOptions(screen.getByLabelText(/priority/i), 'HIGH');
69
70 // Submit the form
71 await user.click(screen.getByRole('button', { name: /create task/i }));
72
73 // Wait for the modal to close and task to appear
74 await waitFor(() => {
75 expect(screen.getByText('Integration Test Task')).toBeInTheDocument();
76 });
77
78 // Verify the task appears in the TODO column
79 const todoColumn = screen.getByText('TODO').closest('div');
80 expect(todoColumn).toContainElement(screen.getByText('Integration Test Task'));
81 });
82
83 it('filters tasks by search term', async () => {
84 render(
85 <TestWrapper>
86 <TaskList />
87 </TestWrapper>
88 );
89
90 // Wait for tasks to load
91 await waitFor(() => {
92 expect(screen.getByText('Setup project')).toBeInTheDocument();
93 });
94
95 // Search for a specific task
96 const searchInput = screen.getByPlaceholderText('Search tasks...');
97 await user.type(searchInput, 'Setup');
98
99 // Verify filtering works
100 expect(screen.getByText('Setup project')).toBeInTheDocument();
101 });
102
103 it('updates task status through drag and drop simulation', async () => {
104 render(
105 <TestWrapper>
106 <TaskList />
107 </TestWrapper>
108 );
109
110 // Wait for tasks to load
111 await waitFor(() => {
112 expect(screen.getByText('Setup project')).toBeInTheDocument();
113 });
114
115 // Find a task card and its status dropdown
116 const taskCard = screen.getByText('Setup project').closest('[data-testid="task-card"]');
117 expect(taskCard).toBeInTheDocument();
118
119 // This would typically involve more complex drag-and-drop testing
120 // For now, we'll test the status update through the dropdown
121 const statusDropdown = screen.getByRole('button', { name: /change status/i });
122 await user.click(statusDropdown);
123
124 await user.click(screen.getByText('In Progress'));
125
126 // Verify the task moved to the correct column
127 await waitFor(() => {
128 const inProgressColumn = screen.getByText('IN PROGRESS').closest('div');
129 expect(inProgressColumn).toContainElement(screen.getByText('Setup project'));
130 });
131 });
132});Create Playwright configuration (playwright.config.ts):
1import { defineConfig, devices } from '@playwright/test';
2
3export default defineConfig({
4 testDir: './e2e',
5 fullyParallel: true,
6 forbidOnly: !!process.env.CI,
7 retries: process.env.CI ? 2 : 0,
8 workers: process.env.CI ? 1 : undefined,
9 reporter: 'html',
10 use: {
11 baseURL: 'http://localhost:3000',
12 trace: 'on-first-retry',
13 screenshot: 'only-on-failure',
14 },
15
16 projects: [
17 {
18 name: 'chromium',
19 use: { ...devices['Desktop Chrome'] },
20 },
21 {
22 name: 'firefox',
23 use: { ...devices['Desktop Firefox'] },
24 },
25 {
26 name: 'webkit',
27 use: { ...devices['Desktop Safari'] },
28 },
29 {
30 name: 'Mobile Chrome',
31 use: { ...devices['Pixel 5'] },
32 },
33 ],
34
35 webServer: {
36 command: 'npm run dev',
37 url: 'http://localhost:3000',
38 reuseExistingServer: !process.env.CI,
39 },
40});Create E2E test (e2e/task-management.spec.ts):
1import { test, expect } from '@playwright/test';
2
3test.describe('Task Management', () => {
4 test.beforeEach(async ({ page }) => {
5 // Navigate to the application
6 await page.goto('/dashboard');
7
8 // Wait for the page to load
9 await page.waitForLoadState('networkidle');
10 });
11
12 test('creates a new task', async ({ page }) => {
13 // Click the "New Task" button
14 await page.click('text=New Task');
15
16 // Wait for modal to appear
17 await expect(page.locator('text=Create New Task')).toBeVisible();
18
19 // Fill out the form
20 await page.fill('[data-testid="task-title"]', 'E2E Test Task');
21 await page.fill('[data-testid="task-description"]', 'Created by Playwright');
22 await page.selectOption('[data-testid="task-priority"]', 'HIGH');
23 await page.selectOption('[data-testid="task-project"]', { index: 1 });
24
25 // Submit the form
26 await page.click('text=Create Task');
27
28 // Verify the task appears
29 await expect(page.locator('text=E2E Test Task')).toBeVisible();
30 });
31
32 test('updates task status', async ({ page }) => {
33 // Assume we have a task visible
34 await expect(page.locator('[data-testid="task-card"]').first()).toBeVisible();
35
36 // Click on the task to open details
37 await page.click('[data-testid="task-card"]');
38
39 // Change status
40 await page.selectOption('[data-testid="task-status"]', 'IN_PROGRESS');
41
42 // Save changes
43 await page.click('text=Save');
44
45 // Verify the task moved to the correct column
46 const inProgressColumn = page.locator('[data-testid="status-column-IN_PROGRESS"]');
47 await expect(inProgressColumn.locator('[data-testid="task-card"]')).toHaveCount(1);
48 });
49
50 test('searches and filters tasks', async ({ page }) => {
51 // Use the search functionality
52 await page.fill('[data-testid="task-search"]', 'test');
53
54 // Verify search results
55 await expect(page.locator('[data-testid="task-card"]')).toHaveCount(1);
56
57 // Clear search
58 await page.fill('[data-testid="task-search"]', '');
59
60 // Filter by status
61 await page.selectOption('[data-testid="status-filter"]', 'TODO');
62
63 // Verify filter results
64 const todoTasks = page.locator('[data-testid="status-column-TODO"] [data-testid="task-card"]');
65 await expect(todoTasks).toHaveCountGreaterThan(0);
66 });
67
68 test('deletes a task', async ({ page }) => {
69 // Click on a task card menu
70 await page.click('[data-testid="task-menu"]');
71
72 // Click delete option
73 await page.click('text=Delete');
74
75 // Confirm deletion
76 await page.click('text=Confirm');
77
78 // Verify task is removed
79 await expect(page.locator('text=Task deleted successfully')).toBeVisible();
80 });
81});Update package.json with comprehensive test scripts:
1{
2 "scripts": {
3 "test": "jest",
4 "test:watch": "jest --watch",
5 "test:coverage": "jest --coverage",
6 "test:e2e": "playwright test",
7 "test:e2e:ui": "playwright test --ui",
8 "test:all": "npm run test && npm run test:e2e",
9 "test:ci": "jest --ci --coverage --watchAll=false && playwright test --reporter=github"
10 }
11}This comprehensive testing setup ensures your production application is thoroughly tested across all layers!
The final phase involves deploying TaskMaster Pro with a complete CI/CD pipeline and production monitoring.
Create production environment files:
**.env.production**:
1# Production configuration (no secrets)
2NODE_ENV=production
3NEXT_PUBLIC_APP_URL=https://taskmaster-pro.vercel.app
4NEXT_PUBLIC_APP_NAME="TaskMaster Pro"
5
6# Feature flags
7NEXT_PUBLIC_FEATURE_ANALYTICS=true
8NEXT_PUBLIC_FEATURE_NOTIFICATIONS=true
9NEXT_PUBLIC_FEATURE_REAL_TIME=true
10
11# Performance
12ENABLE_BUNDLE_ANALYZER=false
13ENABLE_SOURCE_MAPS=falseCreate .github/workflows/ci.yml:
1name: CI/CD Pipeline
2
3on:
4 push:
5 branches: [main, develop]
6 pull_request:
7 branches: [main]
8
9env:
10 NODE_VERSION: '18'
11 POSTGRES_URL: postgresql://postgres:postgres@localhost:5432/taskmaster_test
12
13jobs:
14 # Code Quality & Security
15 quality:
16 name: Code Quality & Security
17 runs-on: ubuntu-latest
18 steps:
19 - name: Checkout code
20 uses: actions/checkout@v4
21
22 - name: Setup Node.js
23 uses: actions/setup-node@v3
24 with:
25 node-version: ${{ env.NODE_VERSION }}
26 cache: 'npm'
27
28 - name: Install dependencies
29 run: npm ci
30
31 - name: Type checking
32 run: npm run type-check
33
34 - name: Lint code
35 run: npm run lint
36
37 - name: Check formatting
38 run: npm run prettier
39
40 - name: Security audit
41 run: npm audit --audit-level high
42
43 - name: Check for vulnerabilities
44 run: npx audit-ci --config audit-ci.json
45
46 # Unit & Integration Tests
47 test:
48 name: Unit & Integration Tests
49 runs-on: ubuntu-latest
50 services:
51 postgres:
52 image: postgres:15
53 env:
54 POSTGRES_PASSWORD: postgres
55 POSTGRES_DB: taskmaster_test
56 options: >-
57 --health-cmd pg_isready
58 --health-interval 10s
59 --health-timeout 5s
60 --health-retries 5
61 ports:
62 - 5432:5432
63
64 steps:
65 - name: Checkout code
66 uses: actions/checkout@v4
67
68 - name: Setup Node.js
69 uses: actions/setup-node@v3
70 with:
71 node-version: ${{ env.NODE_VERSION }}
72 cache: 'npm'
73
74 - name: Install dependencies
75 run: npm ci
76
77 - name: Setup test database
78 run: |
79 npx prisma generate
80 npx prisma db push
81 env:
82 DATABASE_URL: ${{ env.POSTGRES_URL }}
83
84 - name: Run unit tests
85 run: npm run test:coverage
86 env:
87 DATABASE_URL: ${{ env.POSTGRES_URL }}
88
89 - name: Upload coverage to Codecov
90 uses: codecov/codecov-action@v3
91 with:
92 token: ${{ secrets.CODECOV_TOKEN }}
93 files: ./coverage/lcov.info
94
95 # E2E Tests
96 e2e:
97 name: E2E Tests
98 runs-on: ubuntu-latest
99 services:
100 postgres:
101 image: postgres:15
102 env:
103 POSTGRES_PASSWORD: postgres
104 POSTGRES_DB: taskmaster_test
105 options: >-
106 --health-cmd pg_isready
107 --health-interval 10s
108 --health-timeout 5s
109 --health-retries 5
110 ports:
111 - 5432:5432
112
113 steps:
114 - name: Checkout code
115 uses: actions/checkout@v4
116
117 - name: Setup Node.js
118 uses: actions/setup-node@v3
119 with:
120 node-version: ${{ env.NODE_VERSION }}
121 cache: 'npm'
122
123 - name: Install dependencies
124 run: npm ci
125
126 - name: Install Playwright browsers
127 run: npx playwright install --with-deps
128
129 - name: Setup test database
130 run: |
131 npx prisma generate
132 npx prisma db push
133 npx prisma db seed
134 env:
135 DATABASE_URL: ${{ env.POSTGRES_URL }}
136
137 - name: Build application
138 run: npm run build
139 env:
140 DATABASE_URL: ${{ env.POSTGRES_URL }}
141 AUTH0_SECRET: ${{ secrets.AUTH0_SECRET }}
142 AUTH0_BASE_URL: http://localhost:3000
143 AUTH0_ISSUER_BASE_URL: ${{ secrets.AUTH0_ISSUER_BASE_URL }}
144 AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }}
145 AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }}
146
147 - name: Run E2E tests
148 run: npm run test:e2e
149 env:
150 DATABASE_URL: ${{ env.POSTGRES_URL }}
151
152 - name: Upload E2E results
153 uses: actions/upload-artifact@v3
154 if: failure()
155 with:
156 name: playwright-report
157 path: playwright-report/
158 retention-days: 30
159
160 # Performance Tests
161 performance:
162 name: Performance Tests
163 runs-on: ubuntu-latest
164 if: github.event_name == 'pull_request'
165 steps:
166 - name: Checkout code
167 uses: actions/checkout@v4
168
169 - name: Setup Node.js
170 uses: actions/setup-node@v3
171 with:
172 node-version: ${{ env.NODE_VERSION }}
173 cache: 'npm'
174
175 - name: Install dependencies
176 run: npm ci
177
178 - name: Build application
179 run: npm run build
180
181 - name: Run Lighthouse CI
182 run: |
183 npm install -g @lhci/[email protected]
184 lhci autorun
185 env:
186 LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
187
188 # Build & Deploy to Staging
189 deploy-staging:
190 name: Deploy to Staging
191 runs-on: ubuntu-latest
192 needs: [quality, test, e2e]
193 if: github.ref == 'refs/heads/develop'
194 environment: staging
195
196 steps:
197 - name: Checkout code
198 uses: actions/checkout@v4
199
200 - name: Deploy to Vercel (Staging)
201 uses: amondnet/vercel-action@v25
202 with:
203 vercel-token: ${{ secrets.VERCEL_TOKEN }}
204 vercel-args: '--env NODE_ENV=staging'
205 vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
206 vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
207 scope: ${{ secrets.VERCEL_ORG_ID }}
208
209 - name: Run smoke tests against staging
210 run: |
211 npx playwright test smoke.spec.ts
212 env:
213 BASE_URL: ${{ steps.deploy.outputs.preview-url }}
214
215 # Deploy to Production
216 deploy-production:
217 name: Deploy to Production
218 runs-on: ubuntu-latest
219 needs: [quality, test, e2e]
220 if: github.ref == 'refs/heads/main'
221 environment: production
222
223 steps:
224 - name: Checkout code
225 uses: actions/checkout@v4
226
227 - name: Deploy to Vercel (Production)
228 uses: amondnet/vercel-action@v25
229 with:
230 vercel-token: ${{ secrets.VERCEL_TOKEN }}
231 vercel-args: '--prod'
232 vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
233 vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
234 scope: ${{ secrets.VERCEL_ORG_ID }}
235
236 - name: Create Sentry release
237 uses: getsentry/action-release@v1
238 env:
239 SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
240 SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
241 SENTRY_PROJECT: taskmaster-pro
242 with:
243 environment: production
244 version: ${{ github.sha }}
245
246 - name: Post-deploy health check
247 run: |
248 curl -f https://taskmaster-pro.vercel.app/api/health || exit 1
249
250 - name: Notify team of deployment
251 uses: 8398a7/action-slack@v3
252 with:
253 status: ${{ job.status }}
254 channel: '#deployments'
255 webhook_url: ${{ secrets.SLACK_WEBHOOK }}
256 if: always()Create vercel.json:
1{
2 "framework": "nextjs",
3 "buildCommand": "npm run build",
4 "devCommand": "npm run dev",
5 "installCommand": "npm ci",
6 "env": {
7 "NODE_ENV": "production"
8 },
9 "build": {
10 "env": {
11 "NODE_ENV": "production"
12 }
13 },
14 "functions": {
15 "app/**/*.js": {
16 "memory": 1024,
17 "maxDuration": 30
18 }
19 },
20 "regions": ["iad1"],
21 "headers": [
22 {
23 "source": "/(.*)",
24 "headers": [
25 {
26 "key": "X-Frame-Options",
27 "value": "DENY"
28 },
29 {
30 "key": "X-Content-Type-Options",
31 "value": "nosniff"
32 },
33 {
34 "key": "Referrer-Policy",
35 "value": "origin-when-cross-origin"
36 },
37 {
38 "key": "Strict-Transport-Security",
39 "value": "max-age=31536000; includeSubDomains"
40 }
41 ]
42 },
43 {
44 "source": "/api/(.*)",
45 "headers": [
46 {
47 "key": "Cache-Control",
48 "value": "no-store, max-age=0"
49 }
50 ]
51 },
52 {
53 "source": "/_next/static/(.*)",
54 "headers": [
55 {
56 "key": "Cache-Control",
57 "value": "public, max-age=31536000, immutable"
58 }
59 ]
60 }
61 ],
62 "redirects": [
63 {
64 "source": "/dashboard",
65 "destination": "/dashboard/overview",
66 "permanent": false
67 }
68 ],
69 "rewrites": [
70 {
71 "source": "/api/webhooks/stripe",
72 "destination": "/api/webhooks/stripe"
73 }
74 ]
75}Create .lighthouserc.js:
1module.exports = {
2 ci: {
3 collect: {
4 url: [
5 'http://localhost:3000',
6 'http://localhost:3000/dashboard',
7 'http://localhost:3000/projects',
8 'http://localhost:3000/tasks',
9 ],
10 startServerCommand: 'npm run start',
11 startServerReadyPattern: 'ready on',
12 startServerReadyTimeout: 20000,
13 },
14 assert: {
15 assertions: {
16 'categories:performance': ['warn', { minScore: 0.9 }],
17 'categories:accessibility': ['error', { minScore: 0.95 }],
18 'categories:best-practices': ['warn', { minScore: 0.9 }],
19 'categories:seo': ['warn', { minScore: 0.9 }],
20 'categories:pwa': ['warn', { minScore: 0.8 }],
21 },
22 },
23 upload: {
24 target: 'temporary-public-storage',
25 },
26 },
27};Create monitoring configuration (src/lib/monitoring.ts):
1import * as Sentry from '@sentry/nextjs';
2import { PostHog } from 'posthog-js';
3
4// Sentry Configuration
5Sentry.init({
6 dsn: process.env.SENTRY_DSN,
7 environment: process.env.NODE_ENV,
8 tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
9 profilesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
10
11 beforeSend(event) {
12 // Filter out non-critical errors in production
13 if (process.env.NODE_ENV === 'production') {
14 if (event.exception) {
15 const error = event.exception.values?.[0];
16 if (error?.type === 'ChunkLoadError') {
17 return null; // Ignore chunk load errors
18 }
19 }
20 }
21 return event;
22 },
23
24 integrations: [
25 new Sentry.BrowserTracing({
26 routingInstrumentation: Sentry.nextRouterInstrumentation,
27 }),
28 ],
29});
30
31// PostHog Configuration
32if (typeof window !== 'undefined') {
33 PostHog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
34 api_host: 'https://app.posthog.com',
35 loaded: (posthog) => {
36 if (process.env.NODE_ENV === 'development') posthog.debug();
37 },
38 });
39}
40
41// Custom monitoring utilities
42export const monitoring = {
43 // Track performance metrics
44 trackPerformance: (metricName: string, value: number) => {
45 if (typeof window !== 'undefined') {
46 // Track with PostHog
47 PostHog.capture('performance_metric', {
48 metric: metricName,
49 value,
50 url: window.location.pathname,
51 });
52
53 // Track with Sentry
54 Sentry.addBreadcrumb({
55 category: 'performance',
56 message: `${metricName}: ${value}ms`,
57 level: 'info',
58 });
59 }
60 },
61
62 // Track user interactions
63 trackEvent: (eventName: string, properties?: Record<string, any>) => {
64 if (typeof window !== 'undefined') {
65 PostHog.capture(eventName, properties);
66 }
67 },
68
69 // Track errors
70 trackError: (error: Error, context?: Record<string, any>) => {
71 Sentry.captureException(error, {
72 tags: context,
73 });
74 },
75
76 // Track API performance
77 trackAPICall: (endpoint: string, duration: number, status: number) => {
78 const isError = status >= 400;
79
80 if (isError) {
81 Sentry.addBreadcrumb({
82 category: 'api',
83 message: `API Error: ${endpoint} (${status})`,
84 level: 'error',
85 data: { duration, status },
86 });
87 }
88
89 if (typeof window !== 'undefined') {
90 PostHog.capture('api_call', {
91 endpoint,
92 duration,
93 status,
94 success: !isError,
95 });
96 }
97 },
98};
99
100// Performance monitoring hook
101export function usePerformanceMonitoring() {
102 useEffect(() => {
103 // Monitor Core Web Vitals
104 const observer = new PerformanceObserver((list) => {
105 for (const entry of list.getEntries()) {
106 const metricName = entry.name;
107 const value = Math.round(entry.startTime);
108
109 monitoring.trackPerformance(metricName, value);
110 }
111 });
112
113 observer.observe({ entryTypes: ['measure', 'navigation'] });
114
115 // Monitor largest contentful paint
116 new PerformanceObserver((list) => {
117 const entries = list.getEntries();
118 const lastEntry = entries[entries.length - 1];
119 monitoring.trackPerformance('LCP', lastEntry.startTime);
120 }).observe({ entryTypes: ['largest-contentful-paint'] });
121
122 // Monitor first input delay
123 new PerformanceObserver((list) => {
124 for (const entry of list.getEntries()) {
125 monitoring.trackPerformance('FID', entry.processingStart - entry.startTime);
126 }
127 }).observe({ entryTypes: ['first-input'] });
128
129 return () => observer.disconnect();
130 }, []);
131}Create API health check (src/app/api/health/route.ts):
1import { NextResponse } from 'next/server';
2import { prisma } from '@/lib/prisma';
3
4export async function GET() {
5 const checks = {
6 timestamp: new Date().toISOString(),
7 status: 'healthy',
8 version: process.env.npm_package_version || '1.0.0',
9 environment: process.env.NODE_ENV,
10 checks: {
11 database: 'unknown',
12 redis: 'unknown',
13 external_apis: 'unknown',
14 },
15 };
16
17 try {
18 // Database health check
19 await prisma.$queryRaw`SELECT 1`;
20 checks.checks.database = 'healthy';
21 } catch (error) {
22 checks.checks.database = 'unhealthy';
23 checks.status = 'degraded';
24 }
25
26 // Redis health check (if using Redis)
27 try {
28 // Add Redis ping here if using Redis
29 checks.checks.redis = 'healthy';
30 } catch (error) {
31 checks.checks.redis = 'unhealthy';
32 checks.status = 'degraded';
33 }
34
35 // External API health checks
36 try {
37 // Check critical external services
38 const authResponse = await fetch(process.env.AUTH0_ISSUER_BASE_URL + '/.well-known/openid_configuration');
39 if (authResponse.ok) {
40 checks.checks.external_apis = 'healthy';
41 } else {
42 checks.checks.external_apis = 'degraded';
43 checks.status = 'degraded';
44 }
45 } catch (error) {
46 checks.checks.external_apis = 'unhealthy';
47 checks.status = 'degraded';
48 }
49
50 const statusCode = checks.status === 'healthy' ? 200 : 503;
51
52 return NextResponse.json(checks, { status: statusCode });
53}Add production deployment scripts to package.json:
1{
2 "scripts": {
3 "build:prod": "NODE_ENV=production next build",
4 "start:prod": "NODE_ENV=production next start",
5 "deploy:staging": "vercel --env NODE_ENV=staging",
6 "deploy:production": "vercel --prod",
7 "health-check": "curl -f $DEPLOYMENT_URL/api/health",
8 "db:migrate:prod": "NODE_ENV=production npx prisma migrate deploy",
9 "db:seed:prod": "NODE_ENV=production npx prisma db seed",
10 "monitor:setup": "npm run sentry:setup && npm run posthog:setup"
11 }
12}Before going live, ensure:
Your TaskMaster Pro application is now production-ready with enterprise-grade deployment and monitoring!
Your progress is saved in this browser, on this device only.