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
// ============================================================