Files
familienarchiv/frontend/src/routes/+page.svelte
Marcel 19035fbeab
Some checks failed
CI / Backend Unit Tests (pull_request) Failing after 2m37s
CI / E2E Tests (pull_request) Failing after 1h12m25s
CI / Unit & Component Tests (push) Failing after 1m21s
CI / Backend Unit Tests (push) Failing after 2m30s
CI / E2E Tests (push) Failing after 6m59s
CI / Unit & Component Tests (pull_request) Failing after 1m41s
fix(dashboard): move right column first in DOM for mobile-first upload zone
On small screens the upload zone now appears above recent docs.
lg:order-last keeps it visually on the right at desktop width.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 20:42:37 +02:00

125 lines
4.4 KiB
Svelte

<script lang="ts">
import { goto } from '$app/navigation';
import { untrack } from 'svelte';
import { SvelteURLSearchParams } from 'svelte/reactivity';
import SearchFilterBar from './SearchFilterBar.svelte';
import DropZone from './DropZone.svelte';
import DocumentList from './DocumentList.svelte';
import DashboardResumeStrip from '$lib/components/DashboardResumeStrip.svelte';
import DashboardNeedsMetadata from '$lib/components/DashboardNeedsMetadata.svelte';
import DashboardRecentDocuments from '$lib/components/DashboardRecentDocuments.svelte';
import { m } from '$lib/paraglide/messages.js';
let { data } = $props();
let q = $state(untrack(() => data.filters?.q || ''));
let qFocused = $state(false);
let from = $state(untrack(() => data.filters?.from || ''));
let to = $state(untrack(() => data.filters?.to || ''));
let senderId = $state(untrack(() => data.filters?.senderId || ''));
let receiverId = $state(untrack(() => data.filters?.receiverId || ''));
let tagNames = $state<string[]>(untrack(() => data.filters?.tags || []));
const hasAdvancedFilters = (filters: typeof data.filters) =>
(filters?.tags?.length ?? 0) > 0 ||
!!filters?.senderId ||
!!filters?.receiverId ||
!!filters?.from ||
!!filters?.to;
let showAdvanced = $state(untrack(() => hasAdvancedFilters(data.filters)));
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);
if (tagNames) tagNames.forEach((tag) => params.append('tag', tag));
goto(`/?${params.toString()}`, { keepFocus: true, noScroll: true });
}
function handleTextSearch() {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => triggerSearch(), 500);
}
// Trigger search when tags change
let prevTagStr = untrack(() => tagNames.join(','));
$effect(() => {
const cur = tagNames.join(',');
if (cur !== prevTagStr) {
prevTagStr = cur;
triggerSearch();
}
});
// Sync local state with server data after navigation.
// Guard q: skip overwrite while the user is actively typing in the search field.
$effect(() => {
if (!qFocused) q = data.filters?.q || '';
from = data.filters?.from || '';
to = data.filters?.to || '';
senderId = data.filters?.senderId || '';
receiverId = data.filters?.receiverId || '';
tagNames = data.filters?.tags || [];
if (hasAdvancedFilters(data.filters)) showAdvanced = true;
});
// Right column is only rendered when there is something to show.
// Omitting it prevents an empty 300px ghost column for read-only users
// with a complete archive.
const showRightColumn = $derived(data.canWrite || (data.incompleteDocs?.length ?? 0) > 0);
</script>
<svelte:head>
<title>{m.page_title_home()}</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}
initialSenderName={data.initialValues?.senderName}
initialReceiverName={data.initialValues?.receiverName}
onSearch={handleTextSearch}
onfocus={() => (qFocused = true)}
onblur={() => (qFocused = false)}
/>
{#if data.isDashboard}
<DashboardResumeStrip />
<!-- Classic Split: right column first in DOM so it appears above recent docs on mobile.
lg:order-last moves it back to the visual right on desktop. -->
<!-- No items-start — CSS Grid stretch default makes both columns equal height -->
<div class="mt-4 grid grid-cols-1 gap-4 {showRightColumn ? 'lg:grid-cols-[1fr_300px]' : ''}">
{#if showRightColumn}
<div data-testid="dashboard-right-column" class="flex h-full flex-col gap-4 lg:order-last">
{#if data.canWrite}
<DropZone />
{/if}
<!-- flex-1 + min-h-0 fills remaining height after DropZone.
min-h-0 overrides the default min-height:auto that prevents flex
children from shrinking below their content size. -->
<div class="flex min-h-0 flex-1 flex-col">
<DashboardNeedsMetadata incompleteDocs={data.incompleteDocs ?? []} />
</div>
</div>
{/if}
<DashboardRecentDocuments recentDocs={data.recentDocs ?? []} stats={data.stats} />
</div>
{:else}
<DocumentList documents={data.documents ?? []} canWrite={data.canWrite} error={data.error} />
{/if}
</main>