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 { 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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 }) {
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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