Files
GerbilManager/gerbil-manager-web/src/api/pages.ts

168 lines
4.8 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_BASE_URL, api } from './client'
/**
* WEB-3-Fix: Die CMS-Endpunkte liegen unter der /api-Gruppe
* (CmsEndpoints: MapGroup("/api")) — die Aufrufe hier liefen vorher gegen
* /pages und wären gegen die ECHTE API 404 gelaufen (der e2e-Mock hat den
* Unterschied kaschiert, weil er das /api-Präfix normalisiert).
*/
const CMS = '/api'
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[]>(`${CMS}/pages`)
}
export function getPage(slug: string): Promise<Page> {
return api.get<Page>(`${CMS}/pages/${encodeURIComponent(slug)}`)
}
export function updatePage(id: string, input: PageInput): Promise<void> {
return api.put<void>(`${CMS}/pages/${id}`, input)
}
export function addBlock(pageId: string, input: BlockInput): Promise<Block> {
return api.post<Block>(`${CMS}/pages/${pageId}/blocks`, input)
}
export function updateBlock(id: string, input: BlockInput): Promise<void> {
return api.put<void>(`${CMS}/blocks/${id}`, input)
}
export function deleteBlock(id: string): Promise<void> {
return api.delete(`${CMS}/blocks/${id}`)
}
export function reorderBlocks(pageId: string, blockIds: string[]): Promise<void> {
return api.put<void>(`${CMS}/pages/${pageId}/blocks/order`, { blockIds })
}
/**
* WEB-3: Absolute URL der lokalen Vorschau (GET /api/preview/… rendert live).
* "start" liegt auf index.html, alle anderen Seiten auf {slug}/index.html —
* dieselbe Abbildung wie im SiteRenderer.
*/
export function previewUrl(slug?: string | null): string {
const path = !slug || slug === 'start' ? 'index.html' : `${encodeURIComponent(slug)}/index.html`
return `${API_BASE_URL}/api/preview/${path}`
}