/* ============================================================ 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 => `
${esc(w.kanji || '?')}
${esc(w.pinyin || '')}
${esc(w.sino || '')}
${w.vietnamese ? `
${esc(w.vietnamese)}
` : ''}
${w.topic_name ? `${esc(w.topic_name)}` : ''} ${w.hsk_level ? `HSK ${w.hsk_level}` : ''}
`).join(''); resultsList.querySelectorAll('.dict-card').forEach(card => { const open = () => openWordDetail(card.dataset.id); card.addEventListener('click', open); card.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') open(); }); }); } /* ── Word detail modal ────────────────────────────────── */ async function openWordDetail(id) { wordModalBody.innerHTML = `
`; wordModal.show(); try { const res = await fetch(`/dictionary/word/${id}`); const json = await res.json(); if (!json.success) throw new Error(json.message); renderWordModal(json.data); } catch (err) { wordModalBody.innerHTML = `

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 => `
${esc(c.kanji || '')} ${esc(c.pinyin || '')}
`).join(''); collHtml = `
Collocations
${items}
`; } wordModalBody.innerHTML = `
${esc(w.kanji || '?')}
${esc(w.pinyin || '')}
${w.hsk_level ? `HSK ${w.hsk_level}` : ''} ${w.topic_name ? `${esc(w.topic_name)}` : ''}
${detailRow('Meaning (Sino)', w.sino)} ${detailRow('Vietnamese', w.vietnamese)} ${collHtml} `; } /* ── Helpers ──────────────────────────────────────────── */ function detailRow(label, value) { if (!value) return ''; return `
${label}
${esc(value)}
`; } function showState(state) { skeletonWrap.classList.toggle('d-none', state !== 'loading'); resultsList.classList.toggle('d-none', state !== 'results'); emptyState.classList.toggle('d-none', state !== 'empty'); initialState.classList.toggle('d-none', state !== 'initial'); } function esc(str = '') { const d = document.createElement('div'); d.textContent = str; return d.innerHTML; } })();