From 6380f216aab5fb5303f454702a51b3e60a848304 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Fri, 13 Mar 2026 12:21:55 +0100 Subject: [PATCH] perf: migrate Settings, Funnels, and Uptime to SWR data fetching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 3 +- app/sites/[id]/funnels/page.tsx | 27 +---- app/sites/[id]/settings/page.tsx | 177 +++++++++---------------------- app/sites/[id]/uptime/page.tsx | 50 ++------- lib/swr/dashboard.ts | 76 +++++++++++++ 5 files changed, 143 insertions(+), 190 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f707b16..05934f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### 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 diff --git a/app/sites/[id]/funnels/page.tsx b/app/sites/[id]/funnels/page.tsx index f42137b..5154de6 100644 --- a/app/sites/[id]/funnels/page.tsx +++ b/app/sites/[id]/funnels/page.tsx @@ -1,8 +1,8 @@ 'use client' -import { useCallback, useEffect, useState } from 'react' 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 { FunnelsListSkeleton, useMinimumLoading } from '@/components/skeletons' import Link from 'next/link' @@ -12,24 +12,7 @@ export default function FunnelsPage() { const router = useRouter() const siteId = params.id as string - const [funnels, setFunnels] = useState([]) - 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 { data: funnels = [], isLoading, mutate } = useFunnels(siteId) const handleDelete = async (e: React.MouseEvent, funnelId: string) => { e.preventDefault() // Prevent navigation @@ -38,13 +21,13 @@ export default function FunnelsPage() { try { await deleteFunnel(siteId, funnelId) toast.success('Funnel deleted') - loadFunnels() + mutate() } catch (error) { toast.error('Failed to delete funnel') } } - const showSkeleton = useMinimumLoading(loading) + const showSkeleton = useMinimumLoading(isLoading && !funnels.length) if (showSkeleton) { return diff --git a/app/sites/[id]/settings/page.tsx b/app/sites/[id]/settings/page.tsx index 0a32b42..dfd0758 100644 --- a/app/sites/[id]/settings/page.tsx +++ b/app/sites/[id]/settings/page.tsx @@ -2,9 +2,9 @@ import { useEffect, useState, useRef } 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 { listReportSchedules, createReportSchedule, updateReportSchedule, deleteReportSchedule, testReportSchedule, type ReportSchedule, type CreateReportScheduleRequest, type EmailConfig, type WebhookConfig } from '@/lib/api/report-schedules' +import { updateSite, resetSiteData, deleteSite, type Site, type GeoDataLevel } from '@/lib/api/sites' +import { createGoal, updateGoal, deleteGoal, type Goal } from '@/lib/api/goals' +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 { getAuthErrorMessage } from '@ciphera-net/ui' 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 { generatePrivacySnippet } from '@/lib/utils/privacySnippet' 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 { motion, AnimatePresence } from 'framer-motion' import { useAuth } from '@/lib/auth/context' @@ -53,8 +53,7 @@ export default function SiteSettingsPage() { const router = useRouter() const siteId = params.id as string - const [site, setSite] = useState(null) - const [loading, setLoading] = useState(true) + const { data: site, isLoading: siteLoading, mutate: mutateSite } = useSite(siteId) const [saving, setSaving] = useState(false) 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_months: 6 }) - const [subscription, setSubscription] = useState(null) - const [subscriptionLoadFailed, setSubscriptionLoadFailed] = useState(false) + const { data: subscription, error: subscriptionError, mutate: mutateSubscription } = useSubscription() 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 { data: goals = [], isLoading: goalsLoading, mutate: mutateGoals } = useGoals(siteId) const [goalModalOpen, setGoalModalOpen] = useState(false) const [editingGoal, setEditingGoal] = useState(null) const [goalForm, setGoalForm] = useState({ name: '', event_name: '' }) const [goalSaving, setGoalSaving] = useState(false) const initialFormRef = useRef('') - // Report schedules state - const [reportSchedules, setReportSchedules] = useState([]) - const [reportLoading, setReportLoading] = useState(false) + // Report schedules + const { data: reportSchedules = [], isLoading: reportLoading, mutate: mutateReportSchedules } = useReportSchedules(siteId) const [reportModalOpen, setReportModalOpen] = useState(false) const [editingSchedule, setEditingSchedule] = useState(null) const [reportSaving, setReportSaving] = useState(false) @@ -112,32 +108,40 @@ export default function SiteSettingsPage() { }) useEffect(() => { - loadSite() - loadSubscription() - }, [siteId]) - - useEffect(() => { - if (activeTab === 'goals' && siteId) { - loadGoals() - } - }, [activeTab, siteId]) - - useEffect(() => { - if (activeTab === 'reports' && siteId) { - loadReportSchedules() - } - }, [activeTab, siteId]) - - const loadSubscription = async () => { - try { - setSubscriptionLoadFailed(false) - const sub = await getSubscription() - setSubscription(sub) - } catch (e) { - setSubscriptionLoadFailed(true) - toast.error(getAuthErrorMessage(e as Error) || 'Could not load plan limits. Showing default options.') - } - } + if (!site) return + setFormData({ + name: site.name, + timezone: site.timezone || 'UTC', + is_public: site.is_public || false, + password: '', + excluded_paths: (site.excluded_paths || []).join('\n'), + collect_page_paths: site.collect_page_paths ?? true, + 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 + }) + initialFormRef.current = JSON.stringify({ + name: site.name, + timezone: site.timezone || 'UTC', + is_public: site.is_public || false, + excluded_paths: (site.excluded_paths || []).join('\n'), + collect_page_paths: site.collect_page_paths ?? true, + 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 useEffect(() => { @@ -152,83 +156,6 @@ export default function SiteSettingsPage() { }) }, [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 = () => { setReportForm({ channel: 'email', @@ -297,7 +224,7 @@ export default function SiteSettingsPage() { toast.success('Report schedule created') } setReportModalOpen(false) - loadReportSchedules() + mutateReportSchedules() } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || 'Failed to save report schedule') } finally { @@ -310,7 +237,7 @@ export default function SiteSettingsPage() { try { await deleteReportSchedule(siteId, schedule.id) toast.success('Report schedule deleted') - loadReportSchedules() + mutateReportSchedules() } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || 'Failed to delete report schedule') } @@ -320,7 +247,7 @@ export default function SiteSettingsPage() { try { await updateReportSchedule(siteId, schedule.id, { enabled: !schedule.enabled }) toast.success(schedule.enabled ? 'Report paused' : 'Report enabled') - loadReportSchedules() + mutateReportSchedules() } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || 'Failed to update report schedule') } @@ -439,7 +366,7 @@ export default function SiteSettingsPage() { toast.success('Goal created') } setGoalModalOpen(false) - loadGoals() + mutateGoals() } catch (err) { toast.error(getAuthErrorMessage(err as Error) || 'Failed to save goal') } finally { @@ -452,7 +379,7 @@ export default function SiteSettingsPage() { try { await deleteGoal(siteId, goal.id) toast.success('Goal deleted') - loadGoals() + mutateGoals() } catch (err) { 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, data_retention_months: formData.data_retention_months }) - loadSite() + mutateSite() } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || 'Failed to save site settings') } finally { @@ -581,7 +508,7 @@ export default function SiteSettingsPage() { if (site?.domain) document.title = `Settings · ${site.domain} | Pulse` }, [site?.domain]) - const showSkeleton = useMinimumLoading(loading) + const showSkeleton = useMinimumLoading(siteLoading && !site) if (showSkeleton) { return ( @@ -1157,14 +1084,14 @@ export default function SiteSettingsPage() { {/* Data Retention */}

Data Retention

- {subscriptionLoadFailed && ( + {!!subscriptionError && (

Plan limits could not be loaded. Options shown may be limited.