From 49a17b581bfc956dde1043a55ac9b12c473ab3f8 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 25 May 2026 17:52:43 +0200 Subject: [PATCH] feat(themen): /themen dedicated page with root-tag cards and child rows Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/themen/+page.svelte | 85 +++++++++++++++++++ .../src/routes/themen/page.svelte.spec.ts | 57 +++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 frontend/src/routes/themen/+page.svelte create mode 100644 frontend/src/routes/themen/page.svelte.spec.ts diff --git a/frontend/src/routes/themen/+page.svelte b/frontend/src/routes/themen/+page.svelte new file mode 100644 index 00000000..e88a2d5d --- /dev/null +++ b/frontend/src/routes/themen/+page.svelte @@ -0,0 +1,85 @@ + + + + {m.themen_widget_title()} + + +
+
+ +

{m.themen_widget_title()}

+
+ + {#if visibleTree.length === 0} +

{m.themen_leer()}

+ {:else} +
+ {#each visibleTree as tag (tag.id)} + {@const visibleChildren = (tag.children ?? []).filter(hasAnyDocuments)} + {@const shownChildren = visibleChildren.slice(0, MAX_VISIBLE_CHILDREN)} + {@const hiddenCount = visibleChildren.length - shownChildren.length} + + + {/each} +
+ {/if} +
diff --git a/frontend/src/routes/themen/page.svelte.spec.ts b/frontend/src/routes/themen/page.svelte.spec.ts new file mode 100644 index 00000000..d9d9634a --- /dev/null +++ b/frontend/src/routes/themen/page.svelte.spec.ts @@ -0,0 +1,57 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import ThemenPage from './+page.svelte'; +import type { components } from '$lib/generated/api'; + +afterEach(() => { + cleanup(); +}); + +type TagTreeNodeDTO = components['schemas']['TagTreeNodeDTO']; + +function makeTag( + name: string, + documentCount: number, + children: TagTreeNodeDTO[] = [] +): TagTreeNodeDTO { + return { id: 'id-' + name, name, documentCount, children }; +} + +describe('/themen +page', () => { + it('renders one card per visible root tag', async () => { + const tree = [makeTag('Briefe', 5), makeTag('Fotos', 3)]; + render(ThemenPage, { data: { tree } }); + expect(document.body.textContent).toContain('Briefe'); + expect(document.body.textContent).toContain('Fotos'); + }); + + it('does not render a tag with no documents in its subtree', async () => { + const tree = [makeTag('Briefe', 5), makeTag('Leer', 0)]; + render(ThemenPage, { data: { tree } }); + expect(document.body.textContent).not.toContain('Leer'); + }); + + it('shows empty state when all tags filtered out', async () => { + render(ThemenPage, { data: { tree: [makeTag('Leer', 0)] } }); + expect(document.body.textContent).toMatch(/Noch keine Themen/); + }); + + it('shows empty state when tree is empty', async () => { + render(ThemenPage, { data: { tree: [] } }); + expect(document.body.textContent).toMatch(/Noch keine Themen/); + }); + + it('renders child tags for a root tag', async () => { + const tree = [makeTag('Briefe', 5, [makeTag('Brautbriefe', 3), makeTag('Kriegsbriefe', 2)])]; + render(ThemenPage, { data: { tree } }); + expect(document.body.textContent).toContain('Brautbriefe'); + expect(document.body.textContent).toContain('Kriegsbriefe'); + }); + + it('shows "+ N weitere" when a root tag has more than 5 children', async () => { + const children = Array.from({ length: 7 }, (_, i) => makeTag(`Kind${i}`, i + 1)); + const tree = [makeTag('Briefe', 10, children)]; + render(ThemenPage, { data: { tree } }); + expect(document.body.textContent).toMatch(/\+\s*2\s*weitere/); + }); +});