Replace preset topics with dynamic topic listing from server

Changed discover page to fetch and display real topics from the API:
- Added fetchTopics() to call client.offers.getTopics()
- Display actual topic names and active peer counts
- Added loading, error, and empty states
- Added refresh button to reload topics
- Improved UX with better error handling

Updated to @xtr-dev/rondevu-client@0.7.4

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-16 20:17:27 +01:00
parent 7d3b19a2b0
commit 9163e5166c
4 changed files with 183 additions and 50 deletions

8
package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "rondevu-demo",
"version": "0.5.0",
"dependencies": {
"@xtr-dev/rondevu-client": "^0.7.3",
"@xtr-dev/rondevu-client": "^0.7.4",
"@zxing/library": "^0.21.3",
"qrcode": "^1.5.4",
"react": "^18.2.0",
@@ -1171,9 +1171,9 @@
}
},
"node_modules/@xtr-dev/rondevu-client": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/@xtr-dev/rondevu-client/-/rondevu-client-0.7.3.tgz",
"integrity": "sha512-WcKc+q1JOh/v5doX0PaX9pYIJa0ofJHgxUo+xdOIjuBjUQuQW+F1G71NxtzCia2A62VPJdctL5TgADNKYmlIHQ==",
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@xtr-dev/rondevu-client/-/rondevu-client-0.7.4.tgz",
"integrity": "sha512-MPmw9iSc7LxLduu4TtVrcPvBl/Cuul5sqgOAKUWW7XYXYAObFYUtu9RcbWShR+a6Bwwx7oHadz5I2U8eWsebXQ==",
"license": "MIT",
"dependencies": {
"@xtr-dev/rondevu-client": "^0.5.1"

View File

@@ -10,7 +10,7 @@
"deploy": "npm run build && npx wrangler pages deploy dist --project-name=rondevu-demo"
},
"dependencies": {
"@xtr-dev/rondevu-client": "^0.7.3",
"@xtr-dev/rondevu-client": "^0.7.4",
"@zxing/library": "^0.21.3",
"qrcode": "^1.5.4",
"react": "^18.2.0",

View File

@@ -33,8 +33,11 @@ export default function App() {
const [myConnections, setMyConnections] = useState([]);
// Discovery state
const [searchTopic, setSearchTopic] = useState('demo-chat');
const [selectedTopic, setSelectedTopic] = useState(null);
const [discoveredOffers, setDiscoveredOffers] = useState([]);
const [topics, setTopics] = useState([]);
const [topicsLoading, setTopicsLoading] = useState(false);
const [topicsError, setTopicsError] = useState(null);
// Messages
const [messages, setMessages] = useState([]);
@@ -192,8 +195,32 @@ export default function App() {
}
};
// Discover peers
const handleDiscoverPeers = async () => {
// Fetch available topics from server
const fetchTopics = async () => {
if (!client) return;
try {
setTopicsLoading(true);
setTopicsError(null);
const result = await client.offers.getTopics({ limit: 100 });
setTopics(result.topics);
} catch (err) {
console.error('Error fetching topics:', err);
setTopicsError(err.message);
} finally {
setTopicsLoading(false);
}
};
// Fetch topics when discover tab is opened
useEffect(() => {
if (activeTab === 'discover' && topics.length === 0 && !topicsLoading && client) {
fetchTopics();
}
}, [activeTab, client]);
// Discover peers by topic
const handleDiscoverPeers = async (topicName) => {
if (!client) return;
if (!client.isAuthenticated()) {
@@ -202,13 +229,14 @@ export default function App() {
}
try {
const offers = await client.offers.findByTopic(searchTopic.trim(), {limit: 50});
setSelectedTopic(topicName);
const offers = await client.offers.findByTopic(topicName, {limit: 50});
setDiscoveredOffers(offers);
if (offers.length === 0) {
toast.error('No peers found!');
toast(`No peers found for "${topicName}"`);
} else {
toast.success(`Found ${offers.length} peer(s)`);
toast.success(`Found ${offers.length} peer(s) for "${topicName}"`);
}
} catch (err) {
toast.error(`Error: ${err.message}`);
@@ -600,27 +628,87 @@ export default function App() {
{activeTab === 'discover' && (
<div>
<h2>Discover Peers</h2>
<p style={styles.desc}>Search for peers by topic</p>
<p style={styles.desc}>Browse topics to find peers</p>
<div style={{marginBottom: '20px'}}>
<label style={styles.label}>Topic:</label>
<div style={{display: 'flex', gap: '10px'}}>
<input
type="text"
value={searchTopic}
onChange={(e) => setSearchTopic(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleDiscoverPeers()}
style={{...styles.input, flex: 1}}
/>
<button onClick={handleDiscoverPeers} style={styles.btnPrimary}>
🔍 Search
{!selectedTopic ? (
<div>
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px'}}>
<h3>Active Topics ({topics.length})</h3>
<button onClick={fetchTopics} style={styles.btnSecondary} disabled={topicsLoading}>
{topicsLoading ? '⟳ Loading...' : '🔄 Refresh'}
</button>
</div>
{topicsLoading ? (
<div style={{textAlign: 'center', padding: '60px', color: '#999'}}>
<div style={{fontSize: '3em', marginBottom: '10px'}}></div>
<div>Loading topics...</div>
</div>
) : topicsError ? (
<div style={{textAlign: 'center', padding: '40px'}}>
<div style={{...styles.card, background: '#ffebee', color: '#c62828', border: '2px solid #ef9a9a'}}>
<div style={{fontSize: '2em', marginBottom: '10px'}}></div>
<div style={{fontWeight: '600', marginBottom: '5px'}}>Failed to load topics</div>
<div style={{fontSize: '0.9em'}}>{topicsError}</div>
<button onClick={fetchTopics} style={{...styles.btnPrimary, marginTop: '15px'}}>
Try Again
</button>
</div>
</div>
{discoveredOffers.length > 0 && (
) : topics.length === 0 ? (
<div style={{textAlign: 'center', padding: '60px', color: '#999'}}>
<div style={{fontSize: '3em', marginBottom: '10px'}}>📭</div>
<div style={{fontWeight: '600', marginBottom: '5px'}}>No active topics</div>
<div style={{fontSize: '0.9em'}}>Create an offer to start a new topic</div>
</div>
) : (
<div style={styles.topicsGrid}>
{topics.map(topic => (
<div
key={topic.topic}
className="topic-card-hover"
onClick={() => handleDiscoverPeers(topic.topic)}
>
<div style={{fontSize: '2.5em', marginBottom: '10px'}}>💬</div>
<div style={{fontWeight: '600', marginBottom: '5px', wordBreak: 'break-word'}}>{topic.topic}</div>
<div style={{
fontSize: '0.85em',
color: '#667eea',
fontWeight: '600',
background: '#f0f2ff',
padding: '4px 12px',
borderRadius: '12px',
display: 'inline-block',
marginTop: '5px'
}}>
{topic.activePeers} {topic.activePeers === 1 ? 'peer' : 'peers'}
</div>
</div>
))}
</div>
)}
</div>
) : (
<div>
<h3>Found {discoveredOffers.length} Peer(s)</h3>
<div style={{marginBottom: '20px'}}>
<button
onClick={() => {
setSelectedTopic(null);
setDiscoveredOffers([]);
fetchTopics(); // Refresh topics when going back
}}
style={{...styles.btnSecondary, marginBottom: '10px'}}
>
Back to Topics
</button>
<h3>Topic: {selectedTopic}</h3>
</div>
{discoveredOffers.length > 0 ? (
<div>
<p style={{marginBottom: '15px', color: '#666'}}>
Found {discoveredOffers.length} peer(s)
</p>
{discoveredOffers.map(offer => {
const isConnected = myConnections.some(c => c.id === offer.id);
const isMine = credentials && offer.peerId === credentials.peerId;
@@ -647,6 +735,16 @@ export default function App() {
);
})}
</div>
) : (
<div style={{textAlign: 'center', padding: '40px', color: '#999'}}>
<div style={{fontSize: '3em'}}>🔍</div>
<div>No peers available for this topic</div>
<div style={{fontSize: '0.9em', marginTop: '10px'}}>
Try creating an offer or check back later
</div>
</div>
)}
</div>
)}
</div>
)}
@@ -858,6 +956,16 @@ const styles = {
fontWeight: '600',
width: '100%'
},
btnSecondary: {
padding: '10px 20px',
background: '#f5f5f5',
color: '#333',
border: '2px solid #e0e0e0',
borderRadius: '8px',
cursor: 'pointer',
fontSize: '0.95em',
fontWeight: '600'
},
btnDanger: {
padding: '12px 24px',
background: '#f44336',
@@ -895,5 +1003,11 @@ const styles = {
color: 'white',
opacity: 0.8,
fontSize: '0.9em'
},
topicsGrid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
gap: '15px',
marginTop: '20px'
}
};

View File

@@ -997,3 +997,22 @@ input[type="text"]:disabled {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(85, 104, 211, 0.4);
}
/* Topic cards grid for discovery */
.topic-card-hover {
padding: 25px;
background: white;
border-radius: 12px;
border: 2px solid #e0e0e0;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.topic-card-hover:hover {
border-color: #667eea;
background: #fafbfc;
transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.2);
}