'use client' import { useEffect, useState } from 'react' import { useParams, useRouter } from 'next/navigation' import { getSite, updateSite, resetSiteData, deleteSite, type Site, type GeoDataLevel, type ReplayMode } from '@/lib/api/sites' import { getRealtime } from '@/lib/api/stats' import { toast } from 'sonner' import LoadingOverlay from '@/components/LoadingOverlay' import VerificationModal from '@/components/sites/VerificationModal' import PasswordInput from '@/components/PasswordInput' import Select from '@/components/ui/Select' import { APP_URL, API_URL } from '@/lib/api/client' import { generatePrivacySnippet } from '@/lib/utils/privacySnippet' import { motion, AnimatePresence } from 'framer-motion' import { GearIcon, GlobeIcon, FileTextIcon, CheckIcon, CopyIcon, ExclamationTriangleIcon, LightningBoltIcon, VideoIcon, LockClosedIcon, } from '@radix-ui/react-icons' /** Sampling rate options: 25, 50, 75, 100. */ const SAMPLING_RATE_OPTIONS = [25, 50, 75, 100] as const function snapSamplingRate(v: number): number { return (SAMPLING_RATE_OPTIONS as readonly number[]).reduce( (best, x) => (Math.abs(x - v) < Math.abs(best - v) ? x : best) ) } const TIMEZONES = [ 'UTC', 'America/New_York', 'America/Los_Angeles', 'America/Chicago', 'America/Toronto', 'Europe/London', 'Europe/Paris', 'Europe/Berlin', 'Europe/Amsterdam', 'Asia/Tokyo', 'Asia/Singapore', 'Asia/Dubai', 'Australia/Sydney', 'Pacific/Auckland', ] export default function SiteSettingsPage() { const params = useParams() const router = useRouter() const siteId = params.id as string const [site, setSite] = useState(null) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [activeTab, setActiveTab] = useState<'general' | 'visibility' | 'data' | 'replay'>('general') const [formData, setFormData] = useState({ name: '', timezone: 'UTC', is_public: false, password: '', excluded_paths: '', // Data collection settings collect_page_paths: true, collect_referrers: true, collect_device_info: true, collect_geo_data: 'full' as GeoDataLevel, collect_screen_resolution: true, // Performance insights setting enable_performance_insights: false, // Bot and noise filtering filter_bots: true, // Session replay settings replay_mode: 'disabled' as ReplayMode, replay_sampling_rate: 100, replay_retention_days: 30, replay_mask_all_text: false, replay_mask_all_inputs: true }) const [scriptCopied, setScriptCopied] = useState(false) const [linkCopied, setLinkCopied] = useState(false) const [snippetCopied, setSnippetCopied] = useState(false) const [showVerificationModal, setShowVerificationModal] = useState(false) const [isPasswordEnabled, setIsPasswordEnabled] = useState(false) useEffect(() => { loadSite() }, [siteId]) const loadSite = async () => { try { setLoading(true) const data = await getSite(siteId) setSite(data) setFormData({ name: data.name, timezone: data.timezone || 'UTC', is_public: data.is_public || false, password: '', // Don't show existing password excluded_paths: (data.excluded_paths || []).join('\n'), // Data collection settings (default to true/full for backwards compatibility) collect_page_paths: data.collect_page_paths ?? true, collect_referrers: data.collect_referrers ?? true, collect_device_info: data.collect_device_info ?? true, collect_geo_data: data.collect_geo_data || 'full', collect_screen_resolution: data.collect_screen_resolution ?? true, // Performance insights setting (default to false) enable_performance_insights: data.enable_performance_insights ?? false, // Bot and noise filtering (default to true) filter_bots: data.filter_bots ?? true, // Session replay settings (legacy consent_required from API mapped to anonymous_skeleton) replay_mode: ((data as { replay_mode?: string }).replay_mode === 'consent_required' ? 'anonymous_skeleton' : data.replay_mode) || 'disabled', replay_sampling_rate: snapSamplingRate(data.replay_sampling_rate ?? 100), replay_retention_days: data.replay_retention_days ?? 30, replay_mask_all_text: data.replay_mask_all_text ?? false, replay_mask_all_inputs: data.replay_mask_all_inputs ?? true }) if (data.has_password) { setIsPasswordEnabled(true) } else { setIsPasswordEnabled(false) } } catch (error: any) { toast.error('Failed to load site: ' + (error.message || 'Unknown error')) } finally { setLoading(false) } } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setSaving(true) try { const excludedPathsArray = formData.excluded_paths .split('\n') .map(p => p.trim()) .filter(p => p.length > 0) await updateSite(siteId, { name: formData.name, timezone: formData.timezone, is_public: formData.is_public, password: isPasswordEnabled ? (formData.password || undefined) : undefined, clear_password: !isPasswordEnabled, excluded_paths: excludedPathsArray, // Data collection settings collect_page_paths: formData.collect_page_paths, collect_referrers: formData.collect_referrers, collect_device_info: formData.collect_device_info, collect_geo_data: formData.collect_geo_data, collect_screen_resolution: formData.collect_screen_resolution, // Performance insights setting enable_performance_insights: formData.enable_performance_insights, // Bot and noise filtering filter_bots: formData.filter_bots, // Session replay settings replay_mode: formData.replay_mode, replay_sampling_rate: formData.replay_sampling_rate, replay_retention_days: formData.replay_retention_days, replay_mask_all_text: formData.replay_mask_all_text, replay_mask_all_inputs: formData.replay_mask_all_inputs }) toast.success('Site updated successfully') loadSite() } catch (error: any) { toast.error('Failed to update site: ' + (error.message || 'Unknown error')) } finally { setSaving(false) } } const handleResetData = async () => { if (!confirm('Are you sure you want to delete ALL data for this site? This action cannot be undone.')) { return } try { await resetSiteData(siteId) toast.success('All site data has been reset') } catch (error: any) { toast.error('Failed to reset data: ' + (error.message || 'Unknown error')) } } const handleDeleteSite = async () => { const confirmation = prompt('To confirm deletion, please type the site domain:') if (confirmation !== site?.domain) { if (confirmation) toast.error('Domain does not match') return } try { await deleteSite(siteId) toast.success('Site deleted successfully') router.push('/') } catch (error: any) { toast.error('Failed to delete site: ' + (error.message || 'Unknown error')) } } const copyScript = () => { const script = `` navigator.clipboard.writeText(script) setScriptCopied(true) toast.success('Script copied to clipboard') setTimeout(() => setScriptCopied(false), 2000) } const copyLink = () => { const link = `${APP_URL}/share/${siteId}` navigator.clipboard.writeText(link) setLinkCopied(true) toast.success('Link copied to clipboard') setTimeout(() => setLinkCopied(false), 2000) } const copySnippet = () => { if (!site) return navigator.clipboard.writeText(generatePrivacySnippet(site)) setSnippetCopied(true) toast.success('Privacy snippet copied to clipboard') setTimeout(() => setSnippetCopied(false), 2000) } if (loading) { return } if (!site) { return (

Site not found

) } return (

Site Settings

Manage settings for {site.domain}

{/* Sidebar Navigation */} {/* Content Area */}
{activeTab === 'general' && (

General Configuration

Update your site details and tracking script.

setFormData({ ...formData, name: e.target.value })} className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-xl bg-neutral-50/50 dark:bg-neutral-900/50 focus:bg-white dark:focus:bg-neutral-900 focus:border-brand-orange focus:ring-4 focus:ring-brand-orange/10 outline-none transition-all duration-200 dark:text-white" />

Domain cannot be changed after creation

Tracking Script

Add this script to your website to start tracking visitors.

{``}

Check if your site is sending data correctly.

Danger Zone

Irreversible actions for your site.

Reset Data

Delete all stats and events. This cannot be undone.

Delete Site

Permanently delete this site and all data.

)} {activeTab === 'visibility' && (

Visibility Settings

Manage who can view your dashboard.

Public Dashboard

Allow anyone with the link to view this dashboard

{formData.is_public && (

Share this link with others to view the dashboard.

Password Protection

Restrict access to this dashboard.

{isPasswordEnabled && ( setFormData({ ...formData, password: value })} placeholder={site.has_password ? "Change password (leave empty to keep current)" : "Set a password"} />

Visitors will need to enter this password to view the dashboard.

)}
)}
)} {activeTab === 'data' && (

Data & Privacy

Control what visitor data is collected. Less data = more privacy.

{/* Data Collection Controls */}

Data Collection

{/* Page Paths Toggle */}

Page Paths

Track which pages visitors view

{/* Referrers Toggle */}

Referrers

Track where visitors come from

{/* Device Info Toggle */}

Device Info

Track browser, OS, and device type

{/* Geographic Data Dropdown */}

Geographic Data

Control location tracking granularity

setFormData({ ...formData, collect_screen_resolution: e.target.checked })} className="sr-only peer" />
{/* Bot and noise filtering */}

Filtering

Filter bots and referrer spam

Exclude known crawlers, scrapers, and referrer spam domains from your stats

{/* Performance Insights Toggle */}

Performance Insights

Performance Insights (Add-on)

Track Core Web Vitals (LCP, CLS, INP) to monitor site performance

{/* Excluded Paths */}

Path Filtering