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 { 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 { toast } from 'sonner'
import { saveUserPrefsAction } from '@/lib/appwrite/user-prefs-actions' import { saveUserPrefsAction } from '@/lib/appwrite/user-prefs-actions'
import type { UserPrefs as ThemePrefs } 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 { getLocalThemePrefs, saveLocalThemePrefs } from '@/lib/local-theme-prefs'
@@ -58,6 +59,12 @@ export function ThemeCustomizer({ open, onOpenChange, initialPrefs }: ThemeCusto
}) })
}, [sidebarConfig.variant, sidebarConfig.collapsible, sidebarConfig.side]) }, [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 = () => { const handleReset = () => {
setSelectedTheme("default") setSelectedTheme("default")
setSelectedTweakcnTheme("") setSelectedTweakcnTheme("")
@@ -68,7 +75,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 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) => { const handleImport = (themeData: ImportedTheme) => {
@@ -165,20 +172,20 @@ export function ThemeCustomizer({ open, onOpenChange, initialPrefs }: ThemeCusto
setSelectedTheme(value) setSelectedTheme(value)
setSelectedTweakcnTheme("") setSelectedTweakcnTheme("")
saveLocalThemePrefs({ colorTheme: value, tweakcnTheme: "" }) saveLocalThemePrefs({ colorTheme: value, tweakcnTheme: "" })
void saveUserPrefsAction({ colorTheme: value, tweakcnTheme: "" }) savePrefs({ 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 saveUserPrefsAction({ tweakcnTheme: value, colorTheme: "" }) savePrefs({ tweakcnTheme: value, colorTheme: "" })
}} }}
selectedRadius={selectedRadius} selectedRadius={selectedRadius}
setSelectedRadius={(value) => { setSelectedRadius={(value) => {
setSelectedRadius(value) setSelectedRadius(value)
saveLocalThemePrefs({ radius: value }) saveLocalThemePrefs({ radius: value })
void saveUserPrefsAction({ radius: value }) savePrefs({ radius: value })
}} }}
setImportedTheme={setImportedTheme} setImportedTheme={setImportedTheme}
onImportClick={handleImportClick} onImportClick={handleImportClick}
+17 -9
View File
@@ -30,11 +30,12 @@ export async function getUserPrefs(): Promise<UserPrefs> {
if (result.rows.length === 0) return {}; if (result.rows.length === 0) return {};
const row = result.rows[0] as Record<string, unknown>; const row = result.rows[0] as Record<string, unknown>;
const str = (v: unknown) => (v && typeof v === "string" ? v : undefined);
return { return {
theme: (row.theme as UserPrefs["theme"]) ?? undefined, theme: (row.theme as UserPrefs["theme"]) ?? undefined,
colorTheme: (row.colorTheme as string) ?? undefined, colorTheme: str(row.colorTheme),
tweakcnTheme: (row.tweakcnTheme as string) ?? undefined, tweakcnTheme: str(row.tweakcnTheme),
radius: (row.radius as string) ?? undefined, radius: str(row.radius),
sidebarVariant: (row.sidebarVariant as UserPrefs["sidebarVariant"]) ?? undefined, sidebarVariant: (row.sidebarVariant as UserPrefs["sidebarVariant"]) ?? undefined,
sidebarCollapsible: (row.sidebarCollapsible as UserPrefs["sidebarCollapsible"]) ?? undefined, sidebarCollapsible: (row.sidebarCollapsible as UserPrefs["sidebarCollapsible"]) ?? undefined,
sidebarSide: (row.sidebarSide as UserPrefs["sidebarSide"]) ?? 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 { try {
const { account } = await createSessionClient(); const { account } = await createSessionClient();
const user = await account.get(); const user = await account.get();
const { tablesDB } = createAdminClient(); 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> = {}; const clean: Record<string, unknown> = {};
for (const [k, v] of Object.entries(update)) { 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({ const existing = await tablesDB.listRows({
databaseId: DATABASE_ID, databaseId: DATABASE_ID,
@@ -85,7 +89,11 @@ export async function saveUserPrefsAction(update: Partial<UserPrefs>): Promise<v
clean, 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 };
} }
} }