// Страница повторения колоды: карточка за карточкой по случайной выборке.
// Лицевая сторона — поля из 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)}`);
}
})();
}