Spaces:
Sleeping
Sleeping
| /* ============================================================ | |
| 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 => ` | |
| <div class="dict-card" data-id="${w.id}" tabindex="0" role="button" | |
| aria-label="${esc(w.kanji)} β ${esc(w.sino)}"> | |
| <div class="dict-hanzi">${esc(w.kanji || '?')}</div> | |
| <div class="dict-info"> | |
| <div class="dict-pinyin">${esc(w.pinyin || '')}</div> | |
| <div class="dict-meaning">${esc(w.sino || '')}</div> | |
| ${w.vietnamese ? `<div class="dict-vi">${esc(w.vietnamese)}</div>` : ''} | |
| </div> | |
| <div class="dict-card-meta"> | |
| ${w.topic_name ? `<span class="dict-tag">${esc(w.topic_name)}</span>` : ''} | |
| ${w.hsk_level ? `<span class="dict-hsk hsk${w.hsk_level}">HSK ${w.hsk_level}</span>` : ''} | |
| </div> | |
| </div> | |
| `).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 = ` | |
| <div class="py-4 text-center"> | |
| <div class="learn-spinner"></div> | |
| </div>`; | |
| 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 = `<p class="text-danger py-3">Failed to load: ${esc(err.message)}</p>`; | |
| } | |
| } | |
| function renderWordModal(w) { | |
| /* Collocations section */ | |
| let collHtml = ''; | |
| if (w.collocations && w.collocations.length) { | |
| const items = w.collocations.map(c => ` | |
| <div class="coll-item"> | |
| <span class="coll-kanji">${esc(c.kanji || '')}</span> | |
| <span class="coll-pinyin">${esc(c.pinyin || '')}</span> | |
| </div> | |
| `).join(''); | |
| collHtml = ` | |
| <div class="word-detail-row"> | |
| <div class="word-detail-label">Collocations</div> | |
| <div class="coll-list">${items}</div> | |
| </div>`; | |
| } | |
| wordModalBody.innerHTML = ` | |
| <!-- Hanzi & Pinyin --> | |
| <div class="word-modal-hanzi">${esc(w.kanji || '?')}</div> | |
| <div class="word-modal-pinyin">${esc(w.pinyin || '')}</div> | |
| <!-- Badges --> | |
| <div class="d-flex justify-content-center gap-2 mb-3 flex-wrap"> | |
| ${w.hsk_level ? `<span class="hsk-badge hsk${w.hsk_level}">HSK ${w.hsk_level}</span>` : ''} | |
| ${w.topic_name ? `<span class="dict-tag">${esc(w.topic_name)}</span>` : ''} | |
| </div> | |
| <!-- Meanings --> | |
| ${detailRow('Meaning (Sino)', w.sino)} | |
| ${detailRow('Vietnamese', w.vietnamese)} | |
| <!-- Collocations --> | |
| ${collHtml} | |
| `; | |
| } | |
| /* ββ Helpers ββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function detailRow(label, value) { | |
| if (!value) return ''; | |
| return ` | |
| <div class="word-detail-row"> | |
| <div class="word-detail-label">${label}</div> | |
| <div class="word-detail-value">${esc(value)}</div> | |
| </div>`; | |
| } | |
| 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; | |
| } | |
| })(); |