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:
@@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user