'use client' import { useEffect, useState } from 'react' import { useParams, useRouter } from 'next/navigation' import { getSite, updateSite, resetSiteData, deleteSite, type Site, type GeoDataLevel } from '@/lib/api/sites' import { listGoals, createGoal, updateGoal, deleteGoal, type Goal } from '@/lib/api/goals' import { toast } from '@ciphera-net/ui' import { getAuthErrorMessage } from '@/lib/utils/authErrors' import { LoadingOverlay } from '@ciphera-net/ui' import VerificationModal from '@/components/sites/VerificationModal' import { PasswordInput } from '@ciphera-net/ui' import { Select, Modal, Button } from '@ciphera-net/ui' import { APP_URL, API_URL } from '@/lib/api/client' import { generatePrivacySnippet } from '@/lib/utils/privacySnippet' import { motion, AnimatePresence } from 'framer-motion' import { useAuth } from '@/lib/auth/context' import { SettingsIcon, GlobeIcon, CheckIcon, AlertTriangleIcon, ZapIcon, } from '@ciphera-net/ui' 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 { user } = useAuth() const canEdit = user?.role === 'owner' || user?.role === 'admin' 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' | 'goals'>('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 }) 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) const [goals, setGoals] = useState([]) const [goalsLoading, setGoalsLoading] = useState(false) const [goalModalOpen, setGoalModalOpen] = useState(false) const [editingGoal, setEditingGoal] = useState(null) const [goalForm, setGoalForm] = useState({ name: '', event_name: '' }) const [goalSaving, setGoalSaving] = useState(false) useEffect(() => { loadSite() }, [siteId]) useEffect(() => { if (activeTab === 'goals' && siteId) { loadGoals() } }, [activeTab, 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 }) if (data.has_password) { setIsPasswordEnabled(true) } else { setIsPasswordEnabled(false) } } catch (error: any) { toast.error(getAuthErrorMessage(error) || 'Failed to load site: ' + ((error as Error)?.message || 'Unknown error')) } finally { setLoading(false) } } const loadGoals = async () => { try { setGoalsLoading(true) const data = await listGoals(siteId) setGoals(data ?? []) } catch (e) { toast.error(getAuthErrorMessage(e as Error) || 'Failed to load goals') } finally { setGoalsLoading(false) } } const openAddGoal = () => { setEditingGoal(null) setGoalForm({ name: '', event_name: '' }) setGoalModalOpen(true) } const openEditGoal = (goal: Goal) => { setEditingGoal(goal) setGoalForm({ name: goal.name, event_name: goal.event_name }) setGoalModalOpen(true) } const handleGoalSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!goalForm.name.trim() || !goalForm.event_name.trim()) { toast.error('Name and event name are required') return } const eventName = goalForm.event_name.trim().toLowerCase().replace(/\s+/g, '_') if (!/^[a-zA-Z0-9_]+$/.test(eventName)) { toast.error('Event name can only contain letters, numbers, and underscores') return } setGoalSaving(true) try { if (editingGoal) { await updateGoal(siteId, editingGoal.id, { name: goalForm.name.trim(), event_name: eventName }) toast.success('Goal updated') } else { await createGoal(siteId, { name: goalForm.name.trim(), event_name: eventName }) toast.success('Goal created') } setGoalModalOpen(false) loadGoals() } catch (err) { toast.error(getAuthErrorMessage(err as Error) || 'Failed to save goal') } finally { setGoalSaving(false) } } const handleDeleteGoal = async (goal: Goal) => { if (!confirm(`Delete goal "${goal.name}"?`)) return try { await deleteGoal(siteId, goal.id) toast.success('Goal deleted') loadGoals() } catch (err) { toast.error(getAuthErrorMessage(err as Error) || 'Failed to delete goal') } } 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 }) toast.success('Site updated successfully') loadSite() } catch (error: any) { toast.error(getAuthErrorMessage(error) || 'Failed to update site: ' + ((error as 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(getAuthErrorMessage(error) || 'Failed to reset data: ' + ((error as 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(getAuthErrorMessage(error) || 'Failed to delete site: ' + ((error as 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 */}
{!canEdit && (

You have read-only access to this site. Contact an admin to make changes.

)} {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.

{canEdit && ( )}
{canEdit && (

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: e.target.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.

)}
)}
{canEdit && ( )}
)} {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