feat(frontend): add /documents page with search, filter, and year-card list
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m35s
CI / OCR Service Tests (push) Successful in 36s
CI / Backend Unit Tests (push) Failing after 2m53s
CI / Unit & Component Tests (pull_request) Failing after 2m40s
CI / OCR Service Tests (pull_request) Successful in 31s
CI / Backend Unit Tests (pull_request) Failing after 2m46s
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m35s
CI / OCR Service Tests (push) Successful in 36s
CI / Backend Unit Tests (push) Failing after 2m53s
CI / Unit & Component Tests (pull_request) Failing after 2m40s
CI / OCR Service Tests (pull_request) Successful in 31s
CI / Backend Unit Tests (pull_request) Failing after 2m46s
- New documents/+page.svelte wires SearchFilterBar + DocumentList with URL-driven navigation (goto + SvelteURLSearchParams) - Reset button in SearchFilterBar now navigates to /documents - Rename documents/+page.server.spec.ts → page.server.spec.ts to avoid SvelteKit route-file conflict on the + prefix Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -125,7 +125,7 @@ $effect(() => {
|
|||||||
|
|
||||||
<!-- Reset Button -->
|
<!-- Reset Button -->
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/documents"
|
||||||
class="flex items-center justify-center border border-transparent px-3 py-2.5 text-ink-3 transition hover:text-red-500"
|
class="flex items-center justify-center border border-transparent px-3 py-2.5 text-ink-3 transition hover:text-red-500"
|
||||||
title={m.docs_btn_reset_title()}
|
title={m.docs_btn_reset_title()}
|
||||||
>
|
>
|
||||||
|
|||||||
121
frontend/src/routes/documents/+page.svelte
Normal file
121
frontend/src/routes/documents/+page.svelte
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { navigating } from '$app/state';
|
||||||
|
import { untrack } from 'svelte';
|
||||||
|
import { SvelteURLSearchParams } from 'svelte/reactivity';
|
||||||
|
import SearchFilterBar from '../SearchFilterBar.svelte';
|
||||||
|
import DocumentList from '../DocumentList.svelte';
|
||||||
|
import * as m from '$lib/paraglide/messages.js';
|
||||||
|
|
||||||
|
let { data } = $props();
|
||||||
|
|
||||||
|
// Local state initialised from server-returned filter values.
|
||||||
|
// untrack() prevents infinite reactive loops during initialisation.
|
||||||
|
let q = $state(untrack(() => data.q || ''));
|
||||||
|
let qFocused = $state(false);
|
||||||
|
let from = $state(untrack(() => data.from || ''));
|
||||||
|
let to = $state(untrack(() => data.to || ''));
|
||||||
|
let senderId = $state(untrack(() => data.senderId || ''));
|
||||||
|
let receiverId = $state(untrack(() => data.receiverId || ''));
|
||||||
|
let tagNames = $state<{ name: string; id?: string; color?: string; parentId?: string }[]>(
|
||||||
|
untrack(() => (data.tags || []).map((name: string) => ({ name })))
|
||||||
|
);
|
||||||
|
let sort = $state(untrack(() => data.sort || 'DATE'));
|
||||||
|
let dir = $state(untrack(() => data.dir || 'desc'));
|
||||||
|
let tagQ = $state(untrack(() => data.tagQ || ''));
|
||||||
|
let tagOperator = $state<'AND' | 'OR'>(untrack(() => (data.tagOp as 'AND' | 'OR') || 'AND'));
|
||||||
|
|
||||||
|
function hasAdvancedFilters() {
|
||||||
|
return (
|
||||||
|
(data.tags?.length ?? 0) > 0 || !!data.senderId || !!data.receiverId || !!data.from || !!data.to
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let showAdvanced = $state(untrack(hasAdvancedFilters));
|
||||||
|
|
||||||
|
let searchTimer: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
function triggerSearch() {
|
||||||
|
const params = new SvelteURLSearchParams();
|
||||||
|
if (q) params.set('q', q);
|
||||||
|
if (from) params.set('from', from);
|
||||||
|
if (to) params.set('to', to);
|
||||||
|
if (senderId) params.set('senderId', senderId);
|
||||||
|
if (receiverId) params.set('receiverId', receiverId);
|
||||||
|
tagNames.forEach((tag) => params.append('tag', tag.name));
|
||||||
|
if (sort) params.set('sort', sort);
|
||||||
|
if (dir) params.set('dir', dir);
|
||||||
|
if (tagQ) params.set('tagQ', tagQ);
|
||||||
|
if (tagOperator === 'OR') params.set('tagOp', 'OR');
|
||||||
|
goto(`/documents?${params.toString()}`, { keepFocus: true, noScroll: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTextSearch() {
|
||||||
|
clearTimeout(searchTimer);
|
||||||
|
searchTimer = setTimeout(() => triggerSearch(), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleImmediateSearch() {
|
||||||
|
clearTimeout(searchTimer);
|
||||||
|
triggerSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger search reactively when the tag list changes.
|
||||||
|
let prevTagStr = untrack(() => tagNames.map((t) => t.name).join(','));
|
||||||
|
$effect(() => {
|
||||||
|
const cur = tagNames.map((t) => t.name).join(',');
|
||||||
|
if (cur !== prevTagStr) {
|
||||||
|
prevTagStr = cur;
|
||||||
|
triggerSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep local filter state in sync with server data after navigation completes.
|
||||||
|
// Guard q: skip overwrite while the user is actively typing.
|
||||||
|
$effect(() => {
|
||||||
|
if (!qFocused) q = data.q || '';
|
||||||
|
from = data.from || '';
|
||||||
|
to = data.to || '';
|
||||||
|
senderId = data.senderId || '';
|
||||||
|
receiverId = data.receiverId || '';
|
||||||
|
tagNames = (data.tags || []).map((name: string) => ({ name }));
|
||||||
|
sort = data.sort || 'DATE';
|
||||||
|
dir = data.dir || 'desc';
|
||||||
|
tagQ = data.tagQ || '';
|
||||||
|
tagOperator = (data.tagOp as 'AND' | 'OR') || 'AND';
|
||||||
|
if (hasAdvancedFilters()) showAdvanced = true;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{m.nav_documents()} – Familienarchiv</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<main class="mx-auto max-w-7xl px-4 py-8 font-sans sm:px-6 lg:px-8">
|
||||||
|
<SearchFilterBar
|
||||||
|
bind:q={q}
|
||||||
|
bind:from={from}
|
||||||
|
bind:to={to}
|
||||||
|
bind:senderId={senderId}
|
||||||
|
bind:receiverId={receiverId}
|
||||||
|
bind:tagNames={tagNames}
|
||||||
|
bind:showAdvanced={showAdvanced}
|
||||||
|
bind:sort={sort}
|
||||||
|
bind:dir={dir}
|
||||||
|
bind:tagQ={tagQ}
|
||||||
|
bind:tagOperator={tagOperator}
|
||||||
|
isLoading={navigating.to !== null}
|
||||||
|
onSearch={handleTextSearch}
|
||||||
|
onSearchImmediate={handleImmediateSearch}
|
||||||
|
onfocus={() => (qFocused = true)}
|
||||||
|
onblur={() => (qFocused = false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DocumentList
|
||||||
|
items={data.items}
|
||||||
|
total={data.total}
|
||||||
|
q={data.q}
|
||||||
|
canWrite={data.canWrite}
|
||||||
|
error={data.error}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
Reference in New Issue
Block a user