feat(settings): unsaved changes guard with inline confirmation bar
This commit is contained in:
@@ -1,17 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { Button, Toggle, toast, Spinner, getDateRange } from '@ciphera-net/ui'
|
||||
import { ShieldCheck } from '@phosphor-icons/react'
|
||||
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 }: { siteId: string }) {
|
||||
export default function SiteBotSpamTab({ siteId, onDirtyChange }: { siteId: string; onDirtyChange?: (dirty: boolean) => void }) {
|
||||
const { data: site, mutate } = useSite(siteId)
|
||||
const { data: botStats, mutate: mutateBotStats } = useBotFilterStats(siteId)
|
||||
const [filterBots, setFilterBots] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const initialFilterRef = useRef<boolean | null>(null)
|
||||
|
||||
const [botView, setBotView] = useState<'review' | 'blocked'>('review')
|
||||
const [suspiciousOnly, setSuspiciousOnly] = useState(true)
|
||||
@@ -22,14 +23,25 @@ export default function SiteBotSpamTab({ siteId }: { siteId: string }) {
|
||||
const sessions = sessionsData?.sessions
|
||||
|
||||
useEffect(() => {
|
||||
if (site) setFilterBots(site.filter_bots ?? false)
|
||||
if (site) {
|
||||
setFilterBots(site.filter_bots ?? false)
|
||||
initialFilterRef.current = site.filter_bots ?? false
|
||||
}
|
||||
}, [site])
|
||||
|
||||
// Track dirty state
|
||||
useEffect(() => {
|
||||
if (initialFilterRef.current === null) return
|
||||
onDirtyChange?.(filterBots !== initialFilterRef.current)
|
||||
}, [filterBots, onDirtyChange])
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
try {
|
||||
await updateSite(siteId, { name: site?.name || '', filter_bots: filterBots })
|
||||
await mutate()
|
||||
initialFilterRef.current = filterBots
|
||||
onDirtyChange?.(false)
|
||||
toast.success('Bot filtering updated')
|
||||
} catch {
|
||||
toast.error('Failed to save')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Input, Button, Select, toast, Spinner, getAuthErrorMessage, CheckIcon, ZapIcon } from '@ciphera-net/ui'
|
||||
import { useSite } from '@/lib/swr/dashboard'
|
||||
@@ -28,7 +28,7 @@ const TIMEZONES = [
|
||||
{ value: 'Australia/Sydney', label: 'Australia/Sydney (AEST)' },
|
||||
]
|
||||
|
||||
export default function SiteGeneralTab({ siteId }: { siteId: string }) {
|
||||
export default function SiteGeneralTab({ siteId, onDirtyChange }: { siteId: string; onDirtyChange?: (dirty: boolean) => void }) {
|
||||
const router = useRouter()
|
||||
const { user } = useAuth()
|
||||
const { closeUnifiedSettings: closeSettings } = useUnifiedSettings()
|
||||
@@ -40,20 +40,31 @@ export default function SiteGeneralTab({ siteId }: { siteId: string }) {
|
||||
const [showVerificationModal, setShowVerificationModal] = useState(false)
|
||||
|
||||
const canEdit = user?.role === 'owner' || user?.role === 'admin'
|
||||
const initialRef = useRef('')
|
||||
|
||||
useEffect(() => {
|
||||
if (site) {
|
||||
setName(site.name || '')
|
||||
setTimezone(site.timezone || 'UTC')
|
||||
initialRef.current = JSON.stringify({ name: site.name || '', timezone: site.timezone || 'UTC' })
|
||||
}
|
||||
}, [site])
|
||||
|
||||
// Track dirty state
|
||||
useEffect(() => {
|
||||
if (!initialRef.current) return
|
||||
const current = JSON.stringify({ name, timezone })
|
||||
onDirtyChange?.(current !== initialRef.current)
|
||||
}, [name, timezone, onDirtyChange])
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!site) return
|
||||
setSaving(true)
|
||||
try {
|
||||
await updateSite(siteId, { name, timezone })
|
||||
await mutate()
|
||||
initialRef.current = JSON.stringify({ name, timezone })
|
||||
onDirtyChange?.(false)
|
||||
toast.success('Site updated')
|
||||
} catch {
|
||||
toast.error('Failed to save')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { Button, Select, Toggle, toast, Spinner } from '@ciphera-net/ui'
|
||||
import { useSite, useSubscription, usePageSpeedConfig } from '@/lib/swr/dashboard'
|
||||
import { updateSite } from '@/lib/api/sites'
|
||||
@@ -16,7 +16,7 @@ const GEO_OPTIONS = [
|
||||
{ value: 'none', label: 'Disabled' },
|
||||
]
|
||||
|
||||
export default function SitePrivacyTab({ siteId }: { siteId: string }) {
|
||||
export default function SitePrivacyTab({ siteId, onDirtyChange }: { siteId: string; onDirtyChange?: (dirty: boolean) => void }) {
|
||||
const { data: site, mutate } = useSite(siteId)
|
||||
const { data: subscription, error: subscriptionError, mutate: mutateSubscription } = useSubscription()
|
||||
const { data: psiConfig, mutate: mutatePSIConfig } = usePageSpeedConfig(siteId)
|
||||
@@ -30,6 +30,7 @@ export default function SitePrivacyTab({ siteId }: { siteId: string }) {
|
||||
const [excludedPaths, setExcludedPaths] = useState('')
|
||||
const [snippetCopied, setSnippetCopied] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const initialRef = useRef('')
|
||||
|
||||
useEffect(() => {
|
||||
if (site) {
|
||||
@@ -41,9 +42,26 @@ export default function SitePrivacyTab({ siteId }: { siteId: string }) {
|
||||
setHideUnknownLocations(site.hide_unknown_locations ?? false)
|
||||
setDataRetention(site.data_retention_months ?? 6)
|
||||
setExcludedPaths((site.excluded_paths || []).join('\n'))
|
||||
initialRef.current = JSON.stringify({
|
||||
collectPagePaths: site.collect_page_paths ?? true,
|
||||
collectReferrers: site.collect_referrers ?? true,
|
||||
collectDeviceInfo: site.collect_device_info ?? true,
|
||||
collectScreenRes: site.collect_screen_resolution ?? true,
|
||||
collectGeoData: site.collect_geo_data ?? 'full',
|
||||
hideUnknownLocations: site.hide_unknown_locations ?? false,
|
||||
dataRetention: site.data_retention_months ?? 6,
|
||||
excludedPaths: (site.excluded_paths || []).join('\n'),
|
||||
})
|
||||
}
|
||||
}, [site])
|
||||
|
||||
// Track dirty state
|
||||
useEffect(() => {
|
||||
if (!initialRef.current) return
|
||||
const current = JSON.stringify({ collectPagePaths, collectReferrers, collectDeviceInfo, collectScreenRes, collectGeoData, hideUnknownLocations, dataRetention, excludedPaths })
|
||||
onDirtyChange?.(current !== initialRef.current)
|
||||
}, [collectPagePaths, collectReferrers, collectDeviceInfo, collectScreenRes, collectGeoData, hideUnknownLocations, dataRetention, excludedPaths, onDirtyChange])
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
try {
|
||||
@@ -59,6 +77,8 @@ export default function SitePrivacyTab({ siteId }: { siteId: string }) {
|
||||
excluded_paths: excludedPaths.split('\n').map(p => p.trim()).filter(Boolean),
|
||||
})
|
||||
await mutate()
|
||||
initialRef.current = JSON.stringify({ collectPagePaths, collectReferrers, collectDeviceInfo, collectScreenRes, collectGeoData, hideUnknownLocations, dataRetention, excludedPaths })
|
||||
onDirtyChange?.(false)
|
||||
toast.success('Privacy settings updated')
|
||||
} catch {
|
||||
toast.error('Failed to save')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { Button, Input, Toggle, toast, Spinner } from '@ciphera-net/ui'
|
||||
import { Copy, CheckCircle, Lock } from '@phosphor-icons/react'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
@@ -9,21 +9,31 @@ import { updateSite } from '@/lib/api/sites'
|
||||
|
||||
const APP_URL = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3003'
|
||||
|
||||
export default function SiteVisibilityTab({ siteId }: { siteId: string }) {
|
||||
export default function SiteVisibilityTab({ siteId, onDirtyChange }: { siteId: string; onDirtyChange?: (dirty: boolean) => void }) {
|
||||
const { data: site, mutate } = useSite(siteId)
|
||||
const [isPublic, setIsPublic] = useState(false)
|
||||
const [password, setPassword] = useState('')
|
||||
const [passwordEnabled, setPasswordEnabled] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [linkCopied, setLinkCopied] = useState(false)
|
||||
const initialRef = useRef('')
|
||||
|
||||
useEffect(() => {
|
||||
if (site) {
|
||||
setIsPublic(site.is_public ?? false)
|
||||
setPasswordEnabled(site.has_password ?? false)
|
||||
initialRef.current = JSON.stringify({ isPublic: site.is_public ?? false, passwordEnabled: site.has_password ?? false })
|
||||
}
|
||||
}, [site])
|
||||
|
||||
// Track dirty state
|
||||
useEffect(() => {
|
||||
if (!initialRef.current) return
|
||||
const current = JSON.stringify({ isPublic, passwordEnabled })
|
||||
const dirty = current !== initialRef.current || password.length > 0
|
||||
onDirtyChange?.(dirty)
|
||||
}, [isPublic, passwordEnabled, password, onDirtyChange])
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
try {
|
||||
@@ -35,6 +45,8 @@ export default function SiteVisibilityTab({ siteId }: { siteId: string }) {
|
||||
})
|
||||
setPassword('')
|
||||
await mutate()
|
||||
initialRef.current = JSON.stringify({ isPublic, passwordEnabled })
|
||||
onDirtyChange?.(false)
|
||||
toast.success('Visibility updated')
|
||||
} catch {
|
||||
toast.error('Failed to save')
|
||||
|
||||
Reference in New Issue
Block a user