From 40ffe3b722c54e67b2272ec6b4e2c9b3e8d487dd Mon Sep 17 00:00:00 2001 From: Duy Luong <33130695+lmduy2612@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:18:44 +0700 Subject: [PATCH] fix(ux): add helpful empty state messages to list views (#164) --- src/components/sections/activity-feed.tsx | 13 +++++- src/components/sections/top-tippers-table.tsx | 8 ++-- src/components/ui/empty-state.test.tsx | 46 +++++++++++++++++++ src/components/ui/empty-state.tsx | 38 +++++++++++++++ src/components/ui/index.ts | 4 ++ 5 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 src/components/ui/empty-state.test.tsx create mode 100644 src/components/ui/empty-state.tsx diff --git a/src/components/sections/activity-feed.tsx b/src/components/sections/activity-feed.tsx index caca089..b0ef3df 100644 --- a/src/components/sections/activity-feed.tsx +++ b/src/components/sections/activity-feed.tsx @@ -10,6 +10,7 @@ import { ActivityFeedItem, ActivityFeedFilters, ActivityType } from '@/types'; import { formatDateTime, formatCurrency } from '@/utils/formatters'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; +import { EmptyState } from '@/components/ui/empty-state'; import { Bell, Gift, @@ -126,8 +127,16 @@ export function ActivityFeed({ {/* Activity List */} - {filteredItems.length === 0 && !loading ? ( -

No recent activity

+ {loading ? ( +
+ +
+ ) : filteredItems.length === 0 ? ( + ) : (
{filteredItems.map((item) => ( diff --git a/src/components/sections/top-tippers-table.tsx b/src/components/sections/top-tippers-table.tsx index 6359a70..8a993cf 100644 --- a/src/components/sections/top-tippers-table.tsx +++ b/src/components/sections/top-tippers-table.tsx @@ -5,6 +5,7 @@ import type { TopTipper } from '@/types'; import { formatCurrency, formatDate } from '@/utils/formatters'; +import { EmptyState } from '@/components/ui/empty-state'; export interface TopTippersTableProps { data: TopTipper[]; @@ -13,9 +14,10 @@ export interface TopTippersTableProps { export function TopTippersTable({ data }: TopTippersTableProps): JSX.Element { if (data.length === 0) { return ( -
- No tippers for this period -
+ ); } diff --git a/src/components/ui/empty-state.test.tsx b/src/components/ui/empty-state.test.tsx new file mode 100644 index 0000000..968aef1 --- /dev/null +++ b/src/components/ui/empty-state.test.tsx @@ -0,0 +1,46 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { EmptyState } from './empty-state'; +import { Inbox, Plus } from 'lucide-react'; +import { Button } from './button'; + +describe('EmptyState Component (#164)', () => { + it('renders title and description properly', () => { + render( + + ); + + expect(screen.getByRole('status')).toBeInTheDocument(); + expect(screen.getByText('No transactions yet')).toBeInTheDocument(); + expect( + screen.getByText('Send a tip or link your wallet to see your transaction history.') + ).toBeInTheDocument(); + }); + + it('renders custom icon when provided', () => { + render( + + ); + + expect(screen.getByTestId('empty-state')).toBeInTheDocument(); + expect(screen.getByText('Empty inbox')).toBeInTheDocument(); + }); + + it('renders optional action button', () => { + render( + Create New} + /> + ); + + expect(screen.getByRole('button', { name: /create new/i })).toBeInTheDocument(); + }); +}); diff --git a/src/components/ui/empty-state.tsx b/src/components/ui/empty-state.tsx new file mode 100644 index 0000000..23c4adf --- /dev/null +++ b/src/components/ui/empty-state.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { LucideIcon } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { Card } from '@/components/ui/card'; + +export interface EmptyStateProps extends React.HTMLAttributes { + icon?: LucideIcon | React.ComponentType<{ className?: string }>; + title: string; + description?: string; + action?: React.ReactNode; +} + +export function EmptyState({ + icon: Icon, + title, + description, + action, + className, + ...props +}: EmptyStateProps): JSX.Element { + return ( + + {Icon && ( +
+ +
+ )} +

{title}

+ {description &&

{description}

} + {action &&
{action}
} +
+ ); +} diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index 2e3b8c2..7f4a10f 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -47,3 +47,7 @@ export { DropdownMenuSubTrigger, DropdownMenuRadioGroup, } from './dropdown-menu'; + +export { EmptyState } from './empty-state'; +export type { EmptyStateProps } from './empty-state'; +