- JourneyItemCard: 'Brief öffnen' back to a >=44px touch target with the height regression spec restored - GeschichteListRow: REISE badges text-[10px] -> text-xs; drop the hardcoded aria-label and the mobile badge's aria-hidden so phone screen readers learn a row is a Lesereise; mobile avatar initials -> color dot - detail page: badge text-xs, metabar Edit/Delete h-9 -> h-11, avatar color keyed by name to match the list - JourneyReader: dead border-subtle class -> border-line-2 - DocumentPickerDropdown: aria-controls only while the listbox exists - JourneyAddBar: aria-expanded/aria-controls on both toggles + focus hand-off into the revealed picker input / interlude textarea - GeschichteSidebar: section h2s hidden below sm (summary already shows the label there) - JourneyCreate: bg-brand-navy -> semantic bg-primary/text-primary-fg; title maxlength=255 - JourneyItemRow interlude: neutral frame border + left accent only, token utilities instead of arbitrary var() syntax and inline style Review round 3: Leonie (1-8 + round-1 leftovers), Elicit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
133 lines
4.1 KiB
Svelte
133 lines
4.1 KiB
Svelte
<script lang="ts">
|
|
import { goto } from '$app/navigation';
|
|
import { m } from '$lib/paraglide/messages.js';
|
|
import { formatDate } from '$lib/shared/utils/date';
|
|
import { formatAuthorDisplayName } from '$lib/geschichte/utils';
|
|
import { getInitials, personAvatarColor } from '$lib/person/personFormat';
|
|
import { getConfirmService } from '$lib/shared/services/confirm.svelte';
|
|
import { csrfFetch } from '$lib/shared/cookies';
|
|
import { parseBackendError, getErrorMessage } from '$lib/shared/errors';
|
|
import BackButton from '$lib/shared/primitives/BackButton.svelte';
|
|
import StoryReader from '$lib/geschichte/StoryReader.svelte';
|
|
import JourneyReader from '$lib/geschichte/JourneyReader.svelte';
|
|
import type { PageData } from './$types';
|
|
|
|
let { data }: { data: PageData } = $props();
|
|
|
|
const g = $derived(data.geschichte);
|
|
const isJourney = $derived(g.type === 'JOURNEY');
|
|
|
|
const publishedAt = $derived.by(() => {
|
|
if (!g.publishedAt) return null;
|
|
return formatDate(g.publishedAt.slice(0, 10), 'long');
|
|
});
|
|
|
|
const authorName = $derived(formatAuthorDisplayName(g.author));
|
|
|
|
const confirm = getConfirmService();
|
|
|
|
let deleteError = $state<string | null>(null);
|
|
|
|
async function handleDelete() {
|
|
deleteError = null;
|
|
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 csrfFetch(`/api/geschichten/${g.id}`, { method: 'DELETE' });
|
|
if (res.ok) {
|
|
goto('/geschichten');
|
|
} else {
|
|
const err = await parseBackendError(res);
|
|
deleteError = getErrorMessage(err?.code);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="mx-auto max-w-7xl px-4 py-8">
|
|
<div class="mb-6">
|
|
<BackButton />
|
|
</div>
|
|
|
|
<article
|
|
aria-labelledby="geschichte-title"
|
|
class="rounded-sm border border-line bg-sheet px-5 py-6 shadow-sm sm:px-10 sm:py-10"
|
|
>
|
|
<div class="mx-auto max-w-3xl">
|
|
<header class="mb-6">
|
|
{#if isJourney}
|
|
<span
|
|
class="mb-2 inline-flex rounded-sm border border-journey-border bg-journey-tint px-2 py-px text-xs font-bold tracking-widest text-journey uppercase"
|
|
>
|
|
{m.journey_badge_detail()}
|
|
</span>
|
|
{/if}
|
|
<h1 id="geschichte-title" class="mb-4 font-serif text-3xl leading-tight text-ink">
|
|
{g.title}
|
|
</h1>
|
|
<div class="mb-4 flex items-center gap-3 border-b border-line-2 pb-4">
|
|
{#if authorName}
|
|
<span
|
|
aria-hidden="true"
|
|
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full font-sans text-xs font-bold text-white"
|
|
style="background-color: {personAvatarColor(authorName)}"
|
|
>
|
|
{getInitials(authorName)}
|
|
</span>
|
|
{/if}
|
|
<div>
|
|
{#if authorName}
|
|
<p class="font-sans text-sm leading-tight font-semibold text-ink">{authorName}</p>
|
|
{/if}
|
|
{#if publishedAt}
|
|
<p class="font-sans text-xs text-ink-3">
|
|
{#if isJourney}
|
|
{m.journey_compiled_on({ date: publishedAt })}
|
|
{:else}
|
|
{m.geschichten_published_on({ date: publishedAt })}
|
|
{/if}
|
|
</p>
|
|
{/if}
|
|
</div>
|
|
{#if data.canBlogWrite}
|
|
<div class="ml-auto flex items-center gap-3">
|
|
<a
|
|
href="/geschichten/{g.id}/edit"
|
|
class="inline-flex h-11 items-center rounded border border-line bg-surface px-3 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>
|
|
</header>
|
|
|
|
{#if deleteError}
|
|
<p
|
|
role="alert"
|
|
class="mb-4 rounded border border-danger/30 bg-danger/10 px-4 py-3 font-sans text-sm text-danger"
|
|
>
|
|
{deleteError}
|
|
</p>
|
|
{/if}
|
|
|
|
{#if isJourney}
|
|
<JourneyReader geschichte={g} />
|
|
{:else}
|
|
<StoryReader geschichte={g} />
|
|
{/if}
|
|
</div>
|
|
</article>
|
|
</div>
|