fix(frontend): export ui object, harmonize API signatures, and clean view switcher
This commit is contained in:
1 parent
718e9797eb
commit
640da1f27c
4 files changed
+136
-145
No files matched your search
+16
-116
@@ -4,7 +4,7 @@
|
||||
// =======================================================================
|
||||
|
||||
import { api } from './modules/api.js';
|
||||
import { initTheme, openModal, closeModal, showToast, playTextSpeech } from './modules/ui.js';
|
||||
import { initTheme, openModal, closeModal, showToast, playTextSpeech, switchView } from './modules/ui.js';
|
||||
import { initDictionary, loadInitialData } from './modules/dictionary.js';
|
||||
import { initSrs, startSrsTrainingSession, openStatsDashboard } from './modules/srs.js';
|
||||
import { initReader, loadBookshelf, openBook } from './modules/reader.js';
|
||||
@@ -81,25 +81,12 @@ export function applyInitialViewPreflight() {
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const dictionaryView = document.getElementById('dictionary-view');
|
||||
const srsView = document.getElementById('srs-view');
|
||||
const readerView = document.getElementById('reader-view');
|
||||
const cefrView = document.getElementById('cefr-view');
|
||||
const grammarView = document.getElementById('grammar-view');
|
||||
const lessonView = document.getElementById('lesson-view');
|
||||
const lessonRunnerView = document.getElementById('lesson-runner-view');
|
||||
const bookshelfContainer = document.getElementById('bookshelf-container');
|
||||
const readerInterface = document.getElementById('reader-interface');
|
||||
const readerBookTitle = document.getElementById('reader-book-title');
|
||||
|
||||
if (hash.startsWith('#reader') || hash === '#bookshelf') {
|
||||
if (dictionaryView) dictionaryView.classList.add('hidden');
|
||||
if (srsView) srsView.classList.add('hidden');
|
||||
if (cefrView) cefrView.classList.add('hidden');
|
||||
if (grammarView) grammarView.classList.add('hidden');
|
||||
if (lessonView) lessonView.classList.add('hidden');
|
||||
if (lessonRunnerView) lessonRunnerView.classList.add('hidden');
|
||||
if (readerView) readerView.classList.remove('hidden');
|
||||
switchView('reader-view');
|
||||
updateActiveNavState('reader');
|
||||
if (hash.includes('/book/')) {
|
||||
if (bookshelfContainer) bookshelfContainer.classList.add('hidden');
|
||||
@@ -107,40 +94,16 @@ export function applyInitialViewPreflight() {
|
||||
if (readerBookTitle) readerBookTitle.textContent = 'Загрузка книги...';
|
||||
}
|
||||
} else if (hash.startsWith('#srs')) {
|
||||
if (dictionaryView) dictionaryView.classList.add('hidden');
|
||||
if (readerView) readerView.classList.add('hidden');
|
||||
if (cefrView) cefrView.classList.add('hidden');
|
||||
if (grammarView) grammarView.classList.add('hidden');
|
||||
if (lessonView) lessonView.classList.add('hidden');
|
||||
if (lessonRunnerView) lessonRunnerView.classList.add('hidden');
|
||||
if (srsView) srsView.classList.remove('hidden');
|
||||
switchView('srs-view');
|
||||
updateActiveNavState('srs');
|
||||
} else if (hash.startsWith('#cefr')) {
|
||||
if (dictionaryView) dictionaryView.classList.add('hidden');
|
||||
if (readerView) readerView.classList.add('hidden');
|
||||
if (srsView) srsView.classList.add('hidden');
|
||||
if (grammarView) grammarView.classList.add('hidden');
|
||||
if (lessonView) lessonView.classList.add('hidden');
|
||||
if (lessonRunnerView) lessonRunnerView.classList.add('hidden');
|
||||
if (cefrView) cefrView.classList.remove('hidden');
|
||||
switchView('cefr-view');
|
||||
updateActiveNavState('cefr');
|
||||
} else if (hash.startsWith('#grammar')) {
|
||||
if (dictionaryView) dictionaryView.classList.add('hidden');
|
||||
if (readerView) readerView.classList.add('hidden');
|
||||
if (srsView) srsView.classList.add('hidden');
|
||||
if (cefrView) cefrView.classList.add('hidden');
|
||||
if (lessonView) lessonView.classList.add('hidden');
|
||||
if (lessonRunnerView) lessonRunnerView.classList.add('hidden');
|
||||
if (grammarView) grammarView.classList.remove('hidden');
|
||||
switchView('grammar-view');
|
||||
updateActiveNavState('grammar');
|
||||
} else if (hash.startsWith('#lesson')) {
|
||||
if (dictionaryView) dictionaryView.classList.add('hidden');
|
||||
if (readerView) readerView.classList.add('hidden');
|
||||
if (srsView) srsView.classList.add('hidden');
|
||||
if (cefrView) cefrView.classList.add('hidden');
|
||||
if (grammarView) grammarView.classList.add('hidden');
|
||||
if (lessonRunnerView) lessonRunnerView.classList.add('hidden');
|
||||
if (lessonView) lessonView.classList.remove('hidden');
|
||||
switchView('lesson-view');
|
||||
updateActiveNavState('lesson');
|
||||
}
|
||||
}
|
||||
@@ -153,17 +116,7 @@ export function openDictionaryView(updateHash = true) {
|
||||
window.location.hash = '#dictionary';
|
||||
return;
|
||||
}
|
||||
const dictionaryView = document.getElementById('dictionary-view');
|
||||
const srsView = document.getElementById('srs-view');
|
||||
const readerView = document.getElementById('reader-view');
|
||||
const cefrView = document.getElementById('cefr-view');
|
||||
const grammarView = document.getElementById('grammar-view');
|
||||
|
||||
if (srsView) srsView.classList.add('hidden');
|
||||
if (readerView) readerView.classList.add('hidden');
|
||||
if (cefrView) cefrView.classList.add('hidden');
|
||||
if (grammarView) grammarView.classList.add('hidden');
|
||||
if (dictionaryView) dictionaryView.classList.remove('hidden');
|
||||
switchView('dictionary-view');
|
||||
updateActiveNavState('dict');
|
||||
try { localStorage.setItem('notes_active_view', 'dictionary-view'); } catch (e) {}
|
||||
loadInitialData();
|
||||
@@ -175,17 +128,7 @@ export function openSrsView(tagFilter = null, updateHash = true) {
|
||||
window.location.hash = targetHash;
|
||||
return;
|
||||
}
|
||||
const dictionaryView = document.getElementById('dictionary-view');
|
||||
const srsView = document.getElementById('srs-view');
|
||||
const readerView = document.getElementById('reader-view');
|
||||
const cefrView = document.getElementById('cefr-view');
|
||||
const grammarView = document.getElementById('grammar-view');
|
||||
|
||||
if (dictionaryView) dictionaryView.classList.add('hidden');
|
||||
if (readerView) readerView.classList.add('hidden');
|
||||
if (cefrView) cefrView.classList.add('hidden');
|
||||
if (grammarView) grammarView.classList.add('hidden');
|
||||
if (srsView) srsView.classList.remove('hidden');
|
||||
switchView('srs-view');
|
||||
updateActiveNavState('srs');
|
||||
try { localStorage.setItem('notes_active_view', 'srs-view'); } catch (e) {}
|
||||
startSrsTrainingSession(false, tagFilter);
|
||||
@@ -200,19 +143,9 @@ export function openBookshelfView(updateHash = true) {
|
||||
window.location.hash = '#reader';
|
||||
return;
|
||||
}
|
||||
const dictionaryView = document.getElementById('dictionary-view');
|
||||
const srsView = document.getElementById('srs-view');
|
||||
const readerView = document.getElementById('reader-view');
|
||||
const cefrView = document.getElementById('cefr-view');
|
||||
const grammarView = document.getElementById('grammar-view');
|
||||
switchView('reader-view');
|
||||
const bookshelfContainer = document.getElementById('bookshelf-container');
|
||||
const readerInterface = document.getElementById('reader-interface');
|
||||
|
||||
if (dictionaryView) dictionaryView.classList.add('hidden');
|
||||
if (srsView) srsView.classList.add('hidden');
|
||||
if (cefrView) cefrView.classList.add('hidden');
|
||||
if (grammarView) grammarView.classList.add('hidden');
|
||||
if (readerView) readerView.classList.remove('hidden');
|
||||
if (bookshelfContainer) bookshelfContainer.classList.remove('hidden');
|
||||
if (readerInterface) readerInterface.classList.add('hidden');
|
||||
updateActiveNavState('reader');
|
||||
@@ -242,18 +175,7 @@ export function openCefrView(level = null, mode = 'all', updateHash = true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dictionaryView = document.getElementById('dictionary-view');
|
||||
const srsView = document.getElementById('srs-view');
|
||||
const readerView = document.getElementById('reader-view');
|
||||
const cefrView = document.getElementById('cefr-view');
|
||||
const grammarView = document.getElementById('grammar-view');
|
||||
|
||||
if (dictionaryView) dictionaryView.classList.add('hidden');
|
||||
if (srsView) srsView.classList.add('hidden');
|
||||
if (readerView) readerView.classList.add('hidden');
|
||||
if (grammarView) grammarView.classList.add('hidden');
|
||||
if (cefrView) cefrView.classList.remove('hidden');
|
||||
|
||||
switchView('cefr-view');
|
||||
updateActiveNavState('cefr');
|
||||
try { localStorage.setItem('notes_active_view', 'cefr-view'); } catch (e) {}
|
||||
|
||||
@@ -272,18 +194,7 @@ export function openGrammarView(unitId = null, updateHash = true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dictionaryView = document.getElementById('dictionary-view');
|
||||
const srsView = document.getElementById('srs-view');
|
||||
const readerView = document.getElementById('reader-view');
|
||||
const cefrView = document.getElementById('cefr-view');
|
||||
const grammarView = document.getElementById('grammar-view');
|
||||
|
||||
if (dictionaryView) dictionaryView.classList.add('hidden');
|
||||
if (srsView) srsView.classList.add('hidden');
|
||||
if (readerView) readerView.classList.add('hidden');
|
||||
if (cefrView) cefrView.classList.add('hidden');
|
||||
if (grammarView) grammarView.classList.remove('hidden');
|
||||
|
||||
switchView('grammar-view');
|
||||
updateActiveNavState('grammar');
|
||||
try { localStorage.setItem('notes_active_view', 'grammar-view'); } catch (e) {}
|
||||
|
||||
@@ -301,22 +212,7 @@ export function openLessonView(presetId = null, updateHash = true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dictionaryView = document.getElementById('dictionary-view');
|
||||
const srsView = document.getElementById('srs-view');
|
||||
const readerView = document.getElementById('reader-view');
|
||||
const cefrView = document.getElementById('cefr-view');
|
||||
const grammarView = document.getElementById('grammar-view');
|
||||
const lessonView = document.getElementById('lesson-view');
|
||||
const lessonRunnerView = document.getElementById('lesson-runner-view');
|
||||
|
||||
if (dictionaryView) dictionaryView.classList.add('hidden');
|
||||
if (srsView) srsView.classList.add('hidden');
|
||||
if (readerView) readerView.classList.add('hidden');
|
||||
if (cefrView) cefrView.classList.add('hidden');
|
||||
if (grammarView) grammarView.classList.add('hidden');
|
||||
if (lessonRunnerView) lessonRunnerView.classList.add('hidden');
|
||||
if (lessonView) lessonView.classList.remove('hidden');
|
||||
|
||||
switchView('lesson-view');
|
||||
updateActiveNavState('lesson');
|
||||
try { localStorage.setItem('notes_active_view', 'lesson-view'); } catch (e) {}
|
||||
|
||||
@@ -352,6 +248,10 @@ export async function handleRouting() {
|
||||
hash = '#grammar';
|
||||
window.location.hash = hash;
|
||||
return;
|
||||
} else if (lastView === 'lesson-view') {
|
||||
hash = '#lesson';
|
||||
window.location.hash = hash;
|
||||
return;
|
||||
} else {
|
||||
hash = '#dictionary';
|
||||
}
|
||||
|
||||
@@ -167,18 +167,34 @@ export const api = {
|
||||
|
||||
// SRS Spaced Repetition
|
||||
srs: {
|
||||
async getQueue(forceAll = false, tagFilter = null) {
|
||||
let url = '/api/srs/queue?limit=50';
|
||||
async getQueue(forceAllOrOptions = false, tagFilter = null) {
|
||||
let forceAll = false;
|
||||
let tag = tagFilter;
|
||||
let limit = 50;
|
||||
if (typeof forceAllOrOptions === 'object' && forceAllOrOptions !== null) {
|
||||
forceAll = !!forceAllOrOptions.forceAll || !!forceAllOrOptions.force_all;
|
||||
tag = forceAllOrOptions.tagFilter || forceAllOrOptions.tag || tag;
|
||||
if (forceAllOrOptions.limit) limit = forceAllOrOptions.limit;
|
||||
} else {
|
||||
forceAll = !!forceAllOrOptions;
|
||||
}
|
||||
let url = `/api/srs/queue?limit=${limit}`;
|
||||
if (forceAll) url += '&force_all=true';
|
||||
if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`;
|
||||
if (tag) url += `&tag=${encodeURIComponent(tag)}`;
|
||||
const resp = await fetch(url);
|
||||
return resp.ok ? await resp.json() : [];
|
||||
return resp.ok ? await resp.json() : { cards: [], count: 0 };
|
||||
},
|
||||
async submitReview(cardId, rating) {
|
||||
async submitReview(cardIdOrPayload, rating) {
|
||||
let cardId = cardIdOrPayload;
|
||||
let r = rating;
|
||||
if (typeof cardIdOrPayload === 'object' && cardIdOrPayload !== null) {
|
||||
cardId = cardIdOrPayload.card_id ?? cardIdOrPayload.id;
|
||||
r = cardIdOrPayload.rating;
|
||||
}
|
||||
return await fetch('/api/srs/review', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ card_id: cardId, rating })
|
||||
body: JSON.stringify({ card_id: cardId, rating: r })
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -194,15 +210,25 @@ export const api = {
|
||||
if (resp.ok) return await resp.json();
|
||||
throw new Error(`Failed to load listening exercise: ${resp.status}`);
|
||||
},
|
||||
async checkAnswer(scenarioId, userAnswer, story = '', idealAnswer = '') {
|
||||
async checkAnswer(scenarioIdOrPayload, userAnswer = '', story = '', idealAnswer = '') {
|
||||
let sId = scenarioIdOrPayload;
|
||||
let uAns = userAnswer;
|
||||
let st = story;
|
||||
let idAns = idealAnswer;
|
||||
if (typeof scenarioIdOrPayload === 'object' && scenarioIdOrPayload !== null) {
|
||||
sId = scenarioIdOrPayload.scenario_id ?? scenarioIdOrPayload.id;
|
||||
uAns = scenarioIdOrPayload.user_answer ?? scenarioIdOrPayload.answer ?? '';
|
||||
st = scenarioIdOrPayload.story ?? '';
|
||||
idAns = scenarioIdOrPayload.ideal_answer ?? scenarioIdOrPayload.idealAnswer ?? '';
|
||||
}
|
||||
const resp = await fetch('/api/listening/check', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
scenario_id: scenarioId,
|
||||
user_answer: userAnswer,
|
||||
story: story,
|
||||
ideal_answer: idealAnswer
|
||||
scenario_id: sId,
|
||||
user_answer: uAns,
|
||||
story: st,
|
||||
ideal_answer: idAns
|
||||
})
|
||||
});
|
||||
if (resp.ok) return await resp.json();
|
||||
@@ -374,6 +400,9 @@ export const api = {
|
||||
if (resp.ok) return await resp.json();
|
||||
throw new Error(`Failed to complete unit: ${resp.status}`);
|
||||
},
|
||||
async recordCompletion(unitId, score, handwrittenLogged = false) {
|
||||
return this.completeUnit(unitId, score, handwrittenLogged);
|
||||
},
|
||||
async toggleHandwritten(unitId, handwritten) {
|
||||
const resp = await fetch('/api/grammar/handwritten', {
|
||||
method: 'POST',
|
||||
@@ -385,6 +414,9 @@ export const api = {
|
||||
});
|
||||
if (resp.ok) return await resp.json();
|
||||
throw new Error(`Failed to update handwritten status: ${resp.status}`);
|
||||
},
|
||||
async updateHandwritten(unitId, handwritten) {
|
||||
return this.toggleHandwritten(unitId, handwritten);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// =======================================================================
|
||||
|
||||
import { api } from './api.js';
|
||||
import { ui } from './ui.js';
|
||||
import { ui, escapeHtml, showToast, switchView } from './ui.js';
|
||||
|
||||
let presets = [];
|
||||
let currentPreset = null;
|
||||
@@ -1630,12 +1630,3 @@ async function openHistoryModal() {
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
// =======================================================================
|
||||
|
||||
export function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
if (str === null || str === undefined) return '';
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
@@ -13,7 +13,8 @@ export function escapeHtml(str) {
|
||||
}
|
||||
|
||||
export function escapeRegex(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
if (!string) return '';
|
||||
return String(string).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
export function slugify(text) {
|
||||
@@ -25,6 +26,7 @@ export function slugify(text) {
|
||||
}
|
||||
|
||||
export function shuffleArray(array) {
|
||||
if (!array || !Array.isArray(array)) return [];
|
||||
const arr = [...array];
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
@@ -37,6 +39,7 @@ export function shuffleArray(array) {
|
||||
// Toast Notifications
|
||||
// -----------------------------------------------------------------------
|
||||
export function showToast(message, type = 'info') {
|
||||
if (!message) return;
|
||||
let toastContainer = document.getElementById('notes-toast-container');
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement('div');
|
||||
@@ -51,7 +54,11 @@ export function showToast(message, type = 'info') {
|
||||
gap: 8px;
|
||||
pointer-events: none;
|
||||
`;
|
||||
if (document.body) {
|
||||
document.body.appendChild(toastContainer);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const toast = document.createElement('div');
|
||||
@@ -98,14 +105,19 @@ export function showToast(message, type = 'info') {
|
||||
export function initTheme() {
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
const mobileThemeBtn = document.getElementById('mobile-theme-btn');
|
||||
const savedTheme = localStorage.getItem('notes_theme') || 'dark';
|
||||
let savedTheme = 'dark';
|
||||
try {
|
||||
savedTheme = localStorage.getItem('notes_theme') || 'dark';
|
||||
} catch (e) {}
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
|
||||
function toggle() {
|
||||
const current = document.documentElement.getAttribute('data-theme');
|
||||
const next = current === 'dark' ? 'light' : 'dark';
|
||||
document.documentElement.setAttribute('data-theme', next);
|
||||
try {
|
||||
localStorage.setItem('notes_theme', next);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (themeToggle) {
|
||||
@@ -120,16 +132,47 @@ export function initTheme() {
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Modal Open / Close Helpers
|
||||
// Modal Open / Close Helpers (Accepts HTMLElement or string selector/ID)
|
||||
// -----------------------------------------------------------------------
|
||||
export function openModal(modalElem) {
|
||||
if (!modalElem) return;
|
||||
modalElem.classList.remove('hidden');
|
||||
const el = typeof modalElem === 'string'
|
||||
? (document.getElementById(modalElem) || document.querySelector(modalElem))
|
||||
: modalElem;
|
||||
if (el) el.classList.remove('hidden');
|
||||
}
|
||||
|
||||
export function closeModal(modalElem) {
|
||||
if (!modalElem) return;
|
||||
modalElem.classList.add('hidden');
|
||||
const el = typeof modalElem === 'string'
|
||||
? (document.getElementById(modalElem) || document.querySelector(modalElem))
|
||||
: modalElem;
|
||||
if (el) el.classList.add('hidden');
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// View Switcher Helper
|
||||
// -----------------------------------------------------------------------
|
||||
export function switchView(targetViewId) {
|
||||
const allViews = [
|
||||
'dictionary-view',
|
||||
'srs-view',
|
||||
'reader-view',
|
||||
'cefr-view',
|
||||
'grammar-view',
|
||||
'lesson-view',
|
||||
'lesson-runner-view'
|
||||
];
|
||||
allViews.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
if (id === targetViewId) {
|
||||
el.classList.remove('hidden');
|
||||
} else {
|
||||
el.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -197,11 +240,36 @@ export function playAudio(url, speed = 1.0) {
|
||||
export function playTextSpeech(text, lang = 'en', speed = 1.0, voice = null) {
|
||||
if (!text || !text.trim()) return null;
|
||||
const isRu = lang && lang.toLowerCase().startsWith('ru');
|
||||
const savedVoice = isRu ? localStorage.getItem('notes_tts_voice_ru') : localStorage.getItem('notes_tts_voice_en');
|
||||
let savedVoice = null;
|
||||
try {
|
||||
savedVoice = isRu
|
||||
? localStorage.getItem('notes_tts_voice_ru')
|
||||
: localStorage.getItem('notes_tts_voice_en');
|
||||
} catch (e) {}
|
||||
const activeVoice = voice || savedVoice;
|
||||
let url = `/api/tts?text=${encodeURIComponent(text.trim())}&lang=${lang}&speed=${speed}`;
|
||||
let url = `/api/tts?text=${encodeURIComponent(text.trim())}&lang=${lang || 'en'}&speed=${speed}`;
|
||||
if (activeVoice) {
|
||||
url += `&voice=${encodeURIComponent(activeVoice)}`;
|
||||
}
|
||||
return playAudio(url, speed);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Unified UI Object Export (for modules importing `{ ui }` or `ui`)
|
||||
// -----------------------------------------------------------------------
|
||||
export const ui = {
|
||||
escapeHtml,
|
||||
escapeRegex,
|
||||
slugify,
|
||||
shuffleArray,
|
||||
showToast,
|
||||
initTheme,
|
||||
openModal,
|
||||
closeModal,
|
||||
switchView,
|
||||
updateHeaderStats,
|
||||
playAudio,
|
||||
playTextSpeech,
|
||||
};
|
||||
|
||||
export default ui;
|
||||
Reference in new issue
Block a user