From a9eb7eb0c56dc04db432840ea22890db705c44bf Mon Sep 17 00:00:00 2001 From: siddhipatel Date: Fri, 3 Jul 2026 13:54:09 +0530 Subject: [PATCH] fix: resolve stacked confirmation modals via global event delegation --- .env.example | 2 +- css/index.css | 86 ++++++ index.html | 2 +- js/app.js | 762 ++++++++++++++++++---------------------------- js/store.js | 3 + package-lock.json | 2 +- 6 files changed, 380 insertions(+), 477 deletions(-) diff --git a/.env.example b/.env.example index 3453588c..47626090 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ # Google Gemini API Key # Get your key at: https://aistudio.google.com/app/apikey -GEMINI_API_KEY=your_gen_ai_key_here +GEMINI_API_KEY=#random # Server Port (optional, defaults to 3000) PORT=3000 diff --git a/css/index.css b/css/index.css index 1db3b97a..d8c204ec 100644 --- a/css/index.css +++ b/css/index.css @@ -5196,3 +5196,89 @@ body { .subject-sidebar-item:hover .delete-subject-btn { opacity: 1; } + +/* ================= Toast notifications ================= */ +/* Fixed overlay, stacked in the top-right corner of the viewport */ +.toast-container { + position: fixed; + top: 20px; + right: 20px; + z-index: 10000; + display: flex; + flex-direction: column; + gap: 10px; + pointer-events: none; /* clicks pass through empty space in the container */ +} + +.toast-notification { + pointer-events: auto; /* the toast itself is still clickable (close button) */ + display: flex; + align-items: center; + gap: 10px; + min-width: 260px; + max-width: 360px; + padding: 12px 14px; + border-radius: var(--border-radius-md); + background: var(--color-background-primary); + border: 1px solid var(--color-border-tertiary); + box-shadow: var(--shadow-lg); + animation: toastSlideIn 0.2s ease-out; + color: var(--color-text-primary); + font-size: 14px; +} + +.toast-notification.toast-success { border-color: var(--color-border-success); } +.toast-notification.toast-error { border-color: var(--color-border-danger); } +.toast-notification.toast-warning { border-color: rgba(133, 79, 11, 0.3); } +.toast-notification.toast-info { border-color: var(--color-border-info); } + +.toast-icon { font-size: 16px; flex-shrink: 0; } +.toast-message { flex: 1; line-height: 1.4; } +.toast-close { + background: none; + border: none; + cursor: pointer; + font-size: 16px; + color: var(--color-text-tertiary); + flex-shrink: 0; + padding: 0 2px; +} +.toast-close:hover { color: var(--color-text-primary); } + +.toast-hiding { + animation: toastSlideOut 0.2s ease-in forwards; +} + +@keyframes toastSlideIn { + from { transform: translateX(30px); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +@keyframes toastSlideOut { + from { transform: translateX(0); opacity: 1; } + to { transform: translateX(30px); opacity: 0; } +} + +/* ================= Confirm modal ================= */ +/* Fixed, centered, dimmed overlay */ +.custom-confirm-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.55); + z-index: 10001; + display: flex; + align-items: center; + justify-content: center; +} + +.custom-confirm-modal { + background: var(--color-background-primary); + border-radius: 14px; + padding: 20px; + width: 100%; + max-width: 340px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35); + border: 1px solid var(--color-border-tertiary); + color: var(--color-text-primary); + box-sizing: border-box; +} \ No newline at end of file diff --git a/index.html b/index.html index 8fe1303d..97ffa52c 100644 --- a/index.html +++ b/index.html @@ -44,7 +44,7 @@

Wel Sign Up

- + diff --git a/js/app.js b/js/app.js index 6842db4e..6951bcef 100644 --- a/js/app.js +++ b/js/app.js @@ -4,6 +4,11 @@ import { initGlobalErrorBoundary } from './utils/errorBoundary.js'; import { analyzeWorkload } from './utils/scheduler.js'; import { Toast } from './utils/toast.js'; +if (window.__studyplanAppInitialized) { + console.warn('app.js already initialized — skipping duplicate setup'); +} else { + window.__studyplanAppInitialized = true; + initGlobalErrorBoundary(); function getLabelColor(labelStr) { @@ -307,40 +312,26 @@ function renderSidebarSubjects() { listEl.innerHTML = subjects.map(s => { const n = countBySubject[s.id] ?? 0; const safeColor = s.color ? escapeHtml(s.color) : 'var(--color-text-info)'; -return ` - -`; + return ` + + `; }).join(''); - document.querySelectorAll('.delete-subject-btn') - .forEach(btn => { + document.querySelectorAll('.delete-subject-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); - const subjectId = btn.dataset.subjectId; - store.deleteSubject(subjectId); }); }); @@ -400,9 +391,7 @@ function calculateTimeFraction() { } function setCircleDasharray() { - const circleDasharray = `${( - calculateTimeFraction() * FULL_DASH_ARRAY - ).toFixed(0)} 283`; + const circleDasharray = `${(calculateTimeFraction() * FULL_DASH_ARRAY).toFixed(0)} 283`; timerPathRemaining.setAttribute("stroke-dasharray", circleDasharray); } @@ -424,7 +413,6 @@ function loadTimerState() { try { const state = JSON.parse(saved); - TIME_LIMIT = state.TIME_LIMIT || (25 * 60); if (state.isRunning && state.startTime) { @@ -435,7 +423,6 @@ function loadTimerState() { } timeLeft = TIME_LIMIT - timePassed; - if (timeLeft < 0) timeLeft = 0; if (state.durationInput) { @@ -456,20 +443,18 @@ function loadTimerState() { timerText.innerHTML = formatTimeLeft(timeLeft); setCircleDasharray(); updateTimerColor(); - saveTimerState(); if (timeLeft <= 0) { - clearInterval(timerInterval); - timerInterval = null; - localStorage.removeItem('focusTimerState'); - - playCompletionSound(); - showBrowserNotification(); - Toast.show('Focus session complete!', 'success'); - - resetTimer(); -} + clearInterval(timerInterval); + timerInterval = null; + localStorage.removeItem('focusTimerState'); + + playCompletionSound(); + showBrowserNotification(); + Toast.show('Focus session complete!', 'success'); + resetTimer(); + } }, 250); timerPauseBtn.classList.remove('hidden'); @@ -480,6 +465,7 @@ function loadTimerState() { console.error('Failed to load timer state', err); } } + function getTimerColor(timeLeft, totalTime) { const fraction = timeLeft / totalTime; if (fraction <= 0.1) return '#ef4444'; @@ -542,7 +528,7 @@ function startTimer() { startTime = Date.now() - (timePassed * 1000); saveTimerState(); - timerInterval = setInterval(() => { + timerInterval = setInterval(() => { timePassed = Math.floor((Date.now() - startTime) / 1000); timeLeft = TIME_LIMIT - timePassed; @@ -569,9 +555,7 @@ function startTimer() { function pauseTimer() { clearInterval(timerInterval); timerInterval = null; - saveTimerState(); - timerPauseBtn.classList.add('hidden'); timerStartBtn.classList.remove('hidden'); } @@ -579,7 +563,6 @@ function pauseTimer() { function resetTimer() { clearInterval(timerInterval); timerInterval = null; - localStorage.removeItem('focusTimerState'); timePassed = 0; @@ -587,7 +570,6 @@ function resetTimer() { timeLeft = TIME_LIMIT; timerDurationInput.disabled = false; - timerText.innerHTML = formatTimeLeft(timeLeft); timerPathRemaining.setAttribute("stroke-dasharray", "283 283"); updateTimerColor(); @@ -735,7 +717,6 @@ function renderProfileSection() {

View your account summary, study stats, and future account settings in one place.

-

Account details

@@ -752,7 +733,6 @@ function renderProfileSection() { ${escapeHtml(joinedDate)}
-

Study statistics

@@ -775,7 +755,6 @@ function renderProfileSection() {
-

Account overview

Your profile information and study statistics will update automatically as you use StudyPlan.

@@ -807,10 +786,7 @@ function formatDate(dateStr) { async function downloadData() { try { const response = await fetch('/api/download'); - - if (!response.ok) { - throw new Error('Failed to download data'); - } + if (!response.ok) throw new Error('Failed to download data'); const blob = await response.blob(); const url = URL.createObjectURL(blob); @@ -821,35 +797,30 @@ async function downloadData() { a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 100); - } catch (error) { - console.error(error); - Toast.show('Failed to download data', 'error'); + console.error(error); + Toast.show('Failed to download data', 'error'); } } async function downloadCalendar() { - try { - const response = await fetch('/api/download/calendar'); - - if (!response.ok) { - throw new Error('Failed to export calendar'); - } + try { + const response = await fetch('/api/download/calendar'); + if (!response.ok) throw new Error('Failed to export calendar'); - const blob = await response.blob(); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'studyplan_calendar.ics'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - setTimeout(() => URL.revokeObjectURL(url), 100); - - } catch (error) { - console.error(error); - alert('Failed to export calendar'); - } + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'studyplan_calendar.ics'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + setTimeout(() => URL.revokeObjectURL(url), 100); + } catch (error) { + console.error(error); + alert('Failed to export calendar'); + } } function renderTasks() { @@ -860,11 +831,9 @@ function renderTasks() { if (subjects.length === 0) return; // Wait for subjects to load - // Filter based on archived status const activeTasks = tasks.filter(t => !t.archived); const archivedTasks = tasks.filter(t => t.archived); - // Update badges const allTasksBadge = document.querySelector('#all-tasks-btn .badge'); if (allTasksBadge) { allTasksBadge.textContent = activeTasks.length; @@ -874,14 +843,11 @@ function renderTasks() { archivedBadge.textContent = archivedTasks.length; } - - const displayTasksRaw = currentView === 'archived' ? archivedTasks : activeTasks; const displayTasks = activeLabelFilter ? displayTasksRaw.filter(t => t.labels && t.labels.includes(activeLabelFilter)) : displayTasksRaw; - // Extract unique labels to populate the filter dropdown if (labelFilterSelect) { const uniqueLabels = new Set(); store.tasks.forEach(t => { @@ -890,7 +856,6 @@ function renderTasks() { } }); - // Store current selection to restore it const currentSel = labelFilterSelect.value; let optionsHtml = ''; Array.from(uniqueLabels).sort().forEach(lbl => { @@ -900,7 +865,6 @@ function renderTasks() { } const sorted = [...displayTasks].sort((a,b) => new Date(a.due_at) - new Date(b.due_at)); - const now = new Date(); const dueSoon = []; @@ -951,11 +915,9 @@ function renderTasks() { }); } - items.forEach(t => { const sub = subjects.find(s => s.id === t.subject_id) || subjects[0]; const isDone = t.status === 'Done'; - const isHighPriority = t.priority === 'high'; const isOverdue = !isDone && t.due_at && new Date(t.due_at) < now; const isUrgent = isHighPriority && title === '⚠ Due soon'; @@ -972,7 +934,6 @@ function renderTasks() { ).join(''); const localDate = t.due_at ? new Date(t.due_at).toISOString().substring(0, 16) : ''; - const isHighPriority = t.priority === 'high'; const editDurationUnit = t.is_estimated_duration_min === 0 ? 'hours' : 'minutes'; const editDurationValue = t.estimated_duration ? (editDurationUnit === 'hours' ? Math.round(Number(t.estimated_duration) / 60) : Number(t.estimated_duration)) @@ -984,13 +945,10 @@ function renderTasks() { - - -
@@ -1000,16 +958,13 @@ function renderTasks() {
- - -
@@ -1030,62 +985,49 @@ function renderTasks() { labelsHtml = t.labels.map(l => `${l}`).join(' '); } - html += ` -
- - - -
-
${t.title}
- -
- - ${isDone ? 'Done' : 'Due ' + formatDate(t.due_at)} - - - - ${sub.short_code} - - - ${labelsHtml} -
-
- -
- ${actionButtons} + html += ` +
+ +
+
${t.title}
+
+ + ${isDone ? 'Done' : 'Due ' + formatDate(t.due_at)} + + + ${sub.short_code} + + ${labelsHtml}
- `; - } - }); - html += `
`; - return html; - }; +
+ ${actionButtons} +
+
+ `; + } + }); + html += `
`; + return html; + }; + if (currentView === 'calendar' && selectedDate) { const selStr = selectedDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); const actionBar = `
- - -
`; + + +
`; const emptyState = dueSoon.length === 0 && completed.length === 0 ? `
@@ -1100,18 +1042,11 @@ function renderTasks() { : ''; const totalMinutes = [...dueSoon, ...completed].reduce((acc, t) => acc + (Number(t.estimated_duration) || 0), 0); - console.log('[Daily Study Time] Calendar view with selected date:', selectedDate); - console.log('[Daily Study Time] Pending tasks:', dueSoon.length, 'Completed tasks:', completed.length); - console.log('[Daily Study Time] Task durations:', [...dueSoon, ...completed].map(t => ({ title: t.title, estimated_duration: t.estimated_duration }))); - console.log('[Daily Study Time] Total minutes calculated:', totalMinutes); const studyTimeEl = document.getElementById('daily-study-time'); const studyTimeValueEl = document.getElementById('daily-study-time-value'); if (studyTimeEl && studyTimeValueEl) { studyTimeEl.style.display = 'flex'; studyTimeValueEl.textContent = formatDuration(totalMinutes); - console.log('[Daily Study Time] Banner shown with value:', formatDuration(totalMinutes)); - } else { - console.log('[Daily Study Time] Banner elements not found'); } tasksSection.innerHTML = actionBar + @@ -1121,54 +1056,32 @@ function renderTasks() { } else { const selectedCount = getSelectedTasks().length; const bulkToolbar = ` -
- ${selectedCount} selected - - - - - - - - - - -
- `; +
+ ${selectedCount} selected + + + + + +
+ `; + + const actionBar = currentView === 'archived' + ? '' + : ` + ${bulkToolbar} +
+ +
+ `; -const actionBar = currentView === 'archived' - ? '' - : ` - ${bulkToolbar} - -
- -
- `; - console.log('[Daily Study Time] Hiding banner - currentView:', currentView, 'selectedDate:', selectedDate); const studyTimeEl = document.getElementById('daily-study-time'); if (studyTimeEl) { studyTimeEl.style.display = 'none'; } - - const titlePrefix = currentView === 'archived' ? 'Archived: ' : ''; const emptyStateTitle = currentView === 'archived' ? 'No archived tasks' : 'Start your journey'; const emptyStateText = currentView === 'archived' @@ -1189,34 +1102,34 @@ const actionBar = currentView === 'archived'
` : ''; - tasksSection.innerHTML = - actionBar + - renderGroup(titlePrefix + '⚠ Due soon', dueSoon, 'var(--color-text-danger)', true) + - renderGroup(titlePrefix + 'This week', thisWeek, 'var(--color-text-secondary)', true) + - renderGroup(titlePrefix + 'Completed', completed, 'var(--color-text-tertiary)') + - emptyState; - -document.querySelectorAll('.task-item').forEach(taskEl => { - const selectTask = () => { - const taskId = taskEl.dataset.id; - const task = store.tasks.find(t => String(t.id) === String(taskId)); - if (!taskId || (task && task._isEditing)) return; - toggleTaskSelection(taskId); - }; + tasksSection.innerHTML = actionBar + + renderGroup(titlePrefix + '⚠ Due soon', dueSoon, 'var(--color-text-danger)', true) + + renderGroup(titlePrefix + 'This week', thisWeek, 'var(--color-text-secondary)', true) + + renderGroup(titlePrefix + 'Completed', completed, 'var(--color-text-tertiary)') + + emptyState; - taskEl.addEventListener('click', (e) => { - if (e.target.closest('button, input, .task-actions, .edit-field')) return; - selectTask(); - }); + document.querySelectorAll('.task-item').forEach(taskEl => { + const selectTask = () => { + const taskId = taskEl.dataset.id; + const task = store.tasks.find(t => String(t.id) === String(taskId)); + if (!taskId || (task && task._isEditing)) return; + toggleTaskSelection(taskId); + }; + + taskEl.addEventListener('click', (e) => { + if (e.target.closest('button, input, .task-actions, .edit-field')) return; + selectTask(); + }); - taskEl.addEventListener('keydown', (e) => { - if (e.key !== 'Enter' && e.key !== ' ') return; - if (e.target.closest('button, input, .edit-field')) return; - e.preventDefault(); - selectTask(); - }); -}); + taskEl.addEventListener('keydown', (e) => { + if (e.key !== 'Enter' && e.key !== ' ') return; + if (e.target.closest('button, input, .edit-field')) return; + e.preventDefault(); + selectTask(); + }); + }); } + // Bind CTA button in empty state const emptyStateAddBtn = document.getElementById('empty-state-add-btn'); if (emptyStateAddBtn) { @@ -1224,7 +1137,7 @@ document.querySelectorAll('.task-item').forEach(taskEl => { document.getElementById('add-task-btn')?.click(); }); } - + document.querySelectorAll('.edit-task-btn').forEach(el => { el.addEventListener('click', (e) => { e.stopPropagation(); @@ -1256,7 +1169,6 @@ document.querySelectorAll('.task-item').forEach(taskEl => { e.stopPropagation(); const taskId = el.dataset.id; const itemEl = el.closest('.task-item'); - const rawTitle = itemEl.querySelector('.board-edit-title').value; const subject_id = itemEl.querySelector('.board-edit-subject').value; @@ -1306,13 +1218,8 @@ document.querySelectorAll('.task-item').forEach(taskEl => { }); }); - document.querySelectorAll('.delete-task-btn').forEach(el => { - el.addEventListener('click', (e) => { - e.stopPropagation(); - store.deleteTask(el.dataset.id); - }); - }); - + + const markAllPendingBtn = document.getElementById('mark-all-pending-btn'); if (markAllPendingBtn) { markAllPendingBtn.addEventListener('click', (e) => { @@ -1328,53 +1235,8 @@ document.querySelectorAll('.task-item').forEach(taskEl => { store.markPendingTasksForDateCompleted(selectedDate); }); } - const bulkCompleteBtn = - document.getElementById('bulk-complete-btn'); - -if (bulkCompleteBtn) { - bulkCompleteBtn.addEventListener('click', () => { - store.bulkCompleteTasks(); - }); -} - -const bulkArchiveBtn = - document.getElementById('bulk-archive-btn'); - -if (bulkArchiveBtn) { - bulkArchiveBtn.addEventListener('click', () => { - store.bulkArchiveTasks(); - }); -} - -const bulkDeleteBtn = - document.getElementById('bulk-delete-btn'); - -if (bulkDeleteBtn) { - bulkDeleteBtn.addEventListener('click', () => { - store.bulkDeleteTasks(); - }); -} - -const clearSelectionBtn = - document.getElementById('clear-selection-btn'); - -if (clearSelectionBtn) { - clearSelectionBtn.addEventListener('click', () => { - store.clearSelectedTasks(); - }); -} - -const selectAllBtn = - document.getElementById('select-all-btn'); - -if (selectAllBtn) { - selectAllBtn.addEventListener('click', () => { - store.selectAllTasks(); - }); -} } - const summaryBox = document.getElementById('summary-box'); if (summaryBox) { summaryBox.innerHTML = generateSummary(store.tasks, store.subjects); @@ -1387,7 +1249,6 @@ function renderCalendar() { const year = currentMonthDate.getFullYear(); const month = currentMonthDate.getMonth(); - const monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; calTitle.textContent = `${monthNames[month]} ${year}`; @@ -1397,7 +1258,6 @@ function renderCalendar() { const firstDay = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate(); const prevMonthDays = new Date(year, month, 0).getDate(); - const today = new Date(); let html = `
Su
Mo
Tu
We
Th
Fr
Sa
`; @@ -1410,7 +1270,6 @@ function renderCalendar() { const isToday = i === today.getDate() && month === today.getMonth() && year === today.getFullYear(); const isSelected = selectedDate && i === selectedDate.getDate() && month === selectedDate.getMonth() && year === selectedDate.getFullYear(); - // Find tasks for this day const dayTasks = store.tasks.filter(t => { if (t.archived) return false; if (t.status === 'Done') return false; @@ -1431,7 +1290,6 @@ function renderCalendar() { } const extraStyle = isSelected ? `border: 1.5px solid var(--color-text-primary);` : ''; - html += `
${i} ${indicatorHtml} @@ -1441,13 +1299,11 @@ function renderCalendar() { const totalCells = firstDay + daysInMonth; const nextDays = (7 - (totalCells % 7)) % 7; for (let i = 1; i <= nextDays; i++) { - html += `
${i}
`; + html += `
${i}
`; } calGrid.innerHTML = html; - // Bind day clicks document.querySelectorAll('.interactive-day').forEach(el => { el.addEventListener('click', (e) => { const d = parseInt(e.currentTarget.getAttribute('data-day')); @@ -1478,9 +1334,7 @@ function renderExtraction() { let html = `
Extracted — ${pasteItems.length} items
`; pasteItems.forEach((item, index) => { - // try to match subject name const sub = store.subjects.find(s => s.name.toLowerCase().includes((item.subject_name || '').toLowerCase())) || store.subjects[3]; - // Attach subject id to item so Add will work item.subject_id = sub.id; if (item._isEditing) { @@ -1496,16 +1350,12 @@ function renderExtraction() { - - - -
@@ -1548,7 +1398,6 @@ function renderExtraction() { const title = card.querySelector('.edit-title-input').value; let dateVal = card.querySelector('.edit-date-input').value; const notes = card.querySelector('.edit-notes-input').value; - const newSubject = store.subjects.find(s => s.id === subjectId); store.updateExtractedItem(idx, { @@ -1565,8 +1414,8 @@ function renderExtraction() { function calculateStreak(tasks) { const completedTasks = tasks.filter(t => t.status === 'Done' && t.due_at && !t.archived); - const dates = new Set(); + completedTasks.forEach(t => { const d = new Date(t.due_at); if (!isNaN(d.getTime())) { @@ -1578,41 +1427,36 @@ function calculateStreak(tasks) { const today = new Date(); const todayStr = `${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`; - const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); const yesterdayStr = `${yesterday.getFullYear()}-${yesterday.getMonth()}-${yesterday.getDate()}`; let streak = 0; let checkDate = new Date(); - let checkStr = todayStr; if (dates.has(todayStr)) { streak = 1; } else if (dates.has(yesterdayStr)) { streak = 1; checkDate = yesterday; - checkStr = yesterdayStr; } else { return 0; } while (true) { checkDate.setDate(checkDate.getDate() - 1); - checkStr = `${checkDate.getFullYear()}-${checkDate.getMonth()}-${checkDate.getDate()}`; + const checkStr = `${checkDate.getFullYear()}-${checkDate.getMonth()}-${checkDate.getDate()}`; if (dates.has(checkStr)) { streak++; } else { break; } } - return streak; } function renderStreak() { const streakCount = calculateStreak(store.tasks); - const streakCountEl = document.getElementById('streak-count'); if (streakCountEl) { streakCountEl.textContent = streakCount; @@ -1628,15 +1472,10 @@ function renderStreak() { const tooltip = document.getElementById('streak-tooltip'); if (tooltip) { - if (streakCount >= 30) { - tooltip.textContent = '30 day badge unlocked'; - } else if (streakCount >= 7) { - tooltip.textContent = '7 day badge unlocked'; - } else if (streakCount >= 3) { - tooltip.textContent = '3 day badge unlocked'; - } else { - tooltip.textContent = 'Complete tasks to build streak & earn cool badges'; - } + if (streakCount >= 30) tooltip.textContent = '30 day badge unlocked'; + else if (streakCount >= 7) tooltip.textContent = '7 day badge unlocked'; + else if (streakCount >= 3) tooltip.textContent = '3 day badge unlocked'; + else tooltip.textContent = 'Complete tasks to build streak & earn cool badges'; } } @@ -1649,6 +1488,59 @@ store.subscribe(renderSidebarSubjects); store.subscribe(renderStreak); document.addEventListener('DOMContentLoaded', () => { + +const globalTasksSection = document.getElementById('tasks-section'); + +if (globalTasksSection) { + globalTasksSection.addEventListener('click', async (e) => { + // Find if the clicked element or its parent is a delete button + const deleteBtn = e.target.closest('.delete-task-btn'); + if (!deleteBtn) return; + + // Stop propagation and prevent default behavior + e.preventDefault(); + e.stopPropagation(); + + const taskId = deleteBtn.dataset.id; + if (!taskId) return; + + // store.deleteTask() already asks for confirmation and shows + // a success toast internally, so we don't need to repeat it here. + store.deleteTask(taskId); + }); +} + + const bulkCompleteBtn = document.getElementById('bulk-complete-btn'); + if (bulkCompleteBtn) { + bulkCompleteBtn.addEventListener('click', () => store.bulkCompleteTasks()); + } + + const bulkArchiveBtn = document.getElementById('bulk-archive-btn'); + if (bulkArchiveBtn) { + bulkArchiveBtn.addEventListener('click', () => store.bulkArchiveTasks()); + } + + const bulkDeleteBtn = document.getElementById('bulk-delete-btn'); + if (bulkDeleteBtn) { + bulkDeleteBtn.addEventListener('click', async () => { + const confirmed = await Toast.confirm("Are you sure you want to permanently delete all selected tasks?"); + if (confirmed) { + store.bulkDeleteTasks(); + Toast.show("Selected tasks deleted successfully", "success"); + } + }); + } + + const clearSelectionBtn = document.getElementById('clear-selection-btn'); + if (clearSelectionBtn) { + clearSelectionBtn.addEventListener('click', () => store.clearSelectedTasks()); + } + + const selectAllBtn = document.getElementById('select-all-btn'); + if (selectAllBtn) { + selectAllBtn.addEventListener('click', () => store.selectAllTasks()); + } + if (newSubjectColorsEl) { SUBJECT_COLORS.forEach(c => { const btn = document.createElement('button'); @@ -1810,18 +1702,12 @@ document.addEventListener('DOMContentLoaded', () => { renderTasks(); }); -//NEw Task addition event listeners -newTaskBtn.addEventListener('click', () => { - - if (!store.subjects || store.subjects.length === 0) { - Toast.show('Subjects are still loading. Please try again in a moment.', 'warning'); - return; - } - - newTaskSubject.innerHTML = store.subjects - .map(s => ``) - .join(''); - + newTaskBtn.addEventListener('click', () => { + if (!store.subjects || store.subjects.length === 0) { + Toast.show('Subjects are still loading. Please try again in a moment.', 'warning'); + return; + } + newTaskSubject.innerHTML = store.subjects.map(s => ``).join(''); if (selectedDate) { const d = new Date(selectedDate); @@ -1830,10 +1716,10 @@ newTaskBtn.addEventListener('click', () => { } else { newTaskDate.value = ''; } - newTaskTitle.value = ''; - newTaskNotes.value = ''; - if (newTaskEstimatedDuration) newTaskEstimatedDuration.value = ''; - setNewTaskDurationUnit('minutes'); + newTaskTitle.value = ''; + newTaskNotes.value = ''; + if (newTaskEstimatedDuration) newTaskEstimatedDuration.value = ''; + setNewTaskDurationUnit('minutes'); newTaskModal.style.display = 'flex'; }); @@ -1841,83 +1727,77 @@ newTaskBtn.addEventListener('click', () => { newTaskModal.style.display = 'none'; }); -newTaskModal.addEventListener('click', (e) => { - if (e.target === newTaskModal) { - newTaskModal.style.display = 'none'; - } + newTaskModal.addEventListener('click', (e) => { + if (e.target === newTaskModal) newTaskModal.style.display = 'none'; }); - -function setNewTaskDurationUnit(unit) { - selectedTaskDurationUnit = unit; - newTaskDurationSwitch?.setAttribute('data-unit', unit); - newTaskDurationMin?.classList.toggle('active', unit === 'minutes'); - newTaskDurationHr?.classList.toggle('active', unit === 'hours'); -} - -newTaskDurationMin?.addEventListener('click', () => setNewTaskDurationUnit('minutes')); -newTaskDurationHr?.addEventListener('click', () => setNewTaskDurationUnit('hours')); - -newTaskSave.addEventListener('click', async () => { - const rawTitle = newTaskTitle.value.trim(); - const subject_id = newTaskSubject.value; - const notes = newTaskNotes.value.trim(); - const dateVal = newTaskDate.value; - const durationValue = newTaskEstimatedDuration ? Number(newTaskEstimatedDuration.value) : 0; - const estimated_duration = durationValue > 0 - ? Math.round(selectedTaskDurationUnit === 'hours' ? durationValue * 60 : durationValue) - : null; - - if (!rawTitle) { - alert('Please enter a task name'); - return; + + function setNewTaskDurationUnit(unit) { + selectedTaskDurationUnit = unit; + newTaskDurationSwitch?.setAttribute('data-unit', unit); + newTaskDurationMin?.classList.toggle('active', unit === 'minutes'); + newTaskDurationHr?.classList.toggle('active', unit === 'hours'); } - if (!dateVal) { - alert('Please enter a deadline'); - return; -} - -if (!subject_id) { - alert('Please select a subject'); - return; -} - const { cleanTitle, labels } = extractLabels(rawTitle); - const due_at = dateVal ? new Date(dateVal).toISOString() : ''; - - const newTask = { - title: cleanTitle || rawTitle, - subject_id, - due_at, - notes, - priority: 'medium', - status: 'Not Started', - archived: 0, - estimated_duration, - is_estimated_duration_min: selectedTaskDurationUnit === 'minutes' ? 1 : 0, - labels - }; + newTaskDurationMin?.addEventListener('click', () => setNewTaskDurationUnit('minutes')); + newTaskDurationHr?.addEventListener('click', () => setNewTaskDurationUnit('hours')); + + newTaskSave.addEventListener('click', async () => { + const rawTitle = newTaskTitle.value.trim(); + const subject_id = newTaskSubject.value; + const notes = newTaskNotes.value.trim(); + const dateVal = newTaskDate.value; + const durationValue = newTaskEstimatedDuration ? Number(newTaskEstimatedDuration.value) : 0; + const estimated_duration = durationValue > 0 + ? Math.round(selectedTaskDurationUnit === 'hours' ? durationValue * 60 : durationValue) + : null; + + if (!rawTitle) { + alert('Please enter a task name'); + return; + } + if (!dateVal) { + alert('Please enter a deadline'); + return; + } + if (!subject_id) { + alert('Please select a subject'); + return; + } + const { cleanTitle, labels } = extractLabels(rawTitle); + const due_at = dateVal ? new Date(dateVal).toISOString() : ''; + + const newTask = { + title: cleanTitle || rawTitle, + subject_id, + due_at, + notes, + priority: 'medium', + status: 'Not Started', + archived: 0, + estimated_duration, + is_estimated_duration_min: selectedTaskDurationUnit === 'minutes' ? 1 : 0, + labels + }; await store.addTasks([newTask]); newTaskModal.style.display = 'none'; }); - -addItemsBtn.addEventListener('click', () => { - if (store.currentPaste) { - const pasteWithLabels = store.currentPaste.map(t => { - const { cleanTitle, labels } = extractLabels(t.title); - return { ...t, title: cleanTitle || t.title, labels }; - }); - store.addTasks(pasteWithLabels); - store.clearExtracted(); - pasteInput.value = ''; - } -}); + addItemsBtn.addEventListener('click', () => { + if (store.currentPaste) { + const pasteWithLabels = store.currentPaste.map(t => { + const { cleanTitle, labels } = extractLabels(t.title); + return { ...t, title: cleanTitle || t.title, labels }; + }); + store.addTasks(pasteWithLabels); + store.clearExtracted(); + pasteInput.value = ''; + } + }); }); -// Ensures the button is hidden on initial page load if the textarea is empty if (pasteInput.value.trim() === "") { - clearBtn.style.display = 'none'; + clearBtn.style.display = 'none'; } extractBtn.addEventListener('click', async () => { @@ -1928,28 +1808,24 @@ extractBtn.addEventListener('click', async () => { extractBtn.disabled = true; const items = await extractTasksFromText(text); - extractBtn.innerHTML = 'Extract with AI →'; extractBtn.disabled = false; - store.setExtracted(items); }); -// Wipes the text, clears the store, hides the button, and refocuses the cursor clearBtn.addEventListener('click', () => { - pasteInput.value = ''; - store.clearExtracted(); - clearBtn.style.display = 'none'; // Hides the clear button instantly - pasteInput.focus(); // Puts the typing cursor back in the box + pasteInput.value = ''; + store.clearExtracted(); + clearBtn.style.display = 'none'; + pasteInput.focus(); }); -// Listens to typing/pasting to show or hide the button dynamically pasteInput.addEventListener('input', () => { - if (pasteInput.value.trim().length > 0) { - clearBtn.style.display = 'block'; - } else { - clearBtn.style.display = 'none'; - } + if (pasteInput.value.trim().length > 0) { + clearBtn.style.display = 'block'; + } else { + clearBtn.style.display = 'none'; + } }); downloadBtn.addEventListener('click', () => { @@ -1959,7 +1835,6 @@ downloadBtn.addEventListener('click', () => { const fileInput = document.getElementById('file-input'); const dropZone = document.getElementById('drop-zone'); -// Handle File Selection via File Explorer if (fileInput) { fileInput.addEventListener('change', (e) => { const file = e.target.files[0]; @@ -1967,9 +1842,7 @@ if (fileInput) { }); } -// Handle Drag & Drop Events if (dropZone) { - // Prevent browser from opening the file ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { dropZone.addEventListener(eventName, (e) => { e.preventDefault(); @@ -1977,24 +1850,20 @@ if (dropZone) { }); }); - // Add highlight effect ['dragenter', 'dragover'].forEach(eventName => { dropZone.addEventListener(eventName, () => { dropZone.classList.add('paste-zone--dragover'); }); }); - // Remove highlight effect ['dragleave', 'drop'].forEach(eventName => { dropZone.addEventListener(eventName, () => { dropZone.classList.remove('paste-zone--dragover'); }); }); - // Handle dropped file dropZone.addEventListener('drop', (e) => { const dt = e.dataTransfer; - if (dt && dt.files.length > 0) { const file = dt.files[0]; handleFileContent(file); @@ -2002,39 +1871,29 @@ if (dropZone) { }); } -// File Reader Function function handleFileContent(file) { const allowedExtensions = ['txt', 'md', 'json']; const fileExtension = file.name.split('.').pop().toLowerCase(); - // Validate extension if (!allowedExtensions.includes(fileExtension)) { alert('Invalid file format. Please upload a .txt, .md, or .json file.'); return; } const reader = new FileReader(); - reader.onload = (e) => { const pasteInput = document.getElementById('paste-input'); - if (pasteInput) { pasteInput.value = e.target.result; - - alert( - `Loaded "${file.name}" successfully! Click "Extract with AI" to find your tasks.` - ); + alert(`Loaded "${file.name}" successfully! Click "Extract with AI" to find your tasks.`); } }; - reader.onerror = () => { alert('Error reading file content. Please try again.'); }; - reader.readAsText(file); } - const quotes = [ "Small Progress is still Progress", "Focus on being productive instead of busy", @@ -2049,27 +1908,24 @@ const quotes = [ ]; const quoteEl = document.getElementById('motivational-quotes'); - if (quoteEl) { const today = new Date(); const seed = today.toDateString(); - let hash = 0; - for (let i = 0; i < seed.length; i++) { hash = seed.charCodeAt(i) + ((hash << 5) - hash); } - const index = Math.abs(hash % quotes.length); - quoteEl.textContent = quotes[index]; } -calendarDownloadBtn.addEventListener('click', () => { - downloadCalendar(); -}); -// ================= AUTH FRONTEND ================= +if (calendarDownloadBtn) { + calendarDownloadBtn.addEventListener('click', () => { + downloadCalendar(); + }); +} +// ================= AUTH FRONTEND ================= const authModal = document.getElementById('auth-modal'); const authTitle = document.getElementById('auth-title'); const authSubtitle = document.getElementById('auth-subtitle'); @@ -2077,31 +1933,22 @@ const authSubmitBtn = document.getElementById('auth-submit-btn'); const authToggleBtn = document.getElementById('auth-toggle-btn'); const authToggleText = document.getElementById('auth-toggle-text'); const authError = document.getElementById('auth-error'); - const emailInput = document.getElementById('auth-email'); const passwordInput = document.getElementById('auth-password'); - const logoutBtn = document.getElementById('logout-btn'); let isLoginMode = true; -// ================= CHECK LOGIN ================= - const savedUser = localStorage.getItem('studyplan_user'); - if (savedUser) { authModal.style.display = 'none'; } else { authModal.style.display = 'flex'; } -// ================= TOGGLE LOGIN/SIGNUP ================= - authToggleBtn.addEventListener('click', (e) => { e.preventDefault(); - isLoginMode = !isLoginMode; - authError.style.display = 'none'; if (isLoginMode) { @@ -2119,10 +1966,7 @@ authToggleBtn.addEventListener('click', (e) => { } }); -// ================= LOGIN / SIGNUP ================= - authSubmitBtn.addEventListener('click', async () => { - const email = emailInput.value.trim(); const password = passwordInput.value.trim(); @@ -2132,67 +1976,37 @@ authSubmitBtn.addEventListener('click', async () => { return; } - const endpoint = isLoginMode - ? '/api/auth/login' - : '/api/auth/signup'; - + const endpoint = isLoginMode ? '/api/auth/login' : '/api/auth/signup'; try { - const response = await fetch(endpoint, { method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - email, - password - }) + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }) }); const data = await response.json(); - if (!response.ok) { authError.textContent = data.error || 'Authentication failed'; authError.style.display = 'block'; return; } - // Save logged-in user localStorage.setItem('studyplan_user', email); - authModal.style.display = 'none'; - location.reload(); - } catch (err) { - authError.textContent = 'Server error'; authError.style.display = 'block'; } }); -// ================= LOGOUT ================= - logoutBtn.addEventListener('click', async () => { - try { - - await fetch('/api/auth/logout', { - method: 'POST' - }); - + await fetch('/api/auth/logout', { method: 'POST' }); localStorage.removeItem('studyplan_user'); - location.reload(); - } catch (err) { console.error(err); } }); - - -if (calendarDownloadBtn) { - calendarDownloadBtn.addEventListener('click', () => { - downloadCalendar(); - }); -} +} \ No newline at end of file diff --git a/js/store.js b/js/store.js index 075bc62b..a0010f60 100644 --- a/js/store.js +++ b/js/store.js @@ -17,7 +17,9 @@ export const store = { }, subscribe(listener) { + if(!this.listeners.includes(listener)){ this.listeners.push(listener); + } }, notify() { @@ -269,6 +271,7 @@ export const store = { await fetch(`/api/tasks/${taskId}`, { method: 'DELETE' }); + Toast.show("Task deleted successfully", "success"); // moved here from app.js } catch (e) { this.tasks.splice(taskIndex, 0, removedTask); this.notify(); diff --git a/package-lock.json b/package-lock.json index 56528823..e9685a4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "sqlite3": "^6.0.1" }, "engines": { - "node": "20.x" + "node": ">=20.x" } }, "node_modules/@google/genai": {