From 2b805ba04a2f32b4e5ccb2f8c1ea0d2b7531e866 Mon Sep 17 00:00:00 2001 From: devsimze Date: Tue, 26 May 2026 17:06:10 +0100 Subject: [PATCH] fix profile, empty-state, and activity fallbacks Add narrow UI fallbacks so long profile subtitles, empty-state illustrations, missing follower counts, and invalid recent activity timestamps render predictably without changing unrelated behavior. Co-authored-by: Cursor --- .../common/CompactEmptyWalletState.tsx | 15 ++++++++-- .../common/CreatorProfileHeader.tsx | 12 +++++++- .../common/CreatorProfileInfoGrid.tsx | 2 ++ .../common/CreatorProfileStatItem.tsx | 7 +++++ src/components/common/EmptyState.tsx | 13 ++++++-- .../common/EmptyTransactionTimelineState.tsx | 28 ++++++++++++----- .../__tests__/CreatorProfileStatItem.test.tsx | 29 ++++++++++++++++++ .../common/emptyStateIllustration.config.ts | 8 +++++ src/pages/LandingPage.tsx | 15 ++++++++++ .../recentActivityTimestamp.utils.test.ts | 30 +++++++++++++++++++ src/utils/recentActivityTimestamp.utils.ts | 18 +++++++++++ 11 files changed, 165 insertions(+), 12 deletions(-) create mode 100644 src/components/common/__tests__/CreatorProfileStatItem.test.tsx create mode 100644 src/components/common/emptyStateIllustration.config.ts create mode 100644 src/utils/__tests__/recentActivityTimestamp.utils.test.ts create mode 100644 src/utils/recentActivityTimestamp.utils.ts diff --git a/src/components/common/CompactEmptyWalletState.tsx b/src/components/common/CompactEmptyWalletState.tsx index b25d5c6..e10628f 100644 --- a/src/components/common/CompactEmptyWalletState.tsx +++ b/src/components/common/CompactEmptyWalletState.tsx @@ -1,5 +1,6 @@ import { WalletMinimal } from 'lucide-react'; import { cn } from '@/lib/utils'; +import { EMPTY_STATE_ILLUSTRATION_SIZES } from './emptyStateIllustration.config'; interface CompactEmptyWalletStateProps { title?: string; @@ -19,8 +20,18 @@ const CompactEmptyWalletState: React.FC = ({ className )} > -
- +
+

{title}

diff --git a/src/components/common/CreatorProfileHeader.tsx b/src/components/common/CreatorProfileHeader.tsx index d8a449e..06e193b 100644 --- a/src/components/common/CreatorProfileHeader.tsx +++ b/src/components/common/CreatorProfileHeader.tsx @@ -17,6 +17,9 @@ interface CreatorProfileHeaderProps { className?: string; } +const CREATOR_PROFILE_SUBTITLE_WRAP_CLASS_NAME = + 'max-w-full whitespace-normal break-words [overflow-wrap:anywhere]'; + const CreatorProfileHeader: React.FC = ({ name, handle, @@ -77,7 +80,14 @@ const CreatorProfileHeader: React.FC = ({ {isVerified &&
}
-

@{handle}

+

+ @{handle} +

diff --git a/src/components/common/CreatorProfileInfoGrid.tsx b/src/components/common/CreatorProfileInfoGrid.tsx index db1b362..28924b6 100644 --- a/src/components/common/CreatorProfileInfoGrid.tsx +++ b/src/components/common/CreatorProfileInfoGrid.tsx @@ -5,6 +5,7 @@ import CreatorProfileStatItem from './CreatorProfileStatItem'; interface CreatorProfileInfoItem { label: string; value: ReactNode; + helperText?: ReactNode; } interface CreatorProfileInfoGridProps { @@ -28,6 +29,7 @@ const CreatorProfileInfoGrid: React.FC = ({ key={item.label} label={item.label} value={item.value} + helperText={item.helperText} /> ))} diff --git a/src/components/common/CreatorProfileStatItem.tsx b/src/components/common/CreatorProfileStatItem.tsx index 96f8278..57d33c5 100644 --- a/src/components/common/CreatorProfileStatItem.tsx +++ b/src/components/common/CreatorProfileStatItem.tsx @@ -4,12 +4,14 @@ import { cn } from '@/lib/utils'; interface CreatorProfileStatItemProps { label: string; value: ReactNode; + helperText?: ReactNode; className?: string; } const CreatorProfileStatItem: React.FC = ({ label, value, + helperText, className, }) => { return ( @@ -26,6 +28,11 @@ const CreatorProfileStatItem: React.FC = ({
{value}
+ {helperText && ( +

+ {helperText} +

+ )} ); }; diff --git a/src/components/common/EmptyState.tsx b/src/components/common/EmptyState.tsx index 81694f8..c134941 100644 --- a/src/components/common/EmptyState.tsx +++ b/src/components/common/EmptyState.tsx @@ -1,6 +1,7 @@ import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { RotateCcw } from 'lucide-react'; +import { EMPTY_STATE_ILLUSTRATION_SIZES } from './emptyStateIllustration.config'; interface EmptyStateProps { image: string; @@ -26,11 +27,19 @@ const EmptyState: React.FC = ({ role="status" aria-label={title} > -
+
diff --git a/src/components/common/EmptyTransactionTimelineState.tsx b/src/components/common/EmptyTransactionTimelineState.tsx index 5dd7788..3cd035f 100644 --- a/src/components/common/EmptyTransactionTimelineState.tsx +++ b/src/components/common/EmptyTransactionTimelineState.tsx @@ -1,6 +1,9 @@ import { useState } from 'react'; import { Button } from '@/components/ui/button'; import { Check, Clock3, Copy, XCircle } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { EMPTY_STATE_ILLUSTRATION_SIZES } from './emptyStateIllustration.config'; +import { formatRecentActivityCompactTimestamp } from '@/utils/recentActivityTimestamp.utils'; type CopyState = 'idle' | 'success' | 'error'; @@ -9,7 +12,7 @@ interface TimelineEntry { action: string; amount: string; txHash: string; - timeLabel: string; + compactTimestamp?: string | null; status: 'confirmed' | 'pending' | 'failed'; } @@ -19,7 +22,7 @@ const TIMELINE_ENTRIES: TimelineEntry[] = [ action: 'Buy', amount: '+2 keys', txHash: '0x2a43bcfdef77ca4c50ef7d38148dd5d7f0149a6e2e20f70f04ce1f4b66fe55dd', - timeLabel: '2m ago', + compactTimestamp: '2m ago', status: 'confirmed', }, { @@ -27,7 +30,7 @@ const TIMELINE_ENTRIES: TimelineEntry[] = [ action: 'Sell', amount: '-1 key', txHash: '0x90c82ac01478b42fcbf9db73a26ed32bd8e50a8917e2408c31c95e9f6a59fc19', - timeLabel: '18m ago', + compactTimestamp: '18m ago', status: 'pending', }, { @@ -35,7 +38,7 @@ const TIMELINE_ENTRIES: TimelineEntry[] = [ action: 'Buy', amount: '+3 keys', txHash: '0x16d2ffbc4297a8c2c3086e07c16e66f47287df0d5a1ce1aef9e448e2f0f3ab51', - timeLabel: '51m ago', + compactTimestamp: '51m ago', status: 'failed', }, ]; @@ -70,8 +73,15 @@ const EmptyTransactionTimelineState: React.FC = () => { Recent trade events with quick copy access for tx hashes.

-
- +
+
@@ -138,7 +148,11 @@ const EmptyTransactionTimelineState: React.FC = () => {

{entry.status}

-

{entry.timeLabel}

+

+ {formatRecentActivityCompactTimestamp( + entry.compactTimestamp + )} +

); diff --git a/src/components/common/__tests__/CreatorProfileStatItem.test.tsx b/src/components/common/__tests__/CreatorProfileStatItem.test.tsx new file mode 100644 index 0000000..f929f1a --- /dev/null +++ b/src/components/common/__tests__/CreatorProfileStatItem.test.tsx @@ -0,0 +1,29 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import CreatorProfileStatItem from '../CreatorProfileStatItem'; + +describe('CreatorProfileStatItem', () => { + it('renders helper text beneath the value when provided', () => { + render( + + ); + + expect(screen.getByText('Followers')).toBeInTheDocument(); + expect(screen.getByText('Not available')).toBeInTheDocument(); + expect( + screen.getByText('Follower count not available yet.') + ).toBeInTheDocument(); + }); + + it('omits helper text when not provided', () => { + render(); + + expect( + screen.queryByText('Follower count not available yet.') + ).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/common/emptyStateIllustration.config.ts b/src/components/common/emptyStateIllustration.config.ts new file mode 100644 index 0000000..f659ebf --- /dev/null +++ b/src/components/common/emptyStateIllustration.config.ts @@ -0,0 +1,8 @@ +export const EMPTY_STATE_ILLUSTRATION_SIZES = { + heroFrame: 'size-36 sm:size-40 md:size-44', + heroVisual: 'size-full object-contain', + compactBadgeFrame: 'size-8 shrink-0', + compactBadgeIcon: 'size-4', + panelBadgeFrame: 'size-11 shrink-0', + panelBadgeIcon: 'size-5', +} as const; diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx index da70665..9a55cb0 100644 --- a/src/pages/LandingPage.tsx +++ b/src/pages/LandingPage.tsx @@ -35,6 +35,8 @@ const FEATURED_CREATOR_FACTS = [ { label: 'Community', value: 'Private behind-the-scenes notes' }, ]; +const FEATURED_CREATOR_FOLLOWER_COUNT: number | null = null; + // Fallback demo data in case API fails const DEMO_CREATORS: Course[] = [ { @@ -598,6 +600,19 @@ function LandingPage() { { + it('preserves supported compact timestamp labels', () => { + expect(formatRecentActivityCompactTimestamp('2m ago')).toBe('2m ago'); + expect(formatRecentActivityCompactTimestamp('18 min ago')).toBe( + '18 min ago' + ); + expect(formatRecentActivityCompactTimestamp('just now')).toBe('just now'); + }); + + it('falls back when the compact timestamp input is missing', () => { + expect(formatRecentActivityCompactTimestamp(undefined)).toBe( + 'Time unavailable' + ); + expect(formatRecentActivityCompactTimestamp(' ')).toBe( + 'Time unavailable' + ); + }); + + it('falls back when the compact timestamp input is invalid', () => { + expect(formatRecentActivityCompactTimestamp('yesterday')).toBe( + 'Time unavailable' + ); + expect(formatRecentActivityCompactTimestamp('18 minutes')).toBe( + 'Time unavailable' + ); + }); +}); diff --git a/src/utils/recentActivityTimestamp.utils.ts b/src/utils/recentActivityTimestamp.utils.ts new file mode 100644 index 0000000..da5d126 --- /dev/null +++ b/src/utils/recentActivityTimestamp.utils.ts @@ -0,0 +1,18 @@ +const COMPACT_RECENT_ACTIVITY_TIMESTAMP_PATTERN = + /^(just now|\d+\s?(?:s|sec|secs|m|min|mins|h|hr|hrs|d|day|days)\s+ago)$/i; + +const RECENT_ACTIVITY_TIMESTAMP_FALLBACK = 'Time unavailable'; + +export function formatRecentActivityCompactTimestamp( + compactTimestamp: string | null | undefined +): string { + const normalized = compactTimestamp?.trim(); + + if (!normalized) { + return RECENT_ACTIVITY_TIMESTAMP_FALLBACK; + } + + return COMPACT_RECENT_ACTIVITY_TIMESTAMP_PATTERN.test(normalized) + ? normalized + : RECENT_ACTIVITY_TIMESTAMP_FALLBACK; +}