From 87cdcfaa7ab9f8ac576c39c07df07f557d7281a8 Mon Sep 17 00:00:00 2001 From: Nilamma Date: Sun, 24 May 2026 22:26:53 +0530 Subject: [PATCH] fix: prevent accidental multiple CSV downloads Signed-off-by: Nilamma --- css/index.css | 71 +++++++++++++++++++++++++- index.html | 12 +++++ js/app.js | 107 +++++++++++++++++++++++++++++++++++++++- package-lock.json | 13 +++++ tests/exportIcs.test.js | 5 ++ 5 files changed, 206 insertions(+), 2 deletions(-) diff --git a/css/index.css b/css/index.css index f5561bd..7009641 100644 --- a/css/index.css +++ b/css/index.css @@ -1649,11 +1649,80 @@ body { } } +/* Export Data Modal */ +#export-data-modal { + position: fixed; + inset: 0; + z-index: 9998; + display: none; + align-items: center; + justify-content: center; + background-color: rgba(0, 0, 0, 0.55) !important; +} + +#export-data-modal .modal-card { + width: 100%; + max-width: 360px; + background-color: var(--color-background-primary); + border-radius: 14px; + padding: 20px 20px 16px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1); + color: var(--color-text-primary); + box-sizing: border-box; + border: 1px solid var(--color-border-tertiary); + animation: export-modal-in 0.16s ease-out; +} + +#export-data-modal .modal-card h3 { + margin: 0 0 8px; + font-size: 18px; + font-weight: 600; + letter-spacing: 0.01em; + color: var(--color-text-primary); +} + +#export-data-modal .modal-card p { + margin: 0 0 16px; + font-size: 14px; + line-height: 1.5; + color: var(--color-text-secondary); +} + +#export-data-modal .modal-card > div { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +#export-data-modal .btn { + font-size: 13px; + padding: 8px 16px; + border-radius: 6px; + transition: all 0.2s ease; +} + +#export-data-modal .btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +@keyframes export-modal-in { + from { + opacity: 0; + transform: translateY(6px) scale(0.97); + } + + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} @media (max-width: 480px) { #new-task-modal .modal-card, - #new-subject-modal .modal-card { + #new-subject-modal .modal-card, + #export-data-modal .modal-card { max-width: 90vw; padding: 16px 14px 12px; } diff --git a/index.html b/index.html index 3333d17..97274b6 100644 --- a/index.html +++ b/index.html @@ -511,5 +511,17 @@

New task

+ + + diff --git a/js/app.js b/js/app.js index 787d08a..67f23d2 100644 --- a/js/app.js +++ b/js/app.js @@ -78,6 +78,10 @@ let currentMonthDate = new Date(); let selectedDate = null; let currentView = 'calendar'; // 'calendar', 'all-tasks', 'archived' +// Export modal state +let isExportOpen = false; +let isDownloading = false; + const tasksSection = document.getElementById('tasks-section'); const focusSection = document.getElementById('focus-section'); const extractPreview = document.getElementById('extract-preview'); @@ -438,6 +442,74 @@ async function downloadCalendar() { } } +// Export Modal Functions +function openExportModal() { + const modal = document.getElementById('export-data-modal'); + if (modal) { + isExportOpen = true; + modal.style.display = 'flex'; + // Set focus to the download button for accessibility + const downloadBtn = document.getElementById('export-data-download'); + if (downloadBtn) downloadBtn.focus(); + } +} + +function closeExportModal() { + const modal = document.getElementById('export-data-modal'); + if (modal) { + isExportOpen = false; + modal.style.display = 'none'; + } +} + +// Refactored download handler with duplicate prevention +async function handleExportDownload() { + // Prevent duplicate downloads + if (isDownloading) return; + + const downloadBtn = document.getElementById('export-data-download'); + const cancelBtn = document.getElementById('export-data-cancel'); + + try { + isDownloading = true; + if (downloadBtn) { + downloadBtn.disabled = true; + downloadBtn.innerHTML = 'Downloading...'; + } + if (cancelBtn) cancelBtn.disabled = true; + + const response = await fetch('/api/download'); + + if (!response.ok) { + throw new Error('Failed to download data'); + } + + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'study_data.csv'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + setTimeout(() => URL.revokeObjectURL(url), 100); + + Toast.show('Data downloaded successfully', 'success'); + closeExportModal(); + + } catch (error) { + console.error(error); + Toast.show('Failed to download data', 'error'); + } finally { + isDownloading = false; + if (downloadBtn) { + downloadBtn.disabled = false; + downloadBtn.innerHTML = 'Download CSV'; + } + if (cancelBtn) cancelBtn.disabled = false; + } +} + function renderTasks() { const tasks = store.tasks; const subjects = store.subjects; @@ -1214,7 +1286,40 @@ pasteInput.addEventListener('input', () => { }); downloadBtn.addEventListener('click', () => { - downloadData(); + openExportModal(); +}); + +// Export modal button listeners +const exportCancelBtn = document.getElementById('export-data-cancel'); +const exportDownloadBtn = document.getElementById('export-data-download'); + +if (exportCancelBtn) { + exportCancelBtn.addEventListener('click', () => { + closeExportModal(); + }); +} + +if (exportDownloadBtn) { + exportDownloadBtn.addEventListener('click', () => { + handleExportDownload(); + }); +} + +// Close modal when clicking outside (on backdrop) +const exportModal = document.getElementById('export-data-modal'); +if (exportModal) { + exportModal.addEventListener('click', (e) => { + if (e.target === exportModal) { + closeExportModal(); + } + }); +} + +// Close modal with Escape key for accessibility +document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && isExportOpen) { + closeExportModal(); + } }); // Motivational Quotes diff --git a/package-lock.json b/package-lock.json index 329712e..5652882 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1311,6 +1311,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", diff --git a/tests/exportIcs.test.js b/tests/exportIcs.test.js index 189b5d5..0ff3574 100644 --- a/tests/exportIcs.test.js +++ b/tests/exportIcs.test.js @@ -1,6 +1,11 @@ const test = require('node:test'); const assert = require('node:assert/strict'); +// Note: Frontend download behavior has been updated to use an export modal +// to prevent accidental duplicate downloads. User must click "Download CSV" in the modal +// to trigger the actual download. These tests verify the backend CSV/ICS generation logic +// which remains unchanged. + const { buildCalendarIcs, formatIcsDate,