Rust learning: lessons, notes, and exercise crates
This commit is contained in:
+158
@@ -0,0 +1,158 @@
|
||||
// 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 () {});
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user