Add toast notifications for all saves and batch planner task add
Toast: replace buried #admin-msg hint with a floating green/red toast (bottom-right, auto-dismisses after 3s) covering every save action — lists, decks, items, grades, kids, units, courses, bulk imports, and planner saves. Planner batch add: clicking '+ Add' now queues items locally (shown as dashed pending badges) without saving. A '💾 Save N items' button appears in the toolbar; clicking it commits everything at once and refreshes the planner. Queue clears on student change. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
b048ebbd85
commit
2accefd122
3 files changed
+82
-10
No files matched your search
+3
-1
@@ -244,6 +244,7 @@
|
||||
<button id="planner-next" class="secondary">Next →</button>
|
||||
<button id="planner-copy" class="secondary">Copy last week</button>
|
||||
<button id="planner-print">🖨 Print</button>
|
||||
<button id="planner-save-all" hidden>💾 Save 0 items</button>
|
||||
</div>
|
||||
|
||||
<details id="auto-schedule-details" class="admin-card" style="margin-bottom:1rem">
|
||||
@@ -448,7 +449,6 @@
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<p id="admin-msg" class="hint"></p>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -458,6 +458,8 @@
|
||||
<div id="assign-popup-students"></div>
|
||||
</div>
|
||||
|
||||
<div id="toast" role="status" aria-live="polite" hidden></div>
|
||||
|
||||
<script src="admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+65
-9
@@ -21,9 +21,17 @@ async function api(path, opts = {}) {
|
||||
return data;
|
||||
}
|
||||
|
||||
function msg(text) {
|
||||
$('#admin-msg').textContent = text;
|
||||
setTimeout(() => ($('#admin-msg').textContent = ''), 4000);
|
||||
let _toastTimer = null;
|
||||
function msg(text, type = 'success') {
|
||||
const el = $('#toast');
|
||||
el.textContent = text;
|
||||
el.className = type === 'error' ? 'toast-error' : '';
|
||||
el.hidden = false;
|
||||
clearTimeout(_toastTimer);
|
||||
_toastTimer = setTimeout(() => {
|
||||
el.classList.add('toast-out');
|
||||
setTimeout(() => { el.hidden = true; el.classList.remove('toast-out'); }, 400);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
const TYPE_ICON = {
|
||||
@@ -135,6 +143,7 @@ async function loadKids() {
|
||||
btn.addEventListener('click', async () => {
|
||||
if (!confirm('Remove this kid and all their progress? This cannot be undone.')) return;
|
||||
await api(`/api/students/${btn.dataset.delStudent}`, { method: 'DELETE' });
|
||||
msg('Kid removed.');
|
||||
loadKids();
|
||||
})
|
||||
);
|
||||
@@ -146,6 +155,7 @@ $('#add-student-form').addEventListener('submit', async (e) => {
|
||||
if (!name) return;
|
||||
await api('/api/students', { method: 'POST', body: { name, emoji: $('#new-student-emoji').value } });
|
||||
$('#new-student-name').value = '';
|
||||
msg('Kid added.');
|
||||
loadKids();
|
||||
});
|
||||
|
||||
@@ -249,6 +259,7 @@ async function openCourseDetail(id) {
|
||||
btn.addEventListener('click', async () => {
|
||||
if (!confirm('Delete this unit and everything in it?')) return;
|
||||
await api(`/api/units/${btn.dataset.delUnit}`, { method: 'DELETE' });
|
||||
msg('Unit deleted.');
|
||||
openCourseDetail(id);
|
||||
})
|
||||
);
|
||||
@@ -290,6 +301,7 @@ $('#cd-duplicate').addEventListener('click', async () => {
|
||||
$('#cd-delete').addEventListener('click', async () => {
|
||||
if (!confirm('Delete this whole course, including all units, items, and grades?')) return;
|
||||
await api(`/api/courses/${currentCourseId}`, { method: 'DELETE' });
|
||||
msg('Course deleted.');
|
||||
currentCourseId = null;
|
||||
showPanel('courses');
|
||||
});
|
||||
@@ -300,6 +312,7 @@ $('#add-unit-form').addEventListener('submit', async (e) => {
|
||||
if (!name) return;
|
||||
await api('/api/units', { method: 'POST', body: { courseId: currentCourseId, name } });
|
||||
$('#new-unit-name').value = '';
|
||||
msg('Unit added.');
|
||||
openCourseDetail(currentCourseId);
|
||||
});
|
||||
|
||||
@@ -359,6 +372,7 @@ async function openItemEditor(unitId, itemId) {
|
||||
$('#ie-delete').onclick = async () => {
|
||||
if (!confirm('Delete this item and any grades for it?')) return;
|
||||
await api(`/api/items/${itemId}`, { method: 'DELETE' });
|
||||
msg('Item deleted.');
|
||||
showPanel('course-detail');
|
||||
};
|
||||
} else {
|
||||
@@ -702,6 +716,7 @@ function addDays(dateStr, n) {
|
||||
}
|
||||
|
||||
let plannerWeekStart = mondayOf(new Date());
|
||||
let plannerQueue = [];
|
||||
let plannerStudentId = null;
|
||||
let plannerCoursesCache = [];
|
||||
let plannerMode = 'week'; // 'week' | 'month'
|
||||
@@ -750,6 +765,8 @@ $('#auto-schedule-btn').addEventListener('click', async () => {
|
||||
});
|
||||
|
||||
$('#planner-student').addEventListener('change', () => {
|
||||
plannerQueue = [];
|
||||
updatePlannerSaveBtn();
|
||||
plannerStudentId = Number($('#planner-student').value);
|
||||
renderPlanner();
|
||||
});
|
||||
@@ -942,18 +959,52 @@ function toggleAddTaskForm(date) {
|
||||
}
|
||||
})();
|
||||
|
||||
box.querySelector('.task-save-btn').addEventListener('click', async () => {
|
||||
box.querySelector('.task-save-btn').addEventListener('click', () => {
|
||||
const itemId = courseSelect.value;
|
||||
const title = box.querySelector('.task-offline-input').value.trim();
|
||||
if (!itemId && !title) return;
|
||||
await api('/api/schedule', {
|
||||
method: 'POST',
|
||||
body: { studentId: plannerStudentId, date, itemId: itemId || null, title },
|
||||
});
|
||||
renderPlanner();
|
||||
const label = itemId
|
||||
? courseSelect.options[courseSelect.selectedIndex].textContent
|
||||
: `📌 ${title}`;
|
||||
plannerQueue.push({ date, itemId: itemId || null, title, label });
|
||||
updatePlannerSaveBtn();
|
||||
// Show as pending in the day cell without a full re-render
|
||||
const tasksEl = document.querySelector(`.planner-tasks[data-drop-date="${date}"]`);
|
||||
if (tasksEl) {
|
||||
const hint = tasksEl.querySelector('.hint.tiny');
|
||||
if (hint) hint.remove();
|
||||
const el = document.createElement('div');
|
||||
el.className = 'planner-task planner-pending';
|
||||
el.textContent = label;
|
||||
tasksEl.appendChild(el);
|
||||
}
|
||||
courseSelect.value = '';
|
||||
box.querySelector('.task-offline-input').value = '';
|
||||
});
|
||||
}
|
||||
|
||||
function updatePlannerSaveBtn() {
|
||||
const btn = $('#planner-save-all');
|
||||
const n = plannerQueue.length;
|
||||
btn.hidden = n === 0;
|
||||
btn.textContent = `💾 Save ${n} item${n === 1 ? '' : 's'}`;
|
||||
}
|
||||
|
||||
$('#planner-save-all').addEventListener('click', async () => {
|
||||
const count = plannerQueue.length;
|
||||
if (!count) return;
|
||||
for (const item of plannerQueue) {
|
||||
await api('/api/schedule', {
|
||||
method: 'POST',
|
||||
body: { studentId: plannerStudentId, date: item.date, itemId: item.itemId, title: item.title },
|
||||
});
|
||||
}
|
||||
plannerQueue = [];
|
||||
updatePlannerSaveBtn();
|
||||
msg(`${count} item${count === 1 ? '' : 's'} added to planner.`);
|
||||
renderPlanner();
|
||||
});
|
||||
|
||||
function renderPrintTaskBlock(t) {
|
||||
const title = t.itemTitle || t.offlineTitle;
|
||||
const course = t.courseName ? `<span class="task-course">${esc(t.courseName)}</span>` : `<span class="task-course offline">Offline</span>`;
|
||||
@@ -1207,6 +1258,7 @@ async function loadGrading() {
|
||||
method: 'PUT',
|
||||
body: { score: Number(input.value), parentComment: commentInput?.value || '' },
|
||||
});
|
||||
msg('Grade saved.');
|
||||
loadGrading();
|
||||
loadGradingBadge();
|
||||
});
|
||||
@@ -1453,6 +1505,7 @@ function renderLists(lists, filterText = '') {
|
||||
btn.addEventListener('click', async () => {
|
||||
if (!confirm('Delete this list?')) return;
|
||||
await api(`/api/lists/${btn.dataset.delList}`, { method: 'DELETE' });
|
||||
msg('List deleted.');
|
||||
loadSpelling();
|
||||
})
|
||||
);
|
||||
@@ -1585,6 +1638,7 @@ $('#list-bulk-save').addEventListener('click', async () => {
|
||||
statusEl.textContent = `Imported ${done} of ${sections.length}…`;
|
||||
}
|
||||
statusEl.textContent = `Done! ${done} list(s) imported.`;
|
||||
msg(`${done} list${done === 1 ? '' : 's'} imported.`);
|
||||
$('#list-bulk-text').value = '';
|
||||
loadSpelling();
|
||||
});
|
||||
@@ -1693,6 +1747,7 @@ function renderDecks(decks, filterText = '') {
|
||||
btn.addEventListener('click', async () => {
|
||||
if (!confirm('Delete this deck?')) return;
|
||||
await api(`/api/decks/${btn.dataset.delDeck}`, { method: 'DELETE' });
|
||||
msg('Deck deleted.');
|
||||
loadDecks();
|
||||
})
|
||||
);
|
||||
@@ -1758,6 +1813,7 @@ $('#deck-bulk-save').addEventListener('click', async () => {
|
||||
statusEl.textContent = `Imported ${done} of ${sections.length}…`;
|
||||
}
|
||||
statusEl.textContent = `Done! ${done} deck(s) imported.`;
|
||||
msg(`${done} deck${done === 1 ? '' : 's'} imported.`);
|
||||
$('#deck-bulk-text').value = '';
|
||||
loadDecks();
|
||||
});
|
||||
|
||||
@@ -732,3 +732,17 @@ body.theme-pink { --accent: #d45d8a; --accent-dark: #b4487a; --bg: #fff0f5; }
|
||||
.thr-date { color: var(--muted); min-width: 6rem; flex-shrink: 0; }
|
||||
.thr-list { flex: 1; min-width: 8rem; }
|
||||
.thr-score { margin-left: auto; font-weight: 600; white-space: nowrap; }
|
||||
|
||||
/* ---------- toast notification ---------- */
|
||||
#toast {
|
||||
position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 9999;
|
||||
background: var(--good); color: #fff;
|
||||
padding: 0.7rem 1.2rem; border-radius: 12px;
|
||||
font-weight: 600; font-size: 0.95rem;
|
||||
box-shadow: 0 4px 18px rgba(0,0,0,0.18);
|
||||
transition: opacity 0.35s, transform 0.35s;
|
||||
opacity: 1; transform: translateY(0);
|
||||
}
|
||||
#toast.toast-error { background: var(--bad); }
|
||||
#toast.toast-out { opacity: 0; transform: translateY(0.5rem); }
|
||||
.planner-pending { border: 2px dashed var(--accent) !important; background: color-mix(in srgb, var(--accent) 10%, var(--card)) !important; color: var(--accent) !important; opacity: 0.85; cursor: default; }
|
||||
Reference in new issue
Block a user