diff --git a/src/components/sections/creator-collaborations.tsx b/src/components/sections/creator-collaborations.tsx
index 8230987..14f3969 100644
--- a/src/components/sections/creator-collaborations.tsx
+++ b/src/components/sections/creator-collaborations.tsx
@@ -59,6 +59,9 @@ export function CreatorCollaborations({
useEffect(() => {
let active = true;
+ if (typeof client?.getCreatorProfile !== 'function') {
+ return;
+ }
client
.getCreatorProfile(username)
.then((profile) => {
diff --git a/src/components/sections/creator-commission-requests.tsx b/src/components/sections/creator-commission-requests.tsx
index d9e7607..4b7f6a6 100644
--- a/src/components/sections/creator-commission-requests.tsx
+++ b/src/components/sections/creator-commission-requests.tsx
@@ -41,6 +41,9 @@ export function CreatorCommissionRequests({
useEffect(() => {
let active = true;
+ if (typeof client?.getCreatorProfile !== 'function') {
+ return;
+ }
client
.getCreatorProfile(username)
.then((profile) => {
diff --git a/src/components/sections/top-tippers-table.test.tsx b/src/components/sections/top-tippers-table.test.tsx
index dbf4e5c..e236b50 100644
--- a/src/components/sections/top-tippers-table.test.tsx
+++ b/src/components/sections/top-tippers-table.test.tsx
@@ -46,4 +46,11 @@ describe('TopTippersTable', () => {
expect(screen.queryByTestId('top-tippers-table')).not.toBeInTheDocument();
expect(screen.getByText(/no tippers for this period/i)).toBeInTheDocument();
});
+
+ it('renders skeleton placeholder when loading is true', () => {
+ render(
);
+
+ expect(screen.getByRole('status', { name: /loading top tippers/i })).toBeInTheDocument();
+ expect(screen.queryByText(/no tippers for this period/i)).not.toBeInTheDocument();
+ });
});
diff --git a/src/components/sections/top-tippers-table.tsx b/src/components/sections/top-tippers-table.tsx
index 6359a70..8163795 100644
--- a/src/components/sections/top-tippers-table.tsx
+++ b/src/components/sections/top-tippers-table.tsx
@@ -5,12 +5,18 @@
import type { TopTipper } from '@/types';
import { formatCurrency, formatDate } from '@/utils/formatters';
+import { TopTippersTableSkeleton } from '@/components/shared/creator-skeletons';
export interface TopTippersTableProps {
data: TopTipper[];
+ loading?: boolean;
}
-export function TopTippersTable({ data }: TopTippersTableProps): JSX.Element {
+export function TopTippersTable({ data, loading = false }: TopTippersTableProps): JSX.Element {
+ if (loading) {
+ return
;
+ }
+
if (data.length === 0) {
return (
diff --git a/src/components/sections/transaction-filter-bar.tsx b/src/components/sections/transaction-filter-bar.tsx
index 1c8c921..73a0935 100644
--- a/src/components/sections/transaction-filter-bar.tsx
+++ b/src/components/sections/transaction-filter-bar.tsx
@@ -181,7 +181,7 @@ export function TransactionFilterBar({
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-primary text-primary-foreground rounded hover:bg-primary/90 transition disabled:opacity-50 disabled:cursor-not-allowed"
>
- CSV ({resultCount})
+ Export CSV ({resultCount})
{onExportExcel && (
);
}
+
+/**
+ * Matches the top tippers table: header row (Rank, Tipper, Total Tipped, Tips, Last Tip, Latest Message)
+ * plus `rows` body rows, one skeleton block per column.
+ */
+export function TopTippersTableSkeleton({ rows = 5 }: { rows?: number }): JSX.Element {
+ return (
+
+
+
+
+ | Rank |
+ Tipper |
+ Total Tipped |
+ Tips |
+ Last Tip |
+ Latest Message |
+
+
+
+ {Array.from({ length: rows }).map((_, i) => (
+
+ |
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+ ))}
+
+
+
+ );
+}
+
+/**
+ * Matches the activity feed item layout: circular icon/avatar + title + badge + description + timestamp.
+ */
+export function ActivityFeedSkeleton({ count = 3 }: { count?: number }): JSX.Element {
+ return (
+
+ {Array.from({ length: count }).map((_, i) => (
+
+ ))}
+
+ );
+}
diff --git a/src/components/shared/skeleton.tsx b/src/components/shared/skeleton.tsx
index 3edf129..ed7eb54 100644
--- a/src/components/shared/skeleton.tsx
+++ b/src/components/shared/skeleton.tsx
@@ -59,3 +59,44 @@ export function SkeletonGrid({ count = 6, cols = 3, ...props }: SkeletonGridProp
);
}
+
+/**
+ * Skeleton List Item
+ * Reusable placeholder for avatar/icon + multi-line list rows
+ */
+export interface SkeletonListItemProps extends React.HTMLAttributes