From d8db666cd456631fb18e498aae65c7fad607a1eb Mon Sep 17 00:00:00 2001 From: Okoli Johnpaul Sochimaobi <132228270+Johnpii1@users.noreply.github.com> Date: Sat, 25 Apr 2026 06:41:53 +0100 Subject: [PATCH 1/4] Add reduced-motion fallback for creator skeleton shimmer --- src/components/common/CreatorSkeleton.tsx | 63 ++++--------------- .../common/__tests__/CreatorSkeleton.test.tsx | 19 ++++++ src/index.css | 33 ++++++++++ 3 files changed, 65 insertions(+), 50 deletions(-) create mode 100644 src/components/common/__tests__/CreatorSkeleton.test.tsx diff --git a/src/components/common/CreatorSkeleton.tsx b/src/components/common/CreatorSkeleton.tsx index b548bff..1a0c3f8 100644 --- a/src/components/common/CreatorSkeleton.tsx +++ b/src/components/common/CreatorSkeleton.tsx @@ -1,10 +1,12 @@ -import { motion } from 'framer-motion'; import { cn } from '@/lib/utils'; interface CreatorSkeletonProps { className?: string; } +const skeletonBlockClass = + 'rounded-md bg-white/12 skeleton-shimmer motion-reduce:bg-white/18 motion-reduce:ring-1 motion-reduce:ring-white/15'; + const CreatorSkeleton: React.FC = ({ className }) => { return (
= ({ className }) => { className )} > -
- - +
+
- - +
+
- +
); diff --git a/src/components/common/__tests__/CreatorSkeleton.test.tsx b/src/components/common/__tests__/CreatorSkeleton.test.tsx new file mode 100644 index 0000000..398ec68 --- /dev/null +++ b/src/components/common/__tests__/CreatorSkeleton.test.tsx @@ -0,0 +1,19 @@ +import { render } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import CreatorSkeleton, { CreatorGridSkeleton } from '../CreatorSkeleton'; + +describe('CreatorSkeleton', () => { + it('renders shimmer blocks for loading affordance', () => { + const { container } = render(); + const shimmerBlocks = container.querySelectorAll('.skeleton-shimmer'); + + expect(shimmerBlocks).toHaveLength(6); + }); + + it('renders the requested number of cards in grid skeleton', () => { + const { container } = render(); + const shimmerBlocks = container.querySelectorAll('.skeleton-shimmer'); + + expect(shimmerBlocks).toHaveLength(18); + }); +}); diff --git a/src/index.css b/src/index.css index 2fb69cf..80b9c4d 100644 --- a/src/index.css +++ b/src/index.css @@ -153,4 +153,37 @@ margin-top: var(--spacing-section-major); margin-bottom: var(--spacing-section-major); } + + .skeleton-shimmer { + position: relative; + overflow: hidden; + background-image: linear-gradient( + 110deg, + rgba(255, 255, 255, 0.07) 30%, + rgba(255, 255, 255, 0.2) 45%, + rgba(255, 255, 255, 0.07) 60% + ); + background-size: 220% 100%; + animation: skeleton-shimmer 1.6s linear infinite; + } + + @media (prefers-reduced-motion: reduce) { + .skeleton-shimmer { + animation: none; + background-image: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.16), + rgba(255, 255, 255, 0.08) + ); + } + } +} + +@keyframes skeleton-shimmer { + 0% { + background-position: 100% 0; + } + 100% { + background-position: -100% 0; + } } From ab01722926bc56d72aa26eef4f5a6509b15c296c Mon Sep 17 00:00:00 2001 From: Okoli Johnpaul Sochimaobi <132228270+Johnpii1@users.noreply.github.com> Date: Sat, 25 Apr 2026 18:12:58 +0100 Subject: [PATCH 2/4] Add standardized claim action disabled reason helper text --- docs/ClaimActionHelperText-DisabledReason.md | 41 +++++++++++++++++++ .../claimActionDisabledReason.test.ts | 40 ++++++++++++++++++ src/utils/claimActionDisabledReason.ts | 26 ++++++++++++ 3 files changed, 107 insertions(+) create mode 100644 docs/ClaimActionHelperText-DisabledReason.md create mode 100644 src/utils/__tests__/claimActionDisabledReason.test.ts create mode 100644 src/utils/claimActionDisabledReason.ts diff --git a/docs/ClaimActionHelperText-DisabledReason.md b/docs/ClaimActionHelperText-DisabledReason.md new file mode 100644 index 0000000..f9179bb --- /dev/null +++ b/docs/ClaimActionHelperText-DisabledReason.md @@ -0,0 +1,41 @@ +# Claim Action Disabled Reason Helper Text + +This change introduces a centralized helper for claim action disabled reasons: + +- Utility: `getClaimActionDisabledReasonText` +- File: `src/utils/claimActionDisabledReason.ts` + +## Why + +The helper standardizes claim disabled-reason text so messaging stays consistent and always includes: + +1. **Clear cause** (why claim is disabled) +2. **Next step** (what the user should do) + +This mirrors the tone used in trade action helper messaging. + +## Supported reason keys + +- `wallet_not_connected` +- `no_claimable_rewards` +- `claim_in_progress` +- `network_mismatch` +- `insufficient_gas` +- `unknown` + +## Usage + +```ts +import { getClaimActionDisabledReasonText } from '@/utils/claimActionDisabledReason'; + +const disabledReason = getClaimActionDisabledReasonText('wallet_not_connected'); +// "Claim is unavailable because your wallet is not connected. Connect your wallet to continue." +``` + +## Local validation + +Run: + +```bash +pnpm test src/utils/__tests__/claimActionDisabledReason.test.ts +``` diff --git a/src/utils/__tests__/claimActionDisabledReason.test.ts b/src/utils/__tests__/claimActionDisabledReason.test.ts new file mode 100644 index 0000000..802cabc --- /dev/null +++ b/src/utils/__tests__/claimActionDisabledReason.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest'; +import { + getClaimActionDisabledReasonText, + type ClaimActionDisabledReasonKey, +} from '@/utils/claimActionDisabledReason'; + +describe('getClaimActionDisabledReasonText', () => { + it('returns standardized helper text for each disabled reason', () => { + const cases: Array<[ClaimActionDisabledReasonKey, string]> = [ + [ + 'wallet_not_connected', + 'Claim is unavailable because your wallet is not connected. Connect your wallet to continue.', + ], + [ + 'no_claimable_rewards', + 'Claim is unavailable because there are no rewards ready yet. Check back after a new payout accrues.', + ], + [ + 'claim_in_progress', + 'Claim is unavailable because a claim transaction is already in progress. Wait for confirmation before trying again.', + ], + [ + 'network_mismatch', + 'Claim is unavailable because your wallet is on the wrong network. Switch to the supported network to continue.', + ], + [ + 'insufficient_gas', + 'Claim is unavailable because your wallet balance is too low for network fees. Add funds for gas, then retry.', + ], + [ + 'unknown', + 'Claim is temporarily unavailable due to a validation issue. Refresh and try again, or contact support if it persists.', + ], + ]; + + for (const [reason, expectedText] of cases) { + expect(getClaimActionDisabledReasonText(reason)).toBe(expectedText); + } + }); +}); diff --git a/src/utils/claimActionDisabledReason.ts b/src/utils/claimActionDisabledReason.ts new file mode 100644 index 0000000..625b302 --- /dev/null +++ b/src/utils/claimActionDisabledReason.ts @@ -0,0 +1,26 @@ +export type ClaimActionDisabledReasonKey = + | 'wallet_not_connected' + | 'no_claimable_rewards' + | 'claim_in_progress' + | 'network_mismatch' + | 'insufficient_gas' + | 'unknown'; + +const CLAIM_ACTION_DISABLED_REASON_TEXT: Record = { + wallet_not_connected: + 'Claim is unavailable because your wallet is not connected. Connect your wallet to continue.', + no_claimable_rewards: + 'Claim is unavailable because there are no rewards ready yet. Check back after a new payout accrues.', + claim_in_progress: + 'Claim is unavailable because a claim transaction is already in progress. Wait for confirmation before trying again.', + network_mismatch: + 'Claim is unavailable because your wallet is on the wrong network. Switch to the supported network to continue.', + insufficient_gas: + 'Claim is unavailable because your wallet balance is too low for network fees. Add funds for gas, then retry.', + unknown: + 'Claim is temporarily unavailable due to a validation issue. Refresh and try again, or contact support if it persists.', +}; + +export const getClaimActionDisabledReasonText = ( + reason: ClaimActionDisabledReasonKey +): string => CLAIM_ACTION_DISABLED_REASON_TEXT[reason]; From 9380ec05128907bd4b2c43a8d2d8b5499a07cfbe Mon Sep 17 00:00:00 2001 From: Okoli Johnpaul Sochimaobi <132228270+Johnpii1@users.noreply.github.com> Date: Sat, 25 Apr 2026 18:29:44 +0100 Subject: [PATCH 3/4] Add mobile safe-area inset padding to dialog sheets --- src/components/ui/dialog.test.tsx | 21 +++++++++++++++++++++ src/components/ui/dialog.tsx | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 src/components/ui/dialog.test.tsx diff --git a/src/components/ui/dialog.test.tsx b/src/components/ui/dialog.test.tsx new file mode 100644 index 0000000..4bc6d08 --- /dev/null +++ b/src/components/ui/dialog.test.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import { Dialog, DialogContent } from './dialog'; + +describe('DialogContent', () => { + it('adds mobile safe-area bottom inset padding and restores desktop spacing', () => { + render( + + Dialog body + + ); + + const content = screen.getByRole('dialog'); + expect(content.className).toContain( + 'pb-[calc(1.5rem+env(safe-area-inset-bottom))]' + ); + expect(content.className).toContain('sm:pb-6'); + }); +}); diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index b1244d9..5ee4722 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -59,7 +59,7 @@ function DialogContent({ Date: Sat, 25 Apr 2026 18:46:15 +0100 Subject: [PATCH 4/4] Add deterministic fallback avatar colors by creator id --- src/components/common/CreatorCard.tsx | 1 + .../common/CreatorInitialsAvatar.tsx | 10 ++++++- .../common/CreatorProfileHeader.tsx | 4 ++- .../__tests__/CreatorInitialsAvatar.test.tsx | 30 +++++++++++++++++++ src/pages/LandingPage.tsx | 1 + src/utils/avatarColor.util.ts | 19 ++++++++++++ 6 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 src/components/common/__tests__/CreatorInitialsAvatar.test.tsx create mode 100644 src/utils/avatarColor.util.ts diff --git a/src/components/common/CreatorCard.tsx b/src/components/common/CreatorCard.tsx index 69d2c45..ae4a511 100644 --- a/src/components/common/CreatorCard.tsx +++ b/src/components/common/CreatorCard.tsx @@ -108,6 +108,7 @@ const CreatorCard: React.FC = ({ creator, className }) => {
diff --git a/src/components/common/CreatorInitialsAvatar.tsx b/src/components/common/CreatorInitialsAvatar.tsx index ecb2a16..a48ee74 100644 --- a/src/components/common/CreatorInitialsAvatar.tsx +++ b/src/components/common/CreatorInitialsAvatar.tsx @@ -1,8 +1,10 @@ import { useState } from 'react'; import { cn } from '@/lib/utils'; +import { getFallbackAvatarColors } from '@/utils/avatarColor.util'; interface CreatorInitialsAvatarProps { name: string; + creatorId?: string | number | null; imageSrc?: string; className?: string; imageClassName?: string; @@ -24,20 +26,26 @@ const getInitials = (name: string) => { const CreatorInitialsAvatar: React.FC = ({ name, + creatorId, imageSrc, className, imageClassName, }) => { const [hasError, setHasError] = useState(false); const initials = getInitials(name); + const fallbackColors = getFallbackAvatarColors(creatorId); if (!imageSrc || hasError) { return (
{initials}
diff --git a/src/components/common/CreatorProfileHeader.tsx b/src/components/common/CreatorProfileHeader.tsx index 21d9325..1ef14f2 100644 --- a/src/components/common/CreatorProfileHeader.tsx +++ b/src/components/common/CreatorProfileHeader.tsx @@ -9,6 +9,7 @@ import CreatorInitialsAvatar from '@/components/common/CreatorInitialsAvatar'; interface CreatorProfileHeaderProps { name: string; handle: string; + creatorId?: string | number | null; avatarUrl?: string; isVerified?: boolean; className?: string; @@ -17,6 +18,7 @@ interface CreatorProfileHeaderProps { const CreatorProfileHeader: React.FC = ({ name, handle, + creatorId, avatarUrl, isVerified, className, @@ -44,7 +46,7 @@ const CreatorProfileHeader: React.FC = ({ >
- +
diff --git a/src/components/common/__tests__/CreatorInitialsAvatar.test.tsx b/src/components/common/__tests__/CreatorInitialsAvatar.test.tsx new file mode 100644 index 0000000..5157bd8 --- /dev/null +++ b/src/components/common/__tests__/CreatorInitialsAvatar.test.tsx @@ -0,0 +1,30 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import CreatorInitialsAvatar from '@/components/common/CreatorInitialsAvatar'; +import { getFallbackAvatarColors } from '@/utils/avatarColor.util'; + +describe('CreatorInitialsAvatar', () => { + it('uses deterministic fallback colors for the same creator id', () => { + const first = getFallbackAvatarColors('creator-123'); + const second = getFallbackAvatarColors('creator-123'); + const third = getFallbackAvatarColors('creator-456'); + + expect(first).toEqual(second); + expect(first.background).not.toBe(third.background); + expect(first.textColor).toBe('rgba(255, 255, 255, 0.95)'); + }); + + it('renders initials fallback with hashed background when image is missing', () => { + render(); + + const initials = screen.getByLabelText('Alex Rivers initials avatar'); + const avatar = initials.parentElement; + const colors = getFallbackAvatarColors('creator-123'); + + expect(initials).toHaveTextContent('AR'); + expect(avatar).toHaveStyle({ + background: colors.background, + color: colors.textColor, + }); + }); +}); diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx index 128f585..791a67b 100644 --- a/src/pages/LandingPage.tsx +++ b/src/pages/LandingPage.tsx @@ -479,6 +479,7 @@ function LandingPage() { diff --git a/src/utils/avatarColor.util.ts b/src/utils/avatarColor.util.ts new file mode 100644 index 0000000..44d8472 --- /dev/null +++ b/src/utils/avatarColor.util.ts @@ -0,0 +1,19 @@ +const stringHash = (value: string) => { + let hash = 0; + for (let i = 0; i < value.length; i += 1) { + hash = (hash * 31 + value.charCodeAt(i)) >>> 0; + } + return hash; +}; + +export const getFallbackAvatarColors = (creatorId?: string | number | null) => { + const normalizedId = String(creatorId ?? '').trim(); + const hash = stringHash(normalizedId || 'fallback-avatar'); + const baseHue = hash % 360; + const accentHue = (baseHue + 28) % 360; + + return { + background: `linear-gradient(135deg, hsl(${baseHue} 65% 28%), hsl(${accentHue} 72% 36%))`, + textColor: 'rgba(255, 255, 255, 0.95)', + }; +};