fix(features): пустой блок флешкарт, лаба в сайдбаре, мигание (FOUC)
Три проблемы UX отключения модулей: 1) Пустой блок флешкарт на дашборде: виджет #w-flashcard — <div>, а скрытие шло только по [href]. Добавлена карта FEATURE_WIDGETS (флешкарты→#w-flashcard) — контейнер прячется целиком. 2) Лаборатория не уходила из сайдбара: не было ГЛОБАЛЬНОГО тумблера lab (только free-student). Добавлен в whitelist updateFeatures + GAME_FEATURES; map уже знал lab→/lab. Теперь выключение скрывает пункт и редиректит со страницы. 3) Мигание выключенных модулей (FOUC): hideDisabledFeatures асинхронный. Теперь loadFeatures кэширует /api/features в localStorage, а _applyFeatureCss инъектит <style id=ls-feat-hide> синхронно из кэша на ранней загрузке (api.js идёт до sidebar.js) — сайдбар/виджеты строятся уже скрытыми. Геймификация: класс no-gamification ставится на <html> (раньше body), ls.css правило body.no-gamification → .no-gamification (работает и для html, и для body). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -826,10 +826,67 @@ async function loadFeatures() {
|
||||
_featuresCache = await apiFetch('/api/features');
|
||||
} catch { _featuresCache = {}; }
|
||||
_gamificationEnabled = _featuresCache.gamification !== false;
|
||||
try { localStorage.setItem('ls_feat_cache', JSON.stringify(_featuresCache)); } catch {}
|
||||
_applyFeatureCss(_featuresCache); // авторитетное скрытие по свежим данным
|
||||
return _featuresCache;
|
||||
}
|
||||
function clearFeaturesCache() { _featuresCache = null; _gamificationEnabled = null; }
|
||||
|
||||
/* Карта «фича → href пунктов меню» (скрытие из сайдбара + редирект со страницы). */
|
||||
const FEATURE_HREFS = {
|
||||
hangman: ['/hangman'],
|
||||
crossword: ['/crossword'],
|
||||
pet: ['/pet'],
|
||||
red_book: ['/red-book', '/red-book.html', '/red-book-ecosystem.html', '/red-book-biomes.html'],
|
||||
collection: ['/collection.html', '/collection'],
|
||||
lab: ['/lab'],
|
||||
knowledge_map: ['/knowledge-map'],
|
||||
flashcards: ['/flashcards'],
|
||||
board: ['/board'],
|
||||
biochem: ['/biochem', '/biochem-library', '/biochem-reactions'],
|
||||
live_quiz: ['/live-quiz'],
|
||||
classroom: ['/classroom'],
|
||||
sim_builder: ['/sim-builder', '/sim-builder.html'],
|
||||
exam9: ['/exam9', '/exam9.html'],
|
||||
textbooks: ['/textbooks', '/textbooks.html', '/textbook'],
|
||||
quantik: ['/quantik', '/quantik.html'],
|
||||
theory: ['/theory', '/theory.html'],
|
||||
};
|
||||
/* Контейнеры виджетов-модулей (дашборд и т.п.) — прячем блок целиком, а не только
|
||||
ссылку, иначе остаётся пустой блок (напр. виджет флеш-карт #w-flashcard). */
|
||||
const FEATURE_WIDGETS = {
|
||||
flashcards: ['#w-flashcard'],
|
||||
};
|
||||
/* Инъекция CSS, прячущего отключённые фичи. Ставится синхронно из localStorage-кэша
|
||||
на ранней загрузке (ДО построения сайдбара/виджетов) — против мигания (FOUC),
|
||||
затем обновляется по свежему /api/features. */
|
||||
function _applyFeatureCss(feats) {
|
||||
if (!feats) return;
|
||||
const sels = [];
|
||||
for (const [key, hrefs] of Object.entries(FEATURE_HREFS)) {
|
||||
if (feats[key] === false) {
|
||||
hrefs.forEach(h => sels.push(`[href="${h}"]`));
|
||||
(FEATURE_WIDGETS[key] || []).forEach(s => sels.push(s));
|
||||
}
|
||||
}
|
||||
const css = sels.length ? sels.join(',') + '{display:none !important}' : '';
|
||||
let el = document.getElementById('ls-feat-hide');
|
||||
if (!el) {
|
||||
el = document.createElement('style');
|
||||
el.id = 'ls-feat-hide';
|
||||
(document.head || document.documentElement).appendChild(el);
|
||||
}
|
||||
el.textContent = css;
|
||||
// Геймификация: класс на <html> (доступен раньше body) → kill-switch без мигания.
|
||||
document.documentElement.classList.toggle('no-gamification', feats.gamification === false);
|
||||
}
|
||||
/* Ранняя синхронная попытка из кэша прошлой загрузки — нет мигания на повторных заходах.
|
||||
(FEATURE_HREFS — const, поэтому этот вызов идёт ПОСЛЕ его объявления.) */
|
||||
try {
|
||||
const _cachedFeats = JSON.parse(localStorage.getItem('ls_feat_cache') || 'null');
|
||||
if (_cachedFeats) _applyFeatureCss(_cachedFeats);
|
||||
} catch { /* нет кэша / приватный режим — просто ждём async */ }
|
||||
|
||||
/**
|
||||
* Show board sidebar link only for teachers/admins and students in a class.
|
||||
* Call after LS.initPage(). Uses features cache (_no_class flag).
|
||||
@@ -850,32 +907,10 @@ async function showBoardIfAllowed() {
|
||||
}
|
||||
|
||||
async function hideDisabledFeatures() {
|
||||
const feats = await loadFeatures();
|
||||
const map = {
|
||||
hangman: ['/hangman'],
|
||||
crossword: ['/crossword'],
|
||||
pet: ['/pet'],
|
||||
red_book: ['/red-book', '/red-book.html', '/red-book-ecosystem.html', '/red-book-biomes.html'],
|
||||
collection: ['/collection.html', '/collection'],
|
||||
lab: ['/lab'],
|
||||
knowledge_map: ['/knowledge-map'],
|
||||
flashcards: ['/flashcards'],
|
||||
board: ['/board'],
|
||||
biochem: ['/biochem', '/biochem-library', '/biochem-reactions'],
|
||||
live_quiz: ['/live-quiz'],
|
||||
classroom: ['/classroom'],
|
||||
sim_builder: ['/sim-builder', '/sim-builder.html'],
|
||||
exam9: ['/exam9', '/exam9.html'],
|
||||
textbooks: ['/textbooks', '/textbooks.html', '/textbook'],
|
||||
quantik: ['/quantik', '/quantik.html'],
|
||||
theory: ['/theory', '/theory.html'],
|
||||
};
|
||||
for (const [key, hrefs] of Object.entries(map)) {
|
||||
const feats = await loadFeatures(); // loadFeatures уже вызвал _applyFeatureCss (визуальное скрытие)
|
||||
// Редирект со страницы отключённой фичи (CSS прячет ссылки, а тут уводим со страницы).
|
||||
for (const [key, hrefs] of Object.entries(FEATURE_HREFS)) {
|
||||
if (feats[key] === false) {
|
||||
hrefs.forEach(href => {
|
||||
document.querySelectorAll(`[href="${href}"]`).forEach(el => el.style.display = 'none');
|
||||
});
|
||||
// Redirect away if currently on a disabled page
|
||||
const cur = window.location.pathname;
|
||||
if (hrefs.some(h => cur === h || cur === h.replace('.html', ''))) {
|
||||
window.location.href = '/dashboard.html';
|
||||
@@ -883,7 +918,7 @@ async function hideDisabledFeatures() {
|
||||
}
|
||||
}
|
||||
if (feats.gamification === false) {
|
||||
document.body.classList.add('no-gamification');
|
||||
document.body.classList.add('no-gamification'); // дубль на body (html-класс ставит _applyFeatureCss)
|
||||
// If student is already viewing achievements or shop tab, redirect to account tab
|
||||
const active = document.querySelector('#tab-achievements.active, #tab-shop.active');
|
||||
if (active) {
|
||||
|
||||
Reference in New Issue
Block a user