This commit is contained in:
Ace
2026-07-01 13:16:59 +02:00
5 changed files with 88 additions and 48 deletions
+1 -6
View File
@@ -12,7 +12,7 @@ import {
setSessionContext, setSessionContext,
SESSION_COOKIE, SESSION_COOKIE,
} from "@/lib/auth" } from "@/lib/auth"
import { query } from "@/lib/db"
function jsonResponse(data: unknown, status: number) { function jsonResponse(data: unknown, status: number) {
return new Response(JSON.stringify(data), { return new Response(JSON.stringify(data), {
@@ -115,11 +115,6 @@ export async function POST(request: NextRequest) {
true true
) )
await query(
`UPDATE users SET preferences = preferences || $2::jsonb WHERE id = $1`,
[dbUser.id, JSON.stringify({ website_theme: "default" })],
)
const token = await createSession(dbUser.id, dbUser.role_name) const token = await createSession(dbUser.id, dbUser.role_name)
await setSessionContext(dbUser.id, ipAddress) await setSessionContext(dbUser.id, ipAddress)
+25 -7
View File
@@ -8,12 +8,13 @@ export async function GET() {
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
const result = await query( const result = await query(
`SELECT preferences->>'website_theme' AS website_theme FROM users WHERE id = $1`, `SELECT preferences->>'website_theme' AS website_theme, preferences->>'color_theme' AS color_theme FROM users WHERE id = $1`,
[user.id], [user.id],
) )
const websiteTheme = result.rows[0]?.website_theme || "default" const websiteTheme = result.rows[0]?.website_theme || "default"
return NextResponse.json({ websiteTheme }) const colorTheme = result.rows[0]?.color_theme || null
return NextResponse.json({ websiteTheme, colorTheme })
} catch (error) { } catch (error) {
console.error("Website theme GET error:", error) console.error("Website theme GET error:", error)
return NextResponse.json({ error: "Failed to load website theme" }, { status: 500 }) return NextResponse.json({ error: "Failed to load website theme" }, { status: 500 })
@@ -26,14 +27,31 @@ export async function PUT(request: NextRequest) {
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
const body = await request.json() const body = await request.json()
const theme = body.websiteTheme || "default" const update: Record<string, string> = {}
if (body.websiteTheme !== undefined) {
update.website_theme = body.websiteTheme || "default"
}
if (body.colorTheme !== undefined) {
update.color_theme = body.colorTheme || "default"
}
await query( if (Object.keys(update).length > 0) {
`UPDATE users SET preferences = preferences || $2::jsonb WHERE id = $1`, await query(
[user.id, JSON.stringify({ website_theme: theme })], `UPDATE users SET preferences = preferences || $2::jsonb WHERE id = $1`,
[user.id, JSON.stringify(update)],
)
}
const result = await query(
`SELECT preferences->>'website_theme' AS website_theme, preferences->>'color_theme' AS color_theme FROM users WHERE id = $1`,
[user.id],
) )
return NextResponse.json({ success: true, websiteTheme: theme }) return NextResponse.json({
success: true,
websiteTheme: result.rows[0]?.website_theme || "default",
colorTheme: result.rows[0]?.color_theme || null,
})
} catch (error) { } catch (error) {
console.error("Website theme PUT error:", error) console.error("Website theme PUT error:", error)
return NextResponse.json({ error: "Failed to save website theme" }, { status: 500 }) return NextResponse.json({ error: "Failed to save website theme" }, { status: 500 })
+5
View File
@@ -33,6 +33,11 @@ export default function RootLayout({
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body className={`${inter.variable} min-h-screen antialiased`}> <body className={`${inter.variable} min-h-screen antialiased`}>
<script
dangerouslySetInnerHTML={{
__html: `(function(){try{var t=localStorage.getItem("crm-color-theme");if(t&&t!=="default")document.documentElement.classList.add(t);var w=localStorage.getItem("crm-website-theme");if(w==="spidey")document.documentElement.classList.add("theme-spidey")}catch(e){}})()`,
}}
/>
<ThemeProvider attribute="class" defaultTheme="light" disableTransitionOnChange> <ThemeProvider attribute="class" defaultTheme="light" disableTransitionOnChange>
<WebsiteThemeProvider> <WebsiteThemeProvider>
{children} {children}
+2 -27
View File
@@ -9,8 +9,6 @@ import { cn } from "@/lib/utils"
import { Sun, Moon, Monitor, Palette, Shield } from "lucide-react" import { Sun, Moon, Monitor, Palette, Shield } from "lucide-react"
import { useWebsiteTheme } from "@/providers/website-theme-provider" import { useWebsiteTheme } from "@/providers/website-theme-provider"
const COLOR_THEME_KEY = "crm-color-theme"
const modeOptions = [ const modeOptions = [
{ value: "light", icon: Sun, label: "Light" }, { value: "light", icon: Sun, label: "Light" },
{ value: "dark", icon: Moon, label: "Dark" }, { value: "dark", icon: Moon, label: "Dark" },
@@ -35,38 +33,15 @@ const backgroundOptions = [
{ value: "spidey", label: "Spidey", icon: Shield, color: "bg-red-600", ring: "ring-red-600", desc: "Dark theme with red accents" }, { value: "spidey", label: "Spidey", icon: Shield, color: "bg-red-600", ring: "ring-red-600", desc: "Dark theme with red accents" },
] ]
function getStoredColorTheme(): string {
if (typeof window === "undefined") return "default"
return localStorage.getItem(COLOR_THEME_KEY) || "default"
}
function applyColorTheme(theme: string) {
const colorThemes = ["default","ocean","forest","sunset","midnight","rose","amber","violet","slate","ruby"]
const classes = document.documentElement.className.split(" ").filter(c => !colorThemes.includes(c))
if (theme !== "default") {
classes.push(theme)
}
document.documentElement.className = classes.join(" ").trim()
localStorage.setItem(COLOR_THEME_KEY, theme)
}
export function ThemeSettings() { export function ThemeSettings() {
const { theme, setTheme } = useTheme() const { theme, setTheme } = useTheme()
const { websiteTheme, setWebsiteTheme } = useWebsiteTheme() const { websiteTheme, setWebsiteTheme, colorTheme, setColorTheme } = useWebsiteTheme()
const [colorTheme, setColorTheme] = useState("default")
const [mounted, setMounted] = useState(false) const [mounted, setMounted] = useState(false)
useEffect(() => { useEffect(() => {
setMounted(true) setMounted(true)
setColorTheme(getStoredColorTheme())
applyColorTheme(getStoredColorTheme())
}, []) }, [])
function handleColorChange(value: string) {
setColorTheme(value)
applyColorTheme(value)
}
if (!mounted) return null if (!mounted) return null
return ( return (
@@ -107,7 +82,7 @@ export function ThemeSettings() {
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<RadioGroup value={colorTheme} onValueChange={handleColorChange} className="grid grid-cols-5 gap-4"> <RadioGroup value={colorTheme} onValueChange={setColorTheme} className="grid grid-cols-5 gap-4">
{themeOptions.map(({ value, label, color, ring }) => ( {themeOptions.map(({ value, label, color, ring }) => (
<div key={value}> <div key={value}>
<RadioGroupItem value={value} id={`color-${value}`} className="peer sr-only" /> <RadioGroupItem value={value} id={`color-${value}`} className="peer sr-only" />
+55 -8
View File
@@ -3,14 +3,19 @@
import { createContext, useContext, useState, useEffect, ReactNode, useCallback } from "react" import { createContext, useContext, useState, useEffect, ReactNode, useCallback } from "react"
const WEBSITE_THEME_KEY = "crm-website-theme" const WEBSITE_THEME_KEY = "crm-website-theme"
const COLOR_THEME_KEY = "crm-color-theme"
const themeClasses: Record<string, string> = { const themeClasses: Record<string, string> = {
spidey: "theme-spidey", spidey: "theme-spidey",
} }
const colorThemes = ["default","ocean","forest","sunset","midnight","rose","amber","violet","slate","ruby"]
interface WebsiteThemeContextValue { interface WebsiteThemeContextValue {
websiteTheme: string websiteTheme: string
setWebsiteTheme: (theme: string) => Promise<void> setWebsiteTheme: (theme: string) => Promise<void>
colorTheme: string
setColorTheme: (theme: string) => Promise<void>
loading: boolean loading: boolean
} }
@@ -27,9 +32,19 @@ function applyWebsiteTheme(theme: string) {
root.className = classes.join(" ").trim() root.className = classes.join(" ").trim()
} }
function getStoredTheme(): string | null { function applyColorTheme(theme: string) {
const root = document.documentElement
const classes = root.className.split(" ").filter((c) => !colorThemes.includes(c))
if (theme !== "default") {
classes.push(theme)
}
root.className = classes.join(" ").trim()
localStorage.setItem(COLOR_THEME_KEY, theme)
}
function getStored(key: string): string | null {
if (typeof window === "undefined") return null if (typeof window === "undefined") return null
return localStorage.getItem(WEBSITE_THEME_KEY) return localStorage.getItem(key)
} }
function storeTheme(theme: string) { function storeTheme(theme: string) {
@@ -39,22 +54,40 @@ function storeTheme(theme: string) {
export function WebsiteThemeProvider({ children }: { children: ReactNode }) { export function WebsiteThemeProvider({ children }: { children: ReactNode }) {
const [websiteTheme, setWebsiteThemeState] = useState<string>("default") const [websiteTheme, setWebsiteThemeState] = useState<string>("default")
const [colorTheme, setColorThemeState] = useState<string>("default")
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
useEffect(() => { useEffect(() => {
fetch("/api/settings/website-theme") fetch("/api/settings/website-theme")
.then((res) => (res.ok ? res.json() : null)) .then((res) => (res.ok ? res.json() : null))
.then((data) => { .then((data) => {
const theme = data?.websiteTheme || "default" const wt = data?.websiteTheme || "default"
setWebsiteThemeState(theme) setWebsiteThemeState(wt)
storeTheme(theme) storeTheme(wt)
applyWebsiteTheme(theme) applyWebsiteTheme(wt)
if (data?.colorTheme) {
setColorThemeState(data.colorTheme)
applyColorTheme(data.colorTheme)
} else {
const stored = getStored(COLOR_THEME_KEY)
if (stored) {
setColorThemeState(stored)
applyColorTheme(stored)
}
}
}) })
.catch(() => { .catch(() => {
const stored = getStoredTheme() const stored = getStored(WEBSITE_THEME_KEY)
const theme = stored || "default" const theme = stored || "default"
setWebsiteThemeState(theme) setWebsiteThemeState(theme)
applyWebsiteTheme(theme) applyWebsiteTheme(theme)
const storedColor = getStored(COLOR_THEME_KEY)
if (storedColor) {
setColorThemeState(storedColor)
applyColorTheme(storedColor)
}
}) })
.finally(() => setLoading(false)) .finally(() => setLoading(false))
}, []) }, [])
@@ -74,8 +107,22 @@ export function WebsiteThemeProvider({ children }: { children: ReactNode }) {
} }
}, []) }, [])
const setColorTheme = useCallback(async (theme: string) => {
setColorThemeState(theme)
applyColorTheme(theme)
try {
await fetch("/api/settings/website-theme", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ colorTheme: theme }),
})
} catch {
console.warn("Failed to persist color theme")
}
}, [])
return ( return (
<WebsiteThemeContext.Provider value={{ websiteTheme, setWebsiteTheme, loading }}> <WebsiteThemeContext.Provider value={{ websiteTheme, setWebsiteTheme, colorTheme, setColorTheme, loading }}>
{children} {children}
</WebsiteThemeContext.Provider> </WebsiteThemeContext.Provider>
) )