diff --git a/backend/scripts/wrap_textbook_xp.py b/backend/scripts/wrap_textbook_xp.py new file mode 100644 index 0000000..f8ad9ba --- /dev/null +++ b/backend/scripts/wrap_textbook_xp.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +""" +wrap_textbook_xp.py — Add `data-gamified` attribute to XP UI in textbooks. + +Part of Phase 1 of the gamification kill-switch (Variant 2 of plan §E.3). +The CSS rule `body.no-gamification [data-gamified] { display:none }` +catches every wrapped block in one selector, so future authors don't have +to invent new class names just to be hidden by the master switch. + +What we wrap: + •
— chapter & hub hero badges + •
— hub overall-progress XP pill + •
— chapter inline progress card + • inline JS that generates `
` HTML strings + • `.xp-bar` containers when they're standalone + +Idempotent: if `data-gamified` is already present on an opening tag the +script skips it. Reports each file touched. + +Run from repo root: + python backend/scripts/wrap_textbook_xp.py +""" +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] / "frontend" / "textbooks" +if not ROOT.is_dir(): + print(f"[wrap-xp] textbook dir not found: {ROOT}", file=sys.stderr) + sys.exit(1) + +# Selectors that mark an opening tag as an XP block. The first group +# captures ``. +PATTERNS = [ + # <... id="hero-xp-badge" ...> + re.compile(r'(<[a-zA-Z][^>]*?\bid="hero-xp-badge"[^>]*?)(>)'), + # <... class="po-xp" ...> or class="...po-xp..." + re.compile(r'(<[a-zA-Z][^>]*?\bclass="[^"]*\bpo-xp\b[^"]*"[^>]*?)(>)'), + # <... class="...xp-card..."> — covers HTML and inline JS `'
'` + re.compile(r'(<[a-zA-Z][^>]*?\bclass="[^"]*\bxp-card\b[^"]*"[^>]*?)(>)'), + # Standalone class="hero-xp-badge" + re.compile(r'(<[a-zA-Z][^>]*?\bclass="[^"]*\bhero-xp-badge\b[^"]*"[^>]*?)(>)'), +] + +# Already-wrapped check +RX_HAS_ATTR = re.compile(r'\bdata-gamified\b') + + +def patch_file(path: Path) -> int: + """Return number of opening tags newly given data-gamified.""" + text = path.read_text(encoding="utf-8") + new = text + changes = 0 + for rx in PATTERNS: + def sub(m, _rx=rx): + nonlocal changes + head, close = m.group(1), m.group(2) + if RX_HAS_ATTR.search(head): + return m.group(0) + changes += 1 + sep = "" if head.endswith(" ") else " " + return f'{head}{sep}data-gamified{close}' + new = rx.sub(sub, new) + if new != text: + path.write_text(new, encoding="utf-8") + return changes + + +def main(): + files = sorted(ROOT.glob("*.html")) + total_files = 0 + total_tags = 0 + for f in files: + try: + n = patch_file(f) + except Exception as e: + print(f"[wrap-xp] {f.name}: ERROR {e}", file=sys.stderr) + continue + if n: + print(f"[wrap-xp] {f.name}: +{n} tag(s)") + total_files += 1 + total_tags += n + print(f"[wrap-xp] done — patched {total_tags} tags across {total_files} files " + f"(scanned {len(files)})") + + +if __name__ == "__main__": + main() diff --git a/backend/src/controllers/adminController.js b/backend/src/controllers/adminController.js index 2377865..54c9558 100644 --- a/backend/src/controllers/adminController.js +++ b/backend/src/controllers/adminController.js @@ -509,10 +509,12 @@ function getFeatures(_req, res) { /* ── PATCH /api/admin/features ──────────────────────────────────────── */ function updateFeatures(req, res) { const allowed = ['crossword', 'hangman', 'pet', 'red_book', 'collection', - 'flashcards', 'knowledge_map', 'board', 'biochem', 'live_quiz', 'classroom']; + 'flashcards', 'knowledge_map', 'board', 'biochem', 'live_quiz', 'classroom', + 'gamification']; const updates = req.body; const stmt = db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)"); const getOld = db.prepare("SELECT value FROM app_settings WHERE key = ?"); + let touchedGamification = false; for (const [name, enabled] of Object.entries(updates)) { if (!allowed.includes(name)) continue; const settingKey = `feature_${name}_enabled`; @@ -521,6 +523,15 @@ function updateFeatures(req, res) { const newVal = enabled ? '1' : '0'; stmt.run(settingKey, newVal); audit(req, 'feature.update', `feature:${name}`, `${oldVal} -> ${newVal}`); + if (name === 'gamification') touchedGamification = true; + } + // Invalidate the gamification cache so the kill-switch takes effect + // immediately (otherwise awardXP keeps running for up to 30s). + if (touchedGamification) { + try { + const { invalidateGamificationCache } = require('./gamification/_shared'); + invalidateGamificationCache(); + } catch { /* defensive — shouldn't fail */ } } res.json({ ok: true }); } diff --git a/backend/src/controllers/gamification/_shared.js b/backend/src/controllers/gamification/_shared.js index 0510e62..f48cd3f 100644 --- a/backend/src/controllers/gamification/_shared.js +++ b/backend/src/controllers/gamification/_shared.js @@ -187,8 +187,42 @@ const stmts = { ORDER BY up.purchased_at DESC LIMIT 20`), }; +/* ── Kill-switch ───────────────────────────────────────────────── + The 'gamification' feature flag is the master switch. When admin + turns it off, ALL XP/coin awards become no-ops, /api/gamification + routes return 404, and the front-end hides every XP/coin/streak + element via body.no-gamification. + + The flag is read often (every awardXP call) so we cache the value + and only re-read after a short TTL. Toggling the flag also calls + invalidateGamificationCache() from adminController so the change + takes effect immediately without waiting for the TTL. */ +const _gamCache = { value: null, expiresAt: 0 }; +const _GAM_TTL_MS = 30_000; +const _stmtFeatureGam = db.prepare("SELECT value FROM app_settings WHERE key = 'feature_gamification_enabled'"); + +function isGamificationEnabled() { + const now = Date.now(); + if (_gamCache.value !== null && now < _gamCache.expiresAt) return _gamCache.value; + let on = true; + try { + const row = _stmtFeatureGam.get(); + // Default ON if row missing (migration 029 seeds '1', but be defensive). + on = !row || row.value === '1'; + } catch { on = true; } + _gamCache.value = on; + _gamCache.expiresAt = now + _GAM_TTL_MS; + return on; +} + +function invalidateGamificationCache() { + _gamCache.value = null; + _gamCache.expiresAt = 0; +} + module.exports = { xpToLevel, levelMinXp, levelMaxXp, rankName, RANKS, GOAL_TIERS, ACHIEVEMENT_DEFS, AVATAR_FRAMES, stmts, + isGamificationEnabled, invalidateGamificationCache, }; diff --git a/backend/src/controllers/gamification/service.js b/backend/src/controllers/gamification/service.js index de704ad..8b6ff49 100644 --- a/backend/src/controllers/gamification/service.js +++ b/backend/src/controllers/gamification/service.js @@ -4,7 +4,7 @@ const sse = require('../../sse'); const { pushParentNotif } = require('../../utils/notifications'); const { stmts, xpToLevel, levelMinXp, levelMaxXp, rankName, - GOAL_TIERS, ACHIEVEMENT_DEFS, + GOAL_TIERS, ACHIEVEMENT_DEFS, isGamificationEnabled, } = require('./_shared'); /* ═══════════════════════════════════════════════════════════════════════ @@ -16,12 +16,14 @@ const { /* ── Coins ─────────────────────────────────────────────────────────── */ function awardCoins(userId, amount /*, reason */) { if (!amount || amount <= 0) return; + if (!isGamificationEnabled()) return; // master kill-switch stmts.incrCoins.run(amount, userId); } /* ── XP ────────────────────────────────────────────────────────────── */ function awardXP(userId, amount, reason) { if (!amount || amount <= 0) return; + if (!isGamificationEnabled()) return; // master kill-switch stmts.insertXpLog.run(userId, amount, reason); stmts.incrXP.run(amount, userId); const user = stmts.getXP.get(userId); @@ -52,6 +54,7 @@ function getXPInfo(userId) { /* ── Streak (called by onTestFinished) ─────────────────────────────── */ function updateStreak(userId) { + if (!isGamificationEnabled()) return; const user = stmts.getStreak.get(userId); if (!user) return; const today = new Date().toISOString().slice(0, 10); @@ -103,6 +106,7 @@ function pushAchievementNotif(userId, ach) { } function unlockAchievement(userId, slug) { + if (!isGamificationEnabled()) return false; const ach = stmts.getAchBySlug.get(slug); if (!ach) return false; const exists = stmts.hasUserAch.get(userId, ach.id); @@ -114,6 +118,7 @@ function unlockAchievement(userId, slug) { } function checkAchievements(userId) { + if (!isGamificationEnabled()) return; const row = stmts.getUserForAch.get(userId); if (!row) return; const { test_count: testCount, perfect_count: perfectCount, class_count: classCount } = row; diff --git a/backend/src/db/migrations/029_gamification_feature_flag.sql b/backend/src/db/migrations/029_gamification_feature_flag.sql new file mode 100644 index 0000000..79e287b --- /dev/null +++ b/backend/src/db/migrations/029_gamification_feature_flag.sql @@ -0,0 +1,15 @@ +-- ═══════════════════════════════════════════════════════════════ +-- 029: Seed the gamification feature flag +-- +-- Until now there was no row for feature_gamification_enabled in +-- app_settings — meaning /api/features returned `gamification: undefined` +-- and the front-end kill-switch (`body.no-gamification`) never engaged, +-- regardless of admin intent. +-- +-- Default = ON. Admins can disable via PATCH /api/admin/features +-- once 'gamification' is added to the allowed list (handled in +-- adminController.js, not this migration). +-- ═══════════════════════════════════════════════════════════════ + +INSERT OR IGNORE INTO app_settings (key, value) + VALUES ('feature_gamification_enabled', '1'); diff --git a/backend/src/routes/gamification.js b/backend/src/routes/gamification.js index 5db65ee..98f7d84 100644 --- a/backend/src/routes/gamification.js +++ b/backend/src/routes/gamification.js @@ -8,6 +8,18 @@ const { onLabExperiment, selfAward, adminAward, adminReset, adminGamStats, adminGetUser } = require('../controllers/gamificationController'); +const { isGamificationEnabled } = require('../controllers/gamification/_shared'); + +/* When gamification is globally disabled, user-facing routes return 404. + Admin routes (under /admin/*) stay accessible so the kill-switch itself + can still be flipped from the panel. */ +function gamGate(req, res, next) { + if (req.path.startsWith('/admin/')) return next(); + if (!isGamificationEnabled()) { + return res.status(404).json({ error: 'Gamification disabled' }); + } + next(); +} const labLimiter = rateLimit({ windowMs: 60_000, max: 30, message: 'Слишком частые запросы лаборатории' }); const labSchema = { body: { reactionsDiscovered: { type: 'number', min: 0, max: 100, integer: true } } }; @@ -22,6 +34,7 @@ const selfAwardSchema = { }; router.use(authMiddleware); +router.use(gamGate); router.get('/me', getMe); router.get('/achievements', getAchievements); diff --git a/backend/src/routes/shop.js b/backend/src/routes/shop.js index c5c8154..a00a6a2 100644 --- a/backend/src/routes/shop.js +++ b/backend/src/routes/shop.js @@ -6,6 +6,16 @@ const { getItems, purchaseItem, getPurchases, getCoins, getMyActive, activateItem, adminGetItems, adminCreateItem, adminUpdateItem, adminDeleteItem, adminAwardCoins, adminShopStats } = require('../controllers/shopController'); +const { isGamificationEnabled } = require('../controllers/gamification/_shared'); + +/* Same kill-switch as gamification routes — shop is part of the gam loop. */ +function shopGate(req, res, next) { + if (req.path.startsWith('/admin/')) return next(); + if (!isGamificationEnabled()) { + return res.status(404).json({ error: 'Gamification disabled' }); + } + next(); +} const purchaseLimiter = rateLimit({ windowMs: 60_000, max: 10, message: 'Слишком много покупок, подождите минуту' }); const activateSchema = { body: { type: { type: 'string', oneOf: ['frame', 'title', 'effect'] } } }; @@ -20,6 +30,7 @@ const awardCoinsSchema = { body: { }}; router.use(authMiddleware); +router.use(shopGate); router.get('/items', getItems); router.post('/items/:id/purchase', requirePermission('shop.purchase'), purchaseLimiter, purchaseItem); diff --git a/frontend/css/ls.css b/frontend/css/ls.css index 4b7814c..ea7c629 100644 --- a/frontend/css/ls.css +++ b/frontend/css/ls.css @@ -1017,13 +1017,33 @@ body { /* Student without a class: hide leaderboard */ body.no-class #lb-section { display: none !important; } +/* Gamification kill-switch. + When admin turns off the feature, body.no-gamification is set by + api.js/hideDisabledFeatures and EVERY XP / coin / streak / shop / + achievement / frame element must vanish — across the whole app, + not just the dashboard. The rules below cover: + • dashboard widgets (.gam-bar, .lb-widget) + • profile tabs (achievements, shop, frames) + • textbook XP badges and progress cards (.hero-xp-badge, .po-xp, + .xp-bar, .xp-card) + • a catch-all [data-gamified] hook that wraps any future block — + authors of new pages should wrap XP UI in a
+ instead of inventing new classes. */ body.no-gamification .gam-bar, body.no-gamification .lb-widget, body.no-gamification .achievements-section, body.no-gamification #tab-btn-achievements, body.no-gamification #tab-btn-shop, body.no-gamification #tab-achievements, -body.no-gamification #tab-shop { display: none !important; } +body.no-gamification #tab-shop, +body.no-gamification #frames-section, +body.no-gamification .hero-xp-badge, +body.no-gamification .po-xp, +body.no-gamification .xp-card, +body.no-gamification .xp-bar, +body.no-gamification .xp-pill, +body.no-gamification .xp-badge, +body.no-gamification [data-gamified] { display: none !important; } /* ══════════════════════════════════════════ RESPONSIVE — SMALL PHONES (≤ 480px) diff --git a/frontend/textbooks/algebra_10_ch1.html b/frontend/textbooks/algebra_10_ch1.html index c43a018..c2962ba 100644 --- a/frontend/textbooks/algebra_10_ch1.html +++ b/frontend/textbooks/algebra_10_ch1.html @@ -233,7 +233,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -565,7 +565,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/algebra_10_ch2.html b/frontend/textbooks/algebra_10_ch2.html index a92e311..49c193c 100644 --- a/frontend/textbooks/algebra_10_ch2.html +++ b/frontend/textbooks/algebra_10_ch2.html @@ -204,7 +204,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -423,7 +423,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/algebra_10_ch3.html b/frontend/textbooks/algebra_10_ch3.html index 5cf27ad..9674e0d 100644 --- a/frontend/textbooks/algebra_10_ch3.html +++ b/frontend/textbooks/algebra_10_ch3.html @@ -208,7 +208,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -429,7 +429,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/algebra_10_hub.html b/frontend/textbooks/algebra_10_hub.html index f4a7e46..eb75518 100644 --- a/frontend/textbooks/algebra_10_hub.html +++ b/frontend/textbooks/algebra_10_hub.html @@ -137,7 +137,7 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
Загрузка...
- +
diff --git a/frontend/textbooks/algebra_11_ch1.html b/frontend/textbooks/algebra_11_ch1.html index 0de30ad..fc8a740 100644 --- a/frontend/textbooks/algebra_11_ch1.html +++ b/frontend/textbooks/algebra_11_ch1.html @@ -253,7 +253,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0%
-
+
@@ -426,7 +426,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/algebra_11_ch2.html b/frontend/textbooks/algebra_11_ch2.html index b06e806..9d84125 100644 --- a/frontend/textbooks/algebra_11_ch2.html +++ b/frontend/textbooks/algebra_11_ch2.html @@ -233,7 +233,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0% -
+
@@ -406,7 +406,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/algebra_11_ch3.html b/frontend/textbooks/algebra_11_ch3.html index 819395b..968e526 100644 --- a/frontend/textbooks/algebra_11_ch3.html +++ b/frontend/textbooks/algebra_11_ch3.html @@ -254,7 +254,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0% -
+
@@ -432,7 +432,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/algebra_11_hub.html b/frontend/textbooks/algebra_11_hub.html index 555cf75..6025c6a 100644 --- a/frontend/textbooks/algebra_11_hub.html +++ b/frontend/textbooks/algebra_11_hub.html @@ -228,7 +228,7 @@ html.dark .final-cta-sub{color:#fcd34d}
Загрузка...
- +
diff --git a/frontend/textbooks/algebra_7_ch1.html b/frontend/textbooks/algebra_7_ch1.html index 40bfd76..97a987d 100644 --- a/frontend/textbooks/algebra_7_ch1.html +++ b/frontend/textbooks/algebra_7_ch1.html @@ -286,7 +286,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -541,7 +541,7 @@ function buildSidebar(id){ const xpForLv = _xpForLevel(STATE.level), xpNext = _xpForLevel(STATE.level + 1); const xpInLv = STATE.xp - xpForLv, xpRange = xpNext - xpForLv; const xpPct = xpRange > 0 ? Math.round(xpInLv / xpRange * 100) : 100; - html += '
XP-прогрессУр. ' + STATE.level + '
' + STATE.xp + ' XP' + xpNext + ' XP
'; + html += '
XP-прогрессУр. ' + STATE.level + '
' + STATE.xp + ' XP' + xpNext + ' XP
'; html += '

' + sb.title + '

'; sb.rows.forEach(([k,v])=>{ html += '
' + k + '' + (v ? ' — ' + v : '') + '
'; }); html += '
'; diff --git a/frontend/textbooks/algebra_7_ch2.html b/frontend/textbooks/algebra_7_ch2.html index d7caa7c..efada5f 100644 --- a/frontend/textbooks/algebra_7_ch2.html +++ b/frontend/textbooks/algebra_7_ch2.html @@ -253,7 +253,7 @@ a{color:inherit;text-decoration:none}
0% -
+
@@ -528,7 +528,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/algebra_7_ch3.html b/frontend/textbooks/algebra_7_ch3.html index e43a62e..5a88fbc 100644 --- a/frontend/textbooks/algebra_7_ch3.html +++ b/frontend/textbooks/algebra_7_ch3.html @@ -246,7 +246,7 @@ a{color:inherit;text-decoration:none}
0% -
+
@@ -484,7 +484,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/algebra_7_ch4.html b/frontend/textbooks/algebra_7_ch4.html index f2f5a68..db80adb 100644 --- a/frontend/textbooks/algebra_7_ch4.html +++ b/frontend/textbooks/algebra_7_ch4.html @@ -241,7 +241,7 @@ a{color:inherit;text-decoration:none}
0% -
+
@@ -462,7 +462,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/algebra_7_hub.html b/frontend/textbooks/algebra_7_hub.html index 36eca89..01fe758 100644 --- a/frontend/textbooks/algebra_7_hub.html +++ b/frontend/textbooks/algebra_7_hub.html @@ -142,7 +142,7 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
Загрузка...
- +
diff --git a/frontend/textbooks/algebra_8.html b/frontend/textbooks/algebra_8.html index 990ed7b..99a9a75 100644 --- a/frontend/textbooks/algebra_8.html +++ b/frontend/textbooks/algebra_8.html @@ -788,7 +788,7 @@ input,select,textarea{font-family:inherit}
0%
-
+
@@ -1341,8 +1341,8 @@ function buildSidebar(id){ const xpInLevel = STATE.xp - xpForLevel; const xpRange = xpNext - xpForLevel; const xpPct = xpRange > 0 ? Math.round(xpInLevel / xpRange * 100) : 100; - let html = `
-
+ let html = `
+
XP-прогресс Ур. ${STATE.level}
diff --git a/frontend/textbooks/algebra_8_ch2.html b/frontend/textbooks/algebra_8_ch2.html index e2b9127..df2226c 100644 --- a/frontend/textbooks/algebra_8_ch2.html +++ b/frontend/textbooks/algebra_8_ch2.html @@ -373,7 +373,7 @@ input,select,textarea{font-family:inherit}
0%
-
+
@@ -764,8 +764,8 @@ function buildSidebar(id){ const xpInLv = STATE.xp - xpForLv; const xpRange = xpNext - xpForLv; const xpPct = xpRange > 0 ? Math.round(xpInLv / xpRange * 100) : 100; - html += `
-
+ html += `
+
XP-прогресс Ур. ${STATE.level}
diff --git a/frontend/textbooks/algebra_8_ch3.html b/frontend/textbooks/algebra_8_ch3.html index 5b42489..aae9a2a 100644 --- a/frontend/textbooks/algebra_8_ch3.html +++ b/frontend/textbooks/algebra_8_ch3.html @@ -299,7 +299,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -679,8 +679,8 @@ function buildSidebar(id){ const xpInLv = STATE.xp - xpForLv; const xpRange = xpNext - xpForLv; const xpPct = xpRange > 0 ? Math.round(xpInLv / xpRange * 100) : 100; - html += `
-
+ html += `
+
XP-прогресс Ур. ${STATE.level}
diff --git a/frontend/textbooks/algebra_8_hub.html b/frontend/textbooks/algebra_8_hub.html index 2f134ae..3a61b8c 100644 --- a/frontend/textbooks/algebra_8_hub.html +++ b/frontend/textbooks/algebra_8_hub.html @@ -147,7 +147,7 @@ html.dark .ch-tag.indigo{color:#818cf8}
Загрузка...
- +
diff --git a/frontend/textbooks/algebra_9_ch1.html b/frontend/textbooks/algebra_9_ch1.html index e9e4048..7a7111a 100644 --- a/frontend/textbooks/algebra_9_ch1.html +++ b/frontend/textbooks/algebra_9_ch1.html @@ -229,7 +229,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -411,7 +411,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/algebra_9_ch2.html b/frontend/textbooks/algebra_9_ch2.html index fbb048a..7e1fe8e 100644 --- a/frontend/textbooks/algebra_9_ch2.html +++ b/frontend/textbooks/algebra_9_ch2.html @@ -228,7 +228,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -406,7 +406,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/algebra_9_ch3.html b/frontend/textbooks/algebra_9_ch3.html index 31d6b60..56ebc1e 100644 --- a/frontend/textbooks/algebra_9_ch3.html +++ b/frontend/textbooks/algebra_9_ch3.html @@ -229,7 +229,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -407,7 +407,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/algebra_9_ch4.html b/frontend/textbooks/algebra_9_ch4.html index b231f46..194290b 100644 --- a/frontend/textbooks/algebra_9_ch4.html +++ b/frontend/textbooks/algebra_9_ch4.html @@ -224,7 +224,7 @@ a{color:inherit;text-decoration:none}
0% -
+
@@ -410,7 +410,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/algebra_9_hub.html b/frontend/textbooks/algebra_9_hub.html index a27dcec..e8dff14 100644 --- a/frontend/textbooks/algebra_9_hub.html +++ b/frontend/textbooks/algebra_9_hub.html @@ -229,7 +229,7 @@ html.dark .final-cta-sub{color:#fcd34d}
Загрузка...
- +
diff --git a/frontend/textbooks/geometry_10_hub.html b/frontend/textbooks/geometry_10_hub.html index 8ee0928..93ab9b0 100644 --- a/frontend/textbooks/geometry_10_hub.html +++ b/frontend/textbooks/geometry_10_hub.html @@ -233,7 +233,7 @@ html.dark .final-cta-sub{color:#fcd34d}
Загрузка...
- +
diff --git a/frontend/textbooks/geometry_10_r1.html b/frontend/textbooks/geometry_10_r1.html index 0b4df36..a4fef95 100644 --- a/frontend/textbooks/geometry_10_r1.html +++ b/frontend/textbooks/geometry_10_r1.html @@ -212,7 +212,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -370,7 +370,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/geometry_10_r2.html b/frontend/textbooks/geometry_10_r2.html index 0e348e2..1c1824e 100644 --- a/frontend/textbooks/geometry_10_r2.html +++ b/frontend/textbooks/geometry_10_r2.html @@ -206,7 +206,7 @@ a{color:inherit;text-decoration:none}
0% -
+
@@ -365,7 +365,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/geometry_10_r3.html b/frontend/textbooks/geometry_10_r3.html index d2ed8d4..0bf42aa 100644 --- a/frontend/textbooks/geometry_10_r3.html +++ b/frontend/textbooks/geometry_10_r3.html @@ -206,7 +206,7 @@ a{color:inherit;text-decoration:none}
0% -
+
@@ -369,7 +369,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/geometry_10_r4.html b/frontend/textbooks/geometry_10_r4.html index 59359c8..e0f08b2 100644 --- a/frontend/textbooks/geometry_10_r4.html +++ b/frontend/textbooks/geometry_10_r4.html @@ -209,7 +209,7 @@ a{color:inherit;text-decoration:none}
0% -
+
@@ -374,7 +374,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/geometry_11_ch1.html b/frontend/textbooks/geometry_11_ch1.html index 4ad49eb..4b61063 100644 --- a/frontend/textbooks/geometry_11_ch1.html +++ b/frontend/textbooks/geometry_11_ch1.html @@ -256,7 +256,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0% -
+
@@ -426,7 +426,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/geometry_11_ch2.html b/frontend/textbooks/geometry_11_ch2.html index 20a6427..170321f 100644 --- a/frontend/textbooks/geometry_11_ch2.html +++ b/frontend/textbooks/geometry_11_ch2.html @@ -256,7 +256,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0% -
+
@@ -446,7 +446,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/geometry_11_ch3.html b/frontend/textbooks/geometry_11_ch3.html index 0292315..f82714a 100644 --- a/frontend/textbooks/geometry_11_ch3.html +++ b/frontend/textbooks/geometry_11_ch3.html @@ -257,7 +257,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0% -
+
@@ -464,7 +464,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/geometry_11_ch4.html b/frontend/textbooks/geometry_11_ch4.html index 03045b1..b8bfc61 100644 --- a/frontend/textbooks/geometry_11_ch4.html +++ b/frontend/textbooks/geometry_11_ch4.html @@ -258,7 +258,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0% -
+
@@ -438,7 +438,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/geometry_11_hub.html b/frontend/textbooks/geometry_11_hub.html index ac1e694..87eca50 100644 --- a/frontend/textbooks/geometry_11_hub.html +++ b/frontend/textbooks/geometry_11_hub.html @@ -236,7 +236,7 @@ html.dark .final-cta-sub{color:#fcd34d}
Загрузка...
- +
diff --git a/frontend/textbooks/geometry_7_ch1.html b/frontend/textbooks/geometry_7_ch1.html index e024c03..2f30174 100644 --- a/frontend/textbooks/geometry_7_ch1.html +++ b/frontend/textbooks/geometry_7_ch1.html @@ -212,7 +212,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -437,7 +437,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/geometry_7_ch2.html b/frontend/textbooks/geometry_7_ch2.html index ebccbc4..7bb5d60 100644 --- a/frontend/textbooks/geometry_7_ch2.html +++ b/frontend/textbooks/geometry_7_ch2.html @@ -207,7 +207,7 @@ a{color:inherit;text-decoration:none}
0% -
+
@@ -432,7 +432,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/geometry_7_ch3.html b/frontend/textbooks/geometry_7_ch3.html index bd796e6..fcaad80 100644 --- a/frontend/textbooks/geometry_7_ch3.html +++ b/frontend/textbooks/geometry_7_ch3.html @@ -208,7 +208,7 @@ a{color:inherit;text-decoration:none}
0% -
+
@@ -407,7 +407,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/geometry_7_ch4.html b/frontend/textbooks/geometry_7_ch4.html index 43f377e..c4541a5 100644 --- a/frontend/textbooks/geometry_7_ch4.html +++ b/frontend/textbooks/geometry_7_ch4.html @@ -196,7 +196,7 @@ a{color:inherit;text-decoration:none}
0% -
+
@@ -425,7 +425,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/geometry_7_ch5.html b/frontend/textbooks/geometry_7_ch5.html index 1478ee9..53e5688 100644 --- a/frontend/textbooks/geometry_7_ch5.html +++ b/frontend/textbooks/geometry_7_ch5.html @@ -198,7 +198,7 @@ a{color:inherit;text-decoration:none}
0% -
+
@@ -405,7 +405,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/geometry_7_hub.html b/frontend/textbooks/geometry_7_hub.html index ccf8e00..701debb 100644 --- a/frontend/textbooks/geometry_7_hub.html +++ b/frontend/textbooks/geometry_7_hub.html @@ -159,7 +159,7 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
Загрузка...
- +
diff --git a/frontend/textbooks/geometry_8_ch1.html b/frontend/textbooks/geometry_8_ch1.html index 69cc99f..2bd79b7 100644 --- a/frontend/textbooks/geometry_8_ch1.html +++ b/frontend/textbooks/geometry_8_ch1.html @@ -286,7 +286,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -558,7 +558,7 @@ function buildSidebar(id){ const xpForLv = _xpForLevel(STATE.level), xpNext = _xpForLevel(STATE.level + 1); const xpInLv = STATE.xp - xpForLv, xpRange = xpNext - xpForLv; const xpPct = xpRange > 0 ? Math.round(xpInLv / xpRange * 100) : 100; - html += `
XP-прогрессУр. ${STATE.level}
${STATE.xp} XP${xpNext} XP
`; + html += `
XP-прогрессУр. ${STATE.level}
${STATE.xp} XP${xpNext} XP
`; html += `

${sb.title}

`; sb.rows.forEach(([k,v])=>{ html += `
${k}${v ? ' — ' + v : ''}
`; }); html += '
'; diff --git a/frontend/textbooks/geometry_8_ch2.html b/frontend/textbooks/geometry_8_ch2.html index 04f0501..27ca2fa 100644 --- a/frontend/textbooks/geometry_8_ch2.html +++ b/frontend/textbooks/geometry_8_ch2.html @@ -262,7 +262,7 @@ a{color:inherit;text-decoration:none}
0% -
+
@@ -432,7 +432,7 @@ function buildSidebar(id){ const box=document.getElementById('sidebar-content'); const sb=SIDEBARS[id]||SIDEBARS.p1; let html=''; const xpForLv=_xpForLevel(STATE.level),xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv,xpRange=xpNext-xpForLv,xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+=`
XP-прогрессУр. ${STATE.level}
${STATE.xp} XP${xpNext} XP
`; + html+=`
XP-прогрессУр. ${STATE.level}
${STATE.xp} XP${xpNext} XP
`; html+=`

${sb.title}

`; sb.rows.forEach(([k,v])=>{ html+=`
${k}${v?' — '+v:''}
`; }); html+='
'; const tip=TIPS.find(t=>t.sec===id)||TIPS[0]; html+=`

Подсказка

${tip.html}
`; diff --git a/frontend/textbooks/geometry_8_ch3.html b/frontend/textbooks/geometry_8_ch3.html index 9d9f8d8..71623a9 100644 --- a/frontend/textbooks/geometry_8_ch3.html +++ b/frontend/textbooks/geometry_8_ch3.html @@ -254,7 +254,7 @@ a{color:inherit;text-decoration:none}
0% -
+
@@ -368,7 +368,7 @@ const TIPS=[ {sec:'final3',html:'Три признака подобия — главный инструмент геометрических доказательств.'}, ]; -function buildSidebar(id){const box=document.getElementById('sidebar-content');const sb=SIDEBARS[id]||SIDEBARS.p1;let html='';const xpForLv=_xpForLevel(STATE.level),xpNext=_xpForLevel(STATE.level+1);const xpInLv=STATE.xp-xpForLv,xpRange=xpNext-xpForLv,xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;html+=`
XP-прогрессУр. ${STATE.level}
${STATE.xp} XP${xpNext} XP
`;html+=`

${sb.title}

`;sb.rows.forEach(([k,v])=>{html+=`
${k}${v?' — '+v:''}
`;});html+='
';const tip=TIPS.find(t=>t.sec===id)||TIPS[0];html+=`

Подсказка

${tip.html}
`;if(STATE.achievements.size>0){html+=`

Достижения ${STATE.achievements.size}

`;[...STATE.achievements.values()].slice(-4).forEach(text=>{html+=`
✓ ${text}
`;});html+='
';}box.innerHTML=html;if(window.renderMathInElement)try{renderMath(box);}catch(e){}} +function buildSidebar(id){const box=document.getElementById('sidebar-content');const sb=SIDEBARS[id]||SIDEBARS.p1;let html='';const xpForLv=_xpForLevel(STATE.level),xpNext=_xpForLevel(STATE.level+1);const xpInLv=STATE.xp-xpForLv,xpRange=xpNext-xpForLv,xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;html+=`
XP-прогрессУр. ${STATE.level}
${STATE.xp} XP${xpNext} XP
`;html+=`

${sb.title}

`;sb.rows.forEach(([k,v])=>{html+=`
${k}${v?' — '+v:''}
`;});html+='
';const tip=TIPS.find(t=>t.sec===id)||TIPS[0];html+=`

Подсказка

${tip.html}
`;if(STATE.achievements.size>0){html+=`

Достижения ${STATE.achievements.size}

`;[...STATE.achievements.values()].slice(-4).forEach(text=>{html+=`
✓ ${text}
`;});html+='
';}box.innerHTML=html;if(window.renderMathInElement)try{renderMath(box);}catch(e){}} function initTheme(){const t=localStorage.getItem('geometry8_ch3_theme')||'light';if(t==='dark')document.documentElement.classList.add('dark');document.getElementById('theme-lab').textContent=t==='dark'?'Светлая':'Тёмная';document.getElementById('theme-btn').addEventListener('click',()=>{document.documentElement.classList.toggle('dark');const dark=document.documentElement.classList.contains('dark');localStorage.setItem('geometry8_ch3_theme',dark?'dark':'light');document.getElementById('theme-lab').textContent=dark?'Светлая':'Тёмная';});} function renderMath(root){if(window.renderMathInElement){try{renderMathInElement(root,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false});}catch(e){}}} diff --git a/frontend/textbooks/geometry_8_ch4.html b/frontend/textbooks/geometry_8_ch4.html index 3e4e655..89cf1a1 100644 --- a/frontend/textbooks/geometry_8_ch4.html +++ b/frontend/textbooks/geometry_8_ch4.html @@ -260,7 +260,7 @@ a{color:inherit;text-decoration:none}
0% -
+
@@ -402,7 +402,7 @@ const TIPS=[ {sec:'final4',html:'Ключевая теорема: вписанный угол = ½ дуги. Из неё следуют большинство остальных.'}, ]; -function buildSidebar(id){const box=document.getElementById('sidebar-content');const sb=SIDEBARS[id]||SIDEBARS.p1;let html='';const xpForLv=_xpForLevel(STATE.level),xpNext=_xpForLevel(STATE.level+1);const xpInLv=STATE.xp-xpForLv,xpRange=xpNext-xpForLv,xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;html+=`
XP-прогрессУр. ${STATE.level}
${STATE.xp} XP${xpNext} XP
`;html+=`

${sb.title}

`;sb.rows.forEach(([k,v])=>{html+=`
${k}${v?' — '+v:''}
`;});html+='
';const tip=TIPS.find(t=>t.sec===id)||TIPS[0];html+=`

Подсказка

${tip.html}
`;if(STATE.achievements.size>0){html+=`

Достижения ${STATE.achievements.size}

`;[...STATE.achievements.values()].slice(-4).forEach(text=>{html+=`
✓ ${text}
`;});html+='
';}box.innerHTML=html;if(window.renderMathInElement)try{renderMath(box);}catch(e){}} +function buildSidebar(id){const box=document.getElementById('sidebar-content');const sb=SIDEBARS[id]||SIDEBARS.p1;let html='';const xpForLv=_xpForLevel(STATE.level),xpNext=_xpForLevel(STATE.level+1);const xpInLv=STATE.xp-xpForLv,xpRange=xpNext-xpForLv,xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;html+=`
XP-прогрессУр. ${STATE.level}
${STATE.xp} XP${xpNext} XP
`;html+=`

${sb.title}

`;sb.rows.forEach(([k,v])=>{html+=`
${k}${v?' — '+v:''}
`;});html+='
';const tip=TIPS.find(t=>t.sec===id)||TIPS[0];html+=`

Подсказка

${tip.html}
`;if(STATE.achievements.size>0){html+=`

Достижения ${STATE.achievements.size}

`;[...STATE.achievements.values()].slice(-4).forEach(text=>{html+=`
✓ ${text}
`;});html+='
';}box.innerHTML=html;if(window.renderMathInElement)try{renderMath(box);}catch(e){}} function initTheme(){const t=localStorage.getItem('geometry8_ch4_theme')||'light';if(t==='dark')document.documentElement.classList.add('dark');document.getElementById('theme-lab').textContent=t==='dark'?'Светлая':'Тёмная';document.getElementById('theme-btn').addEventListener('click',()=>{document.documentElement.classList.toggle('dark');const dark=document.documentElement.classList.contains('dark');localStorage.setItem('geometry8_ch4_theme',dark?'dark':'light');document.getElementById('theme-lab').textContent=dark?'Светлая':'Тёмная';});} function renderMath(root){if(window.renderMathInElement){try{renderMathInElement(root,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false});}catch(e){}}} diff --git a/frontend/textbooks/geometry_8_hub.html b/frontend/textbooks/geometry_8_hub.html index 1729fcd..07685e5 100644 --- a/frontend/textbooks/geometry_8_hub.html +++ b/frontend/textbooks/geometry_8_hub.html @@ -162,7 +162,7 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
Загрузка...
- +
diff --git a/frontend/textbooks/geometry_9_ch1.html b/frontend/textbooks/geometry_9_ch1.html index 9c4ffc9..5035659 100644 --- a/frontend/textbooks/geometry_9_ch1.html +++ b/frontend/textbooks/geometry_9_ch1.html @@ -248,7 +248,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0%
-
+
@@ -433,7 +433,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/geometry_9_ch2.html b/frontend/textbooks/geometry_9_ch2.html index 7788d5e..2bff135 100644 --- a/frontend/textbooks/geometry_9_ch2.html +++ b/frontend/textbooks/geometry_9_ch2.html @@ -229,7 +229,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0% -
+
@@ -402,7 +402,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/geometry_9_ch3.html b/frontend/textbooks/geometry_9_ch3.html index 9087060..129c613 100644 --- a/frontend/textbooks/geometry_9_ch3.html +++ b/frontend/textbooks/geometry_9_ch3.html @@ -229,7 +229,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0% -
+
@@ -402,7 +402,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/geometry_9_ch4.html b/frontend/textbooks/geometry_9_ch4.html index 616a4bc..93b6982 100644 --- a/frontend/textbooks/geometry_9_ch4.html +++ b/frontend/textbooks/geometry_9_ch4.html @@ -244,7 +244,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0% -
+
@@ -421,7 +421,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/geometry_9_hub.html b/frontend/textbooks/geometry_9_hub.html index a8a4c5b..cf53c08 100644 --- a/frontend/textbooks/geometry_9_hub.html +++ b/frontend/textbooks/geometry_9_hub.html @@ -261,7 +261,7 @@ html.dark .final-cta-sub{color:#fcd34d}
Загрузка...
- +
diff --git a/frontend/textbooks/physics_10_ch1.html b/frontend/textbooks/physics_10_ch1.html index 3f8e8ae..769642a 100644 --- a/frontend/textbooks/physics_10_ch1.html +++ b/frontend/textbooks/physics_10_ch1.html @@ -267,7 +267,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0%
-
+
@@ -476,7 +476,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/physics_10_ch2.html b/frontend/textbooks/physics_10_ch2.html index a387ef8..6f126d3 100644 --- a/frontend/textbooks/physics_10_ch2.html +++ b/frontend/textbooks/physics_10_ch2.html @@ -257,7 +257,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0% -
+
@@ -441,7 +441,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/physics_10_ch3.html b/frontend/textbooks/physics_10_ch3.html index 1b33912..b662426 100644 --- a/frontend/textbooks/physics_10_ch3.html +++ b/frontend/textbooks/physics_10_ch3.html @@ -261,7 +261,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0% -
+
@@ -465,7 +465,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/physics_10_ch4.html b/frontend/textbooks/physics_10_ch4.html index be929a3..4aac39e 100644 --- a/frontend/textbooks/physics_10_ch4.html +++ b/frontend/textbooks/physics_10_ch4.html @@ -254,7 +254,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0% -
+
@@ -423,7 +423,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/physics_10_ch5.html b/frontend/textbooks/physics_10_ch5.html index 9c507b7..cf3d34b 100644 --- a/frontend/textbooks/physics_10_ch5.html +++ b/frontend/textbooks/physics_10_ch5.html @@ -259,7 +259,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0% -
+
@@ -453,7 +453,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/physics_10_ch6.html b/frontend/textbooks/physics_10_ch6.html index 61f577f..7e1b147 100644 --- a/frontend/textbooks/physics_10_ch6.html +++ b/frontend/textbooks/physics_10_ch6.html @@ -256,7 +256,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0% -
+
@@ -435,7 +435,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/physics_10_hub.html b/frontend/textbooks/physics_10_hub.html index 3af7649..b33ef72 100644 --- a/frontend/textbooks/physics_10_hub.html +++ b/frontend/textbooks/physics_10_hub.html @@ -246,7 +246,7 @@ html.dark .final-cta-sub{color:#fcd34d}
Загрузка...
- +
diff --git a/frontend/textbooks/physics_11_ch1.html b/frontend/textbooks/physics_11_ch1.html index e403e07..8d02bc4 100644 --- a/frontend/textbooks/physics_11_ch1.html +++ b/frontend/textbooks/physics_11_ch1.html @@ -194,7 +194,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -367,7 +367,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/physics_11_ch2.html b/frontend/textbooks/physics_11_ch2.html index 54a8c63..43e0527 100644 --- a/frontend/textbooks/physics_11_ch2.html +++ b/frontend/textbooks/physics_11_ch2.html @@ -189,7 +189,7 @@ a{color:inherit;text-decoration:none}
0% -
+
@@ -371,7 +371,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/physics_11_ch3.html b/frontend/textbooks/physics_11_ch3.html index 66da196..54cd5a7 100644 --- a/frontend/textbooks/physics_11_ch3.html +++ b/frontend/textbooks/physics_11_ch3.html @@ -189,7 +189,7 @@ a{color:inherit;text-decoration:none}
0% -
+
@@ -388,7 +388,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/physics_11_ch4.html b/frontend/textbooks/physics_11_ch4.html index 2323523..a4ec5d2 100644 --- a/frontend/textbooks/physics_11_ch4.html +++ b/frontend/textbooks/physics_11_ch4.html @@ -185,7 +185,7 @@ a{color:inherit;text-decoration:none}
0% -
+
@@ -346,7 +346,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/physics_11_ch5.html b/frontend/textbooks/physics_11_ch5.html index 42625e6..16c709a 100644 --- a/frontend/textbooks/physics_11_ch5.html +++ b/frontend/textbooks/physics_11_ch5.html @@ -185,7 +185,7 @@ a{color:inherit;text-decoration:none}
0% -
+
@@ -346,7 +346,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/physics_11_ch6.html b/frontend/textbooks/physics_11_ch6.html index 22b40fe..63cc774 100644 --- a/frontend/textbooks/physics_11_ch6.html +++ b/frontend/textbooks/physics_11_ch6.html @@ -185,7 +185,7 @@ a{color:inherit;text-decoration:none}
0% -
+
@@ -356,7 +356,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/physics_11_ch7.html b/frontend/textbooks/physics_11_ch7.html index bcb6a18..8ab5585 100644 --- a/frontend/textbooks/physics_11_ch7.html +++ b/frontend/textbooks/physics_11_ch7.html @@ -185,7 +185,7 @@ a{color:inherit;text-decoration:none}
0% -
+
@@ -367,7 +367,7 @@ function buildSidebar(id){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

'+sb.title+'

'; sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; }); html+='
'; diff --git a/frontend/textbooks/physics_11_ch8.html b/frontend/textbooks/physics_11_ch8.html index 6fe7844..faf763a 100644 --- a/frontend/textbooks/physics_11_ch8.html +++ b/frontend/textbooks/physics_11_ch8.html @@ -155,7 +155,7 @@ a{color:inherit;text-decoration:none}
0% -
+
@@ -240,7 +240,7 @@ function buildSidebar(){ const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1); const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv; const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100; - html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; + html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
'; html+='

Курс физики 11

' + '
Глав — 8
' + '
Параграфов — 45
' diff --git a/js/api.js b/js/api.js index ad6711a..827be51 100644 --- a/js/api.js +++ b/js/api.js @@ -806,14 +806,24 @@ function initPage({ requireLogin = true } = {}) { /* ── Feature flags (cached per page load, bust on demand) ───────────── */ let _featuresCache = null; +/* Synchronous mirror of features.gamification, populated on first + loadFeatures() resolve. Used by xp.js to bail out without network. */ +let _gamificationEnabled = null; +function isGamificationEnabled() { + // Default to true when the cache hasn't resolved yet — page-load order + // means some code runs before /api/features. The CSS kill-switch + // (body.no-gamification) catches the visual side regardless. + return _gamificationEnabled !== false; +} async function loadFeatures() { if (_featuresCache) return _featuresCache; try { _featuresCache = await apiFetch('/api/features'); } catch { _featuresCache = {}; } + _gamificationEnabled = _featuresCache.gamification !== false; return _featuresCache; } -function clearFeaturesCache() { _featuresCache = null; } +function clearFeaturesCache() { _featuresCache = null; _gamificationEnabled = null; } /** * Show board sidebar link only for teachers/admins and students in a class. @@ -1045,6 +1055,7 @@ window.LS = { patch: (path, body) => apiFetch(path, { method: 'PATCH', body: JSON.stringify(body) }), applyCosmetics: applyCosmetics, refreshNavAvatar, + isGamificationEnabled, loadFeatures, clearFeaturesCache, hideDisabledFeatures, @@ -1060,6 +1071,9 @@ window.LS = { ═══════════════════════════════════════════════════════════════════════ */ async function applyCosmetics() { if (!isLoggedIn()) return; + // Skip the round-trip entirely when gamification is off. Server will + // 404 anyway, but a synchronous bail-out keeps the network quiet. + if (!isGamificationEnabled()) return; try { const c = await getMyActiveCosmetics(); if (!c) return; diff --git a/js/xp.js b/js/xp.js index 1421ce5..13edf41 100644 --- a/js/xp.js +++ b/js/xp.js @@ -73,8 +73,19 @@ try { localStorage.setItem(LS_KEY, String(xp)); } catch (e) {} } + /* ── Master kill-switch ── + If the gamification feature is globally disabled, every public + method becomes a no-op so textbooks don't display XP bars and + don't burn CPU/network on now-pointless syncs. The body class + `.no-gamification` (set by api.js) hides the DOM in parallel. */ + function _gamOff() { + return window.LS && typeof window.LS.isGamificationEnabled === 'function' + && window.LS.isGamificationEnabled() === false; + } + /* ── Загрузка с сервера + merge ── */ async function load() { + if (_gamOff()) return null; try { const data = await _apiFetch(ENDPOINT_ME, { method: 'GET' }); const serverXp = data.xp || 0; @@ -87,6 +98,7 @@ return _state; } catch (e) { if (e && e.status === 401) return null; // не авторизован — тихо пропускаем + if (e && e.status === 404) return null; // геймификация выключена — тихо return null; // сетевая ошибка — не ломаем учебник } } @@ -124,6 +136,7 @@ function flush() { if (_debounceTimer) { clearTimeout(_debounceTimer); _debounceTimer = null; } if (_pending <= 0) return; + if (_gamOff()) { _pending = 0; return; } // drop pending — feature off const amount = _pending; _pending = 0; // source для flush — generic, реальный source записывается в каждом add() @@ -152,6 +165,7 @@ /* ── add: sync обновление localStorage + постановка в очередь ── */ function add(amount, source) { if (!amount || amount <= 0) return; + if (_gamOff()) return; // no-op when gamification globally off // Немедленно обновляем локальный XP const newXp = _localXp() + amount; _saveLocal(newXp);