refactor: integrate useMinimumLoading hook for enhanced loading state management across multiple pages
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
|||||||
import { getAuthErrorMessage } from '@ciphera-net/ui'
|
import { getAuthErrorMessage } from '@ciphera-net/ui'
|
||||||
import { formatTimeAgo, getTypeIcon } from '@/lib/utils/notifications'
|
import { formatTimeAgo, getTypeIcon } from '@/lib/utils/notifications'
|
||||||
import { Button, ArrowLeftIcon } from '@ciphera-net/ui'
|
import { Button, ArrowLeftIcon } from '@ciphera-net/ui'
|
||||||
import { NotificationsListSkeleton } from '@/components/skeletons'
|
import { NotificationsListSkeleton, useMinimumLoading } from '@/components/skeletons'
|
||||||
import { toast } from '@ciphera-net/ui'
|
import { toast } from '@ciphera-net/ui'
|
||||||
|
|
||||||
const PAGE_SIZE = 50
|
const PAGE_SIZE = 50
|
||||||
@@ -30,6 +30,7 @@ export default function NotificationsPage() {
|
|||||||
const [offset, setOffset] = useState(0)
|
const [offset, setOffset] = useState(0)
|
||||||
const [hasMore, setHasMore] = useState(true)
|
const [hasMore, setHasMore] = useState(true)
|
||||||
const [loadingMore, setLoadingMore] = useState(false)
|
const [loadingMore, setLoadingMore] = useState(false)
|
||||||
|
const showSkeleton = useMinimumLoading(loading)
|
||||||
|
|
||||||
const fetchPage = async (pageOffset: number, append: boolean) => {
|
const fetchPage = async (pageOffset: number, append: boolean) => {
|
||||||
if (append) setLoadingMore(true)
|
if (append) setLoadingMore(true)
|
||||||
@@ -128,7 +129,7 @@ export default function NotificationsPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{loading ? (
|
{showSkeleton ? (
|
||||||
<NotificationsListSkeleton />
|
<NotificationsListSkeleton />
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="p-6 text-center text-red-500 bg-red-50 dark:bg-red-900/10 rounded-2xl border border-red-200 dark:border-red-800">
|
<div className="p-6 text-center text-red-500 bg-red-50 dark:bg-red-900/10 rounded-2xl border border-red-200 dark:border-red-800">
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import Locations from '@/components/dashboard/Locations'
|
|||||||
import TechSpecs from '@/components/dashboard/TechSpecs'
|
import TechSpecs from '@/components/dashboard/TechSpecs'
|
||||||
import PerformanceStats from '@/components/dashboard/PerformanceStats'
|
import PerformanceStats from '@/components/dashboard/PerformanceStats'
|
||||||
import { Select, DatePicker as DatePickerModal, Captcha, DownloadIcon, ZapIcon } from '@ciphera-net/ui'
|
import { Select, DatePicker as DatePickerModal, Captcha, DownloadIcon, ZapIcon } from '@ciphera-net/ui'
|
||||||
import { DashboardSkeleton } from '@/components/skeletons'
|
import { DashboardSkeleton, useMinimumLoading } from '@/components/skeletons'
|
||||||
import ExportModal from '@/components/dashboard/ExportModal'
|
import ExportModal from '@/components/dashboard/ExportModal'
|
||||||
|
|
||||||
// Helper to get date ranges
|
// Helper to get date ranges
|
||||||
@@ -193,7 +193,9 @@ export default function PublicDashboardPage() {
|
|||||||
loadDashboard()
|
loadDashboard()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading && !data && !isPasswordProtected) {
|
const showSkeleton = useMinimumLoading(loading && !data && !isPasswordProtected)
|
||||||
|
|
||||||
|
if (showSkeleton) {
|
||||||
return <DashboardSkeleton />
|
return <DashboardSkeleton />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useParams, useRouter } from 'next/navigation'
|
|||||||
import { ApiError } from '@/lib/api/client'
|
import { ApiError } from '@/lib/api/client'
|
||||||
import { getFunnel, getFunnelStats, deleteFunnel, type Funnel, type FunnelStats } from '@/lib/api/funnels'
|
import { getFunnel, getFunnelStats, deleteFunnel, type Funnel, type FunnelStats } from '@/lib/api/funnels'
|
||||||
import { toast, Select, DatePicker, ChevronLeftIcon, ArrowRightIcon, TrashIcon, useTheme, Button } from '@ciphera-net/ui'
|
import { toast, Select, DatePicker, ChevronLeftIcon, ArrowRightIcon, TrashIcon, useTheme, Button } from '@ciphera-net/ui'
|
||||||
import { FunnelDetailSkeleton } from '@/components/skeletons'
|
import { FunnelDetailSkeleton, useMinimumLoading } from '@/components/skeletons'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import {
|
import {
|
||||||
BarChart,
|
BarChart,
|
||||||
@@ -92,7 +92,9 @@ export default function FunnelReportPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading && !funnel) {
|
const showSkeleton = useMinimumLoading(loading && !funnel)
|
||||||
|
|
||||||
|
if (showSkeleton) {
|
||||||
return <FunnelDetailSkeleton />
|
return <FunnelDetailSkeleton />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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 { listFunnels, deleteFunnel, type Funnel } from '@/lib/api/funnels'
|
||||||
import { toast, PlusIcon, ArrowRightIcon, ChevronLeftIcon, TrashIcon, Button } from '@ciphera-net/ui'
|
import { toast, PlusIcon, ArrowRightIcon, ChevronLeftIcon, TrashIcon, Button } from '@ciphera-net/ui'
|
||||||
import { FunnelsListSkeleton } from '@/components/skeletons'
|
import { FunnelsListSkeleton, useMinimumLoading } from '@/components/skeletons'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
export default function FunnelsPage() {
|
export default function FunnelsPage() {
|
||||||
@@ -44,7 +44,9 @@ export default function FunnelsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
const showSkeleton = useMinimumLoading(loading)
|
||||||
|
|
||||||
|
if (showSkeleton) {
|
||||||
return <FunnelsListSkeleton />
|
return <FunnelsListSkeleton />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { toast } from '@ciphera-net/ui'
|
|||||||
import { getAuthErrorMessage } from '@ciphera-net/ui'
|
import { getAuthErrorMessage } from '@ciphera-net/ui'
|
||||||
import { LoadingOverlay, Button } from '@ciphera-net/ui'
|
import { LoadingOverlay, Button } from '@ciphera-net/ui'
|
||||||
import { Select, DatePicker, DownloadIcon } from '@ciphera-net/ui'
|
import { Select, DatePicker, DownloadIcon } from '@ciphera-net/ui'
|
||||||
import { DashboardSkeleton } from '@/components/skeletons'
|
import { DashboardSkeleton, useMinimumLoading } from '@/components/skeletons'
|
||||||
import ExportModal from '@/components/dashboard/ExportModal'
|
import ExportModal from '@/components/dashboard/ExportModal'
|
||||||
import ContentStats from '@/components/dashboard/ContentStats'
|
import ContentStats from '@/components/dashboard/ContentStats'
|
||||||
import TopReferrers from '@/components/dashboard/TopReferrers'
|
import TopReferrers from '@/components/dashboard/TopReferrers'
|
||||||
@@ -216,7 +216,9 @@ export default function SiteDashboardPage() {
|
|||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [siteId, dateRange, todayInterval, multiDayInterval, isSettingsLoaded, loadData, loadRealtime])
|
}, [siteId, dateRange, todayInterval, multiDayInterval, isSettingsLoaded, loadData, loadRealtime])
|
||||||
|
|
||||||
if (loading) {
|
const showSkeleton = useMinimumLoading(loading)
|
||||||
|
|
||||||
|
if (showSkeleton) {
|
||||||
return <DashboardSkeleton />
|
return <DashboardSkeleton />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { getRealtimeVisitors, getSessionDetails, type Visitor, type SessionEvent
|
|||||||
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 { UserIcon } from '@ciphera-net/ui'
|
import { UserIcon } from '@ciphera-net/ui'
|
||||||
import { RealtimeSkeleton, SessionEventsSkeleton } from '@/components/skeletons'
|
import { RealtimeSkeleton, SessionEventsSkeleton, useMinimumLoading } from '@/components/skeletons'
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
|
||||||
function formatTimeAgo(dateString: string) {
|
function formatTimeAgo(dateString: string) {
|
||||||
@@ -91,7 +91,9 @@ export default function RealtimePage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) return <RealtimeSkeleton />
|
const showSkeleton = useMinimumLoading(loading)
|
||||||
|
|
||||||
|
if (showSkeleton) return <RealtimeSkeleton />
|
||||||
if (!site) return <div className="p-8">Site not found</div>
|
if (!site) return <div className="p-8">Site not found</div>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { getSite, updateSite, resetSiteData, deleteSite, type Site, type GeoData
|
|||||||
import { listGoals, createGoal, updateGoal, deleteGoal, type Goal } from '@/lib/api/goals'
|
import { listGoals, createGoal, updateGoal, deleteGoal, type Goal } from '@/lib/api/goals'
|
||||||
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 } from '@/components/skeletons'
|
import { SettingsFormSkeleton, GoalsListSkeleton, useMinimumLoading } from '@/components/skeletons'
|
||||||
import VerificationModal from '@/components/sites/VerificationModal'
|
import VerificationModal from '@/components/sites/VerificationModal'
|
||||||
import ScriptSetupBlock from '@/components/sites/ScriptSetupBlock'
|
import ScriptSetupBlock from '@/components/sites/ScriptSetupBlock'
|
||||||
import { PasswordInput } from '@ciphera-net/ui'
|
import { PasswordInput } from '@ciphera-net/ui'
|
||||||
@@ -317,7 +317,9 @@ export default function SiteSettingsPage() {
|
|||||||
setTimeout(() => setSnippetCopied(false), 2000)
|
setTimeout(() => setSnippetCopied(false), 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
const showSkeleton = useMinimumLoading(loading)
|
||||||
|
|
||||||
|
if (showSkeleton) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
|
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { toast } from '@ciphera-net/ui'
|
|||||||
import { useTheme } from '@ciphera-net/ui'
|
import { useTheme } from '@ciphera-net/ui'
|
||||||
import { getAuthErrorMessage } from '@ciphera-net/ui'
|
import { getAuthErrorMessage } from '@ciphera-net/ui'
|
||||||
import { Button, Modal } from '@ciphera-net/ui'
|
import { Button, Modal } from '@ciphera-net/ui'
|
||||||
import { UptimeSkeleton, ChecksSkeleton } from '@/components/skeletons'
|
import { UptimeSkeleton, ChecksSkeleton, useMinimumLoading } from '@/components/skeletons'
|
||||||
import {
|
import {
|
||||||
AreaChart,
|
AreaChart,
|
||||||
Area,
|
Area,
|
||||||
@@ -703,7 +703,9 @@ export default function UptimePage() {
|
|||||||
setShowEditModal(true)
|
setShowEditModal(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) return <UptimeSkeleton />
|
const showSkeleton = useMinimumLoading(loading)
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
const monitors = Array.isArray(uptimeData?.monitors) ? uptimeData.monitors : []
|
const monitors = Array.isArray(uptimeData?.monitors) ? uptimeData.monitors : []
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
const SK = 'animate-pulse bg-neutral-100 dark:bg-neutral-800'
|
const SK = 'animate-pulse bg-neutral-100 dark:bg-neutral-800'
|
||||||
|
|
||||||
|
export { useMinimumLoading } from './useMinimumLoading'
|
||||||
|
|
||||||
// ─── Primitives ──────────────────────────────────────────────
|
// ─── Primitives ──────────────────────────────────────────────
|
||||||
|
|
||||||
export function SkeletonLine({ className = '' }: { className?: string }) {
|
export function SkeletonLine({ className = '' }: { className?: string }) {
|
||||||
|
|||||||
34
components/useMinimumLoading.ts
Normal file
34
components/useMinimumLoading.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents skeleton flicker on fast loads by keeping it visible
|
||||||
|
* for at least `minMs` once it appears.
|
||||||
|
*
|
||||||
|
* @param loading - The raw loading state from data fetching
|
||||||
|
* @param minMs - Minimum milliseconds the skeleton stays visible (default 300)
|
||||||
|
* @returns Whether the skeleton should be shown
|
||||||
|
*/
|
||||||
|
export function useMinimumLoading(loading: boolean, minMs = 300): boolean {
|
||||||
|
const [show, setShow] = useState(loading)
|
||||||
|
const startRef = useRef<number>(loading ? Date.now() : 0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading) {
|
||||||
|
startRef.current = Date.now()
|
||||||
|
setShow(true)
|
||||||
|
} else {
|
||||||
|
const elapsed = Date.now() - startRef.current
|
||||||
|
const remaining = minMs - elapsed
|
||||||
|
if (remaining > 0) {
|
||||||
|
const timer = setTimeout(() => setShow(false), remaining)
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
} else {
|
||||||
|
setShow(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [loading, minMs])
|
||||||
|
|
||||||
|
return show
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user