fix: skeleton loading states match actual page layouts

- PageSpeed: show 4 gauge rings, screenshot, legend, metrics grid, trend chart
- Uptime: match real layout with status card, 90-day bar, 4-col detail grid
- Remove duplicate local skeletons in behavior components, use shared library
- Strip light-mode classes from dark-only app
This commit is contained in:
Usman Baig
2026-03-24 21:17:21 +01:00
parent 5dfc3a5636
commit 5a03e1f9a5
5 changed files with 114 additions and 102 deletions

View File

@@ -868,35 +868,67 @@ function AuditItem({ item }: { item: Record<string, any> }) {
// * Skeleton loading state // * Skeleton loading state
function PageSpeedSkeleton() { function PageSpeedSkeleton() {
return ( return (
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8 space-y-6"> <div className="w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8 space-y-6 animate-pulse">
<div className="animate-pulse space-y-2 mb-8"> {/* Header — title + subtitle + toggle buttons */}
<div className="h-8 w-48 bg-neutral-200 dark:bg-neutral-700 rounded" /> <div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="h-4 w-72 bg-neutral-200 dark:bg-neutral-700 rounded" /> <div className="space-y-2">
</div> <div className="h-8 w-36 bg-neutral-700 rounded" />
{/* Hero skeleton */} <div className="h-4 w-72 bg-neutral-700 rounded" />
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 sm:p-8 animate-pulse"> </div>
<div className="flex items-center gap-8"> <div className="flex items-center gap-3">
<div className="w-40 h-40 rounded-full bg-neutral-200 dark:bg-neutral-700 flex-shrink-0" /> <div className="flex gap-1">
<div className="flex-1 space-y-3"> <div className="h-8 w-16 bg-neutral-700 rounded" />
<div className="h-5 w-32 bg-neutral-200 dark:bg-neutral-700 rounded" /> <div className="h-8 w-20 bg-neutral-700 rounded" />
<div className="h-5 w-36 bg-neutral-200 dark:bg-neutral-700 rounded" />
<div className="h-5 w-24 bg-neutral-200 dark:bg-neutral-700 rounded" />
</div> </div>
<div className="w-48 h-36 bg-neutral-200 dark:bg-neutral-700 rounded-lg flex-shrink-0 hidden md:block" /> <div className="h-9 w-24 bg-neutral-700 rounded-lg" />
</div> </div>
</div> </div>
{/* Metrics skeleton */}
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 sm:p-8 animate-pulse"> {/* Score overview — 4 gauge circles + screenshot */}
<div className="h-3 w-16 bg-neutral-200 dark:bg-neutral-700 rounded mb-5" /> <div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-6 sm:p-8">
<div className="grid grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-6"> <div className="flex flex-col lg:flex-row items-center gap-8">
<div className="flex-1 flex items-center justify-center gap-6 sm:gap-8 flex-wrap">
{[...Array(4)].map((_, i) => (
<div key={i} className="flex flex-col items-center gap-2">
<div className="w-[90px] h-[90px] rounded-full border-[6px] border-neutral-700 bg-transparent" />
<div className="h-3 w-16 bg-neutral-700 rounded" />
</div>
))}
</div>
<div className="w-48 h-44 bg-neutral-700 rounded-lg flex-shrink-0 hidden md:block" />
</div>
{/* Legend bar */}
<div className="flex items-center gap-4 mt-6 pt-4 border-t border-neutral-800">
<div className="h-3 w-32 bg-neutral-700 rounded" />
<div className="ml-auto flex items-center gap-3">
<div className="h-2 w-10 bg-neutral-700 rounded" />
<div className="h-2 w-10 bg-neutral-700 rounded" />
<div className="h-2 w-10 bg-neutral-700 rounded" />
</div>
</div>
</div>
{/* Metrics card — 6 metrics in 3-col grid */}
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-6 sm:p-8">
<div className="h-3 w-16 bg-neutral-700 rounded mb-5" />
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-6">
{[...Array(6)].map((_, i) => ( {[...Array(6)].map((_, i) => (
<div key={i} className="space-y-2"> <div key={i} className="flex items-start gap-3">
<div className="h-3 w-32 bg-neutral-200 dark:bg-neutral-700 rounded" /> <div className="mt-1.5 w-2.5 h-2.5 rounded-full bg-neutral-700 flex-shrink-0" />
<div className="h-7 w-20 bg-neutral-200 dark:bg-neutral-700 rounded" /> <div className="space-y-2">
<div className="h-3 w-32 bg-neutral-700 rounded" />
<div className="h-7 w-20 bg-neutral-700 rounded" />
</div>
</div> </div>
))} ))}
</div> </div>
</div> </div>
{/* Score trend chart placeholder */}
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-6 sm:p-8">
<div className="h-3 w-40 bg-neutral-700 rounded mb-5" />
<div className="h-48 w-full bg-neutral-800 rounded-lg" />
</div>
</div> </div>
) )
} }

View File

@@ -3,30 +3,13 @@
import { formatNumber } from '@ciphera-net/ui' import { formatNumber } from '@ciphera-net/ui'
import { Files } from '@phosphor-icons/react' import { Files } from '@phosphor-icons/react'
import type { FrustrationByPage } from '@/lib/api/stats' import type { FrustrationByPage } from '@/lib/api/stats'
import { TableSkeleton } from '@/components/skeletons'
interface FrustrationByPageTableProps { interface FrustrationByPageTableProps {
pages: FrustrationByPage[] pages: FrustrationByPage[]
loading: boolean loading: boolean
} }
function SkeletonRows() {
return (
<div className="space-y-2">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="animate-pulse flex items-center justify-between h-9 px-2">
<div className="h-4 w-40 bg-neutral-200 dark:bg-neutral-700 rounded" />
<div className="flex gap-6">
<div className="h-4 w-10 bg-neutral-200 dark:bg-neutral-700 rounded" />
<div className="h-4 w-10 bg-neutral-200 dark:bg-neutral-700 rounded" />
<div className="h-4 w-10 bg-neutral-200 dark:bg-neutral-700 rounded" />
<div className="h-4 w-10 bg-neutral-200 dark:bg-neutral-700 rounded" />
</div>
</div>
))}
</div>
)
}
export default function FrustrationByPageTable({ pages, loading }: FrustrationByPageTableProps) { export default function FrustrationByPageTable({ pages, loading }: FrustrationByPageTableProps) {
const hasData = pages.length > 0 const hasData = pages.length > 0
const maxTotal = Math.max(...pages.map(p => p.total), 1) const maxTotal = Math.max(...pages.map(p => p.total), 1)
@@ -43,7 +26,7 @@ export default function FrustrationByPageTable({ pages, loading }: FrustrationBy
</p> </p>
{loading ? ( {loading ? (
<SkeletonRows /> <TableSkeleton rows={5} cols={5} />
) : hasData ? ( ) : hasData ? (
<div className="overflow-x-auto -mx-6 px-6"> <div className="overflow-x-auto -mx-6 px-6">
{/* Header */} {/* Header */}

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import type { FrustrationSummary } from '@/lib/api/stats' import type { FrustrationSummary } from '@/lib/api/stats'
import { StatCardSkeleton } from '@/components/skeletons'
interface FrustrationSummaryCardsProps { interface FrustrationSummaryCardsProps {
data: FrustrationSummary | null data: FrustrationSummary | null
@@ -39,25 +40,13 @@ function ChangeIndicator({ change }: { change: ReturnType<typeof pctChange> }) {
) )
} }
function SkeletonCard() {
return (
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
<div className="animate-pulse space-y-3">
<div className="h-4 w-24 bg-neutral-200 dark:bg-neutral-700 rounded" />
<div className="h-8 w-16 bg-neutral-200 dark:bg-neutral-700 rounded" />
<div className="h-3 w-32 bg-neutral-200 dark:bg-neutral-700 rounded" />
</div>
</div>
)
}
export default function FrustrationSummaryCards({ data, loading }: FrustrationSummaryCardsProps) { export default function FrustrationSummaryCards({ data, loading }: FrustrationSummaryCardsProps) {
if (loading || !data) { if (loading || !data) {
return ( return (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8">
<SkeletonCard /> <StatCardSkeleton />
<SkeletonCard /> <StatCardSkeleton />
<SkeletonCard /> <StatCardSkeleton />
</div> </div>
) )
} }

View File

@@ -8,26 +8,13 @@ import {
type ChartConfig, type ChartConfig,
} from '@/components/charts' } from '@/components/charts'
import type { FrustrationSummary } from '@/lib/api/stats' import type { FrustrationSummary } from '@/lib/api/stats'
import { WidgetSkeleton } from '@/components/skeletons'
interface FrustrationTrendProps { interface FrustrationTrendProps {
summary: FrustrationSummary | null summary: FrustrationSummary | null
loading: boolean loading: boolean
} }
function SkeletonCard() {
return (
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
<div className="animate-pulse space-y-3 mb-4">
<div className="h-5 w-36 bg-neutral-200 dark:bg-neutral-700 rounded" />
<div className="h-4 w-48 bg-neutral-200 dark:bg-neutral-700 rounded" />
</div>
<div className="flex-1 min-h-[270px] animate-pulse flex items-center justify-center">
<div className="w-[200px] h-[200px] rounded-full bg-neutral-200 dark:bg-neutral-700" />
</div>
</div>
)
}
const LABELS: Record<string, string> = { const LABELS: Record<string, string> = {
rage_clicks: 'Rage Clicks', rage_clicks: 'Rage Clicks',
dead_clicks: 'Dead Clicks', dead_clicks: 'Dead Clicks',
@@ -70,7 +57,7 @@ function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<
} }
export default function FrustrationTrend({ summary, loading }: FrustrationTrendProps) { export default function FrustrationTrend({ summary, loading }: FrustrationTrendProps) {
if (loading || !summary) return <SkeletonCard /> if (loading || !summary) return <WidgetSkeleton />
const hasData = summary.rage_clicks > 0 || summary.dead_clicks > 0 || const hasData = summary.rage_clicks > 0 || summary.dead_clicks > 0 ||
summary.prev_rage_clicks > 0 || summary.prev_dead_clicks > 0 summary.prev_rage_clicks > 0 || summary.prev_dead_clicks > 0

View File

@@ -1,10 +1,10 @@
/** /**
* Reusable skeleton loading primitives and composites for Pulse. * Reusable skeleton loading primitives and composites for Pulse.
* All skeletons follow the design-system pattern: * All skeletons follow the design-system pattern:
* animate-pulse + bg-neutral-100 dark:bg-neutral-800 + rounded * animate-pulse + bg-neutral-800 + rounded
*/ */
const SK = 'animate-pulse bg-neutral-100 dark:bg-neutral-800' const SK = 'animate-pulse bg-neutral-800'
export { useMinimumLoading, useSkeletonFade } from './useMinimumLoading' export { useMinimumLoading, useSkeletonFade } from './useMinimumLoading'
@@ -71,7 +71,7 @@ export function TableSkeleton({ rows = 7, cols = 5 }: { rows?: number; cols?: nu
export function WidgetSkeleton() { export function WidgetSkeleton() {
return ( return (
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col"> <div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<SkeletonLine className="h-6 w-32" /> <SkeletonLine className="h-6 w-32" />
<div className="flex gap-1"> <div className="flex gap-1">
@@ -90,7 +90,7 @@ export function WidgetSkeleton() {
export function StatCardSkeleton() { export function StatCardSkeleton() {
return ( return (
<div className="p-4 rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900"> <div className="p-4 rounded-xl border border-neutral-800 bg-white dark:bg-neutral-900">
<SkeletonLine className="h-4 w-20 mb-2" /> <SkeletonLine className="h-4 w-20 mb-2" />
<SkeletonLine className="h-8 w-28" /> <SkeletonLine className="h-8 w-28" />
</div> </div>
@@ -101,7 +101,7 @@ export function StatCardSkeleton() {
export function ChartSkeleton() { export function ChartSkeleton() {
return ( return (
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6"> <div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-6">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div className="flex gap-4"> <div className="flex gap-4">
{Array.from({ length: 4 }).map((_, i) => ( {Array.from({ length: 4 }).map((_, i) => (
@@ -157,7 +157,7 @@ export function DashboardSkeleton() {
{/* Campaigns table */} {/* Campaigns table */}
<div className="mb-8"> <div className="mb-8">
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6"> <div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-6">
<SkeletonLine className="h-6 w-32 mb-4" /> <SkeletonLine className="h-6 w-32 mb-4" />
<TableSkeleton rows={7} cols={5} /> <TableSkeleton rows={7} cols={5} />
</div> </div>
@@ -187,7 +187,7 @@ export function JourneysSkeleton() {
{/* Sankey area */} {/* Sankey area */}
<SkeletonCard className="h-[400px] mb-6" /> <SkeletonCard className="h-[400px] mb-6" />
{/* Top paths table */} {/* Top paths table */}
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6"> <div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-6">
<SkeletonLine className="h-6 w-24 mb-4" /> <SkeletonLine className="h-6 w-24 mb-4" />
<TableSkeleton rows={5} cols={4} /> <TableSkeleton rows={5} cols={4} />
</div> </div>
@@ -200,28 +200,49 @@ export function JourneysSkeleton() {
export function UptimeSkeleton() { export function UptimeSkeleton() {
return ( return (
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8"> <div className="w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8">
<div className="mb-8"> {/* Header */}
<SkeletonLine className="h-4 w-32 mb-2" /> <div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<SkeletonLine className="h-8 w-24 mb-1" /> <div>
<SkeletonLine className="h-4 w-64" /> <SkeletonLine className="h-8 w-24 mb-2" />
<SkeletonLine className="h-4 w-64" />
</div>
<SkeletonLine className="h-9 w-36 rounded-lg" />
</div> </div>
{/* Overall status */} {/* Overall status card */}
<SkeletonCard className="h-20 mb-6" /> <div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-5 mb-6">
{/* Monitor cards */} <div className="flex items-center justify-between">
<div className="space-y-4"> <div className="flex items-center gap-3">
{Array.from({ length: 3 }).map((_, i) => ( <SkeletonCircle className="w-3.5 h-3.5" />
<div key={i} className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-5 space-y-4"> <SkeletonLine className="h-5 w-32" />
<div className="flex items-center justify-between"> <SkeletonLine className="h-4 w-20" />
<div className="flex items-center gap-3">
<SkeletonCircle className="w-3 h-3" />
<SkeletonLine className="h-5 w-32" />
<SkeletonLine className="h-4 w-48 hidden sm:block" />
</div>
<SkeletonLine className="h-4 w-28" />
</div>
<SkeletonLine className="h-8 w-full rounded-sm" />
</div> </div>
))} <div className="space-y-1 text-right">
<SkeletonLine className="h-4 w-24 ml-auto" />
<SkeletonLine className="h-3 w-32 ml-auto" />
</div>
</div>
</div>
{/* 90-day uptime bar */}
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-5 mb-6">
<SkeletonLine className="h-3 w-28 mb-3" />
<SkeletonLine className="h-6 w-full rounded-sm" />
<div className="flex justify-between mt-1.5">
<SkeletonLine className="h-3 w-16" />
<SkeletonLine className="h-3 w-10" />
</div>
</div>
{/* Monitor details + chart + checks */}
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-5">
{/* 4-col detail grid */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-5">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="space-y-1">
<SkeletonLine className="h-3 w-20" />
<SkeletonLine className="h-4 w-16" />
</div>
))}
</div>
<ChecksSkeleton />
</div> </div>
</div> </div>
) )
@@ -263,7 +284,7 @@ export function FunnelsListSkeleton() {
</div> </div>
<div className="grid gap-4"> <div className="grid gap-4">
{Array.from({ length: 3 }).map((_, i) => ( {Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6"> <div key={i} className="bg-neutral-900 border border-neutral-800 rounded-2xl p-6">
<SkeletonLine className="h-6 w-40 mb-2" /> <SkeletonLine className="h-6 w-40 mb-2" />
<SkeletonLine className="h-4 w-64 mb-4" /> <SkeletonLine className="h-4 w-64 mb-4" />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -308,7 +329,7 @@ export function NotificationsListSkeleton() {
return ( return (
<div className="space-y-3"> <div className="space-y-3">
{Array.from({ length: 6 }).map((_, i) => ( {Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="flex items-start gap-3 p-4 rounded-xl border border-neutral-200 dark:border-neutral-800"> <div key={i} className="flex items-start gap-3 p-4 rounded-xl border border-neutral-800">
<SkeletonCircle className="h-10 w-10 shrink-0" /> <SkeletonCircle className="h-10 w-10 shrink-0" />
<div className="flex-1 space-y-2"> <div className="flex-1 space-y-2">
<SkeletonLine className="h-4 w-3/4" /> <SkeletonLine className="h-4 w-3/4" />
@@ -343,7 +364,7 @@ export function GoalsListSkeleton() {
return ( return (
<div className="space-y-2"> <div className="space-y-2">
{Array.from({ length: 3 }).map((_, i) => ( {Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="flex items-center justify-between py-3 px-4 rounded-2xl border border-neutral-200 dark:border-neutral-800"> <div key={i} className="flex items-center justify-between py-3 px-4 rounded-2xl border border-neutral-800">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<SkeletonLine className="h-4 w-24" /> <SkeletonLine className="h-4 w-24" />
<SkeletonLine className="h-3 w-20" /> <SkeletonLine className="h-3 w-20" />
@@ -387,7 +408,7 @@ export function BehaviorSkeleton() {
{/* Summary cards (3 cols) */} {/* Summary cards (3 cols) */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8">
{Array.from({ length: 3 }).map((_, i) => ( {Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 space-y-3"> <div key={i} className="bg-neutral-900 border border-neutral-800 rounded-2xl p-6 space-y-3">
<SkeletonLine className="h-4 w-24" /> <SkeletonLine className="h-4 w-24" />
<SkeletonLine className="h-8 w-16" /> <SkeletonLine className="h-8 w-16" />
<SkeletonLine className="h-3 w-32" /> <SkeletonLine className="h-3 w-32" />
@@ -403,7 +424,7 @@ export function BehaviorSkeleton() {
{/* By-page table */} {/* By-page table */}
<div className="mb-8"> <div className="mb-8">
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6"> <div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-6">
<SkeletonLine className="h-6 w-40 mb-2" /> <SkeletonLine className="h-6 w-40 mb-2" />
<SkeletonLine className="h-4 w-64 mb-4" /> <SkeletonLine className="h-4 w-64 mb-4" />
<TableSkeleton rows={5} cols={4} /> <TableSkeleton rows={5} cols={4} />