diff --git a/css/index.css b/css/index.css index 59ae8c87..6fce1e5e 100644 --- a/css/index.css +++ b/css/index.css @@ -5183,3 +5183,150 @@ body { .subject-sidebar-item:hover .delete-subject-btn { opacity: 1; } + +/* Delete confirmation dialog */ +body.confirm-open { + overflow: hidden; +} + +.custom-confirm-backdrop { + position: fixed; + inset: 0; + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + background: rgba(0, 0, 0, 0.52); + backdrop-filter: blur(8px); + animation: confirm-backdrop-in 0.18s ease-out both; +} + +.custom-confirm-modal { + width: min(420px, 100%); + display: grid; + grid-template-columns: 44px 1fr; + gap: 14px; + padding: 22px; + border: 1px solid var(--color-border-tertiary); + border-radius: 14px; + background: var(--color-background-primary); + color: var(--color-text-primary); + box-shadow: 0 24px 80px rgba(0, 0, 0, 0.32); + animation: confirm-modal-in 0.2s cubic-bezier(0.16, 1, 0.3, 1) both; +} + +.custom-confirm-icon { + width: 44px; + height: 44px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 50%; + border: 1px solid var(--color-border-danger); + background: var(--color-background-danger); + color: var(--color-text-danger); + font-size: 22px; + font-weight: 800; + line-height: 1; +} + +.custom-confirm-content h3 { + margin: 0 0 8px; + font-size: 20px; + font-weight: 700; + line-height: 1.2; +} + +.custom-confirm-content p { + margin: 0; + color: var(--color-text-secondary); + font-size: 14px; + line-height: 1.55; +} + +.custom-confirm-actions { + grid-column: 1 / -1; + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 6px; +} + +.custom-confirm-actions .btn { + min-width: 92px; + min-height: 38px; + border-radius: 8px; +} + +.custom-confirm-actions .confirm-ok { + background: var(--color-text-danger); + color: #ffffff; +} + +.custom-confirm-actions .confirm-ok:hover { + background: #7f1d1d; +} + +.custom-confirm-closing { + animation: confirm-backdrop-out 0.18s ease-in both; +} + +.custom-confirm-closing .custom-confirm-modal { + animation: confirm-modal-out 0.18s ease-in both; +} + +@keyframes confirm-backdrop-in { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes confirm-backdrop-out { + from { opacity: 1; } + to { opacity: 0; } +} + +@keyframes confirm-modal-in { + from { + opacity: 0; + transform: translateY(18px) scale(0.96); + } + + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes confirm-modal-out { + from { + opacity: 1; + transform: translateY(0) scale(1); + } + + to { + opacity: 0; + transform: translateY(12px) scale(0.97); + } +} + +@media (max-width: 480px) { + .custom-confirm-modal { + grid-template-columns: 1fr; + gap: 12px; + padding: 20px; + } + + .custom-confirm-icon { + width: 40px; + height: 40px; + } + + .custom-confirm-actions { + flex-direction: column-reverse; + } + + .custom-confirm-actions .btn { + width: 100%; + } +} diff --git a/index.html b/index.html index aa1d4645..a2d71477 100644 --- a/index.html +++ b/index.html @@ -191,8 +191,8 @@

StudyPlan

April 2026
- - + +
Su
Mo
Tu
We
Th
Fr
Sa
diff --git a/js/app.js b/js/app.js index ccec3408..773fa465 100644 --- a/js/app.js +++ b/js/app.js @@ -1409,10 +1409,38 @@ document.addEventListener('DOMContentLoaded', () => { }); } - document.getElementById('cal-next').addEventListener('click', () => { - currentMonthDate.setMonth(currentMonthDate.getMonth() + 1); + const calPrevBtn = document.getElementById('cal-prev'); + const calNextBtn = document.getElementById('cal-next'); + + function moveCalendarMonth(offset) { + currentMonthDate = new Date( + currentMonthDate.getFullYear(), + currentMonthDate.getMonth() + offset, + 1 + ); renderCalendar(); - }); + renderTasks(); + } + + if (calPrevBtn) { + calPrevBtn.addEventListener('click', () => moveCalendarMonth(-1)); + calPrevBtn.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + moveCalendarMonth(-1); + } + }); + } + + if (calNextBtn) { + calNextBtn.addEventListener('click', () => moveCalendarMonth(1)); + calNextBtn.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + moveCalendarMonth(1); + } + }); + } document.getElementById('nav-dashboard').addEventListener('click', (e) => { e.preventDefault(); diff --git a/js/utils/toast.js b/js/utils/toast.js index b03b4e8c..c84e9f95 100644 --- a/js/utils/toast.js +++ b/js/utils/toast.js @@ -2,6 +2,15 @@ * Custom modern toast notifications for StudyPlan */ +function escapeHtml(value) { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + class ToastManager { constructor() { this.container = document.createElement('div'); @@ -12,35 +21,32 @@ class ToastManager { show(message, type = 'info', duration = 3000) { const toast = document.createElement('div'); toast.className = `toast-notification toast-${type}`; - + let icon = ''; - if (type === 'success') icon = '✅'; - else if (type === 'error') icon = '❌'; - else if (type === 'warning') icon = '⚠'; - else icon = 'ℹ️'; + if (type === 'success') icon = '\u2705'; + else if (type === 'error') icon = '\u274c'; + else if (type === 'warning') icon = '\u26a0'; + else icon = '\u2139\uFE0F'; - // Handle messages that already have emojis at the start to avoid double icons let cleanMessage = message; - if (/^[✅❌⚠]/.test(message)) { + if (/^[\u2705\u274c\u26a0]/.test(message)) { icon = message.charAt(0); cleanMessage = message.substring(1).trim(); } toast.innerHTML = ` -
${icon}
-
${cleanMessage}
+
${escapeHtml(icon)}
+
${escapeHtml(cleanMessage)}
`; this.container.appendChild(toast); - // Setup close button const closeBtn = toast.querySelector('.toast-close'); closeBtn.addEventListener('click', () => { this.closeToast(toast); }); - // Auto dismiss if (duration > 0) { setTimeout(() => { this.closeToast(toast); @@ -62,45 +68,66 @@ class ToastManager { return new Promise((resolve) => { const backdrop = document.createElement('div'); backdrop.className = 'custom-confirm-backdrop'; - + backdrop.setAttribute('role', 'presentation'); + const modal = document.createElement('div'); - modal.className = 'custom-confirm-modal modal-card'; - + modal.className = 'custom-confirm-modal'; + modal.setAttribute('role', 'dialog'); + modal.setAttribute('aria-modal', 'true'); + modal.setAttribute('aria-labelledby', 'custom-confirm-title'); + modal.setAttribute('aria-describedby', 'custom-confirm-message'); + modal.innerHTML = ` -

Confirm Action

-

${message}

-
- - + +
+

Confirm deletion

+

${escapeHtml(message)}

+
+
+ +
`; - + backdrop.appendChild(modal); document.body.appendChild(backdrop); + document.body.classList.add('confirm-open'); - // Animation in - backdrop.style.animation = 'fadeIn 0.2s ease-out forwards'; - modal.style.animation = 'slideUp 0.2s ease-out forwards'; + const cancelBtn = backdrop.querySelector('.confirm-cancel'); + const okBtn = backdrop.querySelector('.confirm-ok'); + + let isClosing = false; const close = (result) => { - backdrop.style.animation = 'fadeOut 0.2s ease-out forwards'; - modal.style.animation = 'slideDown 0.2s ease-out forwards'; + if (isClosing) return; + isClosing = true; + backdrop.classList.add('custom-confirm-closing'); + document.removeEventListener('keydown', onKeyDown); setTimeout(() => { if (backdrop.parentNode) { backdrop.parentNode.removeChild(backdrop); } + document.body.classList.remove('confirm-open'); resolve(result); - }, 200); // match animation duration + }, 180); }; - backdrop.querySelector('.confirm-cancel').addEventListener('click', () => close(false)); - backdrop.querySelector('.confirm-ok').addEventListener('click', () => close(true)); - + function onKeyDown(e) { + if (e.key === 'Escape') close(false); + } + + cancelBtn.addEventListener('click', () => close(false)); + okBtn.addEventListener('click', () => close(true)); + backdrop.addEventListener('click', (e) => { if (e.target === backdrop) close(false); }); + + document.addEventListener('keydown', onKeyDown); + okBtn.focus(); }); } } export const Toast = new ToastManager(); + 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": {