fix: tema kaydetme race condition düzeltildi

Önceki hata: ThemeTab her iki setter'ı da çağırıyordu (setSelectedTheme +
setSelectedTweakcnTheme). Bunlar wrapper'a bağlıydı, her wrapper kendi
saveThemePrefsAction'ını çağırıyordu. İkinci çağrı colorTheme:'' yazarak
birincinin kaydını siliyordu.

Düzeltme:
- ThemeTab'a RAW React state setter'ları iletildi (wrapper değil)
- ThemeTab'ın cross-clear mantığı olduğu gibi kaldı
- Appwrite kaydı useEffect'e taşındı: React 18 olay yöneticisindeki
  tüm state güncellemelerini batch'ledikten SONRA tek seferde tetiklenir
  → selectedTheme ve selectedTweakcnTheme doğru nihai değerleriyle kaydedilir
This commit is contained in:
egecankomur
2026-05-05 21:23:54 +03:00
parent 63392bab7b
commit fd5c6c645f
+28 -29
View File
@@ -35,17 +35,29 @@ export function ThemeCustomizer({ open, onOpenChange, initialPrefs }: ThemeCusto
const [importModalOpen, setImportModalOpen] = React.useState(false) const [importModalOpen, setImportModalOpen] = React.useState(false)
const [importedTheme, setImportedTheme] = React.useState<ImportedTheme | null>(null) const [importedTheme, setImportedTheme] = React.useState<ImportedTheme | null>(null)
// Save dark/light mode to Appwrite when it changes (skip first mount) // --- Appwrite persistence via useEffect (fires ONCE after React batches state updates) ---
const themeMountRef = React.useRef(false)
const darkModeSaveMountRef = React.useRef(false)
React.useEffect(() => { React.useEffect(() => {
if (!themeMountRef.current) { themeMountRef.current = true; return } if (!darkModeSaveMountRef.current) { darkModeSaveMountRef.current = true; return }
void saveThemePrefsAction({ theme }) void saveThemePrefsAction({ theme })
}, [theme]) }, [theme])
// Save sidebar config to Appwrite when it changes (skip first mount) const colorThemeSaveMountRef = React.useRef(false)
const sidebarMountRef = React.useRef(false)
React.useEffect(() => { React.useEffect(() => {
if (!sidebarMountRef.current) { sidebarMountRef.current = true; return } if (!colorThemeSaveMountRef.current) { colorThemeSaveMountRef.current = true; return }
void saveThemePrefsAction({ colorTheme: selectedTheme, tweakcnTheme: selectedTweakcnTheme })
}, [selectedTheme, selectedTweakcnTheme])
const radiusSaveMountRef = React.useRef(false)
React.useEffect(() => {
if (!radiusSaveMountRef.current) { radiusSaveMountRef.current = true; return }
void saveThemePrefsAction({ radius: selectedRadius })
}, [selectedRadius])
const sidebarSaveMountRef = React.useRef(false)
React.useEffect(() => {
if (!sidebarSaveMountRef.current) { sidebarSaveMountRef.current = true; return }
void saveThemePrefsAction({ void saveThemePrefsAction({
sidebarVariant: sidebarConfig.variant, sidebarVariant: sidebarConfig.variant,
sidebarCollapsible: sidebarConfig.collapsible, sidebarCollapsible: sidebarConfig.collapsible,
@@ -53,6 +65,8 @@ export function ThemeCustomizer({ open, onOpenChange, initialPrefs }: ThemeCusto
}) })
}, [sidebarConfig.variant, sidebarConfig.collapsible, sidebarConfig.side]) }, [sidebarConfig.variant, sidebarConfig.collapsible, sidebarConfig.side])
// --- Theme reset ---
const handleReset = () => { const handleReset = () => {
setSelectedTheme("default") setSelectedTheme("default")
setSelectedTweakcnTheme("") setSelectedTweakcnTheme("")
@@ -61,7 +75,7 @@ export function ThemeCustomizer({ open, onOpenChange, initialPrefs }: ThemeCusto
setBrandColorsValues({}) setBrandColorsValues({})
resetTheme() resetTheme()
applyRadius("0.5rem") applyRadius("0.5rem")
void saveThemePrefsAction({ colorTheme: "default", tweakcnTheme: "", radius: "0.5rem" }) // colorThemeSaveEffect and radiusSaveEffect will fire via the state changes above
} }
const handleImport = (themeData: ImportedTheme) => { const handleImport = (themeData: ImportedTheme) => {
@@ -69,7 +83,7 @@ export function ThemeCustomizer({ open, onOpenChange, initialPrefs }: ThemeCusto
setSelectedTheme("") setSelectedTheme("")
setSelectedTweakcnTheme("") setSelectedTweakcnTheme("")
applyImportedTheme(themeData, isDarkMode) applyImportedTheme(themeData, isDarkMode)
// Imported themes have no stable ID to save // Imported themes have no stable identifier to persist
} }
// Re-apply when dark/light toggles // Re-apply when dark/light toggles
@@ -84,24 +98,6 @@ export function ThemeCustomizer({ open, onOpenChange, initialPrefs }: ThemeCusto
} }
}, [isDarkMode, importedTheme, selectedTheme, selectedTweakcnTheme, applyImportedTheme, applyTheme, applyTweakcnTheme]) }, [isDarkMode, importedTheme, selectedTheme, selectedTweakcnTheme, applyImportedTheme, applyTheme, applyTweakcnTheme])
// Wrappers that also persist to Appwrite
const handleSetSelectedTheme = (value: string) => {
setSelectedTheme(value)
setSelectedTweakcnTheme("")
void saveThemePrefsAction({ colorTheme: value, tweakcnTheme: "" })
}
const handleSetSelectedTweakcnTheme = (value: string) => {
setSelectedTweakcnTheme(value)
setSelectedTheme("")
void saveThemePrefsAction({ tweakcnTheme: value, colorTheme: "" })
}
const handleSetSelectedRadius = (value: string) => {
setSelectedRadius(value)
void saveThemePrefsAction({ radius: value })
}
return ( return (
<> <>
<Sheet open={open} onOpenChange={onOpenChange} modal={false}> <Sheet open={open} onOpenChange={onOpenChange} modal={false}>
@@ -159,13 +155,16 @@ export function ThemeCustomizer({ open, onOpenChange, initialPrefs }: ThemeCusto
</div> </div>
<TabsContent value="theme" className="flex-1 mt-0"> <TabsContent value="theme" className="flex-1 mt-0">
{/* Pass RAW state setters — ThemeTab handles cross-clearing itself.
Appwrite persistence is handled by the useEffects above, which fire
once after React batches all state updates in the event handler. */}
<ThemeTab <ThemeTab
selectedTheme={selectedTheme} selectedTheme={selectedTheme}
setSelectedTheme={handleSetSelectedTheme} setSelectedTheme={setSelectedTheme}
selectedTweakcnTheme={selectedTweakcnTheme} selectedTweakcnTheme={selectedTweakcnTheme}
setSelectedTweakcnTheme={handleSetSelectedTweakcnTheme} setSelectedTweakcnTheme={setSelectedTweakcnTheme}
selectedRadius={selectedRadius} selectedRadius={selectedRadius}
setSelectedRadius={handleSetSelectedRadius} setSelectedRadius={setSelectedRadius}
setImportedTheme={setImportedTheme} setImportedTheme={setImportedTheme}
onImportClick={() => setImportModalOpen(true)} onImportClick={() => setImportModalOpen(true)}
/> />