feat(lesereisen): frontend — type badge, Journey reader, type selector on new #789
@@ -1,10 +1,11 @@
|
||||
# geschichte (frontend)
|
||||
|
||||
UI for family stories: the rich-text editor, story cards, and story list view.
|
||||
UI for family stories (Geschichte) and reading journeys (Lesereise): the rich-text editor, story/journey readers, type badge, and list rows.
|
||||
|
||||
## What this domain owns
|
||||
|
||||
Components: `GeschichteEditor.svelte`, `GeschichtenCard.svelte`.
|
||||
Components: `GeschichteEditor.svelte`, `GeschichtenCard.svelte`, `GeschichteListRow.svelte`, `StoryReader.svelte`, `JourneyReader.svelte`, `JourneyItemCard.svelte`, `JourneyInterlude.svelte`.
|
||||
Utilities: `utils.ts`.
|
||||
|
||||
## What this domain does NOT own
|
||||
|
||||
@@ -14,14 +15,38 @@ Components: `GeschichteEditor.svelte`, `GeschichtenCard.svelte`.
|
||||
|
||||
## Key components
|
||||
|
||||
| Component | Used in | Notes |
|
||||
| ------------------------- | -------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| `GeschichteEditor.svelte` | `/geschichten/new`, `/geschichten/[id]/edit` | Rich-text editor with person/document @-mentions and inline embeds |
|
||||
| `GeschichtenCard.svelte` | `/geschichten` (list), dashboard | Story preview card with cover image and publish status |
|
||||
| Component | Used in | Notes |
|
||||
| -------------------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| `GeschichteEditor.svelte` | `/geschichten/new`, `/geschichten/[id]/edit` | Rich-text editor with person/document @-mentions and inline embeds |
|
||||
| `GeschichtenCard.svelte` | Person-detail sidebar | Sidebar card showing the 3 most recent stories linked to a person — NOT the list page |
|
||||
| `GeschichteListRow.svelte` | `/geschichten` (list) | Row component for the list; shows JOURNEY type badge (`text-xs` orange pill) |
|
||||
| `StoryReader.svelte` | `/geschichten/[id]` (STORY branch) | Renders sanitised rich-text body, persons section, documents section, and author actions |
|
||||
| `JourneyReader.svelte` | `/geschichten/[id]` (JOURNEY branch) | Renders intro paragraph, ordered items list, empty-state; delegates to ItemCard/Interlude |
|
||||
| `JourneyItemCard.svelte` | `JourneyReader.svelte` | Whole-card `<a>` for a document item; dated/undated aria-label, ✎ annotation glyph |
|
||||
| `JourneyInterlude.svelte` | `JourneyReader.svelte` | Typographic aside between letters; ❦ glyph, `aria-label="Kuratorennotiz"` |
|
||||
|
||||
## utils.ts
|
||||
|
||||
`formatAuthorName(author)` — joins `firstName + lastName`, falls back to `email` (for list/summary shapes).
|
||||
`formatAuthorDisplayName(author)` — returns `displayName` (for detail `AuthorView` shape).
|
||||
`formatPublishedAt(publishedAt, style)` — wraps `formatDate` with null check; `style` is `'short'` (list) or `'long'` (detail).
|
||||
|
||||
## Public list is PUBLISHED-only
|
||||
|
||||
`GET /api/geschichten` constrains `status=PUBLISHED`, so DRAFT journeys never appear in the list.
|
||||
The REISE badge is only ever seen for published journeys.
|
||||
Empty-state and draft-preview paths are reachable ONLY via the **detail route** (`/geschichten/[id]`), not the list.
|
||||
Wire empty-state E2E tests through the detail route, not by expecting a draft journey in the list.
|
||||
|
||||
## TypeSelector route component
|
||||
|
||||
`TypeSelector.svelte` lives in `src/routes/geschichten/new/` (single-use route UI).
|
||||
It is NOT in `$lib/geschichte/` — route-specific, not reused elsewhere.
|
||||
`StoryCreate.svelte` (also in `new/`) wraps `GeschichteEditor` so tree-shaking excludes TipTap from the JOURNEY placeholder path.
|
||||
|
||||
## Audience note
|
||||
|
||||
The `/geschichten` route primarily serves readers (younger family members on mobile). Cards must have ≥ 44 px touch targets. Status must not rely on color alone.
|
||||
The `/geschichten` route primarily serves readers (younger family members on mobile). Cards must have ≥ 44 px touch targets. Status must not rely on color alone. JourneyReader mobile layout is Critical; TypeSelector is Minor.
|
||||
|
||||
## Cross-domain imports
|
||||
|
||||
|
||||
28
frontend/src/lib/geschichte/utils.ts
Normal file
28
frontend/src/lib/geschichte/utils.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { formatDate } from '$lib/shared/utils/date';
|
||||
|
||||
type AuthorSummary = { firstName?: string; lastName?: string; email: string };
|
||||
type AuthorView = { displayName: string };
|
||||
|
||||
/** Format an author name from a list summary (firstName + lastName, falling back to email). */
|
||||
export function formatAuthorName(author: AuthorSummary | null | undefined): string {
|
||||
if (!author) return '';
|
||||
const full = [author.firstName, author.lastName].filter(Boolean).join(' ').trim();
|
||||
return full || author.email || '';
|
||||
}
|
||||
|
||||
/** Format an author displayName from a detail view. */
|
||||
export function formatAuthorDisplayName(author: AuthorView | null | undefined): string {
|
||||
return author?.displayName ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a publishedAt ISO string to a localised date string.
|
||||
* Returns null when publishedAt is absent.
|
||||
*/
|
||||
export function formatPublishedAt(
|
||||
publishedAt: string | null | undefined,
|
||||
style: 'short' | 'long' = 'short'
|
||||
): string | null {
|
||||
if (!publishedAt) return null;
|
||||
return formatDate(publishedAt.slice(0, 10), style);
|
||||
}
|
||||
Reference in New Issue
Block a user