feat: decompose frontend into native ES modules (zero bundlers, zero npm)
This commit is contained in:
1 parent
869bf23948
commit
21d94acf4e
9 files changed
+4307
-3893
No files matched your search
@@ -0,0 +1,732 @@
|
||||
// =======================================================================
|
||||
// Notes Application - Dictionary Module (Native ES Module)
|
||||
// =======================================================================
|
||||
|
||||
import { api } from './api.js';
|
||||
import { escapeHtml, showToast, openModal, closeModal } from './ui.js';
|
||||
|
||||
export let allCards = [];
|
||||
export let collectionsList = ['General', 'Разговорный', 'Работа & IT', 'Книги & Сериалы', 'Идиомы'];
|
||||
export let selectedTag = '';
|
||||
export let selectedCollection = '';
|
||||
export let searchQuery = '';
|
||||
|
||||
let currentLookupData = null;
|
||||
let foundDuplicateCard = null;
|
||||
let duplicateCheckTimer = null;
|
||||
let isBatchMode = false;
|
||||
let selectedIds = new Set();
|
||||
|
||||
export function getAllCards() {
|
||||
return allCards;
|
||||
}
|
||||
|
||||
export function initDictionary() {
|
||||
// Elements - Search & Capture
|
||||
const termInput = document.getElementById('term-input');
|
||||
const clearSearchBtn = document.getElementById('clear-search-btn');
|
||||
const translateBtn = document.getElementById('translate-btn');
|
||||
const duplicateWarningBox = document.getElementById('duplicate-warning-box');
|
||||
const dupTermLabel = document.getElementById('dup-term-label');
|
||||
const dupTransLabel = document.getElementById('dup-trans-label');
|
||||
const dupOpenBtn = document.getElementById('dup-open-btn');
|
||||
|
||||
const lookupCard = document.getElementById('lookup-card');
|
||||
const resTerm = document.getElementById('res-term');
|
||||
const resPhonetic = document.getElementById('res-phonetic');
|
||||
const resInLibrary = document.getElementById('res-in-library');
|
||||
const audioPlayBtn = document.getElementById('audio-play-btn');
|
||||
const resPos = document.getElementById('res-pos');
|
||||
const resDefinitionWrap = document.getElementById('res-definition-wrap');
|
||||
const resDefinition = document.getElementById('res-definition');
|
||||
const editTranslation = document.getElementById('edit-translation');
|
||||
const editContext = document.getElementById('edit-context');
|
||||
const editCollection = document.getElementById('edit-collection');
|
||||
const collectionsDatalist = document.getElementById('collections-datalist');
|
||||
const editTags = document.getElementById('edit-tags');
|
||||
const editNotes = document.getElementById('edit-notes');
|
||||
const saveCardBtn = document.getElementById('save-card-btn');
|
||||
|
||||
// Elements - Vocabulary List
|
||||
const filterInput = document.getElementById('filter-input');
|
||||
const cardsCountLabel = document.getElementById('cards-count-label');
|
||||
const collectionsFilterBar = document.getElementById('collections-filter-bar');
|
||||
const newCollectionBtn = document.getElementById('new-collection-btn');
|
||||
const tagsFilterBar = document.getElementById('tags-filter-bar');
|
||||
const cardsList = document.getElementById('cards-list');
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
|
||||
// Elements - Batch Action Bar
|
||||
const batchToggleBtn = document.getElementById('batch-toggle-btn');
|
||||
const batchActionBar = document.getElementById('batch-action-bar');
|
||||
const batchSelectAll = document.getElementById('batch-select-all');
|
||||
const batchCountLabel = document.getElementById('batch-count-label');
|
||||
const batchMoveBtn = document.getElementById('batch-move-btn');
|
||||
const batchTagBtn = document.getElementById('batch-tag-btn');
|
||||
const batchDeleteBtn = document.getElementById('batch-delete-btn');
|
||||
const batchCancelBtn = document.getElementById('batch-cancel-btn');
|
||||
|
||||
// Elements - Modals
|
||||
const editModal = document.getElementById('edit-card-modal');
|
||||
const closeModalBtn = document.getElementById('close-modal-btn');
|
||||
const modalCancelBtn = document.getElementById('modal-cancel-btn');
|
||||
const modalSaveBtn = document.getElementById('modal-save-btn');
|
||||
const modalCardId = document.getElementById('modal-card-id');
|
||||
const modalTerm = document.getElementById('modal-term');
|
||||
const modalTranslation = document.getElementById('modal-translation');
|
||||
const modalContext = document.getElementById('modal-context');
|
||||
const modalPhonetic = document.getElementById('modal-phonetic');
|
||||
const modalPos = document.getElementById('modal-pos');
|
||||
const modalDef = document.getElementById('modal-def');
|
||||
const modalCollection = document.getElementById('modal-collection');
|
||||
const modalTags = document.getElementById('modal-tags');
|
||||
const modalNotes = document.getElementById('modal-notes');
|
||||
|
||||
const newColModal = document.getElementById('new-col-modal');
|
||||
const newColNameInput = document.getElementById('new-col-name-input');
|
||||
const closeNewColModalBtn = document.getElementById('close-new-col-modal-btn');
|
||||
const newColCancelBtn = document.getElementById('new-col-cancel-btn');
|
||||
const newColSaveBtn = document.getElementById('new-col-save-btn');
|
||||
|
||||
const moveColModal = document.getElementById('move-col-modal');
|
||||
const moveColInput = document.getElementById('move-col-input');
|
||||
const moveColQuickList = document.getElementById('move-col-quick-list');
|
||||
const closeMoveModalBtn = document.getElementById('close-move-modal-btn');
|
||||
const moveColCancelBtn = document.getElementById('move-col-cancel-btn');
|
||||
const moveColConfirmBtn = document.getElementById('move-col-confirm-btn');
|
||||
|
||||
const exportOpenBtn = document.getElementById('export-open-btn');
|
||||
const exportModal = document.getElementById('export-modal');
|
||||
const closeExportModalBtn = document.getElementById('close-export-modal-btn');
|
||||
const exportCancelBtn = document.getElementById('export-cancel-btn');
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Duplicate Detection & Search Term
|
||||
// -----------------------------------------------------------------------
|
||||
termInput.addEventListener('input', () => {
|
||||
const val = termInput.value.trim();
|
||||
clearSearchBtn.classList.toggle('hidden', val.length === 0);
|
||||
|
||||
clearTimeout(duplicateCheckTimer);
|
||||
if (val.length >= 2) {
|
||||
duplicateCheckTimer = setTimeout(async () => {
|
||||
try {
|
||||
const resp = await fetch(`/api/cards/check-duplicate?term=${encodeURIComponent(val)}`);
|
||||
if (resp.ok) {
|
||||
const res = await resp.json();
|
||||
if (res.exists && res.card) {
|
||||
foundDuplicateCard = res.card;
|
||||
dupTermLabel.textContent = res.card.term;
|
||||
dupTransLabel.textContent = res.card.translation;
|
||||
duplicateWarningBox.classList.remove('hidden');
|
||||
} else {
|
||||
foundDuplicateCard = null;
|
||||
duplicateWarningBox.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}, 300);
|
||||
} else {
|
||||
foundDuplicateCard = null;
|
||||
duplicateWarningBox.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
dupOpenBtn.addEventListener('click', () => {
|
||||
if (foundDuplicateCard) {
|
||||
openEditModal(foundDuplicateCard);
|
||||
}
|
||||
});
|
||||
|
||||
clearSearchBtn.addEventListener('click', () => {
|
||||
termInput.value = '';
|
||||
clearSearchBtn.classList.add('hidden');
|
||||
duplicateWarningBox.classList.add('hidden');
|
||||
lookupCard.classList.add('hidden');
|
||||
termInput.focus();
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Dictionary Lookup & Capture
|
||||
// -----------------------------------------------------------------------
|
||||
translateBtn.addEventListener('click', performLookup);
|
||||
termInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.ctrlKey && !e.metaKey) {
|
||||
e.preventDefault();
|
||||
performLookup();
|
||||
} else if (e.key === 'Escape') {
|
||||
termInput.value = '';
|
||||
clearSearchBtn.classList.add('hidden');
|
||||
duplicateWarningBox.classList.add('hidden');
|
||||
lookupCard.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
async function performLookup() {
|
||||
const term = termInput.value.trim();
|
||||
if (!term) return;
|
||||
|
||||
translateBtn.disabled = true;
|
||||
translateBtn.textContent = 'Ищем...';
|
||||
|
||||
try {
|
||||
const data = await api.cards.lookup(term);
|
||||
currentLookupData = data;
|
||||
displayLookupResult(data);
|
||||
} catch (e) {
|
||||
console.error('Lookup error:', e);
|
||||
showToast('Не удалось получить перевод', 'error');
|
||||
} finally {
|
||||
translateBtn.disabled = false;
|
||||
translateBtn.textContent = 'Перевести';
|
||||
}
|
||||
}
|
||||
|
||||
function displayLookupResult(data) {
|
||||
resTerm.textContent = data.term;
|
||||
resPhonetic.textContent = data.phonetic || '';
|
||||
resPhonetic.style.display = data.phonetic ? 'inline-block' : 'none';
|
||||
resPos.textContent = data.part_of_speech || 'Phrase';
|
||||
audioPlayBtn.classList.add('hidden');
|
||||
|
||||
if (data.definition_en) {
|
||||
resDefinition.textContent = data.definition_en;
|
||||
resDefinitionWrap.classList.remove('hidden');
|
||||
} else {
|
||||
resDefinitionWrap.classList.add('hidden');
|
||||
}
|
||||
|
||||
const inLib = Boolean(data.in_library || foundDuplicateCard);
|
||||
if (resInLibrary) {
|
||||
resInLibrary.classList.toggle('hidden', !inLib);
|
||||
}
|
||||
|
||||
if (inLib) {
|
||||
saveCardBtn.innerHTML = '⭐ Обновить в словаре <span class="btn-subtext">(Ctrl + Enter)</span>';
|
||||
const lib = data.library_card || foundDuplicateCard;
|
||||
if (lib) {
|
||||
if (lib.translation) editTranslation.value = lib.translation;
|
||||
if (lib.context_sentence) editContext.value = lib.context_sentence;
|
||||
if (lib.collection) editCollection.value = lib.collection;
|
||||
if (lib.tags) editTags.value = lib.tags;
|
||||
if (lib.notes) editNotes.value = lib.notes;
|
||||
} else {
|
||||
editTranslation.value = data.translation || '';
|
||||
}
|
||||
} else {
|
||||
saveCardBtn.innerHTML = '💾 Сохранить в словарь <span class="btn-subtext">(Ctrl + Enter)</span>';
|
||||
editTranslation.value = data.translation || '';
|
||||
editContext.value = '';
|
||||
editNotes.value = '';
|
||||
editCollection.value = selectedCollection || 'General';
|
||||
}
|
||||
|
||||
lookupCard.classList.remove('hidden');
|
||||
editTranslation.focus();
|
||||
}
|
||||
|
||||
// Save Card Action
|
||||
saveCardBtn.addEventListener('click', saveNewCard);
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||
if (!lookupCard.classList.contains('hidden')) {
|
||||
e.preventDefault();
|
||||
saveNewCard();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function saveNewCard() {
|
||||
if (!currentLookupData) return;
|
||||
const term = currentLookupData.term.trim();
|
||||
const translation = editTranslation.value.trim();
|
||||
if (!translation) {
|
||||
alert('Пожалуйста, укажите русский перевод');
|
||||
editTranslation.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
term: term,
|
||||
translation: translation,
|
||||
context_sentence: editContext.value.trim(),
|
||||
phonetic: currentLookupData.phonetic || '',
|
||||
audio_url: currentLookupData.audio_url || '',
|
||||
part_of_speech: currentLookupData.part_of_speech || '',
|
||||
definition_en: currentLookupData.definition_en || '',
|
||||
tags: editTags.value.trim() || 'general',
|
||||
notes: editNotes.value.trim(),
|
||||
collection: editCollection.value.trim() || 'General',
|
||||
};
|
||||
|
||||
saveCardBtn.disabled = true;
|
||||
saveCardBtn.textContent = 'Сохраняем...';
|
||||
|
||||
try {
|
||||
const resp = await api.cards.create(payload);
|
||||
if (resp.ok) {
|
||||
termInput.value = '';
|
||||
clearSearchBtn.classList.add('hidden');
|
||||
duplicateWarningBox.classList.add('hidden');
|
||||
lookupCard.classList.add('hidden');
|
||||
currentLookupData = null;
|
||||
|
||||
showToast(`Слово "${term}" сохранено в словарь!`, 'success');
|
||||
await loadInitialData();
|
||||
termInput.focus();
|
||||
} else {
|
||||
const err = await resp.json();
|
||||
alert('Ошибка сохранения: ' + (err.detail || 'Не удалось сохранить'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Сетевая ошибка при сохранении');
|
||||
} finally {
|
||||
saveCardBtn.disabled = false;
|
||||
saveCardBtn.innerHTML = '💾 Сохранить в словарь <span class="btn-subtext">(Ctrl + Enter)</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Cards List & Filtering
|
||||
// -----------------------------------------------------------------------
|
||||
filterInput.addEventListener('input', () => {
|
||||
searchQuery = filterInput.value.trim();
|
||||
fetchCards();
|
||||
});
|
||||
|
||||
function renderCollectionsFilter(cols) {
|
||||
collectionsFilterBar.innerHTML = '';
|
||||
const allBtn = document.createElement('button');
|
||||
allBtn.className = `col-chip ${selectedCollection === '' ? 'active' : ''}`;
|
||||
allBtn.textContent = 'Все коллекции';
|
||||
allBtn.onclick = () => {
|
||||
selectedCollection = '';
|
||||
updateFilterChips();
|
||||
fetchCards();
|
||||
};
|
||||
collectionsFilterBar.appendChild(allBtn);
|
||||
|
||||
cols.forEach(c => {
|
||||
const chip = document.createElement('button');
|
||||
chip.className = `col-chip ${selectedCollection === c.name ? 'active' : ''}`;
|
||||
chip.textContent = `${c.name} (${c.count})`;
|
||||
chip.onclick = () => {
|
||||
selectedCollection = c.name;
|
||||
updateFilterChips();
|
||||
fetchCards();
|
||||
};
|
||||
collectionsFilterBar.appendChild(chip);
|
||||
});
|
||||
}
|
||||
|
||||
function renderTagsFilter(tags) {
|
||||
tagsFilterBar.innerHTML = '';
|
||||
const allBtn = document.createElement('button');
|
||||
allBtn.className = `filter-chip ${selectedTag === '' ? 'active' : ''}`;
|
||||
allBtn.textContent = 'Все теги';
|
||||
allBtn.onclick = () => {
|
||||
selectedTag = '';
|
||||
updateFilterChips();
|
||||
fetchCards();
|
||||
};
|
||||
tagsFilterBar.appendChild(allBtn);
|
||||
|
||||
tags.slice(0, 15).forEach(t => {
|
||||
const chip = document.createElement('button');
|
||||
chip.className = `filter-chip ${selectedTag === t.tag ? 'active' : ''}`;
|
||||
chip.textContent = `#${t.tag} (${t.count})`;
|
||||
chip.onclick = () => {
|
||||
selectedTag = selectedTag === t.tag ? '' : t.tag;
|
||||
updateFilterChips();
|
||||
fetchCards();
|
||||
};
|
||||
tagsFilterBar.appendChild(chip);
|
||||
});
|
||||
}
|
||||
|
||||
function updateFilterChips() {
|
||||
document.querySelectorAll('.col-chip').forEach(el => {
|
||||
el.classList.toggle('active', el.textContent.startsWith(selectedCollection || 'Все коллекции'));
|
||||
});
|
||||
document.querySelectorAll('.filter-chip').forEach(el => {
|
||||
el.classList.toggle('active', el.textContent.startsWith(`#${selectedTag}`) || (!selectedTag && el.textContent.startsWith('Все теги')));
|
||||
});
|
||||
}
|
||||
|
||||
function renderCardsList() {
|
||||
cardsCountLabel.textContent = `(${allCards.length} записей)`;
|
||||
cardsList.innerHTML = '';
|
||||
|
||||
if (allCards.length === 0) {
|
||||
emptyState.classList.remove('hidden');
|
||||
cardsList.appendChild(emptyState);
|
||||
return;
|
||||
}
|
||||
|
||||
emptyState.classList.add('hidden');
|
||||
|
||||
allCards.forEach(c => {
|
||||
const cardEl = document.createElement('div');
|
||||
cardEl.className = `vocab-card ${isBatchMode && selectedIds.has(c.id) ? 'is-selected' : ''}`;
|
||||
|
||||
const srsStateClass = c.srs_state === 'mastered' ? 'dot-mastered' : (c.srs_state === 'review' ? 'dot-review' : (c.srs_state === 'learning' ? 'dot-learning' : 'dot-new'));
|
||||
|
||||
cardEl.innerHTML = `
|
||||
<div class="card-top">
|
||||
<div class="card-term-group">
|
||||
${isBatchMode ? `<input type="checkbox" class="card-select-checkbox batch-card-check" data-id="${c.id}" ${selectedIds.has(c.id) ? 'checked' : ''}>` : ''}
|
||||
<span class="dot ${srsStateClass}" title="SRS статус: ${c.srs_state || 'new'}"></span>
|
||||
<span class="card-term">${escapeHtml(c.term)}</span>
|
||||
${c.phonetic ? `<span class="card-phonetic">${escapeHtml(c.phonetic)}</span>` : ''}
|
||||
${c.part_of_speech ? `<span class="card-pos">${escapeHtml(c.part_of_speech)}</span>` : ''}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button class="icon-btn edit-card-btn" data-id="${c.id}" title="Редактировать">✏️</button>
|
||||
<button class="icon-btn delete-card-btn" data-id="${c.id}" title="Удалить">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-translation">${escapeHtml(c.translation)}</div>
|
||||
${c.context_sentence ? `<div class="card-context">“${escapeHtml(c.context_sentence)}”</div>` : ''}
|
||||
<div class="card-bottom">
|
||||
<div class="card-tags">
|
||||
<span class="card-tag">📁 ${escapeHtml(c.collection || 'General')}</span>
|
||||
${c.tags ? c.tags.split(',').map(t => `<span class="card-tag">#${escapeHtml(t.trim())}</span>`).join(' ') : ''}
|
||||
</div>
|
||||
${c.notes ? `<div class="card-notes">💡 ${escapeHtml(c.notes)}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Edit button
|
||||
cardEl.querySelector('.edit-card-btn').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
openEditModal(c);
|
||||
});
|
||||
|
||||
// Delete button
|
||||
cardEl.querySelector('.delete-card-btn').addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
if (confirm(`Удалить слово "${c.term}" из словаря?`)) {
|
||||
await api.cards.delete(c.id);
|
||||
showToast(`Слово "${c.term}" удалено`, 'info');
|
||||
await loadInitialData();
|
||||
}
|
||||
});
|
||||
|
||||
// Batch selection click
|
||||
if (isBatchMode) {
|
||||
cardEl.addEventListener('click', () => {
|
||||
toggleBatchSelection(c.id);
|
||||
});
|
||||
}
|
||||
|
||||
cardsList.appendChild(cardEl);
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Batch Operations
|
||||
// -----------------------------------------------------------------------
|
||||
batchToggleBtn.addEventListener('click', () => {
|
||||
isBatchMode = !isBatchMode;
|
||||
batchToggleBtn.textContent = isBatchMode ? '✓ Завершить выбор' : '☑️ Выбрать';
|
||||
batchToggleBtn.classList.toggle('btn-primary', isBatchMode);
|
||||
batchToggleBtn.classList.toggle('btn-outline', !isBatchMode);
|
||||
batchActionBar.classList.toggle('hidden', !isBatchMode);
|
||||
if (!isBatchMode) {
|
||||
selectedIds.clear();
|
||||
}
|
||||
renderCardsList();
|
||||
updateBatchCount();
|
||||
});
|
||||
|
||||
batchCancelBtn.addEventListener('click', () => {
|
||||
isBatchMode = false;
|
||||
batchToggleBtn.textContent = '☑️ Выбрать';
|
||||
batchToggleBtn.classList.remove('btn-primary');
|
||||
batchToggleBtn.classList.add('btn-outline');
|
||||
batchActionBar.classList.add('hidden');
|
||||
selectedIds.clear();
|
||||
renderCardsList();
|
||||
});
|
||||
|
||||
function toggleBatchSelection(id) {
|
||||
if (selectedIds.has(id)) selectedIds.delete(id);
|
||||
else selectedIds.add(id);
|
||||
updateBatchCount();
|
||||
renderCardsList();
|
||||
}
|
||||
|
||||
batchSelectAll.addEventListener('change', () => {
|
||||
if (batchSelectAll.checked) {
|
||||
allCards.forEach(c => selectedIds.add(c.id));
|
||||
} else {
|
||||
selectedIds.clear();
|
||||
}
|
||||
updateBatchCount();
|
||||
renderCardsList();
|
||||
});
|
||||
|
||||
function updateBatchCount() {
|
||||
batchCountLabel.textContent = `Выбрано: ${selectedIds.size}`;
|
||||
batchSelectAll.checked = selectedIds.size > 0 && selectedIds.size === allCards.length;
|
||||
}
|
||||
|
||||
batchDeleteBtn.addEventListener('click', async () => {
|
||||
if (selectedIds.size === 0) return;
|
||||
if (confirm(`Удалить выбранные карточки (${selectedIds.size} шт.)?`)) {
|
||||
await api.cards.batch({
|
||||
card_ids: Array.from(selectedIds),
|
||||
action: 'delete'
|
||||
});
|
||||
showToast(`Удалено карточек: ${selectedIds.size}`, 'info');
|
||||
selectedIds.clear();
|
||||
await loadInitialData();
|
||||
}
|
||||
});
|
||||
|
||||
batchMoveBtn.addEventListener('click', () => {
|
||||
if (selectedIds.size === 0) {
|
||||
alert('Сначала выберите хотя бы одну карточку');
|
||||
return;
|
||||
}
|
||||
openModal(moveColModal);
|
||||
});
|
||||
|
||||
batchTagBtn.addEventListener('click', async () => {
|
||||
if (selectedIds.size === 0) return;
|
||||
const tagVal = prompt('Введите тег(и) для добавления (через запятую):');
|
||||
if (tagVal && tagVal.trim()) {
|
||||
await api.cards.batch({
|
||||
card_ids: Array.from(selectedIds),
|
||||
action: 'add_tags',
|
||||
value: tagVal.trim()
|
||||
});
|
||||
showToast(`Теги добавлены`, 'success');
|
||||
await loadInitialData();
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Edit Card Modal
|
||||
// -----------------------------------------------------------------------
|
||||
function openEditModal(card) {
|
||||
modalCardId.value = card.id;
|
||||
modalTerm.value = card.term;
|
||||
modalTranslation.value = card.translation;
|
||||
modalContext.value = card.context_sentence || '';
|
||||
modalPhonetic.value = card.phonetic || '';
|
||||
modalPos.value = card.part_of_speech || '';
|
||||
modalDef.value = card.definition_en || '';
|
||||
modalCollection.value = card.collection || 'General';
|
||||
modalTags.value = card.tags || '';
|
||||
modalNotes.value = card.notes || '';
|
||||
openModal(editModal);
|
||||
}
|
||||
|
||||
closeModalBtn.addEventListener('click', () => closeModal(editModal));
|
||||
modalCancelBtn.addEventListener('click', () => closeModal(editModal));
|
||||
|
||||
modalSaveBtn.addEventListener('click', async () => {
|
||||
const id = modalCardId.value;
|
||||
const payload = {
|
||||
term: modalTerm.value.trim(),
|
||||
translation: modalTranslation.value.trim(),
|
||||
context_sentence: modalContext.value.trim(),
|
||||
phonetic: modalPhonetic.value.trim(),
|
||||
part_of_speech: modalPos.value.trim(),
|
||||
definition_en: modalDef.value.trim(),
|
||||
collection: modalCollection.value.trim() || 'General',
|
||||
tags: modalTags.value.trim(),
|
||||
notes: modalNotes.value.trim(),
|
||||
};
|
||||
|
||||
const resp = await api.cards.update(id, payload);
|
||||
if (resp.ok) {
|
||||
closeModal(editModal);
|
||||
showToast('Карточка успешно обновлена', 'success');
|
||||
await loadInitialData();
|
||||
} else {
|
||||
alert('Ошибка при сохранении карточки');
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Export Modal
|
||||
// -----------------------------------------------------------------------
|
||||
if (exportOpenBtn) {
|
||||
exportOpenBtn.addEventListener('click', () => openModal(exportModal));
|
||||
}
|
||||
closeExportModalBtn.addEventListener('click', () => closeModal(exportModal));
|
||||
exportCancelBtn.addEventListener('click', () => closeModal(exportModal));
|
||||
|
||||
document.querySelectorAll('.export-tile').forEach(tile => {
|
||||
tile.addEventListener('click', () => {
|
||||
const format = tile.dataset.format;
|
||||
let url = `/api/export/${format}`;
|
||||
if (selectedCollection) url += `?collection=${encodeURIComponent(selectedCollection)}`;
|
||||
window.location.href = url;
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Collections Management
|
||||
// -----------------------------------------------------------------------
|
||||
newCollectionBtn.addEventListener('click', () => {
|
||||
openModal(newColModal);
|
||||
newColNameInput.value = '';
|
||||
newColNameInput.focus();
|
||||
});
|
||||
|
||||
closeNewColModalBtn.addEventListener('click', () => closeModal(newColModal));
|
||||
newColCancelBtn.addEventListener('click', () => closeModal(newColModal));
|
||||
|
||||
newColSaveBtn.addEventListener('click', () => {
|
||||
const name = newColNameInput.value.trim();
|
||||
if (name) {
|
||||
selectedCollection = name;
|
||||
closeModal(newColModal);
|
||||
fetchCards();
|
||||
}
|
||||
});
|
||||
|
||||
closeMoveModalBtn.addEventListener('click', () => closeModal(moveColModal));
|
||||
moveColCancelBtn.addEventListener('click', () => closeModal(moveColModal));
|
||||
|
||||
moveColConfirmBtn.addEventListener('click', async () => {
|
||||
const col = moveColInput.value.trim();
|
||||
if (col && selectedIds.size > 0) {
|
||||
await api.cards.batch({
|
||||
card_ids: Array.from(selectedIds),
|
||||
action: 'set_collection',
|
||||
value: col,
|
||||
});
|
||||
closeModal(moveColModal);
|
||||
selectedIds.clear();
|
||||
showToast(`Перемещено в "${col}"`, 'success');
|
||||
await loadInitialData();
|
||||
}
|
||||
});
|
||||
|
||||
function populateDatalists() {
|
||||
collectionsDatalist.innerHTML = '';
|
||||
const dl = document.getElementById('move-collections-datalist');
|
||||
if (dl) dl.innerHTML = '';
|
||||
|
||||
collectionsList.forEach(c => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = c;
|
||||
collectionsDatalist.appendChild(opt);
|
||||
if (dl) dl.appendChild(opt.cloneNode(true));
|
||||
});
|
||||
|
||||
if (moveColQuickList) {
|
||||
moveColQuickList.innerHTML = '';
|
||||
collectionsList.forEach(c => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn btn-xs btn-outline';
|
||||
btn.textContent = c;
|
||||
btn.onclick = () => { moveColInput.value = c; };
|
||||
moveColQuickList.appendChild(btn);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Data Loading Functions
|
||||
// -----------------------------------------------------------------------
|
||||
async function fetchCards() {
|
||||
try {
|
||||
const params = { limit: 500 };
|
||||
if (selectedCollection) params.collection = selectedCollection;
|
||||
if (selectedTag) params.tag = selectedTag;
|
||||
if (searchQuery) params.search = searchQuery;
|
||||
|
||||
const data = await api.cards.getAll(params);
|
||||
allCards = data.cards || [];
|
||||
renderCardsList();
|
||||
} catch (e) {
|
||||
console.error('Error fetching cards:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCollections() {
|
||||
try {
|
||||
const cols = await api.collections.getAll();
|
||||
collectionsList = cols.map(c => c.name);
|
||||
renderCollectionsFilter(cols);
|
||||
populateDatalists();
|
||||
} catch (e) {
|
||||
console.error('Error fetching collections:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchTags() {
|
||||
try {
|
||||
const tags = await api.tags.getAll();
|
||||
renderTagsFilter(tags);
|
||||
} catch (e) {
|
||||
console.error('Error fetching tags:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchStats() {
|
||||
try {
|
||||
const data = await api.stats.getStats();
|
||||
if (data) {
|
||||
const statTotal = document.getElementById('stat-total');
|
||||
const statToday = document.getElementById('stat-today');
|
||||
if (statTotal) statTotal.textContent = data.total_cards;
|
||||
if (statToday) statToday.textContent = `+${data.today_added}`;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function fetchSrsStats() {
|
||||
try {
|
||||
const stats = await api.stats.getSrsStats();
|
||||
if (stats) {
|
||||
const due = stats.due_cards || 0;
|
||||
const srsBadge = document.getElementById('srs-badge');
|
||||
if (srsBadge) srsBadge.textContent = due;
|
||||
const tabSrsBadge = document.getElementById('tab-srs-badge');
|
||||
if (tabSrsBadge) {
|
||||
tabSrsBadge.textContent = due;
|
||||
tabSrsBadge.classList.toggle('hidden', due === 0);
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function loadInitialData() {
|
||||
await Promise.all([
|
||||
fetchCards(),
|
||||
fetchCollections(),
|
||||
fetchTags(),
|
||||
fetchStats(),
|
||||
fetchSrsStats(),
|
||||
]);
|
||||
}
|
||||
|
||||
// Attach to module exports
|
||||
initDictionary.fetchCards = fetchCards;
|
||||
initDictionary.loadInitialData = loadInitialData;
|
||||
initDictionary.openEditModal = openEditModal;
|
||||
}
|
||||
|
||||
export async function loadInitialData() {
|
||||
if (initDictionary.loadInitialData) {
|
||||
return await initDictionary.loadInitialData();
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchCards() {
|
||||
if (initDictionary.fetchCards) {
|
||||
return await initDictionary.fetchCards();
|
||||
}
|
||||
}
|
||||
|
||||
export function openEditModal(card) {
|
||||
if (initDictionary.openEditModal) {
|
||||
initDictionary.openEditModal(card);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user