diff --git a/src/components/leaderboard/LeaderboardTable.tsx b/src/components/leaderboard/LeaderboardTable.tsx index 7948d6c..2590daa 100644 --- a/src/components/leaderboard/LeaderboardTable.tsx +++ b/src/components/leaderboard/LeaderboardTable.tsx @@ -1,5 +1,6 @@ 'use client'; +import { Trophy, AlertCircle, RefreshCw } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { LeaderboardEntry } from '@/types'; @@ -7,6 +8,9 @@ interface LeaderboardTableProps { entries: LeaderboardEntry[]; currentUserId?: string | null; currentUserEntry?: LeaderboardEntry | null; + isLoading?: boolean; + error?: string | null; + onRetry?: () => void; } const MEDAL: Record = { @@ -138,6 +142,9 @@ export function LeaderboardTable({ entries, currentUserId, currentUserEntry, + isLoading = false, + error = null, + onRetry, }: LeaderboardTableProps) { const isCurrentUser = (entry: LeaderboardEntry) => !!currentUserId && entry.userId === currentUserId; @@ -147,6 +154,47 @@ export function LeaderboardTable({ ? entries.some((e) => e.userId === currentUserEntry.userId) : false; + if (isLoading) { + return ( +
+ {Array.from({ length: 5 }).map((_, idx) => ( +
+ ))} +
+ ); + } + + if (error) { + return ( +
+ +
+

Unable to load leaderboard

+

{error}

+
+ {onRetry && ( + + )} +
+ ); + } + return (
@@ -216,8 +264,15 @@ export function LeaderboardTable({
{entries.length === 0 && ( -
- No data for this period yet. Keep learning! +
+ +

No rankings for this period yet

+

+ No data for this period yet. Keep learning to claim the top spot! +

)}
diff --git a/src/components/leaderboard/__tests__/LeaderboardTable.test.tsx b/src/components/leaderboard/__tests__/LeaderboardTable.test.tsx new file mode 100644 index 0000000..f7a3dc4 --- /dev/null +++ b/src/components/leaderboard/__tests__/LeaderboardTable.test.tsx @@ -0,0 +1,33 @@ +// @vitest-environment jsdom +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { LeaderboardTable } from '../LeaderboardTable'; + +describe('LeaderboardTable empty, loading, and error states (#206)', () => { + it('renders loading skeleton when isLoading is true', () => { + render(); + expect(screen.getByTestId('leaderboard-loading')).toBeInTheDocument(); + }); + + it('renders error state with retry callback when error is provided', () => { + const onRetry = vi.fn(); + render( + + ); + expect(screen.getByTestId('leaderboard-error')).toBeInTheDocument(); + expect(screen.getByText('Could not fetch leaderboard rankings')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: /Retry/i })); + expect(onRetry).toHaveBeenCalledTimes(1); + }); + + it('renders empty state when entries is empty', () => { + render(); + expect(screen.getByTestId('leaderboard-empty')).toBeInTheDocument(); + expect(screen.getByText('No rankings for this period yet')).toBeInTheDocument(); + }); +});