159 lines
5.3 KiB
JavaScript
159 lines
5.3 KiB
JavaScript
// Shared quiz engine for lessons. Two question types, both graded client-side, no server.
|
|
//
|
|
// Recall (free-recall self-check — best for storage strength):
|
|
// <div class="q" data-type="recall" data-topic="Ownership">
|
|
// <p class="topic">Ownership</p>
|
|
// <p class="prompt">...question...</p>
|
|
// <button class="reveal-btn">Show answer</button>
|
|
// <div class="answer hidden">...answer text...</div>
|
|
// <div class="grade hidden">
|
|
// <button data-grade="hit">Got it</button>
|
|
// <button data-grade="miss">Missed it</button>
|
|
// </div>
|
|
// </div>
|
|
//
|
|
// Multiple choice (instant feedback):
|
|
// <div class="q" data-type="mcq" data-topic="Enums">
|
|
// <p class="topic">Enums</p>
|
|
// <p class="prompt">...question...</p>
|
|
// <div class="options">
|
|
// <button class="opt" data-correct="true">...</button>
|
|
// <button class="opt" data-correct="false">...</button>
|
|
// </div>
|
|
// <div class="explain hidden">...explanation...</div>
|
|
// </div>
|
|
//
|
|
// Bottom of page needs:
|
|
// <div id="summary"><div id="summary-body"></div><p id="summary-total"></p>
|
|
// <button id="report-btn">Copy report</button><pre id="report-output" class="hidden"></pre></div>
|
|
|
|
(function () {
|
|
function all(sel, ctx) {
|
|
return Array.from((ctx || document).querySelectorAll(sel));
|
|
}
|
|
|
|
function initRecall(q) {
|
|
var revealBtn = q.querySelector(".reveal-btn");
|
|
var answer = q.querySelector(".answer");
|
|
var grade = q.querySelector(".grade");
|
|
if (!revealBtn || !answer || !grade) return;
|
|
revealBtn.addEventListener("click", function () {
|
|
answer.classList.remove("hidden");
|
|
grade.classList.remove("hidden");
|
|
revealBtn.classList.add("hidden");
|
|
});
|
|
all("button", grade).forEach(function (btn) {
|
|
btn.addEventListener("click", function () {
|
|
q.dataset.result = btn.dataset.grade === "hit" ? "hit" : "miss";
|
|
q.classList.add("graded");
|
|
all("button", grade).forEach(function (b) {
|
|
b.disabled = true;
|
|
});
|
|
btn.classList.add(btn.dataset.grade === "hit" ? "opt-correct" : "opt-incorrect");
|
|
updateSummary();
|
|
});
|
|
});
|
|
}
|
|
|
|
function initMcq(q) {
|
|
var opts = all(".opt", q);
|
|
var explain = q.querySelector(".explain");
|
|
opts.forEach(function (opt) {
|
|
opt.addEventListener("click", function () {
|
|
if (q.classList.contains("graded")) return;
|
|
var correct = opt.dataset.correct === "true";
|
|
q.classList.add("graded");
|
|
q.dataset.result = correct ? "hit" : "miss";
|
|
opts.forEach(function (o) {
|
|
o.disabled = true;
|
|
if (o.dataset.correct === "true") o.classList.add("correct");
|
|
});
|
|
if (!correct) opt.classList.add("incorrect");
|
|
if (explain) explain.classList.remove("hidden");
|
|
updateSummary();
|
|
});
|
|
});
|
|
}
|
|
|
|
function byTopic() {
|
|
var map = {};
|
|
all(".q").forEach(function (q) {
|
|
var topic = q.dataset.topic || "General";
|
|
map[topic] = map[topic] || { hit: 0, graded: 0, total: 0 };
|
|
map[topic].total++;
|
|
if (q.dataset.result) {
|
|
map[topic].graded++;
|
|
if (q.dataset.result === "hit") map[topic].hit++;
|
|
}
|
|
});
|
|
return map;
|
|
}
|
|
|
|
function updateSummary() {
|
|
var body = document.getElementById("summary-body");
|
|
var total = document.getElementById("summary-total");
|
|
var reportBtn = document.getElementById("report-btn");
|
|
if (!body) return;
|
|
var map = byTopic();
|
|
var lines = [];
|
|
var sumHit = 0,
|
|
sumGraded = 0,
|
|
sumAll = 0;
|
|
Object.keys(map).forEach(function (topic) {
|
|
var t = map[topic];
|
|
sumHit += t.hit;
|
|
sumGraded += t.graded;
|
|
sumAll += t.total;
|
|
var pct = t.graded ? Math.round((100 * t.hit) / t.graded) : null;
|
|
lines.push(
|
|
topic +
|
|
": " +
|
|
t.hit +
|
|
"/" +
|
|
t.graded +
|
|
(t.graded < t.total ? " (of " + t.total + ")" : "") +
|
|
(pct === null ? "" : " — " + pct + "%")
|
|
);
|
|
});
|
|
body.textContent = lines.join("\n");
|
|
if (total) total.textContent = sumGraded + " of " + sumAll + " answered, " + sumHit + " correct.";
|
|
if (reportBtn) reportBtn.disabled = sumGraded === 0;
|
|
}
|
|
|
|
function buildReport() {
|
|
var map = byTopic();
|
|
var weak = [];
|
|
var solid = [];
|
|
Object.keys(map).forEach(function (topic) {
|
|
var t = map[topic];
|
|
var entry = topic + " (" + t.hit + "/" + t.total + ")";
|
|
if (t.hit < t.total) weak.push(entry);
|
|
else solid.push(entry);
|
|
});
|
|
var lines = ["Rust diagnostic result:"];
|
|
lines.push(weak.length ? "Weak: " + weak.join(", ") : "Weak: none");
|
|
lines.push(solid.length ? "Solid: " + solid.join(", ") : "Solid: none");
|
|
return lines.join("\n");
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
all(".q").forEach(function (q) {
|
|
if (q.dataset.type === "recall") initRecall(q);
|
|
if (q.dataset.type === "mcq") initMcq(q);
|
|
});
|
|
updateSummary();
|
|
var reportBtn = document.getElementById("report-btn");
|
|
if (reportBtn) {
|
|
reportBtn.addEventListener("click", function () {
|
|
var report = buildReport();
|
|
var out = document.getElementById("report-output");
|
|
if (out) {
|
|
out.textContent = report;
|
|
out.classList.remove("hidden");
|
|
}
|
|
if (navigator.clipboard) navigator.clipboard.writeText(report).catch(function () {});
|
|
});
|
|
}
|
|
});
|
|
})();
|