Skip to content
Merged
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
41 changes: 41 additions & 0 deletions docs/ClaimActionHelperText-DisabledReason.md
Original file line number Diff line number Diff line change
@@ -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
```
63 changes: 13 additions & 50 deletions src/components/common/CreatorSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -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<CreatorSkeletonProps> = ({ className }) => {
return (
<div
Expand All @@ -13,63 +15,24 @@ const CreatorSkeleton: React.FC<CreatorSkeletonProps> = ({ className }) => {
className
)}
>
<motion.div
animate={{ opacity: [0.4, 0.7, 0.4] }}
transition={{ duration: 1.5, repeat: Infinity, ease: 'easeInOut' }}
className="mb-4 aspect-square w-full rounded-xl bg-white/10"
<div
className={cn(
'mb-4 aspect-square w-full rounded-xl',
skeletonBlockClass
)}
/>

<div className="mb-4 space-y-2">
<motion.div
animate={{ opacity: [0.4, 0.7, 0.4] }}
transition={{
duration: 1.5,
repeat: Infinity,
ease: 'easeInOut',
}}
className="h-6 w-3/4 rounded-md bg-white/10"
/>
<motion.div
animate={{ opacity: [0.4, 0.7, 0.4] }}
transition={{
duration: 1.5,
repeat: Infinity,
ease: 'easeInOut',
}}
className="h-4 w-1/2 rounded-md bg-white/10"
/>
<div className={cn('h-6 w-3/4', skeletonBlockClass)} />
<div className={cn('h-4 w-1/2', skeletonBlockClass)} />
</div>

<div className="flex items-center justify-between">
<div className="space-y-1">
<motion.div
animate={{ opacity: [0.4, 0.7, 0.4] }}
transition={{
duration: 1.5,
repeat: Infinity,
ease: 'easeInOut',
}}
className="h-3 w-12 rounded bg-white/10"
/>
<motion.div
animate={{ opacity: [0.4, 0.7, 0.4] }}
transition={{
duration: 1.5,
repeat: Infinity,
ease: 'easeInOut',
}}
className="h-6 w-16 rounded bg-white/10"
/>
<div className={cn('h-3 w-12', skeletonBlockClass)} />
<div className={cn('h-6 w-16', skeletonBlockClass)} />
</div>
<motion.div
animate={{ opacity: [0.4, 0.7, 0.4] }}
transition={{
duration: 1.5,
repeat: Infinity,
ease: 'easeInOut',
}}
className="h-9 w-24 rounded-xl bg-white/10"
/>
<div className={cn('h-9 w-24 rounded-xl', skeletonBlockClass)} />
</div>
</div>
);
Expand Down
19 changes: 19 additions & 0 deletions src/components/common/__tests__/CreatorSkeleton.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<CreatorSkeleton />);
const shimmerBlocks = container.querySelectorAll('.skeleton-shimmer');

expect(shimmerBlocks).toHaveLength(6);
});

it('renders the requested number of cards in grid skeleton', () => {
const { container } = render(<CreatorGridSkeleton count={3} />);
const shimmerBlocks = container.querySelectorAll('.skeleton-shimmer');

expect(shimmerBlocks).toHaveLength(18);
});
});
33 changes: 33 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
40 changes: 40 additions & 0 deletions src/utils/__tests__/claimActionDisabledReason.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
26 changes: 26 additions & 0 deletions src/utils/claimActionDisabledReason.ts
Original file line number Diff line number Diff line change
@@ -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<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.',
};

export const getClaimActionDisabledReasonText = (
reason: ClaimActionDisabledReasonKey
): string => CLAIM_ACTION_DISABLED_REASON_TEXT[reason];
Loading