feat: tema ayarları Appwrite user prefs ile kalıcı hale getirildi

- saveThemePrefsAction: account.updatePrefs ile mevcut prefs merge edilir
- Dashboard layout'ta account.getPrefs ile prefs server-side yüklenir
- PrefsInitializer: mount'ta dark/light, renk teması, radius, sidebar config Appwrite'dan uygulanır
- ThemeCustomizer: renk/tweakcn/radius değişikliği anında Appwrite'a kaydedilir; dark/light toggle useEffect ile izlenir
- Sayfa yenileme ve farklı cihazda giriş sonrasında ayarlar korunur
This commit is contained in:
kovakmedya
2026-05-07 22:26:40 +03:00
parent 78f50755ed
commit 997cc393af
5 changed files with 136 additions and 19 deletions
+7
View File
@@ -7,7 +7,9 @@ import { SiteHeader } from "@/components/site-header";
import { SiteFooter } from "@/components/site-footer"; import { SiteFooter } from "@/components/site-footer";
import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar"; import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar";
import { ThemeCustomizer, ThemeCustomizerTrigger } from "@/components/theme-customizer"; import { ThemeCustomizer, ThemeCustomizerTrigger } from "@/components/theme-customizer";
import { PrefsInitializer } from "@/components/theme-customizer/prefs-initializer";
import { useSidebarConfig } from "@/hooks/use-sidebar-config"; import { useSidebarConfig } from "@/hooks/use-sidebar-config";
import type { ThemePrefs } from "@/lib/appwrite/theme-prefs-actions";
export type ShellUser = { export type ShellUser = {
id: string; id: string;
@@ -25,10 +27,12 @@ export function DashboardShell({
user, user,
company, company,
children, children,
initialPrefs,
}: { }: {
user: ShellUser; user: ShellUser;
company: ShellCompany; company: ShellCompany;
children: React.ReactNode; children: React.ReactNode;
initialPrefs: ThemePrefs;
}) { }) {
const [themeCustomizerOpen, setThemeCustomizerOpen] = React.useState(false); const [themeCustomizerOpen, setThemeCustomizerOpen] = React.useState(false);
const { config } = useSidebarConfig(); const { config } = useSidebarConfig();
@@ -44,6 +48,8 @@ export function DashboardShell({
} }
className={config.collapsible === "none" ? "sidebar-none-mode" : ""} className={config.collapsible === "none" ? "sidebar-none-mode" : ""}
> >
<PrefsInitializer prefs={initialPrefs} />
{config.side === "left" ? ( {config.side === "left" ? (
<> <>
<AppSidebar <AppSidebar
@@ -88,6 +94,7 @@ export function DashboardShell({
<ThemeCustomizer <ThemeCustomizer
open={themeCustomizerOpen} open={themeCustomizerOpen}
onOpenChange={setThemeCustomizerOpen} onOpenChange={setThemeCustomizerOpen}
initialPrefs={initialPrefs}
/> />
</SidebarProvider> </SidebarProvider>
); );
+11 -1
View File
@@ -2,6 +2,8 @@ import { redirect } from "next/navigation";
import { getActiveContext } from "@/lib/appwrite/active-context"; import { getActiveContext } from "@/lib/appwrite/active-context";
import { getLogoUrl } from "@/lib/appwrite/storage"; 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"; import { DashboardShell } from "./dashboard-shell";
export default async function DashboardLayout({ export default async function DashboardLayout({
@@ -12,6 +14,14 @@ export default async function DashboardLayout({
const ctx = await getActiveContext(); const ctx = await getActiveContext();
if (!ctx) redirect("/onboarding"); if (!ctx) redirect("/onboarding");
let themePrefs: ThemePrefs = {};
try {
const { account } = await createSessionClient();
themePrefs = await account.getPrefs<ThemePrefs>();
} catch {
// use defaults if prefs unavailable
}
const company = { const company = {
id: ctx.tenantId, id: ctx.tenantId,
name: ctx.settings?.companyName ?? "Çalışma alanı", name: ctx.settings?.companyName ?? "Çalışma alanı",
@@ -24,7 +34,7 @@ export default async function DashboardLayout({
}; };
return ( return (
<DashboardShell user={user} company={company}> <DashboardShell user={user} company={company} initialPrefs={themePrefs}>
{children} {children}
</DashboardShell> </DashboardShell>
); );
+44 -18
View File
@@ -6,8 +6,11 @@ import { Button } from '@/components/ui/button'
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet' import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { useThemeManager } from '@/hooks/use-theme-manager' import { useThemeManager } from '@/hooks/use-theme-manager'
import { useTheme } from '@/hooks/use-theme'
import { useSidebarConfig } from '@/contexts/sidebar-context' import { useSidebarConfig } from '@/contexts/sidebar-context'
import { tweakcnThemes } from '@/config/theme-data' 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 { ThemeTab } from './theme-tab'
import { LayoutTab } from './layout-tab' import { LayoutTab } from './layout-tab'
import { ImportModal } from './import-modal' import { ImportModal } from './import-modal'
@@ -17,37 +20,49 @@ import type { ImportedTheme } from '@/types/theme-customizer'
interface ThemeCustomizerProps { interface ThemeCustomizerProps {
open: boolean open: boolean
onOpenChange: (open: boolean) => void 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 { applyImportedTheme, isDarkMode, resetTheme, applyRadius, setBrandColorsValues, applyTheme, applyTweakcnTheme } = useThemeManager()
const { theme } = useTheme()
const { config: sidebarConfig, updateConfig: updateSidebarConfig } = useSidebarConfig() const { config: sidebarConfig, updateConfig: updateSidebarConfig } = useSidebarConfig()
const [activeTab, setActiveTab] = React.useState("theme") const [activeTab, setActiveTab] = React.useState("theme")
const [selectedTheme, setSelectedTheme] = React.useState("default") const [selectedTheme, setSelectedTheme] = React.useState(initialPrefs.colorTheme ?? "default")
const [selectedTweakcnTheme, setSelectedTweakcnTheme] = React.useState("") const [selectedTweakcnTheme, setSelectedTweakcnTheme] = React.useState(initialPrefs.tweakcnTheme ?? "")
const [selectedRadius, setSelectedRadius] = React.useState("0.5rem") const [selectedRadius, setSelectedRadius] = React.useState(initialPrefs.radius ?? "0.5rem")
const [importModalOpen, setImportModalOpen] = React.useState(false) const [importModalOpen, setImportModalOpen] = React.useState(false)
const [importedTheme, setImportedTheme] = React.useState<ImportedTheme | null>(null) const [importedTheme, setImportedTheme] = React.useState<ImportedTheme | null>(null)
const handleReset = () => { // Save dark/light mode to Appwrite when it changes (skip first mount)
// Complete reset to application defaults 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") setSelectedTheme("default")
setSelectedTweakcnTheme("") setSelectedTweakcnTheme("")
setSelectedRadius("0.5rem") setSelectedRadius("0.5rem")
setImportedTheme(null) // Clear imported theme setImportedTheme(null)
setBrandColorsValues({}) // Clear brand colors state setBrandColorsValues({})
// 2. Completely remove all custom CSS variables
resetTheme() resetTheme()
// 3. Reset the radius to default
applyRadius("0.5rem") applyRadius("0.5rem")
// 4. Reset sidebar to defaults
updateSidebarConfig({ variant: "inset", collapsible: "offcanvas", side: "left" }) updateSidebarConfig({ variant: "inset", collapsible: "offcanvas", side: "left" })
void saveThemePrefsAction({ colorTheme: "default", tweakcnTheme: "", radius: "0.5rem" })
} }
const handleImport = (themeData: ImportedTheme) => { const handleImport = (themeData: ImportedTheme) => {
@@ -140,11 +155,22 @@ export function ThemeCustomizer({ open, onOpenChange }: ThemeCustomizerProps) {
<TabsContent value="theme" className="flex-1 mt-0"> <TabsContent value="theme" className="flex-1 mt-0">
<ThemeTab <ThemeTab
selectedTheme={selectedTheme} selectedTheme={selectedTheme}
setSelectedTheme={setSelectedTheme} setSelectedTheme={(value) => {
setSelectedTheme(value)
setSelectedTweakcnTheme("")
void saveThemePrefsAction({ colorTheme: value, tweakcnTheme: "" })
}}
selectedTweakcnTheme={selectedTweakcnTheme} selectedTweakcnTheme={selectedTweakcnTheme}
setSelectedTweakcnTheme={setSelectedTweakcnTheme} setSelectedTweakcnTheme={(value) => {
setSelectedTweakcnTheme(value)
setSelectedTheme("")
void saveThemePrefsAction({ tweakcnTheme: value, colorTheme: "" })
}}
selectedRadius={selectedRadius} selectedRadius={selectedRadius}
setSelectedRadius={setSelectedRadius} setSelectedRadius={(value) => {
setSelectedRadius(value)
void saveThemePrefsAction({ radius: value })
}}
setImportedTheme={setImportedTheme} setImportedTheme={setImportedTheme}
onImportClick={handleImportClick} onImportClick={handleImportClick}
/> />
@@ -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;
}
+23
View File
@@ -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<ThemePrefs>): Promise<void> {
try {
const { account } = await createSessionClient();
const existing = await account.getPrefs<Record<string, unknown>>();
await account.updatePrefs({ ...existing, ...update });
} catch {
// best-effort — UI still works without persistence
}
}