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 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-07-27 13:02:45 -04:00
1 parent 24e709e038
commit adf4bf4cae
2 files changed
+100

No files matched your search

+30
View File
@@ -298,6 +298,21 @@
</form>
</details>
<details id="list-bulk-details">
<summary>Bulk import lists</summary>
<div class="admin-card">
<p class="hint" style="margin:0 0 0.5rem">Start each list with <strong># List name</strong> on its own line, then words below it. Optional group applies to all imported lists.</p>
<input id="list-bulk-group" placeholder="Group (e.g. Bible 6) — optional" aria-label="Group for bulk import" list="list-groups-datalist">
<textarea id="list-bulk-text" rows="12"
placeholder="# Week 1&#10;because&#10;enough&#10;friend&#10;&#10;# Week 2&#10;receive | Did you receive my letter?&#10;achieve&#10;believe"></textarea>
<div class="row-actions">
<button type="button" id="list-bulk-save">Import all lists</button>
<button type="button" id="list-bulk-clear" class="secondary">Clear</button>
</div>
<p id="list-bulk-status" class="hint" style="margin:0.4rem 0 0"></p>
</div>
</details>
<h2>Test results</h2>
<div id="results-area"></div>
</section>
@@ -321,6 +336,21 @@
</div>
</form>
</details>
<details id="deck-bulk-details">
<summary>Bulk import decks</summary>
<div class="admin-card">
<p class="hint" style="margin:0 0 0.5rem">Start each deck with <strong># Deck name</strong> on its own line, then cards below as <strong>front | back</strong>. Optional group applies to all imported decks.</p>
<input id="deck-bulk-group" placeholder="Group (e.g. Bible 6) — optional" aria-label="Group for bulk import" list="deck-groups-datalist">
<textarea id="deck-bulk-text" rows="12"
placeholder="# Chapter 1 Vocab&#10;What is photosynthesis? | The process plants use to make food&#10;What does chlorophyll do? | Absorbs light&#10;&#10;# Chapter 2 Vocab&#10;What is mitosis? | Cell division producing identical cells"></textarea>
<div class="row-actions">
<button type="button" id="deck-bulk-save">Import all decks</button>
<button type="button" id="deck-bulk-clear" class="secondary">Clear</button>
</div>
<p id="deck-bulk-status" class="hint" style="margin:0.4rem 0 0"></p>
</div>
</details>
</section>
<!-- Settings -->
+70
View File
@@ -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
// ============================================================