Files
familienarchiv/frontend/src/routes/persons/[id]/PersonDocumentList.svelte
Marcel 0db68da00c
Some checks failed
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
refactor(persons): extract PersonCard, PersonMergePanel, CoCorrespondentsList, PersonDocumentList
Split the 610-line person detail page into four focused co-located components:
- PersonCard: view/edit card with inline form (owns editMode)
- PersonMergePanel: merge target typeahead + two-step confirm (state reset via {#key})
- CoCorrespondentsList: frequency-ranked correspondent chips linking to conversations
- PersonDocumentList: reusable sorted/paginated document list (used for sent + received)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 12:32:01 +01:00

133 lines
4.3 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { m } from '$lib/paraglide/messages.js';
import { formatDate } from '$lib/utils/date';
import { sortDocumentsByDate, type SortDir } from '$lib/utils/sort';
const DOCS_PREVIEW_LIMIT = 5;
let {
documents,
heading,
emptyMessage
}: {
documents: {
id: string;
title?: string | null;
originalFilename: string;
documentDate?: string | null;
location?: string | null;
status: string;
}[];
heading: string;
emptyMessage: string;
} = $props();
const yearRange = $derived.by(() => {
const years = documents
.filter((d) => d.documentDate)
.map((d) => parseInt(d.documentDate!.substring(0, 4)));
if (!years.length) return null;
const min = Math.min(...years);
const max = Math.max(...years);
return min === max ? `${min}` : `${min} ${max}`;
});
let sortDir = $state<SortDir>('DESC');
let showAll = $state(false);
const sortedDocuments = $derived(sortDocumentsByDate(documents, sortDir));
const visibleDocuments = $derived(
showAll ? sortedDocuments : sortedDocuments.slice(0, DOCS_PREVIEW_LIMIT)
);
</script>
<div class="mb-10">
<div class="mb-6 flex items-center gap-3 border-b border-ink/10 pb-2">
<h2 class="font-serif text-xl text-ink">{heading}</h2>
<span class="rounded-full bg-primary px-2 py-1 text-xs font-bold text-white">
{documents.length}
</span>
{#if yearRange}
<span class="font-sans text-xs text-ink-3">{yearRange}</span>
{/if}
{#if documents.length > 1}
<button
onclick={() => (sortDir = sortDir === 'DESC' ? 'ASC' : 'DESC')}
class="ml-auto text-xs font-bold tracking-widest text-ink-3 uppercase transition-colors hover:text-ink"
>
{sortDir === 'DESC' ? m.conv_sort_newest() : m.conv_sort_oldest()}
</button>
{/if}
</div>
{#if documents.length === 0}
<div class="rounded-sm border border-dashed border-line bg-surface p-12 text-center">
<p class="font-sans text-ink-2">{emptyMessage}</p>
</div>
{:else}
<ul class="space-y-3">
{#each visibleDocuments as doc (doc.id)}
<li class="group">
<a
href="/documents/{doc.id}"
class="block border border-line bg-surface p-4 transition-all duration-200 hover:border-primary hover:shadow-md"
>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 overflow-hidden">
<div
class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded bg-muted text-ink transition-colors group-hover:bg-accent group-hover:text-ink"
>
<img
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/PDF-Document-MD.svg"
alt=""
aria-hidden="true"
class="h-5 w-5"
/>
</div>
<div class="min-w-0">
<div
class="truncate font-serif text-base font-medium text-ink decoration-brand-mint decoration-2 underline-offset-2 group-hover:underline"
>
{doc.title || doc.originalFilename}
</div>
<div class="mt-0.5 flex items-center space-x-2 font-sans text-xs text-ink-2">
<span>{doc.documentDate ? formatDate(doc.documentDate) : m.doc_no_date()}</span>
{#if doc.location}
<span class="text-accent"></span>
<span>{doc.location}</span>
{/if}
</div>
</div>
</div>
<div class="flex flex-shrink-0 items-center gap-2 pl-4">
<span
class="hidden items-center rounded-full border px-2 py-0.5 text-[10px] font-bold tracking-wide uppercase sm:inline-flex
{doc.status === 'UPLOADED'
? 'border-accent/50 bg-accent/20 text-ink'
: 'border-yellow-200 bg-yellow-50 text-yellow-800'}"
>
{doc.status}
</span>
<img
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Arrow/Arrow-Right-MD.svg"
alt=""
aria-hidden="true"
class="ml-2 h-5 w-5 opacity-40 transition-opacity group-hover:opacity-100"
/>
</div>
</div>
</a>
</li>
{/each}
</ul>
{#if documents.length > DOCS_PREVIEW_LIMIT && !showAll}
<button
onclick={() => (showAll = true)}
class="mt-3 text-xs font-bold tracking-widest text-ink/50 uppercase transition-colors hover:text-ink"
>
{m.person_show_more({ count: documents.length - DOCS_PREVIEW_LIMIT })}
</button>
{/if}
{/if}
</div>