/* ============================================================ HanziLearn – dictionary.js Schema: Vocab(id, kanji, pinyin, sino, vietnamese, hsk_level, topic_name, hsk_level_label, collocations[]) ============================================================ */ (() => { /* ── DOM refs ─────────────────────────────────────────── */ const searchInput = document.getElementById('dictSearch'); const clearBtn = document.getElementById('clearBtn'); const skeletonWrap = document.getElementById('skeletonWrap'); const resultsList = document.getElementById('resultsList'); const emptyState = document.getElementById('emptyState'); const initialState = document.getElementById('initialState'); const searchHint = document.getElementById('searchHint'); const wordModal = new bootstrap.Modal(document.getElementById('wordModal')); const wordModalBody = document.getElementById('wordModalBody'); let debounceTimer = null; let lastQuery = ''; /* ── Search input ─────────────────────────────────────── */ searchInput.addEventListener('input', () => { const q = searchInput.value.trim(); clearBtn.classList.toggle('d-none', !q); clearTimeout(debounceTimer); if (!q) { showState('initial'); return; } debounceTimer = setTimeout(() => doSearch(q), 350); }); clearBtn.addEventListener('click', () => { searchInput.value = ''; clearBtn.classList.add('d-none'); lastQuery = ''; showState('initial'); searchInput.focus(); }); searchInput.addEventListener('keydown', e => { if (e.key === 'Escape') clearBtn.click(); }); /* ── Fetch & render ───────────────────────────────────── */ async function doSearch(query) { if (query === lastQuery) return; lastQuery = query; showState('loading'); searchHint.textContent = `Searching for "${query}"…`; try { const res = await fetch(`/dictionary/search?q=${encodeURIComponent(query)}`); const json = await res.json(); if (!json.success) throw new Error(json.message); if (!json.data.length) { showState('empty'); return; } renderResults(json.data); searchHint.textContent = `${json.count} result${json.count !== 1 ? 's' : ''} for "${query}"`; showState('results'); } catch (err) { searchHint.textContent = 'Error: ' + err.message; showState('empty'); } } /* ── Result cards ─────────────────────────────────────── */ function renderResults(words) { resultsList.innerHTML = words.map(w => `
Failed to load: ${esc(err.message)}
`; } } function renderWordModal(w) { /* Collocations section */ let collHtml = ''; if (w.collocations && w.collocations.length) { const items = w.collocations.map(c => `