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", "name": "rondevu-demo",
"version": "0.5.0", "version": "0.5.0",
"dependencies": { "dependencies": {
"@xtr-dev/rondevu-client": "^0.7.3", "@xtr-dev/rondevu-client": "^0.7.4",
"@zxing/library": "^0.21.3", "@zxing/library": "^0.21.3",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"react": "^18.2.0", "react": "^18.2.0",
@@ -1171,9 +1171,9 @@
} }
}, },
"node_modules/@xtr-dev/rondevu-client": { "node_modules/@xtr-dev/rondevu-client": {
"version": "0.7.3", "version": "0.7.4",
"resolved": "https://registry.npmjs.org/@xtr-dev/rondevu-client/-/rondevu-client-0.7.3.tgz", "resolved": "https://registry.npmjs.org/@xtr-dev/rondevu-client/-/rondevu-client-0.7.4.tgz",
"integrity": "sha512-WcKc+q1JOh/v5doX0PaX9pYIJa0ofJHgxUo+xdOIjuBjUQuQW+F1G71NxtzCia2A62VPJdctL5TgADNKYmlIHQ==", "integrity": "sha512-MPmw9iSc7LxLduu4TtVrcPvBl/Cuul5sqgOAKUWW7XYXYAObFYUtu9RcbWShR+a6Bwwx7oHadz5I2U8eWsebXQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@xtr-dev/rondevu-client": "^0.5.1" "@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" "deploy": "npm run build && npx wrangler pages deploy dist --project-name=rondevu-demo"
}, },
"dependencies": { "dependencies": {
"@xtr-dev/rondevu-client": "^0.7.3", "@xtr-dev/rondevu-client": "^0.7.4",
"@zxing/library": "^0.21.3", "@zxing/library": "^0.21.3",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"react": "^18.2.0", "react": "^18.2.0",

View File

@@ -33,8 +33,11 @@ export default function App() {
const [myConnections, setMyConnections] = useState([]); const [myConnections, setMyConnections] = useState([]);
// Discovery state // Discovery state
const [searchTopic, setSearchTopic] = useState('demo-chat'); const [selectedTopic, setSelectedTopic] = useState(null);
const [discoveredOffers, setDiscoveredOffers] = useState([]); const [discoveredOffers, setDiscoveredOffers] = useState([]);
const [topics, setTopics] = useState([]);
const [topicsLoading, setTopicsLoading] = useState(false);
const [topicsError, setTopicsError] = useState(null);
// Messages // Messages
const [messages, setMessages] = useState([]); const [messages, setMessages] = useState([]);
@@ -192,8 +195,32 @@ export default function App() {
} }
}; };
// Discover peers // Fetch available topics from server
const handleDiscoverPeers = async () => { 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) return;
if (!client.isAuthenticated()) { if (!client.isAuthenticated()) {
@@ -202,13 +229,14 @@ export default function App() {
} }
try { try {
const offers = await client.offers.findByTopic(searchTopic.trim(), {limit: 50}); setSelectedTopic(topicName);
const offers = await client.offers.findByTopic(topicName, {limit: 50});
setDiscoveredOffers(offers); setDiscoveredOffers(offers);
if (offers.length === 0) { if (offers.length === 0) {
toast.error('No peers found!'); toast(`No peers found for "${topicName}"`);
} else { } else {
toast.success(`Found ${offers.length} peer(s)`); toast.success(`Found ${offers.length} peer(s) for "${topicName}"`);
} }
} catch (err) { } catch (err) {
toast.error(`Error: ${err.message}`); toast.error(`Error: ${err.message}`);
@@ -600,27 +628,87 @@ export default function App() {
{activeTab === 'discover' && ( {activeTab === 'discover' && (
<div> <div>
<h2>Discover Peers</h2> <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'}}> {!selectedTopic ? (
<label style={styles.label}>Topic:</label> <div>
<div style={{display: 'flex', gap: '10px'}}> <div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px'}}>
<input <h3>Active Topics ({topics.length})</h3>
type="text" <button onClick={fetchTopics} style={styles.btnSecondary} disabled={topicsLoading}>
value={searchTopic} {topicsLoading ? '⟳ Loading...' : '🔄 Refresh'}
onChange={(e) => setSearchTopic(e.target.value)} </button>
onKeyPress={(e) => e.key === 'Enter' && handleDiscoverPeers()} </div>
style={{...styles.input, flex: 1}}
/> {topicsLoading ? (
<button onClick={handleDiscoverPeers} style={styles.btnPrimary}> <div style={{textAlign: 'center', padding: '60px', color: '#999'}}>
🔍 Search <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> </button>
</div> </div>
</div> </div>
) : topics.length === 0 ? (
{discoveredOffers.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> <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 => { {discoveredOffers.map(offer => {
const isConnected = myConnections.some(c => c.id === offer.id); const isConnected = myConnections.some(c => c.id === offer.id);
const isMine = credentials && offer.peerId === credentials.peerId; const isMine = credentials && offer.peerId === credentials.peerId;
@@ -647,6 +735,16 @@ export default function App() {
); );
})} })}
</div> </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> </div>
)} )}
@@ -858,6 +956,16 @@ const styles = {
fontWeight: '600', fontWeight: '600',
width: '100%' width: '100%'
}, },
btnSecondary: {
padding: '10px 20px',
background: '#f5f5f5',
color: '#333',
border: '2px solid #e0e0e0',
borderRadius: '8px',
cursor: 'pointer',
fontSize: '0.95em',
fontWeight: '600'
},
btnDanger: { btnDanger: {
padding: '12px 24px', padding: '12px 24px',
background: '#f44336', background: '#f44336',
@@ -895,5 +1003,11 @@ const styles = {
color: 'white', color: 'white',
opacity: 0.8, opacity: 0.8,
fontSize: '0.9em' 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); transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(85, 104, 211, 0.4); 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);
}