feat(housekeeping): add per-hotel theme manager with import/export and background
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:
1 parent
55c13b533c
commit
5b4b275b2a
13 files changed
+1376
-1
No files matched your search
@@ -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 }} />
|
||||
|
||||
Reference in new issue
Block a user