From b031fa98deeaa73fa71b28973e0f635e7764250d Mon Sep 17 00:00:00 2001 From: Daniel Gonzalez Date: Fri, 1 Aug 2025 12:43:01 -0400 Subject: [PATCH] feat(profile): Added tokens-for-sale section to profile --- src/App.js | 3 + .../app-body/nfts-for-sale/token-card.js | 26 +- .../app-body/nostr/profile/index.js | 3 +- .../app-body/nostr/profile/nft-for-sale.js | 321 ++++++++++++++++++ .../app-body/slp-tokens/info-button.js | 4 +- 5 files changed, 335 insertions(+), 22 deletions(-) create mode 100644 src/components/app-body/nostr/profile/nft-for-sale.js diff --git a/src/App.js b/src/App.js index ac6b774..f768cce 100644 --- a/src/App.js +++ b/src/App.js @@ -45,6 +45,9 @@ function App (props) { await asyncLoad.loadWalletLib() const walletTemp = await asyncLoad.initStarterWallet(appData.serverUrl, appData.lsState.mnemonic, appData) appData.setWallet(walletTemp) + // Get the BCH spot price + addToModal('Getting BCH spot price in USD', appData) + await asyncLoad.getUSDExchangeRate(walletTemp, appData.updateBchWalletState, appData) } // Update Modal State diff --git a/src/components/app-body/nfts-for-sale/token-card.js b/src/components/app-body/nfts-for-sale/token-card.js index 91b5b1b..1028580 100644 --- a/src/components/app-body/nfts-for-sale/token-card.js +++ b/src/components/app-body/nfts-for-sale/token-card.js @@ -12,7 +12,7 @@ import InfoButton from './info-button' import BuyButton from './buy-button' function TokenCard (props) { - const { token, appData, handleRefresh } = props + const { token, appData, handleRefresh, hideBuyBtn } = props const [icon, setIcon] = useState(token.icon) const [tokenData, setTokenData] = useState(token.tokenData) @@ -64,16 +64,16 @@ function TokenCard (props) {
- + - - - - - + {!hideBuyBtn && ( + + + + )} @@ -83,17 +83,5 @@ function TokenCard (props) { ) } -// eslint-disable-next-line -{/* - - - - - - - - - - */ } export default TokenCard diff --git a/src/components/app-body/nostr/profile/index.js b/src/components/app-body/nostr/profile/index.js index 936d853..fa684ee 100644 --- a/src/components/app-body/nostr/profile/index.js +++ b/src/components/app-body/nostr/profile/index.js @@ -9,7 +9,7 @@ import ProfileRead from './profile-read.js' import PublicRead from './public-read.js' import { useParams } from 'react-router-dom' import SlpTokensDisplay from './slp-tokens-display' - +import NFTForSale from './nft-for-sale.js' function Profile (props) { const [profile, setProfile] = useState(false) const { npub } = useParams() @@ -20,6 +20,7 @@ function Profile (props) { + ) diff --git a/src/components/app-body/nostr/profile/nft-for-sale.js b/src/components/app-body/nostr/profile/nft-for-sale.js new file mode 100644 index 0000000..e858af2 --- /dev/null +++ b/src/components/app-body/nostr/profile/nft-for-sale.js @@ -0,0 +1,321 @@ +/** + * Component for displaying SLP tokens in a grid layout + */ +import React, { useState, useEffect, useCallback } from 'react' +import { Container, Row, Col, Spinner } from 'react-bootstrap' +import config from '../../../../config' +import axios from 'axios' +import TokenCard from '../../nfts-for-sale/token-card' + +function NFTForSale (props) { + const { appData, npub } = props + + const [offers, setOffers] = useState([]) + const [offersAreLoaded, setOffersAreLoaded] = useState(false) + const [iconsAreLoaded, setIconsAreLoaded] = useState(false) + const [dataAreLoaded, setDataAreLoaded] = useState(false) + + const getAddressByNpub = useCallback(async () => { + const url = `${config.dexServer}/sm/npub/${npub}` + const response = await axios.get(url) + const addr = response.data.bchAddr + return addr + }, [npub]) + + // Handler for refresh button + const handleRefresh = () => { + loadNftOffers() + } + + // Function to process token data + const processTokenData = useCallback(async (offer) => { + try { + const { wallet, bchWalletState } = appData + const { bchjs } = wallet + + // Calculate USD price + const rateInSats = parseInt(offer.rateInBaseUnit) + const bchCost = bchjs.BitcoinCash.toBitcoinCash(rateInSats) + const usdPrice = bchCost * bchWalletState.bchUsdPrice * offer.numTokens + offer.usdPrice = `$${usdPrice.toFixed(3)}` + + return offer + } catch (err) { + console.error('Error processing token:', err) + return offer + } + }, [appData]) + + // Fetch offers + const getNftOffers = useCallback(async (page = 0) => { + try { + setOffersAreLoaded(false) + const addr = await getAddressByNpub(npub) + const url = `${config.dexServer}/offer/list/addr/${addr}` + const result = await axios.get(url) + const rawOffers = result.data + console.log('rawOffers: ', rawOffers) + + // Process each offer + const processedOffers = [] + for (let i = 0; i < rawOffers.length; i++) { + const offer = rawOffers[i] + const processedOffer = await processTokenData(offer) + processedOffers.push(processedOffer) + } + + setOffersAreLoaded(true) + + return processedOffers + } catch (err) { + console.error('getNftOffers error:', err) + setOffersAreLoaded(true) + throw err + } + }, [processTokenData, getAddressByNpub, npub]) + + // This function loads the token data . + const lazyLoadTokenData = useCallback(async (tokens) => { + try { + setDataAreLoaded(false) + // map each token and fetch the token data + for (let i = 0; i < tokens.length; i++) { + const thisToken = tokens[i] + + // data does not need to be downloaded, so continue with the next one + if (thisToken.dataAlreadyDownloaded) continue + + // Try to get token data. + const tokenData = await appData.wallet.getTokenData(thisToken.tokenId) + console.log('tokenData', tokenData) + if (tokenData) { + // Set data to the token object , this can be used to display the token name in the token card component. + thisToken.tokenData = tokenData + } + + // Mark token to prevent fetch token data again. + thisToken.dataAlreadyDownloaded = true + } + + setDataAreLoaded(true) + } catch (error) { + setDataAreLoaded(true) + } + }, [appData]) + + // Fetch mutable data if it exist and get the token icon url + const fetchTokenMutableData = useCallback(async (token) => { + try { + // Get the token data + const tokenData = token.tokenData + + if (!tokenData.mutableData) return false // Return false if no mutable data + + // Get the token icon from the mutable data + const cid = parseCid(tokenData.mutableData) + console.log('mutable data cid', cid) + + const { json } = await appData.wallet.cid2json({ cid }) + console.log('json: ', json) + if (!json) return false + + let iconUrl = json.tokenIcon + + if (json.fullSizedUrl && json.fullSizedUrl.includes('http')) { + iconUrl = json.fullSizedUrl + } + const userData = json.userData + // Return icon url + return { iconUrl, userData } + } catch (error) { + return false + } + }, [appData]) + + // This function loads the token icons from the ipfs gateways. + const lazyLoadMutableData = useCallback(async (tokens) => { + try { + setIconsAreLoaded(false) + // map each token and fetch the icon url + for (let i = 0; i < tokens.length; i++) { + const thisToken = tokens[i] + + // Incon does not need to be downloaded, so continue with the next one + if (thisToken.iconAlreadyDownloaded) continue + + // Try to get token icon url from mutable data. + const { iconUrl, userData } = await fetchTokenMutableData(thisToken) + console.log('iconUrl', iconUrl) + if (iconUrl) { + // Set the icon url to the token , this can be used to display the icon in the token card component. + thisToken.icon = iconUrl + thisToken.tokenData.userData = userData + } + + // Mark token to prevent fetch token icon again. + thisToken.iconAlreadyDownloaded = true + } + + setIconsAreLoaded(true) + } catch (error) { + setIconsAreLoaded(true) + } + }, [fetchTokenMutableData]) + + // Check if token data exists in the cache and add it to the tokens object. + const reviewNftCachedData = useCallback(async (offers) => { + const cacheData = appData.nftForSaleCacheData + console.log(`Token data from cache: ${JSON.stringify(cacheData, null, 2)}`) + // Map all offers + for (let i = 0; i < offers.length; i++) { + const thisToken = offers[i] + const cacheToken = cacheData[thisToken.tokenId] + // If cache data exists, add it to the token object + if (cacheToken) { + thisToken.tokenData = cacheToken.tokenData + thisToken.icon = cacheToken.tokenIcon + } + } + }, [appData]) + + // Check if token data exists in the cache and add it to the tokens object. + const updateNFTCachedData = useCallback(async (offers) => { + // Map all offers + for (let i = 0; i < offers.length; i++) { + const thisToken = offers[i] + // save token icon and token data in cache + const newTokenCacheData = { + tokenIcon: thisToken.icon, + tokenData: thisToken.tokenData + } + + console.log('newTokenCacheData: ', newTokenCacheData) + appData.updateNFTCachedData(thisToken.tokenId, newTokenCacheData) + } + }, [appData]) + + // Main function to load NFT offers + const loadNftOffers = useCallback(async () => { + try { + // Get tokens in offers + const offers = await getNftOffers() + console.log('offers: ', offers) + // Review cached data to populate token data from cached data + await reviewNftCachedData(offers) + // set state to start displaying tokens cards. + setOffers(offers) + // Load tokens data for any updates to the token data + await lazyLoadTokenData(offers) + // Load tokens icons from mutable data + await lazyLoadMutableData(offers) + + // Update cached data with the latest retrieved data + await updateNFTCachedData(offers) + } catch (err) { + console.error('Error loading NFT offers:', err) + } + }, [lazyLoadTokenData, lazyLoadMutableData, getNftOffers, reviewNftCachedData, updateNFTCachedData]) + + // Effect to load NFTs on component mount + useEffect(() => { + console.log('loading nfts for sale') + loadNftOffers() + }, [loadNftOffers]) + + // Get Cid from url + const parseCid = (url) => { + // get the cid from the url format 'ipfs://bafybeicem27xbzs65uvbcgykcmscsgln3lmhbfrcoec3gdttkdgtxv5acq + if (url && url.includes('ipfs://')) { + const cid = url.split('ipfs://')[1] + return cid + } + return url + } + + // This function generates a Token Card for each token in the wallet. + function generateCards (offers) { + console.log('generateCards() offerData: ', offers) + + const tokens = offers + + const tokenCards = [] + + for (let i = 0; i < tokens.length; i++) { + const thisToken = tokens[i] + + const thisTokenCard = ( + + ) + tokenCards.push(thisTokenCard) + } + + return tokenCards + } + + return ( + + {!offersAreLoaded && ( +
+ +
+ )} + {offersAreLoaded && offers.length !== 0 && ( +
+
+
Tokens For Sale
+
+
+ + + + {/** Show spinner info if tokens are loaded but data is not loaded */ + offersAreLoaded && !dataAreLoaded && ( +
+ Loading Token Data + +
+ ) + } + {/** Show spinner info if tokens are loaded but icons are not loaded */ + dataAreLoaded && !iconsAreLoaded && ( +
+ Loading Token Icons + +
+ ) + } + + +
+ + {!offersAreLoaded && ( + + + + )} + + {offersAreLoaded && generateCards(offers)} + + +
+
+ )} +
+ ) +} + +export default NFTForSale diff --git a/src/components/app-body/slp-tokens/info-button.js b/src/components/app-body/slp-tokens/info-button.js index 8fed8ac..bfac91e 100644 --- a/src/components/app-body/slp-tokens/info-button.js +++ b/src/components/app-body/slp-tokens/info-button.js @@ -13,11 +13,11 @@ import { Button, Modal, Container, Row, Col } from 'react-bootstrap' // returned with a link. Otherwise the original string is returned. function linkIfUrl (url) { // Convert the URL into a link if it contains 'http' - if (url.includes('http')) { + if (url?.includes('http')) { url = ({url}) // - } else if (url.includes('ipfs://')) { + } else if (url?.includes('ipfs://')) { // Convert to a Filecoin link if its an IPFS reference. const cid = url.substring(7)