mirror of
https://github.com/Permissionless-Software-Foundation/bch-dex-taker-v2.git
synced 2026-09-21 16:52:01 -07:00
feat(NFT): Lazy-load NFT token icons
This commit is contained in:
+9
-9
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 = (<Jdenticon size='100' value={thisOffer.tokenId} />)
|
||||
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 = (
|
||||
<TokenCard
|
||||
appData={appData}
|
||||
token={thisToken}
|
||||
key={`${thisToken.tokenId}`}
|
||||
key={`${thisToken.tokenId + i}`}
|
||||
/>
|
||||
)
|
||||
tokenCards.push(thisTokenCard)
|
||||
@@ -155,18 +236,53 @@ function NftsForSale (props) {
|
||||
<h1>NFTs for Sale</h1>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<Button variant='success' >
|
||||
<FontAwesomeIcon icon={faRedo} size='lg' /> Refresh
|
||||
</Button>
|
||||
<Col xs={12} style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
{/** Show spinner info if tokens are loaded but data is not loaded */
|
||||
offersAreLoaded && !dataAreLoaded && (
|
||||
<div style={{ borderRadius: '10px', backgroundColor: '#f0f0f0', padding: '10px', display: 'flex', justifyContent: 'center', alignItems: 'center', width: 'fit-content' }}>
|
||||
<span style={{ marginRight: '10px' }}>Loading Tokens Data </span>
|
||||
<Spinner animation='border' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{/** Show spinner info if tokens are loaded but icons are not loaded */
|
||||
dataAreLoaded && !iconsAreLoaded && (
|
||||
<div style={{ borderRadius: '10px', backgroundColor: '#f0f0f0', padding: '10px', display: 'flex', justifyContent: 'center', alignItems: 'center', width: 'fit-content' }}>
|
||||
<span style={{ marginRight: '10px' }}>Loading Tokens Icons </span>
|
||||
<Spinner animation='border' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{!isLoading && (
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<Button variant='success' onClick={handleRefresh}>
|
||||
<FontAwesomeIcon icon={faRedo} size='lg' /> Refresh
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{!offersAreLoaded && (
|
||||
<Row className='d-block text-center'>
|
||||
<Spinner animation='border' variant='primary' style={{ maegin: '0 auto' }} />
|
||||
</Row>
|
||||
)}
|
||||
<Row>
|
||||
{generateCards()}
|
||||
{offersAreLoaded && generateCards(offers)}
|
||||
</Row>
|
||||
|
||||
{/** Display a message if no tokens are found */}
|
||||
{offersAreLoaded && offers.length === 0 && (
|
||||
<Row className='text-center'>
|
||||
<span> No Offers found. </span>
|
||||
</Row>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Button variant='info' onClick={handleOpen}>Info</Button>
|
||||
<Button variant='info' disabled={props.disabled} onClick={handleOpen}>Info</Button>
|
||||
<Modal show={show} onHide={handleClose}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Token Information</Modal.Title>
|
||||
@@ -56,7 +48,7 @@ function InfoButton (props) {
|
||||
|
||||
<Row style={{ backgroundColor: '#eee' }}>
|
||||
<Col xs={4}><b>Name</b>:</Col>
|
||||
<Col xs={8}>{props.token.tokenData.tokenStats.name}</Col>
|
||||
<Col xs={8}>{props.token.tokenData.genesisData.name}</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
@@ -70,19 +62,14 @@ function InfoButton (props) {
|
||||
|
||||
<Row style={{ backgroundColor: '#eee' }}>
|
||||
<Col xs={4}><b>Decimals</b>:</Col>
|
||||
<Col xs={8}>{props.token.tokenData.tokenStats.decimals}</Col>
|
||||
<Col xs={8}>{props.token.tokenData.genesisData.decimals}</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col xs={4}><b>Token Type</b>:</Col>
|
||||
<Col xs={8}>{props.token.tokenType}</Col>
|
||||
</Row>
|
||||
{/*
|
||||
<Row style={{ backgroundColor: '#eee', wordBreak: 'break-all' }}>
|
||||
<Col xs={4}><b>URL</b>:</Col>
|
||||
<Col xs={8}>{url}</Col>
|
||||
</Row>
|
||||
*/}
|
||||
|
||||
</Container>
|
||||
</Modal.Body>
|
||||
<Modal.Footer />
|
||||
@@ -91,4 +78,4 @@ function InfoButton (props) {
|
||||
)
|
||||
}
|
||||
|
||||
export default InfoButton
|
||||
export default InfoButton
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Col xs={12} sm={6} lg={4} style={{ padding: '25px' }}>
|
||||
<Card>
|
||||
<Card.Body style={{ textAlign: 'center' }}>
|
||||
<a href={imageLink} target='_blank' rel='noreferrer'>
|
||||
{props.token.icon}
|
||||
</a>
|
||||
<Card.Title style={{ textAlign: 'center' }}>
|
||||
<h4>{props.token.ticker}</h4>
|
||||
{/** If the icon is loaded, display it */
|
||||
icon && (
|
||||
<Card.Img
|
||||
src={icon}
|
||||
style={{ height: '100px', width: 'auto' }}
|
||||
onError={(e) => {
|
||||
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 && (
|
||||
<Jdenticon size='100' value={token.tokenId} />
|
||||
)
|
||||
}
|
||||
<Card.Title style={{ textAlign: 'center', marginTop: '10px' }}>
|
||||
<h4>{token.ticker}</h4>
|
||||
</Card.Title>
|
||||
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
{props.token.tokenData ? props.token.tokenData.tokenStats.name : null}
|
||||
{tokenData && tokenData.genesisData ? tokenData.genesisData.name : null}
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
|
||||
<Row>
|
||||
<Col>Price:</Col>
|
||||
<Col>{props.token.usdPrice}</Col>
|
||||
<Col>{token.usdPrice}</Col>
|
||||
</Row>
|
||||
<br />
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
<InfoButton token={props.token} />
|
||||
<InfoButton token={token} disabled={!token.tokenData} />
|
||||
</Col>
|
||||
|
||||
<Col></Col>
|
||||
<Col />
|
||||
|
||||
<Col></Col>
|
||||
<Col />
|
||||
</Row>
|
||||
|
||||
</Container>
|
||||
@@ -69,7 +76,7 @@ function TokenCard (props) {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
{/* <Col>
|
||||
<InfoButton token={props.token} />
|
||||
</Col>
|
||||
@@ -80,6 +87,6 @@ function TokenCard (props) {
|
||||
|
||||
<Col>
|
||||
<BuyNftButton appData={props.appData} offer={props.token} />
|
||||
</Col> */}
|
||||
</Col> */ }
|
||||
|
||||
export default TokenCard
|
||||
|
||||
@@ -68,7 +68,7 @@ function NavMenu (props) {
|
||||
>
|
||||
Wallet
|
||||
</NavLink>
|
||||
|
||||
|
||||
<NavLink
|
||||
className={currentPath === '/slp-tokens' ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/slp-tokens'
|
||||
@@ -83,7 +83,7 @@ function NavMenu (props) {
|
||||
>
|
||||
Placeholder2
|
||||
</NavLink>
|
||||
|
||||
|
||||
<NavLink
|
||||
className={currentPath === '/placeholder3' ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/placeholder3'
|
||||
|
||||
Reference in New Issue
Block a user