Download app.js from Haris-83/taskmaster-pro-advanced: direct link, hf CLI and curl.
- Browser
- Download file 63.7 kB
-
https://huggingface.co/spaces/Haris-83/taskmaster-pro-advanced/resolve/main/app.js
- Command line
-
hf download hf://spaces/Haris-83/taskmaster-pro-advanced/app.js
-
curl -L -o app.js https://huggingface.co/spaces/Haris-83/taskmaster-pro-advanced/resolve/main/app.js
63.7 kB
| // ===== Application State ===== | |
| let tasks = []; | |
| let categories = []; | |
| let workspaces = []; | |
| let currentView = 'all'; | |
| let currentFilter = { priority: 'all', workspace: null, date: null, search: '' }; | |
| let currentSort = 'date-desc'; | |
| let editingTaskId = null; | |
| let viewMode = 'list'; | |
| let sortableInstance = null; | |
| let lastDeletedTask = null; | |
| let focusModeEnabled = false; | |
| let smartFilter = 'all'; | |
| let calendarMonth = new Date(); | |
| let calendarViewVisible = false; | |
| let streakCount = 0; | |
| let myDayTasks = []; | |
| // Pomodoro | |
| let pomodoroRunning = false; | |
| let pomodoroTimer = null; | |
| let pomodoroTime = 25 * 60; | |
| let pomodoroPhase = 'focus'; | |
| let pomodoroSessions = 0; | |
| let pomodoroTodayMinutes = 0; | |
| // ===== Initialize ===== | |
| document.addEventListener('DOMContentLoaded', () => { | |
| setTimeout(() => { | |
| document.getElementById('loading-screen').classList.add('hidden'); | |
| loadFromLocalStorage(); | |
| initializeEventListeners(); | |
| setQuickAddDefaults(); | |
| renderTasks(); | |
| renderCategories(); | |
| renderWorkspaces(); | |
| updateWorkspaceSelect(); | |
| updateCounts(); | |
| updateProgress(); | |
| updateInsights(); | |
| applySettings(); | |
| checkDarkMode(); | |
| updateUserStats(); | |
| loadStreak(); | |
| updatePomodoroDisplay(); | |
| initParticles(); | |
| }, 1200); | |
| }); | |
| // ===== Local Storage ===== | |
| function saveToLocalStorage() { | |
| localStorage.setItem('tasks', JSON.stringify(tasks)); | |
| localStorage.setItem('categories', JSON.stringify(categories)); | |
| localStorage.setItem('workspaces', JSON.stringify(workspaces)); | |
| localStorage.setItem('pomodoroSessions', pomodoroSessions.toString()); | |
| localStorage.setItem('pomodoroTodayMinutes', pomodoroTodayMinutes.toString()); | |
| } | |
| function loadFromLocalStorage() { | |
| try { | |
| tasks = JSON.parse(localStorage.getItem('tasks')) || []; | |
| categories = JSON.parse(localStorage.getItem('categories')) || []; | |
| workspaces = JSON.parse(localStorage.getItem('workspaces')) || []; | |
| pomodoroSessions = parseInt(localStorage.getItem('pomodoroSessions')) || 0; | |
| pomodoroTodayMinutes = parseInt(localStorage.getItem('pomodoroTodayMinutes')) || 0; | |
| const savedUserName = localStorage.getItem('userName'); | |
| if (savedUserName) { | |
| document.getElementById('user-name').textContent = savedUserName; | |
| document.getElementById('user-name-input').value = savedUserName; | |
| } | |
| } catch (e) { | |
| tasks = []; | |
| categories = []; | |
| workspaces = []; | |
| } | |
| } | |
| // ===== Streak ===== | |
| function loadStreak() { | |
| const lastDate = localStorage.getItem('lastCompleteDate'); | |
| const today = new Date().toISOString().split('T')[0]; | |
| if (lastDate === today) { | |
| streakCount = parseInt(localStorage.getItem('streakCount')) || 0; | |
| } else if (lastDate === getYesterday()) { | |
| streakCount = parseInt(localStorage.getItem('streakCount')) || 0; | |
| } else { | |
| streakCount = 0; | |
| localStorage.setItem('streakCount', '0'); | |
| } | |
| updateStreakDisplay(); | |
| } | |
| function getYesterday() { | |
| const d = new Date(); | |
| d.setDate(d.getDate() - 1); | |
| return d.toISOString().split('T')[0]; | |
| } | |
| function updateStreakDisplay() { | |
| const existing = document.querySelector('.streak-badge'); | |
| if (existing) existing.remove(); | |
| if (streakCount > 0) { | |
| const badge = document.createElement('span'); | |
| badge.className = 'streak-badge'; | |
| badge.innerHTML = `<i class="fas fa-fire"></i> ${streakCount} day${streakCount > 1 ? 's' : ''}`; | |
| document.querySelector('.user-info h3').after(badge); | |
| } | |
| } | |
| function checkStreak() { | |
| const today = new Date().toISOString().split('T')[0]; | |
| const lastDate = localStorage.getItem('lastCompleteDate'); | |
| if (lastDate === today) return; | |
| if (lastDate === getYesterday()) { | |
| streakCount++; | |
| } else { | |
| streakCount = 1; | |
| } | |
| localStorage.setItem('lastCompleteDate', today); | |
| localStorage.setItem('streakCount', streakCount.toString()); | |
| updateStreakDisplay(); | |
| } | |
| // ===== My Day ===== | |
| function addToMyDay(taskId) { | |
| const task = tasks.find(t => t.id === taskId); | |
| if (!task) return; | |
| if (!myDayTasks.includes(taskId)) { | |
| myDayTasks.push(taskId); | |
| localStorage.setItem('myDayTasks', JSON.stringify(myDayTasks)); | |
| showToast('Added to My Day', 'success'); | |
| } | |
| } | |
| function loadMyDay() { | |
| try { | |
| myDayTasks = JSON.parse(localStorage.getItem('myDayTasks')) || []; | |
| } catch { myDayTasks = []; } | |
| } | |
| // ===== Event Listeners ===== | |
| function initializeEventListeners() { | |
| document.querySelectorAll('.nav-item').forEach(item => { | |
| item.addEventListener('click', (e) => { | |
| switchView(e.currentTarget.dataset.view); | |
| }); | |
| }); | |
| document.getElementById('add-task-btn').addEventListener('click', () => openTaskModal()); | |
| document.getElementById('close-modal').addEventListener('click', closeTaskModal); | |
| document.getElementById('cancel-btn').addEventListener('click', closeTaskModal); | |
| document.getElementById('task-form').addEventListener('submit', saveTask); | |
| document.getElementById('task-recurring').addEventListener('change', (e) => { | |
| document.getElementById('recurring-options').style.display = e.target.checked ? 'block' : 'none'; | |
| }); | |
| document.getElementById('add-category-btn').addEventListener('click', openCategoryModal); | |
| document.getElementById('add-workspace-btn').addEventListener('click', openWorkspaceModal); | |
| document.getElementById('close-workspace-modal').addEventListener('click', closeWorkspaceModal); | |
| document.getElementById('cancel-workspace-btn').addEventListener('click', closeWorkspaceModal); | |
| document.getElementById('workspace-form').addEventListener('submit', saveWorkspace); | |
| document.getElementById('close-category-modal').addEventListener('click', closeCategoryModal); | |
| document.getElementById('cancel-category-btn').addEventListener('click', closeCategoryModal); | |
| document.getElementById('category-form').addEventListener('submit', saveCategory); | |
| document.querySelectorAll('.color-option').forEach(option => { | |
| option.addEventListener('click', function() { | |
| document.querySelectorAll('.color-option').forEach(o => o.classList.remove('active')); | |
| this.classList.add('active'); | |
| }); | |
| }); | |
| document.getElementById('settings-btn').addEventListener('click', openSettingsModal); | |
| document.getElementById('close-settings-modal').addEventListener('click', closeSettingsModal); | |
| document.getElementById('pomodoro-toggle-btn').addEventListener('click', togglePomodoroPanel); | |
| document.getElementById('pomodoro-start').addEventListener('click', togglePomodoro); | |
| document.getElementById('pomodoro-reset').addEventListener('click', resetPomodoro); | |
| document.getElementById('pomodoro-skip').addEventListener('click', skipPomodoro); | |
| document.getElementById('focus-mode-btn').addEventListener('click', toggleFocusMode); | |
| document.getElementById('template-btn').addEventListener('click', openTemplateModal); | |
| document.getElementById('close-template-modal').addEventListener('click', closeTemplateModal); | |
| document.getElementById('smart-filter-btn').addEventListener('click', cycleSmartFilter); | |
| document.getElementById('stats-btn').addEventListener('click', openStatsModal); | |
| document.getElementById('close-stats-modal').addEventListener('click', closeStatsModal); | |
| document.getElementById('shortcuts-btn').addEventListener('click', openShortcutsModal); | |
| document.getElementById('close-shortcuts-modal').addEventListener('click', closeShortcutsModal); | |
| document.getElementById('user-name-input').addEventListener('change', (e) => { | |
| const name = e.target.value.trim() || 'Welcome Back!'; | |
| document.getElementById('user-name').textContent = name; | |
| localStorage.setItem('userName', name); | |
| }); | |
| document.getElementById('search-input').addEventListener('input', handleSearch); | |
| document.getElementById('quick-add-form').addEventListener('submit', handleQuickAddSubmit); | |
| document.getElementById('complete-visible-btn').addEventListener('click', completeVisibleTasks); | |
| document.getElementById('clear-completed-btn').addEventListener('click', clearCompletedTasks); | |
| document.getElementById('filter-btn').addEventListener('click', toggleFilterPanel); | |
| document.getElementById('view-toggle').addEventListener('click', toggleViewMode); | |
| document.getElementById('calendar-toggle').addEventListener('click', toggleCalendarView); | |
| document.getElementById('calendar-prev').addEventListener('click', () => { | |
| calendarMonth = new Date(calendarMonth.getFullYear(), calendarMonth.getMonth() - 1, 1); | |
| renderCalendar(); | |
| }); | |
| document.getElementById('calendar-next').addEventListener('click', () => { | |
| calendarMonth = new Date(calendarMonth.getFullYear(), calendarMonth.getMonth() + 1, 1); | |
| renderCalendar(); | |
| }); | |
| document.querySelectorAll('.filter-option').forEach(option => { | |
| option.addEventListener('click', handleFilter); | |
| }); | |
| document.querySelectorAll('.sort-option').forEach(option => { | |
| option.addEventListener('click', handleSort); | |
| }); | |
| document.getElementById('add-subtask-btn').addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| addSubtaskInput(); | |
| }); | |
| document.getElementById('theme-toggle').addEventListener('click', toggleDarkMode); | |
| document.getElementById('dark-mode-toggle').addEventListener('change', toggleDarkMode); | |
| document.getElementById('animations-toggle').addEventListener('change', (e) => { | |
| localStorage.setItem('animationsEnabled', e.target.checked ? 'true' : 'false'); | |
| applySettings(); | |
| }); | |
| document.getElementById('notifications-toggle').addEventListener('change', (e) => { | |
| localStorage.setItem('notificationsEnabled', e.target.checked ? 'true' : 'false'); | |
| if (e.target.checked) checkNotifications(); | |
| }); | |
| document.getElementById('sound-toggle').addEventListener('change', (e) => { | |
| localStorage.setItem('soundEnabled', e.target.checked ? 'true' : 'false'); | |
| }); | |
| document.getElementById('export-data-btn').addEventListener('click', exportData); | |
| document.getElementById('import-data-btn').addEventListener('click', () => { | |
| document.getElementById('import-file-input').click(); | |
| }); | |
| document.getElementById('import-file-input').addEventListener('change', importData); | |
| document.getElementById('clear-data-btn').addEventListener('click', clearAllData); | |
| document.getElementById('sidebar-toggle').addEventListener('click', () => { | |
| document.getElementById('sidebar').classList.toggle('active'); | |
| }); | |
| document.querySelectorAll('.modal').forEach(modal => { | |
| modal.addEventListener('click', (e) => { | |
| if (e.target === modal) modal.classList.remove('active'); | |
| }); | |
| }); | |
| } | |
| // ===== Pomodoro ===== | |
| function togglePomodoroPanel() { | |
| const panel = document.getElementById('pomodoro-container'); | |
| panel.classList.toggle('active'); | |
| } | |
| function togglePomodoro() { | |
| if (pomodoroRunning) { | |
| clearInterval(pomodoroTimer); | |
| pomodoroRunning = false; | |
| document.getElementById('pomodoro-start').innerHTML = '<i class="fas fa-play"></i>'; | |
| document.getElementById('pomodoro-start').classList.remove('active'); | |
| } else { | |
| pomodoroRunning = true; | |
| document.getElementById('pomodoro-start').innerHTML = '<i class="fas fa-pause"></i>'; | |
| document.getElementById('pomodoro-start').classList.add('active'); | |
| pomodoroTimer = setInterval(() => { | |
| pomodoroTime--; | |
| updatePomodoroDisplay(); | |
| if (pomodoroTime <= 0) { | |
| clearInterval(pomodoroTimer); | |
| pomodoroRunning = false; | |
| document.getElementById('pomodoro-start').innerHTML = '<i class="fas fa-play"></i>'; | |
| document.getElementById('pomodoro-start').classList.remove('active'); | |
| pomodoroComplete(); | |
| } | |
| }, 1000); | |
| } | |
| } | |
| function resetPomodoro() { | |
| clearInterval(pomodoroTimer); | |
| pomodoroRunning = false; | |
| pomodoroPhase = 'focus'; | |
| pomodoroTime = 25 * 60; | |
| document.getElementById('pomodoro-start').innerHTML = '<i class="fas fa-play"></i>'; | |
| document.getElementById('pomodoro-start').classList.remove('active'); | |
| document.getElementById('pomodoro-status').textContent = 'Focus'; | |
| updatePomodoroDisplay(); | |
| } | |
| function skipPomodoro() { | |
| clearInterval(pomodoroTimer); | |
| pomodoroRunning = false; | |
| pomodoroComplete(); | |
| } | |
| function pomodoroComplete() { | |
| if (pomodoroPhase === 'focus') { | |
| pomodoroSessions++; | |
| pomodoroTodayMinutes += 25; | |
| saveToLocalStorage(); | |
| document.getElementById('pomodoro-sessions').textContent = pomodoroSessions; | |
| document.getElementById('pomodoro-today-min').textContent = pomodoroTodayMinutes; | |
| if (pomodoroSessions % 4 === 0) { | |
| pomodoroPhase = 'long-break'; | |
| pomodoroTime = 15 * 60; | |
| showToast('Long break time! Great work! 🎉', 'success'); | |
| triggerConfetti(); | |
| } else { | |
| pomodoroPhase = 'short-break'; | |
| pomodoroTime = 5 * 60; | |
| showToast('Good job! Take a short break ☕', 'success'); | |
| } | |
| document.getElementById('pomodoro-status').textContent = pomodoroPhase === 'short-break' ? 'Short Break' : 'Long Break'; | |
| } else { | |
| pomodoroPhase = 'focus'; | |
| pomodoroTime = 25 * 60; | |
| document.getElementById('pomodoro-status').textContent = 'Focus'; | |
| showToast('Break over! Time to focus 🎯', 'info'); | |
| } | |
| updatePomodoroDisplay(); | |
| } | |
| function updatePomodoroDisplay() { | |
| const mins = Math.floor(pomodoroTime / 60); | |
| const secs = pomodoroTime % 60; | |
| document.getElementById('pomodoro-timer').textContent = | |
| `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`; | |
| document.getElementById('pomodoro-sessions').textContent = pomodoroSessions; | |
| document.getElementById('pomodoro-today-min').textContent = pomodoroTodayMinutes; | |
| } | |
| // ===== Stats ===== | |
| function openStatsModal() { | |
| const total = tasks.length; | |
| const completed = tasks.filter(t => t.completed).length; | |
| const pending = total - completed; | |
| document.getElementById('stats-total').textContent = total; | |
| document.getElementById('stats-completed').textContent = completed; | |
| document.getElementById('stats-pending').textContent = pending; | |
| document.getElementById('stats-streak').textContent = streakCount; | |
| renderChart(); | |
| document.getElementById('stats-modal').classList.add('active'); | |
| } | |
| function closeStatsModal() { | |
| document.getElementById('stats-modal').classList.remove('active'); | |
| } | |
| function renderChart() { | |
| const container = document.getElementById('chart-bars'); | |
| container.innerHTML = ''; | |
| const days = []; | |
| for (let i = 6; i >= 0; i--) { | |
| const d = new Date(); | |
| d.setDate(d.getDate() - i); | |
| days.push(d.toISOString().split('T')[0]); | |
| } | |
| const maxVal = Math.max(1, ...days.map(day => | |
| tasks.filter(t => { | |
| if (!t.createdAt) return false; | |
| const taskDay = t.createdAt.split('T')[0]; | |
| return taskDay === day && t.completed; | |
| }).length | |
| )); | |
| days.forEach(day => { | |
| const count = tasks.filter(t => { | |
| if (!t.createdAt) return false; | |
| return t.createdAt.split('T')[0] === day && t.completed; | |
| }).length; | |
| const bar = document.createElement('div'); | |
| bar.className = 'chart-bar'; | |
| const pct = (count / maxVal) * 100; | |
| bar.style.height = `${Math.max(pct, 4)}%`; | |
| const dateObj = new Date(day + 'T00:00:00'); | |
| const label = dateObj.toLocaleDateString('en-US', { weekday: 'short' }); | |
| bar.innerHTML = ` | |
| <span class="chart-bar-value">${count}</span> | |
| <span class="chart-bar-label">${label}</span> | |
| `; | |
| container.appendChild(bar); | |
| }); | |
| } | |
| // ===== Shortcuts Modal ===== | |
| function openShortcutsModal() { | |
| document.getElementById('shortcuts-modal').classList.add('active'); | |
| } | |
| function closeShortcutsModal() { | |
| document.getElementById('shortcuts-modal').classList.remove('active'); | |
| } | |
| // ===== View Management ===== | |
| function switchView(view) { | |
| currentView = view; | |
| currentFilter.category = null; | |
| currentFilter.workspace = null; | |
| currentFilter.date = null; | |
| document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active')); | |
| document.querySelector(`[data-view="${view}"]`).classList.add('active'); | |
| document.querySelectorAll('.category-item').forEach(item => item.classList.remove('active')); | |
| const titles = { | |
| all: { title: 'All Tasks', subtitle: 'Manage your tasks efficiently' }, | |
| myday: { title: 'My Day', subtitle: 'Tasks you want to focus on today' }, | |
| today: { title: 'Today', subtitle: 'Tasks due today' }, | |
| upcoming: { title: 'Upcoming', subtitle: 'Tasks coming up' }, | |
| overdue: { title: 'Overdue', subtitle: 'Tasks that need attention' }, | |
| completed: { title: 'Completed', subtitle: 'Finished tasks' }, | |
| important: { title: 'Important', subtitle: 'High priority tasks' } | |
| }; | |
| document.getElementById('view-title').textContent = titles[view].title; | |
| document.getElementById('view-subtitle').textContent = titles[view].subtitle; | |
| renderTasks(); | |
| updateProgress(); | |
| } | |
| function toggleViewMode() { | |
| viewMode = viewMode === 'list' ? 'grid' : 'list'; | |
| document.querySelector('#view-toggle i').className = viewMode === 'list' ? 'fas fa-th' : 'fas fa-list'; | |
| localStorage.setItem('viewMode', viewMode); | |
| renderTasks(); | |
| } | |
| // ===== Quick Actions ===== | |
| function setQuickAddDefaults() { | |
| const dateInput = document.getElementById('quick-add-date'); | |
| if (dateInput) dateInput.value = new Date().toISOString().split('T')[0]; | |
| } | |
| function handleQuickAddSubmit(e) { | |
| e.preventDefault(); | |
| const title = document.getElementById('quick-add-title').value.trim(); | |
| if (!title) { showToast('Please enter a task title', 'error'); return; } | |
| const parsedTask = parseNaturalLanguageTask(title); | |
| const newTask = { | |
| id: Date.now(), | |
| title: parsedTask.title, | |
| description: parsedTask.description, | |
| dueDate: parsedTask.dueDate || document.getElementById('quick-add-date').value, | |
| dueTime: parsedTask.dueTime || '', | |
| priority: parsedTask.priority || 'medium', | |
| category: parsedTask.category || '', | |
| workspace: '', | |
| tags: parsedTask.tags || [], | |
| important: parsedTask.important || false, | |
| recurring: false, | |
| recurrence: null, | |
| subtasks: [], | |
| completed: false, | |
| createdAt: new Date().toISOString() | |
| }; | |
| tasks.unshift(newTask); | |
| saveToLocalStorage(); | |
| renderTasks(); | |
| updateCounts(); | |
| updateProgress(); | |
| updateInsights(); | |
| renderCategories(); | |
| updateUserStats(); | |
| showToast('Task added!', 'success'); | |
| playSound('create'); | |
| document.getElementById('quick-add-form').reset(); | |
| setQuickAddDefaults(); | |
| } | |
| function parseNaturalLanguageTask(input) { | |
| const text = input.toLowerCase(); | |
| const result = { title: input.trim(), description: '', dueDate: '', dueTime: '', priority: '', category: '', tags: [], important: false }; | |
| if (text.includes('urgent') || text.includes('asap')) { | |
| result.priority = 'high'; result.important = true; | |
| } else if (text.includes('later')) { | |
| result.priority = 'low'; | |
| } else if (text.includes('important')) { | |
| result.priority = 'high'; result.important = true; | |
| } | |
| const today = new Date(); | |
| const tomorrow = new Date(today); tomorrow.setDate(today.getDate() + 1); | |
| const nextWeek = new Date(today); nextWeek.setDate(today.getDate() + 7); | |
| if (text.includes('today')) result.dueDate = today.toISOString().split('T')[0]; | |
| else if (text.includes('tomorrow')) result.dueDate = tomorrow.toISOString().split('T')[0]; | |
| else if (text.includes('week')) result.dueDate = nextWeek.toISOString().split('T')[0]; | |
| if (text.includes('work')) { result.category = 'work'; result.tags.push('work'); } | |
| else if (text.includes('personal')) { result.category = 'personal'; result.tags.push('personal'); } | |
| const match = input.match(/@([a-zA-Z0-9-_]+)/); | |
| if (match) result.tags.push(match[1]); | |
| return result; | |
| } | |
| function completeVisibleTasks() { | |
| const visibleTasks = getFilteredTasks().filter(task => !task.completed); | |
| if (visibleTasks.length === 0) { showToast('No visible tasks to complete', 'info'); return; } | |
| visibleTasks.forEach(task => { task.completed = true; }); | |
| saveToLocalStorage(); | |
| renderTasks(); | |
| updateCounts(); | |
| updateProgress(); | |
| updateInsights(); | |
| renderCategories(); | |
| updateUserStats(); | |
| showToast(`Completed ${visibleTasks.length} task${visibleTasks.length !== 1 ? 's' : ''}`, 'success'); | |
| } | |
| function toggleFocusMode() { | |
| focusModeEnabled = !focusModeEnabled; | |
| document.body.classList.toggle('focus-mode', focusModeEnabled); | |
| document.getElementById('focus-mode-btn').innerHTML = | |
| focusModeEnabled ? '<i class="fas fa-bullseye"></i> Focus On' : '<i class="fas fa-bullseye"></i> Focus Mode'; | |
| showToast(focusModeEnabled ? 'Focus mode enabled' : 'Focus mode disabled', 'info'); | |
| } | |
| function openTemplateModal() { | |
| const list = document.getElementById('template-list'); | |
| list.innerHTML = ''; | |
| const templates = [ | |
| { name: 'Deep Work', tasks: ['Block 90 minutes for focused work', 'Close all distractions', 'Review progress'] }, | |
| { name: 'Weekly Review', tasks: ['Review completed tasks', 'Plan next week', 'Update notes'] }, | |
| { name: 'Trip Prep', tasks: ['Pack passport', 'Check travel documents', 'Confirm bookings'] }, | |
| { name: 'Daily Standup', tasks: ['What I did yesterday', 'What I will do today', 'Any blockers'] } | |
| ]; | |
| templates.forEach(template => { | |
| const card = document.createElement('div'); | |
| card.className = 'template-card'; | |
| card.innerHTML = `<strong>${template.name}</strong><div>${template.tasks.join('<br>')}</div><button class="btn btn-primary btn-sm" data-template='${JSON.stringify(template.tasks)}'>Apply</button>`; | |
| list.appendChild(card); | |
| }); | |
| list.querySelectorAll('button').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const tasksToAdd = JSON.parse(btn.dataset.template); | |
| tasksToAdd.forEach(taskText => { | |
| const parsed = parseNaturalLanguageTask(taskText); | |
| tasks.unshift({ | |
| id: Date.now() + Math.floor(Math.random() * 9999), | |
| title: parsed.title, | |
| description: parsed.description, | |
| dueDate: parsed.dueDate || '', | |
| dueTime: parsed.dueTime || '', | |
| priority: parsed.priority || 'medium', | |
| category: parsed.category || '', | |
| workspace: '', | |
| reminder: '', | |
| tags: parsed.tags || [], | |
| important: parsed.important || false, | |
| recurring: false, | |
| recurrence: null, | |
| subtasks: [], | |
| completed: false, | |
| createdAt: new Date().toISOString() | |
| }); | |
| }); | |
| saveToLocalStorage(); | |
| renderTasks(); | |
| updateCounts(); | |
| updateProgress(); | |
| updateInsights(); | |
| renderCategories(); | |
| updateUserStats(); | |
| closeTemplateModal(); | |
| showToast('Template applied!', 'success'); | |
| }); | |
| }); | |
| document.getElementById('template-modal').classList.add('active'); | |
| } | |
| function closeTemplateModal() { | |
| document.getElementById('template-modal').classList.remove('active'); | |
| } | |
| function cycleSmartFilter() { | |
| const filters = ['all', 'high', 'today', 'important']; | |
| const index = filters.indexOf(smartFilter); | |
| smartFilter = filters[(index + 1) % filters.length]; | |
| showToast(`Smart filter: ${smartFilter}`, 'info'); | |
| renderTasks(); | |
| } | |
| function clearCompletedTasks() { | |
| const completedCount = tasks.filter(task => task.completed).length; | |
| if (completedCount === 0) { showToast('No completed tasks to clear', 'info'); return; } | |
| tasks = tasks.filter(task => !task.completed); | |
| saveToLocalStorage(); | |
| renderTasks(); | |
| updateCounts(); | |
| updateProgress(); | |
| updateInsights(); | |
| renderCategories(); | |
| updateUserStats(); | |
| showToast('Completed tasks cleared', 'success'); | |
| } | |
| function updateInsights() { | |
| const today = new Date().toISOString().split('T')[0]; | |
| const activeTasks = tasks.filter(task => !task.completed).length; | |
| const overdueTasks = tasks.filter(task => task.dueDate && task.dueDate < today && !task.completed).length; | |
| const dueSoonTasks = tasks.filter(task => { | |
| if (!task.dueDate || task.completed) return false; | |
| const diffDays = Math.round((new Date(`${task.dueDate}T00:00:00`) - new Date(`${today}T00:00:00`)) / (86400000)); | |
| return diffDays >= 0 && diffDays <= 2; | |
| }).length; | |
| document.getElementById('insight-active').textContent = `${activeTasks} task${activeTasks !== 1 ? 's' : ''}`; | |
| document.getElementById('insight-overdue').textContent = `${overdueTasks} task${overdueTasks !== 1 ? 's' : ''}`; | |
| document.getElementById('insight-soon').textContent = `${dueSoonTasks} task${dueSoonTasks !== 1 ? 's' : ''}`; | |
| } | |
| function getTaskDueState(task) { | |
| if (task.completed || !task.dueDate) return { className: '', label: '' }; | |
| const today = new Date(); | |
| const todayStart = new Date(today.getFullYear(), today.getMonth(), today.getDate()); | |
| const dueDate = new Date(`${task.dueDate}T00:00:00`); | |
| const diffDays = Math.round((dueDate - todayStart) / 86400000); | |
| if (diffDays < 0) return { className: 'overdue', label: 'Overdue' }; | |
| if (diffDays <= 2) return { className: 'due-soon', label: diffDays === 0 ? 'Due today' : `${diffDays} day${diffDays === 1 ? '' : 's'} left` }; | |
| return { className: '', label: '' }; | |
| } | |
| // ===== Task Management ===== | |
| function openTaskModal(taskId = null) { | |
| const modal = document.getElementById('task-modal'); | |
| const form = document.getElementById('task-form'); | |
| const modalTitle = document.getElementById('modal-title'); | |
| form.reset(); | |
| document.getElementById('subtasks-container').innerHTML = ''; | |
| document.getElementById('recurring-options').style.display = 'none'; | |
| if (taskId) { | |
| editingTaskId = taskId; | |
| const task = tasks.find(t => t.id === taskId); | |
| if (!task) return; | |
| modalTitle.textContent = 'Edit Task'; | |
| document.getElementById('task-title').value = task.title; | |
| document.getElementById('task-description').value = task.description || ''; | |
| document.getElementById('task-date').value = task.dueDate || ''; | |
| document.getElementById('task-time').value = task.dueTime || ''; | |
| document.getElementById('task-priority').value = task.priority; | |
| document.getElementById('task-category').value = task.category || ''; | |
| document.getElementById('task-workspace').value = task.workspace || ''; | |
| document.getElementById('task-reminder').value = task.reminder || ''; | |
| document.getElementById('task-tags').value = task.tags ? task.tags.join(', ') : ''; | |
| document.getElementById('task-important').checked = task.important || false; | |
| document.getElementById('task-recurring').checked = task.recurring || false; | |
| if (task.recurring) { | |
| document.getElementById('recurring-options').style.display = 'block'; | |
| document.getElementById('task-recurrence').value = task.recurrence || 'daily'; | |
| } | |
| if (task.subtasks && task.subtasks.length > 0) { | |
| task.subtasks.forEach(subtask => addSubtaskInput(subtask.text)); | |
| } | |
| } else { | |
| editingTaskId = null; | |
| modalTitle.textContent = 'Add New Task'; | |
| } | |
| updateCategorySelect(); | |
| updateWorkspaceSelect(); | |
| modal.classList.add('active'); | |
| } | |
| function closeTaskModal() { | |
| document.getElementById('task-modal').classList.remove('active'); | |
| editingTaskId = null; | |
| } | |
| function saveTask(e) { | |
| e.preventDefault(); | |
| const title = document.getElementById('task-title').value.trim(); | |
| if (!title) { showToast('Please enter a task title', 'error'); return; } | |
| const taskData = { | |
| title, | |
| description: document.getElementById('task-description').value.trim(), | |
| dueDate: document.getElementById('task-date').value, | |
| dueTime: document.getElementById('task-time').value, | |
| priority: document.getElementById('task-priority').value, | |
| category: document.getElementById('task-category').value, | |
| workspace: document.getElementById('task-workspace').value, | |
| reminder: document.getElementById('task-reminder').value, | |
| tags: document.getElementById('task-tags').value.trim().split(',').map(t => t.trim()).filter(Boolean), | |
| important: document.getElementById('task-important').checked, | |
| recurring: document.getElementById('task-recurring').checked, | |
| recurrence: document.getElementById('task-recurring').checked ? document.getElementById('task-recurrence').value : null, | |
| subtasks: [], | |
| completed: false | |
| }; | |
| document.querySelectorAll('.subtask-item input[type="text"]').forEach(input => { | |
| if (input.value.trim()) { | |
| taskData.subtasks.push({ id: Date.now() + Math.floor(Math.random() * 9999), text: input.value.trim(), completed: false }); | |
| } | |
| }); | |
| if (editingTaskId) { | |
| const taskIndex = tasks.findIndex(t => t.id === editingTaskId); | |
| if (taskIndex !== -1) { | |
| tasks[taskIndex] = { ...tasks[taskIndex], ...taskData }; | |
| showToast('Task updated!', 'success'); | |
| } | |
| } else { | |
| tasks.unshift({ id: Date.now(), createdAt: new Date().toISOString(), ...taskData }); | |
| showToast('Task created!', 'success'); | |
| playSound('create'); | |
| } | |
| saveToLocalStorage(); | |
| renderTasks(); | |
| updateCounts(); | |
| updateProgress(); | |
| updateInsights(); | |
| renderCategories(); | |
| updateUserStats(); | |
| closeTaskModal(); | |
| } | |
| function deleteTask(taskId) { | |
| const task = tasks.find(t => t.id === taskId); | |
| if (!task) return; | |
| lastDeletedTask = { task: { ...task }, index: tasks.findIndex(t => t.id === taskId) }; | |
| tasks = tasks.filter(t => t.id !== taskId); | |
| saveToLocalStorage(); | |
| renderTasks(); | |
| updateCounts(); | |
| updateProgress(); | |
| updateInsights(); | |
| renderCategories(); | |
| updateUserStats(); | |
| showToast('Task deleted!', 'success', 'Undo', restoreLastDeletedTask); | |
| playSound('delete'); | |
| } | |
| function restoreLastDeletedTask() { | |
| if (!lastDeletedTask) return; | |
| const { task, index } = lastDeletedTask; | |
| if (index >= 0) tasks.splice(index, 0, task); | |
| else tasks.unshift(task); | |
| lastDeletedTask = null; | |
| saveToLocalStorage(); | |
| renderTasks(); | |
| updateCounts(); | |
| updateProgress(); | |
| updateInsights(); | |
| renderCategories(); | |
| updateUserStats(); | |
| showToast('Task restored!', 'info'); | |
| } | |
| function toggleTaskComplete(taskId) { | |
| const task = tasks.find(t => t.id === taskId); | |
| if (!task) return; | |
| task.completed = !task.completed; | |
| if (task.completed) { | |
| showToast('Task completed! 🎉', 'success'); | |
| playSound('complete'); | |
| triggerConfetti(); | |
| checkStreak(); | |
| if (task.recurring && task.recurrence) createRecurringTask(task); | |
| } else { | |
| showToast('Task reopened!', 'info'); | |
| } | |
| saveToLocalStorage(); | |
| renderTasks(); | |
| updateCounts(); | |
| updateProgress(); | |
| updateInsights(); | |
| renderCategories(); | |
| updateUserStats(); | |
| } | |
| function createRecurringTask(originalTask) { | |
| const newTask = { ...originalTask }; | |
| newTask.id = Date.now(); | |
| newTask.completed = false; | |
| newTask.createdAt = new Date().toISOString(); | |
| if (newTask.dueDate) { | |
| const currentDate = new Date(newTask.dueDate); | |
| if (newTask.recurrence === 'daily') currentDate.setDate(currentDate.getDate() + 1); | |
| else if (newTask.recurrence === 'weekly') currentDate.setDate(currentDate.getDate() + 7); | |
| else if (newTask.recurrence === 'monthly') currentDate.setMonth(currentDate.getMonth() + 1); | |
| newTask.dueDate = currentDate.toISOString().split('T')[0]; | |
| } | |
| tasks.unshift(newTask); | |
| saveToLocalStorage(); | |
| } | |
| function toggleTaskImportant(taskId) { | |
| const task = tasks.find(t => t.id === taskId); | |
| if (!task) return; | |
| task.important = !task.important; | |
| saveToLocalStorage(); | |
| renderTasks(); | |
| updateCounts(); | |
| updateInsights(); | |
| showToast(task.important ? 'Marked as important!' : 'Removed from important!', 'info'); | |
| } | |
| function toggleSubtask(taskId, subtaskId) { | |
| const task = tasks.find(t => t.id === taskId); | |
| if (!task || !task.subtasks) return; | |
| const subtask = task.subtasks.find(s => s.id === subtaskId); | |
| if (!subtask) return; | |
| subtask.completed = !subtask.completed; | |
| saveToLocalStorage(); | |
| renderTasks(); | |
| } | |
| // ===== Render ===== | |
| function renderTasks() { | |
| const container = document.getElementById('tasks-container'); | |
| const emptyState = document.getElementById('empty-state'); | |
| if (!container || !emptyState) return; | |
| container.classList.remove('list-view', 'grid-view'); | |
| container.classList.add(viewMode === 'grid' ? 'grid-view' : 'list-view'); | |
| let filteredTasks = getFilteredTasks(); | |
| if (filteredTasks.length === 0) { | |
| emptyState.style.display = 'flex'; | |
| container.querySelectorAll('.task-card').forEach(card => card.remove()); | |
| if (sortableInstance) { sortableInstance.destroy(); sortableInstance = null; } | |
| return; | |
| } | |
| emptyState.style.display = 'none'; | |
| container.innerHTML = ''; | |
| filteredTasks.forEach(task => { | |
| container.appendChild(createTaskCard(task)); | |
| }); | |
| if (currentView === 'all' && !sortableInstance) { | |
| sortableInstance = Sortable.create(container, { | |
| animation: 200, | |
| ghostClass: 'dragging', | |
| onEnd: function(evt) { | |
| const movedTask = filteredTasks[evt.oldIndex]; | |
| filteredTasks.splice(evt.oldIndex, 1); | |
| filteredTasks.splice(evt.newIndex, 0, movedTask); | |
| const allTaskIds = filteredTasks.map(t => t.id); | |
| tasks.sort((a, b) => { | |
| const ai = allTaskIds.indexOf(a.id); | |
| const bi = allTaskIds.indexOf(b.id); | |
| if (ai === -1) return 1; | |
| if (bi === -1) return -1; | |
| return ai - bi; | |
| }); | |
| saveToLocalStorage(); | |
| } | |
| }); | |
| } else if (currentView !== 'all' && sortableInstance) { | |
| sortableInstance.destroy(); | |
| sortableInstance = null; | |
| } | |
| } | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| function createTaskCard(task) { | |
| const card = document.createElement('div'); | |
| const dueState = getTaskDueState(task); | |
| card.className = `task-card ${task.completed ? 'completed' : ''} ${dueState.className}`; | |
| const category = categories.find(c => c.id === task.category); | |
| const workspace = workspaces.find(w => w.id === task.workspace); | |
| const categoryBadge = category ? | |
| `<span class="task-category-badge" style="background:${category.color}15;color:${category.color}"><i class="fas ${category.icon}"></i>${escapeHtml(category.name)}</span>` : ''; | |
| const tagsHtml = task.tags && task.tags.length > 0 ? | |
| `<div class="task-tags">${task.tags.map(tag => `<span class="task-tag">${escapeHtml(tag)}</span>`).join('')}</div>` : ''; | |
| const dueDateHtml = task.dueDate ? | |
| `<div class="task-meta-item"><i class="fas fa-calendar"></i><span>${formatDate(task.dueDate)}</span></div>` : ''; | |
| const dueTimeHtml = task.dueTime ? | |
| `<div class="task-meta-item"><i class="fas fa-clock"></i><span>${task.dueTime}</span></div>` : ''; | |
| const workspaceBadge = workspace ? | |
| `<div class="task-meta-item"><i class="fas fa-folder"></i><span>${escapeHtml(workspace.name)}</span></div>` : ''; | |
| const recurringBadge = task.recurring ? | |
| `<div class="task-meta-item"><i class="fas fa-redo"></i><span>${task.recurrence}</span></div>` : ''; | |
| const dueStateBadge = dueState.label ? | |
| `<div class="task-meta-item due-state ${dueState.className}"><i class="fas fa-bolt"></i><span>${escapeHtml(dueState.label)}</span></div>` : ''; | |
| let subtasksHtml = ''; | |
| if (task.subtasks && task.subtasks.length > 0) { | |
| const completedSubtasks = task.subtasks.filter(s => s.completed).length; | |
| const subtasksItems = task.subtasks.map(subtask => { | |
| const checked = subtask.completed ? 'checked' : ''; | |
| return `<div class="task-subtask"><input type="checkbox" ${checked} data-task-id="${task.id}" data-subtask-id="${subtask.id}" class="subtask-checkbox"><span style="${subtask.completed ? 'text-decoration:line-through' : ''}">${escapeHtml(subtask.text)}</span></div>`; | |
| }).join(''); | |
| subtasksHtml = `<div class="task-subtasks"><div style="font-size:12px;color:var(--text-tertiary);margin-bottom:8px">${completedSubtasks}/${task.subtasks.length} subtasks</div>${subtasksItems}</div>`; | |
| } | |
| const isMyDay = myDayTasks.includes(task.id); | |
| card.innerHTML = ` | |
| <div class="task-header"> | |
| <div class="task-checkbox ${task.completed ? 'checked' : ''}" data-task-id="${task.id}"></div> | |
| <div class="task-content"> | |
| <h3 class="task-title">${escapeHtml(task.title)}</h3> | |
| ${task.description ? `<p class="task-description">${escapeHtml(task.description)}</p>` : ''} | |
| <div class="task-meta"> | |
| <span class="task-priority ${task.priority}">${task.priority}</span> | |
| ${categoryBadge} | |
| ${dueDateHtml} | |
| ${dueTimeHtml} | |
| ${workspaceBadge} | |
| ${recurringBadge} | |
| ${dueStateBadge} | |
| ${isMyDay ? '<span class="task-priority" style="background:rgba(245,158,11,0.1);color:var(--warning);font-size:10px"><i class="fas fa-sun"></i> My Day</span>' : ''} | |
| </div> | |
| ${tagsHtml} | |
| ${subtasksHtml} | |
| </div> | |
| <div class="task-actions"> | |
| <button class="task-action-btn" data-task-id="${task.id}" data-action="myday" title="Add to My Day"><i class="fas fa-sun"></i></button> | |
| <button class="task-action-btn ${task.important ? 'important' : ''}" data-task-id="${task.id}" data-action="important" title="Important"><i class="fas fa-star"></i></button> | |
| <button class="task-action-btn" data-task-id="${task.id}" data-action="edit" title="Edit"><i class="fas fa-edit"></i></button> | |
| <button class="task-action-btn delete" data-task-id="${task.id}" data-action="delete" title="Delete"><i class="fas fa-trash"></i></button> | |
| </div> | |
| </div> | |
| `; | |
| card.querySelector('.task-checkbox').addEventListener('click', function() { | |
| toggleTaskComplete(Number(this.dataset.taskId)); | |
| }); | |
| card.querySelectorAll('.task-action-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| const action = btn.dataset.action; | |
| const taskId = Number(btn.dataset.taskId); | |
| if (action === 'important') toggleTaskImportant(taskId); | |
| else if (action === 'edit') openTaskModal(taskId); | |
| else if (action === 'delete') deleteTask(taskId); | |
| else if (action === 'myday') addToMyDay(taskId); | |
| }); | |
| }); | |
| card.querySelectorAll('.subtask-checkbox').forEach(cb => { | |
| cb.addEventListener('change', (e) => { | |
| toggleSubtask(Number(e.target.dataset.taskId), parseFloat(e.target.dataset.subtaskId)); | |
| }); | |
| }); | |
| return card; | |
| } | |
| function getFilteredTasks() { | |
| let filtered = [...tasks]; | |
| const today = new Date().toISOString().split('T')[0]; | |
| if (smartFilter === 'high') filtered = filtered.filter(task => task.priority === 'high' || task.important); | |
| else if (smartFilter === 'today') filtered = filtered.filter(task => task.dueDate === today || !task.dueDate); | |
| else if (smartFilter === 'important') filtered = filtered.filter(task => task.important); | |
| loadMyDay(); | |
| switch (currentView) { | |
| case 'myday': | |
| filtered = filtered.filter(t => myDayTasks.includes(t.id)); | |
| break; | |
| case 'today': | |
| filtered = filtered.filter(t => t.dueDate === today && !t.completed); | |
| break; | |
| case 'upcoming': | |
| filtered = filtered.filter(t => t.dueDate && t.dueDate > today && !t.completed); | |
| break; | |
| case 'overdue': | |
| filtered = filtered.filter(t => t.dueDate && t.dueDate < today && !t.completed); | |
| break; | |
| case 'completed': | |
| filtered = filtered.filter(t => t.completed); | |
| break; | |
| case 'important': | |
| filtered = filtered.filter(t => t.important && !t.completed); | |
| break; | |
| } | |
| if (currentFilter.priority !== 'all') filtered = filtered.filter(t => t.priority === currentFilter.priority); | |
| if (currentFilter.category) filtered = filtered.filter(t => t.category === currentFilter.category); | |
| if (currentFilter.workspace) filtered = filtered.filter(t => t.workspace === currentFilter.workspace); | |
| if (currentFilter.date) filtered = filtered.filter(t => t.dueDate === currentFilter.date); | |
| if (currentFilter.search) { | |
| const search = currentFilter.search.toLowerCase(); | |
| filtered = filtered.filter(t => | |
| t.title.toLowerCase().includes(search) || | |
| (t.description && t.description.toLowerCase().includes(search)) || | |
| (t.tags && t.tags.some(tag => tag.toLowerCase().includes(search))) | |
| ); | |
| } | |
| filtered.sort((a, b) => { | |
| switch (currentSort) { | |
| case 'date-desc': return new Date(b.createdAt) - new Date(a.createdAt); | |
| case 'date-asc': return new Date(a.createdAt) - new Date(b.createdAt); | |
| case 'priority': return ({ high: 3, medium: 2, low: 1 })[b.priority] - ({ high: 3, medium: 2, low: 1 })[a.priority]; | |
| case 'alphabetical': return a.title.localeCompare(b.title); | |
| case 'duedate': | |
| if (!a.dueDate) return 1; | |
| if (!b.dueDate) return -1; | |
| return new Date(a.dueDate) - new Date(b.dueDate); | |
| default: return 0; | |
| } | |
| }); | |
| return filtered; | |
| } | |
| // ===== Categories ===== | |
| function openCategoryModal() { | |
| document.getElementById('category-modal').classList.add('active'); | |
| } | |
| function closeCategoryModal() { | |
| document.getElementById('category-modal').classList.remove('active'); | |
| document.getElementById('category-form').reset(); | |
| document.querySelectorAll('.color-option').forEach(o => o.classList.remove('active')); | |
| document.querySelector('.color-option').classList.add('active'); | |
| } | |
| function saveCategory(e) { | |
| e.preventDefault(); | |
| const name = document.getElementById('category-name').value.trim(); | |
| if (!name) { showToast('Please enter a category name', 'error'); return; } | |
| categories.push({ | |
| id: Date.now().toString(), | |
| name, | |
| icon: document.getElementById('category-icon').value, | |
| color: document.querySelector('.color-option.active').dataset.color | |
| }); | |
| saveToLocalStorage(); | |
| renderCategories(); | |
| updateCategorySelect(); | |
| closeCategoryModal(); | |
| showToast('Category created!', 'success'); | |
| } | |
| function renderCategories() { | |
| const container = document.getElementById('categories-list'); | |
| container.innerHTML = ''; | |
| categories.forEach(category => { | |
| const item = document.createElement('button'); | |
| item.className = 'category-item'; | |
| item.dataset.categoryId = category.id; | |
| item.addEventListener('click', function() { filterByCategory(category.id, this); }); | |
| const count = tasks.filter(t => t.category === category.id && !t.completed).length; | |
| item.innerHTML = `<div class="category-icon" style="background:${category.color}15;color:${category.color}"><i class="fas ${category.icon}"></i></div><span>${escapeHtml(category.name)}</span><span class="count">${count}</span>`; | |
| container.appendChild(item); | |
| }); | |
| } | |
| function filterByCategory(categoryId, element) { | |
| currentFilter.category = categoryId; | |
| document.querySelectorAll('.category-item').forEach(item => item.classList.remove('active')); | |
| if (element) element.classList.add('active'); | |
| renderTasks(); | |
| } | |
| function renderWorkspaces() { | |
| const container = document.getElementById('workspaces-list'); | |
| container.innerHTML = ''; | |
| workspaces.forEach(workspace => { | |
| const item = document.createElement('button'); | |
| item.className = 'category-item'; | |
| item.innerHTML = `<span>${escapeHtml(workspace.name)}</span><span class="count">${tasks.filter(t => t.workspace === workspace.id).length}</span>`; | |
| item.addEventListener('click', () => { | |
| currentFilter.workspace = workspace.id; | |
| currentFilter.category = null; | |
| document.querySelectorAll('.category-item').forEach(el => el.classList.remove('active')); | |
| renderTasks(); | |
| }); | |
| container.appendChild(item); | |
| }); | |
| } | |
| function openWorkspaceModal() { document.getElementById('workspace-modal').classList.add('active'); } | |
| function closeWorkspaceModal() { document.getElementById('workspace-modal').classList.remove('active'); document.getElementById('workspace-form').reset(); } | |
| function saveWorkspace(e) { | |
| e.preventDefault(); | |
| const name = document.getElementById('workspace-name').value.trim(); | |
| if (!name) return; | |
| workspaces.push({ id: Date.now().toString(), name }); | |
| saveToLocalStorage(); | |
| renderWorkspaces(); | |
| updateWorkspaceSelect(); | |
| closeWorkspaceModal(); | |
| showToast('Workspace created!', 'success'); | |
| } | |
| function updateWorkspaceSelect() { | |
| const select = document.getElementById('task-workspace'); | |
| const currentValue = select.value; | |
| select.innerHTML = '<option value="">No Workspace</option>'; | |
| workspaces.forEach(w => { | |
| const o = document.createElement('option'); | |
| o.value = w.id; o.textContent = w.name; select.appendChild(o); | |
| }); | |
| if (currentValue) select.value = currentValue; | |
| } | |
| function updateCategorySelect() { | |
| const select = document.getElementById('task-category'); | |
| const currentValue = select.value; | |
| select.innerHTML = '<option value="">No Category</option>'; | |
| categories.forEach(c => { | |
| const o = document.createElement('option'); | |
| o.value = c.id; o.textContent = c.name; select.appendChild(o); | |
| }); | |
| if (currentValue) select.value = currentValue; | |
| } | |
| // ===== Filter & Sort ===== | |
| function toggleFilterPanel() { document.getElementById('filter-panel').classList.toggle('active'); } | |
| function handleFilter(e) { | |
| const filterType = e.currentTarget.dataset.filter; | |
| const filterValue = e.currentTarget.dataset.value; | |
| currentFilter[filterType] = filterValue; | |
| e.currentTarget.parentElement.querySelectorAll('.filter-option').forEach(o => o.classList.remove('active')); | |
| e.currentTarget.classList.add('active'); | |
| renderTasks(); | |
| } | |
| function handleSort(e) { | |
| currentSort = e.currentTarget.dataset.sort; | |
| document.querySelectorAll('.sort-option').forEach(o => o.classList.remove('active')); | |
| e.currentTarget.classList.add('active'); | |
| renderTasks(); | |
| } | |
| function handleSearch(e) { | |
| currentFilter.search = e.target.value.trim(); | |
| renderTasks(); | |
| } | |
| // ===== Subtasks ===== | |
| function addSubtaskInput(subtaskText = '') { | |
| const container = document.getElementById('subtasks-container'); | |
| const item = document.createElement('div'); | |
| item.className = 'subtask-item'; | |
| const input = document.createElement('input'); | |
| input.type = 'text'; | |
| input.placeholder = 'Enter subtask'; | |
| input.value = subtaskText; | |
| const removeBtn = document.createElement('button'); | |
| removeBtn.type = 'button'; | |
| removeBtn.innerHTML = '<i class="fas fa-times"></i>'; | |
| removeBtn.addEventListener('click', () => item.remove()); | |
| item.appendChild(input); | |
| item.appendChild(removeBtn); | |
| container.appendChild(item); | |
| } | |
| // ===== Settings ===== | |
| function openSettingsModal() { document.getElementById('settings-modal').classList.add('active'); } | |
| function closeSettingsModal() { document.getElementById('settings-modal').classList.remove('active'); } | |
| function toggleDarkMode() { | |
| document.body.classList.toggle('dark-mode'); | |
| const isDark = document.body.classList.contains('dark-mode'); | |
| localStorage.setItem('darkMode', isDark ? 'enabled' : 'disabled'); | |
| document.getElementById('dark-mode-toggle').checked = isDark; | |
| document.querySelector('#theme-toggle i').className = isDark ? 'fas fa-sun' : 'fas fa-moon'; | |
| } | |
| function applySettings() { | |
| const animationsEnabled = localStorage.getItem('animationsEnabled') !== 'false'; | |
| const notificationsEnabled = localStorage.getItem('notificationsEnabled') !== 'false'; | |
| const soundEnabled = localStorage.getItem('soundEnabled') !== 'false'; | |
| const savedViewMode = localStorage.getItem('viewMode'); | |
| document.body.classList.toggle('no-animations', !animationsEnabled); | |
| document.getElementById('animations-toggle').checked = animationsEnabled; | |
| document.getElementById('notifications-toggle').checked = notificationsEnabled; | |
| document.getElementById('sound-toggle').checked = soundEnabled; | |
| if (savedViewMode === 'grid' || savedViewMode === 'list') { | |
| viewMode = savedViewMode; | |
| const icon = document.querySelector('#view-toggle i'); | |
| if (icon) icon.className = viewMode === 'list' ? 'fas fa-th' : 'fas fa-list'; | |
| } | |
| } | |
| function checkDarkMode() { | |
| if (localStorage.getItem('darkMode') === 'enabled') { | |
| document.body.classList.add('dark-mode'); | |
| document.getElementById('dark-mode-toggle').checked = true; | |
| document.querySelector('#theme-toggle i').className = 'fas fa-sun'; | |
| } | |
| } | |
| function exportData() { | |
| const data = { tasks, categories, workspaces, exportDate: new Date().toISOString(), version: '3.0' }; | |
| const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const link = document.createElement('a'); | |
| link.href = url; | |
| link.download = `taskmaster-backup-${Date.now()}.json`; | |
| link.click(); | |
| URL.revokeObjectURL(url); | |
| showToast('Data exported!', 'success'); | |
| } | |
| function importData(e) { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| try { | |
| const data = JSON.parse(event.target.result); | |
| if (data.tasks && data.categories) { | |
| tasks = data.tasks; | |
| categories = data.categories; | |
| workspaces = data.workspaces || []; | |
| saveToLocalStorage(); | |
| renderTasks(); | |
| renderCategories(); | |
| renderWorkspaces(); | |
| updateWorkspaceSelect(); | |
| updateCounts(); | |
| updateProgress(); | |
| updateInsights(); | |
| updateUserStats(); | |
| showToast('Data imported!', 'success'); | |
| } else { | |
| showToast('Invalid data format!', 'error'); | |
| } | |
| } catch (err) { | |
| showToast('Error: ' + err.message, 'error'); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| e.target.value = ''; | |
| } | |
| function clearAllData() { | |
| if (confirm('Are you sure you want to clear all data? This cannot be undone!')) { | |
| tasks = []; | |
| categories = []; | |
| workspaces = []; | |
| saveToLocalStorage(); | |
| renderTasks(); | |
| renderCategories(); | |
| updateCounts(); | |
| updateProgress(); | |
| updateInsights(); | |
| updateUserStats(); | |
| showToast('All data cleared!', 'success'); | |
| } | |
| } | |
| // ===== Utility ===== | |
| function toggleCalendarView() { | |
| calendarViewVisible = !calendarViewVisible; | |
| document.getElementById('calendar-view-panel').hidden = !calendarViewVisible; | |
| document.getElementById('tasks-container').hidden = calendarViewVisible; | |
| if (calendarViewVisible) renderCalendar(); | |
| } | |
| function renderCalendar() { | |
| const grid = document.getElementById('calendar-grid'); | |
| const label = document.getElementById('calendar-label'); | |
| label.textContent = calendarMonth.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); | |
| const year = calendarMonth.getFullYear(); | |
| const month = calendarMonth.getMonth(); | |
| const firstDay = new Date(year, month, 1).getDay(); | |
| const daysInMonth = new Date(year, month + 1, 0).getDate(); | |
| const cells = []; | |
| for (let i = 0; i < firstDay; i++) cells.push('<div class="calendar-cell muted"></div>'); | |
| for (let day = 1; day <= daysInMonth; day++) { | |
| const dateKey = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; | |
| const dayTasks = tasks.filter(task => task.dueDate === dateKey && !task.completed); | |
| const isSelected = currentFilter.date === dateKey; | |
| cells.push(`<div class="calendar-cell ${isSelected ? 'active' : ''}" data-date="${dateKey}"><strong>${day}</strong><div class="calendar-task-count">${dayTasks.length} task${dayTasks.length === 1 ? '' : 's'}</div></div>`); | |
| } | |
| grid.innerHTML = cells.join(''); | |
| grid.querySelectorAll('.calendar-cell[data-date]').forEach(cell => { | |
| cell.addEventListener('click', () => { | |
| currentFilter.date = cell.dataset.date; | |
| renderCalendar(); | |
| renderTasks(); | |
| }); | |
| }); | |
| } | |
| function updateCounts() { | |
| const today = new Date().toISOString().split('T')[0]; | |
| loadMyDay(); | |
| document.getElementById('all-count').textContent = tasks.filter(t => !t.completed).length; | |
| document.getElementById('myday-count').textContent = myDayTasks.length; | |
| document.getElementById('today-count').textContent = tasks.filter(t => t.dueDate === today && !t.completed).length; | |
| document.getElementById('upcoming-count').textContent = tasks.filter(t => t.dueDate && t.dueDate > today && !t.completed).length; | |
| document.getElementById('overdue-count').textContent = tasks.filter(t => t.dueDate && t.dueDate < today && !t.completed).length; | |
| document.getElementById('completed-count').textContent = tasks.filter(t => t.completed).length; | |
| document.getElementById('important-count').textContent = tasks.filter(t => t.important && !t.completed).length; | |
| document.getElementById('stat-completed').textContent = tasks.filter(t => t.completed).length; | |
| document.getElementById('stat-pending').textContent = tasks.filter(t => !t.completed).length; | |
| } | |
| function updateProgress() { | |
| const today = new Date().toISOString().split('T')[0]; | |
| const todayTasks = tasks.filter(t => t.dueDate === today); | |
| const completedToday = todayTasks.filter(t => t.completed).length; | |
| const totalToday = todayTasks.length; | |
| const percentage = totalToday > 0 ? Math.round((completedToday / totalToday) * 100) : 0; | |
| document.getElementById('progress-text').textContent = `${percentage}%`; | |
| document.getElementById('progress-fill').style.width = `${percentage}%`; | |
| } | |
| function updateUserStats() { | |
| const today = new Date().toISOString().split('T')[0]; | |
| const todayTasks = tasks.filter(t => t.dueDate === today && !t.completed).length; | |
| document.getElementById('user-stats').textContent = `${todayTasks} task${todayTasks !== 1 ? 's' : ''} today`; | |
| } | |
| function formatDate(dateString) { | |
| const date = new Date(dateString); | |
| const today = new Date(); | |
| const tomorrow = new Date(today); | |
| tomorrow.setDate(tomorrow.getDate() + 1); | |
| const todayStr = today.toISOString().split('T')[0]; | |
| const tomorrowStr = tomorrow.toISOString().split('T')[0]; | |
| if (dateString === todayStr) return 'Today'; | |
| else if (dateString === tomorrowStr) return 'Tomorrow'; | |
| else if (dateString < todayStr) return '⚠️ Overdue'; | |
| else return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); | |
| } | |
| function showToast(message, type = 'success', actionLabel = '', actionHandler = null) { | |
| const toast = document.getElementById('toast'); | |
| const toastMessage = document.getElementById('toast-message'); | |
| const toastIcon = toast.querySelector('i'); | |
| const toastAction = document.getElementById('toast-action'); | |
| toastMessage.textContent = message; | |
| if (actionLabel && actionHandler) { | |
| toastAction.textContent = actionLabel; | |
| toastAction.style.display = 'inline-flex'; | |
| toastAction.onclick = actionHandler; | |
| } else { | |
| toastAction.textContent = ''; | |
| toastAction.style.display = 'none'; | |
| toastAction.onclick = null; | |
| } | |
| toastIcon.className = type === 'success' ? 'fas fa-check-circle' : type === 'error' ? 'fas fa-exclamation-circle' : 'fas fa-info-circle'; | |
| toast.style.background = type === 'success' ? 'var(--success)' : type === 'error' ? 'var(--danger)' : 'var(--info)'; | |
| toast.classList.add('show'); | |
| setTimeout(() => toast.classList.remove('show'), 3500); | |
| } | |
| function scheduleReminders() { | |
| const now = new Date(); | |
| tasks.filter(task => task.reminder && !task.completed).forEach(task => { | |
| const reminderTime = new Date(task.reminder); | |
| if (reminderTime > now) { | |
| setTimeout(() => { | |
| if (Notification.permission === 'granted') { | |
| new Notification('Task Reminder', { body: task.title }); | |
| } | |
| }, reminderTime - now); | |
| } | |
| }); | |
| } | |
| function playSound(type) { | |
| if (localStorage.getItem('soundEnabled') === 'false') return; | |
| try { | |
| const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); | |
| const osc = audioCtx.createOscillator(); | |
| const gain = audioCtx.createGain(); | |
| osc.connect(gain); | |
| gain.connect(audioCtx.destination); | |
| if (type === 'complete') { osc.frequency.value = 800; gain.gain.value = 0.1; osc.start(); osc.stop(audioCtx.currentTime + 0.1); } | |
| else if (type === 'create') { osc.frequency.value = 600; gain.gain.value = 0.05; osc.start(); osc.stop(audioCtx.currentTime + 0.05); } | |
| else if (type === 'delete') { osc.frequency.value = 400; gain.gain.value = 0.05; osc.start(); osc.stop(audioCtx.currentTime + 0.05); } | |
| } catch (e) {} | |
| } | |
| function triggerConfetti() { | |
| const canvas = document.getElementById('confetti-canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| const confetti = []; | |
| const count = 60; | |
| const colors = ['#6366f1', '#a855f7', '#ec4899', '#f59e0b', '#22c55e', '#06b6d4']; | |
| for (let i = 0; i < count; i++) { | |
| confetti.push({ | |
| x: Math.random() * canvas.width, | |
| y: Math.random() * canvas.height - canvas.height, | |
| r: Math.random() * 6 + 3, | |
| d: Math.random() * count, | |
| color: colors[Math.floor(Math.random() * colors.length)], | |
| tilt: Math.random() * 10 - 10, | |
| tiltAngleIncremental: Math.random() * 0.07 + 0.05, | |
| tiltAngle: 0 | |
| }); | |
| } | |
| let frameCount = 0; | |
| function draw() { | |
| frameCount++; | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| confetti.forEach((c, i) => { | |
| ctx.beginPath(); | |
| ctx.lineWidth = c.r / 2; | |
| ctx.strokeStyle = c.color; | |
| c.tiltAngle += c.tiltAngleIncremental; | |
| c.y += (Math.cos(c.d) + 3 + c.r / 2) / 2; | |
| c.tilt = Math.sin(c.tiltAngle - i / 3) * 15; | |
| ctx.moveTo(c.x + c.tilt + c.r, c.y); | |
| ctx.lineTo(c.x + c.tilt, c.y + c.tilt + c.r); | |
| ctx.stroke(); | |
| if (c.y > canvas.height) confetti.splice(i, 1); | |
| }); | |
| if (confetti.length > 0 && frameCount < 150) { | |
| requestAnimationFrame(draw); | |
| } else { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| } | |
| } | |
| draw(); | |
| } | |
| function initParticles() { | |
| const canvas = document.getElementById('particles-canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| const particles = []; | |
| const count = 40; | |
| for (let i = 0; i < count; i++) { | |
| particles.push({ | |
| x: Math.random() * canvas.width, | |
| y: Math.random() * canvas.height, | |
| vx: (Math.random() - 0.5) * 0.5, | |
| vy: (Math.random() - 0.5) * 0.5, | |
| r: Math.random() * 2 + 1, | |
| opacity: Math.random() * 0.5 + 0.1 | |
| }); | |
| } | |
| function animate() { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| const isDark = document.body.classList.contains('dark-mode'); | |
| particles.forEach(p => { | |
| p.x += p.vx; | |
| p.y += p.vy; | |
| if (p.x < 0) p.x = canvas.width; | |
| if (p.x > canvas.width) p.x = 0; | |
| if (p.y < 0) p.y = canvas.height; | |
| if (p.y > canvas.height) p.y = 0; | |
| ctx.beginPath(); | |
| ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); | |
| ctx.fillStyle = isDark ? `rgba(255,255,255,${p.opacity * 0.3})` : `rgba(99,102,241,${p.opacity * 0.3})`; | |
| ctx.fill(); | |
| }); | |
| requestAnimationFrame(animate); | |
| } | |
| animate(); | |
| window.addEventListener('resize', () => { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| }); | |
| } | |
| // ===== Keyboard ===== | |
| document.addEventListener('keydown', (e) => { | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'n') { e.preventDefault(); openTaskModal(); } | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'd') { e.preventDefault(); toggleDarkMode(); } | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'f') { e.preventDefault(); document.getElementById('search-input').focus(); } | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'p') { e.preventDefault(); togglePomodoroPanel(); } | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'Shift' && e.key === 'F') { e.preventDefault(); toggleFocusMode(); } | |
| if (e.key === 'Escape') { | |
| document.querySelectorAll('.modal.active').forEach(modal => modal.classList.remove('active')); | |
| } | |
| }); | |
| // ===== Notifications ===== | |
| function checkNotifications() { | |
| if (localStorage.getItem('notificationsEnabled') === 'false') return; | |
| if (!('Notification' in window)) return; | |
| if (Notification.permission === 'granted') { | |
| const today = new Date().toISOString().split('T')[0]; | |
| const todayTasks = tasks.filter(t => t.dueDate === today && !t.completed); | |
| if (todayTasks.length > 0) { | |
| new Notification('TaskMaster Pro', { | |
| body: `You have ${todayTasks.length} task${todayTasks.length !== 1 ? 's' : ''} due today!` | |
| }); | |
| } | |
| } else if (Notification.permission !== 'denied') { | |
| Notification.requestPermission(); | |
| } | |
| } | |
| setTimeout(() => { | |
| checkNotifications(); | |
| scheduleReminders(); | |
| loadMyDay(); | |
| }, 2000); | |