@@ -15,13 +15,7 @@ import { formatDisplayKeyPrice } from '@/utils/keyPriceDisplay.utils';
1515import PercentageBadge from '@/components/common/PercentageBadge' ;
1616import NetworkFeeHint from '@/components/common/NetworkFeeHint' ;
1717import { TRADE_FEE_ESTIMATE } from '@/constants/fees' ;
18- import { clampBuyQuantity } from '@/utils/buyQuantity' ;
19- import {
20- fetchTradeNetworkFeeEstimate ,
21- formatTransactionFeeDisplay ,
22- type NetworkFeeDataProvider ,
23- } from '@/utils/transactionFee.utils' ;
24- import { normalizeCreatorDisplayName } from '@/utils/creatorDisplayName.utils' ;
18+ import { formatTransactionFeeDisplay } from '@/utils/transactionFee.utils' ;
2519
2620export type TradeSide = 'buy' | 'sell' ;
2721
@@ -35,13 +29,8 @@ export interface TradeDialogProps {
3529 onOpenChange : ( open : boolean ) => void ;
3630 onConfirm : ( amount : number ) => Promise < void > | void ;
3731 isSubmitting ?: boolean ;
38- networkFeeEstimateProvider ?: NetworkFeeDataProvider ;
3932}
4033
41- type NetworkFeeEstimateState =
42- | { status : 'idle' | 'loading' | 'error' ; fee : null }
43- | { status : 'success' ; fee : number } ;
44-
4534const TradeDialog : React . FC < TradeDialogProps > = ( {
4635 open,
4736 side,
@@ -51,111 +40,43 @@ const TradeDialog: React.FC<TradeDialogProps> = ({
5140 onOpenChange,
5241 onConfirm,
5342 isSubmitting = false ,
54- networkFeeEstimateProvider,
5543} ) => {
5644 const [ amountText , setAmountText ] = useState ( '1' ) ;
57- const [ networkFeeEstimate , setNetworkFeeEstimate ] =
58- useState < NetworkFeeEstimateState > ( { status : 'idle' , fee : null } ) ;
59- const [ adjustmentNote , setAdjustmentNote ] = useState < string | null > ( null ) ;
45+ const [ touched , setTouched ] = useState ( false ) ;
6046 const amountInputRef = useRef < HTMLInputElement | null > ( null ) ;
6147
6248 useEffect ( ( ) => {
6349 if ( open ) {
6450 setAmountText ( '1' ) ;
65- setAdjustmentNote ( null ) ;
51+ setTouched ( false ) ;
6652 }
6753 } , [ open ] ) ;
6854
69- const handleBlur = ( ) => {
70- if ( side !== 'buy' ) return ;
71-
72- const trimmed = amountText . trim ( ) ;
73- const res = clampBuyQuantity ( trimmed ) ;
74-
75- if ( res . adjusted ) {
76- setAmountText ( res . value . toString ( ) ) ;
77- if ( res . reason === 'below_min' ) {
78- setAdjustmentNote ( `Quantity adjusted to the minimum of ${ res . value } .` ) ;
79- } else if ( res . reason === 'above_max' ) {
80- setAdjustmentNote ( `Quantity adjusted to the maximum of ${ res . value } .` ) ;
81- } else {
82- setAdjustmentNote ( `Quantity rounded to ${ res . value } .` ) ;
83- }
84- } else {
85- setAdjustmentNote ( null ) ;
86- }
87- } ;
88-
8955 const parsedAmount = useMemo ( ( ) => {
9056 const normalized = amountText . trim ( ) ;
9157 if ( ! normalized ) return NaN ;
9258 return Number ( normalized ) ;
9359 } , [ amountText ] ) ;
9460
95- const amountValid =
96- Number . isFinite ( parsedAmount ) &&
97- parsedAmount > 0 &&
98- ( side !== 'sell' || parsedAmount <= availableHoldings ) ;
61+ const validationError = useMemo ( ( ) : string | null => {
62+ const normalized = amountText . trim ( ) ;
63+ if ( ! normalized ) return 'Please enter an amount.' ;
64+ if ( ! Number . isFinite ( parsedAmount ) ) return 'Amount must be a valid number.' ;
65+ if ( parsedAmount <= 0 ) return 'Amount must be greater than zero.' ;
66+ if ( side === 'sell' && parsedAmount > availableHoldings )
67+ return `You can't sell more than your holdings (${ formatNumber ( availableHoldings ) } keys).` ;
68+ return null ;
69+ } , [ amountText , parsedAmount , side , availableHoldings ] ) ;
70+
71+ const amountValid = validationError === null ;
72+ const showError = touched && validationError !== null ;
9973
100- const displayCreatorName =
101- normalizeCreatorDisplayName ( creatorName ) || 'Unnamed creator' ;
10274 const title = side === 'buy' ? 'Buy keys' : 'Sell keys' ;
10375 const confirmLabel = side === 'buy' ? 'Confirm buy' : 'Confirm sell' ;
10476 const estimatedNetworkFee = formatTransactionFeeDisplay (
105- networkFeeEstimate . status === 'success'
106- ? networkFeeEstimate . fee
107- : TRADE_FEE_ESTIMATE . DEFAULT_NETWORK_FEE ,
77+ TRADE_FEE_ESTIMATE . DEFAULT_NETWORK_FEE ,
10878 { unit : TRADE_FEE_ESTIMATE . UNIT }
10979 ) ;
110- const networkFeeCopy =
111- networkFeeEstimate . status === 'loading'
112- ? 'Estimating...'
113- : networkFeeEstimate . status === 'error'
114- ? 'Cannot estimate network fee'
115- : estimatedNetworkFee ;
116-
117- useEffect ( ( ) => {
118- if ( ! open ) {
119- setNetworkFeeEstimate ( { status : 'idle' , fee : null } ) ;
120- return ;
121- }
122-
123- if ( ! amountValid || ! networkFeeEstimateProvider ) {
124- setNetworkFeeEstimate ( { status : 'error' , fee : null } ) ;
125- return ;
126- }
127-
128- let cancelled = false ;
129- setNetworkFeeEstimate ( { status : 'loading' , fee : null } ) ;
130-
131- fetchTradeNetworkFeeEstimate ( networkFeeEstimateProvider , {
132- side,
133- amount : parsedAmount ,
134- } )
135- . then ( fee => {
136- if ( cancelled ) return ;
137- setNetworkFeeEstimate (
138- fee == null
139- ? { status : 'error' , fee : null }
140- : { status : 'success' , fee }
141- ) ;
142- } )
143- . catch ( ( ) => {
144- if ( ! cancelled ) {
145- setNetworkFeeEstimate ( { status : 'error' , fee : null } ) ;
146- }
147- } ) ;
148-
149- return ( ) => {
150- cancelled = true ;
151- } ;
152- } , [
153- amountValid ,
154- networkFeeEstimateProvider ,
155- open ,
156- parsedAmount ,
157- side ,
158- ] ) ;
15980
16081 return (
16182 < Dialog
@@ -181,8 +102,8 @@ const TradeDialog: React.FC<TradeDialogProps> = ({
181102 < DialogTitle > { title } </ DialogTitle >
182103 < DialogDescription >
183104 { side === 'buy'
184- ? `Purchase creator keys for ${ displayCreatorName } .`
185- : `Sell creator keys for ${ displayCreatorName } .` }
105+ ? `Purchase creator keys for ${ creatorName } .`
106+ : `Sell creator keys for ${ creatorName } .` }
186107 </ DialogDescription >
187108 </ DialogHeader >
188109
@@ -203,25 +124,30 @@ const TradeDialog: React.FC<TradeDialogProps> = ({
203124 value = { amountText }
204125 onChange = { event => {
205126 setAmountText ( event . target . value ) ;
206- setAdjustmentNote ( null ) ;
127+ setTouched ( true ) ;
207128 } }
208- onBlur = { handleBlur }
129+ onBlur = { ( ) => setTouched ( true ) }
209130 disabled = { isSubmitting }
210131 className = { cn (
211132 'w-full rounded-xl border bg-white/[0.04] px-3 py-2 text-white outline-none transition-colors' ,
212133 'border-white/10 focus:border-amber-500/50 focus:ring-2 focus:ring-amber-500/15' ,
213- ! amountValid && amountText . trim ( )
214- ? 'border-red-500/40'
215- : ''
134+ showError ? 'border-red-500/60' : ''
216135 ) }
217136 aria-label = "Trade amount"
137+ aria-describedby = { showError ? 'trade-amount-error' : undefined }
138+ aria-invalid = { showError || undefined }
218139 data-focus-order = "1"
219140 data-testid = "trade-dialog-amount"
220141 />
221- { side === 'buy' && adjustmentNote && (
222- < div className = "text-xs text-amber-400 font-medium animate-in fade-in duration-200" data-testid = "buy-qty-adjustment-note" >
223- { adjustmentNote }
224- </ div >
142+ { showError && (
143+ < p
144+ id = "trade-amount-error"
145+ role = "alert"
146+ className = "text-xs text-red-300"
147+ data-testid = "trade-dialog-amount-error"
148+ >
149+ { validationError }
150+ </ p >
225151 ) }
226152 < div className = "flex flex-wrap items-center gap-2 text-xs text-white/45" >
227153 < span
@@ -244,16 +170,12 @@ const TradeDialog: React.FC<TradeDialogProps> = ({
244170 />
245171 ) }
246172 </ div >
247- < NetworkFeeHint
248- variant = "text"
249- label = "Approx. network fee"
250- fee = { networkFeeCopy }
251- className = "text-white/45"
252- />
253- { side === 'sell' && parsedAmount > availableHoldings && (
254- < div className = "text-xs text-red-300" >
255- You can’t sell more than your current holdings.
256- </ div >
173+ { side === 'buy' && (
174+ < NetworkFeeHint
175+ variant = "text"
176+ fee = { estimatedNetworkFee }
177+ className = "text-white/45"
178+ />
257179 ) }
258180 </ div >
259181
0 commit comments