mirror of
https://github.com/xtr-dev/rondevu-demo.git
synced 2025-12-10 02:43:23 +00:00
Compare commits
78 Commits
v1.0.1
...
158e001055
| Author | SHA1 | Date | |
|---|---|---|---|
| 158e001055 | |||
| 9967e8d762 | |||
| 8c3f21f262 | |||
| c511b15fbf | |||
| 71454e31d1 | |||
| b2a17ce42b | |||
| 3a42f74371 | |||
| 2cbd46b27a | |||
| b3dde85cd2 | |||
| 66dc35c1a7 | |||
| 74bf2757ff | |||
| 2550c1ac3f | |||
| 3d9a1c27bd | |||
| e26bdc308d | |||
| 6650f51038 | |||
| 0a975f4bcf | |||
| c889549362 | |||
| fb9830ea8c | |||
| 5714731d71 | |||
| 4ff5da0568 | |||
| 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 | |||
| 2b574526d1 |
91
CLAUDE.md
Normal file
91
CLAUDE.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Rondevu Demo Development Guidelines
|
||||||
|
|
||||||
|
## WebRTC Configuration
|
||||||
|
|
||||||
|
### TURN Server Setup
|
||||||
|
|
||||||
|
When configuring TURN servers:
|
||||||
|
|
||||||
|
- ✅ **DO** use TURNS (secure) on port 5349 when available: `turns:server.com:5349`
|
||||||
|
- ✅ **DO** include TURN fallback on port 3478: `turn:server.com:3478`
|
||||||
|
- ✅ **DO** include the port number in TURN URLs (even if default)
|
||||||
|
- ✅ **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
|
||||||
|
- ❌ **DON'T** assume TURN works without testing
|
||||||
|
|
||||||
|
**Current Configuration:**
|
||||||
|
```javascript
|
||||||
|
const RTC_CONFIG = {
|
||||||
|
iceServers: [
|
||||||
|
{ urls: ["stun:stun.share.fish:3478"] },
|
||||||
|
{
|
||||||
|
urls: [
|
||||||
|
// TURNS (secure) - TLS/DTLS on port 5349 (preferred)
|
||||||
|
"turns:turn.share.fish:5349?transport=tcp",
|
||||||
|
"turns:turn.share.fish:5349?transport=udp",
|
||||||
|
// TURN (fallback) - plain on port 3478
|
||||||
|
"turn:turn.share.fish:3478?transport=tcp",
|
||||||
|
"turn:turn.share.fish:3478?transport=udp",
|
||||||
|
],
|
||||||
|
username: "webrtcuser",
|
||||||
|
credential: "supersecretpassword"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
WebRTC will try TURNS (secure) endpoints first, falling back to plain TURN if needed.
|
||||||
|
|
||||||
|
### 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:**
|
**Related repositories:**
|
||||||
- [rondevu-server](https://github.com/xtr-dev/rondevu-server) - HTTP signaling server
|
- [@xtr-dev/rondevu-client](https://github.com/xtr-dev/rondevu-client) - TypeScript client library ([npm](https://www.npmjs.com/package/@xtr-dev/rondevu-client))
|
||||||
- [rondevu-client](https://github.com/xtr-dev/rondevu-client) - TypeScript client library
|
- [@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
|
### Key Features
|
||||||
👤 **Connect by Peer ID** - Filter and connect to specific peers
|
|
||||||
🔗 **Connect by Connection ID** - Share a code and connect directly
|
|
||||||
|
|
||||||
### 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
|
## Quick Start
|
||||||
- **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
|
### Installation
|
||||||
|
|
||||||
#### Installation
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Development
|
### Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
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
|
```bash
|
||||||
npm run build
|
npm run build
|
||||||
@@ -53,150 +56,190 @@ npm run build
|
|||||||
|
|
||||||
The built files will be in the `dist/` directory.
|
The built files will be in the `dist/` directory.
|
||||||
|
|
||||||
#### Preview Production Build
|
### Preview Production Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run preview
|
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")
|
1. Go to the "Create Offer" tab
|
||||||
2. Click "Join Topic"
|
2. Enter one or more topics (comma-separated), e.g., `demo-room, testing`
|
||||||
3. Rondevu finds the first available peer and connects automatically
|
3. Click "Create Offer"
|
||||||
4. Start chatting!
|
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")
|
1. Once connected, go to the "Chat" tab
|
||||||
2. Click "Discover in [topic]" to list all available peers
|
2. Select a connection from the dropdown
|
||||||
3. See each peer's ID in the list
|
3. Type messages and hit Enter or click Send
|
||||||
4. Click "Connect" on the specific peer you want to talk to
|
4. Messages are sent **directly peer-to-peer** via WebRTC
|
||||||
5. Start chatting!
|
|
||||||
|
|
||||||
**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)
|
## Testing Locally
|
||||||
|
|
||||||
**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
|
|
||||||
|
|
||||||
The easiest way to test:
|
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
|
## Technical Implementation
|
||||||
- Click on any topic to auto-fill the discovery form
|
|
||||||
|
|
||||||
### Server Configuration
|
### RondevuPeer State Machine
|
||||||
|
|
||||||
This demo connects to: `https://rondevu.xtrdev.workers.dev`
|
This demo uses the `RondevuPeer` class which implements a clean state-based connection lifecycle:
|
||||||
|
|
||||||
To use a different server, modify the `baseUrl` in `src/main.js`:
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const client = new RondevuClient({
|
import { Rondevu } from '@xtr-dev/rondevu-client';
|
||||||
baseUrl: 'https://your-server.com'
|
|
||||||
|
// 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
|
**Offerer Flow:**
|
||||||
- **@xtr-dev/rondevu-client** - TypeScript client for Rondevu API
|
1. **idle** - Initial state
|
||||||
- **Vanilla JavaScript** - No framework dependencies
|
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
|
1. **Offerer** calls `peer.createOffer()`:
|
||||||
- `GET /:topic/sessions` - Discover peers in a topic
|
- State → `creating-offer`
|
||||||
- `POST /:topic/offer` - Create a new offer
|
- Creates RTCPeerConnection and data channel
|
||||||
- `POST /answer` - Send answer to a peer
|
- Generates SDP offer
|
||||||
- `POST /poll` - Poll for peer data
|
- Sets up ICE candidate handler (before gathering starts)
|
||||||
- `GET /health` - Check server health
|
- 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
|
### Key Features of Implementation
|
||||||
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
|
- **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
|
### Architecture
|
||||||
- **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
|
|
||||||
|
|
||||||
#### 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
|
## Server Configuration
|
||||||
- **RTCDataChannel API** - Unreliable but fast text messaging
|
|
||||||
- **Rondevu Signaling** - SDP and ICE candidate exchange
|
|
||||||
- **STUN Protocol** - NAT traversal (stun.l.google.com)
|
|
||||||
|
|
||||||
### Development Notes
|
This demo connects to: `https://api.ronde.vu`
|
||||||
|
|
||||||
- Peer IDs are auto-generated on page load
|
To use a different server, modify `API_URL` in `src/App.jsx`:
|
||||||
- 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)
|
|
||||||
|
|
||||||
### 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:**
|
**Quick Deploy via Wrangler:**
|
||||||
|
|
||||||
@@ -213,7 +256,46 @@ npx wrangler pages deploy dist --project-name=rondevu-demo
|
|||||||
4. Set output directory: `dist`
|
4. Set output directory: `dist`
|
||||||
5. Deploy automatically on every push!
|
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
|
MIT
|
||||||
|
|
||||||
|
|||||||
298
package-lock.json
generated
298
package-lock.json
generated
@@ -1,17 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "rondevu-demo",
|
"name": "rondevu-demo",
|
||||||
"version": "1.0.1",
|
"version": "2.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "rondevu-demo",
|
"name": "rondevu-demo",
|
||||||
"version": "1.0.1",
|
"version": "2.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@xtr-dev/rondevu-client": "^0.12.0",
|
||||||
"@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",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"react-hot-toast": "^2.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.0",
|
"@types/react": "^18.2.0",
|
||||||
@@ -20,15 +22,6 @@
|
|||||||
"vite": "^5.4.11"
|
"vite": "^5.4.11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../client": {
|
|
||||||
"name": "@xtr-dev/rondevu-client",
|
|
||||||
"version": "0.0.4",
|
|
||||||
"extraneous": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"devDependencies": {
|
|
||||||
"typescript": "^5.9.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||||
@@ -752,6 +745,15 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@noble/ed25519": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-QyteqMNm0GLqfa5SoYbSC3+Pvykwpn95Zgth4MFVSMKBB75ELl9tX1LAVsN4c3HXOrakHsF2gL4zWDAYCcsnzg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-beta.27",
|
"version": "1.0.0-beta.27",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
||||||
@@ -760,9 +762,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
|
||||||
"integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
|
"integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -774,9 +776,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm64": {
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz",
|
||||||
"integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==",
|
"integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -788,9 +790,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz",
|
||||||
"integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==",
|
"integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -802,9 +804,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-x64": {
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz",
|
||||||
"integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==",
|
"integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -816,9 +818,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz",
|
||||||
"integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==",
|
"integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -830,9 +832,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz",
|
||||||
"integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==",
|
"integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -844,9 +846,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz",
|
||||||
"integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==",
|
"integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -858,9 +860,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz",
|
||||||
"integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==",
|
"integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -872,9 +874,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz",
|
||||||
"integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==",
|
"integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -886,9 +888,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz",
|
||||||
"integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==",
|
"integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -900,9 +902,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz",
|
||||||
"integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==",
|
"integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
@@ -914,9 +916,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz",
|
||||||
"integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==",
|
"integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -928,9 +930,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz",
|
||||||
"integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==",
|
"integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@@ -942,9 +944,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz",
|
||||||
"integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==",
|
"integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@@ -956,9 +958,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz",
|
||||||
"integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==",
|
"integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -970,9 +972,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz",
|
||||||
"integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==",
|
"integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -984,9 +986,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz",
|
||||||
"integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==",
|
"integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -998,9 +1000,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz",
|
||||||
"integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==",
|
"integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1012,9 +1014,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz",
|
||||||
"integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==",
|
"integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1026,9 +1028,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz",
|
||||||
"integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==",
|
"integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -1040,9 +1042,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz",
|
||||||
"integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==",
|
"integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1054,9 +1056,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz",
|
||||||
"integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==",
|
"integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1127,14 +1129,14 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.3.26",
|
"version": "18.3.27",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
|
||||||
"integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
|
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/react-dom": {
|
"node_modules/@types/react-dom": {
|
||||||
@@ -1168,6 +1170,15 @@
|
|||||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@xtr-dev/rondevu-client": {
|
||||||
|
"version": "0.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xtr-dev/rondevu-client/-/rondevu-client-0.12.0.tgz",
|
||||||
|
"integrity": "sha512-c1UecF29Cjck7h7b7KWyCti8YSVVkuvEzyAz7aaFwAYBEumgX1r143mTBRfAMQkFL4upkG/PL5bvBGRY9QCpug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/ed25519": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@zxing/library": {
|
"node_modules/@zxing/library": {
|
||||||
"version": "0.21.3",
|
"version": "0.21.3",
|
||||||
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.21.3.tgz",
|
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.21.3.tgz",
|
||||||
@@ -1215,9 +1226,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.8.23",
|
"version": "2.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.23.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.3.tgz",
|
||||||
"integrity": "sha512-616V5YX4bepJFzNyOfce5Fa8fDJMfoxzOIzDCZwaGL8MKVpFrXqfNUoIpRn9YMI5pXf/VKgzjB4htFMsFKKdiQ==",
|
"integrity": "sha512-8QdH6czo+G7uBsNo0GiUfouPN1lRzKdJTGnKXwe12gkFbnnOUaUKGN55dMkfy+mnxmvjwl9zcI4VncczcVXDhA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -1225,9 +1236,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.27.0",
|
"version": "4.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
||||||
"integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==",
|
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1245,11 +1256,11 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.19",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001751",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
"electron-to-chromium": "^1.5.238",
|
"electron-to-chromium": "^1.5.263",
|
||||||
"node-releases": "^2.0.26",
|
"node-releases": "^2.0.27",
|
||||||
"update-browserslist-db": "^1.1.4"
|
"update-browserslist-db": "^1.2.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"browserslist": "cli.js"
|
"browserslist": "cli.js"
|
||||||
@@ -1268,9 +1279,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001753",
|
"version": "1.0.30001759",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz",
|
||||||
"integrity": "sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==",
|
"integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1325,10 +1336,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
@@ -1365,9 +1375,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.244",
|
"version": "1.5.266",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.266.tgz",
|
||||||
"integrity": "sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==",
|
"integrity": "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@@ -1473,6 +1483,15 @@
|
|||||||
"node": "6.* || 8.* || >= 10.*"
|
"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": {
|
"node_modules/is-fullwidth-code-point": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
@@ -1713,6 +1732,23 @@
|
|||||||
"react": "^18.3.1"
|
"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": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.17.0",
|
"version": "0.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||||
@@ -1739,9 +1775,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.52.5",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
|
||||||
"integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
|
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1755,28 +1791,28 @@
|
|||||||
"npm": ">=8.0.0"
|
"npm": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-android-arm-eabi": "4.52.5",
|
"@rollup/rollup-android-arm-eabi": "4.53.3",
|
||||||
"@rollup/rollup-android-arm64": "4.52.5",
|
"@rollup/rollup-android-arm64": "4.53.3",
|
||||||
"@rollup/rollup-darwin-arm64": "4.52.5",
|
"@rollup/rollup-darwin-arm64": "4.53.3",
|
||||||
"@rollup/rollup-darwin-x64": "4.52.5",
|
"@rollup/rollup-darwin-x64": "4.53.3",
|
||||||
"@rollup/rollup-freebsd-arm64": "4.52.5",
|
"@rollup/rollup-freebsd-arm64": "4.53.3",
|
||||||
"@rollup/rollup-freebsd-x64": "4.52.5",
|
"@rollup/rollup-freebsd-x64": "4.53.3",
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": "4.52.5",
|
"@rollup/rollup-linux-arm-gnueabihf": "4.53.3",
|
||||||
"@rollup/rollup-linux-arm-musleabihf": "4.52.5",
|
"@rollup/rollup-linux-arm-musleabihf": "4.53.3",
|
||||||
"@rollup/rollup-linux-arm64-gnu": "4.52.5",
|
"@rollup/rollup-linux-arm64-gnu": "4.53.3",
|
||||||
"@rollup/rollup-linux-arm64-musl": "4.52.5",
|
"@rollup/rollup-linux-arm64-musl": "4.53.3",
|
||||||
"@rollup/rollup-linux-loong64-gnu": "4.52.5",
|
"@rollup/rollup-linux-loong64-gnu": "4.53.3",
|
||||||
"@rollup/rollup-linux-ppc64-gnu": "4.52.5",
|
"@rollup/rollup-linux-ppc64-gnu": "4.53.3",
|
||||||
"@rollup/rollup-linux-riscv64-gnu": "4.52.5",
|
"@rollup/rollup-linux-riscv64-gnu": "4.53.3",
|
||||||
"@rollup/rollup-linux-riscv64-musl": "4.52.5",
|
"@rollup/rollup-linux-riscv64-musl": "4.53.3",
|
||||||
"@rollup/rollup-linux-s390x-gnu": "4.52.5",
|
"@rollup/rollup-linux-s390x-gnu": "4.53.3",
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.52.5",
|
"@rollup/rollup-linux-x64-gnu": "4.53.3",
|
||||||
"@rollup/rollup-linux-x64-musl": "4.52.5",
|
"@rollup/rollup-linux-x64-musl": "4.53.3",
|
||||||
"@rollup/rollup-openharmony-arm64": "4.52.5",
|
"@rollup/rollup-openharmony-arm64": "4.53.3",
|
||||||
"@rollup/rollup-win32-arm64-msvc": "4.52.5",
|
"@rollup/rollup-win32-arm64-msvc": "4.53.3",
|
||||||
"@rollup/rollup-win32-ia32-msvc": "4.52.5",
|
"@rollup/rollup-win32-ia32-msvc": "4.53.3",
|
||||||
"@rollup/rollup-win32-x64-gnu": "4.52.5",
|
"@rollup/rollup-win32-x64-gnu": "4.53.3",
|
||||||
"@rollup/rollup-win32-x64-msvc": "4.52.5",
|
"@rollup/rollup-win32-x64-msvc": "4.53.3",
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1851,9 +1887,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.1.4",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz",
|
||||||
"integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
|
"integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "rondevu-demo",
|
"name": "rondevu-demo",
|
||||||
"version": "1.0.1",
|
"version": "2.0.0",
|
||||||
"description": "Demo application for Rondevu peer signaling and discovery",
|
"description": "Demo application for Rondevu DNS-like WebRTC with username claiming and service discovery",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -10,10 +10,12 @@
|
|||||||
"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.12.0",
|
||||||
"@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",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"react-hot-toast": "^2.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.0",
|
"@types/react": "^18.2.0",
|
||||||
|
|||||||
1368
src/App-old.jsx
Normal file
1368
src/App-old.jsx
Normal file
File diff suppressed because it is too large
Load Diff
1336
src/App.jsx
1336
src/App.jsx
File diff suppressed because it is too large
Load Diff
@@ -2,95 +2,31 @@ import QRCodeDisplay from './QRCodeDisplay';
|
|||||||
|
|
||||||
function ConnectionForm({
|
function ConnectionForm({
|
||||||
action,
|
action,
|
||||||
method,
|
|
||||||
topic,
|
|
||||||
setTopic,
|
|
||||||
connectionId,
|
connectionId,
|
||||||
setConnectionId,
|
setConnectionId,
|
||||||
peerId,
|
|
||||||
setPeerId,
|
|
||||||
topics,
|
|
||||||
sessions,
|
|
||||||
connectionStatus,
|
connectionStatus,
|
||||||
qrCodeUrl,
|
qrCodeUrl,
|
||||||
currentConnectionId,
|
currentConnectionId,
|
||||||
onConnect,
|
onConnect,
|
||||||
onBack,
|
onBack
|
||||||
onTopicSelect,
|
|
||||||
onDiscoverPeers
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="step-container">
|
<div className="step-container">
|
||||||
<h2>Enter Details</h2>
|
<h2>{action === 'create' ? 'Create Connection' : 'Join Connection'}</h2>
|
||||||
<div className="form-container">
|
<div className="form-container">
|
||||||
{(method === 'topic' || method === 'peer-id' || (method === 'connection-id' && action === 'create')) && (
|
<div className="form-group">
|
||||||
<div className="form-group">
|
<label>Connection ID {action === 'create' && '(optional)'}</label>
|
||||||
<label>Topic</label>
|
<input
|
||||||
<input
|
type="text"
|
||||||
type="text"
|
value={connectionId}
|
||||||
value={topic}
|
onChange={(e) => setConnectionId(e.target.value)}
|
||||||
onChange={(e) => setTopic(e.target.value)}
|
placeholder={action === 'create' ? 'Auto-generated if empty' : 'Enter connection ID'}
|
||||||
placeholder="e.g., game-room"
|
autoFocus={action === 'connect'}
|
||||||
autoFocus
|
/>
|
||||||
/>
|
{action === 'create' && !connectionId && (
|
||||||
{topics.length > 0 && (
|
<p className="help-text">Leave empty to auto-generate a random ID</p>
|
||||||
<div className="topic-list">
|
)}
|
||||||
{topics.map((t) => (
|
</div>
|
||||||
<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="button-row">
|
<div className="button-row">
|
||||||
<button className="back-button" onClick={onBack}>← Back</button>
|
<button className="back-button" onClick={onBack}>← Back</button>
|
||||||
@@ -99,12 +35,10 @@ function ConnectionForm({
|
|||||||
onClick={onConnect}
|
onClick={onConnect}
|
||||||
disabled={
|
disabled={
|
||||||
connectionStatus === 'connecting' ||
|
connectionStatus === 'connecting' ||
|
||||||
(method === 'topic' && !topic) ||
|
(action === 'connect' && !connectionId)
|
||||||
(method === 'peer-id' && (!topic || !peerId)) ||
|
|
||||||
(method === 'connection-id' && action === 'join' && !connectionId)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{connectionStatus === 'connecting' ? 'Connecting...' : 'Connect'}
|
{connectionStatus === 'connecting' ? 'Connecting...' : (action === 'create' ? 'Create' : 'Connect')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ function Header() {
|
|||||||
<header className="header">
|
<header className="header">
|
||||||
<div className="header-content">
|
<div className="header-content">
|
||||||
<h1>Rondevu</h1>
|
<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">
|
<div className="header-links">
|
||||||
<a href="https://github.com/xtr-dev/rondevu-client" target="_blank" rel="noopener noreferrer">
|
<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">
|
<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">
|
<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"/>
|
<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>
|
</svg>
|
||||||
View source
|
Demo
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</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;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.help-text {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
input[type="text"] {
|
input[type="text"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
@@ -790,3 +796,223 @@ input[type="text"]:disabled {
|
|||||||
width: 100%;
|
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