feat: Add data collection settings to Site interface and update SiteSettingsPage
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useParams, useRouter } from 'next/navigation'
|
import { useParams, useRouter } from 'next/navigation'
|
||||||
import { getSite, updateSite, resetSiteData, deleteSite, type Site } from '@/lib/api/sites'
|
import { getSite, updateSite, resetSiteData, deleteSite, type Site, type GeoDataLevel } from '@/lib/api/sites'
|
||||||
import { getRealtime } from '@/lib/api/stats'
|
import { getRealtime } from '@/lib/api/stats'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import LoadingOverlay from '@/components/LoadingOverlay'
|
import LoadingOverlay from '@/components/LoadingOverlay'
|
||||||
@@ -52,7 +52,13 @@ export default function SiteSettingsPage() {
|
|||||||
timezone: 'UTC',
|
timezone: 'UTC',
|
||||||
is_public: false,
|
is_public: false,
|
||||||
password: '',
|
password: '',
|
||||||
excluded_paths: ''
|
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
|
||||||
})
|
})
|
||||||
const [scriptCopied, setScriptCopied] = useState(false)
|
const [scriptCopied, setScriptCopied] = useState(false)
|
||||||
const [linkCopied, setLinkCopied] = useState(false)
|
const [linkCopied, setLinkCopied] = useState(false)
|
||||||
@@ -73,7 +79,13 @@ export default function SiteSettingsPage() {
|
|||||||
timezone: data.timezone || 'UTC',
|
timezone: data.timezone || 'UTC',
|
||||||
is_public: data.is_public || false,
|
is_public: data.is_public || false,
|
||||||
password: '', // Don't show existing password
|
password: '', // Don't show existing password
|
||||||
excluded_paths: (data.excluded_paths || []).join('\n')
|
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
|
||||||
})
|
})
|
||||||
if (data.has_password) {
|
if (data.has_password) {
|
||||||
setIsPasswordEnabled(true)
|
setIsPasswordEnabled(true)
|
||||||
@@ -103,7 +115,13 @@ export default function SiteSettingsPage() {
|
|||||||
is_public: formData.is_public,
|
is_public: formData.is_public,
|
||||||
password: isPasswordEnabled ? (formData.password || undefined) : undefined,
|
password: isPasswordEnabled ? (formData.password || undefined) : undefined,
|
||||||
clear_password: !isPasswordEnabled,
|
clear_password: !isPasswordEnabled,
|
||||||
excluded_paths: excludedPathsArray
|
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
|
||||||
})
|
})
|
||||||
toast.success('Site updated successfully')
|
toast.success('Site updated successfully')
|
||||||
loadSite()
|
loadSite()
|
||||||
@@ -521,10 +539,129 @@ export default function SiteSettingsPage() {
|
|||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-semibold text-neutral-900 dark:text-white mb-1">Data & Privacy</h2>
|
<h2 className="text-xl font-semibold text-neutral-900 dark:text-white mb-1">Data & Privacy</h2>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">Manage data collection and filtering.</p>
|
<p className="text-sm text-neutral-500 dark:text-neutral-400">Control what visitor data is collected. Less data = more privacy.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Data Collection Controls */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm font-medium text-neutral-700 dark:text-neutral-300">Data Collection</h3>
|
||||||
|
|
||||||
|
{/* Page Paths Toggle */}
|
||||||
|
<div className="p-4 bg-neutral-50 dark:bg-neutral-900/50 rounded-xl border border-neutral-100 dark:border-neutral-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-neutral-900 dark:text-white">Page Paths</h4>
|
||||||
|
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
|
||||||
|
Track which pages visitors view
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<label className="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.collect_page_paths}
|
||||||
|
onChange={(e) => setFormData({ ...formData, collect_page_paths: e.target.checked })}
|
||||||
|
className="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div className="w-11 h-6 bg-neutral-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-orange/20 dark:peer-focus:ring-brand-orange/20 rounded-full peer dark:bg-neutral-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-brand-orange"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Referrers Toggle */}
|
||||||
|
<div className="p-4 bg-neutral-50 dark:bg-neutral-900/50 rounded-xl border border-neutral-100 dark:border-neutral-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-neutral-900 dark:text-white">Referrers</h4>
|
||||||
|
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
|
||||||
|
Track where visitors come from
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<label className="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.collect_referrers}
|
||||||
|
onChange={(e) => setFormData({ ...formData, collect_referrers: e.target.checked })}
|
||||||
|
className="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div className="w-11 h-6 bg-neutral-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-orange/20 dark:peer-focus:ring-brand-orange/20 rounded-full peer dark:bg-neutral-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-brand-orange"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Device Info Toggle */}
|
||||||
|
<div className="p-4 bg-neutral-50 dark:bg-neutral-900/50 rounded-xl border border-neutral-100 dark:border-neutral-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-neutral-900 dark:text-white">Device Info</h4>
|
||||||
|
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
|
||||||
|
Track browser, OS, and device type
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<label className="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.collect_device_info}
|
||||||
|
onChange={(e) => setFormData({ ...formData, collect_device_info: e.target.checked })}
|
||||||
|
className="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div className="w-11 h-6 bg-neutral-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-orange/20 dark:peer-focus:ring-brand-orange/20 rounded-full peer dark:bg-neutral-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-brand-orange"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Geographic Data Dropdown */}
|
||||||
|
<div className="p-4 bg-neutral-50 dark:bg-neutral-900/50 rounded-xl border border-neutral-100 dark:border-neutral-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-neutral-900 dark:text-white">Geographic Data</h4>
|
||||||
|
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
|
||||||
|
Control location tracking granularity
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<select
|
||||||
|
value={formData.collect_geo_data}
|
||||||
|
onChange={(e) => setFormData({ ...formData, collect_geo_data: e.target.value as GeoDataLevel })}
|
||||||
|
className="px-4 py-2 border border-neutral-200 dark:border-neutral-700 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white text-sm font-medium appearance-none pr-10 focus:outline-none focus:ring-2 focus:ring-brand-orange/20 focus:border-brand-orange"
|
||||||
|
>
|
||||||
|
<option value="full">Full (country, region, city)</option>
|
||||||
|
<option value="country">Country only</option>
|
||||||
|
<option value="none">None</option>
|
||||||
|
</select>
|
||||||
|
<div className="absolute inset-y-0 right-0 flex items-center px-3 pointer-events-none">
|
||||||
|
<svg className="w-4 h-4 text-neutral-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Screen Resolution Toggle */}
|
||||||
|
<div className="p-4 bg-neutral-50 dark:bg-neutral-900/50 rounded-xl border border-neutral-100 dark:border-neutral-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-neutral-900 dark:text-white">Screen Resolution</h4>
|
||||||
|
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
|
||||||
|
Track visitor screen sizes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<label className="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.collect_screen_resolution}
|
||||||
|
onChange={(e) => setFormData({ ...formData, collect_screen_resolution: e.target.checked })}
|
||||||
|
className="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div className="w-11 h-6 bg-neutral-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-orange/20 dark:peer-focus:ring-brand-orange/20 rounded-full peer dark:bg-neutral-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-brand-orange"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Excluded Paths */}
|
||||||
|
<div className="space-y-4 pt-6 border-t border-neutral-100 dark:border-neutral-800">
|
||||||
|
<h3 className="text-sm font-medium text-neutral-700 dark:text-neutral-300">Path Filtering</h3>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label htmlFor="excludedPaths" className="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
<label htmlFor="excludedPaths" className="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||||
Excluded Paths
|
Excluded Paths
|
||||||
@@ -532,16 +669,16 @@ export default function SiteSettingsPage() {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<textarea
|
<textarea
|
||||||
id="excludedPaths"
|
id="excludedPaths"
|
||||||
rows={6}
|
rows={4}
|
||||||
value={formData.excluded_paths}
|
value={formData.excluded_paths}
|
||||||
onChange={(e) => setFormData({ ...formData, excluded_paths: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, excluded_paths: e.target.value })}
|
||||||
placeholder="/admin/* /staging/*"
|
placeholder="/admin/* /staging/*"
|
||||||
className="w-full px-4 py-3 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
|
className="w-full px-4 py-3 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 font-mono text-sm"
|
focus:border-brand-orange focus:ring-4 focus:ring-brand-orange/10 outline-none transition-all duration-200 dark:text-white font-mono text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-2">
|
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-2">
|
||||||
Enter paths to exclude from tracking (one per line). Supports simple matching (e.g., /admin/*).
|
Enter paths to exclude from tracking (one per line). Supports wildcards (e.g., /admin/*).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -550,7 +687,7 @@ export default function SiteSettingsPage() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
className="flex items-center gap-2 px-6 py-2.5 bg-neutral-900 dark:bg-white text-white dark:text-neutral-900 rounded-xl font-medium
|
className="flex items-center gap-2 px-6 py-2.5 bg-neutral-900 dark:bg-white text-white dark:text-neutral-900 rounded-xl font-medium
|
||||||
hover:bg-neutral-800 dark:hover:bg-neutral-100 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
|
hover:bg-neutral-800 dark:hover:bg-neutral-100 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
|
||||||
>
|
>
|
||||||
{saving ? (
|
{saving ? (
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import apiRequest from './client'
|
import apiRequest from './client'
|
||||||
|
|
||||||
|
export type GeoDataLevel = 'full' | 'country' | 'none'
|
||||||
|
|
||||||
export interface Site {
|
export interface Site {
|
||||||
id: string
|
id: string
|
||||||
user_id: string
|
user_id: string
|
||||||
@@ -9,6 +11,12 @@ export interface Site {
|
|||||||
is_public?: boolean
|
is_public?: boolean
|
||||||
has_password?: boolean
|
has_password?: boolean
|
||||||
excluded_paths?: string[]
|
excluded_paths?: string[]
|
||||||
|
// Data collection settings (privacy controls)
|
||||||
|
collect_page_paths?: boolean
|
||||||
|
collect_referrers?: boolean
|
||||||
|
collect_device_info?: boolean
|
||||||
|
collect_geo_data?: GeoDataLevel
|
||||||
|
collect_screen_resolution?: boolean
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
@@ -25,6 +33,12 @@ export interface UpdateSiteRequest {
|
|||||||
password?: string
|
password?: string
|
||||||
clear_password?: boolean
|
clear_password?: boolean
|
||||||
excluded_paths?: string[]
|
excluded_paths?: string[]
|
||||||
|
// Data collection settings (privacy controls)
|
||||||
|
collect_page_paths?: boolean
|
||||||
|
collect_referrers?: boolean
|
||||||
|
collect_device_info?: boolean
|
||||||
|
collect_geo_data?: GeoDataLevel
|
||||||
|
collect_screen_resolution?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listSites(): Promise<Site[]> {
|
export async function listSites(): Promise<Site[]> {
|
||||||
|
|||||||
Reference in New Issue
Block a user