mirror of
https://git.coastit.co.za/caitlin/CRM_ENVR.git
synced 2026-07-10 11:15:43 +02:00
npm
This commit is contained in:
@@ -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
@@ -1,6 +1,7 @@
|
||||
import type { Metadata, Viewport } from "next"
|
||||
import { Inter } from "next/font/google"
|
||||
import { ThemeProvider } from "@/providers/theme-provider"
|
||||
import { WebsiteThemeProvider } from "@/providers/website-theme-provider"
|
||||
import { Toaster } from "@/components/ui/sonner"
|
||||
import "./globals.css"
|
||||
|
||||
@@ -31,8 +32,10 @@ export default function RootLayout({
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`${inter.variable} min-h-screen antialiased`}>
|
||||
<ThemeProvider attribute="class" defaultTheme="dark" disableTransitionOnChange>
|
||||
{children}
|
||||
<Toaster />
|
||||
<WebsiteThemeProvider>
|
||||
{children}
|
||||
<Toaster />
|
||||
</WebsiteThemeProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -6,7 +6,8 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
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"
|
||||
|
||||
@@ -44,8 +45,17 @@ function applyColorTheme(theme: string) {
|
||||
localStorage.setItem(COLOR_THEME_KEY, theme)
|
||||
}
|
||||
|
||||
const websiteThemes = [
|
||||
{
|
||||
id: "spidey",
|
||||
name: "Spidey",
|
||||
description: "Current website theme",
|
||||
},
|
||||
]
|
||||
|
||||
export function ThemeSettings() {
|
||||
const { theme, setTheme } = useTheme()
|
||||
const { websiteTheme, setWebsiteTheme } = useWebsiteTheme()
|
||||
const [colorTheme, setColorTheme] = useState("default")
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
@@ -119,6 +129,71 @@ export function ThemeSettings() {
|
||||
</RadioGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Website Themes</CardTitle>
|
||||
<CardDescription>
|
||||
Select your website'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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user