This commit is contained in:
2026-06-26 11:21:10 +02:00
parent 9bbaf70145
commit 343f814569
4 changed files with 214 additions and 3 deletions
@@ -0,0 +1,41 @@
import { NextRequest, NextResponse } from "next/server"
import { getSessionUser } from "@/lib/auth"
import { query } from "@/lib/db"
export async function GET() {
try {
const user = await getSessionUser()
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
const result = await query(
`SELECT preferences->>'website_theme' AS website_theme FROM users WHERE id = $1`,
[user.id],
)
const websiteTheme = result.rows[0]?.website_theme || "spidey"
return NextResponse.json({ websiteTheme })
} catch (error) {
console.error("Website theme GET error:", error)
return NextResponse.json({ error: "Failed to load website theme" }, { status: 500 })
}
}
export async function PUT(request: NextRequest) {
try {
const user = await getSessionUser()
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
const body = await request.json()
const theme = body.websiteTheme || "spidey"
await query(
`UPDATE users SET preferences = preferences || $2::jsonb WHERE id = $1`,
[user.id, JSON.stringify({ website_theme: theme })],
)
return NextResponse.json({ success: true, websiteTheme: theme })
} catch (error) {
console.error("Website theme PUT error:", error)
return NextResponse.json({ error: "Failed to save website theme" }, { status: 500 })
}
}
+5 -2
View File
@@ -1,6 +1,7 @@
import type { Metadata, Viewport } from "next" import type { Metadata, Viewport } from "next"
import { Inter } from "next/font/google" import { Inter } from "next/font/google"
import { ThemeProvider } from "@/providers/theme-provider" import { ThemeProvider } from "@/providers/theme-provider"
import { WebsiteThemeProvider } from "@/providers/website-theme-provider"
import { Toaster } from "@/components/ui/sonner" import { Toaster } from "@/components/ui/sonner"
import "./globals.css" import "./globals.css"
@@ -31,8 +32,10 @@ export default function RootLayout({
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body className={`${inter.variable} min-h-screen antialiased`}> <body className={`${inter.variable} min-h-screen antialiased`}>
<ThemeProvider attribute="class" defaultTheme="dark" disableTransitionOnChange> <ThemeProvider attribute="class" defaultTheme="dark" disableTransitionOnChange>
{children} <WebsiteThemeProvider>
<Toaster /> {children}
<Toaster />
</WebsiteThemeProvider>
</ThemeProvider> </ThemeProvider>
</body> </body>
</html> </html>
+76 -1
View File
@@ -6,7 +6,8 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Sun, Moon, Monitor } from "lucide-react" import { useWebsiteTheme } from "@/providers/website-theme-provider"
import { Sun, Moon, Monitor, Check } from "lucide-react"
const COLOR_THEME_KEY = "crm-color-theme" const COLOR_THEME_KEY = "crm-color-theme"
@@ -44,8 +45,17 @@ function applyColorTheme(theme: string) {
localStorage.setItem(COLOR_THEME_KEY, theme) localStorage.setItem(COLOR_THEME_KEY, theme)
} }
const websiteThemes = [
{
id: "spidey",
name: "Spidey",
description: "Current website theme",
},
]
export function ThemeSettings() { export function ThemeSettings() {
const { theme, setTheme } = useTheme() const { theme, setTheme } = useTheme()
const { websiteTheme, setWebsiteTheme } = useWebsiteTheme()
const [colorTheme, setColorTheme] = useState("default") const [colorTheme, setColorTheme] = useState("default")
const [mounted, setMounted] = useState(false) const [mounted, setMounted] = useState(false)
@@ -119,6 +129,71 @@ export function ThemeSettings() {
</RadioGroup> </RadioGroup>
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardHeader>
<CardTitle>Website Themes</CardTitle>
<CardDescription>
Select your website&apos;s visual theme.
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
{websiteThemes.map((wt) => {
const isActive = websiteTheme === wt.id
return (
<button
key={wt.id}
type="button"
onClick={() => setWebsiteTheme(wt.id)}
className={cn(
"group relative flex flex-col items-start gap-3 rounded-xl border-2 p-4 text-left transition-all",
"hover:bg-accent cursor-pointer",
isActive
? "border-primary bg-primary/5 shadow-sm"
: "border-border hover:border-muted-foreground/30"
)}
>
{isActive && (
<span className="absolute right-2 top-2 flex h-5 w-5 items-center justify-center rounded-full bg-primary text-[10px] text-primary-foreground">
<Check className="h-3 w-3" />
</span>
)}
<div className="flex h-20 w-full items-center justify-center overflow-hidden rounded-lg border border-border bg-background">
<div className="flex h-full w-full">
<div className="flex w-1/3 flex-col gap-0.5 bg-[#0a0a0f] p-1.5">
<div className="h-1 w-full rounded-sm bg-[#1a1a24]" />
<div className="h-1 w-2/3 rounded-sm bg-[#1a1a24]" />
<div className="mt-auto h-1.5 w-full rounded-sm bg-[#1a1a24]" />
</div>
<div className="flex flex-1 flex-col">
<div className="flex h-5 items-center gap-1 bg-[#CC0000] px-1.5">
<div className="h-1.5 w-1.5 rounded-full bg-white/30" />
<div className="h-1.5 w-1.5 rounded-full bg-white/30" />
<div className="h-1.5 w-1.5 rounded-full bg-white/30" />
</div>
<div className="flex flex-1 items-center justify-center bg-[#141414]">
<div className="h-2 w-2 rounded-full bg-[#CC0000]/40" />
</div>
</div>
</div>
</div>
<div className="flex w-full items-center justify-between gap-2">
<span className="text-sm font-medium">{wt.name}</span>
{isActive && (
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-semibold text-primary">
Current
</span>
)}
</div>
</button>
)
})}
</div>
</CardContent>
</Card>
</div> </div>
) )
} }
+92
View File
@@ -0,0 +1,92 @@
"use client"
import { createContext, useContext, useState, useEffect, ReactNode, useCallback } from "react"
const WEBSITE_THEME_KEY = "crm-website-theme"
const themeClasses: Record<string, string> = {
spidey: "",
}
interface WebsiteThemeContextValue {
websiteTheme: string
setWebsiteTheme: (theme: string) => Promise<void>
loading: boolean
}
const WebsiteThemeContext = createContext<WebsiteThemeContextValue | null>(null)
function applyWebsiteTheme(theme: string) {
const prefix = "theme-"
const root = document.documentElement
const classes = root.className.split(" ").filter((c) => !c.startsWith(prefix))
const cls = themeClasses[theme]
if (cls) {
classes.push(cls)
}
root.className = classes.join(" ").trim()
}
function getStoredTheme(): string | null {
if (typeof window === "undefined") return null
return localStorage.getItem(WEBSITE_THEME_KEY)
}
function storeTheme(theme: string) {
if (typeof window === "undefined") return
localStorage.setItem(WEBSITE_THEME_KEY, theme)
}
export function WebsiteThemeProvider({ children }: { children: ReactNode }) {
const [websiteTheme, setWebsiteThemeState] = useState<string>("spidey")
const [loading, setLoading] = useState(true)
useEffect(() => {
const stored = getStoredTheme()
if (stored) {
setWebsiteThemeState(stored)
applyWebsiteTheme(stored)
setLoading(false)
return
}
fetch("/api/settings/website-theme")
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
const theme = data?.websiteTheme || "spidey"
setWebsiteThemeState(theme)
storeTheme(theme)
applyWebsiteTheme(theme)
})
.catch(() => {
applyWebsiteTheme("spidey")
})
.finally(() => setLoading(false))
}, [])
const setWebsiteTheme = useCallback(async (theme: string) => {
setWebsiteThemeState(theme)
storeTheme(theme)
applyWebsiteTheme(theme)
try {
await fetch("/api/settings/website-theme", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ websiteTheme: theme }),
})
} catch {
console.warn("Failed to persist website theme")
}
}, [])
return (
<WebsiteThemeContext.Provider value={{ websiteTheme, setWebsiteTheme, loading }}>
{children}
</WebsiteThemeContext.Provider>
)
}
export function useWebsiteTheme() {
const ctx = useContext(WebsiteThemeContext)
if (!ctx) throw new Error("useWebsiteTheme must be used within WebsiteThemeProvider")
return ctx
}