feat: store user theme prefs in DB instead of Appwrite account.getPrefs

db: create user_preferences table (isletmem) — userId unique index,
    theme/colorTheme/tweakcnTheme/radius/sidebar* columns

- user-prefs-actions.ts: getUserPrefs (server-side read, plain object),
  saveUserPrefsAction (upsert by userId, Permission.user for row security)
- schema.ts: TABLES.userPreferences added
- layout.tsx: replace account.getPrefs+JSON.parse hack with getUserPrefs()
- dashboard-shell, prefs-initializer, theme-customizer: import UserPrefs
  type and saveUserPrefsAction instead of old saveThemePrefsAction
- theme-prefs-actions.ts: deleted (no remaining references)

Reason: account.updatePrefs is shared across all apps in the same Appwrite
project (İşletmem + Emlak share project 69f27b51). A dedicated per-app
table gives proper isolation, typed schema, and no prototype-object issues.
This commit is contained in:
kovakmedya
2026-05-08 17:48:31 +03:00
parent 00c740de80
commit 971d8b0a58
7 changed files with 103 additions and 42 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ 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 { 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"; import type { UserPrefs as ThemePrefs } from "@/lib/appwrite/user-prefs-actions";
export type ShellUser = { export type ShellUser = {
id: string; id: string;
+3 -11
View File
@@ -2,8 +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 { getUserPrefs } from "@/lib/appwrite/user-prefs-actions";
import type { ThemePrefs } from "@/lib/appwrite/theme-prefs-actions"; import type { UserPrefs as ThemePrefs } from "@/lib/appwrite/user-prefs-actions";
import { DashboardShell } from "./dashboard-shell"; import { DashboardShell } from "./dashboard-shell";
export default async function DashboardLayout({ export default async function DashboardLayout({
@@ -14,15 +14,7 @@ export default async function DashboardLayout({
const ctx = await getActiveContext(); const ctx = await getActiveContext();
if (!ctx) redirect("/onboarding"); if (!ctx) redirect("/onboarding");
let themePrefs: ThemePrefs = {}; const themePrefs: ThemePrefs = await getUserPrefs();
try {
const { account } = await createSessionClient();
const raw = await account.getPrefs<ThemePrefs>();
// getPrefs returns an Appwrite prototype object — must be a plain object for Server→Client prop
themePrefs = JSON.parse(JSON.stringify(raw)) as ThemePrefs;
} catch {
// use defaults if prefs unavailable
}
const company = { const company = {
id: ctx.tenantId, id: ctx.tenantId,
+6 -6
View File
@@ -9,8 +9,8 @@ import { useThemeManager } from '@/hooks/use-theme-manager'
import { useTheme } from '@/hooks/use-theme' 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 { saveUserPrefsAction } from '@/lib/appwrite/user-prefs-actions'
import type { ThemePrefs } from '@/lib/appwrite/theme-prefs-actions' import type { UserPrefs as ThemePrefs } from '@/lib/appwrite/user-prefs-actions'
import { getLocalThemePrefs, saveLocalThemePrefs } from '@/lib/local-theme-prefs' import { getLocalThemePrefs, saveLocalThemePrefs } from '@/lib/local-theme-prefs'
import { ThemeTab } from './theme-tab' import { ThemeTab } from './theme-tab'
import { LayoutTab } from './layout-tab' import { LayoutTab } from './layout-tab'
@@ -68,7 +68,7 @@ export function ThemeCustomizer({ open, onOpenChange, initialPrefs }: ThemeCusto
applyRadius("0.5rem") applyRadius("0.5rem")
updateSidebarConfig({ variant: "inset", collapsible: "offcanvas", side: "left" }) updateSidebarConfig({ variant: "inset", collapsible: "offcanvas", side: "left" })
saveLocalThemePrefs({ colorTheme: "default", tweakcnTheme: "", radius: "0.5rem", sidebarVariant: "inset", sidebarCollapsible: "offcanvas", sidebarSide: "left" }) saveLocalThemePrefs({ colorTheme: "default", tweakcnTheme: "", radius: "0.5rem", sidebarVariant: "inset", sidebarCollapsible: "offcanvas", sidebarSide: "left" })
void saveThemePrefsAction({ colorTheme: "default", tweakcnTheme: "", radius: "0.5rem", sidebarVariant: "inset", sidebarCollapsible: "offcanvas", sidebarSide: "left" }) void saveUserPrefsAction({ colorTheme: "default", tweakcnTheme: "", radius: "0.5rem", sidebarVariant: "inset", sidebarCollapsible: "offcanvas", sidebarSide: "left" })
} }
const handleImport = (themeData: ImportedTheme) => { const handleImport = (themeData: ImportedTheme) => {
@@ -165,20 +165,20 @@ export function ThemeCustomizer({ open, onOpenChange, initialPrefs }: ThemeCusto
setSelectedTheme(value) setSelectedTheme(value)
setSelectedTweakcnTheme("") setSelectedTweakcnTheme("")
saveLocalThemePrefs({ colorTheme: value, tweakcnTheme: "" }) saveLocalThemePrefs({ colorTheme: value, tweakcnTheme: "" })
void saveThemePrefsAction({ colorTheme: value, tweakcnTheme: "" }) void saveUserPrefsAction({ colorTheme: value, tweakcnTheme: "" })
}} }}
selectedTweakcnTheme={selectedTweakcnTheme} selectedTweakcnTheme={selectedTweakcnTheme}
setSelectedTweakcnTheme={(value) => { setSelectedTweakcnTheme={(value) => {
setSelectedTweakcnTheme(value) setSelectedTweakcnTheme(value)
setSelectedTheme("") setSelectedTheme("")
saveLocalThemePrefs({ tweakcnTheme: value, colorTheme: "" }) saveLocalThemePrefs({ tweakcnTheme: value, colorTheme: "" })
void saveThemePrefsAction({ tweakcnTheme: value, colorTheme: "" }) void saveUserPrefsAction({ tweakcnTheme: value, colorTheme: "" })
}} }}
selectedRadius={selectedRadius} selectedRadius={selectedRadius}
setSelectedRadius={(value) => { setSelectedRadius={(value) => {
setSelectedRadius(value) setSelectedRadius(value)
saveLocalThemePrefs({ radius: value }) saveLocalThemePrefs({ radius: value })
void saveThemePrefsAction({ radius: value }) void saveUserPrefsAction({ radius: value })
}} }}
setImportedTheme={setImportedTheme} setImportedTheme={setImportedTheme}
onImportClick={handleImportClick} onImportClick={handleImportClick}
@@ -6,7 +6,7 @@ import { useSidebarConfig } from "@/contexts/sidebar-context";
import { useTheme } from "@/hooks/use-theme"; import { useTheme } from "@/hooks/use-theme";
import { useThemeManager } from "@/hooks/use-theme-manager"; import { useThemeManager } from "@/hooks/use-theme-manager";
import { tweakcnThemes } from "@/config/theme-data"; import { tweakcnThemes } from "@/config/theme-data";
import type { ThemePrefs } from "@/lib/appwrite/theme-prefs-actions"; import type { UserPrefs as ThemePrefs } from "@/lib/appwrite/user-prefs-actions";
import { getLocalThemePrefs } from "@/lib/local-theme-prefs"; import { getLocalThemePrefs } from "@/lib/local-theme-prefs";
export function PrefsInitializer({ prefs }: { prefs: ThemePrefs }) { export function PrefsInitializer({ prefs }: { prefs: ThemePrefs }) {
+1
View File
@@ -29,6 +29,7 @@ export const TABLES = {
leadActivities: "lead_activities", leadActivities: "lead_activities",
passwordResets: "password_resets", passwordResets: "password_resets",
attachments: "attachments", attachments: "attachments",
userPreferences: "user_preferences",
} as const; } as const;
export type TableId = (typeof TABLES)[keyof typeof TABLES]; export type TableId = (typeof TABLES)[keyof typeof TABLES];
-23
View File
@@ -1,23 +0,0 @@
"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
}
}
+91
View File
@@ -0,0 +1,91 @@
"use server";
import { ID, Permission, Query, Role } from "node-appwrite";
import { createAdminClient, createSessionClient } from "./server";
import { DATABASE_ID, TABLES } from "./schema";
export interface UserPrefs {
theme?: "dark" | "light" | "system";
colorTheme?: string;
tweakcnTheme?: string;
radius?: string;
sidebarVariant?: "sidebar" | "floating" | "inset";
sidebarCollapsible?: "offcanvas" | "icon" | "none";
sidebarSide?: "left" | "right";
}
export async function getUserPrefs(): Promise<UserPrefs> {
try {
const { account } = await createSessionClient();
const user = await account.get();
const { tablesDB } = createAdminClient();
const result = await tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.userPreferences,
queries: [Query.equal("userId", user.$id), Query.limit(1)],
});
if (result.rows.length === 0) return {};
const row = result.rows[0] as Record<string, unknown>;
return {
theme: (row.theme as UserPrefs["theme"]) ?? undefined,
colorTheme: (row.colorTheme as string) ?? undefined,
tweakcnTheme: (row.tweakcnTheme as string) ?? undefined,
radius: (row.radius as string) ?? undefined,
sidebarVariant: (row.sidebarVariant as UserPrefs["sidebarVariant"]) ?? undefined,
sidebarCollapsible: (row.sidebarCollapsible as UserPrefs["sidebarCollapsible"]) ?? undefined,
sidebarSide: (row.sidebarSide as UserPrefs["sidebarSide"]) ?? undefined,
};
} catch {
return {};
}
}
export async function saveUserPrefsAction(update: Partial<UserPrefs>): Promise<void> {
try {
const { account } = await createSessionClient();
const user = await account.get();
const { tablesDB } = createAdminClient();
// Sadece tanımlı (undefined olmayan) alanları yaz
const clean: Record<string, unknown> = {};
for (const [k, v] of Object.entries(update)) {
if (v !== undefined) clean[k] = v;
}
if (Object.keys(clean).length === 0) return;
const existing = await tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.userPreferences,
queries: [Query.equal("userId", user.$id), Query.limit(1)],
});
const perms = [
Permission.read(Role.user(user.$id)),
Permission.update(Role.user(user.$id)),
Permission.delete(Role.user(user.$id)),
];
if (existing.rows.length === 0) {
await tablesDB.createRow(
DATABASE_ID,
TABLES.userPreferences,
ID.unique(),
{ userId: user.$id, ...clean },
perms,
);
} else {
await tablesDB.updateRow(
DATABASE_ID,
TABLES.userPreferences,
existing.rows[0].$id,
clean,
);
}
} catch {
// best-effort
}
}