diff --git a/public/admin.html b/public/admin.html
index d880fdb..d5bc259 100644
--- a/public/admin.html
+++ b/public/admin.html
@@ -396,6 +396,28 @@
+
+
+ 📝 Record Paper Score
+ Use this to log a score from a paper quiz, assignment, or other item done off-screen.
+
+
Spelling Test History
diff --git a/public/admin.js b/public/admin.js
index 6aca485..95bd517 100644
--- a/public/admin.js
+++ b/public/admin.js
@@ -2070,10 +2070,11 @@ function renderAdminMsgs(msgs) {
let gradebookCourseId = null;
async function loadGradebookPanel() {
- const courses = await api('/api/admin/courses');
+ const [courses, students] = await Promise.all([api('/api/admin/courses'), api('/api/students')]);
$('#gradebook-course').innerHTML = courses.map((c) => ``).join('');
if (courses.length) renderGradebook(courses[0].id);
loadSpellingTests();
+ initPaperScoreForm(students, courses);
}
async function loadSpellingTests() {
@@ -2275,6 +2276,7 @@ async function loadSpelling() {
renderLists(lists);
loadPracticeSessions();
loadPracticeActivity();
+ initPaperTestForm();
}
async function loadPracticeSessions() {
@@ -2477,6 +2479,7 @@ function renderLists(lists, filterText = '') {
${l.wordCount} words${l.builtin ? ' · built-in' : ''}
${l.builtin ? '' : ``}
+ ${l.builtin ? '' : ``}
${l.builtin
? ``
@@ -2495,6 +2498,12 @@ function renderLists(lists, filterText = '') {
printSpellingWorksheet(list.name, list.words);
})
);
+ document.querySelectorAll('[data-blank-test]').forEach((btn) =>
+ btn.addEventListener('click', async () => {
+ const list = await api(`/api/lists/${btn.dataset.blankTest}`);
+ printBlankSpellingTest(list.name, list.words);
+ })
+ );
document.querySelectorAll('[data-edit-list]').forEach((btn) =>
btn.addEventListener('click', async () => {
const list = await api(`/api/lists/${btn.dataset.editList}`);
@@ -2595,14 +2604,22 @@ function clearListForm() {
function showSection(type, which) {
$(`#${type}-editor-section`).hidden = which !== 'editor';
$(`#${type}-bulk-section`).hidden = which !== 'bulk';
+ const csvSec = $(`#${type}-csv-section`);
+ if (csvSec) csvSec.hidden = which !== 'csv';
$(`#${type}-new-btn`).classList.toggle('active', which === 'editor');
$(`#${type}-bulk-btn`).classList.toggle('active', which === 'bulk');
+ const csvBtn = $(`#${type}-csv-btn`);
+ if (csvBtn) csvBtn.classList.toggle('active', which === 'csv');
}
function hideSections(type) {
$(`#${type}-editor-section`).hidden = true;
$(`#${type}-bulk-section`).hidden = true;
+ const csvSec = $(`#${type}-csv-section`);
+ if (csvSec) csvSec.hidden = true;
$(`#${type}-new-btn`).classList.remove('active');
$(`#${type}-bulk-btn`).classList.remove('active');
+ const csvBtn = $(`#${type}-csv-btn`);
+ if (csvBtn) csvBtn.classList.remove('active');
}
$('#list-new-btn').addEventListener('click', () => {
@@ -2613,6 +2630,55 @@ $('#list-bulk-btn').addEventListener('click', () => {
if ($('#list-bulk-section').hidden) showSection('list', 'bulk');
else hideSections('list');
});
+$('#list-csv-btn').addEventListener('click', () => {
+ if ($('#list-csv-section').hidden) showSection('list', 'csv');
+ else hideSections('list');
+});
+
+$('#list-csv-browse').addEventListener('click', () => $('#list-csv-file').click());
+$('#list-csv-file').addEventListener('change', () => {
+ const f = $('#list-csv-file').files[0];
+ $('#list-csv-filename').textContent = f ? f.name : '';
+ $('#list-csv-go').disabled = !f;
+ if (f && !$('#list-csv-name').value.trim()) {
+ $('#list-csv-name').value = f.name.replace(/\.[^.]+$/, '').replace(/[_-]/g, ' ');
+ }
+ $('#list-csv-status').textContent = '';
+});
+$('#list-csv-go').addEventListener('click', async () => {
+ const f = $('#list-csv-file').files[0];
+ const name = $('#list-csv-name').value.trim();
+ const groupName = $('#list-csv-group').value.trim();
+ if (!f || !name) { $('#list-csv-status').textContent = 'Choose a file and enter a list name.'; return; }
+ $('#list-csv-status').textContent = 'Parsing…';
+ const text = await f.text();
+ const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
+ const words = [];
+ for (const line of lines) {
+ const parts = line.split(',').map((p) => p.trim().replace(/^["']|["']$/g, ''));
+ const word = parts[0];
+ if (!word || word.toLowerCase() === 'word') continue; // skip header
+ const entry = { word };
+ if (parts[1]) entry.sentence = parts[1];
+ if (parts[2]) entry.definition = parts[2];
+ words.push(entry);
+ }
+ if (words.length === 0) { $('#list-csv-status').textContent = 'No words found in file.'; return; }
+ try {
+ await api('/api/lists', { method: 'POST', body: { name, words, groupName } });
+ msg(`Imported "${name}" (${words.length} words).`);
+ $('#list-csv-file').value = '';
+ $('#list-csv-filename').textContent = '';
+ $('#list-csv-go').disabled = true;
+ $('#list-csv-name').value = '';
+ $('#list-csv-group').value = '';
+ $('#list-csv-status').textContent = '';
+ hideSections('list');
+ loadSpelling();
+ } catch (e) {
+ $('#list-csv-status').textContent = `Error: ${e.message}`;
+ }
+});
function parseBulkSections(text) {
const sections = [];
@@ -2715,6 +2781,82 @@ async function printSpellingReport(testId) {
win.document.close();
}
+function printBlankSpellingTest(name, words) {
+ const today = new Date().toLocaleDateString();
+ const rows = words
+ .map((w, i) => {
+ const hint = w.sentence ? `${esc(w.sentence)}
` : '';
+ return `
+ | ${i + 1}. |
+ ${hint} |
+
`;
+ })
+ .join('');
+ const win = window.open('', '_blank');
+ win.document.write(`Spelling Test: ${esc(name)}
+
+ ⭐ Spelling Test: ${esc(name)}
+
+
+