Project Overview
This weekend, you'll build a robust JavaScript application that demonstrates your mastery of advanced language features. Your project will incorporate object-oriented programming, functional programming concepts, error handling, and modern ES6+ features.
Throughout this project, we'll apply George Polya's four-step problem-solving framework:
This systematic approach will help you tackle complex problems methodically, a crucial skill for professional developers.
Project Requirements
You'll create a Task Management Application with the following features:
- Create, read, update, and delete tasks
- Organize tasks into projects or categories
- Set priorities, due dates, and status for tasks
- Filter and sort tasks based on different criteria
- Data persistence using localStorage
- Error handling and validation
- Modern, responsive user interface
The application should demonstrate these advanced JavaScript concepts:
- Object-oriented programming with classes
- Closures and higher-order functions
- Array methods (map, filter, reduce, etc.)
- ES6+ features (destructuring, spread/rest, template literals, etc.)
- Proper error handling with try/catch
- Custom error types
- Asynchronous programming with Promises and async/await
- Module pattern and code organization
George Polya's Problem-Solving Approach
George Polya was a mathematician who developed a four-step approach to problem-solving in his book "How to Solve It" (1945). This framework is applicable to any complex problem, including software development:
Step 1: Understand the Problem
- What are you being asked to build?
- What are the inputs and expected outputs?
- What constraints exist?
- Can you restate the problem in your own words?
- Do you have all the information you need?
Step 2: Devise a Plan
- Break down the problem into smaller parts
- Consider different approaches and strategies
- Draw on similar problems you've solved before
- Create a step-by-step plan for implementation
- Decide on the tools, patterns, and techniques to use
Step 3: Execute the Plan
- Implement your solution step by step
- Monitor your progress and adjust as needed
- Test each component as you build it
- Document your work as you go
Step 4: Review and Reflect
- Verify that your solution works as expected
- Analyze your solution for efficiency and elegance
- Look for opportunities to refactor or improve
- Reflect on what you've learned and how it might apply to future problems
- Consider alternative approaches that might have worked better
Throughout this project, we'll apply this framework to guide our development process.
Step 1: Understanding the Problem
Let's begin by thoroughly understanding what we're building.
Core Problem Statement
We need to create a task management application that allows users to organize their work effectively, with features similar to popular apps like Todoist, Trello, or Asana, but simplified for a weekend project.
Key Questions to Consider
- What data will we need to store? Tasks, projects/categories, priorities, dates, etc.
- What operations will users perform? CRUD operations on tasks, filtering, sorting
- How will data persist? localStorage for this project scope
- What user interface elements are needed? Forms, lists, filters, sorting controls
- What potential errors might occur? Invalid inputs, duplicate tasks, localStorage limitations
Clarifying the Boundaries
Since this is a weekend project, we'll set clear boundaries:
- Single-user application (no authentication or multi-user features)
- Client-side only (no backend server required)
- Data persists only in the browser (localStorage)
- Focus on functionality over extensive styling
- No external libraries or frameworks required (pure JavaScript)
Understanding this structure helps us conceptualize how our application will work before we start coding.
Step 2: Devising a Plan
Now that we understand what we're building, let's develop a strategic plan for implementation.
Application Architecture
We'll use a modular approach with these main components:
- Models: Classes for Task and Project entities
- Services: Storage service for localStorage operations
- Controllers: TaskManager to handle business logic
- Views: UI rendering and event handling
- Utilities: Helper functions, validation, error handling
Implementation Strategy
- Set up the project structure and files
- Create the data models (Task and Project classes)
- Implement the storage service for data persistence
- Build the TaskManager controller with core functionality
- Create the UI components and event handlers
- Add validation and error handling throughout
- Implement filtering and sorting features
- Add final touches and polish the user experience
Advanced JavaScript Features to Incorporate
Let's plan how we'll showcase advanced JavaScript concepts:
- Classes: For Task, Project, and TaskManager models
- Closures: For encapsulating private functionality
- Higher-order functions: For filtering and sorting operations
- Array methods: map, filter, reduce, find for data manipulation
- Destructuring: For clean parameter handling and data extraction
- Spread/rest: For immutable updates and flexible function parameters
- Template literals: For dynamic HTML generation
- Custom errors: ValidationError for input validation
- try/catch: For graceful error handling, especially with localStorage
- Promises & async/await: For simulated API delays and potential future expansion
- Module pattern: For organizing code into separate concerns
Project Structure
task-manager/
├── index.html
├── css/
│ └── style.css
├── js/
│ ├── app.js # Main application entry point
│ ├── models/
│ │ ├── Task.js # Task class definition
│ │ └── Project.js # Project class definition
│ ├── services/
│ │ └── StorageService.js # localStorage interactions
│ ├── controllers/
│ │ └── TaskManager.js # Business logic
│ ├── views/
│ │ ├── TaskView.js # Task UI rendering
│ │ └── ProjectView.js # Project UI rendering
│ └── utils/
│ ├── ValidationUtils.js # Input validation
│ ├── ErrorHandling.js # Custom errors and handlers
│ └── DOMUtils.js # DOM manipulation helpers
└── README.md
Step 3: Executing the Plan
Let's implement our application component by component, following our plan.
Setting Up the Project
First, create the basic HTML structure:
<!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>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header>
<h1>Task Manager</h1>
</header>
<main>
<div class="container">
<div class="projects-section">
<h2>Projects</h2>
<div class="project-form">
<input type="text" id="project-name" placeholder="New project name">
<input type="color" id="project-color" value="#3498db">
<button id="add-project-btn">Add Project</button>
</div>
<ul id="projects-list" class="projects-list"></ul>
</div>
<div class="tasks-section">
<div class="task-header">
<h2 id="current-project-name">All Tasks</h2>
<div class="task-controls">
<select id="filter-status">
<option value="all">All Statuses</option>
<option value="todo">To Do</option>
<option value="in-progress">In Progress</option>
<option value="completed">Completed</option>
</select>
<select id="sort-by">
<option value="dueDate">Due Date</option>
<option value="priority">Priority</option>
<option value="title">Title</option>
</select>
</div>
</div>
<div class="task-form">
<input type="text" id="task-title" placeholder="New task title">
<textarea id="task-description" placeholder="Description"></textarea>
<div class="form-row">
<select id="task-priority">
<option value="low">Low Priority</option>
<option value="medium">Medium Priority</option>
<option value="high">High Priority</option>
</select>
<input type="date" id="task-due-date">
</div>
<button id="add-task-btn">Add Task</button>
</div>
<ul id="tasks-list" class="tasks-list"></ul>
</div>
</div>
</main>
<div id="error-container" class="error-container"></div>
<script type="module" src="js/app.js"></script>
</body>
</html>
Creating the Models
Let's implement our Task and Project classes:
// Task.js
export class Task {
constructor({ id = null, title, description = '', priority = 'medium',
dueDate = null, status = 'todo', projectId = null }) {
this.id = id || this._generateId();
this.title = title;
this.description = description;
this.priority = priority;
this.dueDate = dueDate;
this.status = status;
this.projectId = projectId;
this.createdAt = new Date().toISOString();
}
_generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
update(updates) {
Object.assign(this, updates);
return this;
}
toJSON() {
return {
id: this.id,
title: this.title,
description: this.description,
priority: this.priority,
dueDate: this.dueDate,
status: this.status,
projectId: this.projectId,
createdAt: this.createdAt
};
}
}
// Project.js
export class Project {
constructor({ id = null, name, color = '#3498db' }) {
this.id = id || this._generateId();
this.name = name;
this.color = color;
this.createdAt = new Date().toISOString();
}
_generateId() {
return 'p_' + Date.now().toString(36) + Math.random().toString(36).substr(2);
}
update(updates) {
Object.assign(this, updates);
return this;
}
toJSON() {
return {
id: this.id,
name: this.name,
color: this.color,
createdAt: this.createdAt
};
}
}
Storage Service Implementation
Let's implement the service for localStorage interactions:
// StorageService.js
export class StorageService {
constructor(storageKey) {
this.storageKey = storageKey;
}
// Save data to localStorage with error handling
save(data) {
try {
const serializedData = JSON.stringify(data);
localStorage.setItem(this.storageKey, serializedData);
return true;
} catch (error) {
console.error(`Failed to save to localStorage: ${error.message}`);
throw new Error(`Storage error: ${error.message}`);
}
}
// Load data from localStorage with error handling
load() {
try {
const serializedData = localStorage.getItem(this.storageKey);
if (!serializedData) return null;
return JSON.parse(serializedData);
} catch (error) {
console.error(`Failed to load from localStorage: ${error.message}`);
throw new Error(`Storage error: ${error.message}`);
}
}
// Clear storage
clear() {
try {
localStorage.removeItem(this.storageKey);
return true;
} catch (error) {
console.error(`Failed to clear localStorage: ${error.message}`);
throw new Error(`Storage error: ${error.message}`);
}
}
}
Custom Error Handling
Let's create custom error types:
// ErrorHandling.js
export class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
export class StorageError extends Error {
constructor(message) {
super(message);
this.name = 'StorageError';
}
}
// Error display utility
export function showError(message, duration = 5000) {
const errorContainer = document.getElementById('error-container');
// Create error element
const errorElement = document.createElement('div');
errorElement.className = 'error-message';
errorElement.textContent = message;
// Add close button
const closeButton = document.createElement('span');
closeButton.textContent = '×';
closeButton.className = 'close-error';
closeButton.addEventListener('click', () => {
errorContainer.removeChild(errorElement);
});
errorElement.appendChild(closeButton);
errorContainer.appendChild(errorElement);
// Auto-remove after duration
setTimeout(() => {
if (errorElement.parentNode === errorContainer) {
errorContainer.removeChild(errorElement);
}
}, duration);
}
Validation Utilities
Let's implement validation for our inputs:
// ValidationUtils.js
import { ValidationError } from './ErrorHandling.js';
export function validateTask(task) {
if (!task.title || task.title.trim() === '') {
throw new ValidationError('Task title is required', 'title');
}
if (task.title.length > 100) {
throw new ValidationError('Task title must be less than 100 characters', 'title');
}
if (task.description && task.description.length > 500) {
throw new ValidationError('Description must be less than 500 characters', 'description');
}
if (task.dueDate && isNaN(new Date(task.dueDate).getTime())) {
throw new ValidationError('Invalid due date format', 'dueDate');
}
const validPriorities = ['low', 'medium', 'high'];
if (task.priority && !validPriorities.includes(task.priority)) {
throw new ValidationError('Invalid priority value', 'priority');
}
const validStatuses = ['todo', 'in-progress', 'completed'];
if (task.status && !validStatuses.includes(task.status)) {
throw new ValidationError('Invalid status value', 'status');
}
return true;
}
export function validateProject(project) {
if (!project.name || project.name.trim() === '') {
throw new ValidationError('Project name is required', 'name');
}
if (project.name.length > 50) {
throw new ValidationError('Project name must be less than 50 characters', 'name');
}
// Validate color is a valid hex color
const hexColorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
if (project.color && !hexColorRegex.test(project.color)) {
throw new ValidationError('Invalid color format', 'color');
}
return true;
}
Task Manager Controller
Let's implement the business logic controller:
// TaskManager.js
import { Task } from '../models/Task.js';
import { Project } from '../models/Project.js';
import { StorageService } from '../services/StorageService.js';
import { validateTask, validateProject } from '../utils/ValidationUtils.js';
import { ValidationError, StorageError } from '../utils/ErrorHandling.js';
export class TaskManager {
constructor() {
this.taskStorage = new StorageService('tasks');
this.projectStorage = new StorageService('projects');
this.tasks = [];
this.projects = [];
this.currentProjectId = null;
// Attempt to load saved data
this._loadData();
}
// Private method to load data from storage
_loadData() {
try {
const savedTasks = this.taskStorage.load();
const savedProjects = this.projectStorage.load();
this.tasks = savedTasks ? savedTasks.map(taskData => new Task(taskData)) : [];
this.projects = savedProjects ? savedProjects.map(projectData => new Project(projectData)) : [];
// Create default project if none exist
if (this.projects.length === 0) {
this.addProject({ name: 'Default Project', color: '#3498db' });
}
} catch (error) {
console.error('Failed to load data:', error);
// Initialize with empty arrays if load fails
this.tasks = [];
this.projects = [];
// Create default project
this.addProject({ name: 'Default Project', color: '#3498db' });
}
}
// Save current state to storage
_saveData() {
try {
this.taskStorage.save(this.tasks);
this.projectStorage.save(this.projects);
return true;
} catch (error) {
console.error('Failed to save data:', error);
throw new StorageError('Failed to save your changes. Please try again.');
}
}
// Task methods with validation and error handling
addTask(taskData) {
try {
validateTask(taskData);
// Set project ID to current project if not specified
if (!taskData.projectId && this.currentProjectId) {
taskData.projectId = this.currentProjectId;
}
const newTask = new Task(taskData);
this.tasks.push(newTask);
this._saveData();
return newTask;
} catch (error) {
if (error instanceof ValidationError) {
throw error;
} else {
console.error('Error adding task:', error);
throw new Error('Failed to add task. Please try again.');
}
}
}
updateTask(taskId, updates) {
try {
const taskIndex = this.tasks.findIndex(task => task.id === taskId);
if (taskIndex === -1) {
throw new Error(`Task with ID ${taskId} not found`);
}
// Create updated task data for validation
const updatedTaskData = { ...this.tasks[taskIndex], ...updates };
validateTask(updatedTaskData);
// Apply updates
this.tasks[taskIndex].update(updates);
this._saveData();
return this.tasks[taskIndex];
} catch (error) {
if (error instanceof ValidationError) {
throw error;
} else {
console.error('Error updating task:', error);
throw new Error('Failed to update task. Please try again.');
}
}
}
deleteTask(taskId) {
try {
const taskIndex = this.tasks.findIndex(task => task.id === taskId);
if (taskIndex === -1) {
throw new Error(`Task with ID ${taskId} not found`);
}
this.tasks.splice(taskIndex, 1);
this._saveData();
return true;
} catch (error) {
console.error('Error deleting task:', error);
throw new Error('Failed to delete task. Please try again.');
}
}
// Project methods
addProject(projectData) {
try {
validateProject(projectData);
const newProject = new Project(projectData);
this.projects.push(newProject);
this._saveData();
return newProject;
} catch (error) {
if (error instanceof ValidationError) {
throw error;
} else {
console.error('Error adding project:', error);
throw new Error('Failed to add project. Please try again.');
}
}
}
updateProject(projectId, updates) {
try {
const projectIndex = this.projects.findIndex(project => project.id === projectId);
if (projectIndex === -1) {
throw new Error(`Project with ID ${projectId} not found`);
}
// Create updated project data for validation
const updatedProjectData = { ...this.projects[projectIndex], ...updates };
validateProject(updatedProjectData);
// Apply updates
this.projects[projectIndex].update(updates);
this._saveData();
return this.projects[projectIndex];
} catch (error) {
if (error instanceof ValidationError) {
throw error;
} else {
console.error('Error updating project:', error);
throw new Error('Failed to update project. Please try again.');
}
}
}
deleteProject(projectId) {
try {
const projectIndex = this.projects.findIndex(project => project.id === projectId);
if (projectIndex === -1) {
throw new Error(`Project with ID ${projectId} not found`);
}
// Check if this is the last project
if (this.projects.length === 1) {
throw new Error('Cannot delete the last project');
}
// Remove project
this.projects.splice(projectIndex, 1);
// Update tasks: either reassign or delete
this.tasks = this.tasks.filter(task => {
if (task.projectId === projectId) {
// Either reassign to another project or delete
// For this example, we'll reassign to the first available project
if (this.projects.length > 0) {
task.projectId = this.projects[0].id;
return true;
} else {
return false; // Delete the task
}
}
return true;
});
// If current project was deleted, select another one
if (this.currentProjectId === projectId) {
this.currentProjectId = this.projects.length > 0 ? this.projects[0].id : null;
}
this._saveData();
return true;
} catch (error) {
console.error('Error deleting project:', error);
throw new Error(error.message || 'Failed to delete project. Please try again.');
}
}
// Set current project for context
setCurrentProject(projectId) {
this.currentProjectId = projectId;
return this.currentProjectId;
}
// Get tasks for the current project, or all tasks if no project selected
getCurrentTasks() {
if (!this.currentProjectId) {
return [...this.tasks]; // Return all tasks
}
return this.tasks.filter(task => task.projectId === this.currentProjectId);
}
// Filter and sort tasks
filterTasks({ projectId = null, status = null, searchTerm = null }) {
let filteredTasks = [...this.tasks];
// Filter by project
if (projectId) {
filteredTasks = filteredTasks.filter(task => task.projectId === projectId);
}
// Filter by status
if (status && status !== 'all') {
filteredTasks = filteredTasks.filter(task => task.status === status);
}
// Filter by search term
if (searchTerm) {
const term = searchTerm.toLowerCase();
filteredTasks = filteredTasks.filter(task =>
task.title.toLowerCase().includes(term) ||
task.description.toLowerCase().includes(term)
);
}
return filteredTasks;
}
sortTasks(tasks, sortBy = 'dueDate', ascending = true) {
const sortedTasks = [...tasks];
sortedTasks.sort((a, b) => {
let comparison = 0;
switch (sortBy) {
case 'dueDate':
// Handle null dates (put them at the end)
if (!a.dueDate && !b.dueDate) return 0;
if (!a.dueDate) return 1;
if (!b.dueDate) return -1;
comparison = new Date(a.dueDate) - new Date(b.dueDate);
break;
case 'priority':
const priorityValues = { high: 3, medium: 2, low: 1 };
comparison = priorityValues[a.priority] - priorityValues[b.priority];
break;
case 'title':
comparison = a.title.localeCompare(b.title);
break;
case 'status':
const statusValues = { 'todo': 1, 'in-progress': 2, 'completed': 3 };
comparison = statusValues[a.status] - statusValues[b.status];
break;
default:
comparison = 0;
}
return ascending ? comparison : -comparison;
});
return sortedTasks;
}
// Get project by ID
getProject(projectId) {
return this.projects.find(project => project.id === projectId);
}
// Get task by ID
getTask(taskId) {
return this.tasks.find(task => task.id === taskId);
}
}
UI Views
Let's implement the views for rendering tasks and projects:
// TaskView.js
export class TaskView {
constructor(taskManager) {
this.taskManager = taskManager;
this.tasksList = document.getElementById('tasks-list');
this.addTaskBtn = document.getElementById('add-task-btn');
this.taskTitleInput = document.getElementById('task-title');
this.taskDescriptionInput = document.getElementById('task-description');
this.taskPrioritySelect = document.getElementById('task-priority');
this.taskDueDateInput = document.getElementById('task-due-date');
this.filterStatusSelect = document.getElementById('filter-status');
this.sortBySelect = document.getElementById('sort-by');
this.currentFilter = { status: 'all' };
this.currentSortBy = 'dueDate';
// Initialize event listeners
this._initEventListeners();
}
_initEventListeners() {
// Add task
this.addTaskBtn.addEventListener('click', () => this._handleAddTask());
// Filter tasks
this.filterStatusSelect.addEventListener('change', () => {
this.currentFilter.status = this.filterStatusSelect.value;
this.renderTasks();
});
// Sort tasks
this.sortBySelect.addEventListener('change', () => {
this.currentSortBy = this.sortBySelect.value;
this.renderTasks();
});
// Delegate clicks for edit, delete, and status changes
this.tasksList.addEventListener('click', (event) => {
const taskElement = event.target.closest('.task-item');
if (!taskElement) return;
const taskId = taskElement.dataset.id;
if (event.target.classList.contains('delete-task')) {
this._handleDeleteTask(taskId);
} else if (event.target.classList.contains('edit-task')) {
this._handleEditTask(taskId);
} else if (event.target.classList.contains('status-toggle')) {
this._handleStatusToggle(taskId);
}
});
}
_handleAddTask() {
const taskData = {
title: this.taskTitleInput.value,
description: this.taskDescriptionInput.value,
priority: this.taskPrioritySelect.value,
dueDate: this.taskDueDateInput.value || null,
status: 'todo'
};
try {
this.taskManager.addTask(taskData);
this._resetForm();
this.renderTasks();
} catch (error) {
alert(error.message);
}
}
_handleDeleteTask(taskId) {
if (confirm('Are you sure you want to delete this task?')) {
try {
this.taskManager.deleteTask(taskId);
this.renderTasks();
} catch (error) {
alert(error.message);
}
}
}
_handleEditTask(taskId) {
const task = this.taskManager.getTask(taskId);
if (!task) return;
// Simple prompt-based editing for this example
// In a real app, you might use a modal form
const newTitle = prompt('Edit task title:', task.title);
if (newTitle === null) return; // User cancelled
try {
this.taskManager.updateTask(taskId, { title: newTitle });
this.renderTasks();
} catch (error) {
alert(error.message);
}
}
_handleStatusToggle(taskId) {
const task = this.taskManager.getTask(taskId);
if (!task) return;
// Cycle through statuses: todo -> in-progress -> completed -> todo
let newStatus;
switch (task.status) {
case 'todo':
newStatus = 'in-progress';
break;
case 'in-progress':
newStatus = 'completed';
break;
case 'completed':
newStatus = 'todo';
break;
default:
newStatus = 'todo';
}
try {
this.taskManager.updateTask(taskId, { status: newStatus });
this.renderTasks();
} catch (error) {
alert(error.message);
}
}
_resetForm() {
this.taskTitleInput.value = '';
this.taskDescriptionInput.value = '';
this.taskPrioritySelect.value = 'medium';
this.taskDueDateInput.value = '';
}
renderTasks() {
// Get tasks based on current filters
let tasks = this.taskManager.getCurrentTasks();
// Apply filter
if (this.currentFilter.status && this.currentFilter.status !== 'all') {
tasks = tasks.filter(task => task.status === this.currentFilter.status);
}
// Apply sorting
tasks = this.taskManager.sortTasks(tasks, this.currentSortBy);
// Clear current list
this.tasksList.innerHTML = '';
// Render each task
tasks.forEach(task => {
const taskElement = this._createTaskElement(task);
this.tasksList.appendChild(taskElement);
});
// Show message if no tasks
if (tasks.length === 0) {
const emptyMessage = document.createElement('li');
emptyMessage.className = 'empty-message';
emptyMessage.textContent = 'No tasks found. Create a new task!';
this.tasksList.appendChild(emptyMessage);
}
}
_createTaskElement(task) {
const taskElement = document.createElement('li');
taskElement.className = `task-item priority-${task.priority} status-${task.status}`;
taskElement.dataset.id = task.id;
// Get project color
let projectColor = '#ccc';
if (task.projectId) {
const project = this.taskManager.getProject(task.projectId);
if (project) {
projectColor = project.color;
}
}
const dueDateFormatted = task.dueDate
? new Date(task.dueDate).toLocaleDateString()
: 'No due date';
taskElement.innerHTML = `
<div class="task-header">
<div class="task-title">${task.title}</div>
<div class="task-actions">
<button class="edit-task">Edit</button>
<button class="delete-task">Delete</button>
</div>
</div>
<div class="task-description">${task.description || 'No description'}</div>
<div class="task-details">
<span class="task-priority">${task.priority}</span>
<span class="task-due-date">${dueDateFormatted}</span>
<button class="status-toggle status-${task.status}">
${task.status.replace('-', ' ')}
</button>
</div>
<div class="task-project" style="background-color: ${projectColor}">
${this.taskManager.getProject(task.projectId)?.name || 'No project'}
</div>
`;
return taskElement;
}
updateProjectContext(projectId) {
const projectName = projectId
? this.taskManager.getProject(projectId)?.name || 'Unknown Project'
: 'All Tasks';
document.getElementById('current-project-name').textContent = projectName;
}
}
// ProjectView.js
export class ProjectView {
constructor(taskManager, taskView) {
this.taskManager = taskManager;
this.taskView = taskView;
this.projectsList = document.getElementById('projects-list');
this.addProjectBtn = document.getElementById('add-project-btn');
this.projectNameInput = document.getElementById('project-name');
this.projectColorInput = document.getElementById('project-color');
// Initialize event listeners
this._initEventListeners();
}
_initEventListeners() {
// Add project
this.addProjectBtn.addEventListener('click', () => this._handleAddProject());
// Delegate clicks for select, edit, and delete
this.projectsList.addEventListener('click', (event) => {
const projectElement = event.target.closest('.project-item');
if (!projectElement) return;
const projectId = projectElement.dataset.id;
if (event.target.classList.contains('delete-project')) {
this._handleDeleteProject(projectId);
} else if (event.target.classList.contains('edit-project')) {
this._handleEditProject(projectId);
} else {
// Select project
this._handleSelectProject(projectId);
}
});
}
_handleAddProject() {
const projectData = {
name: this.projectNameInput.value,
color: this.projectColorInput.value
};
try {
this.taskManager.addProject(projectData);
this._resetForm();
this.renderProjects();
} catch (error) {
alert(error.message);
}
}
_handleDeleteProject(projectId) {
if (confirm('Are you sure you want to delete this project? Tasks will be moved to another project.')) {
try {
this.taskManager.deleteProject(projectId);
this.renderProjects();
this.taskView.renderTasks();
// Update project name display
this.taskView.updateProjectContext(this.taskManager.currentProjectId);
} catch (error) {
alert(error.message);
}
}
}
_handleEditProject(projectId) {
const project = this.taskManager.getProject(projectId);
if (!project) return;
// Simple prompt-based editing for this example
const newName = prompt('Edit project name:', project.name);
if (newName === null) return; // User cancelled
try {
this.taskManager.updateProject(projectId, { name: newName });
this.renderProjects();
this.taskView.renderTasks();
} catch (error) {
alert(error.message);
}
}
_handleSelectProject(projectId) {
this.taskManager.setCurrentProject(projectId);
// Update active project in UI
const projectItems = this.projectsList.querySelectorAll('.project-item');
projectItems.forEach(item => {
item.classList.toggle('active', item.dataset.id === projectId);
});
// Update project name display
this.taskView.updateProjectContext(projectId);
// Update tasks list
this.taskView.renderTasks();
}
_resetForm() {
this.projectNameInput.value = '';
// Don't reset the color input, keep the last color
}
renderProjects() {
// Clear current list
this.projectsList.innerHTML = '';
// Add "All Projects" option
const allProjectsItem = document.createElement('li');
allProjectsItem.className = 'project-item all-projects';
allProjectsItem.classList.toggle('active', this.taskManager.currentProjectId === null);
allProjectsItem.innerHTML = `<span class="project-name">All Projects</span>`;
allProjectsItem.addEventListener('click', () => {
this.taskManager.setCurrentProject(null);
// Update active project in UI
const projectItems = this.projectsList.querySelectorAll('.project-item');
projectItems.forEach(item => {
item.classList.remove('active');
});
allProjectsItem.classList.add('active');
// Update project name display
this.taskView.updateProjectContext(null);
// Update tasks list
this.taskView.renderTasks();
});
this.projectsList.appendChild(allProjectsItem);
// Render each project
this.taskManager.projects.forEach(project => {
const projectElement = this._createProjectElement(project);
this.projectsList.appendChild(projectElement);
});
}
_createProjectElement(project) {
const projectElement = document.createElement('li');
projectElement.className = 'project-item';
projectElement.dataset.id = project.id;
projectElement.classList.toggle('active', project.id === this.taskManager.currentProjectId);
projectElement.innerHTML = `
<div class="project-color" style="background-color: ${project.color}"></div>
<span class="project-name">${project.name}</span>
<div class="project-actions">
<button class="edit-project">Edit</button>
<button class="delete-project">Delete</button>
</div>
`;
return projectElement;
}
}
Main Application Script
Let's connect everything together in our main app.js file:
// app.js
import { TaskManager } from './controllers/TaskManager.js';
import { TaskView } from './views/TaskView.js';
import { ProjectView } from './views/ProjectView.js';
import { showError } from './utils/ErrorHandling.js';
document.addEventListener('DOMContentLoaded', () => {
try {
// Initialize our application
const taskManager = new TaskManager();
// Initialize views
const taskView = new TaskView(taskManager);
const projectView = new ProjectView(taskManager, taskView);
// Render initial UI
projectView.renderProjects();
taskView.renderTasks();
// Catch any uncaught errors
window.addEventListener('error', (event) => {
console.error('Global error:', event.error);
showError(`An unexpected error occurred: ${event.error.message}`);
event.preventDefault();
});
console.log('Task Manager initialized successfully!');
} catch (error) {
console.error('Failed to initialize application:', error);
showError(`Failed to initialize application: ${error.message}`);
}
});
Basic CSS
Let's add some basic styling:
/* style.css */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f5f5;
padding: 0;
margin: 0;
}
header {
background-color: #2c3e50;
color: white;
padding: 1rem;
text-align: center;
}
.container {
display: flex;
max-width: 1200px;
margin: 2rem auto;
padding: 0 1rem;
}
.projects-section {
flex: 0 0 250px;
margin-right: 2rem;
background-color: white;
border-radius: 5px;
padding: 1rem;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.tasks-section {
flex: 1;
background-color: white;
border-radius: 5px;
padding: 1rem;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
h2 {
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #eee;
}
/* Form Styles */
.project-form, .task-form {
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid #eee;
}
input, textarea, select, button {
display: block;
width: 100%;
padding: 0.5rem;
margin-bottom: 0.5rem;
border: 1px solid #ddd;
border-radius: 3px;
font-family: inherit;
}
button {
background-color: #3498db;
color: white;
border: none;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #2980b9;
}
.form-row {
display: flex;
gap: 0.5rem;
}
.form-row > * {
flex: 1;
}
/* Project List Styles */
.projects-list {
list-style: none;
}
.project-item {
display: flex;
align-items: center;
padding: 0.5rem;
border-radius: 3px;
margin-bottom: 0.5rem;
cursor: pointer;
transition: background-color 0.3s;
}
.project-item:hover {
background-color: #f5f5f5;
}
.project-item.active {
background-color: #ecf0f1;
font-weight: bold;
}
.project-color {
width: 15px;
height: 15px;
border-radius: 50%;
margin-right: 0.5rem;
}
.project-name {
flex: 1;
}
.project-actions {
display: none;
}
.project-item:hover .project-actions {
display: flex;
gap: 0.25rem;
}
.project-actions button {
padding: 0.25rem;
font-size: 0.75rem;
}
/* Task List Styles */
.tasks-list {
list-style: none;
}
.task-item {
border: 1px solid #eee;
border-radius: 5px;
padding: 1rem;
margin-bottom: 1rem;
position: relative;
}
.task-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.task-title {
font-weight: bold;
font-size: 1.1rem;
}
.task-description {
margin-bottom: 0.5rem;
color: #666;
}
.task-details {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 0.5rem;
}
.task-priority {
font-size: 0.8rem;
padding: 0.2rem 0.5rem;
border-radius: 3px;
background-color: #f0f0f0;
}
.task-due-date {
font-size: 0.8rem;
color: #666;
}
.task-project {
display: inline-block;
font-size: 0.8rem;
padding: 0.2rem 0.5rem;
border-radius: 3px;
color: white;
}
.task-actions {
display: flex;
gap: 0.5rem;
}
.task-actions button {
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
}
.status-toggle {
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
text-transform: capitalize;
}
.status-toggle.status-todo {
background-color: #f39c12;
}
.status-toggle.status-in-progress {
background-color: #3498db;
}
.status-toggle.status-completed {
background-color: #2ecc71;
}
.priority-high .task-priority {
background-color: #e74c3c;
color: white;
}
.priority-medium .task-priority {
background-color: #f39c12;
color: white;
}
.priority-low .task-priority {
background-color: #3498db;
color: white;
}
.empty-message {
text-align: center;
padding: 2rem;
color: #888;
font-style: italic;
}
/* Task Controls */
.task-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.task-controls {
display: flex;
gap: 0.5rem;
}
.task-controls select {
padding: 0.25rem;
margin: 0;
}
/* Error Container */
.error-container {
position: fixed;
bottom: 20px;
right: 20px;
width: 300px;
z-index: 1000;
}
.error-message {
background-color: #e74c3c;
color: white;
padding: 0.75rem;
margin-bottom: 0.5rem;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
position: relative;
}
.close-error {
position: absolute;
top: 5px;
right: 10px;
cursor: pointer;
font-weight: bold;
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.container {
flex-direction: column;
}
.projects-section {
flex: auto;
margin-right: 0;
margin-bottom: 1rem;
}
}
Step 4: Review and Reflect
Now that we've implemented our application, let's review what we've built and reflect on the process.
Functionality Review
Our Task Management Application successfully implements:
- Task and Project creation, updating, and deletion
- Organization of tasks into projects
- Priority and status management for tasks
- Filtering and sorting capabilities
- Data persistence using localStorage
- Error handling and validation
- A responsive user interface
Code Quality Assessment
Let's evaluate how well we implemented advanced JavaScript concepts:
- Classes and OOP: We used proper class definitions for Task, Project, and service objects
- Error Handling: We implemented custom error types and consistent try/catch blocks
- Modern ES6+ Features: We utilized destructuring, template literals, spread operators, etc.
- Functional Programming: We used array methods like map, filter, and sort for data operations
- Closures: We leveraged closures in event handlers and private methods
- Modules: We organized code into separate modules with clear responsibilities
Polya's Problem-Solving Reflection
Looking back at how we applied Polya's four steps:
- Understanding the Problem: We clearly defined the application requirements and identified key data structures and operations.
- Devising a Plan: We planned our architecture, decided on implementation strategies, and structured our code organization.
- Executing the Plan: We methodically implemented each component of the application, focusing on code quality and error handling.
- Reviewing: We're now assessing what we've built, considering its strengths and potential improvements.
Potential Improvements
If we had more time, we could enhance the application with:
- Unit Tests: Implement Jest tests for models and controllers
- Backend Integration: Connect to a real API instead of localStorage
- Advanced UI Features: Drag-and-drop reordering, rich text descriptions
- Task Dependencies: Allow tasks to depend on other tasks
- User Accounts: Add authentication and multi-user support
- Performance Optimization: Implement lazy loading and virtual scrolling for large task lists
Learning Outcomes
Through this project, we've practiced:
- Applying a systematic problem-solving approach to application development
- Designing a modular, maintainable JavaScript application
- Implementing robust error handling and validation
- Using advanced JavaScript features appropriately
- Building a complete application from planning to implementation
Extension Challenges
If you want to further develop this project, here are some extension challenges:
Challenge 1: Data Export and Import
Add functionality to export tasks and projects as JSON and import them back into the application.
Challenge 2: Task Tagging
Implement a tagging system that allows adding multiple tags to tasks and filtering by tags.
Challenge 3: Recurring Tasks
Allow users to set tasks as recurring (daily, weekly, monthly) with automatic creation of new task instances.
Challenge 4: Statistics Dashboard
Create a dashboard that shows completion rates, task distribution by status/priority, and other useful metrics.
Challenge 5: Notifications
Implement browser notifications for tasks that are approaching their due date.
Submission Guidelines
To submit your weekend project:
- Create a GitHub repository with your code
- Include a well-written README.md that explains:
- Project purpose and features
- Technologies and concepts used
- Installation and usage instructions
- Your reflections on applying Polya's problem-solving approach
- Deploy the application using GitHub Pages or another hosting service
- Submit both the repository URL and the live application URL
Your project will be evaluated based on:
- Correct implementation of required features
- Appropriate use of advanced JavaScript concepts
- Code quality, organization, and documentation
- Error handling and user experience
- Thoughtful reflection on the problem-solving process
Resources
Conclusion
This weekend project has given you the opportunity to apply advanced JavaScript concepts to build a complete, functional application. By following George Polya's problem-solving approach, you've learned not just how to write code, but how to systematically tackle complex development tasks.
Remember that becoming a proficient developer is about more than just knowing syntax—it's about developing a methodical approach to understanding problems, planning solutions, implementing code, and reflecting on your work. This iterative process is the foundation of successful software development.
As you continue your journey as a developer, keep applying these principles to increwasingly complex projects, and you'll find that even the most daunting cchallenges become manageable when approached systematically.