feat(journey): replace new-journey placeholder with JourneyCreate form
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m40s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / fail2ban Regex (pull_request) Has been cancelled
CI / Semgrep Security Scan (pull_request) Has been cancelled
CI / Compose Bucket Idempotency (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m40s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / fail2ban Regex (pull_request) Has been cancelled
CI / Semgrep Security Scan (pull_request) Has been cancelled
CI / Compose Bucket Idempotency (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1170,7 +1170,7 @@
|
||||
"journey_selector_journey_desc": "Eine kuratierte Auswahl von Briefen mit Notizen.",
|
||||
"journey_selector_next_btn": "Weiter",
|
||||
"journey_placeholder_back": "andere Auswahl",
|
||||
"journey_placeholder_heading": "Lesereise-Editor folgt in #753",
|
||||
"journey_create_submit": "Lesereise erstellen",
|
||||
"journey_item_open_aria": "Brief vom {date} öffnen",
|
||||
"journey_item_open_aria_undated": "Brief öffnen",
|
||||
"journey_empty_state": "Diese Lesereise ist noch leer.",
|
||||
|
||||
@@ -1170,7 +1170,7 @@
|
||||
"journey_selector_journey_desc": "A curated selection of letters with notes.",
|
||||
"journey_selector_next_btn": "Continue",
|
||||
"journey_placeholder_back": "different selection",
|
||||
"journey_placeholder_heading": "Reading Journey editor coming in #753",
|
||||
"journey_create_submit": "Create reading journey",
|
||||
"journey_item_open_aria": "Open letter from {date}",
|
||||
"journey_item_open_aria_undated": "Open letter",
|
||||
"journey_empty_state": "This reading journey is still empty.",
|
||||
|
||||
@@ -1170,7 +1170,7 @@
|
||||
"journey_selector_journey_desc": "Una selección curada de cartas con notas.",
|
||||
"journey_selector_next_btn": "Continuar",
|
||||
"journey_placeholder_back": "otra selección",
|
||||
"journey_placeholder_heading": "Editor de viaje de lectura próximamente en #753",
|
||||
"journey_create_submit": "Crear viaje de lectura",
|
||||
"journey_item_open_aria": "Abrir carta del {date}",
|
||||
"journey_item_open_aria_undated": "Abrir carta",
|
||||
"journey_empty_state": "Este viaje de lectura está vacío.",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { m } from '$lib/paraglide/messages.js';
|
||||
import BackButton from '$lib/shared/primitives/BackButton.svelte';
|
||||
import TypeSelector from './TypeSelector.svelte';
|
||||
import StoryCreate from './StoryCreate.svelte';
|
||||
import JourneyCreate from './JourneyCreate.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
@@ -19,12 +20,7 @@ let { data }: { data: PageData } = $props();
|
||||
{#if data.selectedType === 'STORY'}
|
||||
<StoryCreate initialPersons={data.initialPersons} />
|
||||
{:else if data.selectedType === 'JOURNEY'}
|
||||
<div data-testid="journey-placeholder">
|
||||
<p class="mb-4 font-sans text-base text-ink-2">{m.journey_placeholder_heading()}</p>
|
||||
<a href="/geschichten/new" class="font-sans text-sm text-ink-3 underline hover:text-ink">
|
||||
{m.journey_placeholder_back()}
|
||||
</a>
|
||||
</div>
|
||||
<JourneyCreate />
|
||||
{:else}
|
||||
<TypeSelector onweiter={(type) => goto(`/geschichten/new?type=${type}`)} />
|
||||
{/if}
|
||||
|
||||
85
frontend/src/routes/geschichten/new/JourneyCreate.svelte
Normal file
85
frontend/src/routes/geschichten/new/JourneyCreate.svelte
Normal file
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { getErrorMessage } from '$lib/shared/errors';
|
||||
import { csrfFetch } from '$lib/shared/cookies';
|
||||
|
||||
let title = $state('');
|
||||
let titleTouched = $state(false);
|
||||
let submitting = $state(false);
|
||||
let errorMessage: string | null = $state(null);
|
||||
|
||||
const titleEmpty = $derived(title.trim().length === 0);
|
||||
const showTitleError = $derived(titleEmpty && titleTouched);
|
||||
|
||||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
titleTouched = true;
|
||||
if (titleEmpty) return;
|
||||
|
||||
submitting = true;
|
||||
errorMessage = null;
|
||||
try {
|
||||
const res = await csrfFetch('/api/geschichten', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: title.trim(),
|
||||
type: 'JOURNEY',
|
||||
status: 'DRAFT',
|
||||
body: '',
|
||||
personIds: []
|
||||
})
|
||||
});
|
||||
if (!res.ok) {
|
||||
const code = (await res.json().catch(() => ({})))?.code;
|
||||
errorMessage = getErrorMessage(code);
|
||||
return;
|
||||
}
|
||||
const created = await res.json();
|
||||
goto(`/geschichten/${created.id}/edit`);
|
||||
} finally {
|
||||
submitting = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<form onsubmit={handleSubmit} class="max-w-lg">
|
||||
{#if errorMessage}
|
||||
<div
|
||||
class="mb-4 rounded border border-danger bg-danger/10 p-3 text-sm text-danger"
|
||||
role="alert"
|
||||
>
|
||||
{errorMessage}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mb-4">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={title}
|
||||
onblur={() => (titleTouched = true)}
|
||||
placeholder={m.geschichte_editor_title_placeholder()}
|
||||
aria-invalid={showTitleError}
|
||||
class="block w-full rounded border px-3 py-2 font-serif text-lg text-ink placeholder:text-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring {showTitleError
|
||||
? 'border-danger'
|
||||
: 'border-line'}"
|
||||
/>
|
||||
{#if showTitleError}
|
||||
<p class="mt-1 font-sans text-xs text-danger">{m.geschichte_editor_title_required()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
class="rounded bg-brand-navy px-4 py-2 font-sans text-sm font-medium text-white hover:opacity-90 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:opacity-50"
|
||||
>
|
||||
{m.journey_create_submit()}
|
||||
</button>
|
||||
<a href="/geschichten/new" class="font-sans text-sm text-ink-3 underline hover:text-ink">
|
||||
{m.journey_placeholder_back()}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
@@ -73,14 +73,13 @@ describe('geschichten/new page', () => {
|
||||
await expect.element(page.getByRole('radiogroup')).toBeVisible();
|
||||
});
|
||||
|
||||
it('shows JOURNEY placeholder when selectedType is JOURNEY', async () => {
|
||||
it('shows JourneyCreate form when selectedType is JOURNEY', async () => {
|
||||
render(GeschichtenNewPage, { props: { data: baseData({ selectedType: 'JOURNEY' }) } });
|
||||
|
||||
const placeholder = document.querySelector('[data-testid="journey-placeholder"]');
|
||||
expect(placeholder).not.toBeNull();
|
||||
await expect.element(page.getByRole('button', { name: /Lesereise erstellen/i })).toBeVisible();
|
||||
});
|
||||
|
||||
it('JOURNEY placeholder offers a return-to-selection link', async () => {
|
||||
it('JOURNEY create form offers a return-to-selection link', async () => {
|
||||
render(GeschichtenNewPage, { props: { data: baseData({ selectedType: 'JOURNEY' }) } });
|
||||
|
||||
const backLink = page.getByRole('link', { name: /andere Auswahl/i });
|
||||
|
||||
Reference in New Issue
Block a user