Create a complete task management application using all JavaScript concepts learned in this course. Build a real-world project with DOM manipulation, local storage, and modern UI patterns.
Congratulations on reaching the capstone lesson of JavaScript Fundamentals! In this project-based lesson, you'll build a complete task management application that brings together every concept you've learned throughout this course.
This isn't just another coding exercise - you'll create a fully functional web application with features like task creation, editing, deletion, filtering, searching, data persistence, and even dark mode theming. This project demonstrates how JavaScript concepts work together in real-world applications.
By the end of this lesson, you'll have a portfolio-worthy project that showcases your JavaScript skills and serves as a foundation for building more complex applications.
Before writing code, successful applications require proper planning and architecture design.
Project Overview: Task Manager Pro
We'll build a comprehensive task management application with these features:
Architecture Components:
1// Main application structure
2class TaskManagerApp {
3 constructor() {
4 this.tasks = [];
5 this.currentFilter = 'all';
6 this.currentSort = 'created';
7 this.currentTheme = 'light';
8 this.storage = new TaskStorage();
9 this.validator = new TaskValidator();
10 this.stats = new TaskStats();
11 this.filter = new TaskFilter();
12 }
13}
14
15// Task model with business logic
16class Task {
17 constructor(data) {
18 this.id = this.generateId();
19 this.title = data.title;
20 this.description = data.description || '';
21 this.priority = data.priority || 'medium';
22 this.category = data.category || 'other';
23 this.dueDate = data.dueDate || null;
24 this.completed = data.completed || false;
25 this.createdAt = new Date().toISOString();
26 this.updatedAt = new Date().toISOString();
27 }
28}Separation of Concerns:
This modular approach makes code maintainable, testable, and extensible.
Create a solid HTML foundation using semantic elements for accessibility and SEO.
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Task Manager Pro</title>
7 <link rel="stylesheet" href="styles.css">
8</head>
9<body data-theme="light">
10 <!-- Header with branding and controls -->
11 <header class="app-header">
12 <div class="container">
13 <h1 class="app-title">
14 <span class="app-icon">✓</span>
15 Task Manager Pro
16 </h1>
17 <div class="app-controls">
18 <button id="theme-toggle" class="btn btn-secondary">🌙</button>
19 <button id="export-data" class="btn btn-secondary">Export</button>
20 <button id="clear-all" class="btn btn-danger">Clear All</button>
21 </div>
22 </div>
23 </header>
24
25 <main class="app-main">
26 <div class="container">
27 <!-- Task input form -->
28 <section class="task-input-section">
29 <h2>Add New Task</h2>
30 <form id="task-form" class="task-form">
31 <div class="form-group">
32 <label for="task-title">Task Title *</label>
33 <input type="text" id="task-title" required maxlength="100">
34 <div class="input-error" id="title-error"></div>
35 </div>
36
37 <div class="form-row">
38 <div class="form-group">
39 <label for="task-priority">Priority</label>
40 <select id="task-priority">
41 <option value="low">Low</option>
42 <option value="medium" selected>Medium</option>
43 <option value="high">High</option>
44 </select>
45 </div>
46
47 <div class="form-group">
48 <label for="task-category">Category</label>
49 <select id="task-category">
50 <option value="personal">Personal</option>
51 <option value="work">Work</option>
52 <option value="shopping">Shopping</option>
53 <option value="health">Health</option>
54 <option value="other">Other</option>
55 </select>
56 </div>
57
58 <div class="form-group">
59 <label for="task-due-date">Due Date</label>
60 <input type="date" id="task-due-date">
61 </div>
62 </div>
63
64 <div class="form-group">
65 <label for="task-description">Description</label>
66 <textarea id="task-description" rows="3"></textarea>
67 </div>
68
69 <button type="submit" class="btn btn-primary">Add Task</button>
70 </form>
71 </section>
72
73 <!-- Statistics dashboard -->
74 <section class="stats-section">
75 <div class="stats-grid">
76 <div class="stat-card">
77 <h3>Total Tasks</h3>
78 <span class="stat-value" id="total-tasks">0</span>
79 </div>
80 <div class="stat-card">
81 <h3>Completed</h3>
82 <span class="stat-value" id="completed-tasks">0</span>
83 </div>
84 <div class="stat-card">
85 <h3>Pending</h3>
86 <span class="stat-value" id="pending-tasks">0</span>
87 </div>
88 <div class="stat-card">
89 <h3>Overdue</h3>
90 <span class="stat-value" id="overdue-tasks">0</span>
91 </div>
92 </div>
93 </section>
94
95 <!-- Filters and search -->
96 <section class="filter-section">
97 <div class="filter-controls">
98 <div class="search-group">
99 <input type="text" id="search-input" placeholder="Search tasks...">
100 </div>
101
102 <div class="filter-group">
103 <label for="filter-status">Status:</label>
104 <select id="filter-status">
105 <option value="all">All Tasks</option>
106 <option value="pending">Pending</option>
107 <option value="completed">Completed</option>
108 <option value="overdue">Overdue</option>
109 </select>
110 </div>
111
112 <div class="filter-group">
113 <label for="filter-priority">Priority:</label>
114 <select id="filter-priority">
115 <option value="all">All Priorities</option>
116 <option value="high">High</option>
117 <option value="medium">Medium</option>
118 <option value="low">Low</option>
119 </select>
120 </div>
121 </div>
122 </section>
123
124 <!-- Task list display -->
125 <section class="task-list-section">
126 <div class="section-header">
127 <h2>Your Tasks</h2>
128 <div class="view-controls">
129 <button id="view-grid" class="view-btn active">Grid</button>
130 <button id="view-list" class="view-btn">List</button>
131 </div>
132 </div>
133
134 <div id="task-list" class="task-list view-grid"></div>
135
136 <div id="no-tasks" class="no-tasks" style="display: none;">
137 <div class="no-tasks-icon">📝</div>
138 <h3>No tasks found</h3>
139 <p>Create your first task to get started!</p>
140 </div>
141 </section>
142 </div>
143 </main>
144</body>
145</html>Semantic Structure Benefits:
Create robust data models with validation and business logic.
1// Task class with full functionality
2class Task {
3 constructor(data) {
4 this.id = this.generateId();
5 this.title = data.title?.trim() || '';
6 this.description = data.description?.trim() || '';
7 this.priority = data.priority || 'medium';
8 this.category = data.category || 'other';
9 this.dueDate = data.dueDate || null;
10 this.completed = data.completed || false;
11 this.createdAt = data.createdAt || new Date().toISOString();
12 this.updatedAt = new Date().toISOString();
13 }
14
15 // Generate unique ID
16 generateId() {
17 return 'task-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
18 }
19
20 // Update task properties
21 update(data) {
22 Object.keys(data).forEach(key => {
23 if (key !== 'id' && key !== 'createdAt') {
24 this[key] = data[key];
25 }
26 });
27 this.updatedAt = new Date().toISOString();
28 }
29
30 // Toggle completion status
31 toggleComplete() {
32 this.completed = !this.completed;
33 this.updatedAt = new Date().toISOString();
34 }
35
36 // Check if task is overdue
37 isOverdue() {
38 if (!this.dueDate || this.completed) return false;
39 return new Date(this.dueDate) < new Date();
40 }
41
42 // Get formatted due date
43 getFormattedDueDate() {
44 if (!this.dueDate) return '';
45 const date = new Date(this.dueDate);
46 const today = new Date();
47 const diffTime = date.getTime() - today.getTime();
48 const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
49
50 if (diffDays === 0) return 'Due Today';
51 if (diffDays === 1) return 'Due Tomorrow';
52 if (diffDays === -1) return 'Due Yesterday';
53 if (diffDays < 0) return `${Math.abs(diffDays)} days overdue`;
54 return `Due in ${diffDays} days`;
55 }
56
57 // Get priority color
58 getPriorityColor() {
59 const colors = {
60 high: '#ef4444',
61 medium: '#f59e0b',
62 low: '#10b981'
63 };
64 return colors[this.priority] || colors.medium;
65 }
66
67 // Validate task data
68 validate() {
69 const errors = [];
70
71 if (!this.title.trim()) {
72 errors.push('Title is required');
73 }
74
75 if (this.title.length > 100) {
76 errors.push('Title must be less than 100 characters');
77 }
78
79 if (this.description.length > 500) {
80 errors.push('Description must be less than 500 characters');
81 }
82
83 if (!['low', 'medium', 'high'].includes(this.priority)) {
84 errors.push('Invalid priority level');
85 }
86
87 if (this.dueDate && new Date(this.dueDate) < new Date('1900-01-01')) {
88 errors.push('Invalid due date');
89 }
90
91 return {
92 isValid: errors.length === 0,
93 errors
94 };
95 }
96
97 // Convert to JSON for storage
98 toJSON() {
99 return {
100 id: this.id,
101 title: this.title,
102 description: this.description,
103 priority: this.priority,
104 category: this.category,
105 dueDate: this.dueDate,
106 completed: this.completed,
107 createdAt: this.createdAt,
108 updatedAt: this.updatedAt
109 };
110 }
111
112 // Create from JSON
113 static fromJSON(data) {
114 return new Task(data);
115 }
116}
117
118// Task collection management
119class TaskCollection {
120 constructor() {
121 this.tasks = [];
122 }
123
124 add(taskData) {
125 const task = new Task(taskData);
126 const validation = task.validate();
127
128 if (!validation.isValid) {
129 throw new Error(`Invalid task data: ${validation.errors.join(', ')}`);
130 }
131
132 this.tasks.push(task);
133 return task;
134 }
135
136 remove(taskId) {
137 const index = this.tasks.findIndex(task => task.id === taskId);
138 if (index === -1) return false;
139
140 this.tasks.splice(index, 1);
141 return true;
142 }
143
144 update(taskId, updateData) {
145 const task = this.tasks.find(task => task.id === taskId);
146 if (!task) return false;
147
148 task.update(updateData);
149 const validation = task.validate();
150
151 if (!validation.isValid) {
152 throw new Error(`Invalid update data: ${validation.errors.join(', ')}`);
153 }
154
155 return task;
156 }
157
158 find(taskId) {
159 return this.tasks.find(task => task.id === taskId);
160 }
161
162 getAll() {
163 return [...this.tasks];
164 }
165
166 clear() {
167 this.tasks = [];
168 }
169}Key Data Management Concepts:
isOverdue() encapsulate domain logicImplement dynamic UI updates with efficient DOM manipulation techniques.
1// UI Manager for DOM operations
2class TaskUI {
3 constructor(taskCollection) {
4 this.taskCollection = taskCollection;
5 this.taskListElement = document.getElementById('task-list');
6 this.noTasksElement = document.getElementById('no-tasks');
7 this.currentView = 'grid';
8 }
9
10 // Render all tasks
11 renderTasks(tasks = null) {
12 const tasksToRender = tasks || this.taskCollection.getAll();
13
14 // Clear current display
15 this.taskListElement.innerHTML = '';
16
17 // Handle empty state
18 if (tasksToRender.length === 0) {
19 this.showEmptyState();
20 return;
21 }
22
23 this.hideEmptyState();
24
25 // Set layout class
26 this.taskListElement.className = `task-list view-${this.currentView}`;
27
28 // Render each task
29 const fragment = document.createDocumentFragment();
30 tasksToRender.forEach(task => {
31 const taskElement = this.createTaskElement(task);
32 fragment.appendChild(taskElement);
33 });
34
35 this.taskListElement.appendChild(fragment);
36 }
37
38 // Create individual task element
39 createTaskElement(task) {
40 const taskDiv = document.createElement('div');
41 taskDiv.className = `task-card ${task.completed ? 'completed' : ''} ${task.isOverdue() ? 'overdue' : ''}`;
42 taskDiv.setAttribute('data-task-id', task.id);
43 taskDiv.setAttribute('aria-label', `Task: ${task.title}`);
44
45 taskDiv.innerHTML = `
46 <div class="task-header">
47 <div class="task-checkbox">
48 <input type="checkbox"
49 id="check-${task.id}"
50 ${task.completed ? 'checked' : ''}
51 aria-label="Mark task as ${task.completed ? 'incomplete' : 'complete'}">
52 <label for="check-${task.id}"></label>
53 </div>
54
55 <div class="task-priority">
56 <span class="priority-badge priority-${task.priority}"
57 style="background-color: ${task.getPriorityColor()}"
58 title="${task.priority} priority">
59 ${task.priority.toUpperCase()}
60 </span>
61 </div>
62
63 <div class="task-actions">
64 <button class="btn-icon edit-task"
65 title="Edit task"
66 aria-label="Edit ${task.title}">✏️</button>
67 <button class="btn-icon delete-task"
68 title="Delete task"
69 aria-label="Delete ${task.title}">🗑️</button>
70 </div>
71 </div>
72
73 <div class="task-content">
74 <h3 class="task-title">${this.escapeHtml(task.title)}</h3>
75 ${task.description ? `
76 <p class="task-description">${this.escapeHtml(task.description)}</p>
77 ` : ''}
78
79 <div class="task-meta">
80 <span class="task-category">
81 <span class="category-icon">${this.getCategoryIcon(task.category)}</span>
82 ${task.category}
83 </span>
84
85 ${task.dueDate ? `
86 <span class="task-due-date ${task.isOverdue() ? 'overdue' : ''}">
87 <span class="due-icon">📅</span>
88 ${task.getFormattedDueDate()}
89 </span>
90 ` : ''}
91 </div>
92 </div>
93 `;
94
95 // Attach event listeners
96 this.attachTaskEventListeners(taskDiv, task);
97
98 return taskDiv;
99 }
100
101 // Attach event listeners to task element
102 attachTaskEventListeners(taskElement, task) {
103 // Checkbox toggle
104 const checkbox = taskElement.querySelector('input[type="checkbox"]');
105 checkbox.addEventListener('change', (e) => {
106 this.handleTaskToggle(task.id, e.target.checked);
107 });
108
109 // Edit button
110 const editBtn = taskElement.querySelector('.edit-task');
111 editBtn.addEventListener('click', (e) => {
112 e.stopPropagation();
113 this.handleTaskEdit(task.id);
114 });
115
116 // Delete button
117 const deleteBtn = taskElement.querySelector('.delete-task');
118 deleteBtn.addEventListener('click', (e) => {
119 e.stopPropagation();
120 this.handleTaskDelete(task.id);
121 });
122
123 // Keyboard accessibility
124 taskElement.addEventListener('keydown', (e) => {
125 if (e.key === 'Enter' || e.key === ' ') {
126 if (e.target === taskElement) {
127 checkbox.click();
128 }
129 }
130 });
131 }
132
133 // Update statistics display
134 updateStatistics() {
135 const tasks = this.taskCollection.getAll();
136 const stats = {
137 total: tasks.length,
138 completed: tasks.filter(t => t.completed).length,
139 pending: tasks.filter(t => !t.completed).length,
140 overdue: tasks.filter(t => t.isOverdue()).length
141 };
142
143 // Update DOM elements
144 document.getElementById('total-tasks').textContent = stats.total;
145 document.getElementById('completed-tasks').textContent = stats.completed;
146 document.getElementById('pending-tasks').textContent = stats.pending;
147 document.getElementById('overdue-tasks').textContent = stats.overdue;
148
149 // Update progress indicators if they exist
150 const progressBar = document.getElementById('progress-bar');
151 if (progressBar && stats.total > 0) {
152 const percentage = Math.round((stats.completed / stats.total) * 100);
153 progressBar.style.width = `${percentage}%`;
154 progressBar.setAttribute('aria-valuenow', percentage);
155 }
156 }
157
158 // Show empty state
159 showEmptyState() {
160 this.taskListElement.style.display = 'none';
161 this.noTasksElement.style.display = 'block';
162 }
163
164 // Hide empty state
165 hideEmptyState() {
166 this.taskListElement.style.display = 'grid';
167 this.noTasksElement.style.display = 'none';
168 }
169
170 // Utility functions
171 escapeHtml(text) {
172 const div = document.createElement('div');
173 div.textContent = text;
174 return div.innerHTML;
175 }
176
177 getCategoryIcon(category) {
178 const icons = {
179 personal: '👤',
180 work: '💼',
181 shopping: '🛒',
182 health: '🏥',
183 other: '📌'
184 };
185 return icons[category] || icons.other;
186 }
187
188 // Event handlers (to be connected to main app)
189 handleTaskToggle(taskId, completed) {
190 // Dispatch custom event for main app to handle
191 document.dispatchEvent(new CustomEvent('taskToggle', {
192 detail: { taskId, completed }
193 }));
194 }
195
196 handleTaskEdit(taskId) {
197 document.dispatchEvent(new CustomEvent('taskEdit', {
198 detail: { taskId }
199 }));
200 }
201
202 handleTaskDelete(taskId) {
203 document.dispatchEvent(new CustomEvent('taskDelete', {
204 detail: { taskId }
205 }));
206 }
207}DOM Manipulation Best Practices:
Implement comprehensive event handling for user interactions and application flow.
1// Event handling system
2class TaskEventHandler {
3 constructor(app) {
4 this.app = app;
5 this.setupEventListeners();
6 }
7
8 // Initialize all event listeners
9 setupEventListeners() {
10 // Form submission
11 document.getElementById('task-form')
12 .addEventListener('submit', this.handleFormSubmit.bind(this));
13
14 // Search input
15 document.getElementById('search-input')
16 .addEventListener('input', this.debounce(this.handleSearch.bind(this), 300));
17
18 // Filter controls
19 document.getElementById('filter-status')
20 .addEventListener('change', this.handleFilterChange.bind(this));
21 document.getElementById('filter-priority')
22 .addEventListener('change', this.handleFilterChange.bind(this));
23
24 // View toggle buttons
25 document.getElementById('view-grid')
26 .addEventListener('click', () => this.handleViewChange('grid'));
27 document.getElementById('view-list')
28 .addEventListener('click', () => this.handleViewChange('list'));
29
30 // Header controls
31 document.getElementById('theme-toggle')
32 .addEventListener('click', this.handleThemeToggle.bind(this));
33 document.getElementById('export-data')
34 .addEventListener('click', this.handleDataExport.bind(this));
35 document.getElementById('clear-all')
36 .addEventListener('click', this.handleClearAll.bind(this));
37
38 // Custom events from UI
39 document.addEventListener('taskToggle', this.handleTaskToggle.bind(this));
40 document.addEventListener('taskEdit', this.handleTaskEdit.bind(this));
41 document.addEventListener('taskDelete', this.handleTaskDelete.bind(this));
42
43 // Keyboard shortcuts
44 document.addEventListener('keydown', this.handleKeyboardShortcuts.bind(this));
45
46 // Form validation
47 this.setupFormValidation();
48 }
49
50 // Handle form submission
51 handleFormSubmit(event) {
52 event.preventDefault();
53
54 const formData = new FormData(event.target);
55 const taskData = {
56 title: formData.get('title')?.trim(),
57 description: formData.get('description')?.trim(),
58 priority: formData.get('priority'),
59 category: formData.get('category'),
60 dueDate: formData.get('dueDate') || null
61 };
62
63 try {
64 // Validate form data
65 if (!this.validateTaskData(taskData)) {
66 return; // Validation errors already displayed
67 }
68
69 // Add task to collection
70 const task = this.app.taskCollection.add(taskData);
71
72 // Update UI
73 this.app.ui.renderTasks();
74 this.app.ui.updateStatistics();
75
76 // Save to storage
77 this.app.storage.saveTasks(this.app.taskCollection.getAll());
78
79 // Show success message
80 this.app.showNotification(`Task "${task.title}" added successfully!`, 'success');
81
82 // Reset form
83 event.target.reset();
84 this.clearFormErrors();
85
86 } catch (error) {
87 this.app.showNotification(`Error adding task: ${error.message}`, 'error');
88 }
89 }
90
91 // Handle search input
92 handleSearch(event) {
93 const searchTerm = event.target.value.toLowerCase().trim();
94 this.app.currentSearch = searchTerm;
95 this.applyFilters();
96 }
97
98 // Handle filter changes
99 handleFilterChange() {
100 this.app.currentFilter = {
101 status: document.getElementById('filter-status').value,
102 priority: document.getElementById('filter-priority').value
103 };
104 this.applyFilters();
105 }
106
107 // Apply current filters and search
108 applyFilters() {
109 let filteredTasks = this.app.taskCollection.getAll();
110
111 // Apply status filter
112 if (this.app.currentFilter.status !== 'all') {
113 filteredTasks = filteredTasks.filter(task => {
114 switch (this.app.currentFilter.status) {
115 case 'completed':
116 return task.completed;
117 case 'pending':
118 return !task.completed;
119 case 'overdue':
120 return task.isOverdue();
121 default:
122 return true;
123 }
124 });
125 }
126
127 // Apply priority filter
128 if (this.app.currentFilter.priority !== 'all') {
129 filteredTasks = filteredTasks.filter(task =>
130 task.priority === this.app.currentFilter.priority
131 );
132 }
133
134 // Apply search filter
135 if (this.app.currentSearch) {
136 filteredTasks = filteredTasks.filter(task =>
137 task.title.toLowerCase().includes(this.app.currentSearch) ||
138 task.description.toLowerCase().includes(this.app.currentSearch)
139 );
140 }
141
142 // Update UI with filtered tasks
143 this.app.ui.renderTasks(filteredTasks);
144 }
145
146 // Handle view changes
147 handleViewChange(view) {
148 this.app.ui.currentView = view;
149
150 // Update button states
151 document.getElementById('view-grid').classList.toggle('active', view === 'grid');
152 document.getElementById('view-list').classList.toggle('active', view === 'list');
153
154 // Re-render with new view
155 this.app.ui.renderTasks();
156
157 // Save preference
158 localStorage.setItem('taskManager_view', view);
159 }
160
161 // Handle task toggle completion
162 handleTaskToggle(event) {
163 const { taskId, completed } = event.detail;
164
165 try {
166 const task = this.app.taskCollection.find(taskId);
167 if (task) {
168 task.completed = completed;
169 task.updatedAt = new Date().toISOString();
170
171 // Update storage
172 this.app.storage.saveTasks(this.app.taskCollection.getAll());
173
174 // Update statistics
175 this.app.ui.updateStatistics();
176
177 // Show feedback
178 const status = completed ? 'completed' : 'marked as pending';
179 this.app.showNotification(`Task "${task.title}" ${status}`, 'info');
180
181 // Re-apply filters (completed tasks might be filtered out)
182 this.applyFilters();
183 }
184 } catch (error) {
185 this.app.showNotification(`Error updating task: ${error.message}`, 'error');
186 }
187 }
188
189 // Handle task deletion
190 handleTaskDelete(event) {
191 const { taskId } = event.detail;
192
193 const task = this.app.taskCollection.find(taskId);
194 if (task && confirm(`Are you sure you want to delete "${task.title}"?`)) {
195 try {
196 this.app.taskCollection.remove(taskId);
197
198 // Update UI and storage
199 this.app.ui.renderTasks();
200 this.app.ui.updateStatistics();
201 this.app.storage.saveTasks(this.app.taskCollection.getAll());
202
203 this.app.showNotification(`Task "${task.title}" deleted`, 'warning');
204
205 } catch (error) {
206 this.app.showNotification(`Error deleting task: ${error.message}`, 'error');
207 }
208 }
209 }
210
211 // Handle keyboard shortcuts
212 handleKeyboardShortcuts(event) {
213 // Ctrl/Cmd + N: Focus on new task input
214 if ((event.ctrlKey || event.metaKey) && event.key === 'n') {
215 event.preventDefault();
216 document.getElementById('task-title').focus();
217 }
218
219 // Escape: Clear search
220 if (event.key === 'Escape') {
221 const searchInput = document.getElementById('search-input');
222 if (searchInput.value) {
223 searchInput.value = '';
224 this.handleSearch({ target: searchInput });
225 }
226 }
227
228 // Ctrl/Cmd + /: Focus search
229 if ((event.ctrlKey || event.metaKey) && event.key === '/') {
230 event.preventDefault();
231 document.getElementById('search-input').focus();
232 }
233 }
234
235 // Setup form validation
236 setupFormValidation() {
237 const titleInput = document.getElementById('task-title');
238
239 titleInput.addEventListener('blur', this.validateTitleField.bind(this));
240 titleInput.addEventListener('input', this.debounce(this.validateTitleField.bind(this), 500));
241 }
242
243 // Validate task data
244 validateTaskData(data) {
245 this.clearFormErrors();
246 let isValid = true;
247
248 // Title validation
249 if (!data.title) {
250 this.showFieldError('title-error', 'Task title is required');
251 isValid = false;
252 } else if (data.title.length > 100) {
253 this.showFieldError('title-error', 'Title must be less than 100 characters');
254 isValid = false;
255 }
256
257 // Description validation
258 if (data.description && data.description.length > 500) {
259 this.showFieldError('description-error', 'Description must be less than 500 characters');
260 isValid = false;
261 }
262
263 return isValid;
264 }
265
266 // Utility functions
267 debounce(func, wait) {
268 let timeout;
269 return function executedFunction(...args) {
270 const later = () => {
271 clearTimeout(timeout);
272 func(...args);
273 };
274 clearTimeout(timeout);
275 timeout = setTimeout(later, wait);
276 };
277 }
278
279 showFieldError(elementId, message) {
280 const errorElement = document.getElementById(elementId);
281 if (errorElement) {
282 errorElement.textContent = message;
283 errorElement.style.display = 'block';
284 }
285 }
286
287 clearFormErrors() {
288 document.querySelectorAll('.input-error').forEach(element => {
289 element.style.display = 'none';
290 element.textContent = '';
291 });
292 }
293}Event Handling Best Practices:
Implement robust data persistence using localStorage with error handling and backup strategies.
1// Storage management system
2class TaskStorage {
3 constructor() {
4 this.storageKey = 'taskManager_tasks';
5 this.backupKey = 'taskManager_backup';
6 this.settingsKey = 'taskManager_settings';
7 }
8
9 // Save tasks to localStorage
10 saveTasks(tasks) {
11 try {
12 // Create backup of current data
13 const currentData = this.loadTasks();
14 if (currentData.length > 0) {
15 this.createBackup(currentData);
16 }
17
18 // Serialize and save new data
19 const serializedTasks = JSON.stringify(tasks.map(task => task.toJSON()));
20 localStorage.setItem(this.storageKey, serializedTasks);
21
22 // Update metadata
23 this.updateMetadata(tasks.length);
24
25 return true;
26
27 } catch (error) {
28 console.error('Failed to save tasks:', error);
29
30 // Try to save to fallback location
31 try {
32 sessionStorage.setItem(this.storageKey, JSON.stringify(tasks));
33 console.warn('Tasks saved to sessionStorage as fallback');
34 return true;
35 } catch (fallbackError) {
36 console.error('Failed to save to sessionStorage:', fallbackError);
37 throw new Error('Unable to save tasks. Storage may be full.');
38 }
39 }
40 }
41
42 // Load tasks from localStorage
43 loadTasks() {
44 try {
45 // Try localStorage first
46 const data = localStorage.getItem(this.storageKey);
47
48 if (!data) {
49 // Try sessionStorage fallback
50 const sessionData = sessionStorage.getItem(this.storageKey);
51 return sessionData ? this.parseTaskData(sessionData) : [];
52 }
53
54 return this.parseTaskData(data);
55
56 } catch (error) {
57 console.error('Failed to load tasks:', error);
58
59 // Try to load from backup
60 try {
61 const backupData = this.loadBackup();
62 if (backupData.length > 0) {
63 console.warn('Loaded tasks from backup');
64 return backupData;
65 }
66 } catch (backupError) {
67 console.error('Failed to load backup:', backupError);
68 }
69
70 return [];
71 }
72 }
73
74 // Parse task data with validation
75 parseTaskData(jsonString) {
76 const parsedData = JSON.parse(jsonString);
77
78 if (!Array.isArray(parsedData)) {
79 throw new Error('Invalid task data format');
80 }
81
82 // Convert to Task instances and validate
83 return parsedData
84 .map(taskData => {
85 try {
86 const task = Task.fromJSON(taskData);
87 const validation = task.validate();
88
89 if (!validation.isValid) {
90 console.warn(`Skipping invalid task: ${validation.errors.join(', ')}`);
91 return null;
92 }
93
94 return task;
95 } catch (error) {
96 console.warn('Skipping malformed task data:', error);
97 return null;
98 }
99 })
100 .filter(task => task !== null);
101 }
102
103 // Create backup of current tasks
104 createBackup(tasks) {
105 try {
106 const backupData = {
107 tasks: tasks,
108 timestamp: new Date().toISOString(),
109 version: '1.0'
110 };
111
112 localStorage.setItem(this.backupKey, JSON.stringify(backupData));
113 } catch (error) {
114 console.warn('Failed to create backup:', error);
115 }
116 }
117
118 // Load backup data
119 loadBackup() {
120 try {
121 const backupJson = localStorage.getItem(this.backupKey);
122 if (!backupJson) return [];
123
124 const backupData = JSON.parse(backupJson);
125 return this.parseTaskData(JSON.stringify(backupData.tasks));
126
127 } catch (error) {
128 console.error('Failed to load backup:', error);
129 return [];
130 }
131 }
132
133 // Export tasks for download
134 exportTasks(tasks) {
135 const exportData = {
136 tasks: tasks.map(task => task.toJSON()),
137 exportDate: new Date().toISOString(),
138 version: '1.0',
139 source: 'Task Manager Pro'
140 };
141
142 return JSON.stringify(exportData, null, 2);
143 }
144
145 // Import tasks from file
146 importTasks(jsonString) {
147 try {
148 const importData = JSON.parse(jsonString);
149
150 // Validate import structure
151 if (!importData.tasks || !Array.isArray(importData.tasks)) {
152 throw new Error('Invalid import file format');
153 }
154
155 // Parse and validate tasks
156 const tasks = this.parseTaskData(JSON.stringify(importData.tasks));
157
158 return {
159 success: true,
160 tasks: tasks,
161 count: tasks.length,
162 source: importData.source || 'Unknown'
163 };
164
165 } catch (error) {
166 return {
167 success: false,
168 error: error.message
169 };
170 }
171 }
172
173 // Clear all task data
174 clearTasks() {
175 try {
176 // Create backup before clearing
177 const currentTasks = this.loadTasks();
178 if (currentTasks.length > 0) {
179 this.createBackup(currentTasks);
180 }
181
182 localStorage.removeItem(this.storageKey);
183 sessionStorage.removeItem(this.storageKey);
184
185 return true;
186 } catch (error) {
187 console.error('Failed to clear tasks:', error);
188 return false;
189 }
190 }
191
192 // Save application settings
193 saveSettings(settings) {
194 try {
195 localStorage.setItem(this.settingsKey, JSON.stringify(settings));
196 return true;
197 } catch (error) {
198 console.warn('Failed to save settings:', error);
199 return false;
200 }
201 }
202
203 // Load application settings
204 loadSettings() {
205 try {
206 const settingsJson = localStorage.getItem(this.settingsKey);
207 return settingsJson ? JSON.parse(settingsJson) : this.getDefaultSettings();
208 } catch (error) {
209 console.warn('Failed to load settings, using defaults:', error);
210 return this.getDefaultSettings();
211 }
212 }
213
214 // Get default settings
215 getDefaultSettings() {
216 return {
217 theme: 'light',
218 view: 'grid',
219 sortBy: 'created',
220 autoSave: true,
221 notifications: true
222 };
223 }
224
225 // Update metadata
226 updateMetadata(taskCount) {
227 try {
228 const metadata = {
229 lastSaved: new Date().toISOString(),
230 taskCount: taskCount,
231 version: '1.0'
232 };
233
234 localStorage.setItem('taskManager_metadata', JSON.stringify(metadata));
235 } catch (error) {
236 console.warn('Failed to save metadata:', error);
237 }
238 }
239
240 // Get storage usage info
241 getStorageInfo() {
242 try {
243 const tasks = localStorage.getItem(this.storageKey) || '';
244 const backup = localStorage.getItem(this.backupKey) || '';
245 const settings = localStorage.getItem(this.settingsKey) || '';
246
247 return {
248 tasksSize: new Blob([tasks]).size,
249 backupSize: new Blob([backup]).size,
250 settingsSize: new Blob([settings]).size,
251 totalSize: new Blob([tasks + backup + settings]).size,
252 quota: this.getStorageQuota()
253 };
254 } catch (error) {
255 return { error: error.message };
256 }
257 }
258
259 // Estimate storage quota
260 getStorageQuota() {
261 try {
262 // Test storage limit
263 let testKey = 'test_storage_quota';
264 let size = 0;
265 let testValue = 'a'.repeat(1024); // 1KB chunks
266
267 try {
268 while (size < 10 * 1024 * 1024) { // Test up to 10MB
269 localStorage.setItem(testKey + size, testValue);
270 size += 1024;
271 }
272 } catch (error) {
273 // Clean up test data
274 for (let i = 0; i < size; i += 1024) {
275 localStorage.removeItem(testKey + i);
276 }
277 return size;
278 }
279
280 return 'Unknown';
281 } catch (error) {
282 return 'Unknown';
283 }
284 }
285}Storage Best Practices:
🎉 Congratulations on completing JavaScript Fundamentals!
You've built a complete, functional web application that demonstrates mastery of:
Your Next Learning Journey:
You now have the foundation to build any JavaScript application. Keep coding, keep learning, and most importantly - keep building amazing things! 🚀
Your progress is saved in this browser, on this device only.