diff options
| author | Sivaldo <sivaldodavi@disroot.org> | 2026-06-13 19:20:23 -0300 |
|---|---|---|
| committer | Sivaldo <sivaldodavi@disroot.org> | 2026-06-13 19:20:23 -0300 |
| commit | 7316238f16655bd127b04476bbf7864b27f940f9 (patch) | |
| tree | 61f25290b3e5fd14894e52754cfe6dd4e85845ef /layouts/partials | |
Diffstat (limited to 'layouts/partials')
| -rw-r--r-- | layouts/partials/disc-extras.html | 72 | ||||
| -rw-r--r-- | layouts/partials/quiz-runtime.html | 447 |
2 files changed, 519 insertions, 0 deletions
diff --git a/layouts/partials/disc-extras.html b/layouts/partials/disc-extras.html new file mode 100644 index 0000000..adff907 --- /dev/null +++ b/layouts/partials/disc-extras.html @@ -0,0 +1,72 @@ +{{/* disc-extras.html — recebe . (a página, seja _index ou single) + Renderiza colapsáveis: Guia de Estudo, Materiais (com subdivisões: Textos, Vídeos, Áudios) */}} + +{{/* ── Guia de estudo (guide) ── */}} +{{ with .Params.guide }} +<details class="disc-extra"> + <summary class="disc-extra-summary"> + <span class="disc-extra-label">guia de estudo</span> + <span class="disc-extra-chevron">↓</span> + </summary> + <div class="disc-extra-body disc-extra-prose"> + {{ . | markdownify }} + </div> +</details> +{{ end }} + +{{/* ── Materiais unificados (files, videos, audios) ── */}} +{{ $files := .Params.files }} +{{ $videos := .Params.videos }} +{{ $audios := .Params.audios }} +{{ if or $files (or $videos $audios) }} +<details class="disc-extra"> + <summary class="disc-extra-summary"> + <span class="disc-extra-label">materiais</span> + <span class="disc-extra-count"> + {{ $n := 0 }} + {{ with $files }}{{ $n = add $n (len .) }}{{ end }} + {{ with $videos }}{{ $n = add $n (len .) }}{{ end }} + {{ with $audios }}{{ $n = add $n (len .) }}{{ end }} + {{ $n }} item{{ if gt $n 1 }}s{{ end }} + </span> + <span class="disc-extra-chevron">↓</span> + </summary> + <div class="disc-extra-body"> + {{/* Textos (arquivos/links) */}} + {{ with $files }} + <div class="disc-extra-kind-label">textos</div> + {{ range . }} + <a class="disc-extra-row" href="{{ .url }}" target="_blank" rel="noopener"> + <span class="disc-extra-icon">↗</span> + <span class="disc-extra-name">{{ .name }}</span> + <span class="disc-extra-meta">link</span> + </a> + {{ end }} + {{ end }} + + {{/* Vídeos */}} + {{ with $videos }} + <div class="disc-extra-kind-label">vídeos</div> + {{ range . }} + <a class="disc-extra-row" href="{{ .url }}" target="_blank" rel="noopener"> + <span class="disc-extra-icon">▶</span> + <span class="disc-extra-name">{{ .name }}</span> + <span class="disc-extra-meta">youtube</span> + </a> + {{ end }} + {{ end }} + + {{/* Áudios */}} + {{ with $audios }} + <div class="disc-extra-kind-label">áudios</div> + {{ range . }} + <a class="disc-extra-row" href="{{ .url }}" target="_blank" rel="noopener"> + <span class="disc-extra-icon">♪</span> + <span class="disc-extra-name">{{ .name }}</span> + <span class="disc-extra-meta">youtube</span> + </a> + {{ end }} + {{ end }} + </div> +</details> +{{ end }} diff --git a/layouts/partials/quiz-runtime.html b/layouts/partials/quiz-runtime.html new file mode 100644 index 0000000..9ce5aab --- /dev/null +++ b/layouts/partials/quiz-runtime.html @@ -0,0 +1,447 @@ +<script> +/* ── ESTADO DA APLICAÇÃO ── */ +let currentQuiz = null; +/* questions, qIndex, acertos, erros, answered → usam _qState (quiz-engine.html) */ + +const BASE_TITLE = document.title; + +let activeFloater = null; +let currentTextComments = {}; +let lastSubjectRoute = null; + +/* ── UTILITÁRIOS ── */ +/* shuffle, esc, md → quiz-parser.html | _qState, _renderQuestion, etc. → quiz-engine.html */ + +function slugifyText(s) { + return String(s || '') + .normalize('NFD').replace(/[\u0300-\u036f]/g, '') + .toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); +} + +function show(id) { + ['page-home','page-quiz','page-result','page-text'].forEach(p => { + const el = document.getElementById(p); + if (el) el.classList.toggle('active', p === id); + }); +} + +/* ── ROTEAMENTO ── */ +function encodeHash(type, a, b, c) { + if (type === 'home') return ''; + if (type === 'disc') return '#disc/' + encodeURIComponent(a); + if (type === 'assunto') return '#assunto/' + [a, b].map(encodeURIComponent).join('/'); + if (type === 'quiz') return '#quiz/' + [a, b, c].map(encodeURIComponent).join('/'); + if (type === 'simulado') return '#simulado/' + [a, b].map(encodeURIComponent).join('/'); + return ''; +} + +function decodeHash(hash) { + if (!hash || hash === '#') return { type: 'home' }; + const parts = (hash.startsWith('#') ? hash.slice(1) : hash) + .split('/').map(s => { try { return decodeURIComponent(s); } catch(e) { return s; } }); + + const type = parts[0]; + if (type === 'disc' && parts.length >= 2) + return { type: 'disc', disc: parts[1] }; + if (type === 'assunto' && parts.length >= 3) + return { type: 'assunto', discFolder: parts[1], slug: parts[2] }; + if (type === 'quiz' && parts.length >= 4) + return { type: 'quiz', discFolder: parts[1], slug: parts[2], quizId: parts[3] }; + if (type === 'simulado' && parts.length >= 3) + return { type: 'simulado', discFolder: parts[1], quizId: parts[2] }; + + return { type: 'home' }; +} + +function navigate(hash, replace) { + const url = window.location.pathname + window.location.search + (hash || ''); + if (replace) history.replaceState({ hash }, '', url || window.location.pathname); + else history.pushState({ hash }, '', url || window.location.pathname); +} + +function renderRoute(route) { + if (route.type === 'disc') _showDisc(route.disc); + else if (route.type === 'assunto') _openSubject(route.discFolder, route.slug); + else if (route.type === 'quiz') _startQuiz(route.discFolder, route.slug, route.quizId); + else if (route.type === 'simulado')_startSimulado(route.discFolder, route.quizId); + else _showHome(); +} + +window.addEventListener('popstate', () => renderRoute(decodeHash(window.location.hash))); + +/* ── INTERFACE: HOME ── */ +function showHome() { navigate('', false); _showHome(); } + +function _showHome() { + closeFloater(); + buildHome(); + show('page-home'); + document.getElementById('breadcrumb').textContent = ''; + document.title = BASE_TITLE; +} + +function buildHome() { + const el = document.getElementById('page-home'); + el.innerHTML = ` + <div class="disc-grid"> + ${DATA.map(disc => { + const n = disc.subjects.length; + const nq = disc.subjects.reduce((acc, s) => acc + s.quizzes.length, 0) + (disc.quizzes ? disc.quizzes.length : 0); + const meta = [ + n ? `${n} assunto${n > 1 ? 's' : ''}` : null, + nq ? `${nq} quiz${nq > 1 ? 'zes' : ''}` : null, + ].filter(Boolean).join(' · ') || 'sem conteúdo'; + return ` + <a class="disc-card fade" href="${encodeHash('disc', disc.discipline)}" + onclick="openDisc('${esc(disc.discipline)}');return false;"> + <div class="disc-card-name">${esc(disc.discipline)}</div> + <div class="disc-card-meta">${meta}</div> + </a>`; + }).join('')} + </div>`; +} + +/* ── INTERFACE: DISCIPLINA ── */ +function openDisc(discName) { navigate(encodeHash('disc', discName), false); _showDisc(discName); } + +function _showDisc(discName) { + closeFloater(); + const disc = DATA.find(d => d.discipline === discName); + if (!disc) { _showHome(); return; } + + const introHtml = disc.intro && disc.intro.content + ? `<div class="disc-intro"> + <article class="prose disc-intro-prose">${disc.intro.content}</article> + ${disc.intro.references && disc.intro.references.length + ? `<div class="references disc-intro-refs"> + <div class="ref-head">referências</div> + <div class="ref-list">${disc.intro.references.map(r => `<p>${esc(r)}</p>`).join('')}</div> + </div>` + : ''} + </div>` + : ''; + + const simuladosHtml = disc.quizzes && disc.quizzes.length ? ` + <div class="subject-group"> + <div class="sq-label" style="margin-bottom:10px;">Simulados da Disciplina</div> + ${disc.quizzes.map(qz => ` + <a class="sq-item" href="${encodeHash('simulado', disc.folderKey, qz.id)}" + onclick="startSimulado('${esc(disc.folderKey)}','${esc(qz.id)}');return false;" + style="border-bottom:none; background:var(--surface); padding: 10px; border-radius: 4px; margin-bottom: 5px;"> + <span class="sq-name" style="color:var(--amber); font-weight:bold;">${esc(qz.title)}</span> + <span class="sq-meta">${qz.questions.length} questões</span> + </a> + `).join('')} + </div> + ` : ''; + + const subjectsHtml = disc.subjects.map(s => { + const bits = []; + if (s.text) bits.push('texto'); + if (s.quizzes.length) bits.push(`${s.quizzes.length} quiz${s.quizzes.length > 1 ? 'zes' : ''}`); + const meta = bits.join(' · ') || 'vazio'; + return ` + <a class="subject-row" href="${encodeHash('assunto', disc.folderKey, s.slug)}" + onclick="openSubject('${esc(disc.folderKey)}','${esc(s.slug)}');return false;"> + <span class="subject-row-name">${esc(s.name)}</span> + <span class="subject-row-meta">${meta}</span> + </a>`; + }).join(''); + + const el = document.getElementById('page-home'); + el.innerHTML = ` + <div class="discipline fade"> + <div class="disc-back"><a href="#" onclick="showHome();return false;">← disciplinas</a></div> + <div class="discipline-name">${esc(disc.discipline)}</div> + ${introHtml} + ${simuladosHtml} + <div class="subject-list">${subjectsHtml || '<p class="empty-note">sem assuntos ainda</p>'}</div> + </div>`; + + show('page-home'); + document.getElementById('breadcrumb').textContent = disc.discipline; + document.title = disc.discipline + ' · ' + BASE_TITLE; +} + +/* ── INTERFACE: ASSUNTO ── */ +function openSubject(discFolder, slug) { + navigate(encodeHash('assunto', discFolder, slug), false); + _openSubject(discFolder, slug); +} + +function _openSubject(discFolder, slug) { + closeFloater(); + const disc = DATA.find(d => d.folderKey === discFolder); + if (!disc) { _showHome(); return; } + const sub = disc.subjects.find(s => s.slug === slug); + if (!sub) { _showDisc(disc.discipline); return; } + + lastSubjectRoute = { discFolder, slug, discName: disc.discipline, subjectName: sub.name }; + + document.getElementById('text-discipline').textContent = disc.discipline; + document.getElementById('text-title').textContent = sub.name; + + const qzWrap = document.getElementById('text-quizzes'); + const qzList = document.getElementById('text-quizzes-list'); + + if (sub.quizzes.length) { + qzWrap.style.display = ''; + qzList.innerHTML = sub.quizzes.map(qz => { + const mc = qz.questions.filter(q => q.type === 'mc').length; + const open = qz.questions.filter(q => q.type === 'open').length; + const meta = [ + mc ? `${mc} mc` : null, + open ? `${open} abertas` : null, + ].filter(Boolean).join(' · ') || `${qz.questions.length} questões`; + return ` + <a class="sq-item" href="${encodeHash('quiz', discFolder, slug, qz.id)}" + onclick="startQuiz('${esc(discFolder)}','${esc(slug)}','${esc(qz.id)}');return false;"> + <span class="sq-name">${esc(qz.title)}</span> + <span class="sq-meta">${meta}</span> + </a>`; + }).join(''); + } else { + qzWrap.style.display = 'none'; + qzList.innerHTML = ''; + } + + const prose = document.getElementById('prose'); + if (sub.text && sub.text.content) { + prose.innerHTML = sub.text.content; + currentTextComments = {}; + buildToc(prose); + } else { + prose.innerHTML = '<p class="empty-note">Texto ainda não disponível para este assunto.</p>'; + document.getElementById('text-toc').style.display = 'none'; + } + + const refContainer = document.getElementById('text-references-container'); + const refList = document.getElementById('text-references-list'); + const refs = sub.text ? (sub.text.references || []) : []; + + if (refs.length) { + refContainer.style.display = 'block'; + refList.innerHTML = refs.map(r => `<p>${esc(r)}</p>`).join(''); + } else { + refContainer.style.display = 'none'; + } + + document.getElementById('breadcrumb').textContent = `${disc.discipline} · ${sub.name}`; + document.title = sub.name + ' · ' + BASE_TITLE; + show('page-text'); + window.scrollTo(0, 0); +} + +/* ── INTERFACE: SUMÁRIO (TOC) ── */ +function buildToc(proseEl) { + const tocWrap = document.getElementById('text-toc'); + const tocList = document.getElementById('text-toc-list'); + tocList.innerHTML = ''; + + const headings = proseEl.querySelectorAll('h2, h3'); + if (!headings.length) { tocWrap.style.display = 'none'; return; } + + const used = new Set(); + headings.forEach(h => { + if (!h.id) { + let base = slugifyText(h.textContent) || 'sec'; + let id = base, i = 2; + while (used.has(id) || document.getElementById(id)) { id = base + '-' + i++; } + h.id = id; + } + used.add(h.id); + }); + + headings.forEach(h => { + const li = document.createElement('li'); + li.className = 'toc-' + h.tagName.toLowerCase(); + const a = document.createElement('a'); + a.href = '#' + h.id; + a.textContent = h.textContent; + a.addEventListener('click', e => { + e.preventDefault(); + h.scrollIntoView({ behavior: 'smooth', block: 'start' }); + history.replaceState(history.state, '', '#' + h.id); + }); + li.appendChild(a); + tocList.appendChild(li); + }); + + tocWrap.style.display = ''; +} + +/* ── INTERFACE: COMPONENTES FLUTUANTES ── */ +document.addEventListener('click', e => { + const footnoteLink = e.target.closest('a[href^="#fn:"]'); + const closeBtn = e.target.closest('.f-close'); + + if (closeBtn) { closeFloater(); return; } + + if (footnoteLink) { + e.preventDefault(); e.stopPropagation(); closeFloater(); + const targetId = footnoteLink.getAttribute('href').substring(1); + const footnoteElement = document.getElementById(targetId); + if (footnoteElement) { + const contentClone = footnoteElement.cloneNode(true); + const backLink = contentClone.querySelector('.footnote-backref'); + if (backLink) backLink.remove(); + const parentParagraph = footnoteLink.closest('p, li'); + const quoteText = parentParagraph ? parentParagraph.innerText.substring(0, 40) + '…' : 'Nota'; + openFloater(footnoteLink, quoteText, contentClone.innerHTML); + footnoteLink.classList.add('active'); + } + } else if (activeFloater && !e.target.closest('.floater')) { + closeFloater(); + } +}); + +function openFloater(target, quote, innerHTMLContent) { + const f = document.createElement('div'); + f.className = 'floater'; + f.innerHTML = ` + <div class="f-label"><span>comentário</span><span class="f-close">×</span></div> + <div class="f-quote">${quote}</div> + <div>${innerHTMLContent}</div>`; + document.body.appendChild(f); + + const r = target.getBoundingClientRect(); + const top = window.scrollY + r.bottom + 8; + const padding = 20; + let left = window.scrollX + r.left; + const maxLeft = window.innerWidth - f.offsetWidth - padding; + + if (left > maxLeft) left = Math.max(padding, maxLeft); + + f.style.top = top + 'px'; + f.style.left = left + 'px'; + activeFloater = f; +} + +function closeFloater() { + if (activeFloater) { activeFloater.remove(); activeFloater = null; } + document.querySelectorAll('a[href^="#fn:"]').forEach(a => a.classList.remove('active')); +} + +document.addEventListener('keydown', e => { if (e.key === 'Escape') closeFloater(); }); + +/* ── MOTOR: QUIZ E SIMULADO ── */ +function startQuiz(discFolder, slug, quizId) { + navigate(encodeHash('quiz', discFolder, slug, quizId), false); + _startQuiz(discFolder, slug, quizId); +} + +function _startQuiz(discFolder, slug, quizId) { + closeFloater(); + const disc = DATA.find(d => d.folderKey === discFolder); + if (!disc) { _showHome(); return; } + const sub = disc.subjects.find(s => s.slug === slug); + if (!sub) { _showDisc(disc.discipline); return; } + const quiz = sub.quizzes.find(q => q.id === quizId); + if (!quiz) { _openSubject(discFolder, slug); return; } + + lastSubjectRoute = { discFolder, slug, discName: disc.discipline, subjectName: sub.name }; + currentQuiz = { + isSimulado: false, discFolder, slug, quizId, + subjectName: sub.name, quizTitle: quiz.title + }; + + _qState.questions = prepareQuizQuestions(quiz.questions); + _qState.qIndex = _qState.acertos = _qState.erros = 0; + _clearProgress('spa_' + discFolder + '_' + slug + '_' + quizId); + + document.getElementById('breadcrumb').textContent = `${disc.discipline} · ${sub.name} · ${quiz.title}`; + document.title = `${quiz.title} · ${sub.name} · ${BASE_TITLE}`; + + show('page-quiz'); + renderQuestion(); +} + +function startSimulado(discFolder, quizId) { + navigate(encodeHash('simulado', discFolder, quizId), false); + _startSimulado(discFolder, quizId); +} + +function _startSimulado(discFolder, quizId) { + closeFloater(); + const disc = DATA.find(d => d.folderKey === discFolder); + if (!disc) { _showHome(); return; } + const quiz = disc.quizzes.find(q => q.id === quizId); + if (!quiz) { _showDisc(disc.discipline); return; } + + lastSubjectRoute = null; + currentQuiz = { + isSimulado: true, discFolder, quizId, + subjectName: disc.discipline, quizTitle: quiz.title + }; + + _qState.questions = prepareQuizQuestions(quiz.questions); + _qState.qIndex = _qState.acertos = _qState.erros = 0; + _clearProgress('spa_sim_' + discFolder + '_' + quizId); + + document.getElementById('breadcrumb').textContent = `${disc.discipline} · ${quiz.title}`; + document.title = `${quiz.title} · ${disc.discipline} · ${BASE_TITLE}`; + + show('page-quiz'); + renderQuestion(); +} + +function restartQuiz() { + if (currentQuiz.isSimulado) { + _clearProgress('spa_sim_' + currentQuiz.discFolder + '_' + currentQuiz.quizId); + _startSimulado(currentQuiz.discFolder, currentQuiz.quizId); + } else { + _clearProgress('spa_' + currentQuiz.discFolder + '_' + currentQuiz.slug + '_' + currentQuiz.quizId); + _startQuiz(currentQuiz.discFolder, currentQuiz.slug, currentQuiz.quizId); + } +} + +function backToSubject() { + if (currentQuiz && currentQuiz.isSimulado) { + openDisc(currentQuiz.subjectName); + } else if (lastSubjectRoute) { + navigate(encodeHash('assunto', lastSubjectRoute.discFolder, lastSubjectRoute.slug), false); + _openSubject(lastSubjectRoute.discFolder, lastSubjectRoute.slug); + } else { + showHome(); + } +} + +function renderQuestion() { + _renderQuestion(_qState.questions[_qState.qIndex]); + const progKey = currentQuiz + ? 'spa_' + (currentQuiz.isSimulado + ? 'sim_' + currentQuiz.discFolder + '_' + currentQuiz.quizId + : currentQuiz.discFolder + '_' + currentQuiz.slug + '_' + currentQuiz.quizId) + : null; + if (progKey) _saveProgress(progKey); +} + +/* selectOpt, showRef, selfJudge, setFeedback → definidos em quiz-parser.html */ + +function nextQuestion() { + _qState.qIndex++; + if (_qState.qIndex >= _qState.questions.length) { showResult(); return; } + const body = document.getElementById('quiz-body'); + body.classList.add('fade-out'); + setTimeout(() => { body.classList.remove('fade-out'); renderQuestion(); }, 100); +} + +/* ── TELA DE RESULTADOS ── */ +function showResult() { + show('page-result'); + _buildResult(); + document.getElementById('breadcrumb').textContent = currentQuiz.subjectName + ' · ' + currentQuiz.quizTitle; +} + +/* ── INICIALIZAÇÃO DE ROTA ── */ +(function init() { + const route = decodeHash(window.location.hash); + if (route.type !== 'home') { + history.replaceState({ hash: window.location.hash }, '', window.location.href); + renderRoute(route); + } else { + history.replaceState({ hash: '' }, '', window.location.pathname + window.location.search); + _showHome(); + } +})(); +</script> |
