feat(geschichten): wire discovery integrations on Person and Document pages
Person detail (/persons/[id]):
- Server load fetches GET /api/geschichten?status=PUBLISHED&personId={id}
in parallel with the existing person/document queries.
- Renders <GeschichtenCard> below the received-documents list when the
person has at least one published story.
Document detail (/documents/[id]):
- Server load adds the same parallel call with documentId={id}.
- DocumentTopBar gains geschichten + canBlogWrite props that flow through
to DocumentMetadataDrawer.
- DocumentMetadataDrawer's grid expands to lg:grid-cols-4 when the
Geschichten column should appear (stories exist OR user can author),
and shows "+ Geschichte anhängen" / "Alle anzeigen" links following the
>= 3-story threshold from issue comment #5758.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,12 @@ import RelationshipPill from '$lib/components/RelationshipPill.svelte';
|
||||
|
||||
type Person = { id: string; firstName?: string | null; lastName: string; displayName: string };
|
||||
type Tag = { id: string; name: string };
|
||||
type GeschichteSummary = {
|
||||
id: string;
|
||||
title: string;
|
||||
publishedAt?: string;
|
||||
author?: { firstName?: string; lastName?: string; email: string };
|
||||
};
|
||||
|
||||
type Props = {
|
||||
documentDate: string | null;
|
||||
@@ -16,6 +22,9 @@ type Props = {
|
||||
receivers: Person[];
|
||||
tags: Tag[];
|
||||
inferredRelationship?: { labelFromA: string; labelFromB: string } | null;
|
||||
geschichten?: GeschichteSummary[];
|
||||
documentId?: string;
|
||||
canBlogWrite?: boolean;
|
||||
};
|
||||
|
||||
let {
|
||||
@@ -25,10 +34,30 @@ let {
|
||||
sender,
|
||||
receivers,
|
||||
tags,
|
||||
inferredRelationship = null
|
||||
inferredRelationship = null,
|
||||
geschichten = [],
|
||||
documentId,
|
||||
canBlogWrite = false
|
||||
}: Props = $props();
|
||||
|
||||
const VISIBLE_RECEIVER_LIMIT = 5;
|
||||
const VISIBLE_GESCHICHTEN_LIMIT = 3;
|
||||
const showGeschichtenColumn = $derived(geschichten.length > 0 || canBlogWrite);
|
||||
const visibleGeschichten = $derived(geschichten.slice(0, VISIBLE_GESCHICHTEN_LIMIT));
|
||||
const hasGeschichtenOverflow = $derived(geschichten.length >= VISIBLE_GESCHICHTEN_LIMIT);
|
||||
const gridClass = $derived(showGeschichtenColumn ? 'lg:grid-cols-4' : 'lg:grid-cols-3');
|
||||
|
||||
function formatGeschichteAuthor(g: GeschichteSummary): string {
|
||||
const a = g.author;
|
||||
if (!a) return '';
|
||||
const full = [a.firstName, a.lastName].filter(Boolean).join(' ').trim();
|
||||
return full || a.email || '';
|
||||
}
|
||||
|
||||
function formatGeschichteDate(g: GeschichteSummary): string {
|
||||
if (!g.publishedAt) return '';
|
||||
return formatDate(g.publishedAt.slice(0, 10), 'short');
|
||||
}
|
||||
|
||||
const formattedDate = $derived(documentDate ? formatDate(documentDate) : '—');
|
||||
const displayLocation = $derived(location ?? '—');
|
||||
@@ -67,7 +96,7 @@ function getFullName(person: Person): string {
|
||||
{/snippet}
|
||||
|
||||
<div class="border-b border-line p-6">
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
<div class="grid grid-cols-1 gap-6 {gridClass}">
|
||||
<!-- Column 1: Details -->
|
||||
<div>
|
||||
<h2 class="mb-4 font-sans text-xs font-bold tracking-widest text-ink-3 uppercase">
|
||||
@@ -159,5 +188,51 @@ function getFullName(person: Person): string {
|
||||
<p class="font-serif text-sm text-ink-3">{m.doc_details_no_tags()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Column 4: Geschichten (visible when stories exist or user can author) -->
|
||||
{#if showGeschichtenColumn}
|
||||
<div>
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="font-sans text-xs font-bold tracking-widest text-ink-3 uppercase">
|
||||
{m.geschichten_card_heading()}
|
||||
</h2>
|
||||
{#if canBlogWrite && documentId}
|
||||
<a
|
||||
href="/geschichten/new?documentId={documentId}"
|
||||
class="font-sans text-xs font-medium text-ink/60 hover:text-ink"
|
||||
>
|
||||
{m.geschichten_card_attach_action()}
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if geschichten.length === 0}
|
||||
<p class="font-serif text-sm text-ink-3">—</p>
|
||||
{:else}
|
||||
<ul class="space-y-2 font-serif text-sm">
|
||||
{#each visibleGeschichten as g (g.id)}
|
||||
<li>
|
||||
<a href="/geschichten/{g.id}" class="block text-ink hover:underline">
|
||||
{g.title}
|
||||
</a>
|
||||
<p class="font-sans text-xs text-ink-3">
|
||||
{formatGeschichteAuthor(g)}
|
||||
{#if formatGeschichteDate(g)}· {formatGeschichteDate(g)}{/if}
|
||||
</p>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
{#if hasGeschichtenOverflow && documentId}
|
||||
<a
|
||||
href="/geschichten?documentId={documentId}"
|
||||
class="mt-3 inline-flex font-sans text-xs font-medium text-ink hover:underline"
|
||||
>
|
||||
{m.geschichten_card_show_all()} →
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,12 +25,21 @@ type Doc = {
|
||||
tags?: Tag[] | null;
|
||||
};
|
||||
|
||||
type GeschichteSummary = {
|
||||
id: string;
|
||||
title: string;
|
||||
publishedAt?: string;
|
||||
author?: { firstName?: string; lastName?: string; email: string };
|
||||
};
|
||||
|
||||
type Props = {
|
||||
doc: Doc;
|
||||
canWrite: boolean;
|
||||
fileUrl: string;
|
||||
transcribeMode: boolean;
|
||||
inferredRelationship?: { labelFromA: string; labelFromB: string } | null;
|
||||
geschichten?: GeschichteSummary[];
|
||||
canBlogWrite?: boolean;
|
||||
};
|
||||
|
||||
let {
|
||||
@@ -38,7 +47,9 @@ let {
|
||||
canWrite,
|
||||
fileUrl,
|
||||
transcribeMode = $bindable(),
|
||||
inferredRelationship = null
|
||||
inferredRelationship = null,
|
||||
geschichten = [],
|
||||
canBlogWrite = false
|
||||
}: Props = $props();
|
||||
|
||||
let detailsOpen = $state(false);
|
||||
@@ -283,6 +294,9 @@ let mobileMenuOpen = $state(false);
|
||||
receivers={doc.receivers ? [...doc.receivers] : []}
|
||||
tags={doc.tags ? [...doc.tags] : []}
|
||||
inferredRelationship={inferredRelationship}
|
||||
geschichten={geschichten}
|
||||
documentId={doc.id}
|
||||
canBlogWrite={canBlogWrite}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -7,7 +7,12 @@ export async function load({ params, fetch }) {
|
||||
const { id } = params;
|
||||
const api = createApiClient(fetch);
|
||||
|
||||
const docResult = await api.GET('/api/documents/{id}', { params: { path: { id } } });
|
||||
const [docResult, geschichtenResult] = await Promise.all([
|
||||
api.GET('/api/documents/{id}', { params: { path: { id } } }),
|
||||
api.GET('/api/geschichten', {
|
||||
params: { query: { status: 'PUBLISHED', documentId: id } }
|
||||
})
|
||||
]);
|
||||
|
||||
if (docResult.response.status === 401) throw redirect(302, '/login');
|
||||
|
||||
@@ -18,8 +23,9 @@ export async function load({ params, fetch }) {
|
||||
|
||||
const document = docResult.data!;
|
||||
const inferredRelationship = await loadInferredRelationship(api, document);
|
||||
const geschichten = geschichtenResult.data ?? [];
|
||||
|
||||
return { document, inferredRelationship };
|
||||
return { document, inferredRelationship, geschichten };
|
||||
}
|
||||
|
||||
async function loadInferredRelationship(
|
||||
|
||||
@@ -424,6 +424,8 @@ onMount(() => {
|
||||
fileUrl={fileLoader.fileUrl}
|
||||
bind:transcribeMode={transcribeMode}
|
||||
inferredRelationship={data.inferredRelationship}
|
||||
geschichten={data.geschichten ?? []}
|
||||
canBlogWrite={data.canBlogWrite ?? false}
|
||||
/>
|
||||
|
||||
<div class="relative flex-1 overflow-hidden {transcribeMode ? 'flex flex-col md:flex-row' : ''}">
|
||||
|
||||
@@ -17,14 +17,18 @@ export async function load({ params, fetch, locals }) {
|
||||
receivedDocsResult,
|
||||
aliasesResult,
|
||||
relsResult,
|
||||
inferredResult
|
||||
inferredResult,
|
||||
geschichtenResult
|
||||
] = await Promise.all([
|
||||
api.GET('/api/persons/{id}', { params: { path: { id } } }),
|
||||
api.GET('/api/persons/{id}/documents', { params: { path: { id } } }),
|
||||
api.GET('/api/persons/{id}/received-documents', { params: { path: { id } } }),
|
||||
api.GET('/api/persons/{id}/aliases', { params: { path: { id } } }),
|
||||
api.GET('/api/persons/{id}/relationships', { params: { path: { id } } }),
|
||||
api.GET('/api/persons/{id}/inferred-relationships', { params: { path: { id } } })
|
||||
api.GET('/api/persons/{id}/inferred-relationships', { params: { path: { id } } }),
|
||||
api.GET('/api/geschichten', {
|
||||
params: { query: { status: 'PUBLISHED', personId: id } }
|
||||
})
|
||||
]);
|
||||
|
||||
if (!personResult.response.ok) {
|
||||
@@ -39,6 +43,7 @@ export async function load({ params, fetch, locals }) {
|
||||
aliases: aliasesResult.data ?? [],
|
||||
relationships: relsResult.data ?? [],
|
||||
inferredRelationships: inferredResult.data ?? [],
|
||||
geschichten: geschichtenResult.data ?? [],
|
||||
canWrite
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import NameHistoryCard from './NameHistoryCard.svelte';
|
||||
import CoCorrespondentsList from './CoCorrespondentsList.svelte';
|
||||
import PersonDocumentList from './PersonDocumentList.svelte';
|
||||
import PersonRelationshipsCard from './PersonRelationshipsCard.svelte';
|
||||
import GeschichtenCard from '$lib/components/GeschichtenCard.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
@@ -92,6 +93,17 @@ const coCorrespondents = $derived.by(() => {
|
||||
emptyMessage={m.person_no_received_docs()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if data.geschichten && data.geschichten.length > 0}
|
||||
<div class="mt-6">
|
||||
<GeschichtenCard
|
||||
geschichten={data.geschichten}
|
||||
personId={person.id}
|
||||
personName={person.displayName}
|
||||
canWrite={data.canBlogWrite ?? false}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user