diff --git a/app/[communitySlug]/admin/analytics/page.tsx b/app/[communitySlug]/admin/analytics/page.tsx index c38723f..fe4a9e8 100644 --- a/app/[communitySlug]/admin/analytics/page.tsx +++ b/app/[communitySlug]/admin/analytics/page.tsx @@ -20,6 +20,7 @@ import { EmptyState, safeErrorMessage, } from "@/components/ui/api-states"; +import { AnalyticsSkeleton } from "@/components/admin/analytics-skeleton"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; @@ -351,7 +352,7 @@ function AnalyticsContent() { {isSessionExpired ? ( ) : isLoading ? ( - + ) : isError && !(isApiError(error) && error.code === "aborted") ? ( ) : isLoading ? ( - + ) : isError && !(isApiError(error) && error.code === "aborted") ? ( + Loading analytics + + {/* Page header */} +
+ + +
+ + + + {/* Stat cards row */} +
+ {[0, 1, 2].map((i) => ( + + ))} +
+ + {/* Membership Growth Chart skeleton */} + + + {/* Two-column distribution grid */} +
+ {[0, 1].map((col) => ( + + ))} +
+ + {/* Generated-at footer */} +
+ +
+ + ); +} \ No newline at end of file diff --git a/components/admin/rewards-skeleton.tsx b/components/admin/rewards-skeleton.tsx new file mode 100644 index 0000000..90c1940 --- /dev/null +++ b/components/admin/rewards-skeleton.tsx @@ -0,0 +1,102 @@ +import { Skeleton } from "@/components/ui/skeleton"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +/** + * Skeleton placeholder for the Rewards admin page. + * + * Mirrors the layout of the loaded page — page header, deferred-features + * notice, member count header, and a list of member reward cards. + */ +export function RewardsSkeleton() { + return ( +
+ Loading rewards + + {/* Page header */} +
+
+ + +
+ +
+ + + + {/* Deferred features notice */} +
+ +
+ {[0, 1, 2, 3].map((i) => ( + + ))} +
+
+ + {/* Member count header */} + + + {/* Member reward cards */} +
+ {[0, 1, 2].map((i) => ( + + ))} +
+ + {/* Implementation notes */} +
+ +
+ {[0, 1, 2].map((i) => ( + + ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/test/skeleton.test.ts b/test/skeleton.test.ts index f2952ff..a82c4b7 100644 --- a/test/skeleton.test.ts +++ b/test/skeleton.test.ts @@ -5,6 +5,8 @@ import React from 'react' import { renderToStaticMarkup } from 'react-dom/server' import { Skeleton } from '../components/ui/skeleton' import { MembershipCardSkeleton } from '../components/dashboard/membership-card-skeleton' +import { AnalyticsSkeleton } from '../components/admin/analytics-skeleton' +import { RewardsSkeleton } from '../components/admin/rewards-skeleton' test('skeleton uses theme tokens and stays hidden from assistive technology', () => { const html = renderToStaticMarkup( @@ -30,3 +32,36 @@ test('membership skeleton reserves the loaded layout and exposes one loading sta assert.match(html, /Loading membership details/) assert.equal((html.match(/aria-hidden="true"/g) ?? []).length, 7) }) + +test('analytics skeleton reserves the analytics layout and exposes one loading status', () => { + const html = renderToStaticMarkup( + React.createElement(AnalyticsSkeleton), + ) + + assert.match(html, /role="status"/) + assert.match(html, /aria-busy="true"/) + assert.match(html, /Loading analytics/) + // Should have stat cards (3 skeleton cards) + assert.equal((html.match(/class="[^"]*rounded-lg border[^"]*"/g) ?? []).length, 6) + // Should have skeleton bars inside each card + assert.ok(html.includes('h-9')) + assert.ok(html.includes('h-2')) + // All inner skeleton elements should be hidden from AT + assert.ok(html.includes('aria-hidden="true"')) +}) + +test('rewards skeleton reserves the rewards layout and exposes one loading status', () => { + const html = renderToStaticMarkup( + React.createElement(RewardsSkeleton), + ) + + assert.match(html, /role="status"/) + assert.match(html, /aria-busy="true"/) + assert.match(html, /Loading rewards/) + // Should have member reward cards + assert.ok(html.includes('rounded-lg border')) + // Should have skeleton elements for roles badges + assert.ok(html.includes('rounded-full')) + // All inner skeleton elements should be hidden from AT + assert.ok(html.includes('aria-hidden="true"')) +}) diff --git a/test/tsconfig.json b/test/tsconfig.json index dd5309b..7e09272 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -20,6 +20,8 @@ "../components/ui/skeleton.tsx", "../components/ui/card.tsx", "../components/dashboard/membership-card-skeleton.tsx", + "../components/admin/analytics-skeleton.tsx", + "../components/admin/rewards-skeleton.tsx", "../lib/api/mappers.ts", "../lib/api/optimistic.ts", "../lib/api/types.ts",