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 562815b..fae4d94 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?.officeName ?? "Ç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..cea7c44 100644
--- a/src/components/theme-customizer/index.tsx
+++ b/src/components/theme-customizer/index.tsx
@@ -6,78 +6,102 @@ 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 { ThemeTab } from './theme-tab'
import { LayoutTab } from './layout-tab'
import { ImportModal } from './import-modal'
+import { saveThemePrefsAction } from '@/lib/appwrite/theme-prefs-actions'
import { cn } from '@/lib/utils'
import type { ImportedTheme } from '@/types/theme-customizer'
+import type { ThemePrefs } from '@/lib/appwrite/theme-prefs-actions'
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 { config: sidebarConfig, updateConfig: updateSidebarConfig } = useSidebarConfig()
+ const { theme } = useTheme()
+ const { config: sidebarConfig } = 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) => {
setImportedTheme(themeData)
- // Clear other selections to indicate custom import is active
setSelectedTheme("")
setSelectedTweakcnTheme("")
-
- // Apply the imported theme
applyImportedTheme(themeData, isDarkMode)
+ // Imported themes have no stable ID to save
}
- const handleImportClick = () => {
- setImportModalOpen(true)
- }
-
- // Re-apply themes when theme mode changes
+ // Re-apply when dark/light toggles
React.useEffect(() => {
if (importedTheme) {
applyImportedTheme(importedTheme, isDarkMode)
} else if (selectedTheme) {
applyTheme(selectedTheme, isDarkMode)
} else if (selectedTweakcnTheme) {
- const selectedPreset = tweakcnThemes.find(t => t.value === selectedTweakcnTheme)?.preset
- if (selectedPreset) {
- applyTweakcnTheme(selectedPreset, isDarkMode)
- }
+ const preset = tweakcnThemes.find(t => t.value === selectedTweakcnTheme)?.preset
+ if (preset) applyTweakcnTheme(preset, isDarkMode)
}
}, [isDarkMode, importedTheme, selectedTheme, selectedTweakcnTheme, applyImportedTheme, applyTheme, applyTweakcnTheme])
+ // Wrappers that also persist to Appwrite
+ const handleSetSelectedTheme = (value: string) => {
+ setSelectedTheme(value)
+ setSelectedTweakcnTheme("")
+ void saveThemePrefsAction({ colorTheme: value, tweakcnTheme: "" })
+ }
+
+ const handleSetSelectedTweakcnTheme = (value: string) => {
+ setSelectedTweakcnTheme(value)
+ setSelectedTheme("")
+ void saveThemePrefsAction({ tweakcnTheme: value, colorTheme: "" })
+ }
+
+ const handleSetSelectedRadius = (value: string) => {
+ setSelectedRadius(value)
+ void saveThemePrefsAction({ radius: value })
+ }
+
return (
<>
@@ -85,10 +109,7 @@ export function ThemeCustomizer({ open, onOpenChange }: ThemeCustomizerProps) {
side={sidebarConfig.side === "left" ? "right" : "left"}
className="w-[400px] p-0 gap-0 pointer-events-auto [&>button]:hidden overflow-hidden flex flex-col"
onInteractOutside={(e) => {
- // Prevent the sheet from closing when dialog is open
- if (importModalOpen) {
- e.preventDefault()
- }
+ if (importModalOpen) e.preventDefault()
}}
>
@@ -140,13 +161,13 @@ export function ThemeCustomizer({ open, onOpenChange }: ThemeCustomizerProps) {
setImportModalOpen(true)}
/>
@@ -167,7 +188,6 @@ export function ThemeCustomizer({ open, onOpenChange }: ThemeCustomizerProps) {
)
}
-// Floating trigger button - positioned dynamically based on sidebar side
export function ThemeCustomizerTrigger({ onClick }: { onClick: () => void }) {
const { config: sidebarConfig } = useSidebarConfig()
diff --git a/src/components/theme-customizer/prefs-initializer.tsx b/src/components/theme-customizer/prefs-initializer.tsx
new file mode 100644
index 0000000..234c789
--- /dev/null
+++ b/src/components/theme-customizer/prefs-initializer.tsx
@@ -0,0 +1,56 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+import { useTheme } from "@/hooks/use-theme";
+import { useSidebarConfig } from "@/contexts/sidebar-context";
+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;
+
+ // Dark/light mode
+ if (prefs.theme) setTheme(prefs.theme);
+
+ // Determine isDark for CSS variable application
+ const isDark =
+ prefs.theme === "dark" ||
+ (prefs.theme !== "light" &&
+ typeof window !== "undefined" &&
+ window.matchMedia("(prefers-color-scheme: dark)").matches);
+
+ // Radius
+ if (prefs.radius) applyRadius(prefs.radius);
+
+ // Color theme (shadcn presets)
+ if (prefs.colorTheme) {
+ applyTheme(prefs.colorTheme, isDark);
+ }
+
+ // Tweakcn theme
+ if (prefs.tweakcnTheme) {
+ const preset = tweakcnThemes.find((t) => t.value === prefs.tweakcnTheme)?.preset;
+ if (preset) applyTweakcnTheme(preset, isDark);
+ }
+
+ // Sidebar config
+ 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
+ }
+}