chinese_learning_v2 / app /static /js /dictionary.js
GphaHoa156
Add application file
8b3b66c
/* ============================================================
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;
}
})();