mirror of
https://github.com/xtr-dev/rondevu-demo.git
synced 2025-12-10 02:43:23 +00:00
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:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -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"
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
204
src/App.jsx
204
src/App.jsx
@@ -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,52 +628,122 @@ 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 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
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{discoveredOffers.length > 0 && (
|
|
||||||
<div>
|
<div>
|
||||||
<h3>Found {discoveredOffers.length} Peer(s)</h3>
|
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px'}}>
|
||||||
{discoveredOffers.map(offer => {
|
<h3>Active Topics ({topics.length})</h3>
|
||||||
const isConnected = myConnections.some(c => c.id === offer.id);
|
<button onClick={fetchTopics} style={styles.btnSecondary} disabled={topicsLoading}>
|
||||||
const isMine = credentials && offer.peerId === credentials.peerId;
|
{topicsLoading ? '⟳ Loading...' : '🔄 Refresh'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
return (
|
{topicsLoading ? (
|
||||||
<div key={offer.id} style={styles.card}>
|
<div style={{textAlign: 'center', padding: '60px', color: '#999'}}>
|
||||||
<div style={{marginBottom: '10px'}}>
|
<div style={{fontSize: '3em', marginBottom: '10px'}}>⟳</div>
|
||||||
<div style={{fontWeight: '600'}}>{offer.topics.join(', ')}</div>
|
<div>Loading topics...</div>
|
||||||
<div style={{fontSize: '0.85em', color: '#666'}}>
|
</div>
|
||||||
Peer: {offer.peerId.substring(0, 16)}...
|
) : 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>
|
||||||
|
) : 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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<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>
|
||||||
|
|
||||||
{isMine ? (
|
{discoveredOffers.length > 0 ? (
|
||||||
<div style={{...styles.badge, background: '#2196f3'}}>Your offer</div>
|
<div>
|
||||||
) : isConnected ? (
|
<p style={{marginBottom: '15px', color: '#666'}}>
|
||||||
<div style={{...styles.badge, background: '#4caf50'}}>✓ Connected</div>
|
Found {discoveredOffers.length} peer(s)
|
||||||
) : (
|
</p>
|
||||||
<button onClick={() => handleAnswerOffer(offer)} style={styles.btnSuccess}>
|
{discoveredOffers.map(offer => {
|
||||||
🤝 Connect
|
const isConnected = myConnections.some(c => c.id === offer.id);
|
||||||
</button>
|
const isMine = credentials && offer.peerId === credentials.peerId;
|
||||||
)}
|
|
||||||
|
return (
|
||||||
|
<div key={offer.id} style={styles.card}>
|
||||||
|
<div style={{marginBottom: '10px'}}>
|
||||||
|
<div style={{fontWeight: '600'}}>{offer.topics.join(', ')}</div>
|
||||||
|
<div style={{fontSize: '0.85em', color: '#666'}}>
|
||||||
|
Peer: {offer.peerId.substring(0, 16)}...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isMine ? (
|
||||||
|
<div style={{...styles.badge, background: '#2196f3'}}>Your offer</div>
|
||||||
|
) : isConnected ? (
|
||||||
|
<div style={{...styles.badge, background: '#4caf50'}}>✓ Connected</div>
|
||||||
|
) : (
|
||||||
|
<button onClick={() => handleAnswerOffer(offer)} style={styles.btnSuccess}>
|
||||||
|
🤝 Connect
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</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>
|
||||||
)}
|
)}
|
||||||
</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'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user