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:
@@ -9,7 +9,7 @@ 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";
|
||||
import type { UserPrefs as ThemePrefs } from "@/lib/appwrite/user-prefs-actions";
|
||||
|
||||
export type ShellUser = {
|
||||
id: string;
|
||||
|
||||
@@ -2,8 +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 { getUserPrefs } from "@/lib/appwrite/user-prefs-actions";
|
||||
import type { UserPrefs as ThemePrefs } from "@/lib/appwrite/user-prefs-actions";
|
||||
import { DashboardShell } from "./dashboard-shell";
|
||||
|
||||
export default async function DashboardLayout({
|
||||
@@ -14,15 +14,7 @@ export default async function DashboardLayout({
|
||||
const ctx = await getActiveContext();
|
||||
if (!ctx) redirect("/onboarding");
|
||||
|
||||
let themePrefs: ThemePrefs = {};
|
||||
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 themePrefs: ThemePrefs = await getUserPrefs();
|
||||
|
||||
const company = {
|
||||
id: ctx.tenantId,
|
||||
|
||||
@@ -9,8 +9,8 @@ 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 { saveUserPrefsAction } from '@/lib/appwrite/user-prefs-actions'
|
||||
import type { UserPrefs as ThemePrefs } from '@/lib/appwrite/user-prefs-actions'
|
||||
import { getLocalThemePrefs, saveLocalThemePrefs } from '@/lib/local-theme-prefs'
|
||||
import { ThemeTab } from './theme-tab'
|
||||
import { LayoutTab } from './layout-tab'
|
||||
@@ -68,7 +68,7 @@ export function ThemeCustomizer({ open, onOpenChange, initialPrefs }: ThemeCusto
|
||||
applyRadius("0.5rem")
|
||||
updateSidebarConfig({ variant: "inset", collapsible: "offcanvas", side: "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) => {
|
||||
@@ -165,20 +165,20 @@ export function ThemeCustomizer({ open, onOpenChange, initialPrefs }: ThemeCusto
|
||||
setSelectedTheme(value)
|
||||
setSelectedTweakcnTheme("")
|
||||
saveLocalThemePrefs({ colorTheme: value, tweakcnTheme: "" })
|
||||
void saveThemePrefsAction({ colorTheme: value, tweakcnTheme: "" })
|
||||
void saveUserPrefsAction({ colorTheme: value, tweakcnTheme: "" })
|
||||
}}
|
||||
selectedTweakcnTheme={selectedTweakcnTheme}
|
||||
setSelectedTweakcnTheme={(value) => {
|
||||
setSelectedTweakcnTheme(value)
|
||||
setSelectedTheme("")
|
||||
saveLocalThemePrefs({ tweakcnTheme: value, colorTheme: "" })
|
||||
void saveThemePrefsAction({ tweakcnTheme: value, colorTheme: "" })
|
||||
void saveUserPrefsAction({ tweakcnTheme: value, colorTheme: "" })
|
||||
}}
|
||||
selectedRadius={selectedRadius}
|
||||
setSelectedRadius={(value) => {
|
||||
setSelectedRadius(value)
|
||||
saveLocalThemePrefs({ radius: value })
|
||||
void saveThemePrefsAction({ radius: value })
|
||||
void saveUserPrefsAction({ radius: value })
|
||||
}}
|
||||
setImportedTheme={setImportedTheme}
|
||||
onImportClick={handleImportClick}
|
||||
|
||||
@@ -6,7 +6,7 @@ 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";
|
||||
import type { UserPrefs as ThemePrefs } from "@/lib/appwrite/user-prefs-actions";
|
||||
import { getLocalThemePrefs } from "@/lib/local-theme-prefs";
|
||||
|
||||
export function PrefsInitializer({ prefs }: { prefs: ThemePrefs }) {
|
||||
|
||||
@@ -29,6 +29,7 @@ export const TABLES = {
|
||||
leadActivities: "lead_activities",
|
||||
passwordResets: "password_resets",
|
||||
attachments: "attachments",
|
||||
userPreferences: "user_preferences",
|
||||
} as const;
|
||||
|
||||
export type TableId = (typeof TABLES)[keyof typeof TABLES];
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user