Weekend Project: Build a Robust JavaScript Application

Applying Advanced Language Features with George Polya's Problem-Solving Approach

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:

flowchart TD A[1. Understand the Problem] --> B[2. Devise a Plan] B --> C[3. Execute the Plan] C --> D[4. Review and Reflect] D -->|Iterate if needed| A

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:

The application should demonstrate these advanced JavaScript concepts:

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

Step 2: Devise a Plan

Step 3: Execute the Plan

Step 4: Review and Reflect

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

Clarifying the Boundaries

Since this is a weekend project, we'll set clear boundaries:

classDiagram class Task { +String id +String title +String description +String priority +Date dueDate +String status +String projectId +create() +update() +delete() } class Project { +String id +String name +String color +create() +update() +delete() +getTasks() } class TaskManager { +Array tasks +Array projects +addTask() +updateTask() +deleteTask() +addProject() +filterTasks() +sortTasks() +saveToLocalStorage() +loadFromLocalStorage() } Task "many" --> "1" Project: belongs to TaskManager -- Task: manages TaskManager -- Project: manages

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:

Implementation Strategy

  1. Set up the project structure and files
  2. Create the data models (Task and Project classes)
  3. Implement the storage service for data persistence
  4. Build the TaskManager controller with core functionality
  5. Create the UI components and event handlers
  6. Add validation and error handling throughout
  7. Implement filtering and sorting features
  8. Add final touches and polish the user experience

Advanced JavaScript Features to Incorporate

Let's plan how we'll showcase advanced JavaScript concepts:

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:

Code Quality Assessment

Let's evaluate how well we implemented advanced JavaScript concepts:

Polya's Problem-Solving Reflection

Looking back at how we applied Polya's four steps:

  1. Understanding the Problem: We clearly defined the application requirements and identified key data structures and operations.
  2. Devising a Plan: We planned our architecture, decided on implementation strategies, and structured our code organization.
  3. Executing the Plan: We methodically implemented each component of the application, focusing on code quality and error handling.
  4. 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:

Learning Outcomes

Through this project, we've practiced:

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:

  1. Create a GitHub repository with your code
  2. 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
  3. Deploy the application using GitHub Pages or another hosting service
  4. Submit both the repository URL and the live application URL

Your project will be evaluated based on:

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.