diff --git a/src/components/common/TradeDialog.tsx b/src/components/common/TradeDialog.tsx index a4bdd52..728850b 100644 --- a/src/components/common/TradeDialog.tsx +++ b/src/components/common/TradeDialog.tsx @@ -15,13 +15,7 @@ import { formatDisplayKeyPrice } from '@/utils/keyPriceDisplay.utils'; import PercentageBadge from '@/components/common/PercentageBadge'; import NetworkFeeHint from '@/components/common/NetworkFeeHint'; import { TRADE_FEE_ESTIMATE } from '@/constants/fees'; -import { clampBuyQuantity } from '@/utils/buyQuantity'; -import { - fetchTradeNetworkFeeEstimate, - formatTransactionFeeDisplay, - type NetworkFeeDataProvider, -} from '@/utils/transactionFee.utils'; -import { normalizeCreatorDisplayName } from '@/utils/creatorDisplayName.utils'; +import { formatTransactionFeeDisplay } from '@/utils/transactionFee.utils'; export type TradeSide = 'buy' | 'sell'; @@ -35,13 +29,8 @@ export interface TradeDialogProps { onOpenChange: (open: boolean) => void; onConfirm: (amount: number) => Promise | void; isSubmitting?: boolean; - networkFeeEstimateProvider?: NetworkFeeDataProvider; } -type NetworkFeeEstimateState = - | { status: 'idle' | 'loading' | 'error'; fee: null } - | { status: 'success'; fee: number }; - const TradeDialog: React.FC = ({ open, side, @@ -51,111 +40,43 @@ const TradeDialog: React.FC = ({ onOpenChange, onConfirm, isSubmitting = false, - networkFeeEstimateProvider, }) => { const [amountText, setAmountText] = useState('1'); - const [networkFeeEstimate, setNetworkFeeEstimate] = - useState({ status: 'idle', fee: null }); - const [adjustmentNote, setAdjustmentNote] = useState(null); + const [touched, setTouched] = useState(false); const amountInputRef = useRef(null); useEffect(() => { if (open) { setAmountText('1'); - setAdjustmentNote(null); + setTouched(false); } }, [open]); - const handleBlur = () => { - if (side !== 'buy') return; - - const trimmed = amountText.trim(); - const res = clampBuyQuantity(trimmed); - - if (res.adjusted) { - setAmountText(res.value.toString()); - if (res.reason === 'below_min') { - setAdjustmentNote(`Quantity adjusted to the minimum of ${res.value}.`); - } else if (res.reason === 'above_max') { - setAdjustmentNote(`Quantity adjusted to the maximum of ${res.value}.`); - } else { - setAdjustmentNote(`Quantity rounded to ${res.value}.`); - } - } else { - setAdjustmentNote(null); - } - }; - const parsedAmount = useMemo(() => { const normalized = amountText.trim(); if (!normalized) return NaN; return Number(normalized); }, [amountText]); - const amountValid = - Number.isFinite(parsedAmount) && - parsedAmount > 0 && - (side !== 'sell' || parsedAmount <= availableHoldings); + const validationError = useMemo((): string | null => { + const normalized = amountText.trim(); + if (!normalized) return 'Please enter an amount.'; + if (!Number.isFinite(parsedAmount)) return 'Amount must be a valid number.'; + if (parsedAmount <= 0) return 'Amount must be greater than zero.'; + if (side === 'sell' && parsedAmount > availableHoldings) + return `You can't sell more than your holdings (${formatNumber(availableHoldings)} keys).`; + return null; + }, [amountText, parsedAmount, side, availableHoldings]); + + const amountValid = validationError === null; + const showError = touched && validationError !== null; - const displayCreatorName = - normalizeCreatorDisplayName(creatorName) || 'Unnamed creator'; const title = side === 'buy' ? 'Buy keys' : 'Sell keys'; const confirmLabel = side === 'buy' ? 'Confirm buy' : 'Confirm sell'; const estimatedNetworkFee = formatTransactionFeeDisplay( - networkFeeEstimate.status === 'success' - ? networkFeeEstimate.fee - : TRADE_FEE_ESTIMATE.DEFAULT_NETWORK_FEE, + TRADE_FEE_ESTIMATE.DEFAULT_NETWORK_FEE, { unit: TRADE_FEE_ESTIMATE.UNIT } ); - const networkFeeCopy = - networkFeeEstimate.status === 'loading' - ? 'Estimating...' - : networkFeeEstimate.status === 'error' - ? 'Cannot estimate network fee' - : estimatedNetworkFee; - - useEffect(() => { - if (!open) { - setNetworkFeeEstimate({ status: 'idle', fee: null }); - return; - } - - if (!amountValid || !networkFeeEstimateProvider) { - setNetworkFeeEstimate({ status: 'error', fee: null }); - return; - } - - let cancelled = false; - setNetworkFeeEstimate({ status: 'loading', fee: null }); - - fetchTradeNetworkFeeEstimate(networkFeeEstimateProvider, { - side, - amount: parsedAmount, - }) - .then(fee => { - if (cancelled) return; - setNetworkFeeEstimate( - fee == null - ? { status: 'error', fee: null } - : { status: 'success', fee } - ); - }) - .catch(() => { - if (!cancelled) { - setNetworkFeeEstimate({ status: 'error', fee: null }); - } - }); - - return () => { - cancelled = true; - }; - }, [ - amountValid, - networkFeeEstimateProvider, - open, - parsedAmount, - side, - ]); return ( = ({ {title} {side === 'buy' - ? `Purchase creator keys for ${displayCreatorName}.` - : `Sell creator keys for ${displayCreatorName}.`} + ? `Purchase creator keys for ${creatorName}.` + : `Sell creator keys for ${creatorName}.`} @@ -203,25 +124,30 @@ const TradeDialog: React.FC = ({ value={amountText} onChange={event => { setAmountText(event.target.value); - setAdjustmentNote(null); + setTouched(true); }} - onBlur={handleBlur} + onBlur={() => setTouched(true)} disabled={isSubmitting} className={cn( 'w-full rounded-xl border bg-white/[0.04] px-3 py-2 text-white outline-none transition-colors', 'border-white/10 focus:border-amber-500/50 focus:ring-2 focus:ring-amber-500/15', - !amountValid && amountText.trim() - ? 'border-red-500/40' - : '' + showError ? 'border-red-500/60' : '' )} aria-label="Trade amount" + aria-describedby={showError ? 'trade-amount-error' : undefined} + aria-invalid={showError || undefined} data-focus-order="1" data-testid="trade-dialog-amount" /> - {side === 'buy' && adjustmentNote && ( -
- {adjustmentNote} -
+ {showError && ( + )}
= ({ /> )}
- - {side === 'sell' && parsedAmount > availableHoldings && ( -
- You can’t sell more than your current holdings. -
+ {side === 'buy' && ( + )} diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx index 80d8066..a00488b 100644 --- a/src/pages/LandingPage.tsx +++ b/src/pages/LandingPage.tsx @@ -30,7 +30,6 @@ import EmptyTransactionTimelineState from '@/components/common/EmptyTransactionT import TradeDialog, { type TradeSide } from '@/components/common/TradeDialog'; import NetworkMismatchBanner from '@/components/common/NetworkMismatchBanner'; import StellarConnectionQualityBadge from '@/components/common/StellarConnectionQualityBadge'; -import { useEthersProvider } from '@/hooks/useEthersProvider'; import { useNetworkMismatch } from '@/hooks/useNetworkMismatch'; import showToast from '@/utils/toast.util'; import { getSignatureErrorMessage } from '@/utils/errorHandling.utils'; @@ -265,7 +264,6 @@ function LandingPage() { const [tradeSide, setTradeSide] = useState('buy'); const [tradeDialogOpen, setTradeDialogOpen] = useState(false); const [tradeSubmitting, setTradeSubmitting] = useState(false); - const tradeFeeEstimateProvider = useEthersProvider(); const prefersReducedMotion = usePrefersReducedMotion(); const [sortOption, setSortOption] = useState(() => { if (typeof window === 'undefined') return 'featured'; @@ -1246,7 +1244,6 @@ function LandingPage() { availableHoldings={featuredHoldings} keyPriceStroops={resolveCreatorKeyPriceStroops(featuredCreator)} isSubmitting={tradeSubmitting} - networkFeeEstimateProvider={tradeFeeEstimateProvider} onOpenChange={setTradeDialogOpen} onConfirm={handleConfirmTrade} />