import { prisma } from "@/lib/prisma"; import { siteSettings } from "@/lib/services/site-settings"; import { PRESETS, THEME_COLOR_KEYS, type ThemeColorKey, } from "@/lib/theme-presets"; export interface CustomTheme { id: string; name: string; createdAt: number; settings: Record; } const STORE_KEY = "custom_themes"; const EXTRA_KEYS = [ "admin_canvas", "admin_surface", "admin_text", "admin_text_muted", "admin_border", "admin_sidebar_bg", "border_radius", "font_family", "size_heading_h1", "size_heading_h2", "size_heading_h3", "custom_css", "theme_preset", ]; const ALL_KEYS: string[] = [ ...THEME_COLOR_KEYS.flatMap((k) => [k, `${k}_dark`]), ...EXTRA_KEYS, ]; function fallbackFor(key: string): string { const preset = PRESETS["Atom (golden)"]; if (key.endsWith("_dark")) { const base = key.slice(0, -"_dark".length) as ThemeColorKey; return preset.dark[base] ?? ""; // eslint-disable-line security/detect-object-injection -- key derived from internal THEME_COLOR_KEYS } if (key in preset.light) { return preset.light[key as ThemeColorKey] ?? ""; } const extras: Record = { admin_canvas: "#0f172a", admin_surface: "#1e293b", admin_text: "#f1f5f9", admin_text_muted: "#94a3b8", admin_border: "#334155", admin_sidebar_bg: "#0f172a", border_radius: "12", font_family: "nunito", size_heading_h1: "30", size_heading_h2: "22", size_heading_h3: "18", custom_css: "", theme_preset: "Atom (golden)", }; return extras[key] ?? ""; // eslint-disable-line security/detect-object-injection -- key derived from internal THEME_COLOR_KEYS } export async function listCustomThemes(): Promise { const raw = await siteSettings.get(STORE_KEY, "[]"); try { const parsed = JSON.parse(raw ?? "[]"); if (!Array.isArray(parsed)) return []; return parsed as CustomTheme[]; } catch { return []; } } export async function snapshotCurrentTheme(): Promise> { const entries = await Promise.all( ALL_KEYS.map( async (key) => [key, (await siteSettings.get(key, null)) ?? fallbackFor(key)] as const, ), ); return Object.fromEntries(entries); } async function persist(themes: CustomTheme[]): Promise { const value = JSON.stringify(themes); await prisma.websiteSetting.upsert({ where: { key: STORE_KEY }, create: { key: STORE_KEY, value, comment: "Theme (housekeeping)" }, update: { value }, }); siteSettings.reload(); } export async function upsertCustomTheme( name: string, settings: Record, id?: string, ): Promise { const themes = await listCustomThemes(); const trimmed = name.trim() || "Untitled theme"; if (id) { const idx = themes.findIndex((t) => t.id === id); if (idx >= 0) { themes[idx] = { ...themes[idx], name: trimmed, settings }; // eslint-disable-line security/detect-object-injection -- idx from findIndex over internal array await persist(themes); return themes[idx]; // eslint-disable-line security/detect-object-injection -- idx from findIndex over internal array } } const theme: CustomTheme = { id: globalThis.crypto.randomUUID(), name: trimmed, createdAt: Date.now(), settings, }; themes.push(theme); await persist(themes); return theme; } export async function getCustomTheme(id: string): Promise { const themes = await listCustomThemes(); return themes.find((t) => t.id === id) ?? null; } export async function deleteCustomThemeStore(id: string): Promise { const themes = await listCustomThemes(); await persist(themes.filter((t) => t.id !== id)); }