// Страница повторения колоды: карточка за карточкой по случайной выборке. // Лицевая сторона — поля из deck.forward, по тапу дорисовывается deck.backward. // Внизу 4 кнопки самооценки: "не получилось" / "тяжело" / "хорошо" / "легко". // Пока без записи ответов: только навигация по очереди (история — следующий шаг). import { getCustomFields, getDeck, getWordsByIds, renderCardSide, sampleDeckWordIds, wordFieldValues, } from './db-types.ts'; import type { CustomField, Word } from './db-types.ts'; import { hasDatabaseFile } from './opfs.ts'; export type ReviewNav = { goDecks: () => void; }; function escapeHtml(s: string): string { return s .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } const GRADES = [ { label: '😞 Не получилось', cls: 'grade-fail' }, { label: '😟 Тяжело', cls: 'grade-hard' }, { label: '🙂 Хорошо', cls: 'grade-good' }, { label: '🤩 Легко', cls: 'grade-easy' }, ] as const; export function renderReviewPage(root: HTMLElement, deckId: number, nav: ReviewNav): void { root.innerHTML = `

Повторение…

Загрузка карточек…
`; const backBtn = root.querySelector('#review-back')!; const titleEl = root.querySelector('#review-title')!; const progressEl = root.querySelector('#review-progress')!; const statusEl = root.querySelector('#review-status')!; const bodyEl = root.querySelector('#review-body')!; backBtn.addEventListener('click', () => nav.goDecks()); function setStatus(msg: string): void { statusEl.textContent = msg; statusEl.hidden = msg === ''; } // Очередь сессии: перетасованные id без повторений + подгруженные слова. let queue: number[] = []; let words = new Map(); let fields: CustomField[] = []; let forwardTpl = ''; let backwardTpl = ''; let pos = 0; let revealed = false; function sideHtml(values: string[], cls: string): string { if (values.length === 0) { return `
—
`; } return `
` + values.map((v) => `

${escapeHtml(v)}

`).join('') + `
`; } function paint(): void { const w = words.get(queue[pos]!); if (!w) { bodyEl.innerHTML = ''; progressEl.textContent = ''; setStatus('Слово пропало из базы — пропускаю.'); return; } const values = wordFieldValues(w, fields); const front = renderCardSide(forwardTpl, values); const back = revealed ? renderCardSide(backwardTpl, values) : null; progressEl.textContent = `Карточка ${pos + 1} из ${queue.length}`; setStatus(''); const backHtml = !revealed ? `

Тап — показать ответ

` : sideHtml(back ?? [], 'review-back-side'); // Оценки — только после открытия (вместе с backward), до этого лишь тап по карточке. const gradesHtml = revealed ? `
${GRADES.map((g) => ``).join('')}
` : ''; bodyEl.innerHTML = ` ${gradesHtml} `; bodyEl.querySelector('#review-card')!.addEventListener('click', () => { if (!revealed) { revealed = true; paint(); } }); bodyEl.querySelectorAll('.review-grade').forEach((btn) => { btn.addEventListener('click', () => { // Запись ответов (history/states) — следующим шагом, пока просто дальше. pos += 1; revealed = false; if (pos >= queue.length) paintDone(); else paint(); }); }); } function paintDone(): void { progressEl.textContent = `Готово: ${queue.length} из ${queue.length}`; setStatus(''); bodyEl.innerHTML = `

Все карточки сессии просмотрены. Запись оценок — следующим шагом.

`; bodyEl.querySelector('#review-again')!.addEventListener('click', () => { void reshuffle(); }); } async function reshuffle(): Promise { setStatus('Перемешиваю…'); bodyEl.innerHTML = ''; progressEl.textContent = ''; queue = await sampleDeckWordIds(deckId); pos = 0; revealed = false; if (queue.length === 0) { setStatus('В колоде нет слов для повторения.'); return; } words = await getWordsByIds(queue); paint(); } void (async () => { const has = await hasDatabaseFile(); if (!has) { titleEl.textContent = 'Повторение недоступно'; setStatus('Нет локальной базы — скачай сохранение на странице «Синхронизация».'); return; } try { const [deck, customFields] = await Promise.all([getDeck(deckId), getCustomFields()]); if (!deck) { titleEl.textContent = 'Колода не найдена'; setStatus(`Колоды с id=${deckId} нет в локальной базе. Вернись к списку.`); return; } fields = customFields; forwardTpl = deck.forward; backwardTpl = deck.backward; titleEl.textContent = deck.name || `Колода #${deck.id}`; queue = await sampleDeckWordIds(deck.id); if (queue.length === 0) { progressEl.textContent = ''; setStatus('В колоде нет слов для повторения.'); return; } // Слова грузим одним батчем (чанки внутри getWordsByIds), очередь уже перемешана. words = await getWordsByIds(queue); pos = 0; revealed = false; paint(); } catch (err) { titleEl.textContent = 'Повторение недоступно'; setStatus(`Не удалось загрузить карточки: ${err instanceof Error ? err.message : String(err)}`); } })(); }