feat(geschichten): add /geschichten routes (index, detail, new, edit)
- /geschichten — published-stories index with filter pills + "+ Neue Geschichte"
for BLOG_WRITERs; supports ?personId and ?documentId pre-filtering
- /geschichten/[id] — reader detail with sanitised {@html} body, person and
document chip sections, BLOG_WRITER edit/delete with confirm dialog
- /geschichten/new — editor with optional ?personId and ?documentId pre-fill
(silent ignore on unknown IDs to avoid leaking entity existence)
- /geschichten/[id]/edit — editor populated from existing story; BLOG_WRITE
guard redirects readers to the detail page
All routes load via createApiClient(fetch) with !response.ok error handling
following the project pattern; PATCH/DELETE go through raw fetch which the
Vite dev proxy / Caddy production proxy authenticates via cookie.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
36
frontend/src/routes/geschichten/+page.server.ts
Normal file
36
frontend/src/routes/geschichten/+page.server.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
import { createApiClient } from '$lib/api.server';
|
||||||
|
import { getErrorMessage } from '$lib/errors';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ url, fetch }) => {
|
||||||
|
const api = createApiClient(fetch);
|
||||||
|
const personId = url.searchParams.get('personId') ?? undefined;
|
||||||
|
const documentId = url.searchParams.get('documentId') ?? undefined;
|
||||||
|
|
||||||
|
const [listResult, personResult] = await Promise.all([
|
||||||
|
api.GET('/api/geschichten', {
|
||||||
|
params: {
|
||||||
|
query: {
|
||||||
|
status: 'PUBLISHED',
|
||||||
|
personId,
|
||||||
|
documentId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
personId
|
||||||
|
? api.GET('/api/persons/{id}', { params: { path: { id: personId } } })
|
||||||
|
: Promise.resolve(null)
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!listResult.response.ok) {
|
||||||
|
const code = (listResult.error as unknown as { code?: string })?.code;
|
||||||
|
throw error(listResult.response.status, getErrorMessage(code));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
geschichten: listResult.data ?? [],
|
||||||
|
personFilter: personResult && personResult.response.ok ? personResult.data! : null,
|
||||||
|
documentFilter: documentId ?? null
|
||||||
|
};
|
||||||
|
};
|
||||||
130
frontend/src/routes/geschichten/+page.svelte
Normal file
130
frontend/src/routes/geschichten/+page.svelte
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
|
import { plainExcerpt } from '$lib/utils/stripHtml';
|
||||||
|
import { formatDate } from '$lib/utils/date';
|
||||||
|
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
let showPersonPicker = $state(false);
|
||||||
|
|
||||||
|
const filterName = $derived(data.personFilter?.displayName ?? '');
|
||||||
|
|
||||||
|
function clearFilter() {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.delete('personId');
|
||||||
|
url.searchParams.delete('documentId');
|
||||||
|
goto(url.pathname + url.search, { replaceState: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickPerson(personId: string) {
|
||||||
|
if (!personId) return;
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set('personId', personId);
|
||||||
|
url.searchParams.delete('documentId');
|
||||||
|
showPersonPicker = false;
|
||||||
|
goto(url.pathname + url.search);
|
||||||
|
}
|
||||||
|
|
||||||
|
function authorName(g: { author?: { firstName?: string; lastName?: string; email: string } }) {
|
||||||
|
const a = g.author;
|
||||||
|
if (!a) return '';
|
||||||
|
const full = [a.firstName, a.lastName].filter(Boolean).join(' ').trim();
|
||||||
|
return full || a.email || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function publishedAt(g: { publishedAt?: string }): string | null {
|
||||||
|
if (!g.publishedAt) return null;
|
||||||
|
return formatDate(g.publishedAt.slice(0, 10), 'short');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mx-auto max-w-4xl px-4 py-8">
|
||||||
|
<header class="mb-6 flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<h1 class="font-serif text-3xl font-bold text-ink">{m.geschichten_index_title()}</h1>
|
||||||
|
{#if data.canBlogWrite}
|
||||||
|
<a
|
||||||
|
href="/geschichten/new"
|
||||||
|
class="inline-flex h-11 items-center rounded bg-primary px-4 font-sans text-sm font-medium text-primary-fg hover:opacity-90 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||||
|
>
|
||||||
|
{m.geschichten_new_button()}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Filter pills -->
|
||||||
|
<div class="mb-6 flex flex-wrap items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-pressed={!data.personFilter}
|
||||||
|
onclick={clearFilter}
|
||||||
|
class="inline-flex h-9 items-center rounded-full border border-line px-3 font-sans text-xs font-bold tracking-wider text-ink-2 uppercase hover:bg-muted aria-pressed:bg-ink aria-pressed:text-primary-fg"
|
||||||
|
>
|
||||||
|
{m.geschichten_filter_all_pill()}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if data.personFilter}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-pressed="true"
|
||||||
|
onclick={clearFilter}
|
||||||
|
class="inline-flex h-9 items-center gap-2 rounded-full bg-ink px-3 font-sans text-xs font-bold tracking-wider text-primary-fg uppercase"
|
||||||
|
>
|
||||||
|
{filterName}
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={m.geschichten_filter_aria_label()}
|
||||||
|
onclick={() => (showPersonPicker = !showPersonPicker)}
|
||||||
|
class="inline-flex h-9 items-center rounded-full border border-line px-3 font-sans text-xs font-bold tracking-wider text-ink-2 uppercase hover:bg-muted"
|
||||||
|
>
|
||||||
|
+ {m.geschichten_filter_choose_person()}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if showPersonPicker}
|
||||||
|
<div class="mb-4">
|
||||||
|
<PersonTypeahead
|
||||||
|
name="filter-person"
|
||||||
|
label={m.geschichten_filter_choose_person()}
|
||||||
|
compact
|
||||||
|
onchange={pickPerson}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Card list -->
|
||||||
|
{#if data.geschichten.length === 0}
|
||||||
|
<div class="rounded border border-line bg-surface p-6 text-center font-sans text-sm text-ink-3">
|
||||||
|
{#if data.personFilter}
|
||||||
|
{m.geschichten_empty_for_person({ name: filterName })}
|
||||||
|
{:else}
|
||||||
|
{m.geschichten_empty_no_filter()}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<ul class="flex flex-col gap-4">
|
||||||
|
{#each data.geschichten as g (g.id)}
|
||||||
|
<li
|
||||||
|
class="rounded border border-line bg-surface p-5 shadow-sm transition-shadow hover:shadow-md"
|
||||||
|
>
|
||||||
|
<a href="/geschichten/{g.id}" class="block">
|
||||||
|
<h2 class="mb-1 font-serif text-xl font-bold text-ink">{g.title}</h2>
|
||||||
|
<p class="mb-3 font-sans text-xs text-ink-3">
|
||||||
|
{authorName(g)}
|
||||||
|
{#if publishedAt(g)}· {m.geschichten_published_on({ date: publishedAt(g)! })}{/if}
|
||||||
|
</p>
|
||||||
|
{#if g.body}
|
||||||
|
<p class="font-serif text-base text-ink-2">{plainExcerpt(g.body, 150)}</p>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
16
frontend/src/routes/geschichten/[id]/+page.server.ts
Normal file
16
frontend/src/routes/geschichten/[id]/+page.server.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
import { createApiClient } from '$lib/api.server';
|
||||||
|
import { getErrorMessage } from '$lib/errors';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ params, fetch }) => {
|
||||||
|
const api = createApiClient(fetch);
|
||||||
|
const result = await api.GET('/api/geschichten/{id}', {
|
||||||
|
params: { path: { id: params.id } }
|
||||||
|
});
|
||||||
|
if (!result.response.ok) {
|
||||||
|
const code = (result.error as unknown as { code?: string })?.code;
|
||||||
|
throw error(result.response.status, getErrorMessage(code));
|
||||||
|
}
|
||||||
|
return { geschichte: result.data! };
|
||||||
|
};
|
||||||
133
frontend/src/routes/geschichten/[id]/+page.svelte
Normal file
133
frontend/src/routes/geschichten/[id]/+page.svelte
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
|
import { safeHtml } from '$lib/utils/sanitize';
|
||||||
|
import { formatDate } from '$lib/utils/date';
|
||||||
|
import { getConfirmService } from '$lib/services/confirm.svelte';
|
||||||
|
import BackButton from '$lib/components/BackButton.svelte';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
const g = $derived(data.geschichte);
|
||||||
|
const sanitized = $derived(safeHtml(g.body));
|
||||||
|
|
||||||
|
const publishedAt = $derived.by(() => {
|
||||||
|
if (!g.publishedAt) return null;
|
||||||
|
return formatDate(g.publishedAt.slice(0, 10), 'long');
|
||||||
|
});
|
||||||
|
|
||||||
|
function authorName(): string {
|
||||||
|
const a = g.author;
|
||||||
|
if (!a) return '';
|
||||||
|
const full = [a.firstName, a.lastName].filter(Boolean).join(' ').trim();
|
||||||
|
return full || a.email || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirm = getConfirmService();
|
||||||
|
|
||||||
|
async function handleDelete() {
|
||||||
|
const ok = await confirm.confirm({
|
||||||
|
title: m.geschichte_delete_confirm_title(),
|
||||||
|
body: m.geschichte_delete_confirm_body(),
|
||||||
|
confirmLabel: m.btn_delete(),
|
||||||
|
cancelLabel: m.btn_cancel(),
|
||||||
|
destructive: true
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
|
const res = await fetch(`/api/geschichten/${g.id}`, { method: 'DELETE' });
|
||||||
|
if (res.ok) {
|
||||||
|
goto('/geschichten');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mx-auto max-w-3xl px-4 py-8">
|
||||||
|
<div class="mb-6">
|
||||||
|
<BackButton />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<article aria-labelledby="geschichte-title">
|
||||||
|
<header class="mb-6">
|
||||||
|
<h1 id="geschichte-title" class="mb-3 font-serif text-4xl font-bold text-ink">
|
||||||
|
{g.title}
|
||||||
|
</h1>
|
||||||
|
<p class="font-sans text-sm text-ink-3">
|
||||||
|
{authorName()}
|
||||||
|
{#if publishedAt}· {m.geschichten_published_on({ date: publishedAt })}{/if}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="prose font-serif text-lg leading-relaxed text-ink">
|
||||||
|
<!-- Sanitised via safeHtml() (DOMPurify) on render — matches backend OWASP allow-list -->
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
|
{@html sanitized}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<!-- Personen -->
|
||||||
|
{#if g.persons && g.persons.length > 0}
|
||||||
|
<section class="mt-10 border-t border-line pt-6">
|
||||||
|
<h2 class="mb-3 font-sans text-xs font-bold tracking-widest text-ink-3 uppercase">
|
||||||
|
{m.geschichten_persons_section()}
|
||||||
|
</h2>
|
||||||
|
<ul class="flex flex-wrap gap-2">
|
||||||
|
{#each g.persons as p (p.id)}
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="/persons/{p.id}"
|
||||||
|
class="inline-flex items-center rounded-full bg-muted px-3 py-1 font-sans text-sm text-ink hover:bg-accent-bg"
|
||||||
|
>
|
||||||
|
{p.displayName}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Dokumente -->
|
||||||
|
{#if g.documents && g.documents.length > 0}
|
||||||
|
<section class="mt-8 border-t border-line pt-6">
|
||||||
|
<h2 class="mb-3 font-sans text-xs font-bold tracking-widest text-ink-3 uppercase">
|
||||||
|
{m.geschichten_documents_section()}
|
||||||
|
</h2>
|
||||||
|
<ul class="flex flex-col gap-2">
|
||||||
|
{#each g.documents as d (d.id)}
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="/documents/{d.id}"
|
||||||
|
class="block rounded border border-line bg-surface px-4 py-3 font-serif text-base text-ink hover:bg-muted"
|
||||||
|
>
|
||||||
|
{d.title}
|
||||||
|
{#if d.documentDate}
|
||||||
|
<span class="ml-2 font-sans text-xs text-ink-3">
|
||||||
|
{formatDate(d.documentDate, 'short')}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Author actions -->
|
||||||
|
{#if data.canBlogWrite}
|
||||||
|
<div class="mt-10 flex items-center gap-3 border-t border-line pt-6">
|
||||||
|
<a
|
||||||
|
href="/geschichten/{g.id}/edit"
|
||||||
|
class="inline-flex h-11 items-center rounded border border-line bg-surface px-4 font-sans text-sm font-medium text-ink hover:bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||||
|
>
|
||||||
|
{m.btn_edit()}
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={handleDelete}
|
||||||
|
class="inline-flex h-11 items-center rounded font-sans text-sm font-medium text-danger hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||||
|
>
|
||||||
|
{m.btn_delete()}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
20
frontend/src/routes/geschichten/[id]/edit/+page.server.ts
Normal file
20
frontend/src/routes/geschichten/[id]/edit/+page.server.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { error, redirect } from '@sveltejs/kit';
|
||||||
|
import { createApiClient } from '$lib/api.server';
|
||||||
|
import { getErrorMessage } from '$lib/errors';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ params, fetch, parent }) => {
|
||||||
|
const layout = await parent();
|
||||||
|
if (!layout.canBlogWrite) {
|
||||||
|
throw redirect(303, `/geschichten/${params.id}`);
|
||||||
|
}
|
||||||
|
const api = createApiClient(fetch);
|
||||||
|
const result = await api.GET('/api/geschichten/{id}', {
|
||||||
|
params: { path: { id: params.id } }
|
||||||
|
});
|
||||||
|
if (!result.response.ok) {
|
||||||
|
const code = (result.error as unknown as { code?: string })?.code;
|
||||||
|
throw error(result.response.status, getErrorMessage(code));
|
||||||
|
}
|
||||||
|
return { geschichte: result.data! };
|
||||||
|
};
|
||||||
60
frontend/src/routes/geschichten/[id]/edit/+page.svelte
Normal file
60
frontend/src/routes/geschichten/[id]/edit/+page.svelte
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
|
import GeschichteEditor from '$lib/components/GeschichteEditor.svelte';
|
||||||
|
import BackButton from '$lib/components/BackButton.svelte';
|
||||||
|
import { getErrorMessage } from '$lib/errors';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
let submitting = $state(false);
|
||||||
|
let errorMessage: string | null = $state(null);
|
||||||
|
|
||||||
|
async function handleSubmit(payload: {
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
status: 'DRAFT' | 'PUBLISHED';
|
||||||
|
personIds: string[];
|
||||||
|
documentIds: string[];
|
||||||
|
}) {
|
||||||
|
submitting = true;
|
||||||
|
errorMessage = null;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/geschichten/${data.geschichte.id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const code = (await res.json().catch(() => ({})))?.code;
|
||||||
|
errorMessage = getErrorMessage(code);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
goto(`/geschichten/${data.geschichte.id}`);
|
||||||
|
} finally {
|
||||||
|
submitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mx-auto max-w-5xl px-4 py-8">
|
||||||
|
<div class="mb-6">
|
||||||
|
<BackButton />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="mb-6 font-serif text-3xl font-bold text-ink">
|
||||||
|
{m.btn_edit()}: {data.geschichte.title}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{#if errorMessage}
|
||||||
|
<div
|
||||||
|
class="mb-4 rounded border border-danger bg-danger/10 p-3 text-sm text-danger"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<GeschichteEditor geschichte={data.geschichte} onSubmit={handleSubmit} submitting={submitting} />
|
||||||
|
</div>
|
||||||
33
frontend/src/routes/geschichten/new/+page.server.ts
Normal file
33
frontend/src/routes/geschichten/new/+page.server.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import { createApiClient } from '$lib/api.server';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ url, fetch, parent }) => {
|
||||||
|
const layout = await parent();
|
||||||
|
if (!layout.canBlogWrite) {
|
||||||
|
throw redirect(303, '/geschichten');
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = createApiClient(fetch);
|
||||||
|
const personId = url.searchParams.get('personId');
|
||||||
|
const documentId = url.searchParams.get('documentId');
|
||||||
|
|
||||||
|
const [personResult, documentResult] = await Promise.all([
|
||||||
|
personId
|
||||||
|
? api.GET('/api/persons/{id}', { params: { path: { id: personId } } })
|
||||||
|
: Promise.resolve(null),
|
||||||
|
documentId
|
||||||
|
? api.GET('/api/documents/{id}', { params: { path: { id: documentId } } })
|
||||||
|
: Promise.resolve(null)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Silently ignore 404/403 to avoid leaking entity existence on unknown IDs.
|
||||||
|
const initialPersons =
|
||||||
|
personResult && personResult.response.ok && personResult.data ? [personResult.data] : [];
|
||||||
|
const initialDocuments =
|
||||||
|
documentResult && documentResult.response.ok && documentResult.data
|
||||||
|
? [documentResult.data]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return { initialPersons, initialDocuments };
|
||||||
|
};
|
||||||
64
frontend/src/routes/geschichten/new/+page.svelte
Normal file
64
frontend/src/routes/geschichten/new/+page.svelte
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
|
import GeschichteEditor from '$lib/components/GeschichteEditor.svelte';
|
||||||
|
import BackButton from '$lib/components/BackButton.svelte';
|
||||||
|
import { getErrorMessage } from '$lib/errors';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
let submitting = $state(false);
|
||||||
|
let errorMessage: string | null = $state(null);
|
||||||
|
|
||||||
|
async function handleSubmit(payload: {
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
status: 'DRAFT' | 'PUBLISHED';
|
||||||
|
personIds: string[];
|
||||||
|
documentIds: string[];
|
||||||
|
}) {
|
||||||
|
submitting = true;
|
||||||
|
errorMessage = null;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/geschichten', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const code = (await res.json().catch(() => ({})))?.code;
|
||||||
|
errorMessage = getErrorMessage(code);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const created = await res.json();
|
||||||
|
goto(`/geschichten/${created.id}`);
|
||||||
|
} finally {
|
||||||
|
submitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mx-auto max-w-5xl px-4 py-8">
|
||||||
|
<div class="mb-6">
|
||||||
|
<BackButton />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="mb-6 font-serif text-3xl font-bold text-ink">{m.geschichten_new_button()}</h1>
|
||||||
|
|
||||||
|
{#if errorMessage}
|
||||||
|
<div
|
||||||
|
class="mb-4 rounded border border-danger bg-danger/10 p-3 text-sm text-danger"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<GeschichteEditor
|
||||||
|
initialPersons={data.initialPersons}
|
||||||
|
initialDocuments={data.initialDocuments}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
submitting={submitting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user