feat(housekeeping): add per-hotel theme manager with import/export and background
CI / check (push) Failing after 20s
CI / preflight (push) Skipped
CI / deploy (push) Skipped

Theme Manager under /admin-next/hotel/theme-manager lets the owner save, apply, rename, delete, import, and export custom themes, plus set a custom site background by URL or upload. Themes are stored in WebsiteSetting/custom_themes JSON so they survive CMS updates.
This commit is contained in:
openhands committed 2026-09-16 19:45:57 +02:00
1 parent 55c13b533c
commit 5b4b275b2a
13 files changed
+1376 -1

No files matched your search

+42
View File
@@ -1,4 +1,11 @@
import { siteSettings } from "@/lib/services/site-settings";
import {
BACKGROUND_DEFAULTS,
BACKGROUND_POSITIONS,
BACKGROUND_REPEATS,
BACKGROUND_SIZES,
isSafeBackgroundUrl,
} from "@/lib/theme-bg";
import { themePaletteCss } from "@/lib/theme-css";
import {
FONTS,
@@ -51,6 +58,10 @@ export async function ThemeVars({ nonce }: { nonce?: string } = {}) {
adminTextMuted,
adminBorder,
adminSidebarBg,
backgroundImage,
backgroundPosition,
backgroundSize,
backgroundRepeat,
] = await Promise.all([
g("color_primary", "#f59e0b"),
g("color_background", "#f8fafc"),
@@ -91,6 +102,10 @@ export async function ThemeVars({ nonce }: { nonce?: string } = {}) {
g("admin_text_muted", ""),
g("admin_border", ""),
g("admin_sidebar_bg", ""),
g("background_image", BACKGROUND_DEFAULTS.background_image),
g("background_position", BACKGROUND_DEFAULTS.background_position),
g("background_size", BACKGROUND_DEFAULTS.background_size),
g("background_repeat", BACKGROUND_DEFAULTS.background_repeat),
]);
// Only hex/keyword color values are admin-controlled — sanitize defensively.
@@ -172,10 +187,37 @@ export async function ThemeVars({ nonce }: { nonce?: string } = {}) {
? `https://fonts.googleapis.com/css2?family=${font.google}&display=swap`
: null;
const safeBgPosition = (BACKGROUND_POSITIONS as readonly string[]).includes(
backgroundPosition ?? "",
)
? backgroundPosition
: BACKGROUND_DEFAULTS.background_position;
const safeBgSize = (BACKGROUND_SIZES as readonly string[]).includes(
backgroundSize ?? "",
)
? backgroundSize
: BACKGROUND_DEFAULTS.background_size;
const safeBgRepeat = (BACKGROUND_REPEATS as readonly string[]).includes(
backgroundRepeat ?? "",
)
? backgroundRepeat
: BACKGROUND_DEFAULTS.background_repeat;
const backgroundCss =
backgroundImage && isSafeBackgroundUrl(backgroundImage)
? `body.site-bg{background-image:url("${backgroundImage}");background-position:${safeBgPosition};background-size:${safeBgSize};background-repeat:${safeBgRepeat}}`
: "";
return (
<>
{googleHref ? <link rel="stylesheet" href={googleHref} /> : null}
<style nonce={nonce} dangerouslySetInnerHTML={{ __html: css }} />
{backgroundCss ? (
<style
nonce={nonce}
dangerouslySetInnerHTML={{ __html: backgroundCss }}
/>
) : null}
{customCss?.trim() ? (
// Staff-authored custom CSS (housekeeping → Theme). Trusted input.
<style nonce={nonce} dangerouslySetInnerHTML={{ __html: customCss }} />