Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 70 additions & 1 deletion css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
12 changes: 12 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -511,5 +511,17 @@ <h3 style="margin:0 0 12px; font-size:18px; font-weight:600;">New task</h3>
</div>
</div>
</div>

<!-- Export Data Modal -->
<div id="export-data-modal" class="modal-backdrop" style="display:none; position:fixed; inset:0; z-index:9998; align-items:center; justify-content:center;">
<div class="modal-card" style="border-radius:12px; padding:20px; width:360px; box-shadow:0 12px 40px rgba(0,0,0,0.25);">
<h3 style="margin:0 0 12px; font-size:18px; font-weight:600;">Export Study Data</h3>
<p style="margin:0 0 16px; font-size:14px; line-height:1.5; color:#6b7280;">Download your study data as CSV for backup or analysis.</p>
<div style="display:flex; justify-content:flex-end; gap:8px;">
<button id="export-data-cancel" class="btn" style="padding:6px 12px;" aria-label="Cancel export">Cancel</button>
<button id="export-data-download" class="btn btn-primary" style="padding:6px 12px;" aria-label="Download CSV file">Download CSV</button>
</div>
</div>
</div>
</body>
</html>
107 changes: 106 additions & 1 deletion js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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 = '<span aria-live="polite">Downloading...</span>';
}
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;
Expand Down Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions tests/exportIcs.test.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down