Add named custom theme presets (save/load/rename/delete) in admin theme editor
Local Build and Deploy / deploy (push) Successful in 58s
Local Build and Deploy / deploy (push) Successful in 58s
This commit is contained in:
1 parent
01b70c2369
commit
e6d7f2280b
4 files changed
+321
-2
No files matched your search
@@ -1,8 +1,10 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { applyPreset, saveTheme } from "@/actions/admin-theme";
|
||||
import { applyCustomTheme, applyPreset, deleteCustomTheme, saveCustomTheme, saveTheme } from "@/actions/admin-theme";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
import { FONTS, PRESETS } from "@/lib/theme-presets";
|
||||
import { listCustomThemes } from "@/lib/theme-custom-store";
|
||||
import { ColorField } from "./color-field";
|
||||
import { RenameTheme } from "@/components/admin/theme/rename-theme";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const metadata = { title: "Theme" };
|
||||
@@ -106,7 +108,7 @@ function colorFields(
|
||||
export default async function AdminTheme({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ saved?: string; preset?: string }>;
|
||||
searchParams: Promise<{ saved?: string; preset?: string; savedTheme?: string; theme?: string; renamed?: string; deletedTheme?: string }>;
|
||||
}) {
|
||||
const t = await getTranslations("pages.admin.theme");
|
||||
|
||||
@@ -134,6 +136,7 @@ export default async function AdminTheme({
|
||||
),
|
||||
),
|
||||
) as Record<string, string>;
|
||||
const customThemes = await listCustomThemes();
|
||||
const radius = (await siteSettings.get("border_radius", "12")) ?? "12";
|
||||
const fontKey = (await siteSettings.get("font_family", "nunito")) ?? "nunito";
|
||||
const headings = Object.fromEntries(
|
||||
@@ -163,6 +166,20 @@ export default async function AdminTheme({
|
||||
{t("presetApplied", { preset: sp.preset })}
|
||||
</div>
|
||||
) : null}
|
||||
{sp.savedTheme ? (
|
||||
<div className="mb-4 p-3 rounded-lg theme-status-success text-xs">Theme saved as preset.</div>
|
||||
) : null}
|
||||
{sp.theme ? (
|
||||
<div className="mb-4 p-3 rounded-lg theme-status-success text-xs">
|
||||
Applied theme “{sp.theme}”.
|
||||
</div>
|
||||
) : null}
|
||||
{sp.renamed ? (
|
||||
<div className="mb-4 p-3 rounded-lg theme-status-success text-xs">Theme renamed.</div>
|
||||
) : null}
|
||||
{sp.deletedTheme ? (
|
||||
<div className="mb-4 p-3 rounded-lg theme-status-success text-xs">Theme deleted.</div>
|
||||
) : null}
|
||||
|
||||
<section className="mt-6">
|
||||
<h2 className="admin-section-title">{t("presetsTitle")}</h2>
|
||||
@@ -193,6 +210,70 @@ export default async function AdminTheme({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-6">
|
||||
<h2 className="admin-section-title">Your saved themes</h2>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
Save the current configuration as a named preset, then load it anytime. Perfect for keeping multiple brand
|
||||
themes ready to switch.
|
||||
</p>
|
||||
<form action={saveCustomTheme} className="flex flex-wrap items-center gap-2 mb-4">
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Theme name (e.g. Summer Gold)"
|
||||
className="px-3 py-2 rounded-lg border bg-[var(--admin-canvas)] text-[var(--admin-text)] text-sm w-64"
|
||||
style={{ borderColor: "var(--admin-border)" }}
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Save current theme
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{customThemes.length === 0 ? (
|
||||
<p className="text-xs text-[var(--admin-text-muted)]">No saved themes yet — save one above.</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{customThemes.map((theme) => (
|
||||
<div key={theme.id} className="admin-card p-3 flex flex-col gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{["color_primary", "color_accent", "color_surface", "color_text"].map((k) => (
|
||||
<span
|
||||
key={k}
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 4,
|
||||
// eslint-disable-next-line security/detect-object-injection -- k from fixed theme color key list
|
||||
background: theme.settings[k] ?? "#64748b",
|
||||
boxShadow: "inset 0 0 0 1px rgba(0,0,0,0.2)",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="font-semibold text-[var(--admin-text)] truncate">{theme.name}</span>
|
||||
<RenameTheme id={theme.id} name={theme.name} />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<form action={applyCustomTheme}>
|
||||
<input type="hidden" name="id" value={theme.id} />
|
||||
<button type="submit" className="btn btn-sm btn-outline">
|
||||
Load
|
||||
</button>
|
||||
</form>
|
||||
<form action={deleteCustomTheme}>
|
||||
<input type="hidden" name="id" value={theme.id} />
|
||||
<button type="submit" className="btn btn-sm btn-danger">
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<form action={saveTheme}>
|
||||
<h2 className="mt-6 text-lg font-extrabold">☀ Light mode</h2>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3 mt-1">
|
||||
|
||||
Reference in new issue
Block a user