mirror of
https://github.com/xtr-dev/rondevu-demo.git
synced 2025-12-10 18:53:24 +00:00
Compare commits
57 Commits
v1.0.2
...
d575022412
| Author | SHA1 | Date | |
|---|---|---|---|
| d575022412 | |||
| 84ceae9a05 | |||
| c5f640bc62 | |||
| 9163e5166c | |||
| 7d3b19a2b0 | |||
| 7d19557966 | |||
| 70fd6bd16a | |||
| 6dece31f2d | |||
| b741e8f40c | |||
| 2c20af83c9 | |||
| 78c16c95f5 | |||
| 953f62ce81 | |||
| c46bfb40a9 | |||
| 50eeec5164 | |||
| 217b84701f | |||
| 273b6349c6 | |||
| 37c84b7553 | |||
| 2d1b2e8ff4 | |||
| c849f6a109 | |||
| 0348ef5d8e | |||
| 1ac1121793 | |||
| caedff590f | |||
| d922329437 | |||
| 4c58dee371 | |||
| e4868c085b | |||
| 9a424b6015 | |||
| 79030adb09 | |||
| 1f1502940b | |||
| 348a732178 | |||
| 9e761546e7 | |||
| f9fb74de53 | |||
| e5e28c8264 | |||
| 4021a02f6d | |||
| 73f04bc078 | |||
| 257d8f264a | |||
| 678f692b64 | |||
| 83df6aeee3 | |||
| c3045778eb | |||
| dc856b7abf | |||
| ed8709c6f6 | |||
| b27ab02552 | |||
| b321a01d5e | |||
| 2dc4c711e3 | |||
| b8637ed8ad | |||
| 94b2849971 | |||
| 65f4aaffe0 | |||
| e1c8c25ea8 | |||
| 600d6308b9 | |||
| adc363fed0 | |||
| d677f36eeb | |||
| eaf474a984 | |||
| 1efe7346f4 | |||
| 1bbce9295d | |||
| 6538b5a18f | |||
| ee440b083d | |||
| 60f1068bd1 | |||
| 55a3d0ba51 |
66
CLAUDE.md
Normal file
66
CLAUDE.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Rondevu Demo Development Guidelines
|
||||
|
||||
## WebRTC Configuration
|
||||
|
||||
### TURN Server Setup
|
||||
|
||||
When configuring TURN servers:
|
||||
|
||||
- ✅ **DO** include the port number in TURN URLs: `turn:server.com:3478`
|
||||
- ✅ **DO** test TURN connectivity before deploying: `turnutils_uclient -u user -w pass server.com 3478 -y`
|
||||
- ✅ **DO** provide both TCP and UDP transports for maximum compatibility
|
||||
- ❌ **DON'T** omit the port number (even if it's the default 3478)
|
||||
- ❌ **DON'T** assume TURN works without testing
|
||||
|
||||
### ICE Configuration
|
||||
|
||||
**Force Relay Mode for Testing:**
|
||||
```javascript
|
||||
const RTC_CONFIG = {
|
||||
iceServers: [...],
|
||||
iceTransportPolicy: 'relay' // Forces TURN relay, bypasses NAT issues
|
||||
};
|
||||
```
|
||||
|
||||
Use `iceTransportPolicy: 'relay'` to:
|
||||
- Test if TURN server is working correctly
|
||||
- Bypass NAT hairpinning issues (when both peers are on same network)
|
||||
- Ensure maximum compatibility
|
||||
|
||||
**Remove or comment out** `iceTransportPolicy: 'relay'` for production to allow direct connections when possible.
|
||||
|
||||
## Debugging
|
||||
|
||||
### Enable Detailed ICE Logging
|
||||
|
||||
The demo includes detailed ICE candidate logging. Check browser console for:
|
||||
- 🧊 ICE candidate gathering
|
||||
- 🧊 ICE connection state changes
|
||||
- 📤 Candidates sent to server
|
||||
- 📥 Candidates received from server
|
||||
- ✅ Successful candidate pairs
|
||||
- ❌ Failed candidate pairs
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Connection stuck in "connecting":**
|
||||
- Enable relay-only mode to test TURN
|
||||
- Check if both peers are behind same NAT (hairpinning issue)
|
||||
- Verify TURN credentials are correct
|
||||
|
||||
2. **No candidates gathered:**
|
||||
- Check STUN/TURN server URLs
|
||||
- Verify firewall isn't blocking UDP ports
|
||||
- Check TURN server is running
|
||||
|
||||
3. **Candidates gathered but connection fails:**
|
||||
- Check if TURN relay is actually working (use `turnutils_uclient`)
|
||||
- Verify server is filtering candidates by role correctly
|
||||
- Enable detailed logging to see which candidate pairs are failing
|
||||
|
||||
## UI Guidelines
|
||||
|
||||
- Show clear connection status (waiting, connecting, connected, failed)
|
||||
- Display peer role (offerer vs answerer) for debugging
|
||||
- Provide visual feedback for all user actions
|
||||
- Use toast notifications for errors and success messages
|
||||
332
README.md
332
README.md
@@ -1,51 +1,54 @@
|
||||
# Rondevu
|
||||
# Rondevu Demo
|
||||
|
||||
🎯 **Simple WebRTC peer signaling and discovery**
|
||||
🎯 **Interactive WebRTC peer discovery and connection demo**
|
||||
|
||||
Meet peers by topic, by peer ID, or by connection ID.
|
||||
Experience topic-based peer discovery and WebRTC connections using the Rondevu signaling platform.
|
||||
|
||||
**Related repositories:**
|
||||
- [rondevu-server](https://github.com/xtr-dev/rondevu-server) - HTTP signaling server
|
||||
- [rondevu-client](https://github.com/xtr-dev/rondevu-client) - TypeScript client library
|
||||
- [@xtr-dev/rondevu-client](https://github.com/xtr-dev/rondevu-client) - TypeScript client library ([npm](https://www.npmjs.com/package/@xtr-dev/rondevu-client))
|
||||
- [@xtr-dev/rondevu-server](https://github.com/xtr-dev/rondevu-server) - HTTP signaling server ([npm](https://www.npmjs.com/package/@xtr-dev/rondevu-server), [live](https://api.ronde.vu))
|
||||
- [@xtr-dev/rondevu-demo](https://github.com/xtr-dev/rondevu-demo) - Interactive demo ([live](https://ronde.vu))
|
||||
|
||||
---
|
||||
|
||||
## Rondevu Demo
|
||||
## Overview
|
||||
|
||||
**Interactive demo showcasing three ways to connect WebRTC peers.**
|
||||
This demo showcases the complete Rondevu workflow:
|
||||
|
||||
Experience how easy WebRTC peer discovery can be with Rondevu's three connection methods:
|
||||
1. **Register** - Get peer credentials (automatically saved)
|
||||
2. **Create Offers** - Advertise your WebRTC connection on topics
|
||||
3. **Discover Peers** - Find other peers by topic
|
||||
4. **Connect** - Establish direct P2P WebRTC connections via `RondevuPeer`
|
||||
5. **Chat** - Send messages over WebRTC data channels
|
||||
|
||||
🎯 **Connect by Topic** - Auto-discover and join any available peer
|
||||
👤 **Connect by Peer ID** - Filter and connect to specific peers
|
||||
🔗 **Connect by Connection ID** - Share a code and connect directly
|
||||
### Key Features
|
||||
|
||||
### Features
|
||||
- **Topic-Based Discovery** - Find peers by shared topics (like torrent infohashes)
|
||||
- **Real P2P Connections** - Actual WebRTC data channels (not simulated)
|
||||
- **State-Based Peer Management** - Uses `RondevuPeer` with clean state machine (idle → creating-offer → waiting-for-answer → exchanging-ice → connected)
|
||||
- **Trickle ICE** - Fast connection establishment by sending ICE candidates as they're discovered
|
||||
- **Persistent Credentials** - Saves authentication to localStorage
|
||||
- **Topics Browser** - Browse all active topics and peer counts
|
||||
- **Multiple Connections** - Support multiple simultaneous peer connections
|
||||
- **Real-time Chat** - Direct peer-to-peer messaging
|
||||
|
||||
- **Three Connection Methods** - Experience topic discovery, peer filtering, and direct connection
|
||||
- **Real WebRTC** - Actual P2P connections using RTCPeerConnection (not simulated!)
|
||||
- **P2P Data Channel** - Direct peer-to-peer chat without server relay
|
||||
- **Peer Discovery** - Browse topics and discover available peers
|
||||
- **Real-time Chat** - Send and receive messages over WebRTC data channel
|
||||
- **Activity Log** - Monitor all API and WebRTC events
|
||||
## Quick Start
|
||||
|
||||
### Quick Start
|
||||
|
||||
#### Installation
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
#### Development
|
||||
### Development
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
This will start the Vite dev server at `http://localhost:5173`
|
||||
This starts the Vite dev server at `http://localhost:5173`
|
||||
|
||||
#### Build for Production
|
||||
### Build for Production
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
@@ -53,150 +56,190 @@ npm run build
|
||||
|
||||
The built files will be in the `dist/` directory.
|
||||
|
||||
#### Preview Production Build
|
||||
### Preview Production Build
|
||||
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
### Three Ways to Connect
|
||||
## How to Use
|
||||
|
||||
This demo demonstrates all three Rondevu connection methods:
|
||||
### Step 1: Register (One-time)
|
||||
|
||||
#### 1️⃣ Join Topic (Auto-Discovery)
|
||||
The demo automatically registers you when you first visit. Your credentials are saved in localStorage for future visits.
|
||||
|
||||
**Easiest method** - Just enter a topic and auto-connect to first available peer:
|
||||
### Step 2: Create an Offer
|
||||
|
||||
1. Enter a topic name in the "Join Topic" section (e.g., "demo-room")
|
||||
2. Click "Join Topic"
|
||||
3. Rondevu finds the first available peer and connects automatically
|
||||
4. Start chatting!
|
||||
1. Go to the "Create Offer" tab
|
||||
2. Enter one or more topics (comma-separated), e.g., `demo-room, testing`
|
||||
3. Click "Create Offer"
|
||||
4. Your offer is now advertised on those topics
|
||||
|
||||
**Best for:** Quick matching, joining any available game/chat
|
||||
**Share the topic name with peers you want to connect with!**
|
||||
|
||||
---
|
||||
### Step 3: Discover and Connect (Other Peer)
|
||||
|
||||
#### 2️⃣ Discover Peers (Filter by Peer ID)
|
||||
1. Go to the "Discover Offers" tab
|
||||
2. Enter the same topic (e.g., `demo-room`)
|
||||
3. Click "Discover Offers"
|
||||
4. See available peers and their offers
|
||||
5. Click "Answer Offer" to connect
|
||||
|
||||
**Connect to specific peers** - Browse and select which peer to connect to:
|
||||
### Step 4: Chat
|
||||
|
||||
1. Enter a topic name (e.g., "demo-room")
|
||||
2. Click "Discover in [topic]" to list all available peers
|
||||
3. See each peer's ID in the list
|
||||
4. Click "Connect" on the specific peer you want to talk to
|
||||
5. Start chatting!
|
||||
1. Once connected, go to the "Chat" tab
|
||||
2. Select a connection from the dropdown
|
||||
3. Type messages and hit Enter or click Send
|
||||
4. Messages are sent **directly peer-to-peer** via WebRTC
|
||||
|
||||
**Best for:** Connecting to friends, teammates, or specific users
|
||||
### Browse Topics
|
||||
|
||||
---
|
||||
Click the "Topics" tab to:
|
||||
- See all active topics
|
||||
- View peer counts for each topic
|
||||
- Quick-discover by clicking a topic
|
||||
|
||||
#### 3️⃣ Create/Connect by ID (Direct Connection)
|
||||
|
||||
**Share a connection code** - Like sharing a meeting link:
|
||||
|
||||
**To create:**
|
||||
1. Enter a topic name (e.g., "meetings")
|
||||
2. Enter a custom Connection ID (e.g., "my-meeting-123") or leave blank for auto-generation
|
||||
3. Click "Create Connection"
|
||||
4. **Share the Connection ID** with the person you want to connect with
|
||||
|
||||
**To join:**
|
||||
1. Get the Connection ID from your friend (e.g., "my-meeting-123")
|
||||
2. Enter it in the "Connect by ID" section
|
||||
3. Click "Connect to ID"
|
||||
4. Start chatting!
|
||||
|
||||
**Best for:** Meeting rooms, QR code connections, invitation-based sessions
|
||||
|
||||
#### Testing Locally
|
||||
## Testing Locally
|
||||
|
||||
The easiest way to test:
|
||||
1. Open the demo in **two different browser windows** (or tabs)
|
||||
2. In window 1: Create an offer with topic "test-room"
|
||||
3. In window 2: Discover peers in "test-room" and click Connect
|
||||
4. Watch the connection establish and start chatting!
|
||||
|
||||
#### Browse Topics
|
||||
1. Open the demo in **two browser windows** (or tabs)
|
||||
2. **Window 1**: Create an offer with topic `test-room`
|
||||
3. **Window 2**: Discover offers in `test-room` and answer
|
||||
4. Switch to Chat tab in both windows
|
||||
5. Start chatting peer-to-peer!
|
||||
|
||||
- Click "Refresh Topics" to see all active topics
|
||||
- Click on any topic to auto-fill the discovery form
|
||||
## Technical Implementation
|
||||
|
||||
### Server Configuration
|
||||
### RondevuPeer State Machine
|
||||
|
||||
This demo connects to: `https://rondevu.xtrdev.workers.dev`
|
||||
|
||||
To use a different server, modify the `baseUrl` in `src/main.js`:
|
||||
This demo uses the `RondevuPeer` class which implements a clean state-based connection lifecycle:
|
||||
|
||||
```javascript
|
||||
const client = new RondevuClient({
|
||||
baseUrl: 'https://your-server.com'
|
||||
import { Rondevu } from '@xtr-dev/rondevu-client';
|
||||
|
||||
// Create peer
|
||||
const peer = client.createPeer();
|
||||
|
||||
// Set up event listeners
|
||||
peer.on('state', (state) => {
|
||||
console.log('Peer state:', state);
|
||||
// Offerer: idle → creating-offer → waiting-for-answer → exchanging-ice → connected
|
||||
// Answerer: idle → answering → exchanging-ice → connected
|
||||
});
|
||||
|
||||
peer.on('connected', () => {
|
||||
console.log('✅ P2P connection established!');
|
||||
});
|
||||
|
||||
peer.on('datachannel', (channel) => {
|
||||
channel.addEventListener('message', (event) => {
|
||||
console.log('📥 Message:', event.data);
|
||||
});
|
||||
|
||||
channel.addEventListener('open', () => {
|
||||
// Channel is ready, can send messages
|
||||
channel.send('Hello!');
|
||||
});
|
||||
});
|
||||
|
||||
peer.on('failed', (error) => {
|
||||
console.error('❌ Connection failed:', error);
|
||||
});
|
||||
|
||||
// Create offer (offerer)
|
||||
await peer.createOffer({
|
||||
topics: ['demo-room'],
|
||||
ttl: 300000
|
||||
});
|
||||
|
||||
// Or answer an offer (answerer)
|
||||
await peer.answer(offerId, offerSdp, {
|
||||
topics: ['demo-room']
|
||||
});
|
||||
```
|
||||
|
||||
### Technologies
|
||||
### Connection States
|
||||
|
||||
- **Vite** - Fast development and build tool
|
||||
- **@xtr-dev/rondevu-client** - TypeScript client for Rondevu API
|
||||
- **Vanilla JavaScript** - No framework dependencies
|
||||
**Offerer Flow:**
|
||||
1. **idle** - Initial state
|
||||
2. **creating-offer** - Creating WebRTC offer and sending to server
|
||||
3. **waiting-for-answer** - Polling for answer from peer (every 2 seconds)
|
||||
4. **exchanging-ice** - Exchanging ICE candidates (polling every 1 second)
|
||||
5. **connected** - Successfully connected!
|
||||
6. **failed/closed** - Connection failed or was closed
|
||||
|
||||
### API Examples
|
||||
**Answerer Flow:**
|
||||
1. **idle** - Initial state
|
||||
2. **answering** - Creating WebRTC answer and sending to server
|
||||
3. **exchanging-ice** - Exchanging ICE candidates (polling every 1 second)
|
||||
4. **connected** - Successfully connected!
|
||||
5. **failed/closed** - Connection failed or was closed
|
||||
|
||||
The demo showcases all major Rondevu API endpoints:
|
||||
### What Happens Under the Hood
|
||||
|
||||
- `GET /` - List all topics
|
||||
- `GET /:topic/sessions` - Discover peers in a topic
|
||||
- `POST /:topic/offer` - Create a new offer
|
||||
- `POST /answer` - Send answer to a peer
|
||||
- `POST /poll` - Poll for peer data
|
||||
- `GET /health` - Check server health
|
||||
1. **Offerer** calls `peer.createOffer()`:
|
||||
- State → `creating-offer`
|
||||
- Creates RTCPeerConnection and data channel
|
||||
- Generates SDP offer
|
||||
- Sets up ICE candidate handler (before gathering starts)
|
||||
- Sets local description → ICE gathering begins
|
||||
- Posts offer to Rondevu server
|
||||
- State → `waiting-for-answer`
|
||||
- Polls for answers every 2 seconds
|
||||
- When answer received → State → `exchanging-ice`
|
||||
|
||||
### WebRTC Implementation Details
|
||||
2. **Answerer** calls `peer.answer()`:
|
||||
- State → `answering`
|
||||
- Creates RTCPeerConnection
|
||||
- Sets remote description (offer SDP)
|
||||
- Generates SDP answer
|
||||
- Sends answer to server (registers as answerer)
|
||||
- Sets up ICE candidate handler (before gathering starts)
|
||||
- Sets local description → ICE gathering begins
|
||||
- State → `exchanging-ice`
|
||||
|
||||
This demo implements a **complete WebRTC peer-to-peer connection** with:
|
||||
3. **ICE Exchange** (Trickle ICE):
|
||||
- Both peers generate ICE candidates as they're discovered
|
||||
- Candidates are automatically sent to server immediately
|
||||
- Peers poll and receive remote candidates (every 1 second)
|
||||
- ICE establishes the direct P2P path
|
||||
- State → `connected`
|
||||
|
||||
#### Connection Flow
|
||||
4. **Connection Established**:
|
||||
- Data channel opens
|
||||
- Chat messages flow directly between peers
|
||||
- No server relay (true P2P!)
|
||||
|
||||
1. **Offerer** creates an `RTCPeerConnection` and generates an SDP offer
|
||||
2. Offer is sent to the Rondevu signaling server via `POST /:topic/offer`
|
||||
3. **Answerer** discovers the offer via `GET /:topic/sessions`
|
||||
4. Answerer creates an `RTCPeerConnection`, sets the remote offer, and generates an SDP answer
|
||||
5. Answer is sent via `POST /answer`
|
||||
6. Both peers generate ICE candidates and send them via `POST /answer` with `candidate` field
|
||||
7. Both peers poll via `POST /poll` to receive remote ICE candidates
|
||||
8. Once candidates are exchanged, the **direct P2P connection** is established
|
||||
9. Data channel opens and chat messages flow **directly between peers**
|
||||
### Key Features of Implementation
|
||||
|
||||
#### Key Features
|
||||
- **Trickle ICE**: Candidates sent immediately as discovered (no waiting)
|
||||
- **Proper Authorization**: Answer sent to server before ICE gathering to authorize candidate posting
|
||||
- **Event Cleanup**: All event listeners properly removed with `removeEventListener`
|
||||
- **State Management**: Clean state machine with well-defined transitions
|
||||
- **Error Handling**: Graceful failure states with error events
|
||||
|
||||
- **Real RTCPeerConnection** - Not simulated, actual WebRTC
|
||||
- **STUN servers** - Google's public STUN servers for NAT traversal
|
||||
- **Data Channel** - Named "chat" channel for text messaging
|
||||
- **ICE Trickle** - Candidates are sent as they're generated
|
||||
- **Automatic Polling** - Polls every 1 second for remote data
|
||||
- **Connection States** - Visual indicators for connecting/connected/failed states
|
||||
- **Graceful Cleanup** - Properly closes connections and stops polling
|
||||
### Architecture
|
||||
|
||||
#### Technologies
|
||||
- **Frontend**: React + Vite
|
||||
- **Signaling**: Rondevu server (Cloudflare Workers + D1)
|
||||
- **Client**: @xtr-dev/rondevu-client (TypeScript library)
|
||||
- **WebRTC**: RTCPeerConnection with STUN/TURN servers
|
||||
- **Connection Management**: RondevuPeer class with state machine
|
||||
|
||||
- **RTCPeerConnection API** - Core WebRTC connection
|
||||
- **RTCDataChannel API** - Unreliable but fast text messaging
|
||||
- **Rondevu Signaling** - SDP and ICE candidate exchange
|
||||
- **STUN Protocol** - NAT traversal (stun.l.google.com)
|
||||
## Server Configuration
|
||||
|
||||
### Development Notes
|
||||
This demo connects to: `https://api.ronde.vu`
|
||||
|
||||
- Peer IDs are auto-generated on page load
|
||||
- WebRTC connections use **real** RTCPeerConnection (not simulated!)
|
||||
- Sessions expire after the server's configured timeout (5 minutes default)
|
||||
- The demo is completely client-side (no backend required)
|
||||
- Messages are sent P2P - the server only facilitates discovery
|
||||
- Works across different browsers and networks (with STUN support)
|
||||
To use a different server, modify `API_URL` in `src/App.jsx`:
|
||||
|
||||
### Deployment
|
||||
```javascript
|
||||
const API_URL = 'https://your-server.com';
|
||||
```
|
||||
|
||||
#### Deploy to Cloudflare Pages
|
||||
## Deployment
|
||||
|
||||
The demo can be easily deployed to Cloudflare Pages (free tier):
|
||||
### Deploy to Cloudflare Pages
|
||||
|
||||
**Quick Deploy via Wrangler:**
|
||||
|
||||
@@ -213,7 +256,46 @@ npx wrangler pages deploy dist --project-name=rondevu-demo
|
||||
4. Set output directory: `dist`
|
||||
5. Deploy automatically on every push!
|
||||
|
||||
### License
|
||||
## Development Notes
|
||||
|
||||
- Credentials are stored in localStorage and persist across sessions
|
||||
- Offers expire after 5 minutes by default
|
||||
- The peer automatically polls for answers and ICE candidates
|
||||
- Multiple simultaneous connections are supported
|
||||
- WebRTC uses Google's public STUN servers + custom TURN server for NAT traversal
|
||||
- Data channel messages are unreliable but fast (perfect for chat)
|
||||
- Connection cleanup is automatic when peers disconnect
|
||||
|
||||
## Connection Timeouts
|
||||
|
||||
The demo uses these default timeouts:
|
||||
|
||||
- **ICE Gathering**: 10 seconds (not used with trickle ICE)
|
||||
- **Waiting for Answer**: 30 seconds
|
||||
- **Creating Answer**: 10 seconds
|
||||
- **ICE Connection**: 30 seconds
|
||||
|
||||
These can be customized in the `PeerOptions`:
|
||||
|
||||
```javascript
|
||||
await peer.createOffer({
|
||||
topics: ['my-topic'],
|
||||
timeouts: {
|
||||
waitingForAnswer: 60000, // 1 minute
|
||||
iceConnection: 45000 // 45 seconds
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Technologies
|
||||
|
||||
- **React** - UI framework
|
||||
- **Vite** - Build tool and dev server
|
||||
- **@xtr-dev/rondevu-client** - Rondevu client library with `RondevuPeer`
|
||||
- **RTCPeerConnection** - WebRTC connections
|
||||
- **RTCDataChannel** - P2P messaging
|
||||
- **QRCode** - QR code generation for easy topic sharing
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
|
||||
50
package-lock.json
generated
50
package-lock.json
generated
@@ -1,17 +1,19 @@
|
||||
{
|
||||
"name": "rondevu-demo",
|
||||
"version": "1.0.2",
|
||||
"version": "0.5.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "rondevu-demo",
|
||||
"version": "1.0.2",
|
||||
"version": "0.5.0",
|
||||
"dependencies": {
|
||||
"@xtr-dev/rondevu-client": "^0.7.4",
|
||||
"@zxing/library": "^0.21.3",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hot-toast": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.0",
|
||||
@@ -1168,6 +1170,21 @@
|
||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xtr-dev/rondevu-client": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/@xtr-dev/rondevu-client/node_modules/@xtr-dev/rondevu-client": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@xtr-dev/rondevu-client/-/rondevu-client-0.5.1.tgz",
|
||||
"integrity": "sha512-110ejMCizPUPkHwwwNvcdCSZceLaHeFbf1LNkXvbG6pnLBqCf2uoGOOaRkArb7HNNFABFB+HXzm/AVzNdadosw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@zxing/library": {
|
||||
"version": "0.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.21.3.tgz",
|
||||
@@ -1328,7 +1345,6 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
@@ -1473,6 +1489,15 @@
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/goober": {
|
||||
"version": "2.1.18",
|
||||
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz",
|
||||
"integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"csstype": "^3.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
@@ -1713,6 +1738,23 @@
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hot-toast": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
|
||||
"integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.1.3",
|
||||
"goober": "^2.1.16"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16",
|
||||
"react-dom": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "rondevu-demo",
|
||||
"version": "1.0.2",
|
||||
"description": "Demo application for Rondevu peer signaling and discovery",
|
||||
"version": "0.5.0",
|
||||
"description": "Demo application for Rondevu topic-based peer discovery and signaling",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -10,10 +10,12 @@
|
||||
"deploy": "npm run build && npx wrangler pages deploy dist --project-name=rondevu-demo"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xtr-dev/rondevu-client": "^0.7.4",
|
||||
"@zxing/library": "^0.21.3",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hot-toast": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.0",
|
||||
|
||||
1491
src/App.jsx
1491
src/App.jsx
File diff suppressed because it is too large
Load Diff
@@ -2,95 +2,31 @@ import QRCodeDisplay from './QRCodeDisplay';
|
||||
|
||||
function ConnectionForm({
|
||||
action,
|
||||
method,
|
||||
topic,
|
||||
setTopic,
|
||||
connectionId,
|
||||
setConnectionId,
|
||||
peerId,
|
||||
setPeerId,
|
||||
topics,
|
||||
sessions,
|
||||
connectionStatus,
|
||||
qrCodeUrl,
|
||||
currentConnectionId,
|
||||
onConnect,
|
||||
onBack,
|
||||
onTopicSelect,
|
||||
onDiscoverPeers
|
||||
onBack
|
||||
}) {
|
||||
return (
|
||||
<div className="step-container">
|
||||
<h2>Enter Details</h2>
|
||||
<h2>{action === 'create' ? 'Create Connection' : 'Join Connection'}</h2>
|
||||
<div className="form-container">
|
||||
{(method === 'topic' || method === 'peer-id' || (method === 'connection-id' && action === 'create')) && (
|
||||
<div className="form-group">
|
||||
<label>Topic</label>
|
||||
<input
|
||||
type="text"
|
||||
value={topic}
|
||||
onChange={(e) => setTopic(e.target.value)}
|
||||
placeholder="e.g., game-room"
|
||||
autoFocus
|
||||
/>
|
||||
{topics.length > 0 && (
|
||||
<div className="topic-list">
|
||||
{topics.map((t) => (
|
||||
<button
|
||||
key={t.topic}
|
||||
className="topic-item"
|
||||
onClick={() => {
|
||||
onTopicSelect(t.topic);
|
||||
if (method === 'peer-id') {
|
||||
onDiscoverPeers(t.topic);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t.topic} <span className="peer-count">({t.count})</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{method === 'peer-id' && (
|
||||
<div className="form-group">
|
||||
<label>Peer ID</label>
|
||||
<input
|
||||
type="text"
|
||||
value={peerId}
|
||||
onChange={(e) => setPeerId(e.target.value)}
|
||||
placeholder="e.g., player-123"
|
||||
/>
|
||||
{sessions.length > 0 && (
|
||||
<div className="topic-list">
|
||||
{sessions.map((s) => (
|
||||
<button
|
||||
key={s.code}
|
||||
className="topic-item"
|
||||
onClick={() => setPeerId(s.peerId)}
|
||||
>
|
||||
{s.peerId}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{method === 'connection-id' && (
|
||||
<div className="form-group">
|
||||
<label>Connection ID {action === 'create' && '(optional)'}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={connectionId}
|
||||
onChange={(e) => setConnectionId(e.target.value)}
|
||||
placeholder={action === 'create' ? 'Auto-generated if empty' : 'e.g., meeting-123'}
|
||||
autoFocus={action === 'join'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="form-group">
|
||||
<label>Connection ID {action === 'create' && '(optional)'}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={connectionId}
|
||||
onChange={(e) => setConnectionId(e.target.value)}
|
||||
placeholder={action === 'create' ? 'Auto-generated if empty' : 'Enter connection ID'}
|
||||
autoFocus={action === 'connect'}
|
||||
/>
|
||||
{action === 'create' && !connectionId && (
|
||||
<p className="help-text">Leave empty to auto-generate a random ID</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="button-row">
|
||||
<button className="back-button" onClick={onBack}>← Back</button>
|
||||
@@ -99,12 +35,10 @@ function ConnectionForm({
|
||||
onClick={onConnect}
|
||||
disabled={
|
||||
connectionStatus === 'connecting' ||
|
||||
(method === 'topic' && !topic) ||
|
||||
(method === 'peer-id' && (!topic || !peerId)) ||
|
||||
(method === 'connection-id' && action === 'join' && !connectionId)
|
||||
(action === 'connect' && !connectionId)
|
||||
}
|
||||
>
|
||||
{connectionStatus === 'connecting' ? 'Connecting...' : 'Connect'}
|
||||
{connectionStatus === 'connecting' ? 'Connecting...' : (action === 'create' ? 'Create' : 'Connect')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ function Header() {
|
||||
<header className="header">
|
||||
<div className="header-content">
|
||||
<h1>Rondevu</h1>
|
||||
<p className="tagline">Meet WebRTC peers by topic, peer ID, or connection ID</p>
|
||||
<p className="tagline">Simple WebRTC peer signaling and discovery. Meet peers by topic, peer ID, or connection ID.</p>
|
||||
<div className="header-links">
|
||||
<a href="https://github.com/xtr-dev/rondevu-client" target="_blank" rel="noopener noreferrer">
|
||||
<svg className="github-icon" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
||||
@@ -21,7 +21,7 @@ function Header() {
|
||||
<svg className="github-icon" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
||||
</svg>
|
||||
View source
|
||||
Demo
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
120
src/components/TopicsList.jsx
Normal file
120
src/components/TopicsList.jsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
function TopicsList({ rdv, onClose }) {
|
||||
const [topics, setTopics] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pagination, setPagination] = useState(null);
|
||||
const [limit] = useState(20);
|
||||
|
||||
useEffect(() => {
|
||||
loadTopics();
|
||||
}, [page]);
|
||||
|
||||
const loadTopics = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await rdv.api.listTopics(page, limit);
|
||||
setTopics(response.topics);
|
||||
setPagination(response.pagination);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadTopics();
|
||||
};
|
||||
|
||||
const handlePrevPage = () => {
|
||||
if (page > 1) {
|
||||
setPage(page - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNextPage = () => {
|
||||
if (pagination?.hasMore) {
|
||||
setPage(page + 1);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<div className="modal-content topics-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h2>Active Topics</h2>
|
||||
<button className="close-button" onClick={onClose}>×</button>
|
||||
</div>
|
||||
|
||||
<div className="modal-body">
|
||||
{error && (
|
||||
<div className="error-message" style={{ marginBottom: '1rem' }}>
|
||||
Error: {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="loading-message">Loading topics...</div>
|
||||
) : (
|
||||
<>
|
||||
{topics.length === 0 ? (
|
||||
<div className="empty-message">
|
||||
No active topics found. Be the first to create one!
|
||||
</div>
|
||||
) : (
|
||||
<div className="topics-list">
|
||||
{topics.map((topic) => (
|
||||
<div key={topic.topic} className="topic-item">
|
||||
<div className="topic-name">{topic.topic}</div>
|
||||
<div className="topic-count">
|
||||
{topic.count} {topic.count === 1 ? 'peer' : 'peers'}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pagination && (
|
||||
<div className="pagination">
|
||||
<button
|
||||
onClick={handlePrevPage}
|
||||
disabled={page === 1}
|
||||
className="pagination-button"
|
||||
>
|
||||
← Previous
|
||||
</button>
|
||||
<span className="pagination-info">
|
||||
Page {pagination.page} of {Math.ceil(pagination.total / pagination.limit)}
|
||||
{' '}({pagination.total} total)
|
||||
</span>
|
||||
<button
|
||||
onClick={handleNextPage}
|
||||
disabled={!pagination.hasMore}
|
||||
className="pagination-button"
|
||||
>
|
||||
Next →
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<button onClick={handleRefresh} className="button button-secondary">
|
||||
🔄 Refresh
|
||||
</button>
|
||||
<button onClick={onClose} className="button button-primary">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TopicsList;
|
||||
226
src/index.css
226
src/index.css
@@ -162,6 +162,12 @@ body {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
margin-top: 8px;
|
||||
font-size: 0.85rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
@@ -790,3 +796,223 @@ input[type="text"]:disabled {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Topics List Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 24px 32px;
|
||||
border-bottom: 1px solid #e8eaf0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
color: #6c757d;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background: #f8f9fc;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 24px 32px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 20px 32px;
|
||||
border-top: 1px solid #e8eaf0;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.topics-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.topic-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
background: #f8f9fc;
|
||||
border: 1px solid #e8eaf0;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.topic-item:hover {
|
||||
background: #f0f2f8;
|
||||
border-color: #d0d5dd;
|
||||
}
|
||||
|
||||
.topic-name {
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
font-size: 1rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.topic-count {
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
background: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 24px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e8eaf0;
|
||||
}
|
||||
|
||||
.pagination-button {
|
||||
padding: 10px 20px;
|
||||
border: 1px solid #e8eaf0;
|
||||
background: white;
|
||||
color: #1a1a1a;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.pagination-button:hover:not(:disabled) {
|
||||
background: #f8f9fc;
|
||||
border-color: #5568d3;
|
||||
color: #5568d3;
|
||||
}
|
||||
|
||||
.pagination-button:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.loading-message, .empty-message, .error-message {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #6c757d;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #dc3545;
|
||||
background: #ffe8eb;
|
||||
border: 1px solid #ffccd2;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.view-topics-button {
|
||||
position: fixed;
|
||||
bottom: 80px;
|
||||
right: 20px;
|
||||
padding: 14px 24px;
|
||||
background: #5568d3;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(85, 104, 211, 0.3);
|
||||
transition: all 0.2s;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.view-topics-button:hover {
|
||||
background: #667eea;
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user