Compare commits

...
18 Commits
Author SHA1 Message Date
Chris Troutner 990e41e8f6 Merge pull request #27 from Permissionless-Software-Foundation/dh-content-creators
feat(nostr): Created prototype for Social Media Content Creators
2025-07-06 14:36:26 -07:00
Daniel Gonzalez 9d35d80bcf feat(nostr): Created prototype for Social Media Content Creators 2025-07-05 22:05:57 -04:00
Chris Troutner 907bb7e468 Merge pull request #26 from Permissionless-Software-Foundation/ct-unstable
fix(nostr): Tagging posts with BCH address
2025-06-30 08:12:30 -07:00
Chris Troutner 7eb5ff7821 fix(nostr): Tagging posts with BCH address 2025-06-30 08:11:11 -07:00
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
18 changed files with 624 additions and 147 deletions
+17 -2
View File
@@ -21,6 +21,7 @@
"bch-token-sweep": "2.2.1", "bch-token-sweep": "2.2.1",
"bootstrap": "5.2.0", "bootstrap": "5.2.0",
"crypto-browserify": "3.12.1", "crypto-browserify": "3.12.1",
"marked": "^15.0.12",
"nostr-tools": "2.10.4", "nostr-tools": "2.10.4",
"p2wdb": "2.4.1", "p2wdb": "2.4.1",
"process": "0.11.10", "process": "0.11.10",
@@ -18223,8 +18224,9 @@
} }
}, },
"node_modules/marked": { "node_modules/marked": {
"version": "12.0.2", "version": "15.0.12",
"dev": true, "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"marked": "bin/marked.js" "marked": "bin/marked.js"
@@ -26496,6 +26498,19 @@
"js-yaml": "bin/js-yaml.js" "js-yaml": "bin/js-yaml.js"
} }
}, },
"node_modules/semantic-release/node_modules/marked": {
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
"integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==",
"dev": true,
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/semantic-release/node_modules/npm-run-path": { "node_modules/semantic-release/node_modules/npm-run-path": {
"version": "6.0.0", "version": "6.0.0",
"dev": true, "dev": true,
+1
View File
@@ -15,6 +15,7 @@
"bch-token-sweep": "2.2.1", "bch-token-sweep": "2.2.1",
"bootstrap": "5.2.0", "bootstrap": "5.2.0",
"crypto-browserify": "3.12.1", "crypto-browserify": "3.12.1",
"marked": "15.0.12",
"nostr-tools": "2.10.4", "nostr-tools": "2.10.4",
"p2wdb": "2.4.1", "p2wdb": "2.4.1",
"process": "0.11.10", "process": "0.11.10",
+3
View File
@@ -41,6 +41,9 @@
width: auto!important; width: auto!important;
} }
.cursor-pointer {
cursor: pointer;
}
/* Remove input number arrows Chrome, Safari, Edge, Opera */ /* Remove input number arrows Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button, input::-webkit-outer-spin-button,
+3 -1
View File
@@ -26,6 +26,7 @@ import ServerSelectView from './configuration/select-server-view'
import NostrPost from './nostr/nostr-post/index.js' import NostrPost from './nostr/nostr-post/index.js'
import NostrRead from './nostr/nostr-read/index.js' import NostrRead from './nostr/nostr-read/index.js'
import GlobalFeed from './nostr/global-feed/index.js' import GlobalFeed from './nostr/global-feed/index.js'
import ContentCreators from './nostr/content-creators/index.js'
import UserDataReview from './user-data-review' import UserDataReview from './user-data-review'
import Offers from './offers' import Offers from './offers'
function AppBody (props) { function AppBody (props) {
@@ -50,7 +51,8 @@ function AppBody (props) {
<Route path='/nostr-post' element={<NostrPost appData={appData} />} /> <Route path='/nostr-post' element={<NostrPost appData={appData} />} />
<Route path='/nostr-read' element={<NostrRead appData={appData} />} /> <Route path='/nostr-read' element={<NostrRead appData={appData} />} />
<Route path='/global-feed' element={<GlobalFeed appData={appData} />} /> <Route path='/global-feed' element={<GlobalFeed appData={appData} />} />
<Route path='/user-data/:cid' element={<UserDataReview appData={appData} />} /> <Route path='/content-creators' element={<ContentCreators appData={appData} />} />
<Route path='/user-data/:tokenId' element={<UserDataReview appData={appData} />} />
<Route path='/offers' element={<Offers appData={appData} />} /> <Route path='/offers' element={<Offers appData={appData} />} />
</Routes> </Routes>
{/** Show in all paths except the servers view */} {/** Show in all paths except the servers view */}
@@ -102,7 +102,7 @@ function InfoButton (props) {
<Col xs={4}><b>User Data</b>:</Col> <Col xs={4}><b>User Data</b>:</Col>
<Col xs={8}> <Col xs={8}>
<a <a
href={`/user-data/${mutableDataCid}`} href={`/user-data/${props.token.tokenId}`}
target='_blank' target='_blank'
rel='noopener noreferrer' rel='noopener noreferrer'
className='btn btn-link p-0' className='btn btn-link p-0'
@@ -0,0 +1,194 @@
/**
* Content creators card
*/
import React, { useState, useEffect } from 'react'
import { Card } from 'react-bootstrap'
import Jdenticon from '@chris.troutner/react-jdenticon'
import CopyOnClick from '../../bch-wallet/copy-on-click.js'
import { RelayPool } from 'nostr'
function ContentCard (props) {
const [profile, setProfile] = useState([])
const [loaded, setLoaded] = useState(false)
const { creator } = props
useEffect(() => {
const loadProfile = async () => {
const psf = 'wss://nostr-relay.psfoundation.info'
const pool = RelayPool([psf])
pool.on('open', relay => {
relay.subscribe('subid', { limit: 5, kinds: [0], authors: [creator.pubkey] })
})
pool.on('eose', relay => {
console.log('Closing Relay')
relay.close()
})
pool.on('event', (relay, subId, ev) => {
try {
const profile = JSON.parse(ev.content)
console.log('profile', profile)
setProfile(profile)
} catch (error) {
console.log('Error parsing profile', error)
}
})
setLoaded(true)
}
if (!loaded && creator.npub) {
loadProfile()
}
}, [loaded, creator])
return (
<>
<Card className='mb-4 bg-light rounded-4 shadow-sm border-0'>
<Card.Body className='p-4'>
{/* Desktop Layout - Horizontal */}
<div className='d-flex justify-content-end mb-2'>
<small className='text-muted'>
{new Date(creator.createdAt).toLocaleString()}
</small>
</div>
<div className='d-none d-md-flex align-items-center gap-4'>
{/* Profile Picture */}
<div className='flex-shrink-0'>
<Jdenticon size='140' value={creator.npub} />
</div>
{/* Creator Info */}
<div className='flex-grow-1'>
<h5 className='mb-2 fw-bold'>{profile.name}</h5>
<p className='text-muted medium mb-3'>{profile.about}</p>
{/* Nostr Public Key */}
<div className='mb-2'>
<div className='d-flex align-items-center gap-2 mb-1'>
<small className='text-muted fw-semibold'>Nostr Public Key:</small>
</div>
<div className='d-flex align-items-center gap-2'>
<div className='text-break small text-secondary'>
{creator.npub}
</div>
<CopyOnClick
walletProp='npub'
appData={props.appData}
value={creator.npub}
/>
</div>
</div>
{/* BCH Address */}
<div className='mb-3'>
<div className='d-flex align-items-center gap-2 mb-1'>
<small className='text-muted fw-semibold'>BCH Address:</small>
</div>
<div className='d-flex align-items-center gap-2'>
<div className='text-break small text-secondary'>
{creator.bchAddr}
</div>
<CopyOnClick
walletProp='bchAddress'
appData={props.appData}
value={creator.bchAddr}
/>
</div>
</div>
</div>
{/* Action Buttons */}
<div className='flex-shrink-0 d-flex flex-column gap-2'>
<button
className='btn btn-outline-danger btn-sm rounded-pill px-3'
style={{ fontSize: '0.875rem', minWidth: '100px' }}
>
<i className='bi bi-person-plus me-1' />
Follow
</button>
<button
className='btn btn-primary btn-sm rounded-pill px-3'
style={{ fontSize: '0.875rem', minWidth: '100px' }}
>
<i className='bi bi-chat-dots me-1' />
Message
</button>
</div>
</div>
{/* Mobile Layout - Vertical */}
<div className='d-flex d-md-none flex-column align-items-center text-center'>
{/* Profile Picture */}
<div className='mb-3'>
<Jdenticon size='100' value={creator.npub} />
</div>
{/* Creator Info */}
<div className='w-100 mb-3'>
<h5 className='mb-2 fw-bold'>{profile.name}</h5>
<p className='text-muted small mb-3'>{profile.about}</p>
{/* Nostr Public Key */}
<div className='mb-3'>
<div className='mb-1'>
<small className='text-muted fw-semibold'>Nostr Public Key:</small>
</div>
<div className='d-flex align-items-center justify-content-center gap-2'>
<div className='text-break small text-secondary' style={{ maxWidth: '200px' }}>
{creator.npub}
</div>
<CopyOnClick
walletProp='npub'
appData={props.appData}
value={creator.npub}
/>
</div>
</div>
{/* BCH Address */}
<div className='mb-3'>
<div className='mb-1'>
<small className='text-muted fw-semibold'>BCH Address:</small>
</div>
<div className='d-flex align-items-center justify-content-center gap-2'>
<div className='text-break small text-secondary' style={{ maxWidth: '200px' }}>
{creator.bchAddr}
</div>
<CopyOnClick
walletProp='bchAddress'
appData={props.appData}
value={creator.bchAddr}
/>
</div>
</div>
</div>
{/* Action Buttons */}
<div className='d-flex gap-2'>
<button
className='btn btn-outline-danger btn-sm rounded-pill px-3'
style={{ fontSize: '0.875rem' }}
>
<i className='bi bi-person-plus me-1' />
Follow
</button>
<button
className='btn btn-primary btn-sm rounded-pill px-3'
style={{ fontSize: '0.875rem' }}
>
<i className='bi bi-chat-dots me-1' />
Message
</button>
</div>
</div>
</Card.Body>
</Card>
</>
)
}
export default ContentCard
@@ -0,0 +1,65 @@
import React, { useState, useEffect } from 'react'
import { Container, Spinner } from 'react-bootstrap'
import axios from 'axios'
// Local libraries
import config from '../../../../config'
import ContentCard from './content-card'
// Global variables and constants
const SERVER = config.dexServer
function ContentCreators (props) {
const [creators, setCreators] = useState([])
const [loaded, setLoaded] = useState(false)
useEffect(() => {
const loadCreators = async () => {
try {
const creators = await axios.get(`${SERVER}/sm/list/all/0`)
console.log(creators.data)
setCreators(creators.data)
setLoaded(true)
} catch (error) {
setLoaded(true)
}
}
if (!loaded) {
loadCreators()
}
}, [loaded])
return (
<Container className='mt-4 mb-5'>
<div className='mb-4'>
<h2 className='text-center mb-3'>Content Creators</h2>
<p className='text-center text-muted'>
Discover amazing content creators in the Bitcoin Cash ecosystem
</p>
</div>
{!loaded && (
<div className='text-center text-muted py-5 bg-light rounded-4 shadow-sm'>
<Spinner animation='border' />
<div className='mt-3'>Loading content creators...</div>
</div>
)}
{loaded && (
<div>
{creators.map((creator, i) => (
<ContentCard key={`creator-key${i}`} creator={creator} appData={props.appData} />
))}
</div>
)}
{loaded && creators.length === 0 && (
<div className='text-center text-muted py-5 bg-light rounded-4 shadow-sm'>
<i className='bi bi-people-fill fs-1 mb-3 d-block opacity-50' />
<div>No content creators found</div>
</div>
)}
</Container>
)
}
export default ContentCreators
@@ -0,0 +1,15 @@
import React from 'react'
import { Container } from 'react-bootstrap'
import ContentCreatorsList from './content-creators-list.js'
function ContentCreators (props) {
return (
<>
<Container>
<ContentCreatorsList {...props} />
</Container>
</>
)
}
export default ContentCreators
@@ -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 * Component for reading global nostr posts
*/ */
// Global npm libraries // Global npm libraries
import React, { useEffect, useState } from 'react' import React, { useEffect, useState, useCallback } from 'react'
import { Container, Card, Spinner } from 'react-bootstrap' import { Container, Spinner } from 'react-bootstrap'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUser } from '@fortawesome/free-solid-svg-icons'
import { RelayPool } from 'nostr' import { RelayPool } from 'nostr'
import FeedCard from './feed-card'
function Feed (props) { function Feed (props) {
const { bchWalletState } = props.appData const { appData } = props
const { bchWalletState } = appData
const [posts, setPosts] = useState([]) const [posts, setPosts] = useState([])
const [loaded, setLoaded] = useState(false) 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(() => { useEffect(() => {
const start = () => { const start = () => {
const psf = 'wss://nostr-relay.psfoundation.info' const psf = 'wss://nostr-relay.psfoundation.info'
@@ -29,54 +64,25 @@ function Feed (props) {
relay.close() relay.close()
}) })
pool.on('event', (relay, subId, ev) => { pool.on('event', async (relay, subId, ev) => {
console.log('Received event:', ev) console.log('Received event:', ev)
// const profile = JSON.parse(ev.content)
setPosts(currentPosts => [...currentPosts, ev]) setPosts(currentPosts => [...currentPosts, ev])
// fetch post profile and set it to profiles state
fetchProfile(ev.pubkey)
}) })
} }
if (!loaded) { if (!loaded) {
start() start()
} }
}, [bchWalletState, loaded]) }, [bchWalletState, loaded, fetchProfile])
return ( return (
<Container className='mt-4 mb-5' style={{ marginBottom: '50px' }}> <Container className='mt-4 mb-5' style={{ marginBottom: '50px' }}>
<div> <div>
{posts.map((post, index) => ( {posts.map((post, index) => (
<Card key={index} className='mb-4 bg-light rounded-4 shadow-sm border-0'> <FeedCard key={index} post={post} appData={props.appData} profiles={profiles} />
<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>
))} ))}
{!posts.length && loaded && ( {!posts.length && loaded && (
<div className='text-center text-muted py-5 bg-light rounded-4 shadow-sm'> <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
@@ -47,11 +47,14 @@ function PublicPost (props) {
// Relay list // Relay list
const psf = 'wss://nostr-relay.psfoundation.info' const psf = 'wss://nostr-relay.psfoundation.info'
// BCH address of the user.
const bchAddr = bchWalletState.address
// Generate a post. // Generate a post.
const eventTemplate = { const eventTemplate = {
kind: 1, kind: 1,
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
tags: [['t', 'slpdex-socialmedia']], tags: [['t', 'slpdex-socialmedia'], ['u', bchAddr]],
content: formData.content content: formData.content
} }
console.log(`eventTemplate: ${JSON.stringify(eventTemplate, null, 2)}`) console.log(`eventTemplate: ${JSON.stringify(eventTemplate, null, 2)}`)
@@ -7,7 +7,7 @@ import { Container, Button } from 'react-bootstrap'
import CopyOnClick from '../../bch-wallet/copy-on-click.js' import CopyOnClick from '../../bch-wallet/copy-on-click.js'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUser } from '@fortawesome/free-solid-svg-icons' import { faUser } from '@fortawesome/free-solid-svg-icons'
import NostrFormat from '../nostr-format'
import { RelayPool } from 'nostr' import { RelayPool } from 'nostr'
function ProfileRead (props) { function ProfileRead (props) {
@@ -74,7 +74,9 @@ function ProfileRead (props) {
</span> </span>
<CopyOnClick walletProp='npub' appData={props.appData} value={bchWalletState.nostrKeyPair.npub} /> <CopyOnClick walletProp='npub' appData={props.appData} value={bchWalletState.nostrKeyPair.npub} />
</div> </div>
<div className='fs-6 text-secondary'>{post.about}</div> <div className='fs-6 text-secondary'>
<NostrFormat content={post.about} />
</div>
</div> </div>
<div className='d-flex gap-2 align-items-center flex-wrap justify-content-center'> <div className='d-flex gap-2 align-items-center flex-wrap justify-content-center'>
<Button <Button
@@ -6,7 +6,7 @@ import React, { useEffect, useState } from 'react'
import { Container, Card, Spinner } from 'react-bootstrap' import { Container, Card, Spinner } from 'react-bootstrap'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUser } from '@fortawesome/free-solid-svg-icons' import { faUser } from '@fortawesome/free-solid-svg-icons'
import NostrFormat from '../nostr-format'
import { RelayPool } from 'nostr' import { RelayPool } from 'nostr'
function PublicRead (props) { function PublicRead (props) {
@@ -72,7 +72,7 @@ function PublicRead (props) {
</div> </div>
<div className='mb-4'> <div className='mb-4'>
<div className='fs-5 ms-5 text-dark' style={{ lineHeight: '1.3' }}> <div className='fs-5 ms-5 text-dark' style={{ lineHeight: '1.3' }}>
{post.content} <NostrFormat content={post.content} />
</div> </div>
</div> </div>
</Card.Body> </Card.Body>
+48 -48
View File
@@ -85,6 +85,53 @@ function Offers (props) {
console.warn('Error in getOffers() ', 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. // Get Offer data and manipulate it for the sake of presentation.
const handleOffers = useCallback(async () => { const handleOffers = useCallback(async () => {
@@ -129,54 +176,7 @@ function Offers (props) {
setOffers(formattedOffers) setOffers(formattedOffers)
setIsLoading(false) setIsLoading(false)
}, [appData]) }, [appData, handleBuy])
const handleBuy = 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)
const options = {
method: 'post',
url: `${SERVER}offer/take`,
data: {
nostrEventId: targetOfferEventId
}
}
const result = await axios.request(options)
const { eventId, noteId } = result.data
console.log(`Event Id : ${eventId}`)
console.log(`Note Id ${noteId}`)
// Add link to output
const newModalBody = []
newModalBody.push('Success!')
// newModalBody.push(<a href={`https://p2wdb.fullstack.cash/entry/hash/${p2wdbHash}`} target='_blank' rel='noreferrer'>P2WDB Entry</a>)
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)
}
}
useEffect(() => { useEffect(() => {
// Retrieve initial offer data // Retrieve initial offer data
+77 -48
View File
@@ -5,25 +5,34 @@
// Global npm libraries // Global npm libraries
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { useParams } from 'react-router-dom' 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' import ReactMarkdown from 'react-markdown'
function UserDataReview (props) { function UserDataReview (props) {
const appData = props.appData const appData = props.appData
const [media, setMedia] = useState([]) const [media, setMedia] = useState([])
const [markdown, setMarkdown] = useState('') const [markdown, setMarkdown] = useState('')
const [activeTab, setActiveTab] = useState('media')
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState(null) const [error, setError] = useState(null)
const [tokenData, setTokenData] = useState(null)
// Get the id parameter from the URL // Get the id parameter from the URL
const { cid } = useParams() const { tokenId } = useParams()
useEffect(() => { useEffect(() => {
const loadMedia = async () => { const loadData = async () => {
setLoading(true)
try { 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 { json } = await appData.wallet.cid2json({ cid })
const userDataString = json.userData const userDataString = json.userData
if (userDataString) { if (userDataString) {
const userData = JSON.parse(userDataString) const userData = JSON.parse(userDataString)
setMedia(userData.media) setMedia(userData.media)
@@ -34,60 +43,80 @@ function UserDataReview (props) {
} }
setLoading(false) setLoading(false)
} }
loadMedia() loadData()
}, [cid, appData.wallet]) }, [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 ( return (
<Container> <Container>
<Row> <Row>
<Col> <Col>
{/** Error message */}
{error && <p className='text-danger'>{error}</p>} {error && <p className='text-danger'>{error}</p>}
{loading
? ( {/** Loading spinner */}
{loading &&
(
<div className='text-center my-5'> <div className='text-center my-5'>
<Spinner animation='border' role='status' variant='primary'> <Spinner animation='border' role='status' variant='primary'>
<span className='visually-hidden'>Loading...</span> <span className='visually-hidden'>Loading...</span>
</Spinner> </Spinner>
</div> </div>
) )}
: ( {/** User data */}
<Tabs {!loading && !error && (
activeKey={activeTab} <div className='text-center'>
onSelect={(k) => setActiveTab(k)} {/** Name */}
className='mb-4' <h1>{tokenData.genesisData.name}</h1>
>
<Tab eventKey='media' title='Media'> {/** Media Content Carousel */}
{media && media.length > 0 {media && media.length > 0
? ( ? (
<Row className='mt-3'> <div className='my-5 '>
{media.map((item, index) => ( <Carousel>
<Col key={index} xs={12} sm={6} md={4} lg={4} className='mb-3'> {media.map((item, index) => (
<img <Carousel.Item key={index}>
src={item.url} <img
alt={`Review media ${index + 1}`} className='d-block w-100'
style={{ width: '100%', height: '250px', objectFit: 'cover', borderRadius: '8px' }} src={item.url}
/> alt={`Review media ${index + 1}`}
</Col> style={{ height: '400px', objectFit: 'contain' }}
))} />
</Row> <Carousel.Caption>
) <p>Image {index + 1} of {media.length}</p>
: ( </Carousel.Caption>
<p className='mt-3'>No media content available</p> </Carousel.Item>
)} ))}
</Tab> </Carousel>
<Tab eventKey='markdown' title='Content'> <style>{'.carousel-control-next,.carousel-control-prev, .carousel-indicators {filter: invert(100%); } '}
{markdown </style>
? ( </div>
<div className='markdown-content mt-3'> )
<ReactMarkdown>{markdown}</ReactMarkdown> : (
</div> <p className='mt-3'>No media content available</p>
) )}
: (
<p className='mt-3'>No content available</p> {/** Markdown Content */}
)} {markdown
</Tab> ? (
</Tabs> <div className='markdown-content my-5 text-center'>
)} <ReactMarkdown>{markdown}</ReactMarkdown>
</div>
)
: (
<p className='mt-3'>No content available</p>
)}
</div>
)}
</Col> </Col>
</Row> </Row>
</Container> </Container>
+7
View File
@@ -77,6 +77,13 @@ function NavMenu (props) {
> >
Nostr Profile Nostr Profile
</NavLink> </NavLink>
<NavLink
className={currentPath === '/content-creators' ? 'nav-link-active' : 'nav-link-inactive'}
to='/content-creators'
onClick={handleClickEvent}
>
Content Creators
</NavLink>
<hr /> <hr />
+4 -2
View File
@@ -13,7 +13,8 @@ function useAppState () {
ssr: true, ssr: true,
defaultValue: { defaultValue: {
serverUrl: 'https://free-bch.fullstack.cash' // Default server serverUrl: 'https://free-bch.fullstack.cash' // Default server
} },
nftData: {}
}) })
console.log('lsState: ', lsState) console.log('lsState: ', lsState)
@@ -38,7 +39,7 @@ function useAppState () {
const [denyClose, setDenyClose] = useState(false) const [denyClose, setDenyClose] = useState(false)
// NFTs for sale stored data to improve performance // 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 // The wallet state makes this a true progressive web app (PWA). As
// balances, UTXOs, and tokens are retrieved, this state is updated. // 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 const newCacheData = Object.assign({}, cacheData, data) // Merge the new data with the cache data
allCacheData[tokenId] = newCacheData // Update the cache data allCacheData[tokenId] = newCacheData // Update the cache data
setNftForSaleCacheData(allCacheData) // Update the state setNftForSaleCacheData(allCacheData) // Update the state
updateLocalStorage({ nftData: allCacheData }) // Update the local storage
} }
return { return {