From a284bbc1886b26091bea7ea7a5aacee71b4507fe Mon Sep 17 00:00:00 2001 From: Schubert Ferenc Date: Sat, 4 Jul 2026 11:01:29 +0200 Subject: [PATCH] feat(cms): improve admin content editing --- .gitignore | 1 + Dockerfile | 2 +- README.md | 16 ++- app/admin/page.tsx | 29 +++- app/admin/system/page.tsx | 16 +++ app/api/admin/content/route.ts | 2 +- app/funkgeraete-service/page.tsx | 3 +- app/globals.css | 44 +++++- app/kontakt/page.tsx | 3 +- app/layout.tsx | 3 +- app/leistungen/page.tsx | 3 +- app/page.tsx | 8 +- app/reparatur/page.tsx | 31 +++-- app/seo.ts | 22 +-- app/ueber-uns/page.tsx | 3 +- components/Footer.tsx | 2 +- components/Header.tsx | 2 +- components/admin/ContentManager.tsx | 201 +++++++++++++++++----------- docker-compose.yml | 2 +- lib/content/defaults.ts | 29 ++++ lib/content/service.ts | 31 ++++- lib/content/types.ts | 10 ++ lib/runtime/version.ts | 2 +- package-lock.json | 40 +++--- package.json | 2 +- scripts/runtime-start.js | 2 +- 26 files changed, 362 insertions(+), 147 deletions(-) diff --git a/.gitignore b/.gitignore index b537029..7fccc25 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ data/smtp-settings.json storage/uploads/images/* storage/config/* storage/content/* +storage/content/backups/* !storage/.gitkeep !storage/config/.gitkeep !storage/content/.gitkeep diff --git a/Dockerfile b/Dockerfile index 94b7f82..2dffadb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ FROM node:22-alpine AS runner WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 -ENV NEXT_PUBLIC_APP_VERSION=0.4.0 +ENV NEXT_PUBLIC_APP_VERSION=0.4.1 ENV DOCKER_ENV=true ENV PORT=3010 diff --git a/README.md b/README.md index 591218f..bdea9cb 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Eigenständige öffentliche Firmenwebsite für Funktechnik Schubert. Dieses Proj - Nginx/Reverse-Proxy-fähig - SEO Metadata, Sitemap und robots.txt -Version: `0.4.0` +Version: `0.4.1` ## Seiten @@ -180,6 +180,7 @@ Website-Inhalte liegen unter `storage/content/` und werden nicht committed: storage/content/home.json storage/content/services.json storage/content/radio-service.json +storage/content/repair.json storage/content/about.json storage/content/contact.json storage/content/settings.json @@ -193,6 +194,7 @@ Bearbeitbar sind aktuell: - Startseite mit Hero, Call-to-Actions, Featureboxen, Leistungsboxen und Kundenversprechen - Leistungen mit beliebig vielen sortierbaren und ein-/ausblendbaren Einträgen - Funkgeräte-Service mit Einleitung, Marken, Fehlerbildern, Ablauf, Messmöglichkeiten, Abgleich und Reparaturtexten +- Reparaturannahme mit SEO-Daten und Seitentexten - Über Uns mit Firmenbeschreibung, Werkstattbeschreibung und Philosophie - Kontakt mit Kontaktinformationen, Öffnungszeiten, Maps-Link und Seitentexten - Footer, Logo, Copyright, Header-CTA und globale SEO-Einstellungen @@ -200,6 +202,16 @@ Bearbeitbar sind aktuell: Bilder für Logo und Social Image werden aus dem Medienbestand ausgewählt. Neue Uploads bleiben im privaten Storage und werden über `/api/media/[filename]` ausgeliefert. +## Roadmap Kundenportal + +Ein Kundenportal ist vorbereitet, aber noch nicht öffentlich implementiert. Für spätere Ausbaustufen sind folgende Routen vorgesehen: + +- `/status`: öffentliche Status-Einstiegsseite +- `/reparatur/status`: Statusabfrage für Reparaturanfragen +- `/portal/login`: geschützter Kundenlogin + +Diese Routen sind bewusst noch nicht angelegt. Die spätere Umsetzung soll serverseitig erfolgen, ohne Tokens im Browser-JavaScript zu speichern und ohne bestehende Admin- oder CMS-Funktionen zu umgehen. + ## Healthcheck ```bash @@ -217,7 +229,7 @@ Antwort: ```json { "status": "ok", - "version": "0.4.0", + "version": "0.4.1", "storage": "ok", "admin": "configured", "smtp": "configured", diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 9142ef2..5062996 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -1,8 +1,11 @@ import { redirect } from "next/navigation"; import AdminShell from "@/components/admin/AdminShell"; import { requireAdminSession } from "@/lib/admin/auth"; +import { listMediaFiles } from "@/lib/admin/media"; import { getContactInquiries, getRepairInquiries } from "@/lib/admin/store"; +import { getContentSummary } from "@/lib/content/service"; import { getSmtpSettings, isSmtpConfigured } from "@/lib/mail/config"; +import { appVersion, checkStorage } from "@/lib/runtime/config"; function countNew(items: T[]) { return items.filter((item) => item.status === "new").length; @@ -10,8 +13,23 @@ function countNew(items: T[]) { export default async function AdminDashboardPage() { if (!await requireAdminSession()) redirect("/admin/login"); - const [contacts, repairs, smtpSettings] = await Promise.all([getContactInquiries(), getRepairInquiries(), getSmtpSettings()]); + let healthStatus = "OK"; + + try { + await checkStorage(); + } catch { + healthStatus = "Fehler"; + } + + const [contacts, repairs, smtpSettings, mediaFiles, contentSummary] = await Promise.all([ + getContactInquiries(), + getRepairInquiries(), + getSmtpSettings(), + listMediaFiles(), + getContentSummary(), + ]); const latest = [...contacts, ...repairs].sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, 6); + const contentModified = contentSummary.lastModified ? new Date(contentSummary.lastModified).toLocaleString("de-DE") : "keine Änderung"; return ( @@ -20,10 +38,13 @@ export default async function AdminDashboardPage() {

Dashboard

-
Kontaktanfragen{countNew(contacts)}neu
-
Reparaturanfragen{countNew(repairs)}neu
-
WebsiteOnlineOK
+
Kontaktanfragen{contacts.length}{countNew(contacts)} neu
+
Reparaturanfragen{repairs.length}{countNew(repairs)} neu
+
Medien{mediaFiles.length}im Storage
+
Content{contentSummary.pageCount}{contentModified}
SMTP{isSmtpConfigured(smtpSettings) ? "OK" : "Fehlt"}{smtpSettings.lastTestStatus ?? "kein Test"}
+
Versionv{appVersion}Website
+
Health{healthStatus}Storage und Runtime

Letzte Anfragen

diff --git a/app/admin/system/page.tsx b/app/admin/system/page.tsx index 35d1cea..08aff5a 100644 --- a/app/admin/system/page.tsx +++ b/app/admin/system/page.tsx @@ -52,6 +52,22 @@ export default async function AdminSystemPage() {
  • SMTP-Versand ist serverseitig aktiv, sofern eine vollständige Konfiguration gespeichert ist.
  • +
    +

    Letzte Content-Backups

    + + + + {contentStatus.backups.map((backup) => ( + + + + + + ))} + {contentStatus.backups.length === 0 && } + +
    SeiteDatumDateiname
    {backup.page}{new Date(backup.createdAt).toLocaleString("de-DE")}{backup.fileName}
    Noch keine Content-Backups vorhanden.
    +
    ); } diff --git a/app/api/admin/content/route.ts b/app/api/admin/content/route.ts index dc2c9b8..5725549 100644 --- a/app/api/admin/content/route.ts +++ b/app/api/admin/content/route.ts @@ -3,7 +3,7 @@ import { requireAdminSession } from "@/lib/admin/auth"; import { getAllContent, saveContent } from "@/lib/content/service"; import type { ContentDocuments, ContentKey } from "@/lib/content/types"; -const keys: ContentKey[] = ["home", "services", "radio-service", "about", "contact", "settings"]; +const keys: ContentKey[] = ["home", "services", "radio-service", "repair", "about", "contact", "settings"]; function isContentKey(value: unknown): value is ContentKey { return typeof value === "string" && keys.includes(value as ContentKey); diff --git a/app/funkgeraete-service/page.tsx b/app/funkgeraete-service/page.tsx index 2f58d11..e814446 100644 --- a/app/funkgeraete-service/page.tsx +++ b/app/funkgeraete-service/page.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; import PageHero from "@/components/PageHero"; +import { defaultContent } from "@/lib/content/defaults"; import { getContent } from "@/lib/content/service"; import { createContentMetadata } from "../seo"; @@ -7,7 +8,7 @@ export const dynamic = "force-dynamic"; export async function generateMetadata() { const content = await getContent("radio-service"); - return createContentMetadata(content.seo); + return createContentMetadata(content.seo, defaultContent["radio-service"].seo); } export default async function RadioServicePage() { diff --git a/app/globals.css b/app/globals.css index 8ffb79d..1405840 100644 --- a/app/globals.css +++ b/app/globals.css @@ -630,7 +630,7 @@ h3 { .admin-kpis { display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 14px; margin-bottom: 18px; } @@ -1039,6 +1039,11 @@ h3 { margin-bottom: 0; } +.content-save-actions { + display: flex; + justify-content: flex-end; +} + .content-editor-heading { margin: 14px 0 0; border-top: 1px solid var(--line); @@ -1094,6 +1099,27 @@ h3 { padding: 14px; } +.content-image-picker { + display: grid; + gap: 10px; +} + +.content-image-preview { + display: grid; + grid-template-columns: minmax(140px, 240px) auto; + gap: 12px; + align-items: end; +} + +.content-image-preview img { + width: 100%; + aspect-ratio: 16 / 9; + border: 1px solid var(--line); + border-radius: 8px; + background: #f8fafc; + object-fit: cover; +} + .content-preview-card { overflow: hidden; } @@ -1110,6 +1136,10 @@ h3 { margin-bottom: 0; } +.content-preview-toolbar .eyebrow { + margin-bottom: 4px; +} + .content-preview-toolbar div { display: flex; flex-wrap: wrap; @@ -1273,4 +1303,16 @@ h3 { .content-field-grid { grid-template-columns: 1fr; } + + .content-editor-top, + .content-preview-toolbar, + .content-image-preview { + grid-template-columns: 1fr; + align-items: stretch; + } + + .content-editor-top, + .content-preview-toolbar { + display: grid; + } } diff --git a/app/kontakt/page.tsx b/app/kontakt/page.tsx index 4dd02b2..da1c722 100644 --- a/app/kontakt/page.tsx +++ b/app/kontakt/page.tsx @@ -1,5 +1,6 @@ import PageHero from "@/components/PageHero"; import ContactForm from "@/components/ContactForm"; +import { defaultContent } from "@/lib/content/defaults"; import { getContent } from "@/lib/content/service"; import { createContentMetadata } from "../seo"; @@ -7,7 +8,7 @@ export const dynamic = "force-dynamic"; export async function generateMetadata() { const content = await getContent("contact"); - return createContentMetadata(content.seo); + return createContentMetadata(content.seo, defaultContent.contact.seo); } export default async function ContactPage() { diff --git a/app/layout.tsx b/app/layout.tsx index 565bf30..7d808f5 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,12 +1,13 @@ import type { Metadata } from "next"; import SiteFrame from "@/components/SiteFrame"; import "./globals.css"; +import { defaultContent } from "@/lib/content/defaults"; import { getContent } from "@/lib/content/service"; import { createContentMetadata } from "./seo"; export async function generateMetadata(): Promise { const settings = await getContent("settings"); - return createContentMetadata(settings.seo); + return createContentMetadata(settings.seo, defaultContent.settings.seo); } export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { diff --git a/app/leistungen/page.tsx b/app/leistungen/page.tsx index 20afa89..092e4e2 100644 --- a/app/leistungen/page.tsx +++ b/app/leistungen/page.tsx @@ -1,5 +1,6 @@ import PageHero from "@/components/PageHero"; import ServiceCard from "@/components/ServiceCard"; +import { defaultContent } from "@/lib/content/defaults"; import { getContent } from "@/lib/content/service"; import { createContentMetadata } from "../seo"; @@ -7,7 +8,7 @@ export const dynamic = "force-dynamic"; export async function generateMetadata() { const content = await getContent("services"); - return createContentMetadata(content.seo); + return createContentMetadata(content.seo, defaultContent.services.seo); } export default async function ServicesPage() { diff --git a/app/page.tsx b/app/page.tsx index 4d43644..0095e36 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; import ServiceCard from "@/components/ServiceCard"; +import { defaultContent } from "@/lib/content/defaults"; import { getContent } from "@/lib/content/service"; import { createContentMetadata } from "./seo"; @@ -7,7 +8,7 @@ export const dynamic = "force-dynamic"; export async function generateMetadata() { const content = await getContent("home"); - return createContentMetadata(content.seo); + return createContentMetadata(content.seo, defaultContent.home.seo); } export default async function HomePage() { @@ -15,7 +16,10 @@ export default async function HomePage() { return ( <> -
    +

    {content.eyebrow}

    {content.title}

    diff --git a/app/reparatur/page.tsx b/app/reparatur/page.tsx index 22989f5..ff1cf59 100644 --- a/app/reparatur/page.tsx +++ b/app/reparatur/page.tsx @@ -1,27 +1,32 @@ -import type { Metadata } from "next"; import PageHero from "@/components/PageHero"; import RepairForm from "@/components/RepairForm"; -import { createMetadata } from "../seo"; +import { defaultContent } from "@/lib/content/defaults"; +import { getContent } from "@/lib/content/service"; +import { createContentMetadata } from "../seo"; -export const metadata: Metadata = createMetadata("Reparaturannahme", "Reparaturanfrage für Funkgeräte strukturiert vorbereiten.", "/reparatur"); +export const dynamic = "force-dynamic"; + +export async function generateMetadata() { + const content = await getContent("repair"); + return createContentMetadata(content.seo, defaultContent.repair.seo); +} + +export default async function RepairPage() { + const content = await getContent("repair"); + const paragraph = content.paragraphs[0]; -export default function RepairPage() { return ( <> - - Beschreiben Sie Gerät, Fehlerbild, Zubehör und bisherige Vorarbeiten. Die Anfrage wird strukturiert erfasst und für eine spätere technische Bearbeitung vorbereitet. + + {content.subtitle}
    -

    Vor der Einsendung

    -

    Bitte senden Sie Geräte erst nach Rückmeldung ein. Vollständige Gerätedaten und eine präzise Fehlerbeschreibung helfen, den Aufwand besser einzuschätzen.

    +

    {paragraph?.title || content.cta.text}

    +

    {paragraph?.text || content.intro}

      -
    • Hersteller, Modell, Geräteart und Seriennummer bereithalten, soweit vorhanden
    • -
    • Fehler möglichst konkret beschreiben: Empfang, Sendung, Modulation, Frequenz, Anzeige oder Versorgung
    • -
    • Zubehör wie Netzteil, Mikrofon, Antennenadapter oder Kabel angeben
    • -
    • Bereits geöffnete Geräte und durchgeführte Arbeiten ehrlich nennen
    • -
    • Fotos können später nach Rückmeldung ergänzt werden
    • + {content.list.map((item) =>
    • {item}
    • )}
    diff --git a/app/seo.ts b/app/seo.ts index 2baa7ef..8fb7d2d 100644 --- a/app/seo.ts +++ b/app/seo.ts @@ -60,28 +60,32 @@ export function createMetadata(title: string, description: string, path = "/"): }; } -export function createContentMetadata(seo: SeoContent): Metadata { - const path = seo.canonicalUrl || "/"; +export function createContentMetadata(seo: SeoContent, fallbackSeo?: SeoContent): Metadata { + const resolvedSeo = { + ...fallbackSeo, + ...Object.fromEntries(Object.entries(seo).filter(([, value]) => value.trim() !== "")), + } as SeoContent; + const path = resolvedSeo.canonicalUrl || "/"; const url = new URL(path, siteUrl).toString(); - const title = seo.metaTitle || siteName; - const description = seo.metaDescription || defaultTitle; + const title = resolvedSeo.metaTitle || siteName; + const description = resolvedSeo.metaDescription || defaultTitle; return { ...createMetadata(title, description, path), - keywords: seo.keywords ? seo.keywords.split(",").map((keyword) => keyword.trim()).filter(Boolean) : keywords, + keywords: resolvedSeo.keywords ? resolvedSeo.keywords.split(",").map((keyword) => keyword.trim()).filter(Boolean) : keywords, openGraph: { type: "website", locale: "de_DE", url, siteName, - title: seo.openGraphTitle || title, - description: seo.openGraphDescription || description, + title: resolvedSeo.openGraphTitle || title, + description: resolvedSeo.openGraphDescription || description, images: [ { - url: seo.socialImage || "/funktechnik_schubert_logo.jpg", + url: resolvedSeo.socialImage || "/funktechnik_schubert_logo.jpg", width: 1200, height: 630, - alt: seo.openGraphTitle || title, + alt: resolvedSeo.openGraphTitle || title, }, ], }, diff --git a/app/ueber-uns/page.tsx b/app/ueber-uns/page.tsx index 74a8c31..51b630b 100644 --- a/app/ueber-uns/page.tsx +++ b/app/ueber-uns/page.tsx @@ -1,4 +1,5 @@ import PageHero from "@/components/PageHero"; +import { defaultContent } from "@/lib/content/defaults"; import { getContent } from "@/lib/content/service"; import { createContentMetadata } from "../seo"; @@ -6,7 +7,7 @@ export const dynamic = "force-dynamic"; export async function generateMetadata() { const content = await getContent("about"); - return createContentMetadata(content.seo); + return createContentMetadata(content.seo, defaultContent.about.seo); } export default async function AboutPage() { diff --git a/components/Footer.tsx b/components/Footer.tsx index 926df5b..00932ee 100644 --- a/components/Footer.tsx +++ b/components/Footer.tsx @@ -61,7 +61,7 @@ export default function Footer() {
    {settings.siteName}; -const tabs: Array<{ id: ContentTabId; key: ContentKey; label: string }> = [ - { id: "home", key: "home", label: "Startseite" }, - { id: "services", key: "services", label: "Leistungen" }, - { id: "radio-service", key: "radio-service", label: "Funkgeräte-Service" }, - { id: "about", key: "about", label: "Über Uns" }, - { id: "contact", key: "contact", label: "Kontakt" }, - { id: "footer", key: "settings", label: "Footer" }, - { id: "global-seo", key: "settings", label: "SEO" }, - { id: "general-settings", key: "settings", label: "Allgemeine Einstellungen" }, +const tabs: Array<{ id: ContentTabId; label: string }> = [ + { id: "home", label: "Startseite" }, + { id: "services", label: "Leistungen" }, + { id: "radio-service", label: "Funkgeräte-Service" }, + { id: "about", label: "Über uns" }, + { id: "contact", label: "Kontakt" }, + { id: "footer", label: "Footer" }, + { id: "seo", label: "SEO" }, +]; + +const seoPages: Array<{ key: SeoPageKey; label: string }> = [ + { key: "home", label: "Startseite" }, + { key: "services", label: "Leistungen" }, + { key: "radio-service", label: "Funkgeräte-Service" }, + { key: "repair", label: "Reparatur" }, + { key: "about", label: "Über uns" }, + { key: "contact", label: "Kontakt" }, ]; type Props = { @@ -28,6 +39,12 @@ type ApiResponse = { content?: ContentDocuments[K]; }; +function contentKeyForTab(tab: ContentTabId, seoPage: SeoPageKey): ContentKey { + if (tab === "footer") return "settings"; + if (tab === "seo") return seoPage; + return tab; +} + function linesToText(lines: string[]) { return lines.join("\n"); } @@ -58,23 +75,41 @@ function TextArea({ label, value, onChange, rows = 4 }: { label: string; value: ); } -function ImageSelect({ label, value, mediaFiles, onChange }: { label: string; value: string; mediaFiles: MediaFile[]; onChange: (value: string) => void }) { +function SectionTitle({ children }: { children: string }) { + return

    {children}

    ; +} + +function ImagePicker({ label, value, mediaFiles, onChange, fallback = "" }: { label: string; value: string; mediaFiles: MediaFile[]; fallback?: string; onChange: (value: string) => void }) { const images = mediaFiles.filter((file) => file.isImage); + const preview = value || fallback; return ( - +
    + + {preview && ( +
    + + +
    + )} +
    ); } -function SectionTitle({ children }: { children: string }) { - return

    {children}

    ; +function SaveActions({ pending, onSave }: { pending: boolean; onSave: () => void }) { + return ( +
    + +
    + ); } function BlocksEditor({ blocks, onChange }: { blocks: TextBlockContent[]; onChange: (blocks: TextBlockContent[]) => void }) { @@ -96,7 +131,7 @@ function BlocksEditor({ blocks, onChange }: { blocks: TextBlockContent[]; onChan ); } -function PageFields({ page, mediaFiles, onChange }: { page: PageContent; mediaFiles: MediaFile[]; onChange: (page: PageContent) => void }) { +function PageFields({ page, onChange }: { page: PageContent; onChange: (page: PageContent) => void }) { return ( <> Hero @@ -118,20 +153,24 @@ function PageFields({ page, mediaFiles, onChange }: { page: PageContent; mediaFi onChange({ ...page, cta: { ...page.cta, secondary: updateLink(page.cta.secondary, "label", value) } })} /> onChange({ ...page, cta: { ...page.cta, secondary: updateLink(page.cta.secondary, "href", value) } })} />
    - SEO -
    - onChange({ ...page, seo: { ...page.seo, metaTitle: value } })} /> - onChange({ ...page, seo: { ...page.seo, metaDescription: value } })} /> - onChange({ ...page, seo: { ...page.seo, keywords: value } })} /> - onChange({ ...page, seo: { ...page.seo, openGraphTitle: value } })} /> - onChange({ ...page, seo: { ...page.seo, openGraphDescription: value } })} /> - onChange({ ...page, seo: { ...page.seo, socialImage: value } })} /> - onChange({ ...page, seo: { ...page.seo, canonicalUrl: value } })} /> -
    ); } +function SeoFields({ seo, mediaFiles, onChange }: { seo: SeoContent; mediaFiles: MediaFile[]; onChange: (seo: SeoContent) => void }) { + return ( +
    + onChange({ ...seo, metaTitle: value })} /> + onChange({ ...seo, metaDescription: value })} /> + onChange({ ...seo, keywords: value })} /> + onChange({ ...seo, openGraphTitle: value })} /> + onChange({ ...seo, openGraphDescription: value })} /> + onChange({ ...seo, socialImage: value })} /> + onChange({ ...seo, canonicalUrl: value })} /> +
    + ); +} + function ServicesEditor({ services, onChange }: { services: ServiceContent[]; onChange: (services: ServiceContent[]) => void }) { function update(index: number, field: keyof ServiceContent, value: string | number | boolean) { onChange(services.map((service, current) => current === index ? { ...service, [field]: value } : service)); @@ -165,14 +204,16 @@ function ServicesEditor({ services, onChange }: { services: ServiceContent[]; on export default function ContentManager({ initialContent, mediaFiles }: Props) { const [content, setContent] = useState(initialContent); const [activeTab, setActiveTab] = useState("home"); + const [seoPage, setSeoPage] = useState("home"); const [viewport, setViewport] = useState("desktop"); const [message, setMessage] = useState(""); const [error, setError] = useState(""); const [pending, setPending] = useState(false); - const active = tabs.find((tab) => tab.id === activeTab)?.key ?? "home"; + const active = contentKeyForTab(activeTab, seoPage); const current = content[active]; - const activeLabel = useMemo(() => tabs.find((tab) => tab.id === activeTab)?.label ?? active, [active, activeTab]); + const activeLabel = useMemo(() => tabs.find((tab) => tab.id === activeTab)?.label ?? "Startseite", [activeTab]); + const pagePreview = "title" in current ? current : content.home; function update(key: K, value: ContentDocuments[K]) { setContent((previous) => ({ ...previous, [key]: value })); @@ -190,17 +231,17 @@ export default function ContentManager({ initialContent, mediaFiles }: Props) { body: JSON.stringify({ key: active, content: current }), }); const result = await response.json() as ApiResponse; - if (!response.ok || !result.content) throw new Error(result.message ?? "Inhalt konnte nicht gespeichert werden."); + if (!response.ok || !result.content) throw new Error("Inhalt konnte nicht gespeichert werden."); update(active, result.content); - setMessage(result.message ?? "Inhalt gespeichert."); - } catch (err) { - setError(err instanceof Error ? err.message : "Inhalt konnte nicht gespeichert werden."); + setMessage("Inhalt wurde gespeichert."); + } catch { + setError("Der Inhalt konnte nicht gespeichert werden. Bitte prüfen Sie die Eingaben und versuchen Sie es erneut."); } finally { setPending(false); } } - const pagePreview = "title" in current ? current : content.home; + const heroBackground = content.home.heroImage || "/workbench-signal.svg"; return (
    @@ -208,8 +249,8 @@ export default function ContentManager({ initialContent, mediaFiles }: Props) { {error &&

    {error}

    }
    - +
    - {"title" in current && ( - update(active, value as ContentDocuments[typeof active])} /> - )} - - {active === "home" && ( + {activeTab === "home" && ( <> + update("home", { ...content.home, ...value })} /> + Medien + update("home", { ...content.home, heroImage: value })} /> Homepage Builder