From 81fafcf711e011d495a683a5ad1a50afcaf7a07b Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Wed, 25 Mar 2026 20:51:39 +0100 Subject: [PATCH] fix: discard button in sticky save bar instead of browser confirm --- .../settings/unified/UnifiedSettingsModal.tsx | 48 ++++++++++++++----- .../settings/unified/tabs/SiteBotSpamTab.tsx | 17 +++++-- .../settings/unified/tabs/SiteGeneralTab.tsx | 17 +++++-- .../settings/unified/tabs/SitePrivacyTab.tsx | 17 +++++-- .../unified/tabs/SiteVisibilityTab.tsx | 17 +++++-- 5 files changed, 84 insertions(+), 32 deletions(-) diff --git a/components/settings/unified/UnifiedSettingsModal.tsx b/components/settings/unified/UnifiedSettingsModal.tsx index c57b10a..e042a12 100644 --- a/components/settings/unified/UnifiedSettingsModal.tsx +++ b/components/settings/unified/UnifiedSettingsModal.tsx @@ -173,20 +173,25 @@ function TabContent({ activeTab, siteId, onDirtyChange, + hasPendingAction, + onDiscard, }: { context: SettingsContext activeTab: string siteId: string | null onDirtyChange: (dirty: boolean) => void + hasPendingAction: boolean + onDiscard: () => void }) { + const dirtyProps = { onDirtyChange, hasPendingAction, onDiscard } // Site tabs if (context === 'site' && siteId) { switch (activeTab) { - case 'general': return + case 'general': return case 'goals': return - case 'visibility': return - case 'privacy': return - case 'bot-spam': return + case 'visibility': return + case 'privacy': return + case 'bot-spam': return case 'reports': return case 'integrations': return } @@ -229,25 +234,40 @@ export default function UnifiedSettingsModal() { const [sites, setSites] = useState([]) const [activeSiteId, setActiveSiteId] = useState(null) - // ─── Dirty state ────────────────────────────────────────────── + // ─── Dirty state & pending navigation ──────────────────────── const isDirtyRef = useRef(false) + const pendingActionRef = useRef<(() => void) | null>(null) + const [hasPendingAction, setHasPendingAction] = useState(false) const handleDirtyChange = useCallback((dirty: boolean) => { isDirtyRef.current = dirty + // If user saved and there was a pending action, execute it + if (!dirty && pendingActionRef.current) { + const action = pendingActionRef.current + pendingActionRef.current = null + setHasPendingAction(false) + action() + } }, []) - /** Run action if clean, or confirm discard if dirty */ + /** Run action if clean, or store as pending if dirty */ const guardedAction = useCallback((action: () => void) => { if (isDirtyRef.current) { - if (confirm('You have unsaved changes. Discard and continue?')) { - isDirtyRef.current = false - action() - } + pendingActionRef.current = action + setHasPendingAction(true) } else { action() } }, []) + const handleDiscard = useCallback(() => { + isDirtyRef.current = false + setHasPendingAction(false) + const action = pendingActionRef.current + pendingActionRef.current = null + action?.() + }, []) + // Apply initial tab when modal opens useEffect(() => { if (isOpen && initTab) { @@ -262,7 +282,11 @@ export default function UnifiedSettingsModal() { // Reset dirty state when modal opens useEffect(() => { - if (isOpen) isDirtyRef.current = false + if (isOpen) { + isDirtyRef.current = false + pendingActionRef.current = null + setHasPendingAction(false) + } }, [isOpen]) // Detect site from URL and load sites list when modal opens @@ -391,7 +415,7 @@ export default function UnifiedSettingsModal() { transition={{ duration: 0.12 }} className="p-6" > - + diff --git a/components/settings/unified/tabs/SiteBotSpamTab.tsx b/components/settings/unified/tabs/SiteBotSpamTab.tsx index 27ef6c9..3d7eda4 100644 --- a/components/settings/unified/tabs/SiteBotSpamTab.tsx +++ b/components/settings/unified/tabs/SiteBotSpamTab.tsx @@ -7,7 +7,7 @@ import { useSite, useBotFilterStats, useSessions } from '@/lib/swr/dashboard' import { updateSite } from '@/lib/api/sites' import { botFilterSessions, botUnfilterSessions } from '@/lib/api/bot-filter' -export default function SiteBotSpamTab({ siteId, onDirtyChange }: { siteId: string; onDirtyChange?: (dirty: boolean) => void }) { +export default function SiteBotSpamTab({ siteId, onDirtyChange, hasPendingAction, onDiscard }: { siteId: string; onDirtyChange?: (dirty: boolean) => void; hasPendingAction?: boolean; onDiscard?: () => void }) { const { data: site, mutate } = useSite(siteId) const { data: botStats, mutate: mutateBotStats } = useBotFilterStats(siteId) const [filterBots, setFilterBots] = useState(false) @@ -226,10 +226,17 @@ export default function SiteBotSpamTab({ siteId, onDirtyChange }: { siteId: stri {/* Sticky save bar */} {isDirty && (
- Unsaved changes - + {hasPendingAction ? 'Save or discard to continue' : 'Unsaved changes'} +
+ {hasPendingAction && ( + + )} + +
)} diff --git a/components/settings/unified/tabs/SiteGeneralTab.tsx b/components/settings/unified/tabs/SiteGeneralTab.tsx index b80c5e3..6634483 100644 --- a/components/settings/unified/tabs/SiteGeneralTab.tsx +++ b/components/settings/unified/tabs/SiteGeneralTab.tsx @@ -28,7 +28,7 @@ const TIMEZONES = [ { value: 'Australia/Sydney', label: 'Australia/Sydney (AEST)' }, ] -export default function SiteGeneralTab({ siteId, onDirtyChange }: { siteId: string; onDirtyChange?: (dirty: boolean) => void }) { +export default function SiteGeneralTab({ siteId, onDirtyChange, hasPendingAction, onDiscard }: { siteId: string; onDirtyChange?: (dirty: boolean) => void; hasPendingAction?: boolean; onDiscard?: () => void }) { const router = useRouter() const { user } = useAuth() const { closeUnifiedSettings: closeSettings } = useUnifiedSettings() @@ -180,10 +180,17 @@ export default function SiteGeneralTab({ siteId, onDirtyChange }: { siteId: stri {/* Sticky save bar */} {isDirty && (
- Unsaved changes - + {hasPendingAction ? 'Save or discard to continue' : 'Unsaved changes'} +
+ {hasPendingAction && ( + + )} + +
)} diff --git a/components/settings/unified/tabs/SitePrivacyTab.tsx b/components/settings/unified/tabs/SitePrivacyTab.tsx index ffc77de..1708748 100644 --- a/components/settings/unified/tabs/SitePrivacyTab.tsx +++ b/components/settings/unified/tabs/SitePrivacyTab.tsx @@ -28,7 +28,7 @@ function PrivacyToggle({ label, desc, checked, onToggle }: { label: string; desc ) } -export default function SitePrivacyTab({ siteId, onDirtyChange }: { siteId: string; onDirtyChange?: (dirty: boolean) => void }) { +export default function SitePrivacyTab({ siteId, onDirtyChange, hasPendingAction, onDiscard }: { siteId: string; onDirtyChange?: (dirty: boolean) => void; hasPendingAction?: boolean; onDiscard?: () => void }) { const { data: site, mutate } = useSite(siteId) const { data: subscription, error: subscriptionError, mutate: mutateSubscription } = useSubscription() const { data: psiConfig, mutate: mutatePSIConfig } = usePageSpeedConfig(siteId) @@ -253,10 +253,17 @@ export default function SitePrivacyTab({ siteId, onDirtyChange }: { siteId: stri {/* Sticky save bar — only visible when dirty */} {isDirty && (
- Unsaved changes - + {hasPendingAction ? 'Save or discard to continue' : 'Unsaved changes'} +
+ {hasPendingAction && ( + + )} + +
)} diff --git a/components/settings/unified/tabs/SiteVisibilityTab.tsx b/components/settings/unified/tabs/SiteVisibilityTab.tsx index 22c5d9f..441a064 100644 --- a/components/settings/unified/tabs/SiteVisibilityTab.tsx +++ b/components/settings/unified/tabs/SiteVisibilityTab.tsx @@ -9,7 +9,7 @@ import { updateSite } from '@/lib/api/sites' const APP_URL = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3003' -export default function SiteVisibilityTab({ siteId, onDirtyChange }: { siteId: string; onDirtyChange?: (dirty: boolean) => void }) { +export default function SiteVisibilityTab({ siteId, onDirtyChange, hasPendingAction, onDiscard }: { siteId: string; onDirtyChange?: (dirty: boolean) => void; hasPendingAction?: boolean; onDiscard?: () => void }) { const { data: site, mutate } = useSite(siteId) const [isPublic, setIsPublic] = useState(false) const [password, setPassword] = useState('') @@ -149,10 +149,17 @@ export default function SiteVisibilityTab({ siteId, onDirtyChange }: { siteId: s {/* Sticky save bar */} {isDirty && (
- Unsaved changes - + {hasPendingAction ? 'Save or discard to continue' : 'Unsaved changes'} +
+ {hasPendingAction && ( + + )} + +
)}