diff --git a/config-overrides.js b/config-overrides.js index 62b24b0..cf363b2 100644 --- a/config-overrides.js +++ b/config-overrides.js @@ -1,18 +1,18 @@ -const webpack = require('webpack'); +const webpack = require('webpack') -module.exports = function override(config) { +module.exports = function override (config) { config.resolve.fallback = { ...config.resolve.fallback, crypto: require.resolve('crypto-browserify'), stream: require.resolve('stream-browserify'), - vm: require.resolve('vm-browserify'), - }; + vm: require.resolve('vm-browserify') + } config.plugins = (config.plugins || []).concat([ new webpack.ProvidePlugin({ - process: 'process/browser', - }), - ]); + process: 'process/browser' + }) + ]) - return config; -}; + return config +} diff --git a/src/components/app-body/nfts-for-sale/index.js b/src/components/app-body/nfts-for-sale/index.js index 11cfb38..0d6328d 100644 --- a/src/components/app-body/nfts-for-sale/index.js +++ b/src/components/app-body/nfts-for-sale/index.js @@ -1,16 +1,21 @@ /* Shows NFTs for sale on the DEX. + + workflow: + 1. Load offers + 2. display token cards with current offers data + 3. Load tokens data + 4. add data to offers and update card with new data + 5. Load tokens icons + 6. add icons to offers and update card with new icons */ // Global npm libraries -import React, { useState, useEffect } from 'react' -import { Container, Row, Card, Col, Button, Spinner } from 'react-bootstrap' +import React, { useState, useEffect, useCallback } from 'react' +import { Container, Row, Col, Button, Spinner } from 'react-bootstrap' import axios from 'axios' -import Jdenticon from '@chris.troutner/react-jdenticon' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faRedo } from '@fortawesome/free-solid-svg-icons' -// import BchDexLib from 'bch-dex-lib' -// import RetryQueue from '@chris.troutner/retry-queue' // Local libraries import config from '../../../config' @@ -21,111 +26,188 @@ const SERVER = config.dexServer function NftsForSale (props) { // Dependency injection through props const appData = props.appData - console.log('NftsForSale() appData: ', appData) - // State const [offers, setOffers] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [offersAreLoaded, setOffersAreLoaded] = useState(false) + const [iconsAreLoaded, setIconsAreLoaded] = useState(false) + const [dataAreLoaded, setDataAreLoaded] = useState(false) - async function getNftOffers(page = 0) { + // Handler for refresh button + const handleRefresh = () => { + console.log('handling refresh') + loadNftOffers() + } + + // Function to process token data + const processTokenData = useCallback(async (offer) => { try { - const options = { - method: 'GET', - url: `${SERVER}/offer/list/nft/${page}`, - data: {} - } - const result = await axios.request(options) - console.log('result.data: ', result.data) + 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 result = await axios.get(`${SERVER}/offer/list/nft/${page}`) const rawOffers = result.data + console.log('rawOffers: ', rawOffers) - const bchjs = appData.wallet.bchjs - const wallet = appData.wallet - - // Instantiate the BchDexLib object. - // const dexLib = new BchDexLib({bchWallet: wallet, p2wdbRead: {}, p2wdbWrite: {}}) - - const offerDataCallback = (offer) => { - console.log('offerDataCallback() offer: ', offer) - } - - // Add a default icon. + // Process each offer + const processedOffers = [] for (let i = 0; i < rawOffers.length; i++) { - const thisOffer = rawOffers[i] - - thisOffer.icon = () - thisOffer.iconDownloaded = false - - // Convert sats to BCH, and then calculate cost in USD. - const rateInSats = parseInt(thisOffer.rateInBaseUnit) - // console.log('rateInSats: ', rateInSats) - const bchCost = bchjs.BitcoinCash.toBitcoinCash(rateInSats) - // console.log('bchCost: ', bchCost) - // console.log('bchUsdPrice: ', this.state.appData.bchWalletState.bchUsdPrice) - const usdPrice = bchCost * appData.bchWalletState.bchUsdPrice * thisOffer.numTokens - // usdPrice = bchjs.Util.floor2(usdPrice) - // console.log(`usdPrice: ${usdPrice}`) - const priceStr = `$${usdPrice.toFixed(3)}` - thisOffer.usdPrice = priceStr - - // Download token data - // await dexLib.tokenData.getTokenData(thisOffer, offerDataCallback) - const tokenData = await wallet.getTokenData(thisOffer.tokenId) - console.log('complete tokenData: ', tokenData) - - // const mutableCid = tokenData.mutableData.slice(7) - // const url1 = `https://free-bch.fullstack.cash/ipfs/file-info/${mutableCid}` - // console.log('url1: ', url1) - // const resp1 = await axios.get(url1) - // console.log('resp1.data: ', resp1.data) - - try { - const mutableCid = tokenData.mutableData.slice(7) - console.log(`mutable CID for token ${thisOffer.tokenId}: ${mutableCid}`) - let mutableData = await wallet.cid2json({ cid: mutableCid }) - console.log('mutableData: ', mutableData) - mutableData = mutableData.json - - } catch(err) { - console.error(`Could not download mutable data for token ${thisOffer.tokenId}: ${err.message}`) - } + const offer = rawOffers[i] + const processedOffer = await processTokenData(offer) + processedOffers.push(processedOffer) } - return rawOffers - } catch(err) { - console.error('NftsForSale() getNftOffers() error: ', err) - return [] + setOffersAreLoaded(true) + + return processedOffers + } catch (err) { + console.error('getNftOffers error:', err) + setOffersAreLoaded(true) + throw err } - } + }, [processTokenData]) - async function handleOffers() { - let offers = await getNftOffers() - console.log('offers: ', offers) - - return offers - } - - async function handleStartProcessingTokens() { - return await handleOffers() - } - - // This is an async startup function that is called by the useEffect hook. - const asyncStartup = async () => { + // This function loads the token data . + const lazyLoadTokenData = useCallback(async (tokens) => { try { - const offers = await handleStartProcessingTokens() - setOffers(offers) + 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) + + 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) { - console.error('NftsForSale() asyncStartup() error: ', error) + setDataAreLoaded(true) } - } + }, [appData]) - // Load NFT data when the page loads from the server or from the cache. + // Fetch mutable data if it exist and get the token icon url + const fetchTokenInfo = 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 + } + // Return icon url + return iconUrl + } catch (error) { + return false + } + }, [appData]) + + // This function loads the token icons from the ipfs gateways. + const lazyLoadTokenIcons = 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 = await fetchTokenInfo(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 + } + + // Mark token to prevent fetch token icon again. + thisToken.iconAlreadyDownloaded = true + } + + setIconsAreLoaded(true) + } catch (error) { + setIconsAreLoaded(true) + } + }, [fetchTokenInfo]) + + // Main function to load NFT offers + const loadNftOffers = useCallback(async () => { + try { + setIsLoading(true) + // Get tokens in offers + const offers = await getNftOffers() + console.log('offers: ', offers) + setOffers(offers) + // Load tokens data + await lazyLoadTokenData(offers) + // Load tokens icons + await lazyLoadTokenIcons(offers) + setIsLoading(false) + } catch (err) { + setIsLoading(false) + console.error('Error loading NFT offers:', err) + } + }, [lazyLoadTokenData, lazyLoadTokenIcons, getNftOffers]) + + // Effect to load NFTs on component mount useEffect(() => { - asyncStartup() - }, []) + 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 () { - // console.log('generateCards() offerData: ', offerData) + function generateCards (offers) { + console.log('generateCards() offerData: ', offers) const tokens = offers @@ -133,13 +215,12 @@ function NftsForSale (props) { for (let i = 0; i < tokens.length; i++) { const thisToken = tokens[i] - // console.log(`thisToken: ${JSON.stringify(thisToken, null, 2)}`) const thisTokenCard = ( ) tokenCards.push(thisTokenCard) @@ -155,18 +236,53 @@ function NftsForSale (props) {

NFTs for Sale

- - - + + {/** Show spinner info if tokens are loaded but data is not loaded */ + offersAreLoaded && !dataAreLoaded && ( +
+ Loading Tokens Data + +
+ ) + } + {/** Show spinner info if tokens are loaded but icons are not loaded */ + dataAreLoaded && !iconsAreLoaded && ( +
+ Loading Tokens Icons + +
+ ) + } +
+ {!isLoading && ( + + + + + + )} + + {!offersAreLoaded && ( + + + + )} - {generateCards()} + {offersAreLoaded && generateCards(offers)} + + {/** Display a message if no tokens are found */} + {offersAreLoaded && offers.length === 0 && ( + + No Offers found. + + )} ) } diff --git a/src/components/app-body/nfts-for-sale/info-button.js b/src/components/app-body/nfts-for-sale/info-button.js index da71b68..271e9fb 100644 --- a/src/components/app-body/nfts-for-sale/info-button.js +++ b/src/components/app-body/nfts-for-sale/info-button.js @@ -23,8 +23,6 @@ function InfoButton (props) { setShow(true) } - console.log('props.token: ', props.token) - // Replace with dummy button until token data is loaded. if (!props.token.tokenData) { return ( @@ -34,15 +32,9 @@ function InfoButton (props) { ) } - // Convert the url property of the token to a link, if it matches common patterns. - // let url = props.token.tokenurl - // url = linkIfUrl(props.token.url) - - // console.log('props.token: ', props.token) - return ( <> - + Token Information @@ -56,7 +48,7 @@ function InfoButton (props) { Name: - {props.token.tokenData.tokenStats.name} + {props.token.tokenData.genesisData.name} @@ -70,19 +62,14 @@ function InfoButton (props) { Decimals: - {props.token.tokenData.tokenStats.decimals} + {props.token.tokenData.genesisData.decimals} Token Type: {props.token.tokenType} - {/* - - URL: - {url} - - */} + @@ -91,4 +78,4 @@ function InfoButton (props) { ) } -export default InfoButton \ No newline at end of file +export default InfoButton 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 2cb74a4..139f8f3 100644 --- a/src/components/app-body/nfts-for-sale/token-card.js +++ b/src/components/app-body/nfts-for-sale/token-card.js @@ -3,63 +3,70 @@ */ // Global npm libraries -import React from 'react' +import React, { useState, useEffect } from 'react' import { Container, Row, Col, Card } from 'react-bootstrap' -// import BuyNftButton from './buy-button' import InfoButton from './info-button' -// import FlagButton from './flag-button' - -// Local libraries -// import InfoButton from './info-button' -// import SendTokenButton from './send-token-button' -// import SellButton from './sell-button' +import Jdenticon from '@chris.troutner/react-jdenticon' function TokenCard (props) { - let imageLink = '' - if (props.token.mutableData) { - imageLink = props.token.mutableData.tokenIcon + const { token } = props + const [icon, setIcon] = useState(token.icon) + const [tokenData, setTokenData] = useState(token.tokenData) - if (props.token.mutableData.fullSizedUrl && props.token.mutableData.fullSizedUrl.includes('http')) { - imageLink = props.token.mutableData.fullSizedUrl - } - } - - console.log('TokenCard props.token: ', props.token) + // Update icon state every token.icon and token.tokenData changes + useEffect(() => { + setIcon(token.icon) + setTokenData(token.tokenData) + }, [token.icon, token.tokenData]) return ( <> - - {props.token.icon} - - -

{props.token.ticker}

+ {/** If the icon is loaded, display it */ + icon && ( + { + setIcon(null) // Set the icon to null if it fails to load the image url. + }} + /> + ) + } + + {/** If the icon is not loaded, display the Jdenticon */ + !icon && ( + + ) + } + +

{token.ticker}

- {props.token.tokenData ? props.token.tokenData.tokenStats.name : null} + {tokenData && tokenData.genesisData ? tokenData.genesisData.name : null}
Price: - {props.token.usdPrice} + {token.usdPrice}
- + - + - +
@@ -69,7 +76,7 @@ function TokenCard (props) { ) } - +// eslint-disable-next-line {/* @@ -80,6 +87,6 @@ function TokenCard (props) { - */} + */ } export default TokenCard diff --git a/src/components/nav-menu/index.js b/src/components/nav-menu/index.js index 4871352..1e5f8ed 100644 --- a/src/components/nav-menu/index.js +++ b/src/components/nav-menu/index.js @@ -68,7 +68,7 @@ function NavMenu (props) { > Wallet - + Placeholder2 - +