Create a complete to-do application with CRUD operations, local storage persistence, advanced filtering, and collaborative features. Master data management and user interface patterns.
Task management applications are among the most popular and practical web applications. They require complex data operations, state management, user interface design, and persistence - making them excellent learning projects for mastering JavaScript development.
In this comprehensive project, you'll build a professional-grade task management application that rivals popular productivity tools. You'll learn advanced concepts like drag-and-drop interfaces, real-time filtering, local storage management, and responsive design patterns.
This project will showcase your ability to handle complex data relationships, create intuitive user interfaces, and implement features that real users depend on for their daily productivity.
A robust task management system requires well-designed data structures and efficient storage strategies.
Core Task Data Model:
1class Task {
2 constructor(data) {
3 this.id = data.id || this.generateId();
4 this.title = data.title || '';
5 this.description = data.description || '';
6 this.priority = data.priority || 'medium'; // low, medium, high, urgent
7 this.status = data.status || 'pending'; // pending, in-progress, completed, archived
8 this.category = data.category || 'general';
9 this.tags = data.tags || [];
10 this.dueDate = data.dueDate ? new Date(data.dueDate) : null;
11 this.createdAt = data.createdAt ? new Date(data.createdAt) : new Date();
12 this.updatedAt = data.updatedAt ? new Date(data.updatedAt) : new Date();
13 this.completedAt = data.completedAt ? new Date(data.completedAt) : null;
14 this.estimatedTime = data.estimatedTime || null; // minutes
15 this.actualTime = data.actualTime || null;
16 this.subtasks = data.subtasks || [];
17 this.attachments = data.attachments || [];
18 this.reminders = data.reminders || [];
19 }
20
21 generateId() {
22 return 'task_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
23 }
24
25 markComplete() {
26 this.status = 'completed';
27 this.completedAt = new Date();
28 this.updatedAt = new Date();
29 }
30
31 addSubtask(subtaskData) {
32 const subtask = new Task({
33 ...subtaskData,
34 parentId: this.id
35 });
36 this.subtasks.push(subtask);
37 this.updatedAt = new Date();
38 return subtask;
39 }
40
41 toJSON() {
42 return {
43 id: this.id,
44 title: this.title,
45 description: this.description,
46 priority: this.priority,
47 status: this.status,
48 category: this.category,
49 tags: this.tags,
50 dueDate: this.dueDate?.toISOString(),
51 createdAt: this.createdAt.toISOString(),
52 updatedAt: this.updatedAt.toISOString(),
53 completedAt: this.completedAt?.toISOString(),
54 estimatedTime: this.estimatedTime,
55 actualTime: this.actualTime,
56 subtasks: this.subtasks.map(st => st.toJSON()),
57 attachments: this.attachments,
58 reminders: this.reminders
59 };
60 }
61}Storage Management System:
1class TaskStorage {
2 constructor() {
3 this.storageKey = 'taskManager_data';
4 this.version = '1.0';
5 this.maxBackups = 5;
6 }
7
8 saveTask(task) {
9 const tasks = this.getAllTasks();
10 const existingIndex = tasks.findIndex(t => t.id === task.id);
11
12 if (existingIndex >= 0) {
13 tasks[existingIndex] = task.toJSON();
14 } else {
15 tasks.push(task.toJSON());
16 }
17
18 this.saveTasks(tasks);
19 this.createBackup();
20 return task;
21 }
22
23 saveTasks(tasks) {
24 const data = {
25 version: this.version,
26 timestamp: new Date().toISOString(),
27 tasks: tasks
28 };
29
30 try {
31 localStorage.setItem(this.storageKey, JSON.stringify(data));
32 return true;
33 } catch (error) {
34 console.error('Storage quota exceeded:', error);
35 this.cleanupOldData();
36 return false;
37 }
38 }
39
40 getAllTasks() {
41 try {
42 const data = JSON.parse(localStorage.getItem(this.storageKey));
43 if (!data || !data.tasks) return [];
44
45 return data.tasks.map(taskData => new Task(taskData));
46 } catch (error) {
47 console.error('Error loading tasks:', error);
48 return [];
49 }
50 }
51
52 deleteTask(taskId) {
53 const tasks = this.getAllTasks().filter(task => task.id !== taskId);
54 this.saveTasks(tasks.map(t => t.toJSON()));
55 }
56
57 createBackup() {
58 const backups = this.getBackups();
59 const currentData = localStorage.getItem(this.storageKey);
60
61 backups.push({
62 timestamp: new Date().toISOString(),
63 data: currentData
64 });
65
66 // Keep only recent backups
67 if (backups.length > this.maxBackups) {
68 backups.splice(0, backups.length - this.maxBackups);
69 }
70
71 localStorage.setItem(this.storageKey + '_backups', JSON.stringify(backups));
72 }
73
74 exportData() {
75 const tasks = this.getAllTasks();
76 const exportData = {
77 version: this.version,
78 exportDate: new Date().toISOString(),
79 taskCount: tasks.length,
80 tasks: tasks.map(t => t.toJSON())
81 };
82
83 return JSON.stringify(exportData, null, 2);
84 }
85
86 importData(jsonData) {
87 try {
88 const data = JSON.parse(jsonData);
89 if (data.tasks && Array.isArray(data.tasks)) {
90 this.saveTasks(data.tasks);
91 return { success: true, imported: data.tasks.length };
92 }
93 return { success: false, error: 'Invalid data format' };
94 } catch (error) {
95 return { success: false, error: error.message };
96 }
97 }
98}This architecture provides robust data management with backup, export, and recovery capabilities.
Creating an engaging task management interface requires sophisticated UI components and smooth interactions.
Task List Component with Virtual Scrolling:
1class TaskListComponent {
2 constructor(containerId, taskManager) {
3 this.container = document.getElementById(containerId);
4 this.taskManager = taskManager;
5 this.currentView = 'all';
6 this.sortBy = 'dueDate';
7 this.sortDirection = 'asc';
8 this.selectedTasks = new Set();
9
10 this.setupVirtualScrolling();
11 this.setupEventListeners();
12 }
13
14 render(tasks = null) {
15 const tasksToRender = tasks || this.taskManager.getFilteredTasks({
16 view: this.currentView,
17 sortBy: this.sortBy,
18 direction: this.sortDirection
19 });
20
21 this.container.innerHTML = '';
22
23 if (tasksToRender.length === 0) {
24 this.renderEmptyState();
25 return;
26 }
27
28 // Group tasks by status or date
29 const groupedTasks = this.groupTasks(tasksToRender);
30
31 Object.entries(groupedTasks).forEach(([group, tasks]) => {
32 const groupElement = this.createGroupElement(group, tasks);
33 this.container.appendChild(groupElement);
34 });
35
36 this.setupDragAndDrop();
37 }
38
39 createTaskElement(task) {
40 const taskEl = document.createElement('div');
41 taskEl.className = `task-item ${task.status} priority-${task.priority}`;
42 taskEl.dataset.taskId = task.id;
43 taskEl.draggable = true;
44
45 const dueDateClass = this.getDueDateClass(task.dueDate);
46
47 taskEl.innerHTML = `
48 <div class="task-content">
49 <div class="task-header">
50 <input type="checkbox" class="task-checkbox" ${task.status === 'completed' ? 'checked' : ''}>
51 <h3 class="task-title ${task.status === 'completed' ? 'completed' : ''}">${task.title}</h3>
52 <div class="task-actions">
53 <button class="btn-icon edit-task" title="Edit Task">
54 <i class="icon-edit"></i>
55 </button>
56 <button class="btn-icon delete-task" title="Delete Task">
57 <i class="icon-delete"></i>
58 </button>
59 </div>
60 </div>
61
62 ${task.description ? `<p class="task-description">${task.description}</p>` : ''}
63
64 <div class="task-meta">
65 <span class="task-category">${task.category}</span>
66 <span class="task-priority priority-${task.priority}">${task.priority}</span>
67 ${task.dueDate ? `<span class="task-due-date ${dueDateClass}">${this.formatDate(task.dueDate)}</span>` : ''}
68 </div>
69
70 ${task.tags.length > 0 ? `
71 <div class="task-tags">
72 ${task.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
73 </div>
74 ` : ''}
75
76 ${task.subtasks.length > 0 ? `
77 <div class="subtasks">
78 <div class="subtasks-summary">
79 ${task.subtasks.filter(st => st.status === 'completed').length}/${task.subtasks.length} subtasks completed
80 </div>
81 </div>
82 ` : ''}
83
84 <div class="task-progress">
85 ${this.renderProgressBar(task)}
86 </div>
87 </div>
88 `;
89
90 this.setupTaskEventListeners(taskEl, task);
91 return taskEl;
92 }
93
94 setupDragAndDrop() {
95 let draggedElement = null;
96 let draggedTask = null;
97
98 this.container.addEventListener('dragstart', (e) => {
99 if (e.target.classList.contains('task-item')) {
100 draggedElement = e.target;
101 draggedTask = this.taskManager.getTask(e.target.dataset.taskId);
102 e.target.style.opacity = '0.5';
103
104 e.dataTransfer.effectAllowed = 'move';
105 e.dataTransfer.setData('text/html', e.target.outerHTML);
106 }
107 });
108
109 this.container.addEventListener('dragover', (e) => {
110 e.preventDefault();
111 e.dataTransfer.dropEffect = 'move';
112
113 const afterElement = this.getDragAfterElement(e.clientY);
114 const draggable = document.querySelector('.dragging');
115
116 if (afterElement == null) {
117 this.container.appendChild(draggedElement);
118 } else {
119 this.container.insertBefore(draggedElement, afterElement);
120 }
121 });
122
123 this.container.addEventListener('drop', (e) => {
124 e.preventDefault();
125
126 if (draggedTask && draggedElement) {
127 // Update task order/status based on drop location
128 this.handleTaskDrop(draggedTask, e);
129 draggedElement.style.opacity = '1';
130 }
131 });
132 }
133
134 handleTaskDrop(task, dropEvent) {
135 const dropTarget = dropEvent.target.closest('.task-group, .status-column');
136
137 if (dropTarget && dropTarget.dataset.status) {
138 const newStatus = dropTarget.dataset.status;
139 if (task.status !== newStatus) {
140 this.taskManager.updateTaskStatus(task.id, newStatus);
141 this.render();
142 }
143 }
144 }
145}Real-time Search and Filter System:
1class TaskFilterSystem {
2 constructor(taskManager, taskListComponent) {
3 this.taskManager = taskManager;
4 this.taskList = taskListComponent;
5 this.filters = {
6 search: '',
7 status: 'all',
8 priority: 'all',
9 category: 'all',
10 dueDate: 'all',
11 tags: []
12 };
13
14 this.setupFilterUI();
15 this.setupRealTimeSearch();
16 }
17
18 setupRealTimeSearch() {
19 const searchInput = document.getElementById('task-search');
20 let searchTimeout;
21
22 searchInput.addEventListener('input', (e) => {
23 clearTimeout(searchTimeout);
24
25 searchTimeout = setTimeout(() => {
26 this.filters.search = e.target.value.trim();
27 this.applyFilters();
28 }, 300); // Debounce search
29 });
30 }
31
32 applyFilters() {
33 const allTasks = this.taskManager.getAllTasks();
34 const filteredTasks = this.filterTasks(allTasks);
35
36 this.taskList.render(filteredTasks);
37 this.updateFilterSummary(filteredTasks.length, allTasks.length);
38 }
39
40 filterTasks(tasks) {
41 return tasks.filter(task => {
42 // Text search
43 if (this.filters.search) {
44 const searchTerm = this.filters.search.toLowerCase();
45 const searchableText = `
46 ${task.title}
47 ${task.description}
48 ${task.tags.join(' ')}
49 ${task.category}
50 `.toLowerCase();
51
52 if (!searchableText.includes(searchTerm)) {
53 return false;
54 }
55 }
56
57 // Status filter
58 if (this.filters.status !== 'all' && task.status !== this.filters.status) {
59 return false;
60 }
61
62 // Priority filter
63 if (this.filters.priority !== 'all' && task.priority !== this.filters.priority) {
64 return false;
65 }
66
67 // Category filter
68 if (this.filters.category !== 'all' && task.category !== this.filters.category) {
69 return false;
70 }
71
72 // Due date filter
73 if (this.filters.dueDate !== 'all') {
74 if (!this.matchesDueDateFilter(task, this.filters.dueDate)) {
75 return false;
76 }
77 }
78
79 // Tags filter
80 if (this.filters.tags.length > 0) {
81 if (!this.filters.tags.some(tag => task.tags.includes(tag))) {
82 return false;
83 }
84 }
85
86 return true;
87 });
88 }
89
90 createAdvancedFilters() {
91 return `
92 <div class="filter-panel">
93 <div class="filter-group">
94 <label>Search</label>
95 <input type="text" id="task-search" placeholder="Search tasks..." class="search-input">
96 </div>
97
98 <div class="filter-group">
99 <label>Status</label>
100 <select id="filter-status" class="filter-select">
101 <option value="all">All Tasks</option>
102 <option value="pending">Pending</option>
103 <option value="in-progress">In Progress</option>
104 <option value="completed">Completed</option>
105 <option value="archived">Archived</option>
106 </select>
107 </div>
108
109 <div class="filter-group">
110 <label>Priority</label>
111 <div class="priority-filters">
112 <button class="filter-btn active" data-priority="all">All</button>
113 <button class="filter-btn" data-priority="low">Low</button>
114 <button class="filter-btn" data-priority="medium">Medium</button>
115 <button class="filter-btn" data-priority="high">High</button>
116 <button class="filter-btn" data-priority="urgent">Urgent</button>
117 </div>
118 </div>
119
120 <div class="filter-group">
121 <label>Due Date</label>
122 <select id="filter-due-date" class="filter-select">
123 <option value="all">Any Time</option>
124 <option value="overdue">Overdue</option>
125 <option value="today">Today</option>
126 <option value="tomorrow">Tomorrow</option>
127 <option value="this-week">This Week</option>
128 <option value="next-week">Next Week</option>
129 <option value="no-date">No Due Date</option>
130 </select>
131 </div>
132
133 <div class="filter-actions">
134 <button id="clear-filters" class="btn-secondary">Clear All</button>
135 <button id="save-filter" class="btn-primary">Save Filter</button>
136 </div>
137 </div>
138 `;
139 }
140}This advanced UI system provides professional-grade interactions with smooth animations, drag-and-drop functionality, and real-time filtering capabilities.
Implementing robust data persistence ensures users never lose their work and can access tasks across sessions.
Advanced Local Storage with Sync:
1class TaskSyncManager {
2 constructor(storage) {
3 this.storage = storage;
4 this.syncQueue = [];
5 this.isOnline = navigator.onLine;
6 this.lastSyncTime = this.getLastSyncTime();
7
8 this.setupOnlineDetection();
9 this.startAutoSave();
10 }
11
12 setupOnlineDetection() {
13 window.addEventListener('online', () => {
14 this.isOnline = true;
15 console.log('Connection restored - syncing data...');
16 this.processSyncQueue();
17 });
18
19 window.addEventListener('offline', () => {
20 this.isOnline = false;
21 console.log('Connection lost - queuing changes...');
22 });
23 }
24
25 startAutoSave() {
26 // Auto-save every 30 seconds
27 setInterval(() => {
28 this.saveToLocal();
29 }, 30000);
30
31 // Save before page unload
32 window.addEventListener('beforeunload', () => {
33 this.saveToLocal();
34 });
35 }
36
37 queueChange(action, taskData) {
38 const change = {
39 id: this.generateChangeId(),
40 timestamp: new Date().toISOString(),
41 action: action, // 'create', 'update', 'delete'
42 taskData: taskData,
43 synced: false
44 };
45
46 this.syncQueue.push(change);
47
48 if (this.isOnline) {
49 this.processSyncQueue();
50 }
51 }
52
53 async processSyncQueue() {
54 const unsynced = this.syncQueue.filter(change => !change.synced);
55
56 for (const change of unsynced) {
57 try {
58 await this.syncChange(change);
59 change.synced = true;
60 } catch (error) {
61 console.error('Sync failed for change:', change.id, error);
62 break; // Stop processing if sync fails
63 }
64 }
65
66 // Clean up synced changes
67 this.syncQueue = this.syncQueue.filter(change => !change.synced);
68 this.saveLastSyncTime();
69 }
70
71 async syncChange(change) {
72 // Simulate API call - replace with actual sync logic
73 return new Promise((resolve, reject) => {
74 setTimeout(() => {
75 if (Math.random() > 0.1) { // 90% success rate
76 resolve(change);
77 } else {
78 reject(new Error('Sync failed'));
79 }
80 }, 100);
81 });
82 }
83
84 saveToLocal() {
85 try {
86 const data = {
87 tasks: this.storage.getAllTasks().map(t => t.toJSON()),
88 syncQueue: this.syncQueue,
89 lastSaved: new Date().toISOString()
90 };
91
92 localStorage.setItem('taskManager_session', JSON.stringify(data));
93 return true;
94 } catch (error) {
95 console.error('Failed to save to local storage:', error);
96 return false;
97 }
98 }
99
100 restoreFromLocal() {
101 try {
102 const data = JSON.parse(localStorage.getItem('taskManager_session'));
103 if (data && data.tasks) {
104 // Restore tasks
105 data.tasks.forEach(taskData => {
106 this.storage.saveTask(new Task(taskData));
107 });
108
109 // Restore sync queue
110 this.syncQueue = data.syncQueue || [];
111
112 return true;
113 }
114 } catch (error) {
115 console.error('Failed to restore from local storage:', error);
116 }
117 return false;
118 }
119}Conflict Resolution System:
1class ConflictResolver {
2 constructor() {
3 this.resolutionStrategies = {
4 'last-write-wins': this.lastWriteWins.bind(this),
5 'user-choice': this.userChoice.bind(this),
6 'merge': this.mergeChanges.bind(this)
7 };
8 }
9
10 async resolveConflict(localTask, remoteTask, strategy = 'user-choice') {
11 const resolver = this.resolutionStrategies[strategy];
12 if (!resolver) {
13 throw new Error(`Unknown conflict resolution strategy: ${strategy}`);
14 }
15
16 return await resolver(localTask, remoteTask);
17 }
18
19 lastWriteWins(localTask, remoteTask) {
20 return localTask.updatedAt > remoteTask.updatedAt ? localTask : remoteTask;
21 }
22
23 async userChoice(localTask, remoteTask) {
24 return new Promise((resolve) => {
25 this.showConflictDialog(localTask, remoteTask, resolve);
26 });
27 }
28
29 showConflictDialog(localTask, remoteTask, callback) {
30 const dialog = document.createElement('div');
31 dialog.className = 'conflict-dialog-overlay';
32 dialog.innerHTML = `
33 <div class="conflict-dialog">
34 <h3>Task Conflict Detected</h3>
35 <p>The task "${localTask.title}" has been modified both locally and remotely.</p>
36
37 <div class="conflict-options">
38 <div class="option local-option">
39 <h4>Your Changes</h4>
40 <div class="task-preview">
41 <strong>${localTask.title}</strong>
42 <p>${localTask.description}</p>
43 <small>Modified: ${this.formatDate(localTask.updatedAt)}</small>
44 </div>
45 <button class="btn-primary choose-local">Use Your Version</button>
46 </div>
47
48 <div class="option remote-option">
49 <h4>Server Changes</h4>
50 <div class="task-preview">
51 <strong>${remoteTask.title}</strong>
52 <p>${remoteTask.description}</p>
53 <small>Modified: ${this.formatDate(remoteTask.updatedAt)}</small>
54 </div>
55 <button class="btn-primary choose-remote">Use Server Version</button>
56 </div>
57 </div>
58
59 <div class="dialog-actions">
60 <button class="btn-secondary merge-changes">Merge Changes</button>
61 </div>
62 </div>
63 `;
64
65 document.body.appendChild(dialog);
66
67 dialog.querySelector('.choose-local').addEventListener('click', () => {
68 document.body.removeChild(dialog);
69 callback(localTask);
70 });
71
72 dialog.querySelector('.choose-remote').addEventListener('click', () => {
73 document.body.removeChild(dialog);
74 callback(remoteTask);
75 });
76
77 dialog.querySelector('.merge-changes').addEventListener('click', () => {
78 const merged = this.mergeChanges(localTask, remoteTask);
79 document.body.removeChild(dialog);
80 callback(merged);
81 });
82 }
83
84 mergeChanges(localTask, remoteTask) {
85 // Smart merge logic
86 const merged = new Task(localTask.toJSON());
87
88 // Use most recent non-empty values
89 if (remoteTask.updatedAt > localTask.updatedAt) {
90 if (remoteTask.title && remoteTask.title !== localTask.title) {
91 merged.title = remoteTask.title;
92 }
93 if (remoteTask.description && remoteTask.description !== localTask.description) {
94 merged.description = remoteTask.description;
95 }
96 }
97
98 // Merge tags and subtasks
99 merged.tags = [...new Set([...localTask.tags, ...remoteTask.tags])];
100
101 // Use latest status and priority
102 const latestTask = remoteTask.updatedAt > localTask.updatedAt ? remoteTask : localTask;
103 merged.status = latestTask.status;
104 merged.priority = latestTask.priority;
105 merged.updatedAt = new Date();
106
107 return merged;
108 }
109}Export and Backup System:
1class TaskBackupManager {
2 constructor(storage) {
3 this.storage = storage;
4 this.setupAutoBackup();
5 }
6
7 setupAutoBackup() {
8 // Create backup every hour
9 setInterval(() => {
10 this.createAutoBackup();
11 }, 3600000);
12 }
13
14 createAutoBackup() {
15 const backup = {
16 timestamp: new Date().toISOString(),
17 version: '1.0',
18 tasks: this.storage.getAllTasks().map(t => t.toJSON()),
19 metadata: {
20 totalTasks: this.storage.getAllTasks().length,
21 completedTasks: this.storage.getAllTasks().filter(t => t.status === 'completed').length,
22 categories: [...new Set(this.storage.getAllTasks().map(t => t.category))]
23 }
24 };
25
26 const backupKey = `backup_${Date.now()}`;
27 try {
28 localStorage.setItem(backupKey, JSON.stringify(backup));
29 this.cleanOldBackups();
30 } catch (error) {
31 console.error('Backup creation failed:', error);
32 }
33 }
34
35 exportToFile() {
36 const data = this.storage.exportData();
37 const blob = new Blob([data], { type: 'application/json' });
38 const url = URL.createObjectURL(blob);
39
40 const link = document.createElement('a');
41 link.href = url;
42 link.download = `tasks-backup-${new Date().toISOString().split('T')[0]}.json`;
43 document.body.appendChild(link);
44 link.click();
45 document.body.removeChild(link);
46
47 URL.revokeObjectURL(url);
48 }
49
50 async importFromFile(file) {
51 try {
52 const text = await file.text();
53 const result = this.storage.importData(text);
54
55 if (result.success) {
56 return {
57 success: true,
58 message: `Successfully imported ${result.imported} tasks`
59 };
60 } else {
61 return {
62 success: false,
63 message: result.error
64 };
65 }
66 } catch (error) {
67 return {
68 success: false,
69 message: 'Failed to read file: ' + error.message
70 };
71 }
72 }
73}This comprehensive persistence system ensures data reliability, conflict resolution, and backup capabilities for professional task management applications.
Your progress is saved in this browser, on this device only.