|
27 | 27 | const KNOWN_BANKS = [ |
28 | 28 | { cert: "data-engineer-associate", file: "data/data-engineer-associate.json" }, |
29 | 29 | { cert: "data-engineer-professional", file: "data/data-engineer-professional.json" }, |
| 30 | + { cert: "data-analyst-associate", file: "data/data-analyst-associate.json" }, |
30 | 31 | { cert: "ml-associate", file: "data/ml-associate.json" }, |
| 32 | + { cert: "ml-professional", file: "data/ml-professional.json" }, |
| 33 | + { cert: "genai-engineer-associate", file: "data/genai-engineer-associate.json" }, |
31 | 34 | ]; |
32 | 35 |
|
33 | 36 | const STORAGE_PREFIX = "dbx-practice-"; |
| 37 | + const THEME_KEY = "dbx-practice-theme"; |
| 38 | + const THEMES = ["auto", "light", "dark"]; |
| 39 | + const THEME_ICONS = { auto: "🖥️", light: "☀️", dark: "🌙" }; |
34 | 40 | const STATE = { |
35 | 41 | bank: null, |
36 | 42 | history: {}, |
|
111 | 117 | } |
112 | 118 | } |
113 | 119 |
|
| 120 | + const FENCE_OPEN_RE = /^\s*```(\w*)\s*$/; |
| 121 | + const FENCE_CLOSE_RE = /^\s*```\s*$/; |
| 122 | + |
114 | 123 | function renderMarkdown(s) { |
115 | 124 | const frag = document.createDocumentFragment(); |
116 | | - const paragraphs = s.split(/\n\s*\n/); |
117 | | - for (const para of paragraphs) { |
118 | | - const trimmed = para.trim(); |
119 | | - if (!trimmed) continue; |
120 | | - const p = el("p"); |
121 | | - const lines = trimmed.split("\n"); |
122 | | - for (let i = 0; i < lines.length; i++) { |
123 | | - renderInline(lines[i], p); |
124 | | - if (i < lines.length - 1) p.appendChild(el("br")); |
| 125 | + const lines = s.split("\n"); |
| 126 | + let buffer = []; |
| 127 | + |
| 128 | + const flushParagraphs = () => { |
| 129 | + // `buffer` is a slab of lines; split it into paragraphs by blank lines |
| 130 | + // and render each as a <p> with inline formatting + <br> for newlines. |
| 131 | + let para = []; |
| 132 | + const emit = () => { |
| 133 | + if (para.length === 0) return; |
| 134 | + const p = el("p"); |
| 135 | + for (let j = 0; j < para.length; j++) { |
| 136 | + renderInline(para[j], p); |
| 137 | + if (j < para.length - 1) p.appendChild(el("br")); |
| 138 | + } |
| 139 | + frag.appendChild(p); |
| 140 | + para = []; |
| 141 | + }; |
| 142 | + for (const ln of buffer) { |
| 143 | + if (ln.trim() === "") emit(); |
| 144 | + else para.push(ln); |
| 145 | + } |
| 146 | + emit(); |
| 147 | + buffer = []; |
| 148 | + }; |
| 149 | + |
| 150 | + let i = 0; |
| 151 | + while (i < lines.length) { |
| 152 | + const fence = lines[i].match(FENCE_OPEN_RE); |
| 153 | + if (fence) { |
| 154 | + flushParagraphs(); |
| 155 | + const codeLines = []; |
| 156 | + i++; |
| 157 | + while (i < lines.length && !FENCE_CLOSE_RE.test(lines[i])) { |
| 158 | + codeLines.push(lines[i]); |
| 159 | + i++; |
| 160 | + } |
| 161 | + // Skip the closing fence (if present) |
| 162 | + if (i < lines.length) i++; |
| 163 | + const code = el("code"); |
| 164 | + code.textContent = codeLines.join("\n"); |
| 165 | + const pre = el("pre"); |
| 166 | + pre.appendChild(code); |
| 167 | + frag.appendChild(pre); |
| 168 | + continue; |
125 | 169 | } |
126 | | - frag.appendChild(p); |
| 170 | + buffer.push(lines[i]); |
| 171 | + i++; |
127 | 172 | } |
| 173 | + flushParagraphs(); |
128 | 174 | return frag; |
129 | 175 | } |
130 | 176 |
|
|
293 | 339 | diff.textContent = q.difficulty; |
294 | 340 | diff.className = "difficulty " + q.difficulty; |
295 | 341 |
|
296 | | - $("#quiz-title").textContent = q.title; |
| 342 | + // Note: question title is intentionally NOT rendered on the quiz card — |
| 343 | + // many titles paraphrase the answer and would give it away. The title is |
| 344 | + // still used in Stats → "Questions you're working on" and in the feedback |
| 345 | + // panel shown after the user submits. |
297 | 346 |
|
298 | 347 | const qBody = $("#quiz-question"); |
299 | 348 | clear(qBody); |
|
362 | 411 | clear(fb); |
363 | 412 | fb.appendChild(el("h4", {}, |
364 | 413 | correct ? "✓ Correct" : `✗ Incorrect — correct answer: ${q.correctAnswer}`)); |
| 414 | + if (q.title) { |
| 415 | + fb.appendChild(el("p", { className: "fb-topic" }, |
| 416 | + el("strong", {}, "Topic: "), q.title)); |
| 417 | + } |
365 | 418 | if (q.shortAnswer) { |
366 | 419 | const p = el("p"); |
367 | 420 | p.appendChild(renderInlineToFragment(q.shortAnswer)); |
|
492 | 545 | show("quiz"); |
493 | 546 | } |
494 | 547 |
|
| 548 | + // --- Theme toggle -------------------------------------------------------- |
| 549 | + |
| 550 | + function loadTheme() { |
| 551 | + try { |
| 552 | + const t = localStorage.getItem(THEME_KEY); |
| 553 | + return THEMES.includes(t) ? t : "auto"; |
| 554 | + } catch (_) { return "auto"; } |
| 555 | + } |
| 556 | + |
| 557 | + function applyTheme(theme) { |
| 558 | + document.documentElement.setAttribute("data-theme", theme); |
| 559 | + const iconNode = $("#btn-theme-icon"); |
| 560 | + if (iconNode) iconNode.textContent = THEME_ICONS[theme]; |
| 561 | + } |
| 562 | + |
| 563 | + function cycleTheme() { |
| 564 | + const current = loadTheme(); |
| 565 | + const next = THEMES[(THEMES.indexOf(current) + 1) % THEMES.length]; |
| 566 | + try { localStorage.setItem(THEME_KEY, next); } catch (_) { /* ignore */ } |
| 567 | + applyTheme(next); |
| 568 | + } |
| 569 | + |
495 | 570 | // --- Init ---------------------------------------------------------------- |
496 | 571 |
|
497 | 572 | function init() { |
| 573 | + // Apply persisted theme before anything renders so there's no flash |
| 574 | + applyTheme(loadTheme()); |
| 575 | + |
498 | 576 | $("#btn-submit").addEventListener("click", submitAnswer); |
499 | 577 | $("#btn-next").addEventListener("click", renderQuiz); |
500 | 578 | $("#btn-stats").addEventListener("click", () => { renderStats(); show("stats"); }); |
501 | 579 | $("#btn-stats-back").addEventListener("click", () => show("quiz")); |
502 | 580 | $("#btn-export").addEventListener("click", exportProgress); |
503 | 581 | $("#btn-reset").addEventListener("click", resetHistory); |
| 582 | + $("#btn-reset-top").addEventListener("click", resetHistory); |
504 | 583 | $("#btn-settings").addEventListener("click", () => show("settings")); |
505 | 584 | $("#btn-settings-cancel").addEventListener("click", () => show("quiz")); |
506 | 585 | $("#btn-settings-apply").addEventListener("click", applySettings); |
507 | 586 | $("#btn-exit").addEventListener("click", () => location.reload()); |
| 587 | + $("#btn-theme").addEventListener("click", cycleTheme); |
508 | 588 |
|
509 | 589 | probeBanks().then(renderSetup); |
510 | 590 | } |
|
0 commit comments