diff --git a/components/settings/unified/tabs/SiteBotSpamTab.tsx b/components/settings/unified/tabs/SiteBotSpamTab.tsx index bfd736a..173faf3 100644 --- a/components/settings/unified/tabs/SiteBotSpamTab.tsx +++ b/components/settings/unified/tabs/SiteBotSpamTab.tsx @@ -12,6 +12,7 @@ export default function SiteBotSpamTab({ siteId, onDirtyChange }: { siteId: stri const { data: botStats, mutate: mutateBotStats } = useBotFilterStats(siteId) const [filterBots, setFilterBots] = useState(false) const [saving, setSaving] = useState(false) + const [isDirty, setIsDirty] = useState(false) const initialFilterRef = useRef(null) const [botView, setBotView] = useState<'review' | 'blocked'>('review') @@ -32,7 +33,9 @@ export default function SiteBotSpamTab({ siteId, onDirtyChange }: { siteId: stri // Track dirty state useEffect(() => { if (initialFilterRef.current === null) return - onDirtyChange?.(filterBots !== initialFilterRef.current) + const dirty = filterBots !== initialFilterRef.current + setIsDirty(dirty) + onDirtyChange?.(dirty) }, [filterBots, onDirtyChange]) const handleSave = async () => { @@ -219,11 +222,15 @@ export default function SiteBotSpamTab({ siteId, onDirtyChange }: { siteId: stri -
- -
+ {/* Sticky save bar */} + {isDirty && ( +
+ Unsaved changes + +
+ )} ) } diff --git a/components/settings/unified/tabs/SiteGeneralTab.tsx b/components/settings/unified/tabs/SiteGeneralTab.tsx index 4ada9ca..9b40fab 100644 --- a/components/settings/unified/tabs/SiteGeneralTab.tsx +++ b/components/settings/unified/tabs/SiteGeneralTab.tsx @@ -41,12 +41,14 @@ export default function SiteGeneralTab({ siteId, onDirtyChange }: { siteId: stri const canEdit = user?.role === 'owner' || user?.role === 'admin' const initialRef = useRef('') + const [isDirty, setIsDirty] = useState(false) useEffect(() => { if (site) { setName(site.name || '') setTimezone(site.timezone || 'UTC') initialRef.current = JSON.stringify({ name: site.name || '', timezone: site.timezone || 'UTC' }) + setIsDirty(false) } }, [site]) @@ -54,7 +56,9 @@ export default function SiteGeneralTab({ siteId, onDirtyChange }: { siteId: stri useEffect(() => { if (!initialRef.current) return const current = JSON.stringify({ name, timezone }) - onDirtyChange?.(current !== initialRef.current) + const dirty = current !== initialRef.current + setIsDirty(dirty) + onDirtyChange?.(dirty) }, [name, timezone, onDirtyChange]) const handleSave = async () => { @@ -172,12 +176,15 @@ export default function SiteGeneralTab({ siteId, onDirtyChange }: { siteId: stri

- {/* Save */} -
- -
+ {/* Sticky save bar */} + {isDirty && ( +
+ Unsaved changes + +
+ )} {/* Danger Zone */} {canEdit && ( diff --git a/components/settings/unified/tabs/SitePrivacyTab.tsx b/components/settings/unified/tabs/SitePrivacyTab.tsx index 3fc8446..a6a9e51 100644 --- a/components/settings/unified/tabs/SitePrivacyTab.tsx +++ b/components/settings/unified/tabs/SitePrivacyTab.tsx @@ -30,6 +30,7 @@ export default function SitePrivacyTab({ siteId, onDirtyChange }: { siteId: stri const [excludedPaths, setExcludedPaths] = useState('') const [snippetCopied, setSnippetCopied] = useState(false) const [saving, setSaving] = useState(false) + const [isDirty, setIsDirty] = useState(false) const initialRef = useRef('') useEffect(() => { @@ -52,6 +53,7 @@ export default function SitePrivacyTab({ siteId, onDirtyChange }: { siteId: stri dataRetention: site.data_retention_months ?? 6, excludedPaths: (site.excluded_paths || []).join('\n'), }) + setIsDirty(false) } }, [site]) @@ -59,7 +61,9 @@ export default function SitePrivacyTab({ siteId, onDirtyChange }: { siteId: stri useEffect(() => { if (!initialRef.current) return const current = JSON.stringify({ collectPagePaths, collectReferrers, collectDeviceInfo, collectScreenRes, collectGeoData, hideUnknownLocations, dataRetention, excludedPaths }) - onDirtyChange?.(current !== initialRef.current) + const dirty = current !== initialRef.current + setIsDirty(dirty) + onDirtyChange?.(dirty) }, [collectPagePaths, collectReferrers, collectDeviceInfo, collectScreenRes, collectGeoData, hideUnknownLocations, dataRetention, excludedPaths, onDirtyChange]) const handleSave = async () => { @@ -242,11 +246,15 @@ export default function SitePrivacyTab({ siteId, onDirtyChange }: { siteId: stri -
- -
+ {/* Sticky save bar — only visible when dirty */} + {isDirty && ( +
+ Unsaved changes + +
+ )} ) } diff --git a/components/settings/unified/tabs/SiteVisibilityTab.tsx b/components/settings/unified/tabs/SiteVisibilityTab.tsx index 87ab2b9..2c28efc 100644 --- a/components/settings/unified/tabs/SiteVisibilityTab.tsx +++ b/components/settings/unified/tabs/SiteVisibilityTab.tsx @@ -16,6 +16,7 @@ export default function SiteVisibilityTab({ siteId, onDirtyChange }: { siteId: s const [passwordEnabled, setPasswordEnabled] = useState(false) const [saving, setSaving] = useState(false) const [linkCopied, setLinkCopied] = useState(false) + const [isDirty, setIsDirty] = useState(false) const initialRef = useRef('') useEffect(() => { @@ -23,6 +24,7 @@ export default function SiteVisibilityTab({ siteId, onDirtyChange }: { siteId: s setIsPublic(site.is_public ?? false) setPasswordEnabled(site.has_password ?? false) initialRef.current = JSON.stringify({ isPublic: site.is_public ?? false, passwordEnabled: site.has_password ?? false }) + setIsDirty(false) } }, [site]) @@ -31,6 +33,7 @@ export default function SiteVisibilityTab({ siteId, onDirtyChange }: { siteId: s if (!initialRef.current) return const current = JSON.stringify({ isPublic, passwordEnabled }) const dirty = current !== initialRef.current || password.length > 0 + setIsDirty(dirty) onDirtyChange?.(dirty) }, [isPublic, passwordEnabled, password, onDirtyChange]) @@ -142,11 +145,15 @@ export default function SiteVisibilityTab({ siteId, onDirtyChange }: { siteId: s )} -
- -
+ {/* Sticky save bar */} + {isDirty && ( +
+ Unsaved changes + +
+ )} ) }