Files
GerbilManager/gerbil-manager-web/src/api/pages.ts
Gulum c20e6b5426 WEB-0b: CMS-Verwaltung der öffentlichen Webseite (Hand-Editier-UI)
Neuer Bereich /webseite im Manager (Nav: "Mehr"-Blatt) zur Pflege der 6
Webseiten-Seiten gegen Pams WEB-0-Backend (LAN-only, keine Auth):

- Übersicht: alle Seiten mit Status (Entwurf/Veröffentlicht).
- Seiten-Editor: Titel + SEO-Beschreibung + Status speichern.
- Geordneter Block-Editor: Blöcke hinzufügen (Überschrift, Textabschnitt/
  Markdown, Bild, Galerie, Kontaktangaben), bearbeiten, nach oben/unten
  verschieben (PUT .../blocks/order), löschen. Die dynamische AbgabetiereList
  ist nicht manuell anlegbar; nur ihr Einleitungstext ist editierbar.

Vertraege: GET /pages, GET /pages/{slug}, PUT /pages/{id},
POST /pages/{pageId}/blocks, PUT /blocks/{id}, DELETE /blocks/{id},
PUT /pages/{pageId}/blocks/order. Enums als Strings (Draft/Published, Heading...).

Deutsch via de.ts (LF beibehalten), mobile-first. e2e-Mock um die CMS-Routen
erweitert (zustandsbehaftet) + Smoke (Block anlegen/bearbeiten/verschieben,
bleibt erhalten). Gate gruen: build, eslint, vitest 69, e2e 68.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 11:23:34 +02:00

150 lines
4.0 KiB
TypeScript

/**
* WEB-0b: CMS-Inhalte der öffentlichen Webseite (Seiten + Blöcke).
*
* Backend-Vertrag (GerbilManagerWebAPI/Endpoints/CmsEndpoints.cs):
* - GET /pages -> PageSummary[] (kein Paging!)
* - GET /pages/{slug} -> Page (inkl. Blöcke)
* - PUT /pages/{id} -> 204
* - POST /pages/{pageId}/blocks -> 201 Block
* - PUT /blocks/{id} -> 204
* - DELETE /blocks/{id} -> 204
* - PUT /pages/{pageId}/blocks/order -> 204 (Body: { blockIds })
*
* Enums kommen dank globalem JsonStringEnumConverter als STRINGS
* ("Draft"/"Published", "Heading"/"RichText"/…). Block.data ist ein
* typ-spezifisches JSON-Objekt (siehe BlockData-Typen unten).
*/
import { api } from './client'
export type PageStatus = 'Draft' | 'Published'
export type BlockType =
| 'Heading'
| 'RichText'
| 'Image'
| 'Gallery'
| 'ContactInfo'
| 'AbgabetiereList'
/** Block-Typen, die die Hand-Editier-Oberfläche neu anlegen kann (in Anzeige-Reihenfolge). */
export const ADDABLE_BLOCK_TYPES: readonly BlockType[] = [
'Heading',
'RichText',
'Image',
'Gallery',
'ContactInfo',
] as const
// ── Typ-spezifische Block-Daten ────────────────────────────────────────────
export interface HeadingData {
text: string
level: 2 | 3
}
export interface RichTextData {
/** Markdown-Quelltext (wird erst beim Veröffentlichen gerendert + bereinigt). */
markdown: string
}
export interface ImageData {
url: string
alt: string
}
export interface GalleryImage {
url: string
alt: string
}
export interface GalleryData {
images: GalleryImage[]
}
export interface ContactInfoData {
name: string
email: string
phone: string
address: string
}
export interface AbgabetiereListData {
/** auto = aus den aktuellen Abgabetieren aufgelöst; manual = von Hand gepflegt. */
mode: 'auto' | 'manual'
intro: string
}
export type BlockData = Record<string, unknown>
export interface Block {
id: string
order: number
type: BlockType
data: BlockData
}
export interface PageSummary {
id: string
slug: string
title: string
seoDescription: string | null
status: PageStatus
}
export interface Page extends PageSummary {
blocks: Block[]
}
export interface PageInput {
slug: string
title: string
seoDescription: string | null
status: PageStatus
}
export interface BlockInput {
type: BlockType
data: BlockData
order?: number
}
/** Sinnvolle Startwerte für einen neu angelegten Block. */
export function defaultBlockData(type: BlockType): BlockData {
switch (type) {
case 'Heading':
return { text: '', level: 2 }
case 'RichText':
return { markdown: '' }
case 'Image':
return { url: '', alt: '' }
case 'Gallery':
return { images: [] }
case 'ContactInfo':
return { name: '', email: '', phone: '', address: '' }
case 'AbgabetiereList':
return { mode: 'auto', intro: '' }
}
}
// ── API-Aufrufe ──────────────────────────────────────────────────────────────
export function listPages(): Promise<PageSummary[]> {
return api.get<PageSummary[]>('/pages')
}
export function getPage(slug: string): Promise<Page> {
return api.get<Page>(`/pages/${encodeURIComponent(slug)}`)
}
export function updatePage(id: string, input: PageInput): Promise<void> {
return api.put<void>(`/pages/${id}`, input)
}
export function addBlock(pageId: string, input: BlockInput): Promise<Block> {
return api.post<Block>(`/pages/${pageId}/blocks`, input)
}
export function updateBlock(id: string, input: BlockInput): Promise<void> {
return api.put<void>(`/blocks/${id}`, input)
}
export function deleteBlock(id: string): Promise<void> {
return api.delete(`/blocks/${id}`)
}
export function reorderBlocks(pageId: string, blockIds: string[]): Promise<void> {
return api.put<void>(`/pages/${pageId}/blocks/order`, { blockIds })
}