feat: постраничная навигация по главам в teacher-guide (showChapter + hash-роутинг)

This commit is contained in:
Maxim Dolgolyov
2026-04-14 08:51:06 +03:00
parent 8317a991c4
commit dfb7c75fbf
+60 -81
View File
@@ -157,7 +157,12 @@
.tg-hero-chip svg { width: 13px; height: 13px; } .tg-hero-chip svg { width: 13px; height: 13px; }
/* Chapter */ /* Chapter */
.tg-chapter { margin-bottom: 56px; } .tg-chapter { margin-bottom: 0; display: none; }
.tg-chapter.tg-active { display: block; animation: tgFadeIn 0.28s ease; }
@keyframes tgFadeIn { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:translateY(0); } }
/* Search mode: reveal all chapters so matches are visible */
.tg-content.tg-search-mode .tg-chapter { display: block; margin-bottom: 32px; }
.tg-content.tg-search-mode .tg-chapter.search-hidden { display: none; }
.tg-chapter-header { .tg-chapter-header {
display: flex; align-items: center; gap: 16px; display: flex; align-items: center; gap: 16px;
padding: 28px 0 18px; padding: 28px 0 18px;
@@ -1061,56 +1066,54 @@
}); });
lucide.createIcons(); lucide.createIcons();
function navChapterClick(chId, btn) { /* ── Chapter switching ── */
const chapter = btn.closest('.tg-nav-chapter');
chapter.classList.toggle('open');
scrollToChapter(chId);
}
function scrollToChapter(chId) {
const el = document.getElementById(chId);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function scrollToSection(secId) {
const el = document.getElementById(secId);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
/* ── Scroll progress ── */
const scrollEl = document.getElementById('tg-scroll'); const scrollEl = document.getElementById('tg-scroll');
const progBar = document.getElementById('tg-prog-bar'); const progBar = document.getElementById('tg-prog-bar');
const tgContent = document.querySelector('.tg-content');
let _activeChId = null;
scrollEl.addEventListener('scroll', () => { function showChapter(chId, sectionId) {
const pct = scrollEl.scrollTop / (scrollEl.scrollHeight - scrollEl.clientHeight); const newEl = document.getElementById(chId);
progBar.style.width = Math.round(Math.min(pct, 1) * 100) + '%'; if (!newEl) return;
}); if (_activeChId) {
const prev = document.getElementById(_activeChId);
/* ── Scroll spy (sections) ── */ if (prev) prev.classList.remove('tg-active');
const allSections = document.querySelectorAll('.tg-section'); }
const allNavSecs = document.querySelectorAll('.tg-nav-sec-link'); newEl.classList.add('tg-active');
_activeChId = chId;
const sectionObs = new IntersectionObserver((entries) => { history.replaceState(null, '', '#' + chId);
entries.forEach(e => { scrollEl.scrollTop = 0;
if (!e.isIntersecting) return; if (sectionId) {
const id = e.target.id; requestAnimationFrame(() => {
allNavSecs.forEach(a => a.classList.toggle('active', a.dataset.sec === id)); const sec = document.getElementById(sectionId);
// Also open+highlight parent chapter if (sec) {
const ch = CHAPTERS.find(c => c.sections.includes(id)); const top = sec.offsetTop - 80;
if (ch) { scrollEl.scrollTo({ top, behavior: 'smooth' });
document.querySelectorAll('.tg-nav-ch-btn').forEach(b => { }
const parentDiv = b.closest('.tg-nav-chapter'); });
const isThis = parentDiv.dataset.ch === ch.id; }
b.classList.toggle('active', isThis); // Nav: highlight active chapter, open its sub-list
if (isThis && !parentDiv.classList.contains('open')) parentDiv.classList.add('open'); document.querySelectorAll('.tg-nav-chapter').forEach(div => {
}); const isThis = div.dataset.ch === chId;
} div.querySelector('.tg-nav-ch-btn').classList.toggle('active', isThis);
if (isThis && !div.classList.contains('open')) div.classList.add('open');
}); });
}, { root: scrollEl, rootMargin: '-15% 0px -70% 0px' }); document.querySelectorAll('.tg-nav-sec-link').forEach(a => a.classList.remove('active'));
markRead(chId);
}
allSections.forEach(s => sectionObs.observe(s)); function scrollToChapter(chId) { showChapter(chId); }
function scrollToSection(secId) {
const ch = CHAPTERS.find(c => c.sections.includes(secId));
if (ch) showChapter(ch.id, secId);
}
/* ── Chapter read tracking ── */ function navChapterClick(chId, btn) {
btn.closest('.tg-nav-chapter').classList.toggle('open');
showChapter(chId);
}
/* ── Read tracking ── */
const READ_KEY = 'ls_tg_read'; const READ_KEY = 'ls_tg_read';
let readChapters = JSON.parse(localStorage.getItem(READ_KEY) || '[]'); let readChapters = JSON.parse(localStorage.getItem(READ_KEY) || '[]');
@@ -1118,28 +1121,20 @@
if (!readChapters.includes(chId)) { if (!readChapters.includes(chId)) {
readChapters.push(chId); readChapters.push(chId);
localStorage.setItem(READ_KEY, JSON.stringify(readChapters)); localStorage.setItem(READ_KEY, JSON.stringify(readChapters));
updateReadUI();
} }
updateReadUI();
} }
function updateReadUI() { function updateReadUI() {
document.querySelectorAll('.tg-nav-chapter').forEach(div => { document.querySelectorAll('.tg-nav-chapter').forEach(div => {
const isRead = readChapters.includes(div.dataset.ch); div.querySelector('.tg-nav-ch-btn').classList.toggle('read', readChapters.includes(div.dataset.ch));
div.querySelector('.tg-nav-ch-btn').classList.toggle('read', isRead);
}); });
const n = readChapters.length; const n = readChapters.length;
document.getElementById('tg-prog-text').textContent = `${n} из ${CHAPTERS.length} глав прочитано`; document.getElementById('tg-prog-text').textContent = `${n} из ${CHAPTERS.length} глав прочитано`;
progBar.style.width = Math.round(n / CHAPTERS.length * 100) + '%';
} }
updateReadUI(); updateReadUI();
const chapterObs = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting && e.intersectionRatio >= 0.15) markRead(e.target.id);
});
}, { root: scrollEl, threshold: 0.15 });
document.querySelectorAll('.tg-chapter').forEach(c => chapterObs.observe(c));
/* ── Checklist ── */ /* ── Checklist ── */
const CL_KEY = 'ls_tg_checklist'; const CL_KEY = 'ls_tg_checklist';
const clState = JSON.parse(localStorage.getItem(CL_KEY) || '{}'); const clState = JSON.parse(localStorage.getItem(CL_KEY) || '{}');
@@ -1154,7 +1149,6 @@
localStorage.setItem(CL_KEY, JSON.stringify(clState)); localStorage.setItem(CL_KEY, JSON.stringify(clState));
updateChecklist(); updateChecklist();
}); });
// prevent link from triggering checkbox
item.querySelector('.tg-cl-link')?.addEventListener('click', e => e.stopPropagation()); item.querySelector('.tg-cl-link')?.addEventListener('click', e => e.stopPropagation());
}); });
@@ -1168,44 +1162,29 @@
/* ── Accordion ── */ /* ── Accordion ── */
function toggleAcc(head) { function toggleAcc(head) {
const item = head.closest('.tg-acc-item'); head.closest('.tg-acc-item').classList.toggle('open');
item.classList.toggle('open');
} }
/* ── Nav search ── */ /* ── Search: show all chapters matching query ── */
document.getElementById('tg-search').addEventListener('input', function() { document.getElementById('tg-search').addEventListener('input', function() {
const q = this.value.trim().toLowerCase(); const q = this.value.trim().toLowerCase();
if (!q) { if (!q) {
tgContent.classList.remove('tg-search-mode');
document.querySelectorAll('.tg-chapter, .tg-section').forEach(el => el.classList.remove('search-hidden')); document.querySelectorAll('.tg-chapter, .tg-section').forEach(el => el.classList.remove('search-hidden'));
return; return;
} }
tgContent.classList.add('tg-search-mode');
document.querySelectorAll('.tg-section').forEach(sec => { document.querySelectorAll('.tg-section').forEach(sec => {
const text = sec.textContent.toLowerCase(); sec.classList.toggle('search-hidden', !sec.textContent.toLowerCase().includes(q));
sec.classList.toggle('search-hidden', !text.includes(q));
}); });
document.querySelectorAll('.tg-chapter').forEach(ch => { document.querySelectorAll('.tg-chapter').forEach(ch => {
const visibleSecs = ch.querySelectorAll('.tg-section:not(.search-hidden)'); ch.classList.toggle('search-hidden', ch.querySelectorAll('.tg-section:not(.search-hidden)').length === 0);
ch.classList.toggle('search-hidden', visibleSecs.length === 0);
}); });
}); });
/* ── Card entrance animation ── */ /* ── Init from hash or default ch-1 ── */
const chapObs = new IntersectionObserver((entries) => { const initHash = location.hash.replace('#', '');
entries.forEach(e => { showChapter(CHAPTERS.find(c => c.id === initHash) ? initHash : 'ch-1');
if (e.isIntersecting) {
e.target.style.opacity = '1';
e.target.style.transform = 'translateY(0)';
chapObs.unobserve(e.target);
}
});
}, { root: scrollEl, threshold: 0.05 });
document.querySelectorAll('.tg-chapter').forEach((ch, i) => {
ch.style.opacity = '0';
ch.style.transform = 'translateY(20px)';
ch.style.transition = `opacity 0.4s ease ${i * 0.03}s, transform 0.4s ease ${i * 0.03}s`;
chapObs.observe(ch);
});
</script> </script>
</body> </body>
</html> </html>