Skip to content

Commit 24ab04f

Browse files
authored
Merge pull request #71 from ALLEN-AYODEJI/feature/issue-67-market-action-disabled-reason
feat: add disabled reason text to market action component
2 parents 8ba52f4 + f8f55b1 commit 24ab04f

2 files changed

Lines changed: 212 additions & 12 deletions

File tree

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# BuyActionHelperText - Disabled Reason Feature
2+
3+
## Overview
4+
5+
The `BuyActionHelperText` component now supports displaying a disabled reason message when an action cannot be performed. This feature was added in issue-67 to provide users with clear feedback about why a market action is unavailable.
6+
7+
## Usage
8+
9+
### Basic Example
10+
11+
```tsx
12+
import BuyActionHelperText from '@/components/common/BuyActionHelperText';
13+
14+
<BuyActionHelperText
15+
state="idle"
16+
disabledReason="Insufficient balance to complete this purchase"
17+
className="mt-4"
18+
/>
19+
```
20+
21+
### With CreatorCard
22+
23+
```tsx
24+
const CreatorCard: React.FC<CreatorCardProps> = ({ creator, className }) => {
25+
const { isConnected } = useAccount();
26+
const [transactionState, setTransactionState] = useState<
27+
'idle' | 'submitting' | 'failed' | 'success'
28+
>('idle');
29+
30+
// Example: Check if user has sufficient balance
31+
const hasInsufficientBalance = true; // Replace with actual balance check
32+
const disabledReason = hasInsufficientBalance
33+
? 'Insufficient balance to complete this purchase'
34+
: undefined;
35+
36+
return (
37+
<div>
38+
{/* ... other card content ... */}
39+
40+
<BuyActionHelperText
41+
state={transactionState}
42+
disabledReason={disabledReason}
43+
className="mt-4"
44+
/>
45+
</div>
46+
);
47+
};
48+
```
49+
50+
## Props
51+
52+
| Prop | Type | Required | Description |
53+
|------|------|----------|-------------|
54+
| `state` | `'idle' \| 'submitting' \| 'failed' \| 'success'` | Yes | Current transaction state |
55+
| `className` | `string` | No | Additional CSS classes |
56+
| `disabledReason` | `string` | No | Reason message when action is disabled |
57+
58+
## Behavior
59+
60+
- **When `disabledReason` is provided**: Displays the reason text below the main state message with subtle styling (`text-white/40`)
61+
- **When `disabledReason` is empty/null/undefined**: Nothing renders (no empty space or placeholder)
62+
- **Animation**: Smooth fade-in/fade-out with height animation when reason appears/disappears
63+
64+
## Styling
65+
66+
The disabled reason text uses:
67+
- Font size: `0.72rem` (matching the main message)
68+
- Color: `text-white/40` (subtle, non-intrusive)
69+
- Animation: Framer Motion with opacity and height transitions
70+
- Layout: Stacked below the main message with `space-y-2` gap
71+
72+
## Examples of Disabled Reasons
73+
74+
```tsx
75+
// Insufficient balance
76+
disabledReason="Insufficient balance to complete this purchase"
77+
78+
// Network issue
79+
disabledReason="Network connection required to proceed"
80+
81+
// Wallet not connected
82+
disabledReason="Connect your wallet to enable purchases"
83+
84+
// Creator unavailable
85+
disabledReason="This creator is currently unavailable"
86+
87+
// Maximum keys reached
88+
disabledReason="Maximum number of keys already purchased"
89+
```
90+
91+
## Safe Empty Content Handling
92+
93+
The component safely handles all empty cases:
94+
95+
```tsx
96+
// All of these will render nothing for the disabled reason
97+
<BuyActionHelperText state="idle" disabledReason={undefined} />
98+
<BuyActionHelperText state="idle" disabledReason={null} />
99+
<BuyActionHelperText state="idle" disabledReason="" />
100+
<BuyActionHelperText state="idle" disabledReason=" "} /> // whitespace only
101+
<BuyActionHelperText state="idle" /> // prop not provided
102+
```
103+
104+
## Implementation Details
105+
106+
The component uses a conditional check:
107+
108+
```tsx
109+
const hasDisabledReason = disabledReason && disabledReason.trim().length > 0;
110+
```
111+
112+
This ensures:
113+
- Null/undefined values are handled
114+
- Empty strings are ignored
115+
- Whitespace-only strings are ignored
116+
- No empty elements are rendered
117+
118+
## Accessibility
119+
120+
- The disabled reason text is part of the same container as the main message
121+
- Screen readers will announce both the state message and disabled reason
122+
- Color contrast meets WCAG guidelines with `text-white/40` on dark backgrounds
123+
124+
## Complete Example
125+
126+
```tsx
127+
import { useState } from 'react';
128+
import { useAccount } from 'wagmi';
129+
import BuyActionHelperText from '@/components/common/BuyActionHelperText';
130+
import { Button } from '@/components/ui/button';
131+
132+
export function BuyActionExample() {
133+
const { isConnected } = useAccount();
134+
const [transactionState, setTransactionState] = useState<
135+
'idle' | 'submitting' | 'failed' | 'success'
136+
>('idle');
137+
138+
// Example conditions that might disable an action
139+
const userBalance = 0.5; // ETH
140+
const requiredAmount = 1.0; // ETH
141+
const isNetworkAvailable = true;
142+
const hasReachedLimit = false;
143+
144+
// Determine disabled reason based on conditions
145+
const getDisabledReason = () => {
146+
if (!isConnected) {
147+
return 'Connect your wallet to enable purchases';
148+
}
149+
if (userBalance < requiredAmount) {
150+
return `Insufficient balance. You need ${requiredAmount - userBalance} ETH more`;
151+
}
152+
if (!isNetworkAvailable) {
153+
return 'Network connection required to proceed';
154+
}
155+
if (hasReachedLimit) {
156+
return 'Maximum number of keys already purchased';
157+
}
158+
return undefined; // No disabled reason
159+
};
160+
161+
const disabledReason = getDisabledReason();
162+
const isDisabled = !!disabledReason || transactionState === 'submitting';
163+
164+
return (
165+
<div className="space-y-4">
166+
<Button
167+
onClick={() => setTransactionState('submitting')}
168+
disabled={isDisabled}
169+
variant={isConnected ? 'default' : 'outline'}
170+
>
171+
Buy Key
172+
</Button>
173+
174+
<BuyActionHelperText
175+
state={transactionState}
176+
disabledReason={disabledReason}
177+
className="mt-4"
178+
/>
179+
</div>
180+
);
181+
}
182+
```

‎src/components/common/BuyActionHelperText.tsx‎

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import { motion, AnimatePresence } from 'framer-motion';
55
interface BuyActionHelperTextProps {
66
state: 'idle' | 'submitting' | 'failed' | 'success';
77
className?: string;
8+
disabledReason?: string;
89
}
910

1011
const BuyActionHelperText: React.FC<BuyActionHelperTextProps> = ({
1112
state,
1213
className,
14+
disabledReason,
1315
}) => {
1416
const getMessage = () => {
1517
switch (state) {
@@ -64,6 +66,9 @@ const BuyActionHelperText: React.FC<BuyActionHelperTextProps> = ({
6466

6567
const styles = getStyles();
6668

69+
// Handle disabled reason display
70+
const hasDisabledReason = disabledReason && disabledReason.trim().length > 0;
71+
6772
return (
6873
<div
6974
className={cn(
@@ -76,18 +81,31 @@ const BuyActionHelperText: React.FC<BuyActionHelperTextProps> = ({
7681
<div className={cn('absolute left-0 top-0 h-full w-1', styles.accent)} />
7782
<div className="flex items-start gap-3">
7883
<div className="mt-0.5 shrink-0">{styles.icon}</div>
79-
<AnimatePresence mode="wait">
80-
<motion.p
81-
key={state}
82-
initial={{ opacity: 0, x: -4 }}
83-
animate={{ opacity: 1, x: 0 }}
84-
exit={{ opacity: 0, x: 4 }}
85-
transition={{ duration: 0.2 }}
86-
className={cn('text-[0.72rem] font-medium leading-relaxed', styles.text)}
87-
>
88-
{getMessage()}
89-
</motion.p>
90-
</AnimatePresence>
84+
<div className="flex-1 space-y-2">
85+
<AnimatePresence mode="wait">
86+
<motion.p
87+
key={state}
88+
initial={{ opacity: 0, x: -4 }}
89+
animate={{ opacity: 1, x: 0 }}
90+
exit={{ opacity: 0, x: 4 }}
91+
transition={{ duration: 0.2 }}
92+
className={cn('text-[0.72rem] font-medium leading-relaxed', styles.text)}
93+
>
94+
{getMessage()}
95+
</motion.p>
96+
</AnimatePresence>
97+
{hasDisabledReason && (
98+
<motion.p
99+
initial={{ opacity: 0, height: 0 }}
100+
animate={{ opacity: 1, height: 'auto' }}
101+
exit={{ opacity: 0, height: 0 }}
102+
transition={{ duration: 0.2 }}
103+
className="text-[0.72rem] font-medium leading-relaxed text-white/40"
104+
>
105+
{disabledReason}
106+
</motion.p>
107+
)}
108+
</div>
91109
</div>
92110
</div>
93111
);

0 commit comments

Comments
 (0)