diff --git a/components/settings/unified/tabs/SiteIntegrationsTab.tsx b/components/settings/unified/tabs/SiteIntegrationsTab.tsx index 0165464..ab01cd6 100644 --- a/components/settings/unified/tabs/SiteIntegrationsTab.tsx +++ b/components/settings/unified/tabs/SiteIntegrationsTab.tsx @@ -1,11 +1,13 @@ 'use client' +import { useState } from 'react' import { Button, toast, Spinner } from '@ciphera-net/ui' -import { GoogleLogo, ArrowSquareOut, Plugs, Trash } from '@phosphor-icons/react' +import { GoogleLogo, ArrowSquareOut, Plugs, Trash, ShieldCheck, CaretDown } from '@phosphor-icons/react' import { useGSCStatus, useBunnyStatus } from '@/lib/swr/dashboard' import { disconnectGSC, getGSCAuthURL } from '@/lib/api/gsc' -import { disconnectBunny } from '@/lib/api/bunny' +import { disconnectBunny, getBunnyPullZones, connectBunny, type BunnyPullZone } from '@/lib/api/bunny' import { getAuthErrorMessage } from '@ciphera-net/ui' +import { formatDateTime } from '@/lib/utils/formatDate' function IntegrationCard({ icon, @@ -16,6 +18,7 @@ function IntegrationCard({ onConnect, onDisconnect, connectLabel = 'Connect', + children, }: { icon: React.ReactNode name: string @@ -25,40 +28,211 @@ function IntegrationCard({ onConnect: () => void onDisconnect: () => void connectLabel?: string + children?: React.ReactNode }) { return ( -
-
-
{icon}
-
-
-

{name}

- {connected && ( - - - Connected - - )} +
+
+
+
{icon}
+
+
+

{name}

+ {connected && ( + + + Connected + + )} +
+

{detail || description}

-

{detail || description}

+ {connected ? ( + + ) : ( + + )}
- {connected ? ( - - ) : ( - + {children} +
+ ) +} + +function SecurityNote({ text }: { text: string }) { + return ( +
+ +

{text}

+
+ ) +} + +function StatusDot({ status }: { status?: string }) { + const color = + status === 'active' ? 'bg-green-400' : + status === 'syncing' ? 'bg-yellow-400 animate-pulse' : + status === 'error' ? 'bg-red-400' : + 'bg-neutral-500' + + const label = + status === 'active' ? 'Connected' : + status === 'syncing' ? 'Syncing' : + status === 'error' ? 'Error' : + 'Unknown' + + return ( + + + {label} + + ) +} + +function GSCDetails({ gscStatus }: { gscStatus: { connected: boolean; google_email?: string; gsc_property?: string; status?: string; last_synced_at?: string | null; error_message?: string | null } }) { + if (!gscStatus.connected) return null + + const rows = [ + { label: 'Status', value: }, + { label: 'Google Account', value: gscStatus.google_email || 'Unknown' }, + { label: 'GSC Property', value: gscStatus.gsc_property || 'Unknown' }, + { label: 'Last Synced', value: gscStatus.last_synced_at ? formatDateTime(new Date(gscStatus.last_synced_at)) : 'Never' }, + ] + + return ( +
+
+ {rows.map(row => ( +
+ {row.label} + {row.value} +
+ ))} +
+ {gscStatus.error_message && ( +
+

{gscStatus.error_message}

+
)}
) } +function BunnySetupForm({ siteId, onConnected }: { siteId: string; onConnected: () => void }) { + const [apiKey, setApiKey] = useState('') + const [pullZones, setPullZones] = useState([]) + const [selectedZone, setSelectedZone] = useState(null) + const [loadingZones, setLoadingZones] = useState(false) + const [connecting, setConnecting] = useState(false) + const [zonesLoaded, setZonesLoaded] = useState(false) + + const handleLoadZones = async () => { + if (!apiKey.trim()) { + toast.error('Please enter your BunnyCDN API key') + return + } + setLoadingZones(true) + try { + const data = await getBunnyPullZones(siteId, apiKey.trim()) + setPullZones(data.pull_zones || []) + setSelectedZone(null) + setZonesLoaded(true) + if (!data.pull_zones?.length) { + toast.error('No pull zones found for this API key') + } + } catch (err) { + toast.error(getAuthErrorMessage(err as Error) || 'Failed to load pull zones') + } finally { + setLoadingZones(false) + } + } + + const handleConnect = async () => { + if (!selectedZone) { + toast.error('Please select a pull zone') + return + } + setConnecting(true) + try { + await connectBunny(siteId, apiKey.trim(), selectedZone.id, selectedZone.name) + toast.success('BunnyCDN connected successfully') + onConnected() + } catch (err) { + toast.error(getAuthErrorMessage(err as Error) || 'Failed to connect BunnyCDN') + } finally { + setConnecting(false) + } + } + + return ( +
+
+
+ +
+ setApiKey(e.target.value)} + placeholder="Enter your BunnyCDN API key" + className="flex-1 px-3 py-2 text-sm bg-neutral-900 border border-neutral-700 rounded-lg text-white placeholder:text-neutral-500 focus:outline-none focus:border-neutral-500" + /> + +
+
+ + {zonesLoaded && pullZones.length > 0 && ( +
+ +
+ + +
+
+ )} + + {zonesLoaded && pullZones.length > 0 && ( + + )} +
+
+ ) +} + export default function SiteIntegrationsTab({ siteId }: { siteId: string }) { const { data: gscStatus, mutate: mutateGSC } = useGSCStatus(siteId) const { data: bunnyStatus, mutate: mutateBunny } = useBunnyStatus(siteId) + const [showBunnySetup, setShowBunnySetup] = useState(false) const handleConnectGSC = async () => { try { @@ -81,8 +255,7 @@ export default function SiteIntegrationsTab({ siteId }: { siteId: string }) { } const handleConnectBunny = () => { - // Redirect to full settings page for BunnyCDN setup (requires API key input) - window.location.href = `/sites/${siteId}/settings?tab=integrations` + setShowBunnySetup(true) } const handleDisconnectBunny = async () => { @@ -90,12 +263,15 @@ export default function SiteIntegrationsTab({ siteId }: { siteId: string }) { try { await disconnectBunny(siteId) await mutateBunny() + setShowBunnySetup(false) toast.success('BunnyCDN disconnected') } catch (err) { toast.error(getAuthErrorMessage(err as Error) || 'Failed to disconnect') } } + const bunnyConnected = bunnyStatus?.connected ?? false + return (
@@ -113,17 +289,31 @@ export default function SiteIntegrationsTab({ siteId }: { siteId: string }) { onConnect={handleConnectGSC} onDisconnect={handleDisconnectGSC} connectLabel="Connect with Google" - /> + > + {gscStatus?.connected && } + + { (e.target as HTMLImageElement).style.display = 'none' }} />} name="BunnyCDN" description="Monitor bandwidth, cache hit rates, and CDN performance." - connected={bunnyStatus?.connected ?? false} - detail={bunnyStatus?.connected ? `Pull zone: ${bunnyStatus.pull_zone_name || 'connected'}` : undefined} + connected={bunnyConnected} + detail={bunnyConnected ? `Pull zone: ${bunnyStatus?.pull_zone_name || 'connected'}` : undefined} onConnect={handleConnectBunny} onDisconnect={handleDisconnectBunny} - /> + > + {!bunnyConnected && showBunnySetup && ( + { + mutateBunny() + setShowBunnySetup(false) + }} + /> + )} + +
)