Add paper/print alternatives for off-screen learning
Build and Push Docker Image / build-and-push (push) Successful in 3m35s

- Spelling: "Blank test" print button per word list (numbered lines, no words shown) for dictated paper tests
- Spelling: "Record Paper Test" form in admin to log a paper test score to the gradebook
- Gradebook: "Record Paper Score" form to log a paper quiz/assignment score for any course item
- Flashcards: "Print" button per deck generates a cut-and-fold card sheet (fronts page + backs page)
- Crossword: print now includes a second page with a filled answer key
- Lessons + Assignments: 🖨 Print button in student view generates a clean printable page
- Word lists: new "CSV file" import tab — upload a .csv with word, sentence, definition columns

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-10-19 12:43:38 -04:00
1 parent 6baea15986
commit a92db291c7
5 files changed
+392 -3

No files matched your search

+43 -2
View File
@@ -1222,6 +1222,7 @@ async function openLesson(itemId) {
$('#lesson-kicker').textContent = `${item.course_name} · ${item.unit_name}`;
$('#lesson-title').textContent = item.title;
$('#lesson-body').innerHTML = item.body;
$('#lesson-print-btn').onclick = () => printItemPage('📖 ' + item.title, item.course_name + ' · ' + item.unit_name, item.body);
show('lesson');
const btn = $('#lesson-done-btn');
@@ -1252,6 +1253,7 @@ async function openAssignment(itemId) {
$('#assignment-title').textContent = item.title;
$('#assignment-points').textContent = item.points ? `Worth ${item.points} points` : '';
$('#assignment-body').innerHTML = item.body || '';
$('#assignment-print-btn').onclick = () => printItemPage('📝 ' + item.title, item.course_name + ' · ' + item.unit_name, item.body || '');
const btn = $('#assignment-done-btn');
const status = $('#assignment-status');
@@ -2053,7 +2055,27 @@ async function openCrossword(itemId) {
show('crossword');
}
function printCrosswordWorksheet(title, cw) {
function printItemPage(title, kicker, bodyHtml) {
const win = window.open('', '_blank');
win.document.write(`<!DOCTYPE html><html><head><title>${esc(title)}</title>
<style>
body{font-family:Georgia,serif;max-width:680px;margin:2rem auto;color:#222}
.kicker{color:#888;font-size:.85rem;margin-bottom:.25rem}
h1{font-size:1.4rem;margin:0 0 1rem;border-bottom:2px solid #222;padding-bottom:.4rem}
.body{line-height:1.65}
.body h2,.body h3{margin:1rem 0 .4rem}
.body ul,.body ol{padding-left:1.5rem}
@media print{body{margin:.75in;max-width:none}}
</style></head><body>
<p class="kicker">${esc(kicker)}</p>
<h1>${esc(title)}</h1>
<div class="body">${bodyHtml}</div>
<script>window.print()<\/script>
</body></html>`);
win.document.close();
}
function buildCrosswordSvg(cw, filled) {
const CELL = 28;
const numMap = {};
for (const w of [...(cw.across||[]), ...(cw.down||[])]) if (w.num) numMap[`${w.row},${w.col}`] = w.num;
@@ -2067,21 +2089,40 @@ function printCrosswordWorksheet(title, cw) {
svg += `<rect x="${x}" y="${y}" width="${CELL}" height="${CELL}" fill="#fff" stroke="#999" stroke-width="0.5"/>`;
const key = `${r},${c}`;
if (numMap[key]) svg += `<text x="${x+2}" y="${y+9}" font-size="6" font-family="sans-serif">${numMap[key]}</text>`;
if (filled && cw.grid[r][c]) {
const cx = x + CELL / 2, cy = y + CELL / 2 + 5;
svg += `<text x="${cx}" y="${cy}" font-size="13" font-family="sans-serif" text-anchor="middle" fill="#1a1a1a">${esc(cw.grid[r][c])}</text>`;
}
}
}
}
svg += '</svg>';
return svg;
}
function printCrosswordWorksheet(title, cw) {
const svg = buildCrosswordSvg(cw, false);
const svgKey = buildCrosswordSvg(cw, true);
const acrossHtml = (cw.across||[]).map((w) => `<li><strong>${w.num}.</strong> ${esc(w.clue)}</li>`).join('');
const downHtml = (cw.down||[]).map((w) => `<li><strong>${w.num}.</strong> ${esc(w.clue)}</li>`).join('');
const win = window.open('', '_blank');
win.document.write(`<!DOCTYPE html><html><head><title>${esc(title)}</title>
<style>body{font-family:sans-serif;padding:1.5rem}h1{margin-bottom:1rem}.cols{display:flex;gap:2rem}ol{list-style:none;padding:0}li{margin:.2rem 0;font-size:.9rem}@media print{body{padding:.5rem}}</style>
<style>
body{font-family:sans-serif;padding:1.5rem}h1{margin-bottom:1rem}
.cols{display:flex;gap:2rem}ol{list-style:none;padding:0}li{margin:.2rem 0;font-size:.9rem}
.page-break{page-break-before:always;margin-top:2rem}
@media print{body{padding:.5rem}}
</style>
</head><body>
<h1>${esc(title)}</h1>${svg}
<div class="cols">
<div><h3>Across</h3><ol>${acrossHtml}</ol></div>
<div><h3>Down</h3><ol>${downHtml}</ol></div>
</div>
<div class="page-break">
<h2 style="margin-bottom:.75rem">Answer Key — ${esc(title)}</h2>
${svgKey}
</div>
<script>window.print();<\/script>
</body></html>`);
win.document.close();