diff --git a/docs/ClaimActionHelperText-DisabledReason.md b/docs/ClaimActionHelperText-DisabledReason.md new file mode 100644 index 00000000..f9179bb1 --- /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/components/common/CreatorSkeleton.tsx b/src/components/common/CreatorSkeleton.tsx index b548bff5..1a0c3f8e 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 00000000..398ec684 --- /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 2fb69cfc..80b9c4d9 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; + } } diff --git a/src/utils/__tests__/claimActionDisabledReason.test.ts b/src/utils/__tests__/claimActionDisabledReason.test.ts new file mode 100644 index 00000000..802cabc9 --- /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 00000000..625b302e --- /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];