Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/components/sections/activity-feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -126,8 +127,16 @@ export function ActivityFeed({
</div>

{/* Activity List */}
{filteredItems.length === 0 && !loading ? (
<p className="text-sm text-muted-foreground py-8 text-center">No recent activity</p>
{loading ? (
<div className="flex items-center justify-center p-8" role="status" aria-label="Loading activity">
<Loader2 className="w-6 h-6 animate-spin text-primary" />
</div>
) : filteredItems.length === 0 ? (
<EmptyState
icon={Bell}
title="No recent activity"
description="Check back later or follow more creators to see their latest updates and announcements."
/>
) : (
<div className="space-y-3">
{filteredItems.map((item) => (
Expand Down
8 changes: 5 additions & 3 deletions src/components/sections/top-tippers-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -13,9 +14,10 @@ export interface TopTippersTableProps {
export function TopTippersTable({ data }: TopTippersTableProps): JSX.Element {
if (data.length === 0) {
return (
<div className="flex items-center justify-center py-12 text-sm text-muted-foreground">
No tippers for this period
</div>
<EmptyState
title="No tippers for this period"
description="Share your creator link or QR code with your community to start receiving tips."
/>
);
}

Expand Down
46 changes: 46 additions & 0 deletions src/components/ui/empty-state.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<EmptyState
title="No transactions yet"
description="Send a tip or link your wallet to see your transaction history."
/>
);

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(
<EmptyState
icon={Inbox}
title="Empty inbox"
description="No new notifications at this time."
/>
);

expect(screen.getByTestId('empty-state')).toBeInTheDocument();
expect(screen.getByText('Empty inbox')).toBeInTheDocument();
});

it('renders optional action button', () => {
render(
<EmptyState
title="No items found"
action={<Button size="sm"><Plus className="w-4 h-4 mr-1" />Create New</Button>}
/>
);

expect(screen.getByRole('button', { name: /create new/i })).toBeInTheDocument();
});
});
38 changes: 38 additions & 0 deletions src/components/ui/empty-state.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement> {
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 (
<Card
className={cn('flex flex-col items-center justify-center p-8 text-center border-dashed', className)}
data-testid="empty-state"
role="status"
{...props}
>
{Icon && (
<div className="w-12 h-12 rounded-full bg-muted flex items-center justify-center mb-3 text-muted-foreground">
<Icon className="w-6 h-6" />
</div>
)}
<h3 className="font-semibold text-base mb-1 text-foreground">{title}</h3>
{description && <p className="text-sm text-muted-foreground max-w-sm mb-4">{description}</p>}
{action && <div className="mt-1">{action}</div>}
</Card>
);
}
4 changes: 4 additions & 0 deletions src/components/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,7 @@ export {
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
} from './dropdown-menu';

export { EmptyState } from './empty-state';
export type { EmptyStateProps } from './empty-state';

Loading