Code
Recall
Flashcards from your CareerFoundry projects
0
to study
0
learning
0
mastered
Progress by course section
mastered (box 4+)
learning
Progress saves on this phone and works offline
s && s.trim()); let q = '', a = '', code = '', mc = null, mode = null; for (const line of lines.slice(1)) { if (line.startsWith('```')) { mode = mode === 'code' ? 'a' : 'code'; continue; } if (mode === 'code') { code += line + '\n'; continue; } if (line.startsWith('M: ')) { mc = line.slice(3).split(' | ').map(s => s.trim()); mode = null; continue; } if (line.startsWith('Q: ')) { q = line.slice(3); mode = 'q'; continue; } if (line.startsWith('A: ')) { a = line.slice(3); mode = 'a'; continue; } if (line.trim() && mode === 'q') q += ' ' + line.trim(); else if (line.trim() && mode === 'a') a += ' ' + line.trim(); } if (q) CARDS.push({ id: hash(deck + q), deck, src: src || '', q, a, code: code.replace(/\n+$/, ''), mc }); } const DECKS = [...new Set(CARDS.map(c => c.deck))]; // ---- progress (Leitner boxes) ---- const DAY = 864e5; const INTERVAL = [0, 1, 3, 7, 14, 30, 60]; // days until next review, by box const NEW_PER_SESSION = 15; const LS_KEY = 'code-recall-progress'; let progress = {}; try { progress = JSON.parse(localStorage.getItem(LS_KEY) || '{}') || {}; } catch (e) { progress = {}; } function save() { try { localStorage.setItem(LS_KEY, JSON.stringify(progress)); } catch (e) {} } // ---- session ---- let deckFilter = 'All'; try { deckFilter = localStorage.getItem('code-recall-deck') || 'All'; } catch (e) {} if (deckFilter !== 'All' && !DECKS.includes(deckFilter)) deckFilter = 'All'; let queue = [], current = null, revealed = false, sessionDone = 0; let studyMode = 'choice', opts = null, picked = null; try { studyMode = localStorage.getItem('code-recall-mode') || 'choice'; } catch (e) {} function choiceMode(c) { return studyMode === 'choice' && c && c.mc && c.mc.length > 1; } function pick(i) { if (revealed || !opts) return; picked = i; revealed = true; render(); } function inDeck(c) { return deckFilter === 'All' || c.deck === deckFilter; } function shuffle(a) { for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } function buildQueue() { const now = Date.now(); const due = CARDS.filter(c => inDeck(c) && progress[c.id] && progress[c.id].d <= now); const fresh = CARDS.filter(c => inDeck(c) && !progress[c.id]).slice(0, NEW_PER_SESSION); queue = shuffle(due).concat(fresh); if (current && !queue.includes(current)) current = null; if (!current) { current = queue[0] || null; revealed = false; } } function grade(knew) { const c = current; if (!c) return; const p = progress[c.id] || { b: 0 }; p.b = knew ? Math.min(p.b + 1, INTERVAL.length - 1) : 0; p.d = Date.now() + (knew ? INTERVAL[p.b] * DAY : 0); p.t = Date.now(); progress[c.id] = p; queue.shift(); if (!knew) queue.splice(Math.min(3, queue.length), 0, c); // see it again in a few cards sessionDone++; current = queue[0] || null; revealed = false; opts = null; picked = null; save(); render(); } // ---- render ---- const $ = id => document.getElementById(id); function el(tag, attrs, ...kids) { const n = document.createElement(tag); for (const [k, v] of Object.entries(attrs || {})) { if (k === 'class') n.className = v; else if (k.startsWith('on')) n.addEventListener(k.slice(2), v); else n.setAttribute(k, v); } for (const k of kids) if (k != null) n.append(k); return n; } function stats(list) { const now = Date.now(); let due = 0, learn = 0, master = 0, fresh = 0; for (const c of list) { const p = progress[c.id]; if (!p) { fresh++; continue; } if (p.b >= 4) master++; else learn++; if (p.d <= now) due++; } return { due, learn, master, fresh }; } function render() { const all = stats(CARDS); const scoped = stats(CARDS.filter(inDeck)); $('s-due').textContent = scoped.due + Math.min(scoped.fresh, NEW_PER_SESSION); $('s-learn').textContent = all.learn; $('s-master').textContent = all.master + '/' + CARDS.length; const modes = $('modes'); modes.replaceChildren(); for (const [m, label] of [['choice', 'Multiple choice'], ['flip', 'Flip cards']]) { modes.append(el('button', { class: 'mode-btn', 'aria-pressed': String(studyMode === m), onclick: () => { if (studyMode === m) return; studyMode = m; try { localStorage.setItem('code-recall-mode', m); } catch (e) {} revealed = false; opts = null; picked = null; render(); } }, label)); } const decks = $('decks'); decks.replaceChildren(); for (const d of ['All', ...DECKS]) { const n = d === 'All' ? CARDS.length : CARDS.filter(c => c.deck === d).length; decks.append(el('button', { class: 'deck-btn', 'aria-pressed': String(d === deckFilter), onclick: () => { deckFilter = d; try { localStorage.setItem('code-recall-deck', d); } catch (e) {} current = null; buildQueue(); render(); } }, d, el('span', { class: 'n' }, String(n)))); } const study = $('study'); study.replaceChildren(); if (!current) { const next = CARDS.filter(inDeck).map(c => progress[c.id]?.d).filter(Boolean).sort((a, b) => a - b)[0]; const when = next ? new Date(next).toLocaleDateString(undefined, { weekday: 'long', month: 'short', day: 'numeric' }) : null; study.append(el('div', { class: 'done' }, el('h2', {}, sessionDone ? 'Session complete' : 'Nothing due here'), el('p', {}, (sessionDone ? `You reviewed ${sessionDone} cards. ` : '') + (when ? `The next card in this deck is due ${when}.` : 'Pick another deck above.')), el('button', { class: 'link-btn', onclick: () => { for (const c of CARDS.filter(inDeck)) if (progress[c.id]) progress[c.id].d = Date.now(); save(); buildQueue(); render(); } }, 'Review this deck again now') )); } else { const c = current, box = progress[c.id]?.b || 0; const pips = el('div', { class: 'box-pips', title: 'Box ' + box + ' of 6', 'aria-label': 'Mastery ' + box + ' of 6' }); for (let i = 1; i < INTERVAL.length; i++) pips.append(el('i', { class: i <= box ? 'on' : '' })); const card = el('article', { class: 'card' + (revealed ? ' flip' : '') }, el('div', { class: 'card-top' }, el('span', { class: 'deck-label' }, c.deck), pips), el('p', { class: 'q' }, c.q)); const isChoice = choiceMode(c); if (isChoice) { if (!opts || opts.id !== c.id) opts = { id: c.id, order: shuffle(c.mc.map((_, i) => i)) }; const list = el('div', { class: 'opts', role: 'group', 'aria-label': 'Answer choices' }); opts.order.forEach((oi, pos) => { let cls = 'opt'; if (revealed && oi === 0) cls += ' right'; else if (revealed && pos === picked) cls += ' wrong'; const b = el('button', { class: cls, onclick: () => pick(pos) }, el('span', { class: 'letter' }, 'ABCD'[pos]), el('span', {}, c.mc[oi])); if (revealed) b.disabled = true; list.append(b); }); card.append(list); } if (revealed) { const ans = el('div', { class: 'a' }, el('p', {}, c.a)); if (isChoice) ans.prepend(el('p', { class: 'verdict ' + (opts.order[picked] === 0 ? 'yes' : 'no') }, opts.order[picked] === 0 ? 'Correct' : 'Not quite. The right answer is highlighted.')); if (c.code) ans.append(el('pre', {}, el('code', {}, c.code))); card.append(ans); } if (c.src) card.append(el('div', { class: 'src' }, c.src)); const actions = el('div', { class: 'actions' }); if (isChoice) { if (revealed) actions.append(el('button', { class: 'big reveal', id: 'next', onclick: () => grade(opts.order[picked] === 0) }, 'Next card')); } else if (!revealed) actions.append(el('button', { class: 'big reveal', id: 'reveal', onclick: () => { revealed = true; render(); } }, 'Show answer')); else actions.append( el('button', { class: 'big miss', id: 'miss', onclick: () => grade(false) }, 'Missed it'), el('button', { class: 'big good', id: 'good', onclick: () => grade(true) }, 'Knew it')); study.append(card, actions, el('p', { class: 'kbd' }, `${queue.length} left in this session · ${isChoice ? 'Keyboard: 1\u20134 to answer, Enter for next' : 'Keyboard: space to flip, 1 missed, 2 knew it'}`)); } const shelf = $('shelf'); shelf.replaceChildren(); for (const d of DECKS) { const list = CARDS.filter(c => c.deck === d), s = stats(list), n = list.length; shelf.append(el('div', { class: 'row' }, el('span', {}, d), el('div', { class: 'bar' }, el('span', { class: 'm', style: `width:${100 * s.master / n}%` }), el('span', { class: 'l', style: `width:${100 * s.learn / n}%` })), el('span', { class: 'pct' }, Math.round(100 * s.master / n) + '%'))); } } document.addEventListener('keydown', e => { if (!current || e.metaKey || e.ctrlKey || e.target.closest('input,textarea')) return; if (choiceMode(current)) { if (!revealed && '1234'.includes(e.key) && e.key) pick(+e.key - 1); else if (revealed && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); grade(opts.order[picked] === 0); } return; } if (!revealed && (e.key === ' ' || e.key === 'Enter')) { e.preventDefault(); revealed = true; render(); } else if (revealed && e.key === '1') grade(false); else if (revealed && e.key === '2') grade(true); }); buildQueue(); render(); })();