Files
jap_web_ng/src/decks-page.ts
T

97 lines
3.9 KiB
TypeScript
Raw Normal View History

2026-09-15 16:10:52 +03:00
// Страница «Колоды»: плоский список — только название + количество states.
// Раскрытия по нажатию нет: тап переходит на страницу колоды, где все данные.
2026-09-15 10:54:09 +03:00
2026-09-15 16:10:52 +03:00
import { getDecks, getDeckStateCounts } from './db-types.ts';
2026-09-15 10:54:09 +03:00
import type { Deck } from './db-types.ts';
import { hasDatabaseFile } from './opfs.ts';
2026-09-15 16:10:52 +03:00
export type DecksNav = {
goDeck: (deckId: number) => void;
goReview: (deckId: number) => void;
};
2026-09-15 10:54:09 +03:00
function escapeHtml(s: string): string {
return s
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
2026-09-15 16:10:52 +03:00
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 'записей';
2026-09-15 10:54:09 +03:00
}
2026-09-15 16:10:52 +03:00
// Одна колода — плоская строка: имя + количество слева, «▶» справа.
// Тап по имени — на страницу колоды, тап по «▶» — сразу в повторение.
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>`;
2026-09-15 10:54:09 +03:00
}
2026-09-15 16:10:52 +03:00
export function renderDecksPage(root: HTMLElement, nav: DecksNav): void {
2026-09-15 10:54:09 +03:00
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) => {
2026-09-15 16:10:52 +03:00
const btn = (e.target as HTMLElement).closest<HTMLButtonElement>('[data-act]');
2026-09-15 10:54:09 +03:00
if (!btn) return;
const id = Number(btn.dataset.deckId);
if (!Number.isFinite(id)) return;
2026-09-15 16:10:52 +03:00
if (btn.dataset.act === 'review') nav.goReview(id);
else nav.goDeck(id);
2026-09-15 10:54:09 +03:00
});
void (async () => {
const has = await hasDatabaseFile();
if (!has) {
setStatus('Нет локальной базы — скачай сохранение на странице «Синхронизация».');
countEl.textContent = '';
return;
}
setStatus('Загрузка колод…');
try {
2026-09-15 16:10:52 +03:00
const [decks, counts] = await Promise.all([getDecks(), getDeckStateCounts()]);
2026-09-15 10:54:09 +03:00
if (decks.length === 0) {
countEl.textContent = 'Колоды пока отсутствуют.';
setStatus('');
listEl.innerHTML = '';
return;
}
countEl.textContent = `Всего: ${decks.length}`;
setStatus('');
2026-09-15 16:10:52 +03:00
listEl.innerHTML = decks
.map((d) => deckRowHtml(d, counts.get(d.id) ?? 0))
.join('');
2026-09-15 10:54:09 +03:00
} catch (err) {
setStatus(`Не удалось загрузить колоды: ${err instanceof Error ? err.message : String(err)}`);
}
})();
}
2026-09-15 16:10:52 +03:00