// Notes Application Frontend Logic document.addEventListener('DOMContentLoaded', () => { // State let currentUser = null; let currentLookupData = null; let allCards = []; let selectedTag = ''; let searchQuery = ''; // Elements - Auth const authScreen = document.getElementById('auth-screen'); const appContainer = document.getElementById('app-container'); const loginForm = document.getElementById('login-form'); const usernameInput = document.getElementById('username'); const passwordInput = document.getElementById('password'); const authError = document.getElementById('auth-error'); const loginBtn = document.getElementById('login-btn'); const logoutBtn = document.getElementById('logout-btn'); const userDisplayName = document.getElementById('user-display-name'); // Elements - Navbar & Stats const themeToggle = document.getElementById('theme-toggle'); const statTotal = document.getElementById('stat-total'); const statToday = document.getElementById('stat-today'); // Elements - Search & Result const termInput = document.getElementById('term-input'); const clearSearchBtn = document.getElementById('clear-search-btn'); const translateBtn = document.getElementById('translate-btn'); const lookupSpinner = document.getElementById('lookup-spinner'); const resultCard = document.getElementById('result-card'); const resTerm = document.getElementById('res-term'); const resPhonetic = document.getElementById('res-phonetic'); const resPos = document.getElementById('res-pos'); const playAudioBtn = document.getElementById('play-audio-btn'); const resDefinitionBox = document.getElementById('res-definition-box'); const resDefinition = document.getElementById('res-definition'); const editTranslation = document.getElementById('edit-translation'); const translationSuggestions = document.getElementById('translation-suggestions'); const editContext = document.getElementById('edit-context'); const editTags = document.getElementById('edit-tags'); const quickTagsBar = document.getElementById('quick-tags-bar'); const editNotes = document.getElementById('edit-notes'); const saveCardBtn = document.getElementById('save-card-btn'); // Elements - Vocabulary List const filterInput = document.getElementById('filter-input'); const tagsFilterBar = document.getElementById('tags-filter-bar'); const cardsList = document.getElementById('cards-list'); const emptyState = document.getElementById('empty-state'); const cardsCountLabel = document.getElementById('cards-count-label'); // Elements - Modal const editModal = document.getElementById('edit-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 modalTags = document.getElementById('modal-tags'); const modalNotes = document.getElementById('modal-notes'); // Audio const globalAudioPlayer = document.getElementById('global-audio-player'); // Theme Management function initTheme() { const savedTheme = localStorage.getItem('notes_theme') || 'dark'; document.documentElement.setAttribute('data-theme', savedTheme); themeToggle.textContent = savedTheme === 'dark' ? '☀️' : '🌙'; } themeToggle.addEventListener('click', () => { const currentTheme = document.documentElement.getAttribute('data-theme') || 'dark'; const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('notes_theme', newTheme); themeToggle.textContent = newTheme === 'dark' ? '☀️' : '🌙'; }); // Pronunciation Helper (Audio file or Web Speech API fallback) function playAudio(text, audioUrl) { if (audioUrl) { globalAudioPlayer.src = audioUrl; globalAudioPlayer.play().catch(() => { fallbackSpeechSynthesis(text); }); } else { fallbackSpeechSynthesis(text); } } function fallbackSpeechSynthesis(text) { if ('speechSynthesis' in window) { window.speechSynthesis.cancel(); const utterance = new SpeechSynthesisUtterance(text); utterance.lang = 'en-US'; utterance.rate = 0.9; window.speechSynthesis.speak(utterance); } } // Auth Handlers async function checkAuth() { try { const resp = await fetch('/api/me'); if (resp.ok) { const data = await resp.json(); currentUser = data.user; showApp(); } else { showAuth(); } } catch (e) { showAuth(); } } function showAuth() { authScreen.classList.remove('hidden'); appContainer.classList.add('hidden'); usernameInput.focus(); } function showApp() { authScreen.classList.add('hidden'); appContainer.classList.remove('hidden'); userDisplayName.textContent = currentUser ? currentUser.username : 'user'; termInput.focus(); loadStats(); loadCards(); loadTags(); } loginForm.addEventListener('submit', async (e) => { e.preventDefault(); authError.classList.add('hidden'); loginBtn.disabled = true; loginBtn.textContent = 'Вход...'; try { const resp = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: usernameInput.value.trim(), password: passwordInput.value }) }); const data = await resp.json(); if (resp.ok) { currentUser = data.user; passwordInput.value = ''; showApp(); } else { authError.textContent = data.detail || 'Ошибка авторизации'; authError.classList.remove('hidden'); } } catch (err) { authError.textContent = 'Ошибка сети. Проверьте подключение к серверу.'; authError.classList.remove('hidden'); } finally { loginBtn.disabled = false; loginBtn.textContent = 'Войти в систему'; } }); logoutBtn.addEventListener('click', async () => { try { await fetch('/api/logout', { method: 'POST' }); } catch (e) {} currentUser = null; showAuth(); }); // Stats async function loadStats() { try { const resp = await fetch('/api/stats'); if (resp.ok) { const data = await resp.json(); statTotal.textContent = data.total_cards; statToday.textContent = `+${data.today_added}`; } } catch (e) {} } // Search & Translate async function performLookup() { const query = termInput.value.trim(); if (!query) return; lookupSpinner.classList.remove('hidden'); resultCard.classList.add('hidden'); translateBtn.disabled = true; try { const resp = await fetch(`/api/lookup?q=${encodeURIComponent(query)}`); if (resp.status === 401) { showAuth(); return; } if (!resp.ok) throw new Error('Ошибка словаря'); const data = await resp.json(); currentLookupData = data; renderResultCard(data); } catch (err) { alert('Не удалось получить перевод. Попробуйте снова или введите перевод вручную.'); } finally { lookupSpinner.classList.add('hidden'); translateBtn.disabled = false; } } function renderResultCard(data) { resTerm.textContent = data.query; // Phonetic if (data.phonetic) { resPhonetic.textContent = data.phonetic; resPhonetic.classList.remove('hidden'); } else { resPhonetic.classList.add('hidden'); } // Part of Speech if (data.part_of_speech) { resPos.textContent = data.part_of_speech; resPos.classList.remove('hidden'); } else { resPos.classList.add('hidden'); } // Definitions if (data.definitions && data.definitions.length > 0) { resDefinition.textContent = data.definitions[0]; resDefinitionBox.classList.remove('hidden'); } else { resDefinitionBox.classList.add('hidden'); } // Translation field editTranslation.value = data.translation || ''; // Translation suggestions chips translationSuggestions.innerHTML = ''; if (data.translations_list && data.translations_list.length > 1) { data.translations_list.forEach(t => { const chip = document.createElement('span'); chip.className = 'suggestion-chip'; chip.textContent = t; chip.addEventListener('click', () => { editTranslation.value = t; }); translationSuggestions.appendChild(chip); }); } // Context: if user entered a sentence, put it in context field if (data.is_sentence) { editContext.value = data.query; } else { editContext.value = ''; } // Auto-assign default tags let defaultTags = ['general']; if (data.is_sentence) { defaultTags = ['phrase']; } else if (data.query.includes(' ') && data.part_of_speech && data.part_of_speech.toLowerCase().includes('verb')) { defaultTags = ['phrasal-verb']; } editTags.value = defaultTags.join(', '); editNotes.value = ''; resultCard.classList.remove('hidden'); editTranslation.focus(); } // Audio Play for Result Card playAudioBtn.addEventListener('click', () => { if (!currentLookupData) return; playAudio(currentLookupData.query, currentLookupData.audio_url); }); // Quick Tags Selector quickTagsBar.addEventListener('click', (e) => { if (e.target.classList.contains('tag-chip')) { const tag = e.target.dataset.tag; const currentTags = editTags.value.split(',').map(t => t.trim()).filter(Boolean); if (!currentTags.includes(tag)) { currentTags.push(tag); editTags.value = currentTags.join(', '); } } }); // Save Card async function saveCurrentCard() { if (!currentLookupData) return; const term = currentLookupData.query; const translation = editTranslation.value.trim(); if (!translation) { alert('Пожалуйста, укажите перевод слова.'); editTranslation.focus(); return; } saveCardBtn.disabled = true; saveCardBtn.textContent = 'Сохранение...'; try { 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.definitions && currentLookupData.definitions.length ? currentLookupData.definitions[0] : '', tags: editTags.value.trim() || 'general', notes: editNotes.value.trim() }; const resp = await fetch('/api/cards', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (resp.ok) { // Success feedback saveCardBtn.textContent = '✅ Сохранено!'; setTimeout(() => { resultCard.classList.add('hidden'); termInput.value = ''; clearSearchBtn.classList.add('hidden'); termInput.focus(); saveCardBtn.textContent = '💾 Сохранить в словарь (Ctrl + Enter)'; saveCardBtn.disabled = false; }, 600); loadCards(); loadStats(); loadTags(); } else { throw new Error('Ошибка сохранения'); } } catch (err) { alert('Не удалось сохранить карточку. Попробуйте еще раз.'); saveCardBtn.disabled = false; saveCardBtn.textContent = '💾 Сохранить в словарь (Ctrl + Enter)'; } } saveCardBtn.addEventListener('click', saveCurrentCard); // Term Input controls termInput.addEventListener('input', () => { if (termInput.value.trim()) { clearSearchBtn.classList.remove('hidden'); } else { clearSearchBtn.classList.add('hidden'); } }); clearSearchBtn.addEventListener('click', () => { termInput.value = ''; clearSearchBtn.classList.add('hidden'); resultCard.classList.add('hidden'); termInput.focus(); }); translateBtn.addEventListener('click', performLookup); // Global Hotkeys document.addEventListener('keydown', (e) => { // Ctrl+Enter or Cmd+Enter to Save Card if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); if (!resultCard.classList.contains('hidden')) { saveCurrentCard(); } return; } // Enter in search input if (e.key === 'Enter' && document.activeElement === termInput) { e.preventDefault(); performLookup(); return; } // Esc to clear search or close modal if (e.key === 'Escape') { if (!editModal.classList.contains('hidden')) { editModal.classList.add('hidden'); } else if (document.activeElement === termInput) { termInput.value = ''; clearSearchBtn.classList.add('hidden'); resultCard.classList.add('hidden'); } return; } }); // Vocabulary Cards & Tags async function loadCards() { try { let url = `/api/cards?limit=150`; if (searchQuery) url += `&search=${encodeURIComponent(searchQuery)}`; if (selectedTag) url += `&tag=${encodeURIComponent(selectedTag)}`; const resp = await fetch(url); if (resp.status === 401) { showAuth(); return; } if (resp.ok) { const data = await resp.json(); allCards = data.cards; cardsCountLabel.textContent = `(${data.total} записей)`; renderCards(allCards); } } catch (e) {} } function renderCards(cards) { cardsList.innerHTML = ''; if (!cards || cards.length === 0) { emptyState.classList.remove('hidden'); cardsList.appendChild(emptyState); return; } emptyState.classList.add('hidden'); cards.forEach(card => { const cardEl = document.createElement('div'); cardEl.className = 'vocab-card'; // Format date const cardDate = new Date(card.created_at); const dateStr = cardDate.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }); // Format tags const tagsList = (card.tags || '').split(',').map(t => t.trim()).filter(Boolean); const tagsHtml = tagsList.map(t => `#${escapeHtml(t)}`).join(''); // Highlight context let contextHtml = ''; if (card.context_sentence) { let formattedContext = escapeHtml(card.context_sentence); // Highlight term occurrence in context const regex = new RegExp(`(${escapeRegExp(card.term)})`, 'gi'); formattedContext = formattedContext.replace(regex, '$1'); contextHtml = `