Compare commits

...
23 Commits
Author SHA1 Message Date
Chris Troutner 8a982d2ef2 Merge pull request #25 from Permissionless-Software-Foundation/dh-user-data-page
fix(user-data): Reconfigured user data page
2025-06-26 06:00:58 -07:00
Daniel Gonzalez 24bb18c77c fix(user-data): Reconfigured user data page 2025-06-25 18:58:24 -04:00
Chris Troutner 913745ba9f Merge pull request #24 from Permissionless-Software-Foundation/dh-img-carousel
feat(nft): Added image carousel
2025-06-24 08:04:57 -07:00
Chris Troutner f24c199dd6 Merge branch 'master' into dh-img-carousel 2025-06-24 08:03:45 -07:00
Chris Troutner 0da9d4c567 Merge pull request #23 from Permissionless-Software-Foundation/dh-local-storage
feat(cache): Persist NFT cache to Local Storage
2025-06-24 08:02:31 -07:00
Daniel Gonzalez e05948aa32 feat(nft): Added image carousel 2025-06-23 20:20:24 -04:00
Daniel Gonzalez 3cf69cbd95 feat(cache): Persist NFT cache to Local Storage 2025-06-23 12:08:22 -04:00
Chris Troutner 45b28c168f Merge pull request #22 from Permissionless-Software-Foundation/dh-nostr-formating
feat(nostr): Added Nostr post formatting
2025-06-19 17:42:56 -07:00
Chris Troutner 3d622e9d99 Merge branch 'master' into dh-nostr-formating 2025-06-19 17:41:07 -07:00
Chris Troutner 29ce96ae92 Merge pull request #21 from Permissionless-Software-Foundation/dh-fungible-buy-btn
fix(buy): Fixed fungible buy button
2025-06-19 17:38:25 -07:00
Daniel Gonzalez a89d4c21cc feat(nostr): Added Nostr post formatting 2025-06-18 20:41:09 -04:00
Daniel Gonzalez 561ffbaaad fix(buy): Fixed fungible buy button 2025-06-18 13:45:05 -04:00
Chris Troutner 2141f919dd Merge pull request #20 from Permissionless-Software-Foundation/dh-global-feed-profiles
feat(nostr): Display profile information in Global Feed
2025-06-14 08:19:57 -07:00
Daniel Gonzalez 3087533aff feat(nostr): Display profile information in Global Feed 2025-06-13 14:21:56 -04:00
Chris Troutner 81c367dad8 Merge pull request #19 from Permissionless-Software-Foundation/ct-unstable
Ct unstable
2025-06-11 18:41:36 -07:00
Chris Troutner 5a79af5e20 fix(package-lock): fixing package-lock 2025-06-11 18:40:03 -07:00
Chris Troutner 3eb322e9c8 Merge branch 'master' into ct-unstable 2025-06-11 18:25:13 -07:00
Chris Troutner da3d24abaa Merge pull request #18 from Permissionless-Software-Foundation/dh-fungible-tokens
feat(tokens): Added support for fungible tokens
2025-06-11 18:24:58 -07:00
Chris Troutner ae2ba02adb fix(nav): Organizing nav menu 2025-06-11 18:22:19 -07:00
Daniel Gonzalez 7c08a281b5 linting 2025-06-10 19:49:28 -04:00
Daniel Gonzalez e7bbd7f7b4 feat(tokens): Added support for fungible tokens 2025-06-10 19:46:37 -04:00
Chris Troutner db179b3c6c Merge branch 'master' into ct-unstable 2025-06-09 17:11:56 -07:00
Chris Troutner 33a3a21b68 Comments 2025-06-09 17:09:38 -07:00
15 changed files with 6190 additions and 2159 deletions
+5641 -2035
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -15,6 +15,7 @@
"bch-token-sweep": "2.2.1",
"bootstrap": "5.2.0",
"crypto-browserify": "3.12.1",
"marked": "15.0.12",
"nostr-tools": "2.10.4",
"p2wdb": "2.4.1",
"process": "0.11.10",
@@ -22,6 +23,7 @@
"query-string": "7.1.1",
"react": "19.0.0",
"react-bootstrap": "2.10.7",
"react-bs-datatable": "3.15.0",
"react-dom": "19.0.0",
"react-markdown": "10.1.0",
"react-router-dom": "7.1.3",
+3
View File
@@ -41,6 +41,9 @@
width: auto!important;
}
.cursor-pointer {
cursor: pointer;
}
/* Remove input number arrows Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
+3 -1
View File
@@ -27,6 +27,7 @@ import NostrPost from './nostr/nostr-post/index.js'
import NostrRead from './nostr/nostr-read/index.js'
import GlobalFeed from './nostr/global-feed/index.js'
import UserDataReview from './user-data-review'
import Offers from './offers'
function AppBody (props) {
// Dependency injection through props
const appData = props.appData
@@ -49,7 +50,8 @@ function AppBody (props) {
<Route path='/nostr-post' element={<NostrPost appData={appData} />} />
<Route path='/nostr-read' element={<NostrRead appData={appData} />} />
<Route path='/global-feed' element={<GlobalFeed appData={appData} />} />
<Route path='/user-data/:cid' element={<UserDataReview appData={appData} />} />
<Route path='/user-data/:tokenId' element={<UserDataReview appData={appData} />} />
<Route path='/offers' element={<Offers appData={appData} />} />
</Routes>
{/** Show in all paths except the servers view */}
{/* {appData.currentPath !== '/servers' && <SelectServerButton linkTo='/servers' appData={appData} />} */}
@@ -3,11 +3,11 @@
workflow:
1. Load offers
2. display token cards with current offers data
2. Display token cards with current Offer data
3. Load tokens data
4. add data to offers and update card with new 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
6. Add icons to offers and update card with new icons
*/
// Global npm libraries
@@ -20,6 +20,7 @@ import { faRedo } from '@fortawesome/free-solid-svg-icons'
// Local libraries
import config from '../../../config'
import TokenCard from './token-card'
// Global variables and constants
const SERVER = config.dexServer
@@ -173,7 +174,7 @@ function NftsForSale (props) {
}
}, [fetchTokenMutableData, appData])
// Check if exiist token data in the cache and add it to the tokens object
// 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)}`)
@@ -189,7 +190,7 @@ function NftsForSale (props) {
}
}, [appData])
// Check if exiist token data in the cache and add it to the tokens object
// 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++) {
@@ -216,7 +217,7 @@ function NftsForSale (props) {
await reviewNftCachedData(offers)
// set state to start displaying tokens cards.
setOffers(offers)
// Load tokens data for any updates to the token data
// Load tokens data for any updates to the token data
await lazyLoadTokenData(offers)
// Load tokens icons from mutable data
await lazyLoadMutableData(offers)
@@ -102,7 +102,7 @@ function InfoButton (props) {
<Col xs={4}><b>User Data</b>:</Col>
<Col xs={8}>
<a
href={`/user-data/${mutableDataCid}`}
href={`/user-data/${props.token.tokenId}`}
target='_blank'
rel='noopener noreferrer'
className='btn btn-link p-0'
@@ -0,0 +1,100 @@
/**
* Component for reading global nostr posts
*/
// Global npm libraries
import React, { useCallback, useEffect, useState } from 'react'
import { Card, Spinner } from 'react-bootstrap'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUser } from '@fortawesome/free-solid-svg-icons'
import * as nip19 from 'nostr-tools/nip19'
import NostrFormat from '../nostr-format'
function FeedCard (props) {
const { post, appData, profiles } = props
const [profile, setProfile] = useState(profiles[post.pubkey])
const [npub, setNpub] = useState('')
const [isClicked, setIsClicked] = useState(false)
// Get npub from pubkey
useEffect(() => {
const npub = nip19.npubEncode(post.pubkey)
setNpub(npub)
}, [post])
// Verify if pubkey profile exists into the profiles state
useEffect(() => {
if (profiles[post.pubkey]) {
setProfile(profiles[post.pubkey])
}
}, [profiles, post])
// copy to clipboard
const copyToClipboard = useCallback((value) => {
setIsClicked(true)
appData.appUtil.copyToClipboard(value)
setTimeout(() => setIsClicked(false), 200)
}, [appData])
// get short npub
const getShortNpub = useCallback((npub) => {
return npub.slice(0, 8) + '...' + npub.slice(-5)
}, [])
return (
<Card className='mb-4 bg-light rounded-4 shadow-sm border-0'>
<Card.Body className='p-3'>
<div className='d-flex align-items-center gap-3 mb-3'>
<div
className='rounded-circle bg-gradient shadow d-flex align-items-center justify-content-center'
style={{
width: '48px',
height: '48px',
background: 'linear-gradient(45deg, #6c757d, #495057)'
}}
>
<FontAwesomeIcon icon={faUser} size='1x' color='#7c7c7d' />
</div>
<div className='flex-grow-1'>
<div className='fw-bold mb-1'>
{profile && profile.name && <span>{profile.name}</span>}
{!profile?.name && (
<span>
{post.pubkey.slice(0, 8) + '...'}
<Spinner animation='border' size='sm' className='ms-2' />
</span>
)}
</div>
{npub && (
<small className='text-muted'>
<span
title='Copy to clipboard'
style={{
cursor: 'pointer',
transform: isClicked ? 'scale(0.95)' : 'scale(1)',
transition: 'transform 0.1s ease',
display: 'inline-block'
}}
onClick={() => copyToClipboard(npub)}
>
{getShortNpub(npub)}
</span>
</small>
)}
</div>
<small className='text-muted'>
{new Date(post.created_at * 1000).toLocaleString()}
</small>
</div>
<div className='mb-4'>
<div className='fs-5 ms-5 text-dark' style={{ lineHeight: '1.3' }}>
<NostrFormat content={post.content} />
</div>
</div>
</Card.Body>
</Card>
)
}
export default FeedCard
@@ -2,18 +2,53 @@
* Component for reading global nostr posts
*/
// Global npm libraries
import React, { useEffect, useState } from 'react'
import { Container, Card, Spinner } from 'react-bootstrap'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUser } from '@fortawesome/free-solid-svg-icons'
import React, { useEffect, useState, useCallback } from 'react'
import { Container, Spinner } from 'react-bootstrap'
import { RelayPool } from 'nostr'
import FeedCard from './feed-card'
function Feed (props) {
const { bchWalletState } = props.appData
const { appData } = props
const { bchWalletState } = appData
const [posts, setPosts] = useState([])
const [loaded, setLoaded] = useState(false)
const [profiles, setProfiles] = useState({})
// function to fetch profile and set it to profiles state
const fetchProfile = useCallback((pubkey) => {
// no fetch profile again if it exist
if (profiles[pubkey]) {
console.log('profile exists', profiles[pubkey])
return profiles[pubkey]
}
const psf = 'wss://nostr-relay.psfoundation.info'
const pool = RelayPool([psf])
pool.on('open', relay => {
relay.subscribe('subid', { limit: 5, kinds: [0], authors: [pubkey] })
})
pool.on('eose', relay => {
console.log('Closing Relay')
relay.close()
})
pool.on('event', (relay, subId, ev) => {
try {
const profile = JSON.parse(ev.content)
const newProfiles = { ...profiles }
newProfiles[pubkey] = profile
setProfiles(newProfiles)
} catch (error) {
console.log('Error parsing profile', error)
}
})
}, [profiles])
// Get global feed posts
useEffect(() => {
const start = () => {
const psf = 'wss://nostr-relay.psfoundation.info'
@@ -29,54 +64,25 @@ function Feed (props) {
relay.close()
})
pool.on('event', (relay, subId, ev) => {
pool.on('event', async (relay, subId, ev) => {
console.log('Received event:', ev)
// const profile = JSON.parse(ev.content)
setPosts(currentPosts => [...currentPosts, ev])
// fetch post profile and set it to profiles state
fetchProfile(ev.pubkey)
})
}
if (!loaded) {
start()
}
}, [bchWalletState, loaded])
}, [bchWalletState, loaded, fetchProfile])
return (
<Container className='mt-4 mb-5' style={{ marginBottom: '50px' }}>
<div>
{posts.map((post, index) => (
<Card key={index} className='mb-4 bg-light rounded-4 shadow-sm border-0'>
<Card.Body className='p-3'>
<div className='d-flex align-items-center gap-3 mb-3'>
<div
className='rounded-circle bg-gradient shadow d-flex align-items-center justify-content-center'
style={{
width: '48px',
height: '48px',
background: 'linear-gradient(45deg, #6c757d, #495057)'
}}
>
<FontAwesomeIcon icon={faUser} size='1x' color='#7c7c7d' />
</div>
<div className='flex-grow-1'>
<div className='fw-bold mb-1'>
{post.pubkey.slice(0, 8) + '...'}
</div>
<small className='text-muted'>
{`${post.pubkey.slice(0, 8)}...${post.pubkey.slice(-5)}`}
</small>
</div>
<small className='text-muted'>
{new Date(post.created_at * 1000).toLocaleString()}
</small>
</div>
<div className='mb-4'>
<div className='fs-5 ms-5 text-dark' style={{ lineHeight: '1.3' }}>
{post.content}
</div>
</div>
</Card.Body>
</Card>
<FeedCard key={index} post={post} appData={props.appData} profiles={profiles} />
))}
{!posts.length && loaded && (
<div className='text-center text-muted py-5 bg-light rounded-4 shadow-sm'>
@@ -0,0 +1,33 @@
/**
* Parse nostr content to html , support markdown , links , line breaks
*/
import React from 'react'
import { marked } from 'marked' // https://www.npmjs.com/package/marked/v/0.3.9
const NostrFormat = ({ content }) => {
const formatContent = (content) => {
if (!content) return ''
// create marked renderer
const renderer = new marked.Renderer()
// customize link renderer to open in new tab
renderer.link = function ({ href, text }) {
return `<a href="${href}" target="_blank" >${text}</a>`
}
// set marked options
marked.setOptions({
renderer,
breaks: true
})
// parse content to html
const html = marked.parse(content)
return html
}
return (
<div dangerouslySetInnerHTML={{ __html: formatContent(content) }} />
)
}
export default NostrFormat
@@ -7,7 +7,7 @@ import { Container, Button } from 'react-bootstrap'
import CopyOnClick from '../../bch-wallet/copy-on-click.js'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUser } from '@fortawesome/free-solid-svg-icons'
import NostrFormat from '../nostr-format'
import { RelayPool } from 'nostr'
function ProfileRead (props) {
@@ -74,7 +74,9 @@ function ProfileRead (props) {
</span>
<CopyOnClick walletProp='npub' appData={props.appData} value={bchWalletState.nostrKeyPair.npub} />
</div>
<div className='fs-6 text-secondary'>{post.about}</div>
<div className='fs-6 text-secondary'>
<NostrFormat content={post.about} />
</div>
</div>
<div className='d-flex gap-2 align-items-center flex-wrap justify-content-center'>
<Button
@@ -6,7 +6,7 @@ import React, { useEffect, useState } from 'react'
import { Container, Card, Spinner } from 'react-bootstrap'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUser } from '@fortawesome/free-solid-svg-icons'
import NostrFormat from '../nostr-format'
import { RelayPool } from 'nostr'
function PublicRead (props) {
@@ -72,7 +72,7 @@ function PublicRead (props) {
</div>
<div className='mb-4'>
<div className='fs-5 ms-5 text-dark' style={{ lineHeight: '1.3' }}>
{post.content}
<NostrFormat content={post.content} />
</div>
</div>
</Card.Body>
+231
View File
@@ -0,0 +1,231 @@
/*
This React components downloads the active Offers from the REST API and
displays them in a data table.
*/
// Global npm libraries
import React, { useState, useEffect, useCallback } from 'react'
import { Container, Row, Col, Table, Button, Spinner } from 'react-bootstrap'
import axios from 'axios'
import { DatatableWrapper, TableBody, TableHeader } from 'react-bs-datatable'
// Local libraries
import config from '../../../config'
import WaitingModal from '../../waiting-modal'
// Global variables and constants
const SERVER = `${config.dexServer}/`
const TABLE_HEADERS = [
{
prop: 'ticker',
title: 'Ticker',
isFilterable: true
},
{
prop: 'tokenId',
title: 'Token ID'
},
{
prop: 'buyOrSell',
title: 'Type'
},
{
prop: 'smallNostrEventId',
title: 'Event ID'
},
{
prop: 'numTokens',
title: 'Quantity'
},
{
prop: 'usdPrice',
title: 'Price (USD)'
},
{
prop: 'button',
title: 'Action'
}
]
function Offers (props) {
const [appData] = useState(props.appData)
const [offers, setOffers] = useState([])
// Modal state
const [showModal, setShowModal] = useState(false)
const [modalBody, setModalBody] = useState([])
const [hideSpinner, setHideSpinner] = useState(false)
const [denyClose, setDenyClose] = useState(false)
const [isLoading, setIsLoading] = useState(false)
// Given a large string, it will return a string with the first and last
// four characters.
const cutString = (str) => {
try {
const subTxid = str.slice(0, 4)
const subTxid2 = str.slice(-4)
return `${subTxid}...${subTxid2}`
} catch (err) {
console.warn('Error in cutString() ', err)
}
}
// REST request to get data from avax-dex
const getOffers = async () => {
try {
const options = {
method: 'GET',
url: `${SERVER}offer/list/fungible/0`,
data: {}
}
const result = await axios.request(options)
return result.data
} catch (err) {
console.warn('Error in getOffers() ', err)
}
}
const handleBuy = useCallback(async (event) => {
try {
console.log('Buy button clicked. Event: ', event)
const targetOfferEventId = event.target.id
console.log('targetOfferEventId: ', targetOfferEventId)
// Initialize modal
setShowModal(true)
setModalBody(['Generating Counter Offer...', '(This can take a couple minutes)'])
setHideSpinner(false)
setDenyClose(true)
// Generate a counter offer.
const bchDexLib = appData.dexLib
const { offerData, partialHex } = await bchDexLib.take.takeOffer(
targetOfferEventId
)
// Upload the counter offer to Nostr.
const nostr = appData.nostr
const { eventId, noteId } = await nostr.testNostrUpload({
offerData,
partialHex
})
console.log('eventId: ', eventId)
console.log('noteId: ', noteId)
// update output modal
const newModalBody = []
newModalBody.push('Success!')
newModalBody.push('What happens next:')
newModalBody.push('The money has not yet left your wallet! It is still under your control.')
newModalBody.push('If the sellers node is online, they will accept the Counter Offer you just generated in a few minutes.')
newModalBody.push('If the tokens never show up, you can sweep the funds back into your wallet.')
setModalBody(newModalBody)
setHideSpinner(true)
setDenyClose(false)
} catch (error) {
console.warn('Error in handleBuy() ', error)
setModalBody(['Buy failed: ', error.message])
setHideSpinner(true)
setDenyClose(false)
}
}, [appData]) // Dependencies: appData since it's used inside the callback
// Get Offer data and manipulate it for the sake of presentation.
const handleOffers = useCallback(async () => {
setIsLoading(true)
// Get raw offer data.
const offerRawData = await getOffers()
console.log('offerRawData: ', offerRawData)
if (!offerRawData || offerRawData.length === 0) {
setIsLoading(false)
setOffers([])
return
}
// Formatted Data
const formattedOffers = []
for (let i = 0; i < offerRawData.length; i++) {
const thisOffer = offerRawData[i]
// Get and format the token ID
const tokenId = thisOffer.tokenId
const smallTokenId = cutString(tokenId)
thisOffer.tokenId = (<a href={`https://token.fullstack.cash/?tokenid=${tokenId}`} target='_blank' rel='noreferrer'>{smallTokenId}</a>)
// Get and format the P2WDB ID
const nostrEventId = thisOffer.nostrEventId
const smallNostrEventId = cutString(nostrEventId)
thisOffer.smallNostrEventId = smallNostrEventId
thisOffer.button = (<Button text='Buy' variant='success' size='lg' id={nostrEventId} onClick={handleBuy}>Buy</Button>)
// thisOffer.p2wdbHash = (<a href={`https://p2wdb.fullstack.cash/entry/hash/${p2wdbHash}`} target='_blank' rel='noreferrer'>{smallP2wdbHash}</a>)
// Convert sats to BCH, and then calculate cost in USD.
const bchjs = appData.wallet.bchjs
const rateInSats = parseInt(thisOffer.rateInBaseUnit)
const bchCost = bchjs.BitcoinCash.toBitcoinCash(rateInSats)
const usdPrice = bchCost * appData.bchWalletState.bchUsdPrice
const priceStr = `$${usdPrice.toFixed(3)}`
thisOffer.usdPrice = priceStr
formattedOffers.push(thisOffer)
}
setOffers(formattedOffers)
setIsLoading(false)
}, [appData, handleBuy])
useEffect(() => {
// Retrieve initial offer data
handleOffers()
// Get data and update the table periodically.
const interval = setInterval(() => {
handleOffers()
}, 30000)
// Cleanup interval on component unmount
return () => clearInterval(interval)
}, [handleOffers]) // Empty dependency array means this effect runs once on mount
const heading = 'Generating Counter Offer...'
return (
<>
{showModal && (
<WaitingModal
heading={heading}
body={modalBody}
hideSpinner={hideSpinner}
denyClose={denyClose}
/>
)}
<Container>
<Row>
<Col className='text-break' style={{ textAlign: 'center' }}>
{!isLoading && (
<DatatableWrapper body={offers} headers={TABLE_HEADERS}>
<Table>
<TableHeader />
<TableBody />
</Table>
</DatatableWrapper>
)}
</Col>
</Row>
{isLoading && (
<Row>
<Col className='text-break' style={{ textAlign: 'center' }}>
<Spinner animation='border' />
</Col>
</Row>
)}
</Container>
</>
)
}
export default Offers
+77 -48
View File
@@ -5,25 +5,34 @@
// Global npm libraries
import React, { useState, useEffect } from 'react'
import { useParams } from 'react-router-dom'
import { Container, Row, Col, Tabs, Tab, Spinner } from 'react-bootstrap'
import { Container, Row, Col, Spinner, Carousel } from 'react-bootstrap'
import ReactMarkdown from 'react-markdown'
function UserDataReview (props) {
const appData = props.appData
const [media, setMedia] = useState([])
const [markdown, setMarkdown] = useState('')
const [activeTab, setActiveTab] = useState('media')
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [tokenData, setTokenData] = useState(null)
// Get the id parameter from the URL
const { cid } = useParams()
const { tokenId } = useParams()
useEffect(() => {
const loadMedia = async () => {
setLoading(true)
const loadData = async () => {
try {
const tokenData = await appData.wallet.getTokenData(tokenId)
setTokenData(tokenData)
setLoading(true)
if (!tokenData.mutableData) throw new Error('Mutable data not found')
const cid = parseCid(tokenData.mutableData)
const { json } = await appData.wallet.cid2json({ cid })
const userDataString = json.userData
if (userDataString) {
const userData = JSON.parse(userDataString)
setMedia(userData.media)
@@ -34,60 +43,80 @@ function UserDataReview (props) {
}
setLoading(false)
}
loadMedia()
}, [cid, appData.wallet])
loadData()
}, [tokenId, appData.wallet])
// 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
}
return (
<Container>
<Row>
<Col>
{/** Error message */}
{error && <p className='text-danger'>{error}</p>}
{loading
? (
{/** Loading spinner */}
{loading &&
(
<div className='text-center my-5'>
<Spinner animation='border' role='status' variant='primary'>
<span className='visually-hidden'>Loading...</span>
</Spinner>
</div>
)
: (
<Tabs
activeKey={activeTab}
onSelect={(k) => setActiveTab(k)}
className='mb-4'
>
<Tab eventKey='media' title='Media'>
{media && media.length > 0
? (
<Row className='mt-3'>
{media.map((item, index) => (
<Col key={index} xs={12} sm={6} md={4} lg={4} className='mb-3'>
<img
src={item.url}
alt={`Review media ${index + 1}`}
style={{ width: '100%', height: '250px', objectFit: 'cover', borderRadius: '8px' }}
/>
</Col>
))}
</Row>
)
: (
<p className='mt-3'>No media content available</p>
)}
</Tab>
<Tab eventKey='markdown' title='Content'>
{markdown
? (
<div className='markdown-content mt-3'>
<ReactMarkdown>{markdown}</ReactMarkdown>
</div>
)
: (
<p className='mt-3'>No content available</p>
)}
</Tab>
</Tabs>
)}
)}
{/** User data */}
{!loading && !error && (
<div className='text-center'>
{/** Name */}
<h1>{tokenData.genesisData.name}</h1>
{/** Media Content Carousel */}
{media && media.length > 0
? (
<div className='my-5 '>
<Carousel>
{media.map((item, index) => (
<Carousel.Item key={index}>
<img
className='d-block w-100'
src={item.url}
alt={`Review media ${index + 1}`}
style={{ height: '400px', objectFit: 'contain' }}
/>
<Carousel.Caption>
<p>Image {index + 1} of {media.length}</p>
</Carousel.Caption>
</Carousel.Item>
))}
</Carousel>
<style>{'.carousel-control-next,.carousel-control-prev, .carousel-indicators {filter: invert(100%); } '}
</style>
</div>
)
: (
<p className='mt-3'>No media content available</p>
)}
{/** Markdown Content */}
{markdown
? (
<div className='markdown-content my-5 text-center'>
<ReactMarkdown>{markdown}</ReactMarkdown>
</div>
)
: (
<p className='mt-3'>No content available</p>
)}
</div>
)}
</Col>
</Row>
</Container>
+36 -22
View File
@@ -43,9 +43,43 @@ function NavMenu (props) {
to='/nfts-for-sale'
onClick={handleClickEvent}
>
NFTs for Sale
NFTs
</NavLink>
<NavLink
className={currentPath === '/offers' ? 'nav-link-active' : 'nav-link-inactive'}
to='/offers'
onClick={handleClickEvent}
>
Fungible Tokens
</NavLink>
<hr />
<NavLink
className={currentPath === '/global-feed' ? 'nav-link-active' : 'nav-link-inactive'}
to='/global-feed'
onClick={handleClickEvent}
>
Global Feed
</NavLink>
<NavLink
className={currentPath === '/nostr-post' ? 'nav-link-active' : 'nav-link-inactive'}
to='/nostr-post'
onClick={handleClickEvent}
>
Nostr Post
</NavLink>
<NavLink
className={currentPath === '/nostr-read' ? 'nav-link-active' : 'nav-link-inactive'}
to='/nostr-read'
onClick={handleClickEvent}
>
Nostr Profile
</NavLink>
<hr />
<NavLink
className={(currentPath === '/bch') ? 'nav-link-active' : 'nav-link-inactive'}
to='/bch'
@@ -100,27 +134,7 @@ function NavMenu (props) {
>
Configuration
</NavLink>
<NavLink
className={currentPath === '/nostr-post' ? 'nav-link-active' : 'nav-link-inactive'}
to='/nostr-post'
onClick={handleClickEvent}
>
Nostr Post
</NavLink>
<NavLink
className={currentPath === '/nostr-read' ? 'nav-link-active' : 'nav-link-inactive'}
to='/nostr-read'
onClick={handleClickEvent}
>
Nostr Profile
</NavLink>
<NavLink
className={currentPath === '/global-feed' ? 'nav-link-active' : 'nav-link-inactive'}
to='/global-feed'
onClick={handleClickEvent}
>
Global Feed
</NavLink>
</Nav>
</Navbar.Collapse>
</Navbar>
+4 -2
View File
@@ -13,7 +13,8 @@ function useAppState () {
ssr: true,
defaultValue: {
serverUrl: 'https://free-bch.fullstack.cash' // Default server
}
},
nftData: {}
})
console.log('lsState: ', lsState)
@@ -38,7 +39,7 @@ function useAppState () {
const [denyClose, setDenyClose] = useState(false)
// NFTs for sale stored data to improve performance
const [nftForSaleCacheData, setNftForSaleCacheData] = useState({})
const [nftForSaleCacheData, setNftForSaleCacheData] = useState(lsState.nftData || {})
// The wallet state makes this a true progressive web app (PWA). As
// balances, UTXOs, and tokens are retrieved, this state is updated.
@@ -107,6 +108,7 @@ function useAppState () {
const newCacheData = Object.assign({}, cacheData, data) // Merge the new data with the cache data
allCacheData[tokenId] = newCacheData // Update the cache data
setNftForSaleCacheData(allCacheData) // Update the state
updateLocalStorage({ nftData: allCacheData }) // Update the local storage
}
return {