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:
@@ -868,35 +868,67 @@ function AuditItem({ item }: { item: Record<string, any> }) {
|
||||
// * Skeleton loading state
|
||||
function PageSpeedSkeleton() {
|
||||
return (
|
||||
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8 space-y-6">
|
||||
<div className="animate-pulse space-y-2 mb-8">
|
||||
<div className="h-8 w-48 bg-neutral-200 dark:bg-neutral-700 rounded" />
|
||||
<div className="h-4 w-72 bg-neutral-200 dark:bg-neutral-700 rounded" />
|
||||
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8 space-y-6 animate-pulse">
|
||||
{/* Header — title + subtitle + toggle buttons */}
|
||||
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="h-8 w-36 bg-neutral-700 rounded" />
|
||||
<div className="h-4 w-72 bg-neutral-700 rounded" />
|
||||
</div>
|
||||
{/* Hero 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">
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="w-40 h-40 rounded-full bg-neutral-200 dark:bg-neutral-700 flex-shrink-0" />
|
||||
<div className="flex-1 space-y-3">
|
||||
<div className="h-5 w-32 bg-neutral-200 dark: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 className="flex items-center gap-3">
|
||||
<div className="flex gap-1">
|
||||
<div className="h-8 w-16 bg-neutral-700 rounded" />
|
||||
<div className="h-8 w-20 bg-neutral-700 rounded" />
|
||||
</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>
|
||||
{/* 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">
|
||||
<div className="h-3 w-16 bg-neutral-200 dark:bg-neutral-700 rounded mb-5" />
|
||||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-6">
|
||||
|
||||
{/* Score overview — 4 gauge circles + screenshot */}
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-6 sm:p-8">
|
||||
<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) => (
|
||||
<div key={i} className="space-y-2">
|
||||
<div className="h-3 w-32 bg-neutral-200 dark:bg-neutral-700 rounded" />
|
||||
<div className="h-7 w-20 bg-neutral-200 dark:bg-neutral-700 rounded" />
|
||||
<div key={i} className="flex items-start gap-3">
|
||||
<div className="mt-1.5 w-2.5 h-2.5 rounded-full bg-neutral-700 flex-shrink-0" />
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Reusable skeleton loading primitives and composites for Pulse.
|
||||
* 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'
|
||||
|
||||
@@ -71,7 +71,7 @@ export function TableSkeleton({ rows = 7, cols = 5 }: { rows?: number; cols?: nu
|
||||
|
||||
export function WidgetSkeleton() {
|
||||
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">
|
||||
<SkeletonLine className="h-6 w-32" />
|
||||
<div className="flex gap-1">
|
||||
@@ -90,7 +90,7 @@ export function WidgetSkeleton() {
|
||||
|
||||
export function StatCardSkeleton() {
|
||||
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-8 w-28" />
|
||||
</div>
|
||||
@@ -101,7 +101,7 @@ export function StatCardSkeleton() {
|
||||
|
||||
export function ChartSkeleton() {
|
||||
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 gap-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
@@ -157,7 +157,7 @@ export function DashboardSkeleton() {
|
||||
|
||||
{/* Campaigns table */}
|
||||
<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" />
|
||||
<TableSkeleton rows={7} cols={5} />
|
||||
</div>
|
||||
@@ -187,7 +187,7 @@ export function JourneysSkeleton() {
|
||||
{/* Sankey area */}
|
||||
<SkeletonCard className="h-[400px] mb-6" />
|
||||
{/* 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" />
|
||||
<TableSkeleton rows={5} cols={4} />
|
||||
</div>
|
||||
@@ -200,29 +200,50 @@ export function JourneysSkeleton() {
|
||||
export function UptimeSkeleton() {
|
||||
return (
|
||||
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8">
|
||||
<div className="mb-8">
|
||||
<SkeletonLine className="h-4 w-32 mb-2" />
|
||||
<SkeletonLine className="h-8 w-24 mb-1" />
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<SkeletonLine className="h-8 w-24 mb-2" />
|
||||
<SkeletonLine className="h-4 w-64" />
|
||||
</div>
|
||||
{/* Overall status */}
|
||||
<SkeletonCard className="h-20 mb-6" />
|
||||
{/* Monitor cards */}
|
||||
<div className="space-y-4">
|
||||
{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-5 space-y-4">
|
||||
<SkeletonLine className="h-9 w-36 rounded-lg" />
|
||||
</div>
|
||||
{/* Overall status card */}
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-5 mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<SkeletonCircle className="w-3 h-3" />
|
||||
<SkeletonCircle className="w-3.5 h-3.5" />
|
||||
<SkeletonLine className="h-5 w-32" />
|
||||
<SkeletonLine className="h-4 w-48 hidden sm:block" />
|
||||
<SkeletonLine className="h-4 w-20" />
|
||||
</div>
|
||||
<SkeletonLine className="h-4 w-28" />
|
||||
<div className="space-y-1 text-right">
|
||||
<SkeletonLine className="h-4 w-24 ml-auto" />
|
||||
<SkeletonLine className="h-3 w-32 ml-auto" />
|
||||
</div>
|
||||
<SkeletonLine className="h-8 w-full rounded-sm" />
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -263,7 +284,7 @@ export function FunnelsListSkeleton() {
|
||||
</div>
|
||||
<div className="grid gap-4">
|
||||
{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-4 w-64 mb-4" />
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -308,7 +329,7 @@ export function NotificationsListSkeleton() {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{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" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<SkeletonLine className="h-4 w-3/4" />
|
||||
@@ -343,7 +364,7 @@ export function GoalsListSkeleton() {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{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">
|
||||
<SkeletonLine className="h-4 w-24" />
|
||||
<SkeletonLine className="h-3 w-20" />
|
||||
@@ -387,7 +408,7 @@ export function BehaviorSkeleton() {
|
||||
{/* Summary cards (3 cols) */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8">
|
||||
{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-8 w-16" />
|
||||
<SkeletonLine className="h-3 w-32" />
|
||||
@@ -403,7 +424,7 @@ export function BehaviorSkeleton() {
|
||||
|
||||
{/* By-page table */}
|
||||
<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-4 w-64 mb-4" />
|
||||
<TableSkeleton rows={5} cols={4} />
|
||||
|
||||
Reference in New Issue
Block a user