feat(catalog): авто-mark-as-read + Физика 8 как полноценный хаб

A. textbook-tracker.js: первый клик по .para-pill теперь автоматически
   помечает параграф как прочитанный. «Прочитано» = «открыто». Сразу
   даёт осмысленный счётчик для chemistry-9 и physics-9 в каталоге.
   Slug fallback: physics8_* → physics-8-* (корректный слаг).

B. Физика 8 — миграция 015:
   - 3 children: physics-8-thermal / electro / optics с parent_slug
   - parent физики-8 обновлён: para_count=40, описание трёх разделов
   - sub-файлы получили textbook-tracker.js + правильный слаг
   - physics_8.html переписана в стиле algebra_8_hub: 3 цветные
     карточки, агрегированный прогресс, ачивка «Эксперт физики 8»

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-27 17:00:36 +03:00
parent c806a5137a
commit 1a347650f4
6 changed files with 207 additions and 168 deletions
@@ -0,0 +1,29 @@
-- Physics 8 hub migration.
-- Converts physics-8 from a monolithic entry into a hub with 3 children,
-- mirroring the algebra-8 hub pattern established in migration 014.
-- 1. Insert 3 child rows for the sub-textbooks.
INSERT OR IGNORE INTO textbooks
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug)
VALUES
('physics-8-thermal', 'physics', 8, 'Физика 8 · Тепловые явления',
'',
'Внутренняя энергия, теплопередача, удельная теплоёмкость, фазовые переходы, тепловые двигатели.',
'physics8_thermal.html', 11, 'orange', 1, 1, 'physics-8'),
('physics-8-electro', 'physics', 8, 'Физика 8 · Электрические явления',
'',
'Электростатика, закон Кулона, электрический ток, закон Ома, электрические цепи.',
'physics8_electro.html', 20, 'blue', 2, 1, 'physics-8'),
('physics-8-optics', 'physics', 8, 'Физика 8 · Световые явления',
'',
'Источники света, отражение, преломление, линзы, оптические приборы.',
'physics8_optics.html', 9, 'purple', 3, 1, 'physics-8');
-- 2. Update the parent physics-8 row: clear author, set para_count=40, update description.
UPDATE textbooks
SET
author = '',
para_count = 40,
html_path = 'physics_8.html',
description = 'Полный курс физики 8 класса в трёх разделах: тепловые явления (§1–§11), электрические явления (§12–§31), световые явления (§32–§40).'
WHERE slug = 'physics-8';
+8 -1
View File
@@ -12,7 +12,9 @@
if (m) return m[1];
// Fallback for direct file access during dev
const fname = location.pathname.split('/').pop().replace(/\.html$/, '');
return fname.replace(/_/g, '-');
// Normalise physics8_* → physics-8-* (e.g. physics8_thermal → physics-8-thermal)
const norm = fname.replace(/^physics8_/, 'physics-8-');
return norm.replace(/_/g, '-');
})();
const lsKey = 'textbook_progress_' + slug;
@@ -196,6 +198,11 @@
if (!pill) return;
const key = pill.dataset.para;
setLastPara(key);
// Auto-mark-as-read: первый клик по пилюле = открыл параграф = считается прочитанным
// (мягкая семантика — соответствует реальному поведению учеников)
if (!localState.read.includes(key)) {
markRead(key);
}
});
}
+1
View File
@@ -10,6 +10,7 @@
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
<script src="/js/xp.js" defer></script>
<script src="/js/textbook-tracker.js" defer></script>
<script src="/js/textbook-xp-widget.js" defer></script>
<style>
/* ═══════════ ДИЗАЙН-ТОКЕНЫ v2 ═══════════ */
+1
View File
@@ -10,6 +10,7 @@
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
<script src="/js/xp.js" defer></script>
<script src="/js/textbook-tracker.js" defer></script>
<script src="/js/textbook-xp-widget.js" defer></script>
<style>
/* ═══════════ ДИЗАЙН-ТОКЕНЫ v2 ═══════════ */
+1
View File
@@ -10,6 +10,7 @@
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
<script src="/js/xp.js" defer></script>
<script src="/js/textbook-tracker.js" defer></script>
<script src="/js/textbook-xp-widget.js" defer></script>
<style>
/* ═══════════ ДИЗАЙН-ТОКЕНЫ v2 ═══════════ */
+167 -167
View File
@@ -3,33 +3,37 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Физика 8 — учебник</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800;900&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<title>Физика 8 класс — учебник</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800;900&family=Inter:wght@400;500;600;700&family=Unbounded:wght@400;700;800;900&display=swap" rel="stylesheet">
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<script src="/js/textbook-xp-widget.js" defer></script>
<style>
:root{
--bg:#eff6ff; --card:#fff;
--text:#1e293b; --muted:#64748b;
--border:#dbeafe;
--pri:#2563eb; --pri-d:#1d4ed8;
--amber:#d97706; --amber-d:#b45309; --amber-bg:#fef3c7;
--blue:#2563eb; --blue-d:#1d4ed8; --blue-bg:#dbeafe;
--violet:#7c3aed; --violet-d:#6d28d9; --violet-bg:#ede9fe;
--sh:0 4px 16px rgba(37,99,235,.10);
--sh-h:0 12px 36px rgba(37,99,235,.18);
--text:#1a1a2e; --muted:#5b6b7d;
--border:#bfdbfe;
--pri:#1d4ed8; --pri-d:#1e40af;
--pri-soft:#dbeafe;
--acc:#06b6d4; --acc-d:#0891b2;
--ch1:#f97316; --ch1-d:#ea580c;
--ch2:#2563eb; --ch2-d:#1d4ed8;
--ch3:#9333ea; --ch3-d:#7c3aed;
--sh:0 4px 16px rgba(29,78,216,.09);
--sh-h:0 12px 36px rgba(29,78,216,.18);
}
html.dark{
--bg:#0b1220; --card:#152033;
--text:#e2e8f0; --muted:#94a3b8;
--border:#1e293b;
--amber-bg:rgba(217,119,6,.16);
--blue-bg:rgba(37,99,235,.18);
--violet-bg:rgba(124,58,237,.18);
--bg:#0b1220; --card:#111c2d;
--text:#e2e8f0; --muted:#8094aa;
--border:#1e3a5a;
--pri-soft:rgba(29,78,216,.16);
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{min-height:100vh}
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;transition:background .25s,color .25s}
.hdr{position:relative;background:linear-gradient(120deg,#1e3a8a 0%,#2563eb 55%,#3b82f6 100%);color:#fff;padding:32px 24px 28px;overflow:hidden;border-bottom:2px solid rgba(255,255,255,.08)}
/* HEADER */
.hdr{position:relative;background:linear-gradient(110deg,#1e3a8a 0%,#1d4ed8 55%,#06b6d4 100%);color:#fff;padding:32px 24px 28px;overflow:hidden;border-bottom:2px solid rgba(255,255,255,.08)}
.hdr::before{content:'ФИЗИКА';position:absolute;right:-14px;top:-18%;font-family:'Outfit',sans-serif;font-size:clamp(5rem,16vw,13rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(255,255,255,.10);line-height:1;pointer-events:none;user-select:none}
.hdr-inner{position:relative;z-index:1;max-width:1100px;margin:0 auto;display:flex;align-items:center;gap:18px;flex-wrap:wrap}
.hdr-back{display:inline-flex;align-items:center;gap:8px;padding:8px 14px;background:rgba(255,255,255,.14);border-radius:9px;color:#fff;text-decoration:none;font-size:.85rem;font-weight:600;transition:background .15s}
@@ -37,29 +41,21 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--t
.hdr h1{font-family:'Outfit',sans-serif;font-size:1.85rem;font-weight:900;letter-spacing:-.01em}
.hdr-sub{font-size:.92rem;opacity:.85;margin-top:4px}
.hdr-side{margin-left:auto;display:flex;gap:8px}
.hdr-btn{padding:8px 12px;background:rgba(255,255,255,.14);border:none;color:#fff;border-radius:9px;cursor:pointer;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s}
.hdr-btn{padding:8px 12px;background:rgba(255,255,255,.14);border:none;color:#fff;border-radius:9px;cursor:pointer;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;font-family:inherit}
.hdr-btn:hover{background:rgba(255,255,255,.24)}
.ic{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
/* INTRO */
.intro{background:var(--card);border:1px solid var(--border);border-radius:18px;padding:24px 26px;margin-bottom:30px;box-shadow:var(--sh);position:relative;overflow:hidden}
.intro::before{content:'⚡';position:absolute;right:-10px;top:-30px;font-size:11rem;opacity:.05;line-height:1;pointer-events:none}
.intro h2{font-family:'Outfit',sans-serif;font-size:1.45rem;font-weight:800;color:var(--pri-d);margin-bottom:10px;letter-spacing:-.01em}
.intro p{font-size:1.02rem;color:var(--text);opacity:.9;max-width:720px;margin-bottom:8px}
.intro .meta{margin-top:14px;display:flex;gap:18px;flex-wrap:wrap;font-size:.86rem;color:var(--muted)}
.intro .meta b{color:var(--pri-d)}
/* OVERALL PROGRESS */
.prog-overall{background:linear-gradient(135deg,var(--blue-bg),var(--violet-bg));border:1px solid var(--border);border-radius:14px;padding:14px 18px;margin-bottom:28px;display:flex;gap:14px;align-items:center;flex-wrap:wrap}
.po-icon{width:46px;height:46px;border-radius:12px;background:linear-gradient(135deg,#3b82f6,#7c3aed);color:#fff;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-family:'Outfit',sans-serif;font-size:1.4rem;font-weight:900}
.prog-overall{background:linear-gradient(135deg,var(--pri-soft),rgba(6,182,212,.12));border:1px solid var(--border);border-radius:14px;padding:14px 18px;margin-bottom:28px;display:flex;gap:14px;align-items:center;flex-wrap:wrap}
.po-icon{width:46px;height:46px;border-radius:12px;background:linear-gradient(135deg,#1d4ed8,#06b6d4);color:#fff;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-family:'Outfit',sans-serif;font-size:1.4rem;font-weight:900}
.po-text{flex:1;min-width:160px}
.po-label{font-size:.78rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px}
.po-bar{height:8px;background:rgba(37,99,235,.18);border-radius:5px;overflow:hidden;margin-top:6px}
.po-fill{height:100%;background:linear-gradient(90deg,var(--blue),var(--violet));border-radius:5px;transition:width .5s}
.po-bar{height:8px;background:rgba(29,78,216,.12);border-radius:5px;overflow:hidden;margin-top:6px}
.po-fill{height:100%;background:linear-gradient(90deg,#1d4ed8,#06b6d4);border-radius:5px;transition:width .5s}
/* CHAPTERS */
/* CHAPTER GRID */
.ch-grid{display:grid;grid-template-columns:1fr;gap:18px;margin-bottom:30px}
@media(min-width:760px){.ch-grid{grid-template-columns:1fr 1fr 1fr}}
@@ -67,42 +63,42 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
.ch-card:hover{transform:translateY(-4px);box-shadow:var(--sh-h)}
.ch-cover{padding:22px 22px 18px;color:#fff;position:relative;overflow:hidden}
.ch-cover-wm{position:absolute;right:-10px;top:-30px;font-size:8rem;font-weight:900;font-family:'Outfit',sans-serif;line-height:1;letter-spacing:-.04em;color:rgba(255,255,255,.13);pointer-events:none}
.ch-num{display:inline-block;padding:4px 10px;background:rgba(255,255,255,.20);border-radius:99px;font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;position:relative;z-index:1}
.ch-title{font-family:'Outfit',sans-serif;font-size:1.25rem;font-weight:800;letter-spacing:-.01em;position:relative;z-index:1}
.ch-num{display:inline-block;padding:4px 10px;background:rgba(255,255,255,.22);border-radius:99px;font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;position:relative;z-index:1}
.ch-title{font-family:'Outfit',sans-serif;font-size:1.2rem;font-weight:800;letter-spacing:-.01em;position:relative;z-index:1;line-height:1.3}
.ch-range{font-size:.86rem;opacity:.85;margin-top:4px;position:relative;z-index:1;font-weight:500}
.ch-cover.thermal{background:linear-gradient(135deg,#92400e,#d97706 60%,#f59e0b)}
.ch-cover.electro{background:linear-gradient(135deg,#1e40af,#2563eb 60%,#3b82f6)}
.ch-cover.optics{background:linear-gradient(135deg,#5b21b6,#7c3aed 60%,#a855f7)}
.ch-cover.ch1{background:linear-gradient(135deg,#c2410c,#f97316 60%,#fb923c)}
.ch-cover.ch2{background:linear-gradient(135deg,#1e3a8a,#2563eb 60%,#3b82f6)}
.ch-cover.ch3{background:linear-gradient(135deg,#6b21a8,#9333ea 60%,#a855f7)}
.ch-body{padding:18px 22px 20px;display:flex;flex-direction:column;flex:1}
.ch-desc{font-size:.92rem;color:var(--text);opacity:.85;flex:1;margin-bottom:14px;line-height:1.55}
.ch-topics{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:14px}
.ch-topic{padding:3px 9px;background:var(--blue-bg);color:var(--blue-d);border-radius:6px;font-size:.72rem;font-weight:600}
.ch-card .ch-topic.amber{background:var(--amber-bg);color:var(--amber-d)}
.ch-card .ch-topic.violet{background:var(--violet-bg);color:var(--violet-d)}
.ch-desc{font-size:.9rem;color:var(--text);opacity:.82;flex:1;margin-bottom:14px;line-height:1.55}
.ch-prog{margin-bottom:14px}
.ch-prog-label{display:flex;justify-content:space-between;font-size:.74rem;color:var(--muted);font-weight:600;margin-bottom:4px}
.ch-prog-bar{height:6px;background:rgba(37,99,235,.12);border-radius:4px;overflow:hidden}
.ch-prog-bar{height:6px;background:rgba(29,78,216,.10);border-radius:4px;overflow:hidden}
.ch-prog-fill{height:100%;border-radius:4px;transition:width .5s}
.ch-card.thermal .ch-prog-fill{background:linear-gradient(90deg,#d97706,#f59e0b)}
.ch-card.electro .ch-prog-fill{background:linear-gradient(90deg,#2563eb,#3b82f6)}
.ch-card.optics .ch-prog-fill{background:linear-gradient(90deg,#7c3aed,#a855f7)}
.ch-card.ch1-card .ch-prog-fill{background:linear-gradient(90deg,var(--ch1),var(--ch1-d))}
.ch-card.ch2-card .ch-prog-fill{background:linear-gradient(90deg,var(--ch2),var(--ch2-d))}
.ch-card.ch3-card .ch-prog-fill{background:linear-gradient(90deg,var(--ch3),var(--ch3-d))}
.ch-action{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-radius:11px;font-weight:700;font-size:.92rem;color:#fff;transition:filter .15s}
.ch-action:hover{filter:brightness(1.08)}
.ch-card.thermal .ch-action{background:linear-gradient(135deg,#d97706,#f59e0b)}
.ch-card.electro .ch-action{background:linear-gradient(135deg,#2563eb,#3b82f6)}
.ch-card.optics .ch-action{background:linear-gradient(135deg,#7c3aed,#a855f7)}
.ch-card.ch1-card .ch-action{background:linear-gradient(135deg,var(--ch1),#fb923c)}
.ch-card.ch2-card .ch-action{background:linear-gradient(135deg,var(--ch2),#3b82f6)}
.ch-card.ch3-card .ch-action{background:linear-gradient(135deg,var(--ch3),#a855f7)}
/* INFO BLOCKS */
.info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-bottom:30px}
.info-card{background:var(--card);border:1px solid var(--border);border-radius:13px;padding:16px 18px;display:flex;gap:12px;align-items:flex-start}
.info-card-ic{width:38px;height:38px;border-radius:10px;background:var(--blue-bg);color:var(--blue-d);display:flex;align-items:center;justify-content:center;flex-shrink:0}
.info-card-ic .ic{width:20px;height:20px;stroke-width:2.2}
.info-card-title{font-weight:700;color:var(--pri-d);margin-bottom:3px}
.info-card-text{font-size:.85rem;color:var(--muted);line-height:1.5}
/* ACHIEVEMENT STRIP */
.ach-strip{background:var(--card);border:1.5px solid var(--border);border-radius:16px;padding:18px 22px;margin-bottom:28px;display:flex;align-items:center;gap:16px;transition:border-color .4s,box-shadow .4s}
.ach-strip.lit{border-color:#f59e0b;box-shadow:0 0 0 3px rgba(245,158,11,.18)}
.ach-icon{width:52px;height:52px;border-radius:14px;background:rgba(0,0,0,.06);display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .4s}
.ach-strip.lit .ach-icon{background:linear-gradient(135deg,#fbbf24,#f59e0b)}
.ach-icon svg{width:28px;height:28px;stroke:var(--muted)}
.ach-strip.lit .ach-icon svg{stroke:#fff}
.ach-text{flex:1}
.ach-title{font-weight:800;font-size:1.02rem;color:var(--text)}
.ach-sub{font-size:.85rem;color:var(--muted);margin-top:2px}
.ach-strip.lit .ach-title{color:#92400e}
.foot{text-align:center;padding:24px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border)}
</style>
@@ -118,8 +114,8 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
</a>
</div>
<div>
<h1>Физика 8 класс</h1>
<div class="hdr-sub">Интерактивный учебник · 40 параграфов · 3 раздела</div>
<h1>Физика 8 класс</h1>
<div class="hdr-sub">Три раздела: тепловые, электрические и световые явления</div>
</div>
<div class="hdr-side">
<button id="theme-btn" class="hdr-btn" title="Сменить тему">
@@ -133,89 +129,73 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
<main>
<section class="prog-overall">
<div class="po-icon">Σ</div>
<div class="po-icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
</div>
<div class="po-text">
<div class="po-label">Общий прогресс по курсу</div>
<div id="overall-text" style="font-size:1.05rem;font-weight:700">— параграфов</div>
<div id="overall-text" style="font-size:1.05rem;font-weight:700">Загрузка...</div>
<div class="po-bar"><div id="overall-fill" class="po-fill" style="width:0%"></div></div>
</div>
</section>
<div class="ch-grid">
<a href="/textbooks/physics8_thermal.html" class="ch-card thermal" id="ch-thermal">
<div class="ch-cover thermal">
<div class="ch-cover-wm">I</div>
<div class="ch-num">Раздел I · §1–§11</div>
<a href="/textbook/physics-8-thermal" class="ch-card ch1-card" id="ch-1">
<div class="ch-cover ch1">
<div class="ch-cover-wm">T</div>
<div class="ch-num">Раздел 1</div>
<div class="ch-title">Тепловые явления</div>
<div class="ch-range">11 параграфов</div>
<div class="ch-range">§1–§11</div>
</div>
<div class="ch-body">
<div class="ch-desc">Внутренняя энергия, теплопередача, удельная теплоёмкость, фазовые переходы, плавление и парообразование, тепловые двигатели.</div>
<div class="ch-topics">
<span class="ch-topic amber">Температура</span>
<span class="ch-topic amber">Теплоёмкость</span>
<span class="ch-topic amber">Кипение</span>
<span class="ch-topic amber">КПД</span>
</div>
<div class="ch-desc">Внутренняя энергия и её изменение, виды теплопередачи, удельная теплоёмкость, фазовые переходы, тепловые двигатели и их КПД.</div>
<div class="ch-prog">
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-thermal">0%</span></div>
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-thermal" style="width:0%"></div></div>
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-1">0%</span></div>
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-1" style="width:0%"></div></div>
</div>
<div class="ch-action">
<span id="btn-thermal">Открыть раздел</span>
<span id="btn-1">Открыть раздел</span>
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</a>
<a href="/textbooks/physics8_electro.html" class="ch-card electro" id="ch-electro">
<div class="ch-cover electro">
<div class="ch-cover-wm">II</div>
<div class="ch-num">Раздел II · §12–§31</div>
<a href="/textbook/physics-8-electro" class="ch-card ch2-card" id="ch-2">
<div class="ch-cover ch2">
<div class="ch-cover-wm">&#934;</div>
<div class="ch-num">Раздел 2</div>
<div class="ch-title">Электрические явления</div>
<div class="ch-range">20 параграфов</div>
<div class="ch-range">§12–§31</div>
</div>
<div class="ch-body">
<div class="ch-desc">Электризация, закон Кулона, электрический ток, закон Ома, работа и мощность тока, электромагнитные явления, генератор и трансформатор.</div>
<div class="ch-topics">
<span class="ch-topic">Электричество</span>
<span class="ch-topic">Сопротивление</span>
<span class="ch-topic">Закон Ома</span>
<span class="ch-topic">Магнетизм</span>
</div>
<div class="ch-desc">Электростатика, закон Кулона, постоянный ток, закон Ома, последовательные и параллельные цепи, работа и мощность тока.</div>
<div class="ch-prog">
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-electro">0%</span></div>
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-electro" style="width:0%"></div></div>
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-2">0%</span></div>
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-2" style="width:0%"></div></div>
</div>
<div class="ch-action">
<span id="btn-electro">Открыть раздел</span>
<span id="btn-2">Открыть раздел</span>
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</a>
<a href="/textbooks/physics8_optics.html" class="ch-card optics" id="ch-optics">
<div class="ch-cover optics">
<div class="ch-cover-wm">III</div>
<div class="ch-num">Раздел III · §32–§40</div>
<a href="/textbook/physics-8-optics" class="ch-card ch3-card" id="ch-3">
<div class="ch-cover ch3">
<div class="ch-cover-wm">&#955;</div>
<div class="ch-num">Раздел 3</div>
<div class="ch-title">Световые явления</div>
<div class="ch-range">9 параграфов</div>
<div class="ch-range">§32–§40</div>
</div>
<div class="ch-body">
<div class="ch-desc">Источники света, прямолинейное распространение, отражение и преломление, плоское зеркало, линзы, оптические приборы, цвет и спектр.</div>
<div class="ch-topics">
<span class="ch-topic violet">Свет</span>
<span class="ch-topic violet">Линзы</span>
<span class="ch-topic violet">Зеркала</span>
<span class="ch-topic violet">Спектр</span>
</div>
<div class="ch-desc">Источники и распространение света, отражение и преломление, полное внутреннее отражение, линзы, оптические приборы, цвет и спектр.</div>
<div class="ch-prog">
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-optics">0%</span></div>
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-optics" style="width:0%"></div></div>
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-3">0%</span></div>
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-3" style="width:0%"></div></div>
</div>
<div class="ch-action">
<span id="btn-optics">Открыть раздел</span>
<span id="btn-3">Открыть раздел</span>
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
@@ -223,6 +203,17 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
</div>
<div class="ach-strip" id="ach-strip">
<div class="ach-icon">
<svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="8" r="6"/><path d="M15.477 12.89L17 22l-5-3-5 3 1.523-9.11"/>
</svg>
</div>
<div class="ach-text">
<div class="ach-title">Эксперт физики 8</div>
<div class="ach-sub" id="ach-sub">Прочитайте все 40 параграфов трёх разделов, чтобы получить достижение</div>
</div>
</div>
</main>
@@ -233,81 +224,90 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
<script>
'use strict';
/* THEME (sync with chapter files: they likely also use localStorage 'theme' or similar) */
/* THEME */
(function(){
const t = localStorage.getItem('physics8_theme') || localStorage.getItem('theme') || 'light';
if(t === 'dark') document.documentElement.classList.add('dark');
const lab = document.getElementById('theme-lab');
if(lab) lab.textContent = t === 'dark' ? 'Светлая' : 'Тёмная';
document.getElementById('theme-btn').addEventListener('click', ()=>{
var saved = localStorage.getItem('physics8_theme') || localStorage.getItem('theme') || 'light';
if (saved === 'dark') document.documentElement.classList.add('dark');
var lab = document.getElementById('theme-lab');
if (lab) lab.textContent = saved === 'dark' ? 'Светлая' : 'Тёмная';
document.getElementById('theme-btn').addEventListener('click', function(){
document.documentElement.classList.toggle('dark');
const dark = document.documentElement.classList.contains('dark');
var dark = document.documentElement.classList.contains('dark');
localStorage.setItem('physics8_theme', dark ? 'dark' : 'light');
localStorage.setItem('theme', dark ? 'dark' : 'light');
if(lab) lab.textContent = dark ? 'Светлая' : 'Тёмная';
if (lab) lab.textContent = dark ? 'Светлая' : 'Тёмная';
});
})();
/* PROGRESS — читаем из LocalStorage главных файлов
(каждая глава физики 8 хранит свой прогресс, попробуем найти ключи) */
function readChapterProgress(prefix, totalParas){
// Поищем ключи в localStorage: физика 8 главы хранят что-то типа "p1_done", "thermal_p3" и т.д.
// Применим эвристику: ищем ключи, содержащие prefix
let count = 0;
for(let i = 0; i < localStorage.length; i++){
const k = localStorage.key(i);
if(!k) continue;
const v = localStorage.getItem(k);
if(k.toLowerCase().includes(prefix) && (v === 'true' || v === '1' || v === 'done')){
count++;
}
/* PROGRESS
Overall progress = weighted average by para_count across the 3 children.
Weight = child.para_count / 40 (total).
This gives each paragraph equal weight regardless of section size. */
var TOTAL = 40;
var CH_PARA = { 'physics-8-thermal': 11, 'physics-8-electro': 20, 'physics-8-optics': 9 };
var CH_IDX = { 'physics-8-thermal': 1, 'physics-8-electro': 2, 'physics-8-optics': 3 };
function setChProg(idx, readCount, total) {
var pct = total ? Math.round(readCount * 100 / total) : 0;
var labelEl = document.getElementById('prog-' + idx);
var fillEl = document.getElementById('fill-' + idx);
var btnEl = document.getElementById('btn-' + idx);
if (labelEl) labelEl.textContent = pct + '%';
if (fillEl) fillEl.style.width = pct + '%';
if (btnEl) {
if (readCount > 0 && readCount < total) btnEl.textContent = 'Продолжить';
else if (readCount >= total) btnEl.textContent = 'Открыть снова';
else btnEl.textContent = 'Открыть раздел';
}
// Fallback: попробуем разные форматы — JSON-массив пройденных
const candidates = [`physics8_${prefix}_done`, `${prefix}_progress`, `physics8_${prefix}_read`];
for(const key of candidates){
const v = localStorage.getItem(key);
if(v){
try{
const arr = JSON.parse(v);
if(Array.isArray(arr)) return Math.min(arr.length, totalParas);
}catch{}
}
}
return Math.min(count, totalParas);
return pct;
}
function updateProgress(){
const tBy = readChapterProgress('thermal', 11);
const eBy = readChapterProgress('electro', 20);
const oBy = readChapterProgress('optics', 9);
function set(id, fillId, n, total){
const pct = total ? Math.round(n * 100 / total) : 0;
const el = document.getElementById(id);
const fl = document.getElementById(fillId);
if(el) el.textContent = pct + '% · ' + n + '/' + total;
if(fl) fl.style.width = pct + '%';
function renderProgress(children) {
var totalRead = 0;
for (var i = 0; i < children.length; i++) {
var ch = children[i];
var idx = CH_IDX[ch.slug];
if (!idx) continue;
var read = ch.progress ? (Array.isArray(ch.progress.read) ? ch.progress.read.length : 0) : 0;
var total = ch.para_count || CH_PARA[ch.slug] || 1;
totalRead += read;
setChProg(idx, read, total);
}
var pct = Math.round(totalRead * 100 / TOTAL);
var overallEl = document.getElementById('overall-text');
var fillEl = document.getElementById('overall-fill');
if (overallEl) overallEl.textContent = totalRead + ' из ' + TOTAL + ' параграфов · ' + pct + '%';
if (fillEl) fillEl.style.width = pct + '%';
if (totalRead >= TOTAL) {
var strip = document.getElementById('ach-strip');
var sub = document.getElementById('ach-sub');
if (strip) strip.classList.add('lit');
if (sub) sub.textContent = 'Выполнено! Вы прочитали весь курс физики 8 класса.';
}
set('prog-thermal', 'fill-thermal', tBy, 11);
set('prog-electro', 'fill-electro', eBy, 20);
set('prog-optics', 'fill-optics', oBy, 9);
const total = tBy + eBy + oBy;
const totalPct = Math.round(total * 100 / 40);
document.getElementById('overall-text').textContent = total + ' из 40 параграфов · ' + totalPct + '%';
document.getElementById('overall-fill').style.width = totalPct + '%';
// Buttons text
['thermal','electro','optics'].forEach(k=>{
const elBtn = document.getElementById('btn-'+k);
if(elBtn){
const n = k==='thermal'?tBy : k==='electro'?eBy : oBy;
const total = k==='thermal'?11 : k==='electro'?20 : 9;
if(n > 0 && n < total) elBtn.textContent = 'Продолжить';
else if(n >= total) elBtn.textContent = 'Открыть снова';
else elBtn.textContent = 'Открыть раздел';
}
});
}
updateProgress();
window.addEventListener('focus', updateProgress);
function loadProgress() {
if (typeof window.LS === 'undefined' || typeof window.LS.api !== 'function') {
renderProgress([]);
return;
}
window.LS.api('/api/textbooks/physics-8/children')
.then(function(data) {
if (data && data.children) renderProgress(data.children);
else renderProgress([]);
})
.catch(function() { renderProgress([]); });
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadProgress);
} else {
loadProgress();
}
window.addEventListener('focus', loadProgress);
</script>
</body>