fix(frontend): export ui object, harmonize API signatures, and clean view switcher

This commit is contained in:
2026-10-07 13:13:45 +03:00
parent 718e9797eb
commit 640da1f27c
4 changed files with 138 additions and 147 deletions
+78 -10
View File
@@ -3,7 +3,7 @@
// =======================================================================
export function escapeHtml(str) {
if (!str) return '';
if (str === null || str === undefined) return '';
return String(str)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
@@ -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;
`;
document.body.appendChild(toastContainer);
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);
localStorage.setItem('notes_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;