deck display

This commit is contained in:
micialware committed 2026-09-15 10:54:09 +03:00
1 parent 6e609cc9fa
commit 37b3b61f79
9 files changed
+226 -58

No files matched your search

+11
View File
@@ -166,6 +166,17 @@ export async function getWordGroups(): Promise<WordGroup[]> {
}));
}
export async function getDecks(): Promise<Deck[]> {
const rows = await query("SELECT id, name, forward, backward, filter FROM decks ORDER BY id");
return (rows as Record<string, unknown>[]).map((r) => ({
id: Number(r["id"]),
name: String(r["name"] ?? ""),
forward: String(r["forward"] ?? ""),
backward: String(r["backward"] ?? ""),
filter: String(r["filter"] ?? ""),
}));
}
export async function getCustomFields(): Promise<CustomField[]> {
const rows = await query("SELECT id, name, identifier FROM custom_fields ORDER BY id");
return (rows as Record<string, unknown>[]).map((r) => ({
+139
View File
@@ -0,0 +1,139 @@
// Страница «Колоды»: плоский список строк из таблицы decks 1:1.
// Никаких связей (states/history/words) здесь не додумываем — только колонки таблицы.
import { getDecks } from './db-types.ts';
import type { Deck } from './db-types.ts';
import { hasDatabaseFile } from './opfs.ts';
function escapeHtml(s: string): string {
return s
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
// Одна колода — плоская строка: имя слева, направление справа.
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>`;
}
// Раскрытие — сырые поля строки 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>`;
}
export function renderDecksPage(root: HTMLElement): 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')!;
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');
if (!btn) return;
const id = Number(btn.dataset.deckId);
if (!Number.isFinite(id)) return;
setExpanded(expandedId === id ? null : id);
});
void (async () => {
const has = await hasDatabaseFile();
if (!has) {
setStatus('Нет локальной базы — скачай сохранение на странице «Синхронизация».');
countEl.textContent = '';
return;
}
setStatus('Загрузка колод…');
try {
decks = await getDecks();
if (decks.length === 0) {
countEl.textContent = 'Колоды пока отсутствуют.';
setStatus('');
listEl.innerHTML = '';
return;
}
countEl.textContent = `Всего: ${decks.length}`;
setStatus('');
listEl.innerHTML = decks.map((d) => deckRowHtml(d, d.id === expandedId)).join('');
} catch (err) {
setStatus(`Не удалось загрузить колоды: ${err instanceof Error ? err.message : String(err)}`);
}
})();
}
+7 -3
View File
@@ -1,14 +1,16 @@
import './style.css';
import { renderHomePage } from './home-page.ts';
import { renderDecksPage } from './decks-page.ts';
import { renderSyncPage } from './sync-page.ts';
import { initWorker } from './sqlite-api.ts';
// Каркас приложения: топ-бар + бургер-меню + две страницы (Главная / Синхронизация).
// Каркас приложения: топ-бар + бургер-меню + страницы (Главная / Колоды / Синхронизация).
type Page = 'home' | 'sync';
type Page = 'home' | 'decks' | 'sync';
const PAGE_TITLES: Record<Page, string> = {
home: 'Главная',
decks: 'Колоды',
sync: 'Синхронизация',
};
@@ -46,6 +48,8 @@ function renderPage(page: Page): void {
setActiveButton(page);
if (page === 'home') {
renderHomePage(pageRoot, {});
} else if (page === 'decks') {
renderDecksPage(pageRoot);
} else {
renderSyncPage(pageRoot);
}
@@ -66,7 +70,7 @@ document.addEventListener('keydown', (e) => {
drawer.querySelectorAll<HTMLButtonElement>('.drawer-btn').forEach((btn) => {
btn.addEventListener('click', () => {
const page = btn.dataset.page as Page | undefined;
if (page === 'home' || page === 'sync') renderPage(page);
if (page === 'home' || page === 'decks' || page === 'sync') renderPage(page);
setDrawer(false);
});
});
+5
View File
@@ -472,3 +472,8 @@ body.drawer-open {
margin: 2px 0 0;
word-break: break-word;
}
/* Пустые значения в раскрытии колоды — приглушённое тире */
.empty-val {
opacity: 0.5;
}