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

@@ -3,30 +3,13 @@
import { formatNumber } from '@ciphera-net/ui'
import { Files } from '@phosphor-icons/react'
import type { FrustrationByPage } from '@/lib/api/stats'
import { TableSkeleton } from '@/components/skeletons'
interface FrustrationByPageTableProps {
pages: FrustrationByPage[]
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) {
const hasData = pages.length > 0
const maxTotal = Math.max(...pages.map(p => p.total), 1)
@@ -43,7 +26,7 @@ export default function FrustrationByPageTable({ pages, loading }: FrustrationBy
</p>
{loading ? (
<SkeletonRows />
<TableSkeleton rows={5} cols={5} />
) : hasData ? (
<div className="overflow-x-auto -mx-6 px-6">
{/* Header */}

View File

@@ -1,6 +1,7 @@
'use client'
import type { FrustrationSummary } from '@/lib/api/stats'
import { StatCardSkeleton } from '@/components/skeletons'
interface FrustrationSummaryCardsProps {
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) {
if (loading || !data) {
return (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8">
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
<StatCardSkeleton />
<StatCardSkeleton />
<StatCardSkeleton />
</div>
)
}

View File

@@ -8,26 +8,13 @@ import {
type ChartConfig,
} from '@/components/charts'
import type { FrustrationSummary } from '@/lib/api/stats'
import { WidgetSkeleton } from '@/components/skeletons'
interface FrustrationTrendProps {
summary: FrustrationSummary | null
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> = {
rage_clicks: 'Rage Clicks',
dead_clicks: 'Dead Clicks',
@@ -70,7 +57,7 @@ function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<
}
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 ||
summary.prev_rage_clicks > 0 || summary.prev_dead_clicks > 0