import { faTrash } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { Flex, Text, Button, Select, TableCell, TableRow, FormatCryptoCurrency, Input, } from 'components/primitives' import { Dispatch, FC, SetStateAction, useCallback, useContext, useEffect, useMemo, useState, } from 'react' import { Currency } from '@reservoir0x/reservoir-kit-ui' import expirationOptions from 'utils/defaultExpirationOptions' import { ExpirationOption } from 'types/ExpirationOption' import { UserToken } from 'pages/portfolio/[[...address]]' import CryptoCurrencyIcon from 'components/primitives/CryptoCurrencyIcon' import useMarketplaceFees from 'hooks/useOpenseaFees' import { ToastContext } from 'context/ToastContextProvider' import { BatchListing, Marketplace } from './BatchListings' import optimizeImage from 'utils/optimizeImage' type BatchListingsTableRowProps = { listing: BatchListing listings: BatchListing[] onChainRoyaltiesBps: number displayQuantity: boolean gridTemplateColumns: string setListings: Dispatch> updateListing: (updatedListing: BatchListing) => void globalExpirationOption: ExpirationOption globalPrice: string currency: Currency defaultCurrency: Currency isLargeDevice: boolean selectedItems: UserToken[] setSelectedItems: Dispatch> selectedMarketplaces: Marketplace[] } const MINIMUM_AMOUNT = 0.000001 export const BatchListingsTableRow: FC = ({ listing, listings, onChainRoyaltiesBps, setListings, updateListing, selectedItems, displayQuantity, gridTemplateColumns, isLargeDevice, setSelectedItems, globalExpirationOption, globalPrice, currency, defaultCurrency, selectedMarketplaces, }) => { const [expirationOption, setExpirationOption] = useState( globalExpirationOption ) const [price, setPrice] = useState(listing.price) const [quantity, setQuantity] = useState(1) const [marketplaceFee, setMarketplaceFee] = useState(0) const [marketplaceFeePercent, setMarketplaceFeePercent] = useState(0) const tokenImage = useMemo(() => { return optimizeImage(listing.token.token?.image, 250) }, [listing.token.token?.image]) const { addToast } = useContext(ToastContext) const marketplace = selectedMarketplaces.find( (m) => m.orderbook === listing.orderbook ) const handleMarketplaceFeeChange = useCallback( (marketplaceFee: number) => { setMarketplaceFee(marketplaceFee) const updatedListing = { ...listing, marketplaceFee: marketplaceFee } updateListing(updatedListing) }, [listing, updateListing] ) const removeListing = useCallback( (token: string, orderbook: string) => { const updatedListings = listings.filter( (listing) => `${listing.token.token?.contract}:${listing.token.token?.tokenId}` !== token || listing.orderbook !== orderbook ) // Update selectedItems const selectedItemIndex = selectedItems.findIndex( (item) => `${item?.token?.contract}:${item?.token?.tokenId}` === token ) if ( selectedItemIndex !== -1 && !updatedListings.some( (listing) => `${listing.token.token?.contract}:${listing.token.token?.tokenId}` === token ) ) { const updatedSelectedItems = [...selectedItems] updatedSelectedItems.splice(selectedItemIndex, 1) setSelectedItems(updatedSelectedItems) } setListings(updatedListings) }, [listings] ) let openseaFees = useMarketplaceFees( listing.orderbook == 'opensea' ? (listing.token.token?.collection?.id as string) : undefined ) useEffect(() => { if ( openseaFees && openseaFees.fee && openseaFees.fee.bps && listing.orderbook == 'opensea' ) { // Remove listing and emit toast if listing not enabled if (!openseaFees.listingEnabled) { addToast?.({ title: 'Listing not enabled', description: `Cannnot list ${listing.token.token?.name} on OpenSea`, }) removeListing( `${listing.token.token?.contract}:${listing.token.token?.tokenId}`, listing.orderbook as string ) } setMarketplaceFeePercent(openseaFees.fee.bps / 100 || 0) handleMarketplaceFeeChange( (openseaFees.fee.bps / 10000) * Number(price) * listing.quantity || 0 ) } }, [openseaFees, price, quantity, marketplace]) const creatorRoyalties = (onChainRoyaltiesBps || listing?.token?.token?.collection?.royaltiesBps || 0) / 10000 const profit = Number(price) * listing.quantity - marketplaceFee - creatorRoyalties * Number(price) * listing.quantity const topTraitPrice = useMemo(() => { if (!listing.token.token?.attributes) return undefined // Find the highest floor price return Math.max( ...listing.token.token.attributes.map( (attribute) => attribute.floorAskPrice ?? 0 ) ) }, []) useEffect(() => { handlePriceChange(globalPrice) }, [globalPrice]) useEffect(() => { if (listing.price != price && Number(listing.price) != 0) { handlePriceChange(listing.price) } }, [listing.price]) useEffect(() => { handleExpirationChange(globalExpirationOption.value) }, [globalExpirationOption]) const handleExpirationChange = useCallback( (value: string) => { const option = expirationOptions.find((option) => option.value === value) if (option) { setExpirationOption(option) const updatedListing = { ...listing, expirationOption: option } updateListing(updatedListing) } }, [listing, updateListing] ) const handlePriceChange = useCallback( (value: string) => { setPrice(value) const updatedListing = { ...listing, price: value } updateListing(updatedListing) }, [listing, updateListing] ) const handleQuantityChange = useCallback( (quantity: number) => { setQuantity(quantity) const updatedListing = { ...listing, quantity: quantity } updateListing(updatedListing) }, [listing, updateListing] ) return ( {marketplace?.name} {listing?.token?.token?.collection?.name} #{listing?.token?.token?.tokenId} {displayQuantity ? ( { const inputValue = Number(e.target.value) const max = Number(listing.token.ownership?.tokenCount) if (e.target.value === '') { setQuantity(undefined) } else if (inputValue > max) { handleQuantityChange(max) } else { handleQuantityChange(inputValue) } }} onBlur={() => { if (quantity === undefined || quantity <= 0) { handleQuantityChange(1) } }} css={{ maxWidth: 45 }} disabled={ listing.token.token?.kind !== 'erc1155' || Number(listing?.token?.ownership?.tokenCount) <= 1 } /> {listing.token.ownership?.tokenCount} available ) : null} {isLargeDevice ? ( <> {listing.token?.token?.collection?.floorAskPrice?.amount ?.native ? ( {`${listing.token?.token?.collection?.floorAskPrice?.amount?.native} ${defaultCurrency.symbol}`} ) : null} {topTraitPrice && topTraitPrice > 0 ? ( {topTraitPrice} {defaultCurrency.symbol} ) : null} ) : null} {currency.symbol} { handlePriceChange(e.target.value) }} css={{ width: 100, '@bp1500': { width: 150 } }} /> {price !== undefined && price !== '' && Number(price) !== 0 && Number(price) < MINIMUM_AMOUNT && ( Must exceed {MINIMUM_AMOUNT} )} ({creatorRoyalties * 100}%) ({marketplaceFeePercent || 0}%) ) }