perf: migrate Settings, Funnels, and Uptime to SWR data fetching

Replace manual useState/useEffect fetch patterns with SWR hooks so
cached data renders instantly on tab revisit. Skeleton loading now
only appears on the initial cold load, not every navigation.

New hooks: useFunnels, useUptimeStatus, useGoals, useReportSchedules,
useSubscription — all with background revalidation.
This commit is contained in:
Usman Baig
2026-03-13 12:21:55 +01:00
parent b6a7c642f2
commit 6380f216aa
5 changed files with 143 additions and 190 deletions

View File

@@ -8,7 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
### Improved ### Improved
- **Smoother loading on the Journeys page.** The Journeys tab now shows a polished skeleton placeholder while data loads, matching the loading experience on Dashboard, Funnels, Uptime, and Settings. - **Faster tab switching across the board.** Switching between Settings, Funnels, Uptime, and other tabs now shows your data instantly instead of flashing a loading skeleton every time. Previously visited tabs remember their data and show it right away, while quietly refreshing in the background so you always see the latest numbers without the wait.
- **Smoother loading on the Journeys page.** The Journeys tab now shows a polished skeleton placeholder while data loads, matching the loading experience on other tabs.
### Fixed ### Fixed

View File

@@ -1,8 +1,8 @@
'use client' 'use client'
import { useCallback, useEffect, useState } from 'react'
import { useParams, useRouter } from 'next/navigation' import { useParams, useRouter } from 'next/navigation'
import { listFunnels, deleteFunnel, type Funnel } from '@/lib/api/funnels' import { deleteFunnel, type Funnel } from '@/lib/api/funnels'
import { useFunnels } from '@/lib/swr/dashboard'
import { toast, PlusIcon, ArrowRightIcon, ChevronLeftIcon, TrashIcon, Button } from '@ciphera-net/ui' import { toast, PlusIcon, ArrowRightIcon, ChevronLeftIcon, TrashIcon, Button } from '@ciphera-net/ui'
import { FunnelsListSkeleton, useMinimumLoading } from '@/components/skeletons' import { FunnelsListSkeleton, useMinimumLoading } from '@/components/skeletons'
import Link from 'next/link' import Link from 'next/link'
@@ -12,24 +12,7 @@ export default function FunnelsPage() {
const router = useRouter() const router = useRouter()
const siteId = params.id as string const siteId = params.id as string
const [funnels, setFunnels] = useState<Funnel[]>([]) const { data: funnels = [], isLoading, mutate } = useFunnels(siteId)
const [loading, setLoading] = useState(true)
const loadFunnels = useCallback(async () => {
try {
setLoading(true)
const data = await listFunnels(siteId)
setFunnels(data)
} catch (error) {
toast.error('Failed to load your funnels')
} finally {
setLoading(false)
}
}, [siteId])
useEffect(() => {
loadFunnels()
}, [loadFunnels])
const handleDelete = async (e: React.MouseEvent, funnelId: string) => { const handleDelete = async (e: React.MouseEvent, funnelId: string) => {
e.preventDefault() // Prevent navigation e.preventDefault() // Prevent navigation
@@ -38,13 +21,13 @@ export default function FunnelsPage() {
try { try {
await deleteFunnel(siteId, funnelId) await deleteFunnel(siteId, funnelId)
toast.success('Funnel deleted') toast.success('Funnel deleted')
loadFunnels() mutate()
} catch (error) { } catch (error) {
toast.error('Failed to delete funnel') toast.error('Failed to delete funnel')
} }
} }
const showSkeleton = useMinimumLoading(loading) const showSkeleton = useMinimumLoading(isLoading && !funnels.length)
if (showSkeleton) { if (showSkeleton) {
return <FunnelsListSkeleton /> return <FunnelsListSkeleton />

View File

@@ -2,9 +2,9 @@
import { useEffect, useState, useRef } from 'react' import { useEffect, useState, useRef } from 'react'
import { useParams, useRouter } from 'next/navigation' import { useParams, useRouter } from 'next/navigation'
import { getSite, updateSite, resetSiteData, deleteSite, type Site, type GeoDataLevel } from '@/lib/api/sites' import { updateSite, resetSiteData, deleteSite, type Site, type GeoDataLevel } from '@/lib/api/sites'
import { listGoals, createGoal, updateGoal, deleteGoal, type Goal } from '@/lib/api/goals' import { createGoal, updateGoal, deleteGoal, type Goal } from '@/lib/api/goals'
import { listReportSchedules, createReportSchedule, updateReportSchedule, deleteReportSchedule, testReportSchedule, type ReportSchedule, type CreateReportScheduleRequest, type EmailConfig, type WebhookConfig } from '@/lib/api/report-schedules' import { createReportSchedule, updateReportSchedule, deleteReportSchedule, testReportSchedule, type ReportSchedule, type CreateReportScheduleRequest, type EmailConfig, type WebhookConfig } from '@/lib/api/report-schedules'
import { toast } from '@ciphera-net/ui' import { toast } from '@ciphera-net/ui'
import { getAuthErrorMessage } from '@ciphera-net/ui' import { getAuthErrorMessage } from '@ciphera-net/ui'
import { SettingsFormSkeleton, GoalsListSkeleton, useMinimumLoading } from '@/components/skeletons' import { SettingsFormSkeleton, GoalsListSkeleton, useMinimumLoading } from '@/components/skeletons'
@@ -15,7 +15,7 @@ import { Select, Modal, Button } from '@ciphera-net/ui'
import { APP_URL } from '@/lib/api/client' import { APP_URL } from '@/lib/api/client'
import { generatePrivacySnippet } from '@/lib/utils/privacySnippet' import { generatePrivacySnippet } from '@/lib/utils/privacySnippet'
import { useUnsavedChanges } from '@/lib/hooks/useUnsavedChanges' import { useUnsavedChanges } from '@/lib/hooks/useUnsavedChanges'
import { getSubscription, type SubscriptionDetails } from '@/lib/api/billing' import { useSite, useGoals, useReportSchedules, useSubscription } from '@/lib/swr/dashboard'
import { getRetentionOptionsForPlan, formatRetentionMonths } from '@/lib/plans' import { getRetentionOptionsForPlan, formatRetentionMonths } from '@/lib/plans'
import { motion, AnimatePresence } from 'framer-motion' import { motion, AnimatePresence } from 'framer-motion'
import { useAuth } from '@/lib/auth/context' import { useAuth } from '@/lib/auth/context'
@@ -53,8 +53,7 @@ export default function SiteSettingsPage() {
const router = useRouter() const router = useRouter()
const siteId = params.id as string const siteId = params.id as string
const [site, setSite] = useState<Site | null>(null) const { data: site, isLoading: siteLoading, mutate: mutateSite } = useSite(siteId)
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [activeTab, setActiveTab] = useState<'general' | 'visibility' | 'data' | 'goals' | 'reports'>('general') const [activeTab, setActiveTab] = useState<'general' | 'visibility' | 'data' | 'goals' | 'reports'>('general')
@@ -79,23 +78,20 @@ export default function SiteSettingsPage() {
// Data retention (6 = free-tier max; safe default) // Data retention (6 = free-tier max; safe default)
data_retention_months: 6 data_retention_months: 6
}) })
const [subscription, setSubscription] = useState<SubscriptionDetails | null>(null) const { data: subscription, error: subscriptionError, mutate: mutateSubscription } = useSubscription()
const [subscriptionLoadFailed, setSubscriptionLoadFailed] = useState(false)
const [linkCopied, setLinkCopied] = useState(false) const [linkCopied, setLinkCopied] = useState(false)
const [snippetCopied, setSnippetCopied] = useState(false) const [snippetCopied, setSnippetCopied] = useState(false)
const [showVerificationModal, setShowVerificationModal] = useState(false) const [showVerificationModal, setShowVerificationModal] = useState(false)
const [isPasswordEnabled, setIsPasswordEnabled] = useState(false) const [isPasswordEnabled, setIsPasswordEnabled] = useState(false)
const [goals, setGoals] = useState<Goal[]>([]) const { data: goals = [], isLoading: goalsLoading, mutate: mutateGoals } = useGoals(siteId)
const [goalsLoading, setGoalsLoading] = useState(false)
const [goalModalOpen, setGoalModalOpen] = useState(false) const [goalModalOpen, setGoalModalOpen] = useState(false)
const [editingGoal, setEditingGoal] = useState<Goal | null>(null) const [editingGoal, setEditingGoal] = useState<Goal | null>(null)
const [goalForm, setGoalForm] = useState({ name: '', event_name: '' }) const [goalForm, setGoalForm] = useState({ name: '', event_name: '' })
const [goalSaving, setGoalSaving] = useState(false) const [goalSaving, setGoalSaving] = useState(false)
const initialFormRef = useRef<string>('') const initialFormRef = useRef<string>('')
// Report schedules state // Report schedules
const [reportSchedules, setReportSchedules] = useState<ReportSchedule[]>([]) const { data: reportSchedules = [], isLoading: reportLoading, mutate: mutateReportSchedules } = useReportSchedules(siteId)
const [reportLoading, setReportLoading] = useState(false)
const [reportModalOpen, setReportModalOpen] = useState(false) const [reportModalOpen, setReportModalOpen] = useState(false)
const [editingSchedule, setEditingSchedule] = useState<ReportSchedule | null>(null) const [editingSchedule, setEditingSchedule] = useState<ReportSchedule | null>(null)
const [reportSaving, setReportSaving] = useState(false) const [reportSaving, setReportSaving] = useState(false)
@@ -112,32 +108,40 @@ export default function SiteSettingsPage() {
}) })
useEffect(() => { useEffect(() => {
loadSite() if (!site) return
loadSubscription() setFormData({
}, [siteId]) name: site.name,
timezone: site.timezone || 'UTC',
useEffect(() => { is_public: site.is_public || false,
if (activeTab === 'goals' && siteId) { password: '',
loadGoals() excluded_paths: (site.excluded_paths || []).join('\n'),
} collect_page_paths: site.collect_page_paths ?? true,
}, [activeTab, siteId]) collect_referrers: site.collect_referrers ?? true,
collect_device_info: site.collect_device_info ?? true,
useEffect(() => { collect_geo_data: site.collect_geo_data || 'full',
if (activeTab === 'reports' && siteId) { collect_screen_resolution: site.collect_screen_resolution ?? true,
loadReportSchedules() enable_performance_insights: site.enable_performance_insights ?? false,
} filter_bots: site.filter_bots ?? true,
}, [activeTab, siteId]) hide_unknown_locations: site.hide_unknown_locations ?? false,
data_retention_months: site.data_retention_months ?? 6
const loadSubscription = async () => { })
try { initialFormRef.current = JSON.stringify({
setSubscriptionLoadFailed(false) name: site.name,
const sub = await getSubscription() timezone: site.timezone || 'UTC',
setSubscription(sub) is_public: site.is_public || false,
} catch (e) { excluded_paths: (site.excluded_paths || []).join('\n'),
setSubscriptionLoadFailed(true) collect_page_paths: site.collect_page_paths ?? true,
toast.error(getAuthErrorMessage(e as Error) || 'Could not load plan limits. Showing default options.') collect_referrers: site.collect_referrers ?? true,
} collect_device_info: site.collect_device_info ?? true,
} collect_geo_data: site.collect_geo_data || 'full',
collect_screen_resolution: site.collect_screen_resolution ?? true,
enable_performance_insights: site.enable_performance_insights ?? false,
filter_bots: site.filter_bots ?? true,
hide_unknown_locations: site.hide_unknown_locations ?? false,
data_retention_months: site.data_retention_months ?? 6
})
setIsPasswordEnabled(!!site.has_password)
}, [site])
// * Snap data_retention_months to nearest valid option when subscription loads // * Snap data_retention_months to nearest valid option when subscription loads
useEffect(() => { useEffect(() => {
@@ -152,83 +156,6 @@ export default function SiteSettingsPage() {
}) })
}, [subscription]) }, [subscription])
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,
// Hide unknown locations (default to false)
hide_unknown_locations: data.hide_unknown_locations ?? false,
// Data retention (default 6 = free-tier max; avoids flash-then-clamp for existing sites)
data_retention_months: data.data_retention_months ?? 6
})
initialFormRef.current = JSON.stringify({
name: data.name,
timezone: data.timezone || 'UTC',
is_public: data.is_public || false,
excluded_paths: (data.excluded_paths || []).join('\n'),
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,
enable_performance_insights: data.enable_performance_insights ?? false,
filter_bots: data.filter_bots ?? true,
hide_unknown_locations: data.hide_unknown_locations ?? false,
data_retention_months: data.data_retention_months ?? 6
})
if (data.has_password) {
setIsPasswordEnabled(true)
} else {
setIsPasswordEnabled(false)
}
} catch (error: unknown) {
toast.error(getAuthErrorMessage(error) || 'Failed to load site settings')
} 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 loadReportSchedules = async () => {
try {
setReportLoading(true)
const data = await listReportSchedules(siteId)
setReportSchedules(data)
} catch (error: unknown) {
toast.error(getAuthErrorMessage(error) || 'Failed to load report schedules')
} finally {
setReportLoading(false)
}
}
const resetReportForm = () => { const resetReportForm = () => {
setReportForm({ setReportForm({
channel: 'email', channel: 'email',
@@ -297,7 +224,7 @@ export default function SiteSettingsPage() {
toast.success('Report schedule created') toast.success('Report schedule created')
} }
setReportModalOpen(false) setReportModalOpen(false)
loadReportSchedules() mutateReportSchedules()
} catch (error: unknown) { } catch (error: unknown) {
toast.error(getAuthErrorMessage(error) || 'Failed to save report schedule') toast.error(getAuthErrorMessage(error) || 'Failed to save report schedule')
} finally { } finally {
@@ -310,7 +237,7 @@ export default function SiteSettingsPage() {
try { try {
await deleteReportSchedule(siteId, schedule.id) await deleteReportSchedule(siteId, schedule.id)
toast.success('Report schedule deleted') toast.success('Report schedule deleted')
loadReportSchedules() mutateReportSchedules()
} catch (error: unknown) { } catch (error: unknown) {
toast.error(getAuthErrorMessage(error) || 'Failed to delete report schedule') toast.error(getAuthErrorMessage(error) || 'Failed to delete report schedule')
} }
@@ -320,7 +247,7 @@ export default function SiteSettingsPage() {
try { try {
await updateReportSchedule(siteId, schedule.id, { enabled: !schedule.enabled }) await updateReportSchedule(siteId, schedule.id, { enabled: !schedule.enabled })
toast.success(schedule.enabled ? 'Report paused' : 'Report enabled') toast.success(schedule.enabled ? 'Report paused' : 'Report enabled')
loadReportSchedules() mutateReportSchedules()
} catch (error: unknown) { } catch (error: unknown) {
toast.error(getAuthErrorMessage(error) || 'Failed to update report schedule') toast.error(getAuthErrorMessage(error) || 'Failed to update report schedule')
} }
@@ -439,7 +366,7 @@ export default function SiteSettingsPage() {
toast.success('Goal created') toast.success('Goal created')
} }
setGoalModalOpen(false) setGoalModalOpen(false)
loadGoals() mutateGoals()
} catch (err) { } catch (err) {
toast.error(getAuthErrorMessage(err as Error) || 'Failed to save goal') toast.error(getAuthErrorMessage(err as Error) || 'Failed to save goal')
} finally { } finally {
@@ -452,7 +379,7 @@ export default function SiteSettingsPage() {
try { try {
await deleteGoal(siteId, goal.id) await deleteGoal(siteId, goal.id)
toast.success('Goal deleted') toast.success('Goal deleted')
loadGoals() mutateGoals()
} catch (err) { } catch (err) {
toast.error(getAuthErrorMessage(err as Error) || 'Failed to delete goal') toast.error(getAuthErrorMessage(err as Error) || 'Failed to delete goal')
} }
@@ -506,7 +433,7 @@ export default function SiteSettingsPage() {
hide_unknown_locations: formData.hide_unknown_locations, hide_unknown_locations: formData.hide_unknown_locations,
data_retention_months: formData.data_retention_months data_retention_months: formData.data_retention_months
}) })
loadSite() mutateSite()
} catch (error: unknown) { } catch (error: unknown) {
toast.error(getAuthErrorMessage(error) || 'Failed to save site settings') toast.error(getAuthErrorMessage(error) || 'Failed to save site settings')
} finally { } finally {
@@ -581,7 +508,7 @@ export default function SiteSettingsPage() {
if (site?.domain) document.title = `Settings · ${site.domain} | Pulse` if (site?.domain) document.title = `Settings · ${site.domain} | Pulse`
}, [site?.domain]) }, [site?.domain])
const showSkeleton = useMinimumLoading(loading) const showSkeleton = useMinimumLoading(siteLoading && !site)
if (showSkeleton) { if (showSkeleton) {
return ( return (
@@ -1157,14 +1084,14 @@ export default function SiteSettingsPage() {
{/* Data Retention */} {/* Data Retention */}
<div className="space-y-4 pt-6 border-t border-neutral-100 dark:border-neutral-800"> <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">Data Retention</h3> <h3 className="text-sm font-medium text-neutral-700 dark:text-neutral-300">Data Retention</h3>
{subscriptionLoadFailed && ( {!!subscriptionError && (
<div className="p-3 rounded-xl border border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900/20 flex items-center justify-between gap-3"> <div className="p-3 rounded-xl border border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900/20 flex items-center justify-between gap-3">
<p className="text-sm text-amber-800 dark:text-amber-200"> <p className="text-sm text-amber-800 dark:text-amber-200">
Plan limits could not be loaded. Options shown may be limited. Plan limits could not be loaded. Options shown may be limited.
</p> </p>
<button <button
type="button" type="button"
onClick={loadSubscription} onClick={() => mutateSubscription()}
className="shrink-0 text-sm font-medium text-amber-800 dark:text-amber-200 hover:underline" className="shrink-0 text-sm font-medium text-amber-800 dark:text-amber-200 hover:underline"
> >
Retry Retry

View File

@@ -1,12 +1,11 @@
'use client' 'use client'
import { useAuth } from '@/lib/auth/context' import { useAuth } from '@/lib/auth/context'
import { useEffect, useState, useCallback, useRef } from 'react' import { useEffect, useState, useRef } from 'react'
import { useParams, useRouter } from 'next/navigation' import { useParams, useRouter } from 'next/navigation'
import { motion, AnimatePresence } from 'framer-motion' import { motion, AnimatePresence } from 'framer-motion'
import { getSite, type Site } from '@/lib/api/sites' import { useSite, useUptimeStatus } from '@/lib/swr/dashboard'
import { import {
getUptimeStatus,
createUptimeMonitor, createUptimeMonitor,
updateUptimeMonitor, updateUptimeMonitor,
deleteUptimeMonitor, deleteUptimeMonitor,
@@ -561,9 +560,8 @@ export default function UptimePage() {
const router = useRouter() const router = useRouter()
const siteId = params.id as string const siteId = params.id as string
const [site, setSite] = useState<Site | null>(null) const { data: site } = useSite(siteId)
const [loading, setLoading] = useState(true) const { data: uptimeData, isLoading, mutate: mutateUptime } = useUptimeStatus(siteId)
const [uptimeData, setUptimeData] = useState<UptimeStatusResponse | null>(null)
const [expandedMonitor, setExpandedMonitor] = useState<string | null>(null) const [expandedMonitor, setExpandedMonitor] = useState<string | null>(null)
const [showAddModal, setShowAddModal] = useState(false) const [showAddModal, setShowAddModal] = useState(false)
const [showEditModal, setShowEditModal] = useState(false) const [showEditModal, setShowEditModal] = useState(false)
@@ -577,38 +575,6 @@ export default function UptimePage() {
}) })
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const loadData = useCallback(async () => {
try {
const [siteData, statusData] = await Promise.all([
getSite(siteId),
getUptimeStatus(siteId),
])
setSite(siteData)
setUptimeData(statusData)
} catch (error: unknown) {
toast.error(getAuthErrorMessage(error) || 'Failed to load uptime monitors')
} finally {
setLoading(false)
}
}, [siteId])
useEffect(() => {
loadData()
}, [loadData])
// * Auto-refresh every 30 seconds; show toast on failure (e.g. network loss or auth expiry)
useEffect(() => {
const interval = setInterval(async () => {
try {
const statusData = await getUptimeStatus(siteId)
setUptimeData(statusData)
} catch {
toast.error('Could not refresh uptime data. Check your connection or sign in again.')
}
}, 30000)
return () => clearInterval(interval)
}, [siteId])
const handleAddMonitor = async () => { const handleAddMonitor = async () => {
if (!formData.name || !formData.url) { if (!formData.name || !formData.url) {
toast.error('Name and URL are required') toast.error('Name and URL are required')
@@ -620,7 +586,7 @@ export default function UptimePage() {
toast.success('Monitor created successfully') toast.success('Monitor created successfully')
setShowAddModal(false) setShowAddModal(false)
setFormData({ name: '', url: '', check_interval_seconds: 300, expected_status_code: 200, timeout_seconds: 30 }) setFormData({ name: '', url: '', check_interval_seconds: 300, expected_status_code: 200, timeout_seconds: 30 })
await loadData() mutateUptime()
} catch (error: unknown) { } catch (error: unknown) {
toast.error(getAuthErrorMessage(error) || 'Failed to create monitor') toast.error(getAuthErrorMessage(error) || 'Failed to create monitor')
} finally { } finally {
@@ -643,7 +609,7 @@ export default function UptimePage() {
toast.success('Monitor updated successfully') toast.success('Monitor updated successfully')
setShowEditModal(false) setShowEditModal(false)
setEditingMonitor(null) setEditingMonitor(null)
await loadData() mutateUptime()
} catch (error: unknown) { } catch (error: unknown) {
toast.error(getAuthErrorMessage(error) || 'Failed to update monitor') toast.error(getAuthErrorMessage(error) || 'Failed to update monitor')
} finally { } finally {
@@ -656,7 +622,7 @@ export default function UptimePage() {
try { try {
await deleteUptimeMonitor(siteId, monitorId) await deleteUptimeMonitor(siteId, monitorId)
toast.success('Monitor deleted') toast.success('Monitor deleted')
await loadData() mutateUptime()
} catch (error: unknown) { } catch (error: unknown) {
toast.error(getAuthErrorMessage(error) || 'Failed to delete monitor') toast.error(getAuthErrorMessage(error) || 'Failed to delete monitor')
} }
@@ -678,7 +644,7 @@ export default function UptimePage() {
if (site?.domain) document.title = `Uptime · ${site.domain} | Pulse` if (site?.domain) document.title = `Uptime · ${site.domain} | Pulse`
}, [site?.domain]) }, [site?.domain])
const showSkeleton = useMinimumLoading(loading) const showSkeleton = useMinimumLoading(isLoading && !uptimeData)
if (showSkeleton) return <UptimeSkeleton /> if (showSkeleton) return <UptimeSkeleton />
if (!site) return <div className="p-8 text-neutral-500">Site not found</div> if (!site) return <div className="p-8 text-neutral-500">Site not found</div>

View File

@@ -29,6 +29,11 @@ import { listAnnotations } from '@/lib/api/annotations'
import type { Annotation } from '@/lib/api/annotations' import type { Annotation } from '@/lib/api/annotations'
import { getSite } from '@/lib/api/sites' import { getSite } from '@/lib/api/sites'
import type { Site } from '@/lib/api/sites' import type { Site } from '@/lib/api/sites'
import { listFunnels, type Funnel } from '@/lib/api/funnels'
import { getUptimeStatus, type UptimeStatusResponse } from '@/lib/api/uptime'
import { listGoals, type Goal } from '@/lib/api/goals'
import { listReportSchedules, type ReportSchedule } from '@/lib/api/report-schedules'
import { getSubscription, type SubscriptionDetails } from '@/lib/api/billing'
import type { import type {
Stats, Stats,
DailyStat, DailyStat,
@@ -69,6 +74,11 @@ const fetchers = {
getJourneyTopPaths(siteId, start, end, { limit, minSessions, entryPath }), getJourneyTopPaths(siteId, start, end, { limit, minSessions, entryPath }),
journeyEntryPoints: (siteId: string, start: string, end: string) => journeyEntryPoints: (siteId: string, start: string, end: string) =>
getJourneyEntryPoints(siteId, start, end), getJourneyEntryPoints(siteId, start, end),
funnels: (siteId: string) => listFunnels(siteId),
uptimeStatus: (siteId: string) => getUptimeStatus(siteId),
goals: (siteId: string) => listGoals(siteId),
reportSchedules: (siteId: string) => listReportSchedules(siteId),
subscription: () => getSubscription(),
} }
// * Standard SWR config for dashboard data // * Standard SWR config for dashboard data
@@ -334,5 +344,71 @@ export function useJourneyEntryPoints(siteId: string, start: string, end: string
) )
} }
// * Hook for funnels list
export function useFunnels(siteId: string) {
return useSWR<Funnel[]>(
siteId ? ['funnels', siteId] : null,
() => fetchers.funnels(siteId),
{
...dashboardSWRConfig,
refreshInterval: 60 * 1000,
dedupingInterval: 10 * 1000,
}
)
}
// * Hook for uptime status (refreshes every 30s to match original polling)
export function useUptimeStatus(siteId: string) {
return useSWR<UptimeStatusResponse>(
siteId ? ['uptimeStatus', siteId] : null,
() => fetchers.uptimeStatus(siteId),
{
...dashboardSWRConfig,
refreshInterval: 30 * 1000,
dedupingInterval: 10 * 1000,
keepPreviousData: true,
}
)
}
// * Hook for goals list
export function useGoals(siteId: string) {
return useSWR<Goal[]>(
siteId ? ['goals', siteId] : null,
() => fetchers.goals(siteId),
{
...dashboardSWRConfig,
refreshInterval: 60 * 1000,
dedupingInterval: 10 * 1000,
}
)
}
// * Hook for report schedules
export function useReportSchedules(siteId: string) {
return useSWR<ReportSchedule[]>(
siteId ? ['reportSchedules', siteId] : null,
() => fetchers.reportSchedules(siteId),
{
...dashboardSWRConfig,
refreshInterval: 60 * 1000,
dedupingInterval: 10 * 1000,
}
)
}
// * Hook for subscription details (changes rarely)
export function useSubscription() {
return useSWR<SubscriptionDetails>(
'subscription',
() => fetchers.subscription(),
{
...dashboardSWRConfig,
refreshInterval: 5 * 60 * 1000,
dedupingInterval: 30 * 1000,
}
)
}
// * Re-export for convenience // * Re-export for convenience
export { fetchers } export { fetchers }