diff --git a/components/settings/unified/tabs/SiteBotSpamTab.tsx b/components/settings/unified/tabs/SiteBotSpamTab.tsx index cf8a618..464ba9a 100644 --- a/components/settings/unified/tabs/SiteBotSpamTab.tsx +++ b/components/settings/unified/tabs/SiteBotSpamTab.tsx @@ -1,18 +1,16 @@ 'use client' -import { useState, useEffect, useRef } from 'react' -import { Button, Toggle, toast, Spinner, getDateRange } from '@ciphera-net/ui' +import { useState, useEffect, useRef, useCallback } from 'react' +import { 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, onDirtyChange, hasPendingAction, onDiscard }: { siteId: string; onDirtyChange?: (dirty: boolean) => void; hasPendingAction?: boolean; onDiscard?: () => void }) { +export default function SiteBotSpamTab({ siteId, onDirtyChange, onRegisterSave }: { siteId: string; onDirtyChange?: (dirty: boolean) => void; onRegisterSave?: (fn: () => Promise) => 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 [isDirty, setIsDirty] = useState(false) const initialFilterRef = useRef(null) const [botView, setBotView] = useState<'review' | 'blocked'>('review') @@ -35,12 +33,10 @@ export default function SiteBotSpamTab({ siteId, onDirtyChange, hasPendingAction useEffect(() => { if (initialFilterRef.current === null) return const dirty = filterBots !== initialFilterRef.current - setIsDirty(dirty) onDirtyChange?.(dirty) }, [filterBots, onDirtyChange]) - const handleSave = async () => { - setSaving(true) + const handleSave = useCallback(async () => { try { await updateSite(siteId, { name: site?.name || '', filter_bots: filterBots }) await mutate() @@ -49,10 +45,12 @@ export default function SiteBotSpamTab({ siteId, onDirtyChange, hasPendingAction toast.success('Bot filtering updated') } catch { toast.error('Failed to save') - } finally { - setSaving(false) } - } + }, [siteId, site?.name, filterBots, mutate, onDirtyChange]) + + useEffect(() => { + onRegisterSave?.(handleSave) + }, [handleSave, onRegisterSave]) const handleBotFilter = async (sessionIds: string[]) => { try { @@ -223,28 +221,6 @@ export default function SiteBotSpamTab({ siteId, onDirtyChange, hasPendingAction - {/* Sticky save bar */} - {isDirty && ( -
- - {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 2d6faa9..086f227 100644 --- a/components/settings/unified/tabs/SiteGeneralTab.tsx +++ b/components/settings/unified/tabs/SiteGeneralTab.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, useEffect, useRef } from 'react' +import { useState, useEffect, useRef, useCallback } 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,21 +28,19 @@ const TIMEZONES = [ { value: 'Australia/Sydney', label: 'Australia/Sydney (AEST)' }, ] -export default function SiteGeneralTab({ siteId, onDirtyChange, hasPendingAction, onDiscard }: { siteId: string; onDirtyChange?: (dirty: boolean) => void; hasPendingAction?: boolean; onDiscard?: () => void }) { +export default function SiteGeneralTab({ siteId, onDirtyChange, onRegisterSave }: { siteId: string; onDirtyChange?: (dirty: boolean) => void; onRegisterSave?: (fn: () => Promise) => void }) { const router = useRouter() const { user } = useAuth() const { closeUnifiedSettings: closeSettings } = useUnifiedSettings() const { data: site, mutate } = useSite(siteId) const [name, setName] = useState('') const [timezone, setTimezone] = useState('UTC') - const [saving, setSaving] = useState(false) const [showDeleteModal, setShowDeleteModal] = useState(false) const [showVerificationModal, setShowVerificationModal] = useState(false) const canEdit = user?.role === 'owner' || user?.role === 'admin' const initialRef = useRef('') const hasInitialized = useRef(false) - const [isDirty, setIsDirty] = useState(false) useEffect(() => { if (!site || hasInitialized.current) return @@ -50,7 +48,6 @@ export default function SiteGeneralTab({ siteId, onDirtyChange, hasPendingAction setTimezone(site.timezone || 'UTC') initialRef.current = JSON.stringify({ name: site.name || '', timezone: site.timezone || 'UTC' }) hasInitialized.current = true - setIsDirty(false) }, [site]) // Track dirty state @@ -58,13 +55,11 @@ export default function SiteGeneralTab({ siteId, onDirtyChange, hasPendingAction if (!initialRef.current) return const current = JSON.stringify({ name, timezone }) const dirty = current !== initialRef.current - setIsDirty(dirty) onDirtyChange?.(dirty) }, [name, timezone, onDirtyChange]) - const handleSave = async () => { + const handleSave = useCallback(async () => { if (!site) return - setSaving(true) try { await updateSite(siteId, { name, timezone }) await mutate() @@ -73,10 +68,12 @@ export default function SiteGeneralTab({ siteId, onDirtyChange, hasPendingAction toast.success('Site updated') } catch { toast.error('Failed to save') - } finally { - setSaving(false) } - } + }, [site, siteId, name, timezone, mutate, onDirtyChange]) + + useEffect(() => { + onRegisterSave?.(handleSave) + }, [handleSave, onRegisterSave]) const handleResetData = async () => { if (!confirm('Are you sure you want to delete ALL data for this site? This action cannot be undone.')) return @@ -177,29 +174,6 @@ export default function SiteGeneralTab({ siteId, onDirtyChange, hasPendingAction

- {/* Sticky save bar */} - {isDirty && ( -
- - {hasPendingAction ? 'Save or discard to continue' : 'Unsaved changes'} - -
- {hasPendingAction && ( - - )} - -
-
- )} - {/* Danger Zone */} {canEdit && (
diff --git a/components/settings/unified/tabs/SiteVisibilityTab.tsx b/components/settings/unified/tabs/SiteVisibilityTab.tsx index e63c3b7..b88d712 100644 --- a/components/settings/unified/tabs/SiteVisibilityTab.tsx +++ b/components/settings/unified/tabs/SiteVisibilityTab.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, useEffect, useRef } from 'react' +import { useState, useEffect, useRef, useCallback } 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,14 +9,12 @@ import { updateSite } from '@/lib/api/sites' const APP_URL = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3003' -export default function SiteVisibilityTab({ siteId, onDirtyChange, hasPendingAction, onDiscard }: { siteId: string; onDirtyChange?: (dirty: boolean) => void; hasPendingAction?: boolean; onDiscard?: () => void }) { +export default function SiteVisibilityTab({ siteId, onDirtyChange, onRegisterSave }: { siteId: string; onDirtyChange?: (dirty: boolean) => void; onRegisterSave?: (fn: () => Promise) => 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 [isDirty, setIsDirty] = useState(false) const initialRef = useRef('') const hasInitialized = useRef(false) @@ -26,7 +24,6 @@ export default function SiteVisibilityTab({ siteId, onDirtyChange, hasPendingAct setPasswordEnabled(site.has_password ?? false) initialRef.current = JSON.stringify({ isPublic: site.is_public ?? false, passwordEnabled: site.has_password ?? false }) hasInitialized.current = true - setIsDirty(false) }, [site]) // Track dirty state @@ -34,12 +31,10 @@ export default function SiteVisibilityTab({ siteId, onDirtyChange, hasPendingAct 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]) - const handleSave = async () => { - setSaving(true) + const handleSave = useCallback(async () => { try { await updateSite(siteId, { name: site?.name || '', @@ -54,10 +49,12 @@ export default function SiteVisibilityTab({ siteId, onDirtyChange, hasPendingAct toast.success('Visibility updated') } catch { toast.error('Failed to save') - } finally { - setSaving(false) } - } + }, [siteId, site?.name, isPublic, passwordEnabled, password, mutate, onDirtyChange]) + + useEffect(() => { + onRegisterSave?.(handleSave) + }, [handleSave, onRegisterSave]) const copyLink = () => { navigator.clipboard.writeText(`${APP_URL}/share/${siteId}`) @@ -146,28 +143,6 @@ export default function SiteVisibilityTab({ siteId, onDirtyChange, hasPendingAct )} - {/* Sticky save bar */} - {isDirty && ( -
- - {hasPendingAction ? 'Save or discard to continue' : 'Unsaved changes'} - -
- {hasPendingAction && ( - - )} - -
-
- )}
) }