fix: surface saveUserPrefsAction errors, convert empty strings to null

- saveUserPrefsAction now returns {ok, error} instead of void
- empty string values converted to null before writing (Appwrite nullable attrs)
- getUserPrefs treats empty strings as absent (str() helper)
- ThemeCustomizer savePrefs wrapper shows toast on failure
- manually cleared corrupted colorTheme/tweakcnTheme empty strings from DB
This commit is contained in:
kovakmedya
2026-05-08 18:00:22 +03:00
parent 971d8b0a58
commit 2b6877736f
2 changed files with 28 additions and 13 deletions
+11 -4
View File
@@ -9,6 +9,7 @@ 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 { toast } from 'sonner'
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'
@@ -58,6 +59,12 @@ export function ThemeCustomizer({ open, onOpenChange, initialPrefs }: ThemeCusto
})
}, [sidebarConfig.variant, sidebarConfig.collapsible, sidebarConfig.side])
const savePrefs = (update: Parameters<typeof saveUserPrefsAction>[0]) => {
saveUserPrefsAction(update).then((res) => {
if (!res.ok) toast.error("Tercihler kaydedilemedi: " + res.error)
})
}
const handleReset = () => {
setSelectedTheme("default")
setSelectedTweakcnTheme("")
@@ -68,7 +75,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 saveUserPrefsAction({ colorTheme: "default", tweakcnTheme: "", radius: "0.5rem", sidebarVariant: "inset", sidebarCollapsible: "offcanvas", sidebarSide: "left" })
savePrefs({ colorTheme: "default", tweakcnTheme: "", radius: "0.5rem", sidebarVariant: "inset", sidebarCollapsible: "offcanvas", sidebarSide: "left" })
}
const handleImport = (themeData: ImportedTheme) => {
@@ -165,20 +172,20 @@ export function ThemeCustomizer({ open, onOpenChange, initialPrefs }: ThemeCusto
setSelectedTheme(value)
setSelectedTweakcnTheme("")
saveLocalThemePrefs({ colorTheme: value, tweakcnTheme: "" })
void saveUserPrefsAction({ colorTheme: value, tweakcnTheme: "" })
savePrefs({ colorTheme: value, tweakcnTheme: "" })
}}
selectedTweakcnTheme={selectedTweakcnTheme}
setSelectedTweakcnTheme={(value) => {
setSelectedTweakcnTheme(value)
setSelectedTheme("")
saveLocalThemePrefs({ tweakcnTheme: value, colorTheme: "" })
void saveUserPrefsAction({ tweakcnTheme: value, colorTheme: "" })
savePrefs({ tweakcnTheme: value, colorTheme: "" })
}}
selectedRadius={selectedRadius}
setSelectedRadius={(value) => {
setSelectedRadius(value)
saveLocalThemePrefs({ radius: value })
void saveUserPrefsAction({ radius: value })
savePrefs({ radius: value })
}}
setImportedTheme={setImportedTheme}
onImportClick={handleImportClick}
+17 -9
View File
@@ -30,11 +30,12 @@ export async function getUserPrefs(): Promise<UserPrefs> {
if (result.rows.length === 0) return {};
const row = result.rows[0] as Record<string, unknown>;
const str = (v: unknown) => (v && typeof v === "string" ? v : undefined);
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,
colorTheme: str(row.colorTheme),
tweakcnTheme: str(row.tweakcnTheme),
radius: str(row.radius),
sidebarVariant: (row.sidebarVariant as UserPrefs["sidebarVariant"]) ?? undefined,
sidebarCollapsible: (row.sidebarCollapsible as UserPrefs["sidebarCollapsible"]) ?? undefined,
sidebarSide: (row.sidebarSide as UserPrefs["sidebarSide"]) ?? undefined,
@@ -44,18 +45,21 @@ export async function getUserPrefs(): Promise<UserPrefs> {
}
}
export async function saveUserPrefsAction(update: Partial<UserPrefs>): Promise<void> {
export async function saveUserPrefsAction(
update: Partial<UserPrefs>,
): Promise<{ ok: boolean; error?: string }> {
try {
const { account } = await createSessionClient();
const user = await account.get();
const { tablesDB } = createAdminClient();
// Sadece tanımlı (undefined olmayan) alanları yaz
// undefined → skip, "" → null (Appwrite rejects empty strings on nullable attrs)
const clean: Record<string, unknown> = {};
for (const [k, v] of Object.entries(update)) {
if (v !== undefined) clean[k] = v;
if (v === undefined) continue;
clean[k] = v === "" ? null : v;
}
if (Object.keys(clean).length === 0) return;
if (Object.keys(clean).length === 0) return { ok: true };
const existing = await tablesDB.listRows({
databaseId: DATABASE_ID,
@@ -85,7 +89,11 @@ export async function saveUserPrefsAction(update: Partial<UserPrefs>): Promise<v
clean,
);
}
} catch {
// best-effort
return { ok: true };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.error("[saveUserPrefsAction]", msg);
return { ok: false, error: msg };
}
}