mirror of
https://github.com/xtr-dev/rondevu-demo.git
synced 2025-12-09 18:33:23 +00:00
Initial commit: Modern step-based UI for Rondevu demo
- Step-based flow: Choose action → Choose method → Enter details → Chat - Modern design with clean header and footer - GitHub links with icons to client and server repos - Footer link to ronde.vu - Three connection methods: Topic, Peer ID, Connection ID - Real WebRTC peer-to-peer chat - Mobile-responsive design - Collapsible activity log 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
.vite
|
||||||
1
.node-version
Normal file
1
.node-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
18
|
||||||
289
DEPLOYMENT.md
Normal file
289
DEPLOYMENT.md
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
# Deploying Rondevu Demo to Cloudflare Pages
|
||||||
|
|
||||||
|
This guide covers deploying the Rondevu demo to Cloudflare Pages.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Cloudflare account (free tier works)
|
||||||
|
- Node.js 18+ installed locally
|
||||||
|
- Git repository (for automatic deployments)
|
||||||
|
|
||||||
|
## Option 1: Deploy via Git Integration (Recommended)
|
||||||
|
|
||||||
|
This is the easiest method with automatic deployments on every push.
|
||||||
|
|
||||||
|
### Step 1: Push to Git
|
||||||
|
|
||||||
|
If you haven't already, initialize a git repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd demo
|
||||||
|
git init
|
||||||
|
git add .
|
||||||
|
git commit -m "Initial commit: Rondevu demo"
|
||||||
|
git remote add origin https://github.com/YOUR_USERNAME/rondevu-demo.git
|
||||||
|
git push -u origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Connect to Cloudflare Pages
|
||||||
|
|
||||||
|
1. Go to [Cloudflare Dashboard](https://dash.cloudflare.com/)
|
||||||
|
2. Navigate to **Pages** in the sidebar
|
||||||
|
3. Click **Create a project**
|
||||||
|
4. Click **Connect to Git**
|
||||||
|
5. Authorize Cloudflare to access your GitHub/GitLab account
|
||||||
|
6. Select your `rondevu-demo` repository
|
||||||
|
|
||||||
|
### Step 3: Configure Build Settings
|
||||||
|
|
||||||
|
Use these settings:
|
||||||
|
|
||||||
|
- **Project name**: `rondevu-demo` (or your preferred name)
|
||||||
|
- **Production branch**: `main`
|
||||||
|
- **Framework preset**: `Vite`
|
||||||
|
- **Build command**: `npm run build`
|
||||||
|
- **Build output directory**: `dist`
|
||||||
|
- **Root directory**: `/` (or leave empty)
|
||||||
|
- **Node version**: `18` (or higher)
|
||||||
|
|
||||||
|
### Step 4: Deploy
|
||||||
|
|
||||||
|
1. Click **Save and Deploy**
|
||||||
|
2. Cloudflare will build and deploy your site
|
||||||
|
3. You'll get a URL like: `https://rondevu-demo.pages.dev`
|
||||||
|
|
||||||
|
### Automatic Deployments
|
||||||
|
|
||||||
|
Every push to `main` will trigger a new deployment automatically!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Option 2: Deploy via Wrangler CLI
|
||||||
|
|
||||||
|
Deploy directly from your local machine using Wrangler.
|
||||||
|
|
||||||
|
### Step 1: Install Wrangler
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g wrangler
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use without installing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx wrangler
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Login to Cloudflare
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wrangler login
|
||||||
|
```
|
||||||
|
|
||||||
|
This will open your browser to authenticate.
|
||||||
|
|
||||||
|
### Step 3: Build Your Project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates the `dist/` directory with your static files.
|
||||||
|
|
||||||
|
### Step 4: Deploy to Cloudflare Pages
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wrangler pages deploy dist --project-name=rondevu-demo
|
||||||
|
```
|
||||||
|
|
||||||
|
Or if you prefer, use the simpler command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx wrangler pages deploy dist
|
||||||
|
```
|
||||||
|
|
||||||
|
Wrangler will:
|
||||||
|
- Create the Pages project if it doesn't exist
|
||||||
|
- Upload all files from `dist/`
|
||||||
|
- Deploy to production
|
||||||
|
|
||||||
|
### Step 5: Access Your Site
|
||||||
|
|
||||||
|
After deployment, you'll see output like:
|
||||||
|
|
||||||
|
```
|
||||||
|
✨ Deployment complete! Take a peek over at https://rondevu-demo.pages.dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Option 3: Deploy via Dashboard Upload
|
||||||
|
|
||||||
|
For quick testing without Git or CLI.
|
||||||
|
|
||||||
|
### Step 1: Build Locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Create a ZIP
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd dist
|
||||||
|
zip -r ../demo.zip .
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Upload to Cloudflare
|
||||||
|
|
||||||
|
1. Go to [Cloudflare Pages](https://dash.cloudflare.com/?to=/:account/pages)
|
||||||
|
2. Click **Create a project**
|
||||||
|
3. Click **Direct Upload**
|
||||||
|
4. Drag and drop your `demo.zip` file
|
||||||
|
5. Wait for deployment
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Custom Domain (Optional)
|
||||||
|
|
||||||
|
### Add a Custom Domain
|
||||||
|
|
||||||
|
1. Go to your Pages project in the Cloudflare dashboard
|
||||||
|
2. Click **Custom domains**
|
||||||
|
3. Click **Set up a custom domain**
|
||||||
|
4. Enter your domain (e.g., `demo.rondevu.dev`)
|
||||||
|
5. Follow the DNS instructions
|
||||||
|
6. Cloudflare will automatically provision an SSL certificate
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
The demo doesn't require any environment variables since the server URL is hardcoded. However, if you want to make it configurable:
|
||||||
|
|
||||||
|
### Step 1: Update vite.config.js
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
define: {
|
||||||
|
'import.meta.env.VITE_RONDEVU_URL': JSON.stringify(
|
||||||
|
process.env.VITE_RONDEVU_URL || 'https://rondevu.xtrdev.workers.dev'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
// ... rest of config
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Update src/main.js
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const client = new RondevuClient({
|
||||||
|
baseUrl: import.meta.env.VITE_RONDEVU_URL || 'https://rondevu.xtrdev.workers.dev'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Set Environment Variables in Cloudflare
|
||||||
|
|
||||||
|
In your Pages project settings:
|
||||||
|
1. Go to **Settings** → **Environment variables**
|
||||||
|
2. Add: `VITE_RONDEVU_URL` = `https://your-server.workers.dev`
|
||||||
|
3. Redeploy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Build Fails
|
||||||
|
|
||||||
|
**Issue**: Build command fails with module errors
|
||||||
|
|
||||||
|
**Solution**: Ensure `package.json` and `package-lock.json` are committed to your repository.
|
||||||
|
|
||||||
|
### Wrong Node Version
|
||||||
|
|
||||||
|
**Issue**: Build uses wrong Node.js version
|
||||||
|
|
||||||
|
**Solution**: Add `.node-version` file (already created) or set in Pages settings.
|
||||||
|
|
||||||
|
### 404 Errors
|
||||||
|
|
||||||
|
**Issue**: Page shows 404 when deployed
|
||||||
|
|
||||||
|
**Solution**: Ensure `build output directory` is set to `dist` in Pages settings.
|
||||||
|
|
||||||
|
### CORS Errors
|
||||||
|
|
||||||
|
**Issue**: API requests fail with CORS errors
|
||||||
|
|
||||||
|
**Solution**: Ensure your Rondevu server has CORS properly configured (already fixed in latest version).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Updating Your Deployment
|
||||||
|
|
||||||
|
### Git Integration Method
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Update demo"
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
Cloudflare Pages will automatically deploy the update.
|
||||||
|
|
||||||
|
### Wrangler CLI Method
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
wrangler pages deploy dist --project-name=rondevu-demo
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monitoring and Analytics
|
||||||
|
|
||||||
|
Cloudflare Pages provides:
|
||||||
|
- **Analytics**: View page views and requests
|
||||||
|
- **Real-time logs**: Monitor deployments
|
||||||
|
- **Build history**: View all deployments and rollback if needed
|
||||||
|
|
||||||
|
Access these in your Pages project dashboard.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cost
|
||||||
|
|
||||||
|
Cloudflare Pages is **free** for:
|
||||||
|
- Unlimited static requests
|
||||||
|
- Unlimited bandwidth
|
||||||
|
- 500 builds per month
|
||||||
|
- 1 concurrent build
|
||||||
|
|
||||||
|
Perfect for this demo! 🎉
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced: Branch Previews
|
||||||
|
|
||||||
|
Cloudflare automatically creates preview deployments for all branches:
|
||||||
|
|
||||||
|
1. Create a new branch: `git checkout -b new-feature`
|
||||||
|
2. Make changes and push: `git push origin new-feature`
|
||||||
|
3. Cloudflare creates a preview URL: `https://new-feature.rondevu-demo.pages.dev`
|
||||||
|
4. Test your changes before merging to main
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
After deploying:
|
||||||
|
|
||||||
|
1. **Test the demo** at your Pages URL
|
||||||
|
2. **Share the link** with others to test P2P connections
|
||||||
|
3. **Add a custom domain** for a professional look
|
||||||
|
4. **Monitor usage** in the Cloudflare dashboard
|
||||||
|
|
||||||
|
Happy deploying! 🚀
|
||||||
221
README.md
Normal file
221
README.md
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
# Rondevu
|
||||||
|
|
||||||
|
🎯 Meet WebRTC peers by topic, by peer ID, or by connection ID.
|
||||||
|
|
||||||
|
## Rondevu Demo
|
||||||
|
|
||||||
|
**Interactive demo showcasing three ways to connect WebRTC peers.**
|
||||||
|
|
||||||
|
Experience how easy WebRTC peer discovery can be with Rondevu's three connection methods:
|
||||||
|
|
||||||
|
🎯 **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
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **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
|
||||||
|
- **ICE Candidate Exchange** - Automatic NAT traversal using STUN servers
|
||||||
|
- **Real-time Chat** - Send and receive messages over WebRTC data channel
|
||||||
|
- **Connection Stats** - Monitor RTT, bytes sent/received in real-time
|
||||||
|
- **Connection Status** - Live connection state indicators
|
||||||
|
- **Activity Log** - Monitor all API and WebRTC events
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
#### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This will start the Vite dev server at `http://localhost:5173`
|
||||||
|
|
||||||
|
#### Build for Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
The built files will be in the `dist/` directory.
|
||||||
|
|
||||||
|
#### Preview Production Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
### Three Ways to Connect
|
||||||
|
|
||||||
|
This demo demonstrates all three Rondevu connection methods:
|
||||||
|
|
||||||
|
#### 1️⃣ Join Topic (Auto-Discovery)
|
||||||
|
|
||||||
|
**Easiest method** - Just enter a topic and auto-connect to first available peer:
|
||||||
|
|
||||||
|
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!
|
||||||
|
|
||||||
|
**Best for:** Quick matching, joining any available game/chat
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2️⃣ Discover Peers (Filter by Peer ID)
|
||||||
|
|
||||||
|
**Connect to specific peers** - Browse and select which peer to connect to:
|
||||||
|
|
||||||
|
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!
|
||||||
|
|
||||||
|
**Best for:** Connecting to friends, teammates, or specific users
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
- Click "Refresh Topics" to see all active topics
|
||||||
|
- Click on any topic to auto-fill the discovery form
|
||||||
|
|
||||||
|
### Server Configuration
|
||||||
|
|
||||||
|
This demo connects to: `https://rondevu.xtrdev.workers.dev`
|
||||||
|
|
||||||
|
To use a different server, modify the `baseUrl` in `src/main.js`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const client = new RondevuClient({
|
||||||
|
baseUrl: 'https://your-server.com'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technologies
|
||||||
|
|
||||||
|
- **Vite** - Fast development and build tool
|
||||||
|
- **@xtr-dev/rondevu-client** - TypeScript client for Rondevu API
|
||||||
|
- **Vanilla JavaScript** - No framework dependencies
|
||||||
|
|
||||||
|
### API Examples
|
||||||
|
|
||||||
|
The demo showcases all major Rondevu API endpoints:
|
||||||
|
|
||||||
|
- `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
|
||||||
|
|
||||||
|
### WebRTC Implementation Details
|
||||||
|
|
||||||
|
This demo implements a **complete WebRTC peer-to-peer connection** with:
|
||||||
|
|
||||||
|
#### Connection Flow
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
- **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
|
||||||
|
|
||||||
|
#### Technologies
|
||||||
|
|
||||||
|
- **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)
|
||||||
|
|
||||||
|
### Development Notes
|
||||||
|
|
||||||
|
- 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)
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
#### Deploy to Cloudflare Pages
|
||||||
|
|
||||||
|
The demo can be easily deployed to Cloudflare Pages (free tier):
|
||||||
|
|
||||||
|
**Quick Deploy via Wrangler:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npx wrangler pages deploy dist --project-name=rondevu-demo
|
||||||
|
```
|
||||||
|
|
||||||
|
**Or via Git Integration:**
|
||||||
|
|
||||||
|
1. Push to GitHub/GitLab
|
||||||
|
2. Connect to Cloudflare Pages
|
||||||
|
3. Set build command: `npm run build`
|
||||||
|
4. Set output directory: `dist`
|
||||||
|
5. Deploy automatically on every push!
|
||||||
|
|
||||||
|
See [DEPLOYMENT.md](./DEPLOYMENT.md) for detailed instructions including:
|
||||||
|
- Git integration setup
|
||||||
|
- Wrangler CLI deployment
|
||||||
|
- Custom domain configuration
|
||||||
|
- Environment variables
|
||||||
|
- Branch preview deployments
|
||||||
|
|
||||||
|
### License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
12
index.html
Normal file
12
index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Rondevu Demo</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1679
package-lock.json
generated
Normal file
1679
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
package.json
Normal file
23
package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "rondevu-demo",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Demo application for Rondevu peer signaling and discovery",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"deploy": "npm run build && npx wrangler pages deploy dist --project-name=rondevu-demo"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@xtr-dev/rondevu-client": "file:../client",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.0",
|
||||||
|
"@types/react-dom": "^18.2.0",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"vite": "^5.4.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
452
src/App.jsx
Normal file
452
src/App.jsx
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Rondevu, RondevuClient } from '@xtr-dev/rondevu-client';
|
||||||
|
|
||||||
|
const rdv = new Rondevu({
|
||||||
|
baseUrl: 'https://rondevu.xtrdev.workers.dev',
|
||||||
|
rtcConfig: {
|
||||||
|
iceServers: [
|
||||||
|
{ urls: 'stun:stun.l.google.com:19302' },
|
||||||
|
{ urls: 'stun:stun1.l.google.com:19302' },
|
||||||
|
{
|
||||||
|
urls: 'turn:relay1.expressturn.com:3480',
|
||||||
|
username: 'ef13B1E5PH265HK1N2',
|
||||||
|
credential: 'TTcTPEy3ndxsS0Gp'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = new RondevuClient({
|
||||||
|
baseUrl: 'https://rondevu.xtrdev.workers.dev'
|
||||||
|
});
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
// Step-based state
|
||||||
|
const [step, setStep] = useState(1); // 1: action, 2: method, 3: details, 4: connected
|
||||||
|
const [action, setAction] = useState(null); // 'create' or 'join'
|
||||||
|
const [method, setMethod] = useState(null); // 'topic', 'peer-id', 'connection-id'
|
||||||
|
|
||||||
|
// Connection state
|
||||||
|
const [topic, setTopic] = useState('');
|
||||||
|
const [connectionId, setConnectionId] = useState('');
|
||||||
|
const [peerId, setPeerId] = useState('');
|
||||||
|
const [topics, setTopics] = useState([]);
|
||||||
|
const [sessions, setSessions] = useState([]);
|
||||||
|
const [connectionStatus, setConnectionStatus] = useState('disconnected');
|
||||||
|
const [connectedPeer, setConnectedPeer] = useState(null);
|
||||||
|
const [currentConnectionId, setCurrentConnectionId] = useState(null);
|
||||||
|
|
||||||
|
// Chat state
|
||||||
|
const [messages, setMessages] = useState([]);
|
||||||
|
const [messageInput, setMessageInput] = useState('');
|
||||||
|
const [logs, setLogs] = useState([]);
|
||||||
|
|
||||||
|
const connectionRef = useRef(null);
|
||||||
|
const dataChannelRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
log('Demo initialized', 'info');
|
||||||
|
loadTopics();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const log = (message, type = 'info') => {
|
||||||
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
|
setLogs(prev => [...prev, { message, type, timestamp }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadTopics = async () => {
|
||||||
|
try {
|
||||||
|
const { topics } = await client.listTopics();
|
||||||
|
setTopics(topics);
|
||||||
|
} catch (error) {
|
||||||
|
log(`Error loading topics: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const discoverPeers = async (topicName) => {
|
||||||
|
try {
|
||||||
|
const { sessions: foundSessions } = await client.listSessions(topicName);
|
||||||
|
const otherSessions = foundSessions.filter(s => s.peerId !== rdv.peerId);
|
||||||
|
setSessions(otherSessions);
|
||||||
|
} catch (error) {
|
||||||
|
log(`Error discovering peers: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupConnection = (connection) => {
|
||||||
|
connectionRef.current = connection;
|
||||||
|
|
||||||
|
connection.on('connect', () => {
|
||||||
|
log('✅ Connected!', 'success');
|
||||||
|
setConnectionStatus('connected');
|
||||||
|
setStep(4);
|
||||||
|
|
||||||
|
const channel = connection.dataChannel('chat');
|
||||||
|
setupDataChannel(channel);
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on('disconnect', () => {
|
||||||
|
log('Disconnected', 'info');
|
||||||
|
reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on('error', (error) => {
|
||||||
|
log(`Error: ${error.message}`, 'error');
|
||||||
|
if (error.message.includes('timeout')) {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.on('datachannel', (channel) => {
|
||||||
|
if (channel.label === 'chat') {
|
||||||
|
setupDataChannel(channel);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupDataChannel = (channel) => {
|
||||||
|
dataChannelRef.current = channel;
|
||||||
|
|
||||||
|
channel.onmessage = (event) => {
|
||||||
|
setMessages(prev => [...prev, {
|
||||||
|
text: event.data,
|
||||||
|
type: 'received',
|
||||||
|
timestamp: new Date()
|
||||||
|
}]);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConnect = async () => {
|
||||||
|
try {
|
||||||
|
setConnectionStatus('connecting');
|
||||||
|
log('Connecting...', 'info');
|
||||||
|
|
||||||
|
let connection;
|
||||||
|
|
||||||
|
if (action === 'create') {
|
||||||
|
if (method === 'connection-id') {
|
||||||
|
const id = connectionId || `conn-${Date.now()}`;
|
||||||
|
connection = await rdv.create(id, topic || 'default');
|
||||||
|
setCurrentConnectionId(id);
|
||||||
|
log(`Created connection: ${id}`, 'success');
|
||||||
|
} else {
|
||||||
|
const id = `conn-${Date.now()}`;
|
||||||
|
connection = await rdv.create(id, topic);
|
||||||
|
setCurrentConnectionId(id);
|
||||||
|
log(`Created connection: ${id}`, 'success');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (method === 'topic') {
|
||||||
|
connection = await rdv.join(topic);
|
||||||
|
setCurrentConnectionId(connection.id);
|
||||||
|
} else if (method === 'peer-id') {
|
||||||
|
connection = await rdv.join(topic, {
|
||||||
|
filter: (s) => s.peerId === peerId
|
||||||
|
});
|
||||||
|
setCurrentConnectionId(connection.id);
|
||||||
|
} else if (method === 'connection-id') {
|
||||||
|
connection = await rdv.connect(connectionId);
|
||||||
|
setCurrentConnectionId(connectionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setConnectedPeer(connection.remotePeerId || 'Waiting...');
|
||||||
|
setupConnection(connection);
|
||||||
|
} catch (error) {
|
||||||
|
log(`Error: ${error.message}`, 'error');
|
||||||
|
setConnectionStatus('disconnected');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendMessage = () => {
|
||||||
|
if (!messageInput || !dataChannelRef.current || dataChannelRef.current.readyState !== 'open') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dataChannelRef.current.send(messageInput);
|
||||||
|
setMessages(prev => [...prev, {
|
||||||
|
text: messageInput,
|
||||||
|
type: 'sent',
|
||||||
|
timestamp: new Date()
|
||||||
|
}]);
|
||||||
|
setMessageInput('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
if (connectionRef.current) {
|
||||||
|
connectionRef.current.close();
|
||||||
|
}
|
||||||
|
setStep(1);
|
||||||
|
setAction(null);
|
||||||
|
setMethod(null);
|
||||||
|
setTopic('');
|
||||||
|
setConnectionId('');
|
||||||
|
setPeerId('');
|
||||||
|
setSessions([]);
|
||||||
|
setConnectionStatus('disconnected');
|
||||||
|
setConnectedPeer(null);
|
||||||
|
setCurrentConnectionId(null);
|
||||||
|
setMessages([]);
|
||||||
|
connectionRef.current = null;
|
||||||
|
dataChannelRef.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app">
|
||||||
|
<header className="header">
|
||||||
|
<div className="header-content">
|
||||||
|
<h1>Rondevu</h1>
|
||||||
|
<p className="tagline">Meet WebRTC 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">
|
||||||
|
<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>
|
||||||
|
Client
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/xtr-dev/rondevu-server" target="_blank" rel="noopener noreferrer">
|
||||||
|
<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>
|
||||||
|
Server
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="main">
|
||||||
|
{step === 1 && (
|
||||||
|
<div className="step-container">
|
||||||
|
<h2>Choose Action</h2>
|
||||||
|
<div className="button-grid">
|
||||||
|
<button
|
||||||
|
className="action-button"
|
||||||
|
onClick={() => {
|
||||||
|
setAction('create');
|
||||||
|
setStep(2);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="button-title">Create</div>
|
||||||
|
<div className="button-description">Start a new connection</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="action-button"
|
||||||
|
onClick={() => {
|
||||||
|
setAction('join');
|
||||||
|
setStep(2);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="button-title">Join</div>
|
||||||
|
<div className="button-description">Connect to existing peers</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 2 && (
|
||||||
|
<div className="step-container">
|
||||||
|
<h2>{action === 'create' ? 'Create' : 'Join'} by...</h2>
|
||||||
|
<div className="button-grid">
|
||||||
|
<button
|
||||||
|
className="action-button"
|
||||||
|
onClick={() => {
|
||||||
|
setMethod('topic');
|
||||||
|
setStep(3);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="button-title">Topic</div>
|
||||||
|
<div className="button-description">
|
||||||
|
{action === 'create' ? 'Create in a topic' : 'Auto-connect to first peer'}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{action === 'join' && (
|
||||||
|
<button
|
||||||
|
className="action-button"
|
||||||
|
onClick={() => {
|
||||||
|
setMethod('peer-id');
|
||||||
|
setStep(3);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="button-title">Peer ID</div>
|
||||||
|
<div className="button-description">Connect to specific peer</div>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="action-button"
|
||||||
|
onClick={() => {
|
||||||
|
setMethod('connection-id');
|
||||||
|
setStep(3);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="button-title">Connection ID</div>
|
||||||
|
<div className="button-description">
|
||||||
|
{action === 'create' ? 'Custom connection code' : 'Direct connection'}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button className="back-button" onClick={() => setStep(1)}>← Back</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 3 && (
|
||||||
|
<div className="step-container">
|
||||||
|
<h2>Enter Details</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={() => {
|
||||||
|
setTopic(t.topic);
|
||||||
|
if (method === 'peer-id') {
|
||||||
|
discoverPeers(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">
|
||||||
|
<button className="back-button" onClick={() => setStep(2)}>← Back</button>
|
||||||
|
<button
|
||||||
|
className="primary-button"
|
||||||
|
onClick={handleConnect}
|
||||||
|
disabled={
|
||||||
|
connectionStatus === 'connecting' ||
|
||||||
|
(method === 'topic' && !topic) ||
|
||||||
|
(method === 'peer-id' && (!topic || !peerId)) ||
|
||||||
|
(method === 'connection-id' && action === 'join' && !connectionId)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{connectionStatus === 'connecting' ? 'Connecting...' : 'Connect'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 4 && (
|
||||||
|
<div className="chat-container">
|
||||||
|
<div className="chat-header">
|
||||||
|
<div>
|
||||||
|
<h2>Connected</h2>
|
||||||
|
<p className="connection-details">
|
||||||
|
Peer: {connectedPeer || 'Unknown'} • ID: {currentConnectionId}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button className="disconnect-button" onClick={reset}>Disconnect</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="messages">
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<p className="empty">No messages yet. Start chatting!</p>
|
||||||
|
) : (
|
||||||
|
messages.map((msg, idx) => (
|
||||||
|
<div key={idx} className={`message ${msg.type}`}>
|
||||||
|
<div className="message-text">{msg.text}</div>
|
||||||
|
<div className="message-time">{msg.timestamp.toLocaleTimeString()}</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="message-input">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={messageInput}
|
||||||
|
onChange={(e) => setMessageInput(e.target.value)}
|
||||||
|
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
|
||||||
|
placeholder="Type a message..."
|
||||||
|
disabled={!dataChannelRef.current || dataChannelRef.current.readyState !== 'open'}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={sendMessage}
|
||||||
|
disabled={!dataChannelRef.current || dataChannelRef.current.readyState !== 'open'}
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{logs.length > 0 && (
|
||||||
|
<details className="logs">
|
||||||
|
<summary>Activity Log ({logs.length})</summary>
|
||||||
|
<div className="log-entries">
|
||||||
|
{logs.map((log, idx) => (
|
||||||
|
<div key={idx} className={`log-entry ${log.type}`}>
|
||||||
|
[{log.timestamp}] {log.message}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="peer-id-badge">Your Peer ID: {rdv.peerId}</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className="footer">
|
||||||
|
<a href="https://ronde.vu" target="_blank" rel="noopener noreferrer">
|
||||||
|
ronde.vu
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
566
src/index.css
Normal file
566
src/index.css
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
padding: 40px 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagline {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
opacity: 0.95;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-links a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-links a:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-icon {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
position: relative;
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 48px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-container h2 {
|
||||||
|
color: #667eea;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
background: white;
|
||||||
|
border: 3px solid #e0e0e0;
|
||||||
|
padding: 32px 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
text-align: left;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: #f8f9ff;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-title {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-description {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #6c757d;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:disabled {
|
||||||
|
background: #f5f5f5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-list {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-item {
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #333;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-item:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: #f0f2ff;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-item .peer-count {
|
||||||
|
background: transparent;
|
||||||
|
color: #667eea;
|
||||||
|
padding: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
background: transparent;
|
||||||
|
color: #6c757d;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
color: #667eea;
|
||||||
|
background: transparent;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-button {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-button:hover:not(:disabled) {
|
||||||
|
background: #5568d3;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-button:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 32px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 2px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header h2 {
|
||||||
|
color: #667eea;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-details {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disconnect-button {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disconnect-button:hover {
|
||||||
|
background: #c82333;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
min-height: 400px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages::-webkit-scrollbar-track {
|
||||||
|
background: #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages::-webkit-scrollbar-thumb {
|
||||||
|
background: #667eea;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
text-align: center;
|
||||||
|
color: #6c757d;
|
||||||
|
padding: 40px 20px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
animation: slideIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-text {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
max-width: 70%;
|
||||||
|
word-wrap: break-word;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.sent .message-text {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
margin-left: auto;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.received .message-text {
|
||||||
|
background: white;
|
||||||
|
color: #333;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6c757d;
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.sent .message-time {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input input {
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input button {
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input button:hover:not(:disabled) {
|
||||||
|
background: #5568d3;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input button:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs {
|
||||||
|
margin-top: 24px;
|
||||||
|
border-top: 2px solid #f0f0f0;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs summary {
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #667eea;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entries {
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry.info {
|
||||||
|
color: #4ec9b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry.success {
|
||||||
|
color: #6a9955;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry.error {
|
||||||
|
color: #f48771;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-id-badge {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
color: #667eea;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px 30px;
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 20px 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagline {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-container {
|
||||||
|
padding: 32px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-container {
|
||||||
|
padding: 24px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages {
|
||||||
|
min-height: 300px;
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-text {
|
||||||
|
max-width: 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-id-badge {
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.header-content h1 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-container h2 {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disconnect-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/main.jsx
Normal file
10
src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
14
vite.config.js
Normal file
14
vite.config.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
open: true
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
sourcemap: true
|
||||||
|
}
|
||||||
|
});
|
||||||
10
wrangler.toml
Normal file
10
wrangler.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
name = "rondevu-demo"
|
||||||
|
compatibility_date = "2024-01-01"
|
||||||
|
|
||||||
|
pages_build_output_dir = "dist"
|
||||||
|
|
||||||
|
[env.production]
|
||||||
|
name = "rondevu-demo"
|
||||||
|
|
||||||
|
[env.production.vars]
|
||||||
|
# No environment variables needed for static site
|
||||||
Reference in New Issue
Block a user