feat(nostr): Filter Creators by Follower Count

This commit is contained in:
Daniel Gonzalez
2025-07-17 11:41:57 -04:00
parent 638e6f9e6d
commit a81dbb70e1
3 changed files with 112 additions and 52 deletions
@@ -7,43 +7,14 @@ import { Card } from 'react-bootstrap'
import Jdenticon from '@chris.troutner/react-jdenticon'
import CopyOnClick from '../../bch-wallet/copy-on-click.js'
import FollowBtn from './follow-btn.js'
import { RelayPool } from 'nostr'
function ContentCard (props) {
const [profile, setProfile] = useState([])
const [loaded, setLoaded] = useState(false)
const { creator, followList, refreshFollowList } = props
const [profile, setProfile] = useState(null)
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])
setProfile(creator?.profile || {})
}, [creator.profile])
const goToProfile = () => {
const profileUrl = `${window.location.origin}/profile/${creator.npub}`
@@ -69,8 +40,8 @@ function ContentCard (props) {
{/* Creator Info */}
<div className='flex-grow-1'>
<h5 className='mb-2 fw-bold cursor-pointer' onClick={goToProfile}>{profile.name}</h5>
<p className='text-muted medium mb-3'>{profile.about}</p>
<h5 className='mb-2 fw-bold cursor-pointer' onClick={goToProfile}>{profile?.name}</h5>
<p className='text-muted medium mb-3'>{profile?.about}</p>
{/* Nostr Public Key */}
<div className='mb-2'>
@@ -126,7 +97,16 @@ function ContentCard (props) {
Message
</button>
<small>Followers: {creator.followerCnt}</small>
<div className='d-flex align-items-center justify-content-center gap-2 mt-2'>
<span
className='badge rounded-pill bg-white border text-dark px-3 py-2 shadow-sm d-flex align-items-center gap-2'
style={{ fontSize: '1rem', fontWeight: 600, cursor: 'auto' }}
>
<i className='bi bi-people-fill text-primary' style={{ fontSize: '1.2rem' }} />
<span style={{ fontWeight: 700 }}>{creator.followerCnt}</span>
<span className='text-muted' style={{ fontWeight: 400, fontSize: '0.9rem' }}>Followers</span>
</span>
</div>
</div>
</div>
@@ -139,8 +119,8 @@ function ContentCard (props) {
{/* Creator Info */}
<div className='w-100 mb-3'>
<h5 className='mb-2 fw-bold' onClick={goToProfile}>{profile.name}</h5>
<p className='text-muted small mb-3'>{profile.about}</p>
<h5 className='mb-2 fw-bold' onClick={goToProfile}>{profile?.name}</h5>
<p className='text-muted small mb-3'>{profile?.about}</p>
{/* Nostr Public Key */}
<div className='mb-3'>
@@ -194,6 +174,13 @@ function ContentCard (props) {
Message
</button>
</div>
<div className='d-flex align-items-center justify-content-center gap-2 mt-2'>
<span className='badge rounded-pill bg-white border text-dark px-3 py-2 shadow-sm d-flex align-items-center gap-2' style={{ fontSize: '1rem', fontWeight: 600 }}>
<i className='bi bi-people-fill text-primary' style={{ fontSize: '1.2rem' }} />
<span style={{ fontWeight: 700 }}>{creator.followerCnt}</span>
<span className='text-muted' style={{ fontWeight: 400, fontSize: '0.9rem' }}>Followers</span>
</span>
</div>
</div>
</Card.Body>
</Card>
@@ -1,5 +1,7 @@
import React, { useState, useEffect, useCallback } from 'react'
import { Container, Spinner, Form } from 'react-bootstrap'
import { Container, Spinner, Dropdown } from 'react-bootstrap'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faFilter } from '@fortawesome/free-solid-svg-icons'
import axios from 'axios'
// Local libraries
@@ -12,6 +14,7 @@ const SERVER = config.dexServer
function ContentCreators (props) {
const [creators, setCreators] = useState([])
const [loaded, setLoaded] = useState(false)
const [selectedFilter, setSelectedFilter] = useState('Most Followers')
const { appData } = props
const [followList, setFollowList] = useState([])
@@ -47,13 +50,57 @@ function ContentCreators (props) {
setFollowList(list)
}, [appData])
const loadProfile = useCallback(async (pubKey) => {
return new Promise((resolve) => {
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()
resolve(false)
})
pool.on('event', (relay, subId, ev) => {
try {
const profile = JSON.parse(ev.content)
// console.log('profile', profile)
resolve(profile)
} catch (error) {
resolve(false)
}
})
})
}, [])
useEffect(() => {
const loadCreators = async () => {
try {
const creators = await axios.get(`${SERVER}/sm/list/all/0`)
console.log(creators.data)
setCreators(creators.data)
const creatorsRes = await axios.get(`${SERVER}/sm/list/all/0`)
const creators = creatorsRes.data
// console.log('creators', creators)
setCreators(creators)
setLoaded(true)
for (let i = 0; i < creators.length; i++) {
try {
const creator = creators[i]
const profile = await loadProfile(creator.pubkey)
// the folowing lines should re-render the ContentCard
setCreators(prevCreators => {
const updatedCreators = [...prevCreators]
updatedCreators[i] = { ...updatedCreators[i], profile }
return updatedCreators
})
} catch (error) {
console.warn(error)
}
}
} catch (error) {
setLoaded(true)
}
@@ -63,7 +110,21 @@ function ContentCreators (props) {
loadCreators()
getFollowList()
}
}, [loaded, followList, getFollowList])
}, [loaded, followList, getFollowList, loadProfile])
const filteredCreators = useCallback(() => {
if (!creators || creators.length === 0) {
return []
}
let filtered = [...creators]
if (selectedFilter === 'Most Followers') {
filtered = filtered.sort((a, b) => (b.followerCnt || 0) - (a.followerCnt || 0))
}
return filtered
}, [creators, selectedFilter])
return (
<Container className='mt-4 mb-5'>
@@ -74,13 +135,25 @@ function ContentCreators (props) {
</p>
</div>
<div className='mb-4'>
<h3>Filters</h3>
<Form.Select>
<option>Most Followers</option>
<option>Most Tokens</option>
<option>Most Likes</option>
</Form.Select>
<div className='mb-4 d-flex justify-content-end'>
<Dropdown>
<Dropdown.Toggle variant='outline-secondary' className='d-flex align-items-center gap-2'>
<FontAwesomeIcon icon={faFilter} />
<span>{selectedFilter}</span>
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item onClick={() => setSelectedFilter('Most Followers')}>
Most Followers
</Dropdown.Item>
<Dropdown.Item onClick={() => setSelectedFilter('Most Tokens')}>
Most Tokens
</Dropdown.Item>
<Dropdown.Item onClick={() => setSelectedFilter('Most Likes')}>
Most Likes
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</div>
{!loaded && (
@@ -92,7 +165,7 @@ function ContentCreators (props) {
{loaded && (
<div>
{creators.map((creator, i) => (
{filteredCreators().map((creator, i) => (
<ContentCard
key={`creator-key${i}`}
creator={creator}
@@ -104,7 +177,7 @@ function ContentCreators (props) {
</div>
)}
{loaded && creators.length === 0 && (
{loaded && filteredCreators().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>
+1 -1
View File
@@ -15,7 +15,7 @@ const config = {
radicleUrl: 'https://app.radicle.network/seeds/maple.radicle.garden/rad:git:hnrkd5cjwwb5tzx37hq9uqm5ubon7ee468xcy/remotes/hyyycncbn9qzqmobnhjq9rry6t4mbjiadzjoyhaknzxjcz3cxkpfpc',
dexServer: 'https://dex-api.fullstack.cash',
//dexServer: 'http://localhost:5700',
// dexServer: 'http://localhost:5700',
nostrTopic: 'bch-dex-test-topic-02',
nostrRelay: 'wss://nostr-relay.psfoundation.info'