repetition woks
This commit is contained in:
1 parent
37b3b61f79
commit
36d2c0b48f
15 files changed
+905
-185
No files matched your search
+34
-76
@@ -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)}`);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user