Build a Modern JavaScript Task Manager

A Complete Weekend Project with Webpack, Babel, and Testing

Project Overview

In this weekend project, we'll build a modern Task Manager application using all the tools and techniques we've learned this week. This project will demonstrate real-world usage of Webpack, Babel, Jest testing, and modern JavaScript features.

graph TD A[Task Manager App] --> B[Modern JavaScript] A --> C[Webpack Build] A --> D[Babel Transpilation] A --> E[Jest Testing] B --> B1[ES6+ Features] B --> B2[Modules] B --> B3[Classes] C --> C1[Bundle Management] C --> C2[Dev Server] C --> C3[Production Build] D --> D1[Browser Compatibility] D --> D2[Polyfills] D --> D3[Modern Syntax] E --> E1[Unit Tests] E --> E2[Integration Tests] E --> E3[TDD Approach] style A fill:#f9f,stroke:#333,stroke-width:4px

Step 1: Understand the Problem

We need to create a Task Manager application with the following features:

Expected Input

Expected Output

Step 2: Devise a Plan

Development Plan

  1. Set up project structure and dependencies
  2. Configure Webpack and Babel
  3. Create Task model with tests (TDD)
  4. Build TaskManager service with tests
  5. Implement localStorage persistence
  6. Create UI components
  7. Add event handling and interactivity
  8. Style the application
  9. Configure production build
  10. Add error handling and edge cases

Step 3: Implement the Solution

Project Setup

First, let's create our project structure:

# Create project directory
mkdir task-manager
cd task-manager

# Initialize npm project
npm init -y

# Install dependencies
npm install --save-dev webpack webpack-cli webpack-dev-server
npm install --save-dev @babel/core @babel/preset-env babel-loader
npm install --save-dev jest @babel/preset-react @testing-library/jest-dom
npm install --save-dev html-webpack-plugin css-loader style-loader
npm install --save-dev clean-webpack-plugin mini-css-extract-plugin

# Create project structure
mkdir src
mkdir src/js
mkdir src/css
mkdir src/js/models
mkdir src/js/services
mkdir src/js/components
mkdir src/__tests__
mkdir dist

File Structure

task-manager/
├── src/
│   ├── js/
│   │   ├── models/
│   │   │   └── Task.js
│   │   ├── services/
│   │   │   └── TaskManager.js
│   │   ├── components/
│   │   │   ├── TaskForm.js
│   │   │   ├── TaskList.js
│   │   │   └── TaskFilter.js
│   │   └── app.js
│   ├── css/
│   │   └── styles.css
│   ├── index.html
│   └── __tests__/
│       ├── Task.test.js
│       └── TaskManager.test.js
├── webpack.config.js
├── babel.config.js
├── package.json
└── jest.config.js

Configure Webpack

Create webpack.config.js:

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = (env, argv) => {
  const isProduction = argv.mode === 'production';
  
  return {
    entry: './src/js/app.js',
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: isProduction ? '[name].[contenthash].js' : '[name].js',
      clean: true
    },
    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader'
          }
        },
        {
          test: /\.css$/,
          use: [
            isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
            'css-loader'
          ]
        }
      ]
    },
    plugins: [
      new CleanWebpackPlugin(),
      new HtmlWebpackPlugin({
        template: './src/index.html',
        title: 'Task Manager'
      }),
      isProduction && new MiniCssExtractPlugin({
        filename: '[name].[contenthash].css'
      })
    ].filter(Boolean),
    devServer: {
      static: './dist',
      hot: true,
      open: true
    },
    devtool: isProduction ? 'source-map' : 'eval-source-map'
  };
};

Configure Babel

Create babel.config.js:

// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', {
      targets: '> 0.25%, not dead',
      useBuiltIns: 'usage',
      corejs: 3
    }]
  ]
};

Configure Jest

Create jest.config.js:

// jest.config.js
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['@testing-library/jest-dom'],
  moduleNameMapper: {
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy'
  }
};

Create Task Model with TDD

First, write the test for our Task model:

// src/__tests__/Task.test.js
import Task from '../js/models/Task';

describe('Task', () => {
  describe('constructor', () => {
    test('creates task with required properties', () => {
      const task = new Task('Buy groceries', 'Get milk and eggs');
      
      expect(task.id).toBeDefined();
      expect(task.title).toBe('Buy groceries');
      expect(task.description).toBe('Get milk and eggs');
      expect(task.completed).toBe(false);
      expect(task.createdAt).toBeInstanceOf(Date);
    });
    
    test('creates task with optional properties', () => {
      const dueDate = new Date('2024-12-31');
      const task = new Task('Submit report', 'Quarterly financial report', {
        dueDate,
        priority: 'high'
      });
      
      expect(task.dueDate).toEqual(dueDate);
      expect(task.priority).toBe('high');
    });
  });
  
  describe('complete', () => {
    test('marks task as completed', () => {
      const task = new Task('Test task');
      task.complete();
      
      expect(task.completed).toBe(true);
      expect(task.completedAt).toBeInstanceOf(Date);
    });
  });
  
  describe('uncomplete', () => {
    test('marks task as not completed', () => {
      const task = new Task('Test task');
      task.complete();
      task.uncomplete();
      
      expect(task.completed).toBe(false);
      expect(task.completedAt).toBeNull();
    });
  });
  
  describe('isOverdue', () => {
    test('returns true for overdue tasks', () => {
      const yesterday = new Date();
      yesterday.setDate(yesterday.getDate() - 1);
      
      const task = new Task('Overdue task', '', { dueDate: yesterday });
      
      expect(task.isOverdue()).toBe(true);
    });
    
    test('returns false for future tasks', () => {
      const tomorrow = new Date();
      tomorrow.setDate(tomorrow.getDate() + 1);
      
      const task = new Task('Future task', '', { dueDate: tomorrow });
      
      expect(task.isOverdue()).toBe(false);
    });
  });
});

Now implement the Task model:

// src/js/models/Task.js
export default class Task {
  constructor(title, description = '', options = {}) {
    this.id = Date.now().toString(36) + Math.random().toString(36).substr(2);
    this.title = title;
    this.description = description;
    this.completed = false;
    this.createdAt = new Date();
    this.completedAt = null;
    this.dueDate = options.dueDate || null;
    this.priority = options.priority || 'normal';
  }
  
  complete() {
    this.completed = true;
    this.completedAt = new Date();
  }
  
  uncomplete() {
    this.completed = false;
    this.completedAt = null;
  }
  
  isOverdue() {
    if (!this.dueDate || this.completed) return false;
    return new Date() > this.dueDate;
  }
  
  toJSON() {
    return {
      id: this.id,
      title: this.title,
      description: this.description,
      completed: this.completed,
      createdAt: this.createdAt.toISOString(),
      completedAt: this.completedAt ? this.completedAt.toISOString() : null,
      dueDate: this.dueDate ? this.dueDate.toISOString() : null,
      priority: this.priority
    };
  }
  
  static fromJSON(json) {
    const task = new Task(json.title, json.description, {
      dueDate: json.dueDate ? new Date(json.dueDate) : null,
      priority: json.priority
    });
    
    task.id = json.id;
    task.completed = json.completed;
    task.createdAt = new Date(json.createdAt);
    task.completedAt = json.completedAt ? new Date(json.completedAt) : null;
    
    return task;
  }
}

Create TaskManager Service

Write tests for TaskManager:

// src/__tests__/TaskManager.test.js
import TaskManager from '../js/services/TaskManager';
import Task from '../js/models/Task';

describe('TaskManager', () => {
  let taskManager;
  
  beforeEach(() => {
    taskManager = new TaskManager();
    localStorage.clear();
  });
  
  describe('addTask', () => {
    test('adds a new task', () => {
      const task = taskManager.addTask('Test task', 'Description');
      
      expect(task).toBeInstanceOf(Task);
      expect(task.title).toBe('Test task');
      expect(taskManager.getAllTasks()).toHaveLength(1);
    });
  });
  
  describe('removeTask', () => {
    test('removes a task by id', () => {
      const task = taskManager.addTask('Test task');
      taskManager.removeTask(task.id);
      
      expect(taskManager.getAllTasks()).toHaveLength(0);
    });
  });
  
  describe('getTask', () => {
    test('retrieves a task by id', () => {
      const task = taskManager.addTask('Test task');
      const retrieved = taskManager.getTask(task.id);
      
      expect(retrieved).toEqual(task);
    });
  });
  
  describe('updateTask', () => {
    test('updates task properties', () => {
      const task = taskManager.addTask('Test task');
      const updated = taskManager.updateTask(task.id, {
        title: 'Updated task',
        description: 'New description'
      });
      
      expect(updated.title).toBe('Updated task');
      expect(updated.description).toBe('New description');
    });
  });
  
  describe('getFilteredTasks', () => {
    beforeEach(() => {
      taskManager.addTask('Task 1').complete();
      taskManager.addTask('Task 2');
      taskManager.addTask('Task 3').complete();
    });
    
    test('filters all tasks', () => {
      const tasks = taskManager.getFilteredTasks('all');
      expect(tasks).toHaveLength(3);
    });
    
    test('filters active tasks', () => {
      const tasks = taskManager.getFilteredTasks('active');
      expect(tasks).toHaveLength(1);
      expect(tasks[0].title).toBe('Task 2');
    });
    
    test('filters completed tasks', () => {
      const tasks = taskManager.getFilteredTasks('completed');
      expect(tasks).toHaveLength(2);
    });
  });
  
  describe('persistence', () => {
    test('saves tasks to localStorage', () => {
      taskManager.addTask('Test task');
      const newManager = new TaskManager();
      
      expect(newManager.getAllTasks()).toHaveLength(1);
    });
  });
});

Implement TaskManager:

// src/js/services/TaskManager.js
import Task from '../models/Task';

export default class TaskManager {
  constructor() {
    this.tasks = [];
    this.loadTasks();
  }
  
  loadTasks() {
    const savedTasks = localStorage.getItem('tasks');
    if (savedTasks) {
      this.tasks = JSON.parse(savedTasks).map(Task.fromJSON);
    }
  }
  
  saveTasks() {
    localStorage.setItem('tasks', JSON.stringify(this.tasks));
  }
  
  addTask(title, description = '', options = {}) {
    const task = new Task(title, description, options);
    this.tasks.push(task);
    this.saveTasks();
    return task;
  }
  
  removeTask(id) {
    this.tasks = this.tasks.filter(task => task.id !== id);
    this.saveTasks();
  }
  
  getTask(id) {
    return this.tasks.find(task => task.id === id);
  }
  
  updateTask(id, updates) {
    const task = this.getTask(id);
    if (task) {
      Object.assign(task, updates);
      this.saveTasks();
    }
    return task;
  }
  
  getAllTasks() {
    return [...this.tasks];
  }
  
  getFilteredTasks(filter = 'all') {
    switch (filter) {
      case 'active':
        return this.tasks.filter(task => !task.completed);
      case 'completed':
        return this.tasks.filter(task => task.completed);
      default:
        return this.getAllTasks();
    }
  }
}

Create UI Components

Create the TaskForm component:

// src/js/components/TaskForm.js
export default class TaskForm {
  constructor(onSubmit) {
    this.onSubmit = onSubmit;
    this.element = this.createElement();
    this.attachEventListeners();
  }
  
  createElement() {
    const form = document.createElement('form');
    form.className = 'task-form';
    form.innerHTML = `
      <div class="form-group">
        <input type="text" name="title" placeholder="Task title" required>
      </div>
      <div class="form-group">
        <textarea name="description" placeholder="Description"></textarea>
      </div>
      <div class="form-group">
        <input type="date" name="dueDate">
      </div>
      <div class="form-group">
        <select name="priority">
          <option value="low">Low Priority</option>
          <option value="normal" selected>Normal Priority</option>
          <option value="high">High Priority</option>
        </select>
      </div>
      <button type="submit">Add Task</button>
    `;
    return form;
  }
  
  attachEventListeners() {
    this.element.addEventListener('submit', (e) => {
      e.preventDefault();
      const formData = new FormData(e.target);
      
      this.onSubmit({
        title: formData.get('title'),
        description: formData.get('description'),
        dueDate: formData.get('dueDate') ? new Date(formData.get('dueDate')) : null,
        priority: formData.get('priority')
      });
      
      e.target.reset();
    });
  }
}

Create the TaskList component:

// src/js/components/TaskList.js
export default class TaskList {
  constructor(onToggle, onDelete) {
    this.onToggle = onToggle;
    this.onDelete = onDelete;
    this.element = this.createElement();
  }
  
  createElement() {
    const list = document.createElement('ul');
    list.className = 'task-list';
    return list;
  }
  
  render(tasks) {
    this.element.innerHTML = '';
    
    if (tasks.length === 0) {
      this.element.innerHTML = '<li class="empty-message">No tasks yet!</li>';
      return;
    }
    
    tasks.forEach(task => {
      const li = document.createElement('li');
      li.className = `task-item ${task.completed ? 'completed' : ''} ${task.isOverdue() ? 'overdue' : ''}`;
      li.innerHTML = `
        <div class="task-content">
          <input type="checkbox" ${task.completed ? 'checked' : ''}>
          <div class="task-info">
            <h3>${task.title}</h3>
            <p>${task.description}</p>
            ${task.dueDate ? `<small>Due: ${task.dueDate.toLocaleDateString()}</small>` : ''}
            <span class="priority ${task.priority}">${task.priority}</span>
          </div>
        </div>
        <button class="delete-btn">Delete</button>
      `;
      
      // Add event listeners
      const checkbox = li.querySelector('input[type="checkbox"]');
      checkbox.addEventListener('change', () => this.onToggle(task.id));
      
      const deleteBtn = li.querySelector('.delete-btn');
      deleteBtn.addEventListener('click', () => this.onDelete(task.id));
      
      this.element.appendChild(li);
    });
  }
}

Create the TaskFilter component:

// src/js/components/TaskFilter.js
export default class TaskFilter {
  constructor(onFilterChange) {
    this.onFilterChange = onFilterChange;
    this.currentFilter = 'all';
    this.element = this.createElement();
    this.attachEventListeners();
  }
  
  createElement() {
    const filter = document.createElement('div');
    filter.className = 'task-filter';
    filter.innerHTML = `
      <button class="filter-btn active" data-filter="all">All</button>
      <button class="filter-btn" data-filter="active">Active</button>
      <button class="filter-btn" data-filter="completed">Completed</button>
    `;
    return filter;
  }
  
  attachEventListeners() {
    this.element.addEventListener('click', (e) => {
      if (e.target.classList.contains('filter-btn')) {
        const filter = e.target.dataset.filter;
        this.setFilter(filter);
      }
    });
  }
  
  setFilter(filter) {
    this.currentFilter = filter;
    
    // Update active button
    this.element.querySelectorAll('.filter-btn').forEach(btn => {
      btn.classList.toggle('active', btn.dataset.filter === filter);
    });
    
    this.onFilterChange(filter);
  }
}

Create Main Application

Create the main application file:

// src/js/app.js
import TaskManager from './services/TaskManager';
import TaskForm from './components/TaskForm';
import TaskList from './components/TaskList';
import TaskFilter from './components/TaskFilter';
import '../css/styles.css';

class TaskApp {
  constructor() {
    this.taskManager = new TaskManager();
    this.currentFilter = 'all';
    
    this.initializeComponents();
    this.render();
  }
  
  initializeComponents() {
    // Create form
    this.taskForm = new TaskForm(this.handleAddTask.bind(this));
    
    // Create list
    this.taskList = new TaskList(
      this.handleToggleTask.bind(this),
      this.handleDeleteTask.bind(this)
    );
    
    // Create filter
    this.taskFilter = new TaskFilter(this.handleFilterChange.bind(this));
    
    // Add components to DOM
    const app = document.getElementById('app');
    app.appendChild(this.taskForm.element);
    app.appendChild(this.taskFilter.element);
    app.appendChild(this.taskList.element);
  }
  
  handleAddTask(taskData) {
    this.taskManager.addTask(
      taskData.title,
      taskData.description,
      {
        dueDate: taskData.dueDate,
        priority: taskData.priority
      }
    );
    this.render();
  }
  
  handleToggleTask(taskId) {
    const task = this.taskManager.getTask(taskId);
    if (task) {
      if (task.completed) {
        task.uncomplete();
      } else {
        task.complete();
      }
      this.taskManager.saveTasks();
      this.render();
    }
  }
  
  handleDeleteTask(taskId) {
    this.taskManager.removeTask(taskId);
    this.render();
  }
  
  handleFilterChange(filter) {
    this.currentFilter = filter;
    this.render();
  }
  
  render() {
    const tasks = this.taskManager.getFilteredTasks(this.currentFilter);
    this.taskList.render(tasks);
  }
}

// Initialize app when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
  new TaskApp();
});

Create HTML Template

Create the HTML file:

<!-- src/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Task Manager</title>
</head>
<body>
    <div class="container">
        <header>
            <h1>Task Manager</h1>
            <p>Organize your tasks efficiently</p>
        </header>
        
        <main id="app">
            <!-- App components will be inserted here -->
        </main>
        
        <footer>
            <p>Built with modern JavaScript, Webpack, and Babel</p>
        </footer>
    </div>
</body>
</html>

Add Styles

Create the CSS file:

/* src/css/styles.css */
* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
    line-height: 1.6;
    color: #333;
    background-color: #f5f5f5;
}

.container {
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
}

header {
    text-align: center;
    margin-bottom: 2rem;
}

h1 {
    color: #2c3e50;
    margin-bottom: 0.5rem;
}

/* Form styles */
.task-form {
    background: white;
    padding: 1.5rem;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    margin-bottom: 2rem;
}

.form-group {
    margin-bottom: 1rem;
}

input, textarea, select {
    width: 100%;
    padding: 0.5rem;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 1rem;
}

button {
    background: #3498db;
    color: white;
    border: none;
    padding: 0.75rem 1.5rem;
    border-radius: 4px;
    cursor: pointer;
    font-size: 1rem;
    transition: background 0.3s;
}

button:hover {
    background: #2980b9;
}

/* Filter styles */
.task-filter {
    display: flex;
    gap: 0.5rem;
    margin-bottom: 1.5rem;
    justify-content: center;
}

.filter-btn {
    background: #ecf0f1;
    color: #2c3e50;
    padding: 0.5rem 1rem;
}

.filter-btn.active {
    background: #3498db;
    color: white;
}

/* Task list styles */
.task-list {
    list-style: none;
}

.task-item {
    background: white;
    margin-bottom: 1rem;
    padding: 1rem;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.task-item.completed {
    opacity: 0.7;
}

.task-item.completed .task-info h3 {
    text-decoration: line-through;
    color: #7f8c8d;
}

.task-item.overdue {
    border-left: 4px solid #e74c3c;
}

.task-content {
    display: flex;
    align-items: center;
    gap: 1rem;
    flex: 1;
}

.task-info {
    flex: 1;
}

.task-info h3 {
    margin-bottom: 0.25rem;
}

.task-info p {
    color: #7f8c8d;
    font-size: 0.9rem;
    margin-bottom: 0.25rem;
}

.task-info small {
    display: block;
    color: #95a5a6;
    font-size: 0.8rem;
}

.priority {
    display: inline-block;
    padding: 0.25rem 0.5rem;
    border-radius: 4px;
    font-size: 0.75rem;
    font-weight: bold;
    text-transform: uppercase;
}

.priority.low {
    background: #2ecc71;
    color: white;
}

.priority.normal {
    background: #3498db;
    color: white;
}

.priority.high {
    background: #e74c3c;
    color: white;
}

.delete-btn {
    background: #e74c3c;
    padding: 0.5rem 1rem;
}

.delete-btn:hover {
    background: #c0392b;
}

.empty-message {
    text-align: center;
    padding: 2rem;
    color: #7f8c8d;
    font-style: italic;
}

footer {
    text-align: center;
    margin-top: 2rem;
    color: #7f8c8d;
    font-size: 0.9rem;
}

Add NPM Scripts

Update package.json:

{
  "name": "task-manager",
  "version": "1.0.0",
  "scripts": {
    "start": "webpack serve --mode development",
    "build": "webpack --mode production",
    "test": "jest",
    "test:watch": "jest --watch"
  },
  // ... dependencies
}

Step 4: Test and Refine

Running the Application

# Start development server
npm start

# Run tests
npm test

# Build for production
npm run build

Testing the Application

  1. Run all tests to ensure they pass
  2. Start the development server
  3. Test adding tasks with various inputs
  4. Test completing and uncompleting tasks
  5. Test filtering tasks
  6. Test deleting tasks
  7. Refresh the page to ensure persistence works
  8. Test edge cases (empty inputs, long text, etc.)

Advanced Features to Add

Feature Ideas

Example: Adding Search Functionality

// Add to TaskManager.js
searchTasks(query) {
  const lowercaseQuery = query.toLowerCase();
  return this.tasks.filter(task => 
    task.title.toLowerCase().includes(lowercaseQuery) ||
    task.description.toLowerCase().includes(lowercaseQuery)
  );
}

// Add search input to app
const searchInput = document.createElement('input');
searchInput.type = 'search';
searchInput.placeholder = 'Search tasks...';
searchInput.addEventListener('input', (e) => {
  const results = taskManager.searchTasks(e.target.value);
  taskList.render(results);
});

Real-World Applications

This Project Demonstrates

Skills Learned

Project Structure Overview

graph TD A[app.js] --> B[TaskManager Service] A --> C[UI Components] B --> D[Task Model] B --> E[localStorage] C --> F[TaskForm] C --> G[TaskList] C --> H[TaskFilter] I[Webpack] --> J[Bundle] K[Babel] --> J L[Tests] --> M[Jest] style A fill:#f9f,stroke:#333,stroke-width:4px style B fill:#bbf,stroke:#333 style C fill:#bbf,stroke:#333 style D fill:#bfb,stroke:#333 style I fill:#ff9,stroke:#333 style K fill:#ff9,stroke:#333 style L fill:#9ff,stroke:#333

Next Steps

Enhance Your Project

  1. Add more comprehensive tests
  2. Implement additional features
  3. Improve error handling
  4. Add animations and transitions
  5. Optimize for performance
  6. Deploy to a hosting service

Deployment Options

Congratulations! You've built a modern JavaScript application using industry-standard tools and practices!