From 17ff89c59aef99e336f9a748ec683c405278e0d4 Mon Sep 17 00:00:00 2001 From: Mohamed Genaidy Date: Mon, 22 Sep 2025 11:32:26 +0300 Subject: [PATCH] feat(default-meeting-ui): implement meeting creation and participant addition features --- samples/default-meeting-ui/package.json | 7 +- .../src/AddParticipantModal.tsx | 76 ++++++++ samples/default-meeting-ui/src/App.tsx | 43 +++-- .../src/CreateMeetingPage.tsx | 77 ++++++++ samples/default-meeting-ui/src/HomePage.tsx | 167 ++++++++++++++++++ 5 files changed, 349 insertions(+), 21 deletions(-) create mode 100644 samples/default-meeting-ui/src/AddParticipantModal.tsx create mode 100644 samples/default-meeting-ui/src/CreateMeetingPage.tsx create mode 100644 samples/default-meeting-ui/src/HomePage.tsx diff --git a/samples/default-meeting-ui/package.json b/samples/default-meeting-ui/package.json index ba1a2bc..80e965b 100644 --- a/samples/default-meeting-ui/package.json +++ b/samples/default-meeting-ui/package.json @@ -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", diff --git a/samples/default-meeting-ui/src/AddParticipantModal.tsx b/samples/default-meeting-ui/src/AddParticipantModal.tsx new file mode 100644 index 0000000..2f06de1 --- /dev/null +++ b/samples/default-meeting-ui/src/AddParticipantModal.tsx @@ -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 = ({ 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 ( +
+
+

Add a participant

+
+
+ + setName(e.target.value)} required style={{ width: "100%", padding: 8, marginTop: 4 }} /> +
+
+ + setPicture(e.target.value)} style={{ width: "100%", padding: 8, marginTop: 4 }} /> +
+
+ + setCustomId(e.target.value)} style={{ width: "100%", padding: 8, marginTop: 4 }} /> +
+ +
+ {error &&
{error}
} + +
+
+ ); +}; + +export default AddParticipantModal; diff --git a/samples/default-meeting-ui/src/App.tsx b/samples/default-meeting-ui/src/App.tsx index 3099489..1443e3d 100644 --- a/samples/default-meeting-ui/src/App.tsx +++ b/samples/default-meeting-ui/src/App.tsx @@ -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 ; } +function App() { + return ( + + + } /> + } /> + } /> + + + ); +} + export default App; diff --git a/samples/default-meeting-ui/src/CreateMeetingPage.tsx b/samples/default-meeting-ui/src/CreateMeetingPage.tsx new file mode 100644 index 0000000..8d35e17 --- /dev/null +++ b/samples/default-meeting-ui/src/CreateMeetingPage.tsx @@ -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 ( +
+

Create a meeting

+
+
+ + setTitle(e.target.value)} + style={{ width: "100%", padding: 8, marginTop: 4 }} + required + /> +
+
+ +
+ +
+ {error &&
{error}
} + +
+ ); +}; + +export default CreateMeetingPage; diff --git a/samples/default-meeting-ui/src/HomePage.tsx b/samples/default-meeting-ui/src/HomePage.tsx new file mode 100644 index 0000000..d1a686f --- /dev/null +++ b/samples/default-meeting-ui/src/HomePage.tsx @@ -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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [modalOpen, setModalOpen] = useState(false); + const [selectedMeetingId, setSelectedMeetingId] = useState(""); + 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 ( + <> +
+
+ setSearch(e.target.value)} + /> + +
+ + + + + + + + + + + + {loading ? ( + + ) : error ? ( + + ) : pagedMeetings.length === 0 ? ( + + ) : ( + pagedMeetings.map((meeting: any) => ( + + + + + + + + )) + )} + +
Meeting IDTitleCreated AtStatusActions
Loading...
{error}
No meetings found.
+ {meeting.id?.slice(-12)} + {meeting.title}{meeting.created_at ? new Date(meeting.created_at).toLocaleString() : "-"}{meeting.status || "Active"} + {meeting.status !== "INACTIVE" && ( + + )} +
+ {/* Paging controls */} +
+ + + Page {page} of {totalPages || 1} + + +
+
+ setModalOpen(false)} + onSuccess={token => { + setModalOpen(false); + navigate(`/meeting/${token}`); + }} + /> + + ); +}; + +export default HomePage;