feat: add scoped theme builder system with per-route, per-module, and multi-site support
- Add theme_scopes and theme_scope_values database tables for scoped themes - Implement theme resolver engine with inheritance: global > site > module > route - Add module detection for 20+ routes (shop, guilds, radio, news, etc.) - Create admin UI at /admin/theme-builder with scope tree and color editor - Add ScopedThemeVars component for injecting scoped CSS via data-attributes - Add ThemeScopeDetector client component for runtime module/route detection - Add site-resolver for multi-site domain detection - Add /api/themes/export endpoint (JSON, CSS, variables formats) - Add /api/themes/export/embed.js for external integration widget - Add server actions for full CRUD on scopes and theme values - Add admin nav link and EN/NL translations
This commit is contained in:
1 parent
b57697b2e0
commit
a98b194386
19 files changed
+1843
No files matched your search
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
export function ColorField({
|
||||
name,
|
||||
label,
|
||||
description,
|
||||
defaultValue,
|
||||
}: {
|
||||
name: string;
|
||||
label: string;
|
||||
description: string;
|
||||
defaultValue: string;
|
||||
backgroundColorKey?: string;
|
||||
}) {
|
||||
const [val, setVal] = useState(defaultValue);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg p-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<input
|
||||
type="color"
|
||||
name={name}
|
||||
value={val}
|
||||
onChange={(e) => setVal(e.target.value)}
|
||||
className="w-[42px] h-[36px] p-0 border-none bg-none cursor-pointer flex-shrink-0 rounded"
|
||||
/>
|
||||
<div className="flex flex-col min-w-0 flex-1">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<span className="text-sm font-semibold text-[var(--admin-text)] leading-tight">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-[var(--admin-text-muted)] leading-snug line-clamp-2">
|
||||
{description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { getThemeBuilderData } from "@/actions/theme-builder";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { ScopeForm } from "./scope-form";
|
||||
import { ScopeTree } from "./scope-tree";
|
||||
import { ThemeEditor } from "./theme-editor";
|
||||
|
||||
export const metadata = { title: "Theme Builder" };
|
||||
|
||||
export default async function ThemeBuilderPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<Record<string, string | undefined>>;
|
||||
}) {
|
||||
const ctx = await getAdminContext();
|
||||
if (!ctx || !canAccess(ctx.permissions, PERMS.SETTINGS_VIEW)) {
|
||||
redirect("/admin");
|
||||
}
|
||||
|
||||
const sp = await searchParams;
|
||||
const { scopes, tree, moduleIds } = await getThemeBuilderData();
|
||||
|
||||
const selectedId = sp.scope ? Number(sp.scope) : (scopes[0]?.id ?? null);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="admin-section-title mb-4">Theme Builder</h1>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-6">
|
||||
Create and manage scoped themes for different parts of your site. Themes
|
||||
inherit from parent scopes — only override what you need.
|
||||
</p>
|
||||
|
||||
{sp.created ? (
|
||||
<div className="mb-4 p-3 rounded-lg theme-status-success text-xs">
|
||||
Scope created.
|
||||
</div>
|
||||
) : null}
|
||||
{sp.updated ? (
|
||||
<div className="mb-4 p-3 rounded-lg theme-status-success text-xs">
|
||||
Scope updated.
|
||||
</div>
|
||||
) : null}
|
||||
{sp.deleted ? (
|
||||
<div className="mb-4 p-3 rounded-lg theme-status-success text-xs">
|
||||
Scope deleted.
|
||||
</div>
|
||||
) : null}
|
||||
{sp.saved ? (
|
||||
<div className="mb-4 p-3 rounded-lg theme-status-success text-xs">
|
||||
Theme values saved.
|
||||
</div>
|
||||
) : null}
|
||||
{sp.duplicated ? (
|
||||
<div className="mb-4 p-3 rounded-lg theme-status-success text-xs">
|
||||
Scope duplicated.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||
<div className="lg:col-span-3">
|
||||
<ScopeTree tree={tree} selectedId={selectedId} />
|
||||
<div className="mt-4">
|
||||
<ScopeForm moduleIds={moduleIds} scopeCount={scopes.length} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-9">
|
||||
{selectedId ? (
|
||||
<ThemeEditor scopeId={selectedId} scopes={scopes} />
|
||||
) : (
|
||||
<div className="admin-card p-8 text-center text-[var(--admin-text-muted)]">
|
||||
Select or create a scope to start editing.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user