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 Person = { id: string; firstName?: string | null; lastName: string; displayName: string };
|
||||||
type Tag = { id: string; name: string };
|
type Tag = { id: string; name: string };
|
||||||
|
type GeschichteSummary = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
publishedAt?: string;
|
||||||
|
author?: { firstName?: string; lastName?: string; email: string };
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
documentDate: string | null;
|
documentDate: string | null;
|
||||||
@@ -16,6 +22,9 @@ type Props = {
|
|||||||
receivers: Person[];
|
receivers: Person[];
|
||||||
tags: Tag[];
|
tags: Tag[];
|
||||||
inferredRelationship?: { labelFromA: string; labelFromB: string } | null;
|
inferredRelationship?: { labelFromA: string; labelFromB: string } | null;
|
||||||
|
geschichten?: GeschichteSummary[];
|
||||||
|
documentId?: string;
|
||||||
|
canBlogWrite?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -25,10 +34,30 @@ let {
|
|||||||
sender,
|
sender,
|
||||||
receivers,
|
receivers,
|
||||||
tags,
|
tags,
|
||||||
inferredRelationship = null
|
inferredRelationship = null,
|
||||||
|
geschichten = [],
|
||||||
|
documentId,
|
||||||
|
canBlogWrite = false
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const VISIBLE_RECEIVER_LIMIT = 5;
|
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 formattedDate = $derived(documentDate ? formatDate(documentDate) : '—');
|
||||||
const displayLocation = $derived(location ?? '—');
|
const displayLocation = $derived(location ?? '—');
|
||||||
@@ -67,7 +96,7 @@ function getFullName(person: Person): string {
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<div class="border-b border-line p-6">
|
<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 -->
|
<!-- Column 1: Details -->
|
||||||
<div>
|
<div>
|
||||||
<h2 class="mb-4 font-sans text-xs font-bold tracking-widest text-ink-3 uppercase">
|
<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>
|
<p class="font-serif text-sm text-ink-3">{m.doc_details_no_tags()}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,12 +25,21 @@ type Doc = {
|
|||||||
tags?: Tag[] | null;
|
tags?: Tag[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type GeschichteSummary = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
publishedAt?: string;
|
||||||
|
author?: { firstName?: string; lastName?: string; email: string };
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
doc: Doc;
|
doc: Doc;
|
||||||
canWrite: boolean;
|
canWrite: boolean;
|
||||||
fileUrl: string;
|
fileUrl: string;
|
||||||
transcribeMode: boolean;
|
transcribeMode: boolean;
|
||||||
inferredRelationship?: { labelFromA: string; labelFromB: string } | null;
|
inferredRelationship?: { labelFromA: string; labelFromB: string } | null;
|
||||||
|
geschichten?: GeschichteSummary[];
|
||||||
|
canBlogWrite?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -38,7 +47,9 @@ let {
|
|||||||
canWrite,
|
canWrite,
|
||||||
fileUrl,
|
fileUrl,
|
||||||
transcribeMode = $bindable(),
|
transcribeMode = $bindable(),
|
||||||
inferredRelationship = null
|
inferredRelationship = null,
|
||||||
|
geschichten = [],
|
||||||
|
canBlogWrite = false
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let detailsOpen = $state(false);
|
let detailsOpen = $state(false);
|
||||||
@@ -283,6 +294,9 @@ let mobileMenuOpen = $state(false);
|
|||||||
receivers={doc.receivers ? [...doc.receivers] : []}
|
receivers={doc.receivers ? [...doc.receivers] : []}
|
||||||
tags={doc.tags ? [...doc.tags] : []}
|
tags={doc.tags ? [...doc.tags] : []}
|
||||||
inferredRelationship={inferredRelationship}
|
inferredRelationship={inferredRelationship}
|
||||||
|
geschichten={geschichten}
|
||||||
|
documentId={doc.id}
|
||||||
|
canBlogWrite={canBlogWrite}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -7,7 +7,12 @@ export async function load({ params, fetch }) {
|
|||||||
const { id } = params;
|
const { id } = params;
|
||||||
const api = createApiClient(fetch);
|
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');
|
if (docResult.response.status === 401) throw redirect(302, '/login');
|
||||||
|
|
||||||
@@ -18,8 +23,9 @@ export async function load({ params, fetch }) {
|
|||||||
|
|
||||||
const document = docResult.data!;
|
const document = docResult.data!;
|
||||||
const inferredRelationship = await loadInferredRelationship(api, document);
|
const inferredRelationship = await loadInferredRelationship(api, document);
|
||||||
|
const geschichten = geschichtenResult.data ?? [];
|
||||||
|
|
||||||
return { document, inferredRelationship };
|
return { document, inferredRelationship, geschichten };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadInferredRelationship(
|
async function loadInferredRelationship(
|
||||||
|
|||||||
@@ -424,6 +424,8 @@ onMount(() => {
|
|||||||
fileUrl={fileLoader.fileUrl}
|
fileUrl={fileLoader.fileUrl}
|
||||||
bind:transcribeMode={transcribeMode}
|
bind:transcribeMode={transcribeMode}
|
||||||
inferredRelationship={data.inferredRelationship}
|
inferredRelationship={data.inferredRelationship}
|
||||||
|
geschichten={data.geschichten ?? []}
|
||||||
|
canBlogWrite={data.canBlogWrite ?? false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="relative flex-1 overflow-hidden {transcribeMode ? 'flex flex-col md:flex-row' : ''}">
|
<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,
|
receivedDocsResult,
|
||||||
aliasesResult,
|
aliasesResult,
|
||||||
relsResult,
|
relsResult,
|
||||||
inferredResult
|
inferredResult,
|
||||||
|
geschichtenResult
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
api.GET('/api/persons/{id}', { params: { path: { id } } }),
|
api.GET('/api/persons/{id}', { params: { path: { id } } }),
|
||||||
api.GET('/api/persons/{id}/documents', { 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}/received-documents', { params: { path: { id } } }),
|
||||||
api.GET('/api/persons/{id}/aliases', { 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}/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) {
|
if (!personResult.response.ok) {
|
||||||
@@ -39,6 +43,7 @@ export async function load({ params, fetch, locals }) {
|
|||||||
aliases: aliasesResult.data ?? [],
|
aliases: aliasesResult.data ?? [],
|
||||||
relationships: relsResult.data ?? [],
|
relationships: relsResult.data ?? [],
|
||||||
inferredRelationships: inferredResult.data ?? [],
|
inferredRelationships: inferredResult.data ?? [],
|
||||||
|
geschichten: geschichtenResult.data ?? [],
|
||||||
canWrite
|
canWrite
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import NameHistoryCard from './NameHistoryCard.svelte';
|
|||||||
import CoCorrespondentsList from './CoCorrespondentsList.svelte';
|
import CoCorrespondentsList from './CoCorrespondentsList.svelte';
|
||||||
import PersonDocumentList from './PersonDocumentList.svelte';
|
import PersonDocumentList from './PersonDocumentList.svelte';
|
||||||
import PersonRelationshipsCard from './PersonRelationshipsCard.svelte';
|
import PersonRelationshipsCard from './PersonRelationshipsCard.svelte';
|
||||||
|
import GeschichtenCard from '$lib/components/GeschichtenCard.svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
@@ -92,6 +93,17 @@ const coCorrespondents = $derived.by(() => {
|
|||||||
emptyMessage={m.person_no_received_docs()}
|
emptyMessage={m.person_no_received_docs()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user