feat(settings): sticky save bar appears only when dirty, replaces static button
This commit is contained in:
@@ -12,6 +12,7 @@ export default function SiteBotSpamTab({ siteId, onDirtyChange }: { siteId: stri
|
|||||||
const { data: botStats, mutate: mutateBotStats } = useBotFilterStats(siteId)
|
const { data: botStats, mutate: mutateBotStats } = useBotFilterStats(siteId)
|
||||||
const [filterBots, setFilterBots] = useState(false)
|
const [filterBots, setFilterBots] = useState(false)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [isDirty, setIsDirty] = useState(false)
|
||||||
const initialFilterRef = useRef<boolean | null>(null)
|
const initialFilterRef = useRef<boolean | null>(null)
|
||||||
|
|
||||||
const [botView, setBotView] = useState<'review' | 'blocked'>('review')
|
const [botView, setBotView] = useState<'review' | 'blocked'>('review')
|
||||||
@@ -32,7 +33,9 @@ export default function SiteBotSpamTab({ siteId, onDirtyChange }: { siteId: stri
|
|||||||
// Track dirty state
|
// Track dirty state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialFilterRef.current === null) return
|
if (initialFilterRef.current === null) return
|
||||||
onDirtyChange?.(filterBots !== initialFilterRef.current)
|
const dirty = filterBots !== initialFilterRef.current
|
||||||
|
setIsDirty(dirty)
|
||||||
|
onDirtyChange?.(dirty)
|
||||||
}, [filterBots, onDirtyChange])
|
}, [filterBots, onDirtyChange])
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
@@ -219,11 +222,15 @@ export default function SiteBotSpamTab({ siteId, onDirtyChange }: { siteId: stri
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end pt-2">
|
{/* Sticky save bar */}
|
||||||
<Button onClick={handleSave} variant="primary" disabled={saving}>
|
{isDirty && (
|
||||||
{saving ? 'Saving...' : 'Save Changes'}
|
<div className="sticky bottom-0 -mx-6 -mb-6 px-6 py-3 bg-neutral-900/95 backdrop-blur-sm border-t border-neutral-800 flex items-center justify-between">
|
||||||
</Button>
|
<span className="text-xs text-neutral-400">Unsaved changes</span>
|
||||||
</div>
|
<Button onClick={handleSave} variant="primary" disabled={saving} className="text-sm">
|
||||||
|
{saving ? 'Saving...' : 'Save Changes'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,12 +41,14 @@ export default function SiteGeneralTab({ siteId, onDirtyChange }: { siteId: stri
|
|||||||
|
|
||||||
const canEdit = user?.role === 'owner' || user?.role === 'admin'
|
const canEdit = user?.role === 'owner' || user?.role === 'admin'
|
||||||
const initialRef = useRef('')
|
const initialRef = useRef('')
|
||||||
|
const [isDirty, setIsDirty] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (site) {
|
if (site) {
|
||||||
setName(site.name || '')
|
setName(site.name || '')
|
||||||
setTimezone(site.timezone || 'UTC')
|
setTimezone(site.timezone || 'UTC')
|
||||||
initialRef.current = JSON.stringify({ name: site.name || '', timezone: site.timezone || 'UTC' })
|
initialRef.current = JSON.stringify({ name: site.name || '', timezone: site.timezone || 'UTC' })
|
||||||
|
setIsDirty(false)
|
||||||
}
|
}
|
||||||
}, [site])
|
}, [site])
|
||||||
|
|
||||||
@@ -54,7 +56,9 @@ export default function SiteGeneralTab({ siteId, onDirtyChange }: { siteId: stri
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initialRef.current) return
|
if (!initialRef.current) return
|
||||||
const current = JSON.stringify({ name, timezone })
|
const current = JSON.stringify({ name, timezone })
|
||||||
onDirtyChange?.(current !== initialRef.current)
|
const dirty = current !== initialRef.current
|
||||||
|
setIsDirty(dirty)
|
||||||
|
onDirtyChange?.(dirty)
|
||||||
}, [name, timezone, onDirtyChange])
|
}, [name, timezone, onDirtyChange])
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
@@ -172,12 +176,15 @@ export default function SiteGeneralTab({ siteId, onDirtyChange }: { siteId: stri
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Save */}
|
{/* Sticky save bar */}
|
||||||
<div className="flex justify-end pt-2">
|
{isDirty && (
|
||||||
<Button onClick={handleSave} variant="primary" disabled={saving}>
|
<div className="sticky bottom-0 -mx-6 -mb-6 px-6 py-3 bg-neutral-900/95 backdrop-blur-sm border-t border-neutral-800 flex items-center justify-between">
|
||||||
{saving ? 'Saving...' : 'Save Changes'}
|
<span className="text-xs text-neutral-400">Unsaved changes</span>
|
||||||
</Button>
|
<Button onClick={handleSave} variant="primary" disabled={saving} className="text-sm">
|
||||||
</div>
|
{saving ? 'Saving...' : 'Save Changes'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Danger Zone */}
|
{/* Danger Zone */}
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export default function SitePrivacyTab({ siteId, onDirtyChange }: { siteId: stri
|
|||||||
const [excludedPaths, setExcludedPaths] = useState('')
|
const [excludedPaths, setExcludedPaths] = useState('')
|
||||||
const [snippetCopied, setSnippetCopied] = useState(false)
|
const [snippetCopied, setSnippetCopied] = useState(false)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [isDirty, setIsDirty] = useState(false)
|
||||||
const initialRef = useRef('')
|
const initialRef = useRef('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -52,6 +53,7 @@ export default function SitePrivacyTab({ siteId, onDirtyChange }: { siteId: stri
|
|||||||
dataRetention: site.data_retention_months ?? 6,
|
dataRetention: site.data_retention_months ?? 6,
|
||||||
excludedPaths: (site.excluded_paths || []).join('\n'),
|
excludedPaths: (site.excluded_paths || []).join('\n'),
|
||||||
})
|
})
|
||||||
|
setIsDirty(false)
|
||||||
}
|
}
|
||||||
}, [site])
|
}, [site])
|
||||||
|
|
||||||
@@ -59,7 +61,9 @@ export default function SitePrivacyTab({ siteId, onDirtyChange }: { siteId: stri
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initialRef.current) return
|
if (!initialRef.current) return
|
||||||
const current = JSON.stringify({ collectPagePaths, collectReferrers, collectDeviceInfo, collectScreenRes, collectGeoData, hideUnknownLocations, dataRetention, excludedPaths })
|
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])
|
}, [collectPagePaths, collectReferrers, collectDeviceInfo, collectScreenRes, collectGeoData, hideUnknownLocations, dataRetention, excludedPaths, onDirtyChange])
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
@@ -242,11 +246,15 @@ export default function SitePrivacyTab({ siteId, onDirtyChange }: { siteId: stri
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end pt-2">
|
{/* Sticky save bar — only visible when dirty */}
|
||||||
<Button onClick={handleSave} variant="primary" disabled={saving}>
|
{isDirty && (
|
||||||
{saving ? 'Saving...' : 'Save Changes'}
|
<div className="sticky bottom-0 -mx-6 -mb-6 px-6 py-3 bg-neutral-900/95 backdrop-blur-sm border-t border-neutral-800 flex items-center justify-between">
|
||||||
</Button>
|
<span className="text-xs text-neutral-400">Unsaved changes</span>
|
||||||
</div>
|
<Button onClick={handleSave} variant="primary" disabled={saving} className="text-sm">
|
||||||
|
{saving ? 'Saving...' : 'Save Changes'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export default function SiteVisibilityTab({ siteId, onDirtyChange }: { siteId: s
|
|||||||
const [passwordEnabled, setPasswordEnabled] = useState(false)
|
const [passwordEnabled, setPasswordEnabled] = useState(false)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [linkCopied, setLinkCopied] = useState(false)
|
const [linkCopied, setLinkCopied] = useState(false)
|
||||||
|
const [isDirty, setIsDirty] = useState(false)
|
||||||
const initialRef = useRef('')
|
const initialRef = useRef('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -23,6 +24,7 @@ export default function SiteVisibilityTab({ siteId, onDirtyChange }: { siteId: s
|
|||||||
setIsPublic(site.is_public ?? false)
|
setIsPublic(site.is_public ?? false)
|
||||||
setPasswordEnabled(site.has_password ?? false)
|
setPasswordEnabled(site.has_password ?? false)
|
||||||
initialRef.current = JSON.stringify({ isPublic: site.is_public ?? false, passwordEnabled: site.has_password ?? false })
|
initialRef.current = JSON.stringify({ isPublic: site.is_public ?? false, passwordEnabled: site.has_password ?? false })
|
||||||
|
setIsDirty(false)
|
||||||
}
|
}
|
||||||
}, [site])
|
}, [site])
|
||||||
|
|
||||||
@@ -31,6 +33,7 @@ export default function SiteVisibilityTab({ siteId, onDirtyChange }: { siteId: s
|
|||||||
if (!initialRef.current) return
|
if (!initialRef.current) return
|
||||||
const current = JSON.stringify({ isPublic, passwordEnabled })
|
const current = JSON.stringify({ isPublic, passwordEnabled })
|
||||||
const dirty = current !== initialRef.current || password.length > 0
|
const dirty = current !== initialRef.current || password.length > 0
|
||||||
|
setIsDirty(dirty)
|
||||||
onDirtyChange?.(dirty)
|
onDirtyChange?.(dirty)
|
||||||
}, [isPublic, passwordEnabled, password, onDirtyChange])
|
}, [isPublic, passwordEnabled, password, onDirtyChange])
|
||||||
|
|
||||||
@@ -142,11 +145,15 @@ export default function SiteVisibilityTab({ siteId, onDirtyChange }: { siteId: s
|
|||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
<div className="flex justify-end pt-2">
|
{/* Sticky save bar */}
|
||||||
<Button onClick={handleSave} variant="primary" disabled={saving}>
|
{isDirty && (
|
||||||
{saving ? 'Saving...' : 'Save Changes'}
|
<div className="sticky bottom-0 -mx-6 -mb-6 px-6 py-3 bg-neutral-900/95 backdrop-blur-sm border-t border-neutral-800 flex items-center justify-between">
|
||||||
</Button>
|
<span className="text-xs text-neutral-400">Unsaved changes</span>
|
||||||
</div>
|
<Button onClick={handleSave} variant="primary" disabled={saving} className="text-sm">
|
||||||
|
{saving ? 'Saving...' : 'Save Changes'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user