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.
Step 1: Understand the Problem
We need to create a Task Manager application with the following features:
- Add, edit, and delete tasks
- Mark tasks as complete
- Filter tasks by status
- Persist tasks in localStorage
- Use modern JavaScript features
- Bundle with Webpack
- Transpile with Babel for browser compatibility
- Include comprehensive test coverage
Expected Input
- User interactions: clicks, form submissions
- Task data: title, description, due date, priority
- Filter selections: all, active, completed
Expected Output
- Dynamic task list display
- Visual feedback for task status
- Persistent data storage
- Responsive user interface
Step 2: Devise a Plan
Development Plan
- Set up project structure and dependencies
- Configure Webpack and Babel
- Create Task model with tests (TDD)
- Build TaskManager service with tests
- Implement localStorage persistence
- Create UI components
- Add event handling and interactivity
- Style the application
- Configure production build
- 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
- Run all tests to ensure they pass
- Start the development server
- Test adding tasks with various inputs
- Test completing and uncompleting tasks
- Test filtering tasks
- Test deleting tasks
- Refresh the page to ensure persistence works
- Test edge cases (empty inputs, long text, etc.)
Advanced Features to Add
Feature Ideas
- Task categories/tags
- Search functionality
- Drag and drop reordering
- Due date notifications
- Task priority sorting
- Export/import tasks
- Dark mode toggle
- Keyboard shortcuts
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
- Modern JavaScript: Classes, modules, arrow functions, template literals
- Build Tools: Webpack for bundling, Babel for transpilation
- Testing: Jest for unit testing with TDD approach
- Component Architecture: Modular design with separation of concerns
- State Management: Service layer for business logic
- Persistence: localStorage for data persistence
- Event Handling: Custom events and DOM manipulation
- Responsive Design: Mobile-friendly interface
Skills Learned
- Setting up a modern JavaScript development environment
- Writing testable code with TDD
- Creating reusable components
- Managing application state
- Handling user interactions
- Building for production
Project Structure Overview
Next Steps
Enhance Your Project
- Add more comprehensive tests
- Implement additional features
- Improve error handling
- Add animations and transitions
- Optimize for performance
- Deploy to a hosting service
Deployment Options
- GitHub Pages
- Netlify
- Vercel
- Heroku
Congratulations! You've built a modern JavaScript application using industry-standard tools and practices!