From adf4bf4caeadb0733fb3068eda2971b82cfe26b2 Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Mon, 27 Jul 2026 13:02:45 -0400 Subject: [PATCH] Add bulk import for spelling lists and flashcard decks Paste multiple lists/decks in one shot using # Name headers: # Week 1 word1 word2 | example sentence # Week 2 ... An optional group field applies to all imported items at once. Progress shown inline; list/deck panel refreshes on completion. Co-Authored-By: Claude Sonnet 4.6 --- public/admin.html | 30 ++++++++++++++++++++ public/admin.js | 70 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/public/admin.html b/public/admin.html index 20fc2ec..262d0d3 100644 --- a/public/admin.html +++ b/public/admin.html @@ -298,6 +298,21 @@ +
+ Bulk import lists +
+

Start each list with # List name on its own line, then words below it. Optional group applies to all imported lists.

+ + +
+ + +
+

+
+
+

Test results

@@ -321,6 +336,21 @@ + +
+ Bulk import decks +
+

Start each deck with # Deck name on its own line, then cards below as front | back. Optional group applies to all imported decks.

+ + +
+ + +
+

+
+
diff --git a/public/admin.js b/public/admin.js index 3847080..69fc223 100644 --- a/public/admin.js +++ b/public/admin.js @@ -1410,6 +1410,49 @@ function clearListForm() { $('#list-editor-title').textContent = 'New list'; } +function parseBulkSections(text) { + const sections = []; + let current = null; + for (const raw of text.split('\n')) { + const line = raw.trim(); + if (line.startsWith('#')) { + if (current) sections.push(current); + current = { name: line.replace(/^#+\s*/, ''), lines: [] }; + } else if (current && line) { + current.lines.push(line); + } + } + if (current) sections.push(current); + return sections; +} + +$('#list-bulk-save').addEventListener('click', async () => { + const groupName = $('#list-bulk-group').value.trim(); + const sections = parseBulkSections($('#list-bulk-text').value); + if (sections.length === 0) return ($('#list-bulk-status').textContent = 'Paste at least one # Section with words below it.'); + const statusEl = $('#list-bulk-status'); + statusEl.textContent = `Importing ${sections.length} list(s)…`; + let done = 0; + for (const sec of sections) { + const words = sec.lines.map((line) => { + const [word, sentence = ''] = line.split('|').map((p) => p.trim()); + return { word, sentence }; + }).filter((w) => w.word); + if (!words.length) continue; + await api('/api/lists', { method: 'POST', body: { name: sec.name, words, groupName } }); + done++; + statusEl.textContent = `Imported ${done} of ${sections.length}…`; + } + statusEl.textContent = `Done! ${done} list(s) imported.`; + $('#list-bulk-text').value = ''; + loadSpelling(); +}); +$('#list-bulk-clear').addEventListener('click', () => { + $('#list-bulk-text').value = ''; + $('#list-bulk-group').value = ''; + $('#list-bulk-status').textContent = ''; +}); + function renderResults(students) { $('#results-area').innerHTML = students .map((s) => { @@ -1559,6 +1602,33 @@ function clearDeckForm() { $('#deck-editor-title').textContent = 'New deck'; } +$('#deck-bulk-save').addEventListener('click', async () => { + const groupName = $('#deck-bulk-group').value.trim(); + const sections = parseBulkSections($('#deck-bulk-text').value); + if (sections.length === 0) return ($('#deck-bulk-status').textContent = 'Paste at least one # Section with cards below it.'); + const statusEl = $('#deck-bulk-status'); + statusEl.textContent = `Importing ${sections.length} deck(s)…`; + let done = 0; + for (const sec of sections) { + const cards = sec.lines.map((line) => { + const [front, back = ''] = line.split('|').map((p) => p.trim()); + return { front, back }; + }).filter((c) => c.front); + if (!cards.length) continue; + await api('/api/decks', { method: 'POST', body: { name: sec.name, cards, groupName } }); + done++; + statusEl.textContent = `Imported ${done} of ${sections.length}…`; + } + statusEl.textContent = `Done! ${done} deck(s) imported.`; + $('#deck-bulk-text').value = ''; + loadDecks(); +}); +$('#deck-bulk-clear').addEventListener('click', () => { + $('#deck-bulk-text').value = ''; + $('#deck-bulk-group').value = ''; + $('#deck-bulk-status').textContent = ''; +}); + // ============================================================ // Settings // ============================================================