repetition woks

This commit is contained in:
micialware committed 2026-09-15 16:10:52 +03:00
1 parent 37b3b61f79
commit 36d2c0b48f
15 files changed
+905 -185

No files matched your search

+34 -76
View File
@@ -1,10 +1,15 @@
// Страница «Колоды»: плоский список строк из таблицы decks 1:1.
// Никаких связей (states/history/words) здесь не додумываем — только колонки таблицы.
// Страница «Колоды»: плоский список — только название + количество states.
// Раскрытия по нажатию нет: тап переходит на страницу колоды, где все данные.
import { getDecks } from './db-types.ts';
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, '&')
@@ -14,34 +19,28 @@ function escapeHtml(s: string): string {
.replace(/'/g, ''');
}
// Одна колода — плоская строка: имя слева, направление справа.
function deckRowHtml(d: Deck, expanded: boolean): string {
const dir = [d.forward, d.backward].filter((s) => s.trim().length > 0).join(' → ');
return `<div class="word-row-wrap" data-deck-id="${d.id}">`
+ `<button class="word-row" type="button" data-deck-id="${d.id}"`
+ ` aria-expanded="${expanded}" aria-controls="deck-extra-${d.id}">`
+ `<span class="word-main"><span class="word-key">${escapeHtml(d.name)}</span>`
+ (dir ? `<span class="word-sep"> · </span><span class="word-value">${escapeHtml(dir)}</span>` : '')
+ `</span></button>`
+ `<div class="word-extra" id="deck-extra-${d.id}"${expanded ? '' : ' hidden'}>`
+ (expanded ? deckExtraHtml(d) : '')
+ `</div></div>`;
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 'записей';
}
// Раскрытие — сырые поля строки decks (имена колонок как в dbschema.sql).
function deckExtraHtml(d: Deck): string {
const rows: Array<[string, string]> = [
['forward', d.forward],
['backward', d.backward],
['filter', d.filter],
];
const html = rows
.map(([k, v]) => `<div class="word-extra-row"><dt>${escapeHtml(k)}</dt><dd>${v ? escapeHtml(v) : '<span class="empty-val">—</span>'}</dd></div>`)
.join('');
return `<dl class="word-extra-list">${html}</dl>`;
// Одна колода — плоская строка: имя + количество слева, «▶» справа.
// Тап по имени — на страницу колоды, тап по «▶» — сразу в повторение.
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): void {
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>
@@ -55,62 +54,18 @@ export function renderDecksPage(root: HTMLElement): void {
const statusEl = root.querySelector<HTMLElement>('#decks-status')!;
const listEl = root.querySelector<HTMLElement>('#decks-list')!;
let decks: Deck[] = [];
let expandedId: number | null = null;
function setStatus(msg: string): void {
statusEl.textContent = msg;
statusEl.hidden = msg === '';
}
function collapseRow(id: number): void {
const wrap = listEl.querySelector(`.word-row-wrap[data-deck-id="${id}"]`);
if (!wrap) return;
const btn = wrap.querySelector<HTMLButtonElement>('.word-row');
const extra = wrap.querySelector<HTMLElement>('.word-extra');
if (btn) {
btn.classList.remove('open');
btn.setAttribute('aria-expanded', 'false');
}
if (extra) {
extra.hidden = true;
extra.innerHTML = '';
}
}
function expandRow(id: number): void {
const d = decks.find((x) => x.id === id);
const wrap = listEl.querySelector(`.word-row-wrap[data-deck-id="${id}"]`);
if (!d || !wrap) return;
const btn = wrap.querySelector<HTMLButtonElement>('.word-row');
const extra = wrap.querySelector<HTMLElement>('.word-extra');
if (btn) {
btn.classList.add('open');
btn.setAttribute('aria-expanded', 'true');
}
if (extra) {
extra.hidden = false;
extra.innerHTML = deckExtraHtml(d);
}
}
function setExpanded(id: number | null): void {
if (expandedId === id) {
if (id != null) collapseRow(id);
expandedId = null;
return;
}
if (expandedId != null) collapseRow(expandedId);
expandedId = id;
if (id != null) expandRow(id);
}
listEl.addEventListener('click', (e) => {
const btn = (e.target as HTMLElement).closest<HTMLButtonElement>('.word-row');
const btn = (e.target as HTMLElement).closest<HTMLButtonElement>('[data-act]');
if (!btn) return;
const id = Number(btn.dataset.deckId);
if (!Number.isFinite(id)) return;
setExpanded(expandedId === id ? null : id);
if (btn.dataset.act === 'review') nav.goReview(id);
else nav.goDeck(id);
});
void (async () => {
@@ -122,7 +77,7 @@ export function renderDecksPage(root: HTMLElement): void {
}
setStatus('Загрузка колод…');
try {
decks = await getDecks();
const [decks, counts] = await Promise.all([getDecks(), getDeckStateCounts()]);
if (decks.length === 0) {
countEl.textContent = 'Колоды пока отсутствуют.';
setStatus('');
@@ -131,9 +86,12 @@ export function renderDecksPage(root: HTMLElement): void {
}
countEl.textContent = `Всего: ${decks.length}`;
setStatus('');
listEl.innerHTML = decks.map((d) => deckRowHtml(d, d.id === expandedId)).join('');
listEl.innerHTML = decks
.map((d) => deckRowHtml(d, counts.get(d.id) ?? 0))
.join('');
} catch (err) {
setStatus(`Не удалось загрузить колоды: ${err instanceof Error ? err.message : String(err)}`);
}
})();
}