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
147 changes: 147 additions & 0 deletions css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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%;
}
}
4 changes: 2 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,8 @@ <h1 class="site-title">StudyPlan</h1>
<div class="cal-section">
<div class="cal-header">
<div id="cal-month-title" class="cal-title">April 2026</div>
<span id="cal-prev" class="cal-nav" role="button" aria-label="Previous Month">‹</span>
<span id="cal-next" class="cal-nav" role="button" aria-label="Next Month">›</span>
<span id="cal-prev" class="cal-nav" role="button" tabindex="0" aria-label="Previous Month">‹</span>
<span id="cal-next" class="cal-nav" role="button" tabindex="0" aria-label="Next Month">›</span>
</div>
<div id="cal-grid" class="cal-grid">
<div class="cal-day-label">Su</div><div class="cal-day-label">Mo</div><div class="cal-day-label">Tu</div><div class="cal-day-label">We</div><div class="cal-day-label">Th</div><div class="cal-day-label">Fr</div><div class="cal-day-label">Sa</div>
Expand Down
34 changes: 31 additions & 3 deletions js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
85 changes: 56 additions & 29 deletions js/utils/toast.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
* Custom modern toast notifications for StudyPlan
*/

function escapeHtml(value) {
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}

class ToastManager {
constructor() {
this.container = document.createElement('div');
Expand All @@ -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 = `
<div class="toast-icon">${icon}</div>
<div class="toast-message">${cleanMessage}</div>
<div class="toast-icon">${escapeHtml(icon)}</div>
<div class="toast-message">${escapeHtml(cleanMessage)}</div>
<button class="toast-close" aria-label="Close">&times;</button>
`;

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);
Expand All @@ -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 = `
<h3 style="margin:0 0 12px; font-size:18px; font-weight:600;">Confirm Action</h3>
<p style="font-size:14px; margin-bottom: 24px; color: var(--color-text-secondary); line-height: 1.5;">${message}</p>
<div style="display:flex; justify-content:flex-end; gap:8px;">
<button class="btn confirm-cancel" style="padding:6px 16px;">Cancel</button>
<button class="btn btn-primary task-btn-danger confirm-ok" style="padding:6px 16px;">Confirm</button>
<div class="custom-confirm-icon" aria-hidden="true">!</div>
<div class="custom-confirm-content">
<h3 id="custom-confirm-title">Confirm deletion</h3>
<p id="custom-confirm-message">${escapeHtml(message)}</p>
</div>
<div class="custom-confirm-actions">
<button class="btn confirm-cancel" type="button">Cancel</button>
<button class="btn btn-primary confirm-ok" type="button">Delete</button>
</div>
`;

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();

2 changes: 1 addition & 1 deletion package-lock.json

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