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 | |
Diffstat (limited to 'layouts')
| -rw-r--r-- | layouts/_default/baseof.html | 46 | ||||
| -rw-r--r-- | layouts/_default/list.html | 146 | ||||
| -rw-r--r-- | layouts/partials/disc-extras.html | 72 | ||||
| -rw-r--r-- | layouts/partials/quiz-runtime.html | 447 | ||||
| -rw-r--r-- | layouts/quizzes/list.html | 57 | ||||
| -rw-r--r-- | layouts/quizzes/single.html | 60 | ||||
| -rw-r--r-- | layouts/txts/single.html | 224 |
7 files changed, 1052 insertions, 0 deletions
diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html new file mode 100644 index 0000000..4fca1f9 --- /dev/null +++ b/layouts/_default/baseof.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<html lang="pt-BR"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="{{ .Site.Params.description }}"> + <meta name="theme-color" content="#c9a961"> + <link rel="manifest" href="{{ "/manifest.json" | relURL }}"> + <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect fill='%230f0f13' width='100' height='100'/><text x='50' y='75' font-size='60' font-weight='bold' text-anchor='middle' fill='%23c9a961' font-family='monospace'>D</text></svg>"> + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> + <link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,600;1,400&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> + <link rel="stylesheet" href="{{ "/main.css" | relURL }}"> + <title>{{ block "title" . }}{{ .Site.Title }}{{ end }}</title> +</head> +<body> + <header> + {{ block "header" . }} + <a class="site-name" href="{{ "/" | relURL }}">direito</a> + {{ end }} + </header> + <main> + {{ block "main" . }}{{ end }} + </main> + + <script src="{{ "/quiz-parser.js" | relURL }}"></script> + <script src="{{ "/quiz-engine.js" | relURL }}"></script> + <script src="{{ "/quiz-runtime.js" | relURL }}"></script> + <script src="{{ "/footnotes.js" | relURL }}"></script> + <script src="{{ "/home-filter.js" | relURL }}"></script> + <script src="{{ "/toc.js" | relURL }}"></script> + <script src="{{ "/dark-mode.js" | relURL }}"></script> + <script src="{{ "/keyboard-shortcuts.js" | relURL }}"></script> + <script src="{{ "/dashboard.js" | relURL }}"></script> + + {{ block "scripts" . }}{{ end }} + + <script> + if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/sw.js').catch(() => {}); + }); + } + </script> +</body> +</html> diff --git a/layouts/_default/list.html b/layouts/_default/list.html new file mode 100644 index 0000000..09804c9 --- /dev/null +++ b/layouts/_default/list.html @@ -0,0 +1,146 @@ +{{ define "title" }} + {{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} · {{ .Site.Title }}{{ end }} +{{ end }} + +{{ define "header" }} + <a class="site-name" href="{{ "/" | relURL }}">direito</a> + {{ if not .IsHome }} + <span class="header-sep">/</span> + <span class="breadcrumb">{{ .Title }}</span> + {{ end }} +{{ end }} + +{{ define "main" }} +{{ if .IsHome }} + + {{/* Mapas por slug de disciplina */}} + {{ $simMap := dict }} + {{ $quizMap := dict }} + {{ $discsQz := slice }} + {{ range .Site.RegularPages }} + {{ if eq .Section "quizzes" }} + {{ $slug := .File.Dir | strings.TrimSuffix "/" | path.Base }} + {{ if not (in $discsQz $slug) }}{{ $discsQz = $discsQz | append $slug }}{{ end }} + {{ $isSim := or (.Params.simulado) (in (.File.BaseFileName | lower) "simulado") }} + {{ if $isSim }} + {{ $simMap = merge $simMap (dict $slug (add (index $simMap $slug | default 0) 1)) }} + {{ else }} + {{ $quizMap = merge $quizMap (dict $slug (add (index $quizMap $slug | default 0) 1)) }} + {{ end }} + {{ end }} + {{ end }} + + <div class="disc-grid"> + {{ $txtsSlugs := slice }} + {{ range .Site.Sections }} + {{ if eq .Section "txts" }} + {{ range .Sections }} + {{ $slug := .RelPermalink | strings.TrimSuffix "/" | path.Base }} + {{ $txtsSlugs = $txtsSlugs | append $slug }} + {{ $sims := index $simMap $slug | default 0 }} + {{ $qz := index $quizMap $slug | default 0 }} + <a class="disc-card fade" href="{{ .RelPermalink }}" data-filterable> + <div class="disc-card-name">{{ .Title }}</div> + <div class="disc-card-meta"> + {{ $n := len .RegularPages }} + {{ if $n }}{{ $n }} assunto{{ if gt $n 1 }}s{{ end }}{{ else }}sem textos{{ end }} + {{ if $sims }} · simulado{{ end }} + {{ if $qz }} · {{ $qz }} exercício{{ if gt $qz 1 }}s{{ end }}{{ end }} + </div> + </a> + {{ end }} + {{ end }} + {{ end }} + + {{/* Disciplinas que só existem em quizzes (sem txts) */}} + {{ range $slug := $discsQz }} + {{ if not (in $txtsSlugs $slug) }} + {{ $sims := index $simMap $slug | default 0 }} + {{ $qz := index $quizMap $slug | default 0 }} + <a class="disc-card fade" href="/{{ $slug }}/" data-filterable> + <div class="disc-card-name">{{ humanize (replace $slug "-" " ") }}</div> + <div class="disc-card-meta"> + {{ if $sims }}simulado · {{ end }}{{ $qz }} exercício{{ if gt $qz 1 }}s{{ end }} + </div> + </a> + {{ end }} + {{ end }} + </div> + +{{ else }} + {{/* Página de disciplina */}} + {{ $discSlug := .RelPermalink | strings.TrimSuffix "/" | path.Base }} + <div class="discipline fade"> + <div class="disc-back"><a href="{{ "/" | relURL }}">← disciplinas</a></div> + <div class="discipline-name">{{ .Title }}</div> + + {{ if .Content }} + <div class="disc-intro"> + <article class="prose disc-intro-prose">{{ .Content }}</article> + </div> + {{ end }} + + <div class="disc-extras-wrap"> + {{ partial "disc-extras.html" . }} + </div> + + {{/* Simulados desta disciplina */}} + {{ $simulados := slice }} + {{ range $.Site.RegularPages }} + {{ if eq .Section "quizzes" }} + {{ $parent := .File.Dir | strings.TrimSuffix "/" | path.Base }} + {{ if eq $parent $discSlug }} + {{ if or (.Params.simulado) (in (.File.BaseFileName | lower) "simulado") }} + {{ $simulados = $simulados | append . }} + {{ end }} + {{ end }} + {{ end }} + {{ end }} + {{ with $simulados }} + <div class="subject-group" style="margin-bottom:1.5rem;"> + <div class="sq-label" style="margin-bottom:.65rem;">simulados</div> + <div class="sq-list"> + {{ range . }} + <a class="sq-item" href="{{ .RelPermalink }}"> + <span class="sq-name" style="color:var(--amber);">{{ .Title }}</span> + <span class="sq-meta">simulado</span> + </a> + {{ end }} + </div> + </div> + {{ end }} + + {{/* Assuntos com vínculo aos exercícios */}} + {{ $exercises := slice }} + {{ range $.Site.RegularPages }} + {{ if eq .Section "quizzes" }} + {{ $parent := .File.Dir | strings.TrimSuffix "/" | path.Base }} + {{ if eq $parent $discSlug }} + {{ if not (or (.Params.simulado) (in (.File.BaseFileName | lower) "simulado")) }} + {{ $exercises = $exercises | append . }} + {{ end }} + {{ end }} + {{ end }} + {{ end }} + + <div class="subject-list"> + {{ range .RegularPages }} + {{ $assuntoSlug := .File.BaseFileName }} + {{ $linkedQuiz := false }} + {{ range $exercises }} + {{ if eq .File.BaseFileName $assuntoSlug }} + {{ $linkedQuiz = . }} + {{ end }} + {{ end }} + <a class="subject-row" href="{{ .RelPermalink }}"> + <span class="subject-row-name">{{ .Title }}</span> + <span class="subject-row-meta"> + texto{{ if $linkedQuiz }} · exercícios{{ end }} + </span> + </a> + {{ end }} + {{ if not .RegularPages }}<p class="empty-note">sem assuntos ainda</p>{{ end }} + </div> + </div> +{{ end }} +{{ end }} 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> diff --git a/layouts/quizzes/list.html b/layouts/quizzes/list.html new file mode 100644 index 0000000..c37a486 --- /dev/null +++ b/layouts/quizzes/list.html @@ -0,0 +1,57 @@ +{{ define "title" }}{{ .Title }} · {{ .Site.Title }}{{ end }} + +{{ define "header" }} + <a class="site-name" href="{{ "/" | relURL }}">direito</a> + <span class="header-sep">/</span> + <span class="breadcrumb">{{ .Title }}</span> +{{ end }} + +{{ define "main" }} +{{ $discSlug := .RelPermalink | strings.TrimSuffix "/" | path.Base }} +<div class="discipline fade"> + <div class="disc-back"><a href="{{ "/" | relURL }}">← disciplinas</a></div> + <div class="discipline-name">{{ .Title }}</div> + + {{/* Separa simulados (frontmatter `simulado: true` OU nome contendo "simulado") dos quizzes + desta disciplina, usando .File.Dir já que as subpastas de quizzes/ não são sections. */}} + {{ $simulados := slice }} + {{ $quizzes := slice }} + {{ range .RegularPages }} + {{ if or (.Params.simulado) (in (.File.BaseFileName | lower) "simulado") }} + {{ $simulados = $simulados | append . }} + {{ else }} + {{ $quizzes = $quizzes | append . }} + {{ end }} + {{ end }} + + {{ with $simulados }} + <div class="subject-group" style="margin-bottom:1.5rem;"> + <div class="sq-label" style="margin-bottom:.65rem;">simulados</div> + <div class="sq-list"> + {{ range . }} + <a class="sq-item" href="{{ .RelPermalink }}"> + <span class="sq-name" style="color:var(--amber);">{{ .Title }}</span> + <span class="sq-meta">simulado</span> + </a> + {{ end }} + </div> + </div> + {{ end }} + + {{ with $quizzes }} + <div class="subject-list"> + <div class="sq-label" style="margin-bottom:.65rem;">exercícios</div> + {{ range . }} + <a class="subject-row" href="{{ .RelPermalink }}"> + <span class="subject-row-name">{{ .Title }}</span> + <span class="subject-row-meta">quiz</span> + </a> + {{ end }} + </div> + {{ end }} + + {{ if and (not $simulados) (not $quizzes) }} + <p class="empty-note">sem conteúdo ainda</p> + {{ end }} +</div> +{{ end }} diff --git a/layouts/quizzes/single.html b/layouts/quizzes/single.html new file mode 100644 index 0000000..0a51a9c --- /dev/null +++ b/layouts/quizzes/single.html @@ -0,0 +1,60 @@ +{{ define "title" }}{{ .Title }} · {{ .Site.Title }}{{ end }} +{{ define "header" }} + {{ $discSlug := .File.Dir | path.Dir | path.Base }} + <a class="site-name" href="{{ "/" | relURL }}">direito</a> + <span class="header-sep">/</span> + {{ $discPage := $.Site.GetPage (printf "/txts/%s" $discSlug) }} + {{ if $discPage }} + <a class="breadcrumb-link" href="{{ $discPage.RelPermalink }}">{{ $discPage.Title }}</a> + <span class="header-sep">/</span> + {{ end }} + <span class="breadcrumb">{{ .Title }}</span> +{{ end }} +{{ define "main" }} +{{ $discSlug := .File.Dir | path.Dir | path.Base }} +{{ $discPage := $.Site.GetPage (printf "/txts/%s" $discSlug) }} +<div class="fade"> + {{ if $discPage }} + <div class="disc-back"><a href="{{ $discPage.RelPermalink }}">← {{ $discPage.Title }}</a></div> + {{ else }} + <div class="disc-back"><a href="{{ "/" | relURL }}">← disciplinas</a></div> + {{ end }} + <div class="discipline-name" style="margin-bottom:1.5rem;">{{ .Title }}</div> + <div id="quiz-wrap"> + <div class="progress-wrap"><div class="progress-fill" id="prog"></div></div> + <div class="q-meta" id="q-counter"></div> + <div id="quiz-body"></div> + <div class="next-row"> + <button class="btn" id="btn-next" onclick="nextQuestion()" style="display:none">próxima →</button> + </div> + </div> + <div id="quiz-result" style="display:none;"> + <div class="res-score" id="res-pct">0%</div> + <div class="res-subtitle"><span id="res-a">0</span> de <span id="res-t">0</span> acertos</div> + <div class="res-divider"></div> + <div class="res-stats"> + <div class="rs-item"><div class="rs-num g" id="res-a2">0</div><div class="rs-label">acertos</div></div> + <div class="rs-item"><div class="rs-num r" id="res-e">0</div><div class="rs-label">erros</div></div> + </div> + <div class="res-msg"> + <div class="res-msg-label">diagnóstico</div> + <div id="res-msg-body"></div> + </div> + <div class="res-btns"> + <button class="btn" onclick="restartQuiz()">refazer</button> + {{ if $discPage }} + <a class="btn" href="{{ $discPage.RelPermalink }}">voltar</a> + {{ end }} + </div> + </div> +</div> + +<script> + // Definir a chave única para este quiz + window.QUIZ_KEY = '{{ $discSlug }}_{{ .File.BaseFileName }}'; + // Conteúdo bruto do quiz em JSON (sem notas de rodapé Markdown) + const RAW_CONTENT = {{ .RawContent | jsonify | safeJS }}; + // Remover notas de rodapé Markdown ([^1]: ...) + const RAW_QUIZ = RAW_CONTENT.replace(/\n\[\^\d+\]:[^\n]*/g, ''); +</script> +{{ end }} diff --git a/layouts/txts/single.html b/layouts/txts/single.html new file mode 100644 index 0000000..d8f7eb1 --- /dev/null +++ b/layouts/txts/single.html @@ -0,0 +1,224 @@ +{{ define "title" }}{{ .Title }} · {{ .Site.Title }}{{ end }} + +{{ define "header" }} + {{ $discSlug := .RelPermalink | strings.TrimSuffix "/" | path.Dir | path.Base }} + <a class="site-name" href="{{ "/" | relURL }}">direito</a> + {{ range $.Site.Sections }} + {{ if and (eq .Section "txts") (eq (.RelPermalink | strings.TrimSuffix "/" | path.Base) $discSlug) }} + <span class="header-sep">/</span> + <a class="breadcrumb-link" href="{{ .RelPermalink }}">{{ .Title }}</a> + {{ end }} + {{ end }} + <span class="header-sep">/</span> + <span class="breadcrumb">{{ .Title }}</span> +{{ end }} + +{{ define "main" }} +{{ $slug := .File.BaseFileName }} +{{ $discSlug := .RelPermalink | strings.TrimSuffix "/" | path.Dir | path.Base }} + +{{/* Encontra quiz do assunto (exercício, não simulado) */}} +{{ $quizPage := false }} +{{ range $.Site.RegularPages }} + {{ if eq .Section "quizzes" }} + {{ $parent := .File.Dir | strings.TrimSuffix "/" | path.Base }} + {{ if and (eq $parent $discSlug) (eq .File.BaseFileName $slug) }} + {{ if not (or (.Params.simulado) (in (.File.BaseFileName | lower) "simulado")) }} + {{ $quizPage = . }} + {{ end }} + {{ end }} + {{ end }} +{{ end }} + +<div class="page-text fade"> + <div class="text-meta"> + {{ range $.Site.Sections }} + {{ if and (eq .Section "txts") (eq (.RelPermalink | strings.TrimSuffix "/" | path.Base) $discSlug) }} + <span>{{ .Title }}</span><span class="dot"></span> + {{ end }} + {{ end }} + <span>assunto</span> + </div> + <h1 class="text-title">{{ .Title }}</h1> + + {{/* Colapsáveis: guia de estudo e materiais do assunto */}} + {{ $hasExtras := or .Params.guide (or .Params.files (or .Params.videos .Params.audios)) }} + {{ if $hasExtras }} + <div class="disc-extras-wrap"> + {{ partial "disc-extras.html" . }} + </div> + {{ end }} + + <nav class="toc" id="toc-nav" style="display:none;" aria-label="Sumário"> + <div class="toc-label">sumário</div> + <ol id="toc-list"></ol> + </nav> + + <article class="prose" id="prose">{{ .Content }}</article> + + {{/* Notas de rodapé — renderizadas pelo Hugo automaticamente no .Content; + o floater (footnotes.js) intercepta os links #fn: e mostra tooltip */}} + + {{/* Questões do assunto — quiz inline embutido */}} + {{ if $quizPage }} + <div class="inline-quiz-wrap" id="inline-quiz-section"> + <div class="inline-quiz-head"> + <span class="sq-label">exercícios deste assunto</span> + <a class="inline-quiz-link" href="{{ $quizPage.RelPermalink }}">modo completo →</a> + </div> + <div id="inline-quiz-body"> + <button class="btn btn-start-quiz" id="btn-start-inline" onclick="startInlineQuiz()"> + Iniciar exercícios + </button> + </div> + <div id="inline-quiz-player" style="display:none;"> + <div class="progress-wrap" style="margin-bottom:.75rem;"><div class="progress-fill" id="iprog"></div></div> + <div class="q-meta" id="iq-counter"></div> + <div id="iq-body"></div> + <div class="next-row"> + <button class="btn" id="ibtn-next" onclick="inlineNextQuestion()" style="display:none">próxima →</button> + </div> + </div> + <div id="inline-quiz-result" style="display:none;"> + <div class="res-score" id="ires-pct">0%</div> + <div class="res-subtitle"><span id="ires-a">0</span> de <span id="ires-t">0</span> acertos</div> + <div class="res-msg" style="margin-top:1rem;"> + <div class="res-msg-label">diagnóstico</div> + <div id="ires-msg"></div> + </div> + <div class="res-btns" style="margin-top:1.2rem;"> + <button class="btn" onclick="restartInlineQuiz()">refazer</button> + <a class="btn" href="{{ $quizPage.RelPermalink }}">modo completo</a> + </div> + </div> + </div> + <script> + (function() { + const RAW = {{ $quizPage.RawContent | jsonify | safeJS }}; + // Remove frontmatter YAML e notas de rodapé + const body = RAW.replace(/^---[\s\S]*?---\n/, '').replace(/\n\[\^\d+\]:[^\n]*/g, ''); + window._IQ = { raw: body, questions: [], idx: 0, acertos: 0, erros: 0, answered: false, flipped: false }; + })(); + + function startInlineQuiz() { + document.getElementById('btn-start-inline').style.display = 'none'; + const qs = prepareQuizQuestions(parseQuizBody(window._IQ.raw)); + window._IQ.questions = qs; + window._IQ.idx = 0; window._IQ.acertos = 0; window._IQ.erros = 0; + document.getElementById('inline-quiz-player').style.display = ''; + _iqRender(); + } + + function _iqRender() { + const q = window._IQ.questions[window._IQ.idx]; + if (!q) return; + window._IQ.answered = false; window._IQ.flipped = false; + const total = window._IQ.questions.length; + document.getElementById('iq-counter').textContent = (window._IQ.idx + 1) + ' de ' + total; + document.getElementById('iprog').style.width = ((window._IQ.idx + 1) / total * 100) + '%'; + document.getElementById('ibtn-next').style.display = 'none'; + const body = document.getElementById('iq-body'); + if (q.type === 'mc') { + body.innerHTML = '<div class="mc-area fade"><p class="q-text">' + md(q.text) + '</p><div class="options">' + + q.options.map((o, i) => '<button class="opt" data-opt="' + esc(o) + '" data-answer="' + esc(q.answer) + '" onclick="iqSelectOpt(this)" aria-label="Opção ' + LETRAS[i] + ': ' + o + '"><span class="opt-letter">' + LETRAS[i] + '</span><span>' + md(o) + '</span></button>').join('') + + '</div><div class="feedback-line" id="iq-feedback" aria-live="polite"><span class="feedback-dot"></span><span id="iq-feedback-msg"></span></div>' + + (q.comment ? '<div class="comment-block" id="iq-comment"><div class="comment-label">comentário</div><div>' + md(q.comment) + '</div></div>' : '') + + '</div>'; + } else { + body.innerHTML = '<div class="flashcard-area fade"><p class="q-text">' + md(q.text) + '</p>' + + '<div class="flashcard-simple" id="iq-flashcard"><button class="btn btn-reveal" onclick="iqFlip()">Revelar Resposta</button></div>' + + '<div class="feedback-line" id="iq-feedback" aria-live="polite"><span class="feedback-dot"></span><span id="iq-feedback-msg"></span></div>' + + (q.comment ? '<div class="comment-block" id="iq-comment"><div class="comment-label">comentário</div><div>' + md(q.comment) + '</div></div>' : '') + + '<div class="flashcard-btns" id="iq-fc-btns"><button class="btn btn-ok" onclick="iqMarkOk()">✓ Acertei</button><button class="btn btn-bad" onclick="iqMarkBad()">✗ Errei</button></div>' + + '</div>'; + } + } + + function iqSelectOpt(btn) { + if (window._IQ.answered) return; + window._IQ.answered = true; + const chosen = btn.dataset.opt, correct = btn.dataset.answer; + btn.closest('.options').querySelectorAll('.opt').forEach(b => { + b.disabled = true; + if (b.dataset.opt === correct) b.classList.add('correct'); + else if (b === btn) b.classList.add('wrong'); + else b.classList.add('revealed'); + }); + const ok = chosen === correct; + ok ? window._IQ.acertos++ : window._IQ.erros++; + _iqFeedback(ok, ok ? 'Correto' : 'Incorreto — a correta está destacada'); + document.getElementById('iq-comment')?.classList.add('show'); + document.getElementById('ibtn-next').style.display = ''; + } + + function iqFlip() { + const q = window._IQ.questions[window._IQ.idx]; + const card = document.getElementById('iq-flashcard'); + if (card) card.innerHTML = '<div class="ref-label">resposta de referência</div><div class="ref-content">' + md(q.reference) + '</div>'; + document.getElementById('iq-fc-btns')?.style && (document.getElementById('iq-fc-btns').style.display = ''); + } + + function iqMarkOk() { _iqMark(true); } + function iqMarkBad() { _iqMark(false); } + function _iqMark(ok) { + if (window._IQ.answered) return; + window._IQ.answered = true; + ok ? window._IQ.acertos++ : window._IQ.erros++; + _iqFeedback(ok, ok ? 'Marcado como acertado' : 'Marcado como errado'); + document.getElementById('iq-comment')?.classList.add('show'); + const btns = document.getElementById('iq-fc-btns'); + if (btns) btns.innerHTML = ''; + document.getElementById('ibtn-next').style.display = ''; + } + + function _iqFeedback(ok, msg) { + const fb = document.getElementById('iq-feedback'); + if (fb) { fb.className = 'feedback-line show ' + (ok ? 'ok' : 'bad'); } + const fm = document.getElementById('iq-feedback-msg'); + if (fm) fm.textContent = msg; + } + + function inlineNextQuestion() { + window._IQ.idx++; + if (window._IQ.idx >= window._IQ.questions.length) { + _iqShowResult(); + } else { + _iqRender(); + } + } + + function _iqShowResult() { + document.getElementById('inline-quiz-player').style.display = 'none'; + document.getElementById('inline-quiz-result').style.display = ''; + const { acertos, erros, questions } = window._IQ; + const total = questions.length; + const pct = total ? Math.round(acertos / total * 100) : 0; + document.getElementById('ires-pct').textContent = pct + '%'; + document.getElementById('ires-a').textContent = acertos; + document.getElementById('ires-t').textContent = total; + let nivel = pct >= 90 ? 'Excelente' : pct >= 70 ? 'Bom' : pct >= 50 ? 'Regular' : 'Insuficiente'; + let det = pct >= 90 ? 'Domínio sólido deste assunto.' : pct >= 70 ? 'Boa base, revise os erros.' : pct >= 50 ? 'Lacunas importantes, releia o texto.' : 'Retome o conteúdo antes de prosseguir.'; + document.getElementById('ires-msg').innerHTML = '<strong>' + nivel + '</strong> — ' + acertos + ' de ' + total + ' (' + pct + '%). ' + det; + } + + function restartInlineQuiz() { + document.getElementById('inline-quiz-result').style.display = 'none'; + window._IQ.idx = 0; window._IQ.acertos = 0; window._IQ.erros = 0; + window._IQ.questions = prepareQuizQuestions(parseQuizBody(window._IQ.raw)); + document.getElementById('inline-quiz-player').style.display = ''; + _iqRender(); + } + </script> + {{ end }} + + {{ if .Params.references }} + <div class="references"> + <div class="ref-head">referências</div> + <div class="ref-list"> + {{ range .Params.references }}<p>{{ . }}</p>{{ end }} + </div> + </div> + {{ end }} +</div> + +{{ end }} |
