diff --git a/src/app/(dashboard)/dashboard-shell.tsx b/src/app/(dashboard)/dashboard-shell.tsx index c81e77e..b873a27 100644 --- a/src/app/(dashboard)/dashboard-shell.tsx +++ b/src/app/(dashboard)/dashboard-shell.tsx @@ -7,7 +7,9 @@ import { SiteHeader } from "@/components/site-header"; import { SiteFooter } from "@/components/site-footer"; import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar"; import { ThemeCustomizer, ThemeCustomizerTrigger } from "@/components/theme-customizer"; +import { PrefsInitializer } from "@/components/theme-customizer/prefs-initializer"; import { useSidebarConfig } from "@/hooks/use-sidebar-config"; +import type { ThemePrefs } from "@/lib/appwrite/theme-prefs-actions"; export type ShellUser = { id: string; @@ -25,10 +27,12 @@ export function DashboardShell({ user, company, children, + initialPrefs, }: { user: ShellUser; company: ShellCompany; children: React.ReactNode; + initialPrefs: ThemePrefs; }) { const [themeCustomizerOpen, setThemeCustomizerOpen] = React.useState(false); const { config } = useSidebarConfig(); @@ -44,6 +48,8 @@ export function DashboardShell({ } className={config.collapsible === "none" ? "sidebar-none-mode" : ""} > + + {config.side === "left" ? ( <> ); diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index 572292e..4f2ff30 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -2,6 +2,8 @@ import { redirect } from "next/navigation"; import { getActiveContext } from "@/lib/appwrite/active-context"; import { getLogoUrl } from "@/lib/appwrite/storage"; +import { createSessionClient } from "@/lib/appwrite/server"; +import type { ThemePrefs } from "@/lib/appwrite/theme-prefs-actions"; import { DashboardShell } from "./dashboard-shell"; export default async function DashboardLayout({ @@ -12,6 +14,14 @@ export default async function DashboardLayout({ const ctx = await getActiveContext(); if (!ctx) redirect("/onboarding"); + let themePrefs: ThemePrefs = {}; + try { + const { account } = await createSessionClient(); + themePrefs = await account.getPrefs(); + } catch { + // use defaults if prefs unavailable + } + const company = { id: ctx.tenantId, name: ctx.settings?.companyName ?? "Çalışma alanı", @@ -24,7 +34,7 @@ export default async function DashboardLayout({ }; return ( - + {children} ); diff --git a/src/components/theme-customizer/index.tsx b/src/components/theme-customizer/index.tsx index e0a2aaa..eb14914 100644 --- a/src/components/theme-customizer/index.tsx +++ b/src/components/theme-customizer/index.tsx @@ -6,8 +6,11 @@ import { Button } from '@/components/ui/button' import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { useThemeManager } from '@/hooks/use-theme-manager' +import { useTheme } from '@/hooks/use-theme' import { useSidebarConfig } from '@/contexts/sidebar-context' import { tweakcnThemes } from '@/config/theme-data' +import { saveThemePrefsAction } from '@/lib/appwrite/theme-prefs-actions' +import type { ThemePrefs } from '@/lib/appwrite/theme-prefs-actions' import { ThemeTab } from './theme-tab' import { LayoutTab } from './layout-tab' import { ImportModal } from './import-modal' @@ -17,37 +20,49 @@ import type { ImportedTheme } from '@/types/theme-customizer' interface ThemeCustomizerProps { open: boolean onOpenChange: (open: boolean) => void + initialPrefs: ThemePrefs } -export function ThemeCustomizer({ open, onOpenChange }: ThemeCustomizerProps) { +export function ThemeCustomizer({ open, onOpenChange, initialPrefs }: ThemeCustomizerProps) { const { applyImportedTheme, isDarkMode, resetTheme, applyRadius, setBrandColorsValues, applyTheme, applyTweakcnTheme } = useThemeManager() + const { theme } = useTheme() const { config: sidebarConfig, updateConfig: updateSidebarConfig } = useSidebarConfig() const [activeTab, setActiveTab] = React.useState("theme") - const [selectedTheme, setSelectedTheme] = React.useState("default") - const [selectedTweakcnTheme, setSelectedTweakcnTheme] = React.useState("") - const [selectedRadius, setSelectedRadius] = React.useState("0.5rem") + const [selectedTheme, setSelectedTheme] = React.useState(initialPrefs.colorTheme ?? "default") + const [selectedTweakcnTheme, setSelectedTweakcnTheme] = React.useState(initialPrefs.tweakcnTheme ?? "") + const [selectedRadius, setSelectedRadius] = React.useState(initialPrefs.radius ?? "0.5rem") const [importModalOpen, setImportModalOpen] = React.useState(false) const [importedTheme, setImportedTheme] = React.useState(null) - const handleReset = () => { - // Complete reset to application defaults + // Save dark/light mode to Appwrite when it changes (skip first mount) + const themeMountRef = React.useRef(false) + React.useEffect(() => { + if (!themeMountRef.current) { themeMountRef.current = true; return } + void saveThemePrefsAction({ theme }) + }, [theme]) - // 1. Reset all state variables to initial values + // Save sidebar config to Appwrite when it changes (skip first mount) + const sidebarMountRef = React.useRef(false) + React.useEffect(() => { + if (!sidebarMountRef.current) { sidebarMountRef.current = true; return } + void saveThemePrefsAction({ + sidebarVariant: sidebarConfig.variant, + sidebarCollapsible: sidebarConfig.collapsible, + sidebarSide: sidebarConfig.side, + }) + }, [sidebarConfig.variant, sidebarConfig.collapsible, sidebarConfig.side]) + + const handleReset = () => { setSelectedTheme("default") setSelectedTweakcnTheme("") setSelectedRadius("0.5rem") - setImportedTheme(null) // Clear imported theme - setBrandColorsValues({}) // Clear brand colors state - - // 2. Completely remove all custom CSS variables + setImportedTheme(null) + setBrandColorsValues({}) resetTheme() - - // 3. Reset the radius to default applyRadius("0.5rem") - - // 4. Reset sidebar to defaults updateSidebarConfig({ variant: "inset", collapsible: "offcanvas", side: "left" }) + void saveThemePrefsAction({ colorTheme: "default", tweakcnTheme: "", radius: "0.5rem" }) } const handleImport = (themeData: ImportedTheme) => { @@ -140,11 +155,22 @@ export function ThemeCustomizer({ open, onOpenChange }: ThemeCustomizerProps) { { + setSelectedTheme(value) + setSelectedTweakcnTheme("") + void saveThemePrefsAction({ colorTheme: value, tweakcnTheme: "" }) + }} selectedTweakcnTheme={selectedTweakcnTheme} - setSelectedTweakcnTheme={setSelectedTweakcnTheme} + setSelectedTweakcnTheme={(value) => { + setSelectedTweakcnTheme(value) + setSelectedTheme("") + void saveThemePrefsAction({ tweakcnTheme: value, colorTheme: "" }) + }} selectedRadius={selectedRadius} - setSelectedRadius={setSelectedRadius} + setSelectedRadius={(value) => { + setSelectedRadius(value) + void saveThemePrefsAction({ radius: value }) + }} setImportedTheme={setImportedTheme} onImportClick={handleImportClick} /> diff --git a/src/components/theme-customizer/prefs-initializer.tsx b/src/components/theme-customizer/prefs-initializer.tsx new file mode 100644 index 0000000..93d2902 --- /dev/null +++ b/src/components/theme-customizer/prefs-initializer.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useEffect, useRef } from "react"; + +import { useSidebarConfig } from "@/contexts/sidebar-context"; +import { useTheme } from "@/hooks/use-theme"; +import { useThemeManager } from "@/hooks/use-theme-manager"; +import { tweakcnThemes } from "@/config/theme-data"; +import type { ThemePrefs } from "@/lib/appwrite/theme-prefs-actions"; + +export function PrefsInitializer({ prefs }: { prefs: ThemePrefs }) { + const { setTheme } = useTheme(); + const { updateConfig } = useSidebarConfig(); + const { applyTheme, applyTweakcnTheme, applyRadius } = useThemeManager(); + const applied = useRef(false); + + useEffect(() => { + if (applied.current) return; + applied.current = true; + + if (prefs.theme) setTheme(prefs.theme); + + const isDark = + prefs.theme === "dark" || + (prefs.theme !== "light" && + typeof window !== "undefined" && + window.matchMedia("(prefers-color-scheme: dark)").matches); + + if (prefs.radius) applyRadius(prefs.radius); + + if (prefs.colorTheme) { + applyTheme(prefs.colorTheme, isDark); + } + + if (prefs.tweakcnTheme) { + const preset = tweakcnThemes.find((t) => t.value === prefs.tweakcnTheme)?.preset; + if (preset) applyTweakcnTheme(preset, isDark); + } + + if (prefs.sidebarVariant || prefs.sidebarCollapsible || prefs.sidebarSide) { + updateConfig({ + ...(prefs.sidebarVariant && { variant: prefs.sidebarVariant }), + ...(prefs.sidebarCollapsible && { collapsible: prefs.sidebarCollapsible }), + ...(prefs.sidebarSide && { side: prefs.sidebarSide }), + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return null; +} diff --git a/src/lib/appwrite/theme-prefs-actions.ts b/src/lib/appwrite/theme-prefs-actions.ts new file mode 100644 index 0000000..c5d20d0 --- /dev/null +++ b/src/lib/appwrite/theme-prefs-actions.ts @@ -0,0 +1,23 @@ +"use server"; + +import { createSessionClient } from "@/lib/appwrite/server"; + +export interface ThemePrefs { + theme?: "dark" | "light" | "system"; + colorTheme?: string; + tweakcnTheme?: string; + radius?: string; + sidebarVariant?: "sidebar" | "floating" | "inset"; + sidebarCollapsible?: "offcanvas" | "icon" | "none"; + sidebarSide?: "left" | "right"; +} + +export async function saveThemePrefsAction(update: Partial): Promise { + try { + const { account } = await createSessionClient(); + const existing = await account.getPrefs>(); + await account.updatePrefs({ ...existing, ...update }); + } catch { + // best-effort — UI still works without persistence + } +}