98 lines
3.9 KiB
TypeScript
98 lines
3.9 KiB
TypeScript
// Страница «Колоды»: плоский список — только название + количество states.
|
|
// Раскрытия по нажатию нет: тап переходит на страницу колоды, где все данные.
|
|
|
|
import { getDecks, getDeckStateCounts } from './db-types.ts';
|
|
import type { Deck } from './db-types.ts';
|
|
import { hasDatabaseFile } from './opfs.ts';
|
|
|
|
export type DecksNav = {
|
|
goDeck: (deckId: number) => void;
|
|
goReview: (deckId: number) => void;
|
|
};
|
|
|
|
function escapeHtml(s: string): string {
|
|
return s
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function plural(n: number): string {
|
|
const m10 = n % 10;
|
|
const m100 = n % 100;
|
|
if (m10 === 1 && m100 !== 11) return 'запись';
|
|
if (m10 >= 2 && m10 <= 4 && (m100 < 12 || m100 > 14)) return 'записи';
|
|
return 'записей';
|
|
}
|
|
|
|
// Одна колода — плоская строка: имя + количество слева, «▶» справа.
|
|
// Тап по имени — на страницу колоды, тап по «▶» — сразу в повторение.
|
|
function deckRowHtml(d: Deck, count: number): string {
|
|
const name = escapeHtml(d.name) || `<span class="empty-val">Колода #${d.id}</span>`;
|
|
return `<div class="word-row-wrap deck-row" data-deck-id="${d.id}">`
|
|
+ `<button class="word-row deck-name" type="button" data-act="open" data-deck-id="${d.id}">`
|
|
+ `<span class="word-main"><span class="word-key">${name}</span></span>`
|
|
+ `<span class="word-tag">${count} ${plural(count)}</span>`
|
|
+ `</button>`
|
|
+ `<button class="deck-play" type="button" data-act="review" data-deck-id="${d.id}" aria-label="Повторять: ${escapeHtml(d.name) || `колода ${d.id}`}">▶</button>`
|
|
+ `</div>`;
|
|
}
|
|
|
|
export function renderDecksPage(root: HTMLElement, nav: DecksNav): void {
|
|
root.innerHTML = `
|
|
<section class="card" aria-labelledby="decks-title">
|
|
<h2 id="decks-title" class="card-title">🗂️ Колоды</h2>
|
|
<p id="decks-count" class="card-hint" role="status" aria-live="polite"></p>
|
|
<div id="decks-status" class="status-bar" role="status">Проверка локальной базы…</div>
|
|
<div id="decks-list" class="words-list" aria-live="polite"></div>
|
|
</section>
|
|
`;
|
|
|
|
const countEl = root.querySelector<HTMLElement>('#decks-count')!;
|
|
const statusEl = root.querySelector<HTMLElement>('#decks-status')!;
|
|
const listEl = root.querySelector<HTMLElement>('#decks-list')!;
|
|
|
|
function setStatus(msg: string): void {
|
|
statusEl.textContent = msg;
|
|
statusEl.hidden = msg === '';
|
|
}
|
|
|
|
listEl.addEventListener('click', (e) => {
|
|
const btn = (e.target as HTMLElement).closest<HTMLButtonElement>('[data-act]');
|
|
if (!btn) return;
|
|
const id = Number(btn.dataset.deckId);
|
|
if (!Number.isFinite(id)) return;
|
|
if (btn.dataset.act === 'review') nav.goReview(id);
|
|
else nav.goDeck(id);
|
|
});
|
|
|
|
void (async () => {
|
|
const has = await hasDatabaseFile();
|
|
if (!has) {
|
|
setStatus('Нет локальной базы — скачай сохранение на странице «Синхронизация».');
|
|
countEl.textContent = '';
|
|
return;
|
|
}
|
|
setStatus('Загрузка колод…');
|
|
try {
|
|
const [decks, counts] = await Promise.all([getDecks(), getDeckStateCounts()]);
|
|
if (decks.length === 0) {
|
|
countEl.textContent = 'Колоды пока отсутствуют.';
|
|
setStatus('');
|
|
listEl.innerHTML = '';
|
|
return;
|
|
}
|
|
countEl.textContent = `Всего: ${decks.length}`;
|
|
setStatus('');
|
|
listEl.innerHTML = decks
|
|
.map((d) => deckRowHtml(d, counts.get(d.id) ?? 0))
|
|
.join('');
|
|
} catch (err) {
|
|
setStatus(`Не удалось загрузить колоды: ${err instanceof Error ? err.message : String(err)}`);
|
|
}
|
|
})();
|
|
}
|
|
|