Skip to content
Open
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
107 changes: 84 additions & 23 deletions frontend/src/components/BuyNFTModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Buy NFT Confirmation Modal
// Buy NFT Confirmation Modal (Refactored)

import { memo, useCallback } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { AlertTriangle, User, ShoppingCart } from 'lucide-react';
import clsx from 'clsx';
import { Modal } from './Modal';
Expand Down Expand Up @@ -29,42 +29,73 @@ export const BuyNFTModal = memo<BuyNFTModalProps>(function BuyNFTModal({
isProcessing = false,
className,
}) {
const display = formatNFTDisplay(token);
const price = token.listing?.price ?? 0;
const hasEnoughBalance = userBalance >= price;
const remainingBalance = userBalance - price;
const [isSubmitting, setIsSubmitting] = useState(false);

const display = useMemo(() => formatNFTDisplay(token), [token]);

const price = useMemo(() => token.listing?.price ?? 0, [token]);
const hasEnoughBalance = useMemo(() => userBalance >= price, [userBalance, price]);
const remainingBalance = useMemo(() => userBalance - price, [userBalance, price]);

const sellerAddress = useMemo(() => {
return token.listing?.seller || token.owner || 'Unknown';
}, [token]);

const handleConfirm = useCallback(async () => {
await onConfirm();
}, [onConfirm]);
if (isSubmitting || isProcessing || !hasEnoughBalance) return;

try {
setIsSubmitting(true);
await onConfirm();
} catch (error) {
console.error('NFT purchase failed:', error);
} finally {
setIsSubmitting(false);
}
}, [onConfirm, isSubmitting, isProcessing, hasEnoughBalance]);

const handleClose = useCallback(() => {
if (!isProcessing) {
if (!isProcessing && !isSubmitting) {
onClose();
}
}, [onClose, isProcessing]);
}, [onClose, isProcessing, isSubmitting]);

const handleImageError = useCallback((e: React.SyntheticEvent<HTMLImageElement>) => {
e.currentTarget.src = '/images/nft-placeholder.png';
}, []);

const isBusy = isProcessing || isSubmitting;

return (
<Modal
isOpen={isOpen}
onClose={handleClose}
title="Confirm Purchase"
aria-busy={isBusy}
>
<div className={clsx('buy-nft-modal', className)}>
<div
className={clsx('buy-nft-modal', className)}
aria-describedby="buy-nft-warning"
>
<div className="buy-nft-modal__preview">
<img
src={display.image}
src={display.image || '/images/nft-placeholder.png'}
alt={display.title}
className="buy-nft-modal__image"
onError={handleImageError}
/>
<div className="buy-nft-modal__details">
<h3 className="buy-nft-modal__title">{display.title}</h3>
<p className="buy-nft-modal__description">{display.description}</p>
<p className="buy-nft-modal__description">
{display.description || 'No description available.'}
</p>
<div className="buy-nft-modal__seller">
<User size={12} className="buy-nft-modal__seller-icon" />
<span className="buy-nft-modal__seller-label">Seller:</span>
<span className="buy-nft-modal__seller-address">
{truncateAddress(token.listing?.seller || token.owner || '')}
{sellerAddress !== 'Unknown'
? truncateAddress(sellerAddress)
: 'Unknown'}
</span>
</div>
</div>
Expand All @@ -73,39 +104,69 @@ export const BuyNFTModal = memo<BuyNFTModalProps>(function BuyNFTModal({
<div className="buy-nft-modal__breakdown">
<div className="buy-nft-modal__row">
<span>NFT Price</span>
<span className="buy-nft-modal__price">{formatSTX(price, 2)}</span>
<span className="buy-nft-modal__price">
{formatSTX(price, 2)}
</span>
</div>

<div className="buy-nft-modal__row">
<span>Your Balance</span>
<span className={clsx(!hasEnoughBalance && 'buy-nft-modal__insufficient')}>
<span
className={clsx(
!hasEnoughBalance && 'buy-nft-modal__insufficient'
)}
>
{formatSTX(userBalance, 2)}
</span>
</div>

<div className="buy-nft-modal__divider" />

<div className="buy-nft-modal__row buy-nft-modal__row--total">
<span>Remaining After Purchase</span>
<span className={clsx(remainingBalance < 0 && 'buy-nft-modal__insufficient')}>
<span
className={clsx(
remainingBalance < 0 && 'buy-nft-modal__insufficient'
)}
>
{formatSTX(Math.max(0, remainingBalance), 2)}
</span>
</div>
</div>

{!hasEnoughBalance && (
<div className="buy-nft-modal__warning" role="alert">
<AlertTriangle size={20} className="buy-nft-modal__warning-icon" />
<span>Insufficient balance. You need {formatSTX(price - userBalance, 2)} more.</span>
<div
id="buy-nft-warning"
className="buy-nft-modal__warning"
role="alert"
>
<AlertTriangle
size={20}
className="buy-nft-modal__warning-icon"
/>
<span>
Insufficient balance. You need{' '}
{formatSTX(price - userBalance, 2)} more.
</span>
</div>
)}

<div className="buy-nft-modal__actions">
<Button variant="secondary" onClick={handleClose} disabled={isProcessing}>
<Button
variant="secondary"
onClick={handleClose}
disabled={isBusy}
aria-disabled={isBusy}
>
Cancel
</Button>

<Button
variant="primary"
onClick={handleConfirm}
disabled={!hasEnoughBalance || isProcessing}
loading={isProcessing}
disabled={!hasEnoughBalance || isBusy}
aria-disabled={!hasEnoughBalance || isBusy}
loading={isBusy}
leftIcon={<ShoppingCart size={16} />}
>
Buy for {formatSTX(price, 2)}
Expand Down