Skip to content
This repository was archived by the owner on Oct 30, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions samples/default-meeting-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
"preview": "vite preview"
},
"dependencies": {
"@cloudflare/realtimekit": "^1.1.6",
"@cloudflare/realtimekit-react": "^1.1.6",
"@cloudflare/realtimekit-react-ui": "^1.0.5",
"@cloudflare/realtimekit-ui": "^1.0.5",
"@cloudflare/realtimekit-ui-addons": "^0.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@cloudflare/realtimekit": "^1.1.6",
"@cloudflare/realtimekit-ui": "^1.0.5",
"@cloudflare/realtimekit-ui-addons": "^0.0.4"
"react-router-dom": "^7.8.2"
},
"devDependencies": {
"@types/react": "^18.0.24",
Expand Down
76 changes: 76 additions & 0 deletions samples/default-meeting-ui/src/AddParticipantModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { useState } from "react";

interface AddParticipantModalProps {
meetingId: string;
open: boolean;
onClose: () => void;
onSuccess: (token: string) => void;
}

const AddParticipantModal: React.FC<AddParticipantModalProps> = ({ meetingId, open, onClose, onSuccess }) => {
const [name, setName] = useState("");
const [picture, setPicture] = useState("");
const [customId, setCustomId] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");

if (!open) return null;

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError("");
try {
const res = await fetch(`https://api.realtime.cloudflare.com/v2/meetings/${meetingId}/participants`, {
method: "POST",
headers: {
Accept: "application/json",
"Authorization": import.meta.env.VITE_AUTHORIZATION,
"Content-Type": "application/json",
},
body: JSON.stringify({
name,
picture,
preset_name: "group_call_host",
custom_participant_id: customId,
}),
});
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.message || "Failed to add participant");
onSuccess(data.data.token);
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};

return (
<div style={{ position: "fixed", top: 0, left: 0, width: "100vw", height: "100vh", background: "rgba(0,0,0,0.2)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000 }}>
<div style={{ background: "#fff", padding: 24, borderRadius: 8, minWidth: 340, boxShadow: "0 2px 8px #eee" }}>
<h3>Add a participant</h3>
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: 12 }}>
<label>Name</label>
<input type="text" value={name} onChange={e => setName(e.target.value)} required style={{ width: "100%", padding: 8, marginTop: 4 }} />
</div>
<div style={{ marginBottom: 12 }}>
<label>Picture URL</label>
<input type="text" value={picture} onChange={e => setPicture(e.target.value)} style={{ width: "100%", padding: 8, marginTop: 4 }} />
</div>
<div style={{ marginBottom: 12 }}>
<label>Custom Participant ID</label>
<input type="text" value={customId} onChange={e => setCustomId(e.target.value)} style={{ width: "100%", padding: 8, marginTop: 4 }} />
</div>
<button type="submit" disabled={loading} style={{ width: "100%", padding: 10, background: "#1677ff", color: "#fff", border: "none", borderRadius: 4 }}>
{loading ? "Adding..." : "Add Participant"}
</button>
</form>
{error && <div style={{ color: "red", marginTop: 12 }}>{error}</div>}
<button onClick={onClose} style={{ marginTop: 16, background: "#eee", border: "none", padding: "8px 16px", borderRadius: 4 }}>Cancel</button>
</div>
</div>
);
};

export default AddParticipantModal;
43 changes: 25 additions & 18 deletions samples/default-meeting-ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
import { useEffect } from 'react';

import React from "react";
import { BrowserRouter, Routes, Route, useParams, useNavigate } from "react-router-dom";
import HomePage from "./HomePage";
import CreateMeetingPage from "./CreateMeetingPage";
import { RtkMeeting } from '@cloudflare/realtimekit-react-ui';
import { useRealtimeKitClient } from '@cloudflare/realtimekit-react';

function App() {
function MeetingRoute() {
const { authToken } = useParams();
const navigate = useNavigate();
const [meeting, initMeeting] = useRealtimeKitClient();

useEffect(() => {
const searchParams = new URL(window.location.href).searchParams;

const authToken = searchParams.get('authToken');

React.useEffect(() => {
if (!authToken) {
alert(
"An authToken wasn't passed, please pass an authToken in the URL query to join a meeting."
);
alert("No authToken provided in URL. Redirecting to home page.");
navigate("/");
return;
}
initMeeting({ authToken });
}, [authToken, initMeeting, navigate]);

initMeeting({
authToken,
});
}, []);

// By default this component will cover the entire viewport.
// To avoid that and to make it fill a parent container, pass the prop:
// `mode="fill"` to the component.
return <RtkMeeting meeting={meeting!} />;
}

function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/create-meeting" element={<CreateMeetingPage />} />
<Route path="/meeting/:authToken" element={<MeetingRoute />} />
</Routes>
</BrowserRouter>
);
}

export default App;
77 changes: 77 additions & 0 deletions samples/default-meeting-ui/src/CreateMeetingPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";

const CreateMeetingPage: React.FC = () => {
const [title, setTitle] = useState("");
const [recordOnStart, setRecordOnStart] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const navigate = useNavigate();

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError("");
try {
// Create Meeting only
const meeting_result = await fetch("https://api.realtime.cloudflare.com/v2/meetings", {
method: "POST",
headers: {
Accept: "application/json",
"Authorization": import.meta.env.VITE_AUTHORIZATION,
"Content-Type": "application/json",
},
body: JSON.stringify({
title,
preferred_region: "ap-south-1",
record_on_start: recordOnStart,
live_stream_on_start: false,
}),
});
const meeting_data = await meeting_result.json();
if (!meeting_result.ok) throw new Error(meeting_data.message || "Failed to create meeting");
// Redirect to home page after success
navigate("/");
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};

return (
<div style={{ maxWidth: 400, margin: "40px auto", padding: 24, boxShadow: "0 2px 8px #eee", borderRadius: 8 }}>
<h2>Create a meeting</h2>
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: 16 }}>
<label>Meeting Title</label>
<input
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
style={{ width: "100%", padding: 8, marginTop: 4 }}
required
/>
</div>
<div style={{ marginBottom: 16 }}>
<label>
<input
type="checkbox"
checked={recordOnStart}
onChange={e => setRecordOnStart(e.target.checked)}
/>
Record on start
</label>
</div>
<button type="submit" disabled={loading} style={{ width: "100%", padding: 10, background: "#1677ff", color: "#fff", border: "none", borderRadius: 4 }}>
{loading ? "Creating..." : "Create Meeting"}
</button>
</form>
{error && <div style={{ color: "red", marginTop: 16 }}>{error}</div>}

</div>
);
};

export default CreateMeetingPage;
167 changes: 167 additions & 0 deletions samples/default-meeting-ui/src/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import AddParticipantModal from "./AddParticipantModal";
const PAGE_SIZE = 20;

const HomePage: React.FC = () => {
const [meetings, setMeetings] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [modalOpen, setModalOpen] = useState(false);
const [selectedMeetingId, setSelectedMeetingId] = useState<string>("");
const [search, setSearch] = useState("");
const [page, setPage] = useState(1); // <-- Add page state
const navigate = useNavigate();

useEffect(() => {
const fetchMeetings = async () => {
setLoading(true);
setError("");
try {
const res = await fetch("https://api.realtime.cloudflare.com/v2/meetings", {
method: "GET",
headers: {
Accept: "application/json",
"Authorization": import.meta.env.VITE_AUTHORIZATION,
},
});
const data = await res.json();
if (!res.ok) throw new Error(data.message || "Failed to fetch meetings");
setMeetings(data.data || []);
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchMeetings();
}, []);

// Filter meetings by meeting_id or title
const filteredMeetings = meetings.filter((meeting: any) => {
if (!search) return true;
const searchLower = search.toLowerCase();
return (
(meeting.id && meeting.id.toLowerCase().includes(searchLower)) ||
(meeting.title && meeting.title.toLowerCase().includes(searchLower))
);
});

// Paging logic
const totalPages = Math.ceil(filteredMeetings.length / PAGE_SIZE);
const pagedMeetings = filteredMeetings.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);

// Reset to first page if search changes
React.useEffect(() => {
setPage(1);
}, [search]);

return (
<>
<div style={{ padding: 32 }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 24 }}>
<input
type="text"
placeholder="Search by Meeting ID or Title"
style={{ width: 320, padding: 8, borderRadius: 4, border: "1px solid #ccc" }}
value={search}
onChange={e => setSearch(e.target.value)}
/>
<button
style={{ background: "#1677ff", color: "#fff", border: "none", borderRadius: 4, padding: "8px 20px", fontWeight: 600 }}
onClick={() => navigate("/create-meeting")}
>
+ Create meeting
</button>
</div>
<table style={{ width: "100%", borderCollapse: "collapse", background: "#fff" }}>
<thead style={{ background: "#f7f7f7" }}>
<tr>
<th style={{ padding: 12, textAlign: "left" }}>Meeting ID</th>
<th style={{ padding: 12, textAlign: "left" }}>Title</th>
<th style={{ padding: 12, textAlign: "left" }}>Created At</th>
<th style={{ padding: 12, textAlign: "left" }}>Status</th>
<th style={{ padding: 12, textAlign: "left" }}>Actions</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr><td colSpan={5} style={{ textAlign: "center", padding: 24 }}>Loading...</td></tr>
) : error ? (
<tr><td colSpan={5} style={{ color: "red", textAlign: "center", padding: 24 }}>{error}</td></tr>
) : pagedMeetings.length === 0 ? (
<tr><td colSpan={5} style={{ textAlign: "center", padding: 24 }}>No meetings found.</td></tr>
) : (
pagedMeetings.map((meeting: any) => (
<tr key={meeting.id}>
<td style={{ padding: 12 }}>
<a href="#" style={{ color: "#1677ff", textDecoration: "underline" }}>{meeting.id?.slice(-12)}</a>
</td>
<td style={{ padding: 12 }}>{meeting.title}</td>
<td style={{ padding: 12 }}>{meeting.created_at ? new Date(meeting.created_at).toLocaleString() : "-"}</td>
<td style={{ padding: 12 }}>{meeting.status || "Active"}</td>
<td style={{ padding: 12 }}>
{meeting.status !== "INACTIVE" && (
<button
style={{ background: "#222", color: "#fff", border: "none", borderRadius: 4, padding: "6px 16px" }}
onClick={() => {
setSelectedMeetingId(meeting.id);
setModalOpen(true);
}}
>
Join
</button>
)}
</td>
</tr>
))
)}
</tbody>
</table>
{/* Paging controls */}
<div style={{ marginTop: 24, display: "flex", justifyContent: "center", gap: 16 }}>
<button
onClick={() => setPage(page - 1)}
disabled={page === 1}
style={{
padding: "6px 16px",
borderRadius: 4,
border: "1px solid #ccc",
background: page === 1 ? "#eee" : "#fff",
cursor: page === 1 ? "not-allowed" : "pointer"
}}
>
Previous
</button>
<span style={{ alignSelf: "center" }}>
Page {page} of {totalPages || 1}
</span>
<button
onClick={() => setPage(page + 1)}
disabled={page === totalPages || totalPages === 0}
style={{
padding: "6px 16px",
borderRadius: 4,
border: "1px solid #ccc",
background: page === totalPages || totalPages === 0 ? "#eee" : "#fff",
cursor: page === totalPages || totalPages === 0 ? "not-allowed" : "pointer"
}}
>
Next
</button>
</div>
</div>
<AddParticipantModal
meetingId={selectedMeetingId}
open={modalOpen}
onClose={() => setModalOpen(false)}
onSuccess={token => {
setModalOpen(false);
navigate(`/meeting/${token}`);
}}
/>
</>
);
};

export default HomePage;