Add admin site settings sections and locale defaults

This commit is contained in:
MOH
2026-03-15 04:06:33 +01:00
parent 3e9edc5873
commit 5d3f77962a
21 changed files with 993 additions and 456 deletions
+77 -35
View File
@@ -12,10 +12,16 @@ import {
SITE_SETTINGS_FAVICON_FIELD_KEY,
SITE_SETTINGS_LOGO_DARK_FIELD_KEY,
SITE_SETTINGS_LOGO_LIGHT_FIELD_KEY,
getSiteSettings,
updateSiteSettings,
} from "@/lib/app-config";
import { getAdminAppPath, toInternalAdminPath } from "@/lib/admin-routing";
import { PAGE_TITLE_TOKEN, type SiteSettings } from "@/lib/site-settings";
import {
PAGE_TITLE_TOKEN,
normalizeSiteDefaultLocale,
normalizeSitePrimaryColor,
type SiteSettings,
} from "@/lib/site-settings";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { replaceEntityMediaUsages } from "@/lib/media";
import { resolveMediaSelection } from "@/lib/media-service";
@@ -91,12 +97,17 @@ async function revalidateSiteSettingsPages() {
}
export async function saveSiteSettingsAction(formData: FormData) {
return saveSiteBrandSettingsAction(formData);
}
export async function saveSiteBrandSettingsAction(formData: FormData) {
await ensureAdmin();
const createdMediaAssetIds: string[] = [];
const uploadedPaths: string[] = [];
try {
const currentSettings = await getSiteSettings();
const siteLogoLightMedia = parseJsonObject(formData.get("siteLogoLightMedia"), "siteLogoLightMedia");
const siteLogoDarkMedia = parseJsonObject(formData.get("siteLogoDarkMedia"), "siteLogoDarkMedia");
const faviconMedia = parseJsonObject(formData.get("faviconMedia"), "faviconMedia");
@@ -106,41 +117,12 @@ export async function saveSiteSettingsAction(formData: FormData) {
);
const parsedSettings: SiteSettings = {
locales: {
ar: {
siteName: String(formData.get("siteNameAr") ?? "").trim(),
titleTemplate: String(formData.get("titleTemplateAr") ?? "").trim(),
siteDescription: String(formData.get("siteDescriptionAr") ?? "").trim(),
subhead: String(formData.get("subheadAr") ?? "").trim(),
},
en: {
siteName: String(formData.get("siteNameEn") ?? "").trim(),
titleTemplate: String(formData.get("titleTemplateEn") ?? "").trim(),
siteDescription: String(formData.get("siteDescriptionEn") ?? "").trim(),
subhead: String(formData.get("subheadEn") ?? "").trim(),
},
de: {
siteName: String(formData.get("siteNameDe") ?? "").trim(),
titleTemplate: String(formData.get("titleTemplateDe") ?? "").trim(),
siteDescription: String(formData.get("siteDescriptionDe") ?? "").trim(),
subhead: String(formData.get("subheadDe") ?? "").trim(),
},
...currentSettings,
brand: {
primaryColor: normalizeSitePrimaryColor(formData.get("primaryColor")),
},
};
for (const locale of routing.locales) {
if (!parsedSettings.locales[locale].siteName) {
throw new Error(`Site Name fuer ${locale} ist erforderlich.`);
}
if (
!parsedSettings.locales[locale].titleTemplate ||
!parsedSettings.locales[locale].titleTemplate.includes(PAGE_TITLE_TOKEN)
) {
throw new Error(`Title Template fuer ${locale} muss {pageTitle} enthalten.`);
}
}
const siteLogoLightSelection = siteLogoLightMedia
? await resolveMediaSelection({
media: mediaFieldInputSchema.parse(siteLogoLightMedia),
@@ -278,7 +260,7 @@ export async function saveSiteSettingsAction(formData: FormData) {
});
await revalidateSiteSettingsPages();
redirect(withMessage(getAdminAppPath("/site-settings"), "success", "Einstellungen gespeichert."));
redirect(withMessage(getAdminAppPath("/site-settings/brand"), "success", "Einstellungen gespeichert."));
} catch (error) {
if (isRedirectError(error)) {
throw error;
@@ -291,6 +273,66 @@ export async function saveSiteSettingsAction(formData: FormData) {
? error.message
: "Einstellungen konnten nicht gespeichert werden.";
redirect(withMessage(getAdminAppPath("/site-settings"), "error", message));
redirect(withMessage(getAdminAppPath("/site-settings/brand"), "error", message));
}
}
export async function saveSiteLocalizationSettingsAction(formData: FormData) {
await ensureAdmin();
try {
const currentSettings = await getSiteSettings();
const parsedSettings: SiteSettings = {
...currentSettings,
defaultLocale: normalizeSiteDefaultLocale(formData.get("defaultLocale")),
locales: {
ar: {
siteName: String(formData.get("siteNameAr") ?? "").trim(),
titleTemplate: String(formData.get("titleTemplateAr") ?? "").trim(),
siteDescription: String(formData.get("siteDescriptionAr") ?? "").trim(),
subhead: String(formData.get("subheadAr") ?? "").trim(),
},
en: {
siteName: String(formData.get("siteNameEn") ?? "").trim(),
titleTemplate: String(formData.get("titleTemplateEn") ?? "").trim(),
siteDescription: String(formData.get("siteDescriptionEn") ?? "").trim(),
subhead: String(formData.get("subheadEn") ?? "").trim(),
},
de: {
siteName: String(formData.get("siteNameDe") ?? "").trim(),
titleTemplate: String(formData.get("titleTemplateDe") ?? "").trim(),
siteDescription: String(formData.get("siteDescriptionDe") ?? "").trim(),
subhead: String(formData.get("subheadDe") ?? "").trim(),
},
},
};
for (const locale of routing.locales) {
if (!parsedSettings.locales[locale].siteName) {
throw new Error(`Site Name fuer ${locale} ist erforderlich.`);
}
if (
!parsedSettings.locales[locale].titleTemplate ||
!parsedSettings.locales[locale].titleTemplate.includes(PAGE_TITLE_TOKEN)
) {
throw new Error(`Title Template fuer ${locale} muss {pageTitle} enthalten.`);
}
}
await updateSiteSettings(parsedSettings);
await revalidateSiteSettingsPages();
redirect(withMessage(getAdminAppPath("/site-settings/localization"), "success", "Einstellungen gespeichert."));
} catch (error) {
if (isRedirectError(error)) {
throw error;
}
const message =
error instanceof Error
? error.message
: "Einstellungen konnten nicht gespeichert werden.";
redirect(withMessage(getAdminAppPath("/site-settings/localization"), "error", message));
}
}
+73
View File
@@ -0,0 +1,73 @@
import { MediaKind } from "@prisma/client";
import { redirect } from "next/navigation";
import { MotionFade } from "@/components/motion-fade";
import { SiteSettingsForm } from "@/components/admin/site-settings-form";
import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getAdminAppPath } from "@/lib/admin-routing";
import {
getSiteSettings,
getSiteSettingsMediaBindings,
} from "@/lib/app-config";
import { getMediaOptions } from "@/lib/media";
import { saveSiteBrandSettingsAction } from "../actions";
export const dynamic = "force-dynamic";
const copy = {
title: "Brand",
subtitle: "Primaerfarbe, Logos, Favicon und OG Assets verwalten.",
overview: "Uebersicht",
maintenance: "Wartungsmodus",
uiKit: "UI Kit",
media: "Media",
siteSettings: "Settings",
brandSettings: "Brand",
localizationSettings: "Localization",
smtp: "SMTP",
portfolio: "Portfolio",
logout: "Ausloggen",
backToSite: "Zur Website",
};
export default async function AdminSiteBrandSettingsPage() {
if (!(await isAdminAuthenticated())) {
redirect(getAdminAppPath("/"));
}
async function logoutAction() {
"use server";
await clearAdminSessionCookie();
redirect(getAdminAppPath("/"));
}
const [siteSettings, mediaBindings, mediaOptions] = await Promise.all([
getSiteSettings(),
getSiteSettingsMediaBindings(),
getMediaOptions({ kind: MediaKind.IMAGE }),
]);
return (
<AdminDashboardShell
copy={copy}
active="site-settings"
siteSettingsChild="brand"
logoutAction={logoutAction}
headerTitle={copy.title}
headerDescription={copy.subtitle}
>
<MotionFade delay={0.16}>
<SiteSettingsForm
mode="brand"
action={saveSiteBrandSettingsAction}
initialSettings={siteSettings}
initialBindings={mediaBindings}
mediaOptions={mediaOptions}
/>
</MotionFade>
</AdminDashboardShell>
);
}
@@ -0,0 +1,67 @@
import { redirect } from "next/navigation";
import { MotionFade } from "@/components/motion-fade";
import { SiteSettingsForm } from "@/components/admin/site-settings-form";
import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getAdminAppPath } from "@/lib/admin-routing";
import {
getDefaultSiteSettingsMediaBindings,
getSiteSettings,
} from "@/lib/app-config";
import { saveSiteLocalizationSettingsAction } from "../actions";
export const dynamic = "force-dynamic";
const copy = {
title: "Localization",
subtitle: "Seitennamen, Titelvorlagen und Standardsprache verwalten.",
overview: "Uebersicht",
maintenance: "Wartungsmodus",
uiKit: "UI Kit",
media: "Media",
siteSettings: "Settings",
brandSettings: "Brand",
localizationSettings: "Localization",
smtp: "SMTP",
portfolio: "Portfolio",
logout: "Ausloggen",
backToSite: "Zur Website",
};
export default async function AdminSiteLocalizationSettingsPage() {
if (!(await isAdminAuthenticated())) {
redirect(getAdminAppPath("/"));
}
async function logoutAction() {
"use server";
await clearAdminSessionCookie();
redirect(getAdminAppPath("/"));
}
const siteSettings = await getSiteSettings();
return (
<AdminDashboardShell
copy={copy}
active="site-settings"
siteSettingsChild="localization"
logoutAction={logoutAction}
headerTitle={copy.title}
headerDescription={copy.subtitle}
>
<MotionFade delay={0.16}>
<SiteSettingsForm
mode="localization"
action={saveSiteLocalizationSettingsAction}
initialSettings={siteSettings}
initialBindings={getDefaultSiteSettingsMediaBindings()}
mediaOptions={[]}
/>
</MotionFade>
</AdminDashboardShell>
);
}
+2 -64
View File
@@ -1,71 +1,9 @@
import { MediaKind } from "@prisma/client";
import { redirect } from "next/navigation";
import { MotionFade } from "@/components/motion-fade";
import { SiteSettingsForm } from "@/components/admin/site-settings-form";
import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
import { getAdminAppPath } from "@/lib/admin-routing";
import {
getSiteSettings,
getSiteSettingsMediaBindings,
} from "@/lib/app-config";
import { getMediaOptions } from "@/lib/media";
import { saveSiteSettingsAction } from "./actions";
export const dynamic = "force-dynamic";
const copy = {
title: "Settings",
subtitle: "Globale Titel, Copy und Brand Assets verwalten.",
overview: "Uebersicht",
maintenance: "Wartungsmodus",
uiKit: "UI Kit",
media: "Media",
siteSettings: "Settings",
smtp: "SMTP",
portfolio: "Portfolio",
logout: "Ausloggen",
backToSite: "Zur Website",
};
export default async function AdminSiteSettingsPage() {
if (!(await isAdminAuthenticated())) {
redirect(getAdminAppPath("/"));
}
async function logoutAction() {
"use server";
await clearAdminSessionCookie();
redirect(getAdminAppPath("/"));
}
const [siteSettings, mediaBindings, mediaOptions] = await Promise.all([
getSiteSettings(),
getSiteSettingsMediaBindings(),
getMediaOptions({ kind: MediaKind.IMAGE }),
]);
return (
<AdminDashboardShell
copy={copy}
active="site-settings"
logoutAction={logoutAction}
headerTitle={copy.title}
headerDescription={copy.subtitle}
>
<div className="space-y-6">
<MotionFade delay={0.16}>
<SiteSettingsForm
action={saveSiteSettingsAction}
initialSettings={siteSettings}
initialBindings={mediaBindings}
mediaOptions={mediaOptions}
/>
</MotionFade>
</div>
</AdminDashboardShell>
);
export default function AdminSiteSettingsPage() {
redirect(getAdminAppPath("/site-settings/brand"));
}
@@ -0,0 +1 @@
export { default } from "../../../_admin/site-settings/brand/page";
@@ -0,0 +1 @@
export { default } from "../../../_admin/site-settings/localization/page";
+20
View File
@@ -0,0 +1,20 @@
import { NextResponse } from "next/server";
import { getSiteSettings } from "@/lib/app-config";
export const dynamic = "force-dynamic";
export async function GET() {
const settings = await getSiteSettings();
return NextResponse.json(
{
defaultLocale: settings.defaultLocale,
},
{
headers: {
"Cache-Control": "no-store, max-age=0",
},
},
);
}
+9 -1
View File
@@ -7,8 +7,10 @@ import { QueryToastBridge } from "@/components/admin/query-toast-bridge";
import { SoundProvider } from "@/components/sound-provider";
import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/toaster";
import { getSiteSettings } from "@/lib/app-config";
import { buildAppMetadata } from "@/lib/metadata";
import { getDirection } from "@/lib/locale";
import { buildSiteThemeStyleText } from "@/lib/site-theme";
import "./globals.css";
const museo = localFont({
@@ -82,10 +84,16 @@ export default async function RootLayout({
}>) {
noStore();
const locale = await getLocale().catch(() => "de");
const [locale, siteSettings] = await Promise.all([
getLocale().catch(() => "de"),
getSiteSettings(),
]);
return (
<html lang={locale} dir={getDirection(locale)} suppressHydrationWarning>
<head>
<style id="dynamic-site-theme">{buildSiteThemeStyleText(siteSettings.brand.primaryColor)}</style>
</head>
<body className={`${museo.variable} ${dubai.variable}`}>
<ThemeProvider>
<SoundProvider>
+1
View File
@@ -0,0 +1 @@
export { default } from "../../../_admin/site-settings/brand/page";
@@ -0,0 +1 @@
export { default } from "../../../_admin/site-settings/localization/page";