Weekend Project: Develop a Complete MERN Stack Application with Authentication

Applying George Polya's 4-Step Problem Solving Process to Full-Stack Development

Introduction

Throughout this week, we've explored the key components of MERN stack integration: connecting a React frontend to an Express backend, making API requests with Axios, and implementing authentication flows. Now it's time to put everything together in a complete project.

For this weekend project, we'll develop a Task Management Application with authentication, allowing users to register, log in, and manage their personal tasks. We'll approach this project using George Polya's 4-step problem-solving procedure, which provides a structured framework for tackling complex development challenges.

flowchart TD subgraph polya [George Polya's 4-Step Procedure] A[1. Understand the Problem] --> B[2. Devise a Plan] B --> C[3. Execute the Plan] C --> D[4. Look Back and Reflect] end subgraph mern [MERN Application Components] E[MongoDB & Models] --- F[Express & API Routes] F --- G[React & UI Components] G --- H[Node.js & Environment] end polya -.- mern style polya fill:#d4f0f0,stroke:#000 style mern fill:#ffe6cc,stroke:#000

Project Scope

Our Task Management Application will include the following features:

George Polya's 4-Step Problem Solving Procedure

Before diving into the implementation, let's understand the 4-step problem-solving procedure proposed by mathematician George Polya in his book "How to Solve It" (1945). This approach provides a structured way to tackle complex problems:

  1. Understand the Problem: Clearly define what you're trying to accomplish.
  2. Devise a Plan: Develop a strategy or algorithm to solve the problem.
  3. Execute the Plan: Implement your strategy meticulously.
  4. Look Back and Reflect: Review your solution, evaluate its effectiveness, and identify improvements.

We'll apply these steps to our MERN stack application development process, breaking down each phase into concrete tasks.

flowchart LR A[Understand the Problem] --> B[Devise a Plan] B --> C[Execute the Plan] C --> D[Look Back and Reflect] D -.--> A style A fill:#d4f0f0,stroke:#000 style B fill:#d4f0f0,stroke:#000 style C fill:#d4f0f0,stroke:#000 style D fill:#d4f0f0,stroke:#000

Real-world analogy: Polya's approach is similar to how architects design and construct buildings. They first understand client requirements, then create blueprints, next oversee construction, and finally evaluate the finished building against the original requirements.

Step 1: Understand the Problem

The first step is to thoroughly understand what we're trying to build. This involves defining requirements, identifying potential challenges, and setting clear goals for our application.

Key Questions to Ask

Project Requirements

User Authentication:

Task Management:

Technical Requirements:

Define The Data Models

Let's define the core data models for our application:

// User Model
const userSchema = {
  name: { type: String, required: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
}

// Task Model
const taskSchema = {
  title: { type: String, required: true },
  description: { type: String },
  status: { type: String, enum: ['pending', 'in-progress', 'completed'], default: 'pending' },
  priority: { type: String, enum: ['low', 'medium', 'high'], default: 'medium' },
  dueDate: { type: Date },
  user: { type: ObjectId, ref: 'User', required: true },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
}

Identify Potential Challenges

erDiagram USER { string _id string name string email string password date createdAt date updatedAt } TASK { string _id string title string description string status string priority date dueDate string user date createdAt date updatedAt } USER ||--o{ TASK : "creates"

Step 2: Devise a Plan

With a clear understanding of the problem, we can now devise a plan for building our application. This includes creating a project structure, defining the component hierarchy, and mapping out the development workflow.

Project Structure

task-management-app/
├── .env                   # Environment variables
├── .gitignore             # Git ignore file
├── package.json           # Project dependencies and scripts
├── README.md              # Project documentation
├── client/                # React frontend
│   ├── public/            # Static files
│   ├── src/               # Source code
│   │   ├── components/    # React components
│   │   ├── context/       # Context API providers
│   │   ├── hooks/         # Custom hooks
│   │   ├── pages/         # Page components
│   │   ├── services/      # API service functions
│   │   ├── utils/         # Utility functions
│   │   ├── App.js         # Main App component
│   │   └── index.js       # Entry point
│   └── package.json       # Frontend dependencies
└── server/                # Express backend
    ├── config/            # Configuration files
    ├── controllers/       # Request handlers
    ├── middleware/        # Custom middleware
    ├── models/            # Mongoose models
    ├── routes/            # API routes
    ├── utils/             # Utility functions
    ├── server.js          # Entry point
    └── package.json       # Backend dependencies

Component Hierarchy

Let's map out the key components of our React frontend:

graph TD A[App] --> B[AuthProvider Context] A --> C[TaskProvider Context] A --> D[Router] D --> E[PublicRoutes] D --> F[PrivateRoutes] E --> G[Login] E --> H[Register] E --> I[ForgotPassword] F --> J[Dashboard] F --> K[Profile] F --> L[TaskForm] F --> M[TaskDetail] J --> N[TaskList] N --> O[TaskItem] style A fill:#d4f0f0,stroke:#000 style B fill:#ffe6cc,stroke:#000 style C fill:#ffe6cc,stroke:#000 style D fill:#d4f0f0,stroke:#000

API Endpoints

Next, let's define the API endpoints our application will need:

Endpoint Method Description Authentication
/api/auth/register POST Register a new user Public
/api/auth/login POST Login user and get tokens Public
/api/auth/refresh-token POST Refresh access token Public (with refresh token)
/api/auth/me GET Get current user information Private
/api/auth/logout POST Logout user Private
/api/tasks GET Get all tasks for user Private
/api/tasks/:id GET Get a single task Private
/api/tasks POST Create a new task Private
/api/tasks/:id PUT Update a task Private
/api/tasks/:id DELETE Delete a task Private
/api/profile GET Get user profile Private
/api/profile PUT Update user profile Private

Authentication Flow

sequenceDiagram participant User participant React participant Auth Context participant Local Storage participant Express participant MongoDB User->>React: Enter credentials React->>Express: Login request Express->>MongoDB: Verify credentials MongoDB-->>Express: User data Express->>Express: Generate tokens Express-->>React: Return tokens & user data React->>Auth Context: Update auth state Auth Context->>Local Storage: Store tokens React-->>User: Redirect to dashboard Note over User,React: Later API requests React->>Local Storage: Get access token Local Storage-->>React: Return token React->>Express: API request with token Express->>Express: Verify token Express->>MongoDB: Fetch data MongoDB-->>Express: Return data Express-->>React: Return requested data React-->>User: Display data Note over User,React: Token expiration React->>Express: API request Express-->>React: 401 Unauthorized React->>Local Storage: Get refresh token Local Storage-->>React: Return refresh token React->>Express: Request new access token Express->>Express: Verify refresh token Express-->>React: Return new access token React->>Local Storage: Update access token React->>Express: Retry original request Express-->>React: Return requested data React-->>User: Display data

Development Workflow

Let's plan our development workflow, breaking it down into manageable steps:

  1. Setup Project Structure
    • Initialize Git repository
    • Create frontend and backend folders
    • Set up package.json files and install dependencies
  2. Backend Development
    • Create database models
    • Implement authentication system
    • Create RESTful API endpoints
    • Add validation and error handling
  3. Frontend Development
    • Set up React app with routing
    • Create authentication context and service
    • Build authentication components
    • Implement task management components
    • Add styling and responsive design
  4. Integration and Testing
    • Connect frontend to backend
    • Test all features and user flows
    • Fix bugs and improve UX
  5. Deployment
    • Prepare for production
    • Deploy to hosting platform

Step 3: Execute the Plan

Now that we have a solid plan, let's start implementing our application. We'll follow the workflow outlined above, focusing on key components and functionality.

Initial Setup

Project Initialization

# Create project directory
mkdir task-management-app
cd task-management-app

# Initialize Git repository
git init

# Create README and .gitignore
touch README.md
touch .gitignore

# Add node_modules and environment files to .gitignore
echo "node_modules\n.env\n.DS_Store" > .gitignore

# Initialize npm and install concurrently for running both servers
npm init -y
npm install concurrently nodemon --save-dev

# Update package.json with scripts
# Add the following to package.json:
# "scripts": {
#   "start": "node server/server.js",
#   "server": "nodemon server/server.js",
#   "client": "npm start --prefix client",
#   "dev": "concurrently \"npm run server\" \"npm run client\"",
#   "install-all": "npm install && cd client && npm install && cd ../server && npm install"
# }

# Create frontend with Create React App
npx create-react-app client

# Create server directory and initialize
mkdir server
cd server
npm init -y

Backend Dependencies

# Install backend dependencies
npm install express mongoose bcryptjs jsonwebtoken cors dotenv express-validator

# For development
npm install nodemon --save-dev

Frontend Dependencies

# Navigate to client directory
cd ../client

# Install frontend dependencies
npm install axios react-router-dom formik yup react-toastify date-fns

Backend Implementation

Database Configuration (server/config/db.js)

const mongoose = require('mongoose');

const connectDB = async () => {
  try {
    const conn = await mongoose.connect(process.env.MONGO_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    
    console.log(`MongoDB Connected: ${conn.connection.host}`);
  } catch (error) {
    console.error(`Error: ${error.message}`);
    process.exit(1);
  }
};

module.exports = connectDB;

Environment Variables (server/.env)

NODE_ENV=development
PORT=5000
MONGO_URI=mongodb://localhost:27017/task-manager
JWT_SECRET=your_jwt_secret_here
JWT_EXPIRE=1h
REFRESH_TOKEN_SECRET=your_refresh_token_secret_here
REFRESH_TOKEN_EXPIRE=7d

User Model (server/models/User.js)

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: [true, 'Please provide a name'],
    trim: true,
    maxlength: [50, 'Name cannot be more than 50 characters']
  },
  email: {
    type: String,
    required: [true, 'Please provide an email'],
    unique: true,
    trim: true,
    lowercase: true,
    match: [
      /^([\w-\.]+@([\w-]+\.)+[\w-]{2,4})?$/,
      'Please provide a valid email'
    ]
  },
  password: {
    type: String,
    required: [true, 'Please provide a password'],
    minlength: [6, 'Password must be at least 6 characters'],
    select: false
  },
  createdAt: {
    type: Date,
    default: Date.now
  },
  updatedAt: {
    type: Date,
    default: Date.now
  }
});

// Encrypt password before saving
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) {
    return next();
  }
  
  const salt = await bcrypt.genSalt(10);
  this.password = await bcrypt.hash(this.password, salt);
  next();
});

// Update the 'updatedAt' field on every update
userSchema.pre('findOneAndUpdate', function(next) {
  this.set({ updatedAt: Date.now() });
  next();
});

// Method to compare password
userSchema.methods.comparePassword = async function(candidatePassword) {
  return await bcrypt.compare(candidatePassword, this.password);
};

module.exports = mongoose.model('User', userSchema);

Task Model (server/models/Task.js)

const mongoose = require('mongoose');

const taskSchema = new mongoose.Schema({
  title: {
    type: String,
    required: [true, 'Please provide a task title'],
    trim: true,
    maxlength: [100, 'Title cannot be more than 100 characters']
  },
  description: {
    type: String,
    trim: true,
    maxlength: [500, 'Description cannot be more than 500 characters']
  },
  status: {
    type: String,
    enum: ['pending', 'in-progress', 'completed'],
    default: 'pending'
  },
  priority: {
    type: String,
    enum: ['low', 'medium', 'high'],
    default: 'medium'
  },
  dueDate: {
    type: Date
  },
  user: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true
  },
  createdAt: {
    type: Date,
    default: Date.now
  },
  updatedAt: {
    type: Date,
    default: Date.now
  }
});

// Update the 'updatedAt' field on every update
taskSchema.pre('findOneAndUpdate', function(next) {
  this.set({ updatedAt: Date.now() });
  next();
});

module.exports = mongoose.model('Task', taskSchema);

Authentication Controller (server/controllers/authController.js)

const User = require('../models/User');
const jwt = require('jsonwebtoken');
const { validationResult } = require('express-validator');

// Helper function to generate tokens
const generateTokens = (userId) => {
  // Generate access token
  const accessToken = jwt.sign(
    { id: userId },
    process.env.JWT_SECRET,
    { expiresIn: process.env.JWT_EXPIRE }
  );
  
  // Generate refresh token
  const refreshToken = jwt.sign(
    { id: userId },
    process.env.REFRESH_TOKEN_SECRET,
    { expiresIn: process.env.REFRESH_TOKEN_EXPIRE }
  );
  
  return { accessToken, refreshToken };
};

// @desc    Register user
// @route   POST /api/auth/register
// @access  Public
exports.register = async (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  
  const { name, email, password } = req.body;
  
  try {
    // Check if user already exists
    let user = await User.findOne({ email });
    
    if (user) {
      return res.status(400).json({ 
        success: false,
        message: 'User already exists' 
      });
    }
    
    // Create new user
    user = new User({
      name,
      email,
      password
    });
    
    await user.save();
    
    // Generate tokens
    const { accessToken, refreshToken } = generateTokens(user._id);
    
    res.status(201).json({
      success: true,
      accessToken,
      refreshToken,
      user: {
        id: user._id,
        name: user.name,
        email: user.email
      }
    });
  } catch (err) {
    console.error(err.message);
    res.status(500).json({ 
      success: false,
      message: 'Server error' 
    });
  }
};

// @desc    Login user
// @route   POST /api/auth/login
// @access  Public
exports.login = async (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  
  const { email, password } = req.body;
  
  try {
    // Find user by email and include password
    const user = await User.findOne({ email }).select('+password');
    
    if (!user) {
      return res.status(401).json({ 
        success: false,
        message: 'Invalid credentials' 
      });
    }
    
    // Check if password matches
    const isMatch = await user.comparePassword(password);
    
    if (!isMatch) {
      return res.status(401).json({ 
        success: false,
        message: 'Invalid credentials' 
      });
    }
    
    // Generate tokens
    const { accessToken, refreshToken } = generateTokens(user._id);
    
    res.json({
      success: true,
      accessToken,
      refreshToken,
      user: {
        id: user._id,
        name: user.name,
        email: user.email
      }
    });
  } catch (err) {
    console.error(err.message);
    res.status(500).json({ 
      success: false,
      message: 'Server error' 
    });
  }
};

// @desc    Refresh access token
// @route   POST /api/auth/refresh-token
// @access  Public
exports.refreshToken = async (req, res) => {
  const { refreshToken } = req.body;
  
  if (!refreshToken) {
    return res.status(400).json({ 
      success: false,
      message: 'No refresh token provided' 
    });
  }
  
  try {
    // Verify refresh token
    const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
    
    // Generate new tokens
    const { accessToken, refreshToken: newRefreshToken } = generateTokens(decoded.id);
    
    res.json({
      success: true,
      accessToken,
      refreshToken: newRefreshToken
    });
  } catch (err) {
    console.error(err.message);
    res.status(401).json({ 
      success: false,
      message: 'Invalid refresh token' 
    });
  }
};

// @desc    Get current user
// @route   GET /api/auth/me
// @access  Private
exports.getMe = async (req, res) => {
  try {
    // req.user.id comes from the auth middleware
    const user = await User.findById(req.user.id);
    
    if (!user) {
      return res.status(404).json({ 
        success: false,
        message: 'User not found' 
      });
    }
    
    res.json({
      success: true,
      user: {
        id: user._id,
        name: user.name,
        email: user.email,
        createdAt: user.createdAt
      }
    });
  } catch (err) {
    console.error(err.message);
    res.status(500).json({ 
      success: false,
      message: 'Server error' 
    });
  }
};

Tasks Controller (server/controllers/taskController.js)

const Task = require('../models/Task');
const { validationResult } = require('express-validator');

// @desc    Get all tasks for current user
// @route   GET /api/tasks
// @access  Private
exports.getTasks = async (req, res) => {
  try {
    // Get query parameters for filtering
    const { status, priority, sortBy } = req.query;
    
    // Build query
    const query = { user: req.user.id };
    
    // Add status filter if provided
    if (status) {
      query.status = status;
    }
    
    // Add priority filter if provided
    if (priority) {
      query.priority = priority;
    }
    
    // Build sort options
    let sortOptions = {};
    
    if (sortBy) {
      switch (sortBy) {
        case 'dueDate_asc':
          sortOptions = { dueDate: 1 };
          break;
        case 'dueDate_desc':
          sortOptions = { dueDate: -1 };
          break;
        case 'priority_asc':
          sortOptions = { priority: 1 };
          break;
        case 'priority_desc':
          sortOptions = { priority: -1 };
          break;
        case 'createdAt_asc':
          sortOptions = { createdAt: 1 };
          break;
        case 'createdAt_desc':
          sortOptions = { createdAt: -1 };
          break;
        default:
          sortOptions = { createdAt: -1 };
      }
    } else {
      // Default sort by creation date (newest first)
      sortOptions = { createdAt: -1 };
    }
    
    // Find tasks with query and sort
    const tasks = await Task.find(query).sort(sortOptions);
    
    res.json({
      success: true,
      count: tasks.length,
      data: tasks
    });
  } catch (err) {
    console.error(err.message);
    res.status(500).json({ 
      success: false,
      message: 'Server error' 
    });
  }
};

// @desc    Get single task
// @route   GET /api/tasks/:id
// @access  Private
exports.getTask = async (req, res) => {
  try {
    const task = await Task.findById(req.params.id);
    
    if (!task) {
      return res.status(404).json({ 
        success: false,
        message: 'Task not found' 
      });
    }
    
    // Check task belongs to user
    if (task.user.toString() !== req.user.id) {
      return res.status(403).json({ 
        success: false,
        message: 'Not authorized to access this task' 
      });
    }
    
    res.json({
      success: true,
      data: task
    });
  } catch (err) {
    console.error(err.message);
    
    // Handle invalid ObjectId
    if (err.kind === 'ObjectId') {
      return res.status(404).json({ 
        success: false,
        message: 'Task not found' 
      });
    }
    
    res.status(500).json({ 
      success: false,
      message: 'Server error' 
    });
  }
};

// @desc    Create new task
// @route   POST /api/tasks
// @access  Private
exports.createTask = async (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  
  const { title, description, status, priority, dueDate } = req.body;
  
  try {
    const newTask = new Task({
      title,
      description,
      status: status || 'pending',
      priority: priority || 'medium',
      dueDate: dueDate || null,
      user: req.user.id
    });
    
    const task = await newTask.save();
    
    res.status(201).json({
      success: true,
      data: task
    });
  } catch (err) {
    console.error(err.message);
    res.status(500).json({ 
      success: false,
      message: 'Server error' 
    });
  }
};

// @desc    Update task
// @route   PUT /api/tasks/:id
// @access  Private
exports.updateTask = async (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  
  const { title, description, status, priority, dueDate } = req.body;
  
  // Build task object
  const taskFields = {};
  if (title !== undefined) taskFields.title = title;
  if (description !== undefined) taskFields.description = description;
  if (status !== undefined) taskFields.status = status;
  if (priority !== undefined) taskFields.priority = priority;
  if (dueDate !== undefined) taskFields.dueDate = dueDate;
  
  try {
    let task = await Task.findById(req.params.id);
    
    if (!task) {
      return res.status(404).json({ 
        success: false,
        message: 'Task not found' 
      });
    }
    
    // Check task belongs to user
    if (task.user.toString() !== req.user.id) {
      return res.status(403).json({ 
        success: false,
        message: 'Not authorized to update this task' 
      });
    }
    
    // Update task
    task = await Task.findByIdAndUpdate(
      req.params.id,
      { $set: taskFields },
      { new: true }
    );
    
    res.json({
      success: true,
      data: task
    });
  } catch (err) {
    console.error(err.message);
    
    // Handle invalid ObjectId
    if (err.kind === 'ObjectId') {
      return res.status(404).json({ 
        success: false,
        message: 'Task not found' 
      });
    }
    
    res.status(500).json({ 
      success: false,
      message: 'Server error' 
    });
  }
};

// @desc    Delete task
// @route   DELETE /api/tasks/:id
// @access  Private
exports.deleteTask = async (req, res) => {
  try {
    const task = await Task.findById(req.params.id);
    
    if (!task) {
      return res.status(404).json({ 
        success: false,
        message: 'Task not found' 
      });
    }
    
    // Check task belongs to user
    if (task.user.toString() !== req.user.id) {
      return res.status(403).json({ 
        success: false,
        message: 'Not authorized to delete this task' 
      });
    }
    
    await task.remove();
    
    res.json({
      success: true,
      message: 'Task removed'
    });
  } catch (err) {
    console.error(err.message);
    
    // Handle invalid ObjectId
    if (err.kind === 'ObjectId') {
      return res.status(404).json({ 
        success: false,
        message: 'Task not found' 
      });
    }
    
    res.status(500).json({ 
      success: false,
      message: 'Server error' 
    });
  }
};

Authentication Middleware (server/middleware/auth.js)

const jwt = require('jsonwebtoken');
const User = require('../models/User');

exports.protect = async (req, res, next) => {
  let token;
  
  // Check for token in Authorization header
  if (
    req.headers.authorization &&
    req.headers.authorization.startsWith('Bearer')
  ) {
    // Format: Bearer <token>
    token = req.headers.authorization.split(' ')[1];
  }
  
  // Check if token exists
  if (!token) {
    return res.status(401).json({
      success: false,
      message: 'Not authorized to access this route'
    });
  }
  
  try {
    // Verify token
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    // Get user from database
    const user = await User.findById(decoded.id);
    
    if (!user) {
      return res.status(401).json({
        success: false,
        message: 'User not found'
      });
    }
    
    // Set user on request object
    req.user = {
      id: user._id
    };
    
    next();
  } catch (err) {
    return res.status(401).json({
      success: false,
      message: 'Not authorized to access this route'
    });
  }
};

Validation Middleware (server/middleware/validation.js)

const { check } = require('express-validator');

exports.registerValidation = [
  check('name', 'Name is required').not().isEmpty(),
  check('email', 'Please include a valid email').isEmail(),
  check('password', 'Password must be at least 6 characters').isLength({ min: 6 })
];

exports.loginValidation = [
  check('email', 'Please include a valid email').isEmail(),
  check('password', 'Password is required').exists()
];

exports.taskValidation = [
  check('title', 'Title is required').not().isEmpty(),
  check('status', 'Status must be one of: pending, in-progress, completed')
    .optional()
    .isIn(['pending', 'in-progress', 'completed']),
  check('priority', 'Priority must be one of: low, medium, high')
    .optional()
    .isIn(['low', 'medium', 'high']),
  check('dueDate', 'Due date must be a valid date')
    .optional()
    .isISO8601()
    .toDate()
];

Routes Setup

// server/routes/auth.js
const express = require('mongoose');
const router = express.Router();
const { 
  register, 
  login, 
  getMe, 
  refreshToken 
} = require('../controllers/authController');
const { protect } = require('../middleware/auth');
const { 
  registerValidation, 
  loginValidation 
} = require('../middleware/validation');

router.post('/register', registerValidation, register);
router.post('/login', loginValidation, login);
router.post('/refresh-token', refreshToken);
router.get('/me', protect, getMe);

module.exports = router;

// server/routes/tasks.js
const express = require('express');
const router = express.Router();
const { 
  getTasks, 
  getTask, 
  createTask, 
  updateTask, 
  deleteTask 
} = require('../controllers/taskController');
const { protect } = require('../middleware/auth');
const { taskValidation } = require('../middleware/validation');

router.route('/')
  .get(protect, getTasks)
  .post(protect, taskValidation, createTask);

router.route('/:id')
  .get(protect, getTask)
  .put(protect, taskValidation, updateTask)
  .delete(protect, deleteTask);

module.exports = router;

Server Setup (server/server.js)

const express = require('express');
const cors = require('cors');
const connectDB = require('./config/db');
require('dotenv').config();

// Connect to database
connectDB();

// Initialize express
const app = express();

// Middleware
app.use(cors());
app.use(express.json());

// Define routes
app.use('/api/auth', require('./routes/auth'));
app.use('/api/tasks', require('./routes/tasks'));

// Error handling middleware
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({
    success: false,
    message: 'Server Error'
  });
});

// Set port
const PORT = process.env.PORT || 5000;

// Start server
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Frontend Implementation

API Service (client/src/services/api.js)

import axios from 'axios';

const api = axios.create({
  baseURL: '/api',
  headers: {
    'Content-Type': 'application/json'
  }
});

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

// Response interceptor for handling token refresh
let isRefreshing = false;
let failedQueue = [];

const processQueue = (error, token = null) => {
  failedQueue.forEach(prom => {
    if (error) {
      prom.reject(error);
    } else {
      prom.resolve(token);
    }
  });
  
  failedQueue = [];
};

api.interceptors.response.use(
  (response) => {
    return response;
  },
  async (error) => {
    const originalRequest = error.config;
    
    if (error.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject });
        })
          .then(token => {
            originalRequest.headers['Authorization'] = `Bearer ${token}`;
            return api(originalRequest);
          })
          .catch(err => {
            return Promise.reject(err);
          });
      }
      
      originalRequest._retry = true;
      isRefreshing = true;
      
      try {
        const refreshToken = localStorage.getItem('refreshToken');
        if (!refreshToken) {
          throw new Error('No refresh token available');
        }
        
        const response = await axios.post('/api/auth/refresh-token', {
          refreshToken
        });
        
        const { accessToken, refreshToken: newRefreshToken } = response.data;
        
        localStorage.setItem('accessToken', accessToken);
        localStorage.setItem('refreshToken', newRefreshToken);
        
        api.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
        originalRequest.headers['Authorization'] = `Bearer ${accessToken}`;
        
        processQueue(null, accessToken);
        
        return api(originalRequest);
      } catch (err) {
        localStorage.removeItem('accessToken');
        localStorage.removeItem('refreshToken');
        
        processQueue(err, null);
        
        if (window.location.pathname !== '/login') {
          window.location.href = '/login';
        }
        
        return Promise.reject(err);
      } finally {
        isRefreshing = false;
      }
    }
    
    return Promise.reject(error);
  }
);

export default api;

Auth Service (client/src/services/authService.js)

import api from './api';

export const authService = {
  register: async (userData) => {
    const response = await api.post('/auth/register', userData);
    
    if (response.data.accessToken) {
      localStorage.setItem('accessToken', response.data.accessToken);
      localStorage.setItem('refreshToken', response.data.refreshToken);
    }
    
    return response.data;
  },
  
  login: async (credentials) => {
    const response = await api.post('/auth/login', credentials);
    
    if (response.data.accessToken) {
      localStorage.setItem('accessToken', response.data.accessToken);
      localStorage.setItem('refreshToken', response.data.refreshToken);
    }
    
    return response.data;
  },
  
  logout: () => {
    localStorage.removeItem('accessToken');
    localStorage.removeItem('refreshToken');
  },
  
  getCurrentUser: async () => {
    const response = await api.get('/auth/me');
    return response.data.user;
  },
  
  refreshToken: async () => {
    const refreshToken = localStorage.getItem('refreshToken');
    
    if (!refreshToken) {
      throw new Error('No refresh token available');
    }
    
    const response = await api.post('/auth/refresh-token', { refreshToken });
    
    if (response.data.accessToken) {
      localStorage.setItem('accessToken', response.data.accessToken);
      localStorage.setItem('refreshToken', response.data.refreshToken);
    }
    
    return response.data;
  },
  
  isAuthenticated: () => {
    return !!localStorage.getItem('accessToken');
  }
};

Task Service (client/src/services/taskService.js)

import api from './api';

export const taskService = {
  getTasks: async (params = {}) => {
    const response = await api.get('/tasks', { params });
    return response.data;
  },
  
  getTask: async (id) => {
    const response = await api.get(`/tasks/${id}`);
    return response.data;
  },
  
  createTask: async (taskData) => {
    const response = await api.post('/tasks', taskData);
    return response.data;
  },
  
  updateTask: async (id, taskData) => {
    const response = await api.put(`/tasks/${id}`, taskData);
    return response.data;
  },
  
  deleteTask: async (id) => {
    const response = await api.delete(`/tasks/${id}`);
    return response.data;
  }
};

Auth Context (client/src/context/AuthContext.js)

import React, { createContext, useReducer, useEffect } from 'react';
import { authService } from '../services/authService';

// Create context
export const AuthContext = createContext();

// Initial state
const initialState = {
  isAuthenticated: false,
  user: null,
  loading: true,
  error: null
};

// Reducer function
const authReducer = (state, action) => {
  switch (action.type) {
    case 'LOGIN_SUCCESS':
    case 'REGISTER_SUCCESS':
      return {
        ...state,
        isAuthenticated: true,
        user: action.payload,
        loading: false,
        error: null
      };
    case 'USER_LOADED':
      return {
        ...state,
        isAuthenticated: true,
        user: action.payload,
        loading: false
      };
    case 'AUTH_ERROR':
    case 'LOGIN_FAIL':
    case 'REGISTER_FAIL':
    case 'LOGOUT':
      return {
        ...state,
        isAuthenticated: false,
        user: null,
        loading: false,
        error: action.payload
      };
    case 'CLEAR_ERRORS':
      return {
        ...state,
        error: null
      };
    default:
      return state;
  }
};

// Provider component
export const AuthProvider = ({ children }) => {
  const [state, dispatch] = useReducer(authReducer, initialState);
  
  // Load user on initial render if token exists
  useEffect(() => {
    const loadUser = async () => {
      if (authService.isAuthenticated()) {
        try {
          const user = await authService.getCurrentUser();
          dispatch({
            type: 'USER_LOADED',
            payload: user
          });
        } catch (err) {
          console.error('Error loading user:', err);
          dispatch({ type: 'AUTH_ERROR' });
        }
      } else {
        dispatch({ type: 'AUTH_ERROR' });
      }
    };
    
    loadUser();
  }, []);
  
  // Register user
  const register = async (formData) => {
    try {
      const data = await authService.register(formData);
      
      dispatch({
        type: 'REGISTER_SUCCESS',
        payload: data.user
      });
      
      return data;
    } catch (err) {
      console.error('Register error:', err);
      
      dispatch({
        type: 'REGISTER_FAIL',
        payload: err.response?.data?.message || 'Registration failed'
      });
      
      throw err;
    }
  };
  
  // Login user
  const login = async (formData) => {
    try {
      const data = await authService.login(formData);
      
      dispatch({
        type: 'LOGIN_SUCCESS',
        payload: data.user
      });
      
      return data;
    } catch (err) {
      console.error('Login error:', err);
      
      dispatch({
        type: 'LOGIN_FAIL',
        payload: err.response?.data?.message || 'Invalid credentials'
      });
      
      throw err;
    }
  };
  
  // Logout user
  const logout = () => {
    authService.logout();
    dispatch({ type: 'LOGOUT' });
  };
  
  // Clear errors
  const clearErrors = () => {
    dispatch({ type: 'CLEAR_ERRORS' });
  };
  
  return (
    <AuthContext.Provider
      value={{
        isAuthenticated: state.isAuthenticated,
        user: state.user,
        loading: state.loading,
        error: state.error,
        register,
        login,
        logout,
        clearErrors
      }}
    >
      {children}
    </AuthContext.Provider>
  );
};

Task Context (client/src/context/TaskContext.js)

import React, { createContext, useReducer } from 'react';
import { taskService } from '../services/taskService';

// Create context
export const TaskContext = createContext();

// Initial state
const initialState = {
  tasks: [],
  currentTask: null,
  loading: true,
  error: null
};

// Reducer function
const taskReducer = (state, action) => {
  switch (action.type) {
    case 'GET_TASKS':
      return {
        ...state,
        tasks: action.payload,
        loading: false
      };
    case 'GET_TASK':
      return {
        ...state,
        currentTask: action.payload,
        loading: false
      };
    case 'ADD_TASK':
      return {
        ...state,
        tasks: [action.payload, ...state.tasks],
        loading: false
      };
    case 'UPDATE_TASK':
      return {
        ...state,
        tasks: state.tasks.map(task => 
          task._id === action.payload._id ? action.payload : task
        ),
        currentTask: action.payload,
        loading: false
      };
    case 'DELETE_TASK':
      return {
        ...state,
        tasks: state.tasks.filter(task => task._id !== action.payload),
        loading: false
      };
    case 'TASK_ERROR':
      return {
        ...state,
        error: action.payload,
        loading: false
      };
    case 'SET_LOADING':
      return {
        ...state,
        loading: true
      };
    case 'CLEAR_TASK':
      return {
        ...state,
        currentTask: null
      };
    case 'CLEAR_ERRORS':
      return {
        ...state,
        error: null
      };
    default:
      return state;
  }
};

// Provider component
export const TaskProvider = ({ children }) => {
  const [state, dispatch] = useReducer(taskReducer, initialState);
  
  // Get all tasks
  const getTasks = async (filters = {}) => {
    try {
      dispatch({ type: 'SET_LOADING' });
      
      const response = await taskService.getTasks(filters);
      
      dispatch({
        type: 'GET_TASKS',
        payload: response.data
      });
    } catch (err) {
      console.error('Get tasks error:', err);
      
      dispatch({
        type: 'TASK_ERROR',
        payload: err.response?.data?.message || 'Error fetching tasks'
      });
    }
  };
  
  // Get a single task
  const getTask = async (id) => {
    try {
      dispatch({ type: 'SET_LOADING' });
      
      const response = await taskService.getTask(id);
      
      dispatch({
        type: 'GET_TASK',
        payload: response.data
      });
    } catch (err) {
      console.error('Get task error:', err);
      
      dispatch({
        type: 'TASK_ERROR',
        payload: err.response?.data?.message || 'Error fetching task'
      });
    }
  };
  
  // Create a task
  const createTask = async (taskData) => {
    try {
      dispatch({ type: 'SET_LOADING' });
      
      const response = await taskService.createTask(taskData);
      
      dispatch({
        type: 'ADD_TASK',
        payload: response.data
      });
      
      return response.data;
    } catch (err) {
      console.error('Create task error:', err);
      
      dispatch({
        type: 'TASK_ERROR',
        payload: err.response?.data?.message || 'Error creating task'
      });
      
      throw err;
    }
  };
  
  // Update a task
  const updateTask = async (id, taskData) => {
    try {
      dispatch({ type: 'SET_LOADING' });
      
      const response = await taskService.updateTask(id, taskData);
      
      dispatch({
        type: 'UPDATE_TASK',
        payload: response.data
      });
      
      return response.data;
    } catch (err) {
      console.error('Update task error:', err);
      
      dispatch({
        type: 'TASK_ERROR',
        payload: err.response?.data?.message || 'Error updating task'
      });
      
      throw err;
    }
  };
  
  // Delete a task
  const deleteTask = async (id) => {
    try {
      dispatch({ type: 'SET_LOADING' });
      
      await taskService.deleteTask(id);
      
      dispatch({
        type: 'DELETE_TASK',
        payload: id
      });
    } catch (err) {
      console.error('Delete task error:', err);
      
      dispatch({
        type: 'TASK_ERROR',
        payload: err.response?.data?.message || 'Error deleting task'
      });
      
      throw err;
    }
  };
  
  // Clear current task
  const clearCurrentTask = () => {
    dispatch({ type: 'CLEAR_TASK' });
  };
  
  // Clear errors
  const clearErrors = () => {
    dispatch({ type: 'CLEAR_ERRORS' });
  };
  
  return (
    <TaskContext.Provider
      value={{
        tasks: state.tasks,
        currentTask: state.currentTask,
        loading: state.loading,
        error: state.error,
        getTasks,
        getTask,
        createTask,
        updateTask,
        deleteTask,
        clearCurrentTask,
        clearErrors
      }}
    >
      {children}
    </TaskContext.Provider>
  );
};

Private Route Component (client/src/components/routing/PrivateRoute.js)

import React, { useContext } from 'react';
import { Navigate } from 'react-router-dom';
import { AuthContext } from '../../context/AuthContext';

const PrivateRoute = ({ children }) => {
  const { isAuthenticated, loading } = useContext(AuthContext);
  
  if (loading) {
    return <div>Loading...</div>;
  }
  
  return isAuthenticated ? children : <Navigate to="/login" />;
};

export default PrivateRoute;

App Component with Routes (client/src/App.js)

import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

// Components
import Navbar from './components/layout/Navbar';
import PrivateRoute from './components/routing/PrivateRoute';

// Pages
import Home from './pages/Home';
import Login from './pages/Login';
import Register from './pages/Register';
import Dashboard from './pages/Dashboard';
import TaskDetail from './pages/TaskDetail';
import TaskForm from './pages/TaskForm';
import Profile from './pages/Profile';
import NotFound from './pages/NotFound';

// Context Providers
import { AuthProvider } from './context/AuthContext';
import { TaskProvider } from './context/TaskContext';

// Styles
import './App.css';

function App() {
  return (
    <AuthProvider>
      <TaskProvider>
        <Router>
          <div className="App">
            <Navbar />
            <div className="container">
              <Routes>
                <Route path="/" element={<Home />} />
                <Route path="/login" element={<Login />} />
                <Route path="/register" element={<Register />} />
                <Route 
                  path="/dashboard" 
                  element={
                    <PrivateRoute>
                      <Dashboard />
                    </PrivateRoute>
                  } 
                />
                <Route 
                  path="/tasks/:id" 
                  element={
                    <PrivateRoute>
                      <TaskDetail />
                    </PrivateRoute>
                  } 
                />
                <Route 
                  path="/tasks/new" 
                  element={
                    <PrivateRoute>
                      <TaskForm />
                    </PrivateRoute>
                  } 
                />
                <Route 
                  path="/tasks/edit/:id" 
                  element={
                    <PrivateRoute>
                      <TaskForm />
                    </PrivateRoute>
                  } 
                />
                <Route 
                  path="/profile" 
                  element={
                    <PrivateRoute>
                      <Profile />
                    </PrivateRoute>
                  } 
                />
                <Route path="*" element={<NotFound />} />
              </Routes>
            </div>
          </div>
          <ToastContainer position="bottom-right" />
        </Router>
      </TaskProvider>
    </AuthProvider>
  );
}

export default App;

Key UI Components

Let's implement some of the key UI components for our application:

Login Page (client/src/pages/Login.js)

import React, { useState, useContext, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
import { toast } from 'react-toastify';
import { AuthContext } from '../context/AuthContext';

const LoginSchema = Yup.object().shape({
  email: Yup.string()
    .email('Invalid email')
    .required('Email is required'),
  password: Yup.string()
    .required('Password is required')
});

const Login = () => {
  const { login, isAuthenticated, error, clearErrors } = useContext(AuthContext);
  const navigate = useNavigate();
  
  useEffect(() => {
    // Redirect if already authenticated
    if (isAuthenticated) {
      navigate('/dashboard');
    }
    
    // Show error notification if error exists
    if (error) {
      toast.error(error);
      clearErrors();
    }
    
    // eslint-disable-next-line
  }, [isAuthenticated, error]);
  
  const handleSubmit = async (values, { setSubmitting }) => {
    try {
      await login(values);
      toast.success('Login successful!');
      navigate('/dashboard');
    } catch (err) {
      // Error is handled in the AuthContext and displayed via useEffect
    } finally {
      setSubmitting(false);
    }
  };
  
  return (
    <div className="auth-container">
      <div className="auth-card">
        <h2>Welcome Back</h2>
        <p className="auth-subtitle">Sign in to access your tasks</p>
        
        <Formik
          initialValues={{ email: '', password: '' }}
          validationSchema={LoginSchema}
          onSubmit={handleSubmit}
        >
          {({ isSubmitting }) => (
            <Form>
              <div className="form-group">
                <label htmlFor="email">Email</label>
                <Field 
                  type="email" 
                  name="email" 
                  id="email" 
                  className="form-control" 
                  placeholder="Enter your email" 
                />
                <ErrorMessage name="email" component="div" className="error-message" />
              </div>
              
              <div className="form-group">
                <label htmlFor="password">Password</label>
                <Field 
                  type="password" 
                  name="password" 
                  id="password" 
                  className="form-control" 
                  placeholder="Enter your password" 
                />
                <ErrorMessage name="password" component="div" className="error-message" />
              </div>
              
              <button 
                type="submit" 
                className="btn btn-primary btn-block" 
                disabled={isSubmitting}
              >
                {isSubmitting ? 'Signing in...' : 'Sign In'}
              </button>
            </Form>
          )}
        </Formik>
        
        <p className="auth-redirect">
          Don't have an account? <Link to="/register">Register</Link>
        </p>
      </div>
    </div>
  );
};

export default Login;

Task Filter Component (client/src/components/tasks/TaskFilter.js)

import React from 'react';

const TaskFilter = ({ filters, onFilterChange }) => {
  const handleChange = (e) => {
    const { name, value } = e.target;
    onFilterChange({ [name]: value });
  };
  
  return (
    <div className="task-filter">
      <div className="filter-group">
        <label htmlFor="status">Status:</label>
        <select 
          name="status" 
          id="status" 
          value={filters.status} 
          onChange={handleChange}
        >
          <option value="">All</option>
          <option value="pending">Pending</option>
          <option value="in-progress">In Progress</option>
          <option value="completed">Completed</option>
        </select>
      </div>
      
      <div className="filter-group">
        <label htmlFor="priority">Priority:</label>
        <select 
          name="priority" 
          id="priority" 
          value={filters.priority} 
          onChange={handleChange}
        >
          <option value="">All</option>
          <option value="low">Low</option>
          <option value="medium">Medium</option>
          <option value="high">High</option>
        </select>
      </div>
      
      <div className="filter-group">
        <label htmlFor="sortBy">Sort By:</label>
        <select 
          name="sortBy" 
          id="sortBy" 
          value={filters.sortBy} 
          onChange={handleChange}
        >
          <option value="createdAt_desc">Newest First</option>
          <option value="createdAt_asc">Oldest First</option>
          <option value="dueDate_asc">Due Date (Ascending)</option>
          <option value="dueDate_desc">Due Date (Descending)</option>
          <option value="priority_desc">Priority (High to Low)</option>
          <option value="priority_asc">Priority (Low to High)</option>
        </select>
      </div>
    </div>
  );
};

export default TaskFilter;

Basic Styling (client/src/App.css)

/* Global Styles */
:root {
  --primary-color: #3498db;
  --primary-hover: #2980b9;
  --secondary-color: #e74c3c;
  --dark-color: #34495e;
  --light-color: #f4f4f4;
  --danger-color: #e74c3c;
  --success-color: #2ecc71;
  --warning-color: #f1c40f;
  --border-color: #ddd;
  --text-color: #333;
  --bg-color: #fff;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  font-size: 1rem;
  line-height: 1.6;
  background-color: #f8f9fa;
  color: var(--text-color);
}

a {
  color: var(--primary-color);
  text-decoration: none;
}

a:hover {
  color: var(--primary-hover);
}

ul {
  list-style: none;
}

.container {
  max-width: 1100px;
  margin: 0 auto;
  padding: 0 2rem;
  overflow: hidden;
}

/* Utility Classes */
.btn {
  display: inline-block;
  background: var(--light-color);
  color: #333;
  padding: 0.5rem 1.2rem;
  border: none;
  cursor: pointer;
  margin-right: 0.5rem;
  transition: opacity 0.2s ease-in;
  outline: none;
  border-radius: 4px;
  font-size: 1rem;
}

.btn-primary {
  background: var(--primary-color);
  color: white;
}

.btn-primary:hover {
  background: var(--primary-hover);
}

.btn-dark {
  background: var(--dark-color);
  color: white;
}

.btn-dark:hover {
  background: #23303b;
}

.btn-danger {
  background: var(--danger-color);
  color: white;
}

.btn-danger:hover {
  background: #bd2a1c;
}

.btn-success {
  background: var(--success-color);
  color: white;
}

.btn-success:hover {
  background: #25a25a;
}

.btn-block {
  display: block;
  width: 100%;
  text-align: center;
}

.btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.loader {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 200px;
  font-size: 1.2rem;
  color: var(--primary-color);
}

/* Form Styles */
.form-group {
  margin-bottom: 1.2rem;
}

.form-row {
  display: flex;
  gap: 1rem;
}

.form-row .form-group {
  flex: 1;
}

.form-group label {
  display: block;
  margin-bottom: 0.3rem;
  font-weight: 500;
}

.form-control {
  display: block;
  width: 100%;
  padding: 0.5rem;
  font-size: 1rem;
  border: 1px solid var(--border-color);
  border-radius: 4px;
}

.form-control:focus {
  outline: none;
  border-color: var(--primary-color);
}

.error-message {
  color: var(--danger-color);
  font-size: 0.8rem;
  margin-top: 0.2rem;
}

/* Auth Styles */
.auth-container {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 80vh;
}

.auth-card {
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  padding: 2rem;
  width: 100%;
  max-width: 400px;
}

.auth-card h2 {
  text-align: center;
  margin-bottom: 0.5rem;
}

.auth-subtitle {
  text-align: center;
  margin-bottom: 1.5rem;
  color: #666;
}

.auth-redirect {
  margin-top: 1.5rem;
  text-align: center;
  font-size: 0.9rem;
}

/* Task Styles */
.dashboard {
  padding: 2rem 0;
}

.dashboard-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 2rem;
}

.task-filter {
  display: flex;
  gap: 1rem;
  background: white;
  padding: 1rem;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  margin-bottom: 1.5rem;
}

.filter-group {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.task-count {
  margin-bottom: 1rem;
  font-size: 0.9rem;
  color: #666;
}

.task-list {
  display: grid;
  gap: 1rem;
}

.task-item {
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.task-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
}

.task-title {
  display: flex;
  align-items: center;
  gap: 0.8rem;
}

.task-title h3 {
  font-size: 1.2rem;
  margin: 0;
}

.task-priority {
  font-size: 0.8rem;
  padding: 0.2rem 0.5rem;
  border-radius: 12px;
  font-weight: 500;
}

.priority-high {
  background-color: #fceaea;
  color: var(--danger-color);
}

.priority-medium {
  background-color: #fff4de;
  color: var(--warning-color);
}

.priority-low {
  background-color: #e5f5eb;
  color: var(--success-color);
}

.task-actions {
  display: flex;
  gap: 0.5rem;
}

.btn-sm {
  padding: 0.25rem 0.5rem;
  font-size: 0.8rem;
}

.btn-edit {
  background-color: #eef2ff;
  color: #4f46e5;
}

.btn-delete {
  background-color: #fef2f2;
  color: #ef4444;
}

.task-description {
  color: #555;
  font-size: 0.95rem;
}

.task-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 0.85rem;
}

.task-dates {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  color: #666;
}

.task-status select {
  padding: 0.3rem 0.5rem;
  border-radius: 4px;
  border: 1px solid var(--border-color);
  font-size: 0.85rem;
}

.status-pending {
  color: var(--warning-color);
}

.status-in-progress {
  color: var(--primary-color);
}

.status-completed {
  color: var(--success-color);
}

.task-completed {
  opacity: 0.8;
}

.task-completed .task-title h3 {
  text-decoration: line-through;
}

.no-tasks {
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  padding: 2rem;
  text-align: center;
}

.task-form-container {
  max-width: 700px;
  margin: 2rem auto;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  padding: 2rem;
}

.task-form-container h1 {
  margin-bottom: 1.5rem;
}

.form-buttons {
  display: flex;
  justify-content: flex-end;
  gap: 1rem;
  margin-top: 1.5rem;
}

Step 4: Look Back and Reflect

The final step in Polya's problem-solving process is to look back and reflect on our solution. This step is crucial for learning and improving our development process for future projects.

Evaluating Our Solution

Let's evaluate our task management application against our initial requirements:

Potential Improvements

While our application meets the core requirements, several improvements could be made in a real-world scenario:

  1. Testing: Add comprehensive unit and integration tests for both frontend and backend components.
  2. Advanced Features: Implement additional features like task categories, subtasks, file attachments, and sharing.
  3. Performance Optimization: Implement pagination, caching, and other optimizations for improved performance.
  4. Security Enhancements: Add rate limiting, CSRF protection, and additional security measures.
  5. Accessibility: Ensure the application is fully accessible by following WCAG guidelines.
  6. Offline Support: Implement service workers for offline functionality.
  7. Notifications: Add email or push notifications for task reminders.

Learning Points

Through this project, we've learned several key concepts and best practices:

flowchart TD A[Understand the Problem] -->|Define Requirements| B[Devise a Plan] B -->|Create Project Structure| C[Execute the Plan] C -->|Implement Backend| C1[MongoDB Models] C -->|Implement Frontend| C2[React Components] C1 --> C3[Express Routes & Controllers] C2 --> C4[React Context & Hooks] C3 --> D[Look Back and Reflect] C4 --> D D -->|Evaluate Solution| E[Identify Improvements] E --> F[Learn and Refine] F -.--> A style A fill:#d4f0f0,stroke:#000 style B fill:#d4f0f0,stroke:#000 style C fill:#d4f0f0,stroke:#000 style D fill:#d4f0f0,stroke:#000 style E fill:#ffe6cc,stroke:#000 style F fill:#ffe6cc,stroke:#000

Applying Polya's Process to Future Projects

George Polya's problem-solving process provides a structured approach that can be applied to any development project:

  1. Understand the Problem: Define clear requirements, identify user needs, and understand the constraints.
  2. Devise a Plan: Create a project structure, define the component hierarchy, and map out the development workflow.
  3. Execute the Plan: Implement the solution, breaking it down into manageable tasks and following best practices.
  4. Look Back and Reflect: Evaluate the solution, identify improvements, and learn from the process.

This systematic approach helps ensure that your development process is well-structured, efficient, and leads to high-quality solutions.

Practice Activities

Activity 1: Basic Task Manager

Implement a simplified version of the task management application with basic functionality:

  • User registration and login
  • Create, read, update, and delete tasks
  • Mark tasks as complete/incomplete

Focus on applying Polya's 4 steps to structure your development process.

Activity 2: Enhanced Task Manager

Extend the basic task manager with additional features:

  • Task categories or tags
  • Search functionality
  • Task reminders with date/time selection
  • User profile management

Document how you applied each of Polya's problem-solving steps in your implementation.

Activity 3: Collaborative Task Manager

Build a collaborative task management system where users can:

  • Create team workspaces
  • Invite other users to collaborate
  • Assign tasks to team members
  • Comment on tasks
  • Track task activity history

Implement real-time updates using Socket.io or a similar technology.

Activity 4: Enterprise Task Manager

Develop a comprehensive task management system suitable for enterprise use:

  • Role-based access control (admin, manager, member)
  • Task dependencies and subtasks
  • File attachments and document management
  • Reporting and analytics dashboard
  • Integration with third-party services (email, calendar)

Focus on scalability, security, and performance optimization.

Summary

In this weekend project, we've applied George Polya's 4-step problem-solving procedure to develop a complete MERN stack application with authentication:

  1. Understand the Problem: We defined clear requirements for our task management application, identifying key features, data models, and potential challenges.
  2. Devise a Plan: We created a comprehensive project structure, designed the component hierarchy, defined API endpoints, and mapped out our development workflow.
  3. Execute the Plan: We implemented the solution, setting up the backend with Express.js and MongoDB, building the frontend with React, and connecting them through a RESTful API.
  4. Look Back and Reflect: We evaluated our solution against the requirements, identified potential improvements, and reflected on the learning points and best practices.

Through this process, we've built a functional task management application with user authentication, CRUD operations, and a responsive UI. We've learned valuable lessons about full-stack development with the MERN stack, including:

Most importantly, we've learned how to apply a structured problem-solving approach to software development, breaking down complex tasks into manageable steps and systematically working through them.

Further Resources