From 4b7d1ac8ba99306fb872286b8f3643dc5df960a0 Mon Sep 17 00:00:00 2001 From: priyanshunitr Date: Sun, 23 Aug 2026 02:50:12 +0530 Subject: [PATCH 1/3] removed hardcoded backend url --- frontend/src/components/DebatePopup.tsx | 9 +++++---- frontend/src/components/RoomBrowser.tsx | 20 +++++++++++--------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/DebatePopup.tsx b/frontend/src/components/DebatePopup.tsx index 190792b1..ce6a06db 100644 --- a/frontend/src/components/DebatePopup.tsx +++ b/frontend/src/components/DebatePopup.tsx @@ -8,6 +8,10 @@ interface DebatePopupProps { onClose: () => void; } +const baseURL = ( + import.meta.env.VITE_BASE_URL || 'http://localhost:1313' +).replace(/\/+$/, ''); + const DebatePopup: React.FC = ({ onClose }) => { const navigate = useNavigate(); const [roomCode, setRoomCode] = useState(''); @@ -26,9 +30,6 @@ const DebatePopup: React.FC = ({ onClose }) => { return; } - const baseURL = - import.meta.env.VITE_BASE_URL || 'http://localhost:1313'; - try { const response = await fetch( `${baseURL}/rooms/${encodeURIComponent(trimmedRoomCode)}/join`, @@ -59,7 +60,7 @@ const DebatePopup: React.FC = ({ onClose }) => { try { // Sending a POST request to create a new room. // You might also send additional parameters (e.g., room type, settings). - const response = await fetch('http://localhost:1313/rooms', { + const response = await fetch(`${baseURL}/rooms`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/frontend/src/components/RoomBrowser.tsx b/frontend/src/components/RoomBrowser.tsx index 6869beec..b7ec43aa 100644 --- a/frontend/src/components/RoomBrowser.tsx +++ b/frontend/src/components/RoomBrowser.tsx @@ -13,6 +13,10 @@ interface Room { participants: Participant[] | null; } +const baseURL = ( + import.meta.env.VITE_BASE_URL || 'http://localhost:1313' +).replace(/\/+$/, ''); + const RoomBrowser: React.FC = () => { const [rooms, setRooms] = useState([]); const [loading, setLoading] = useState(true); @@ -21,7 +25,7 @@ const RoomBrowser: React.FC = () => { const fetchRooms = async () => { const token = localStorage.getItem('token'); try { - const response = await fetch('http://localhost:1313/rooms', { + const response = await fetch(`${baseURL}/rooms`, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -45,7 +49,8 @@ const RoomBrowser: React.FC = () => { })); setRooms(normalizedRooms); - } catch (error) { + } catch { + setRooms([]); } finally { setLoading(false); } @@ -60,22 +65,19 @@ const RoomBrowser: React.FC = () => { const handleJoinMatch = async (roomId: string) => { const token = localStorage.getItem('token'); try { - const response = await fetch( - `http://localhost:1313/rooms/${roomId}/join`, - { + const response = await fetch(`${baseURL}/rooms/${roomId}/join`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, - } - ); + }); if (!response.ok) { alert(`Failed to join room ${roomId}.`); return; } navigate(`/debate-room/${roomId}`); - } catch (error) { + } catch { alert('An error occurred while joining the match.'); } }; @@ -177,4 +179,4 @@ const RoomBrowser: React.FC = () => { ); }; -export default RoomBrowser; \ No newline at end of file +export default RoomBrowser; From de89ff7093e336b31a5dfa7aeb8863d0ac69792c Mon Sep 17 00:00:00 2001 From: priyanshunitr Date: Sun, 23 Aug 2026 02:50:41 +0530 Subject: [PATCH 2/3] fixed avatar --- backend/websocket/websocket.go | 3 +++ backend/websocket/websocket_test.go | 21 ++++++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/backend/websocket/websocket.go b/backend/websocket/websocket.go index 79470aba..b7ab79f4 100644 --- a/backend/websocket/websocket.go +++ b/backend/websocket/websocket.go @@ -171,8 +171,11 @@ func buildParticipantsMessage(room *Room) map[string]interface{} { participants = append(participants, map[string]interface{}{ "id": client.UserID, + "username": client.Username, "displayName": client.Username, "email": client.Email, + "avatarUrl": client.AvatarURL, + "elo": client.Elo, "role": client.Role, "ready": client.Ready, "isMuted": client.IsMuted, diff --git a/backend/websocket/websocket_test.go b/backend/websocket/websocket_test.go index d065d1c2..fa67c659 100644 --- a/backend/websocket/websocket_test.go +++ b/backend/websocket/websocket_test.go @@ -11,11 +11,13 @@ func TestBuildParticipantsMessageIncludesRecoverableRoomState(t *testing.T) { room := &Room{ Clients: map[*gorilla.Conn]*Client{ conn: { - UserID: "user-1", - Username: "Alice", - Email: "alice@example.com", - Role: "for", - Ready: true, + UserID: "user-1", + Username: "Alice", + Email: "alice@example.com", + AvatarURL: "https://example.com/alice.png", + Elo: 1425, + Role: "for", + Ready: true, }, }, } @@ -36,4 +38,13 @@ func TestBuildParticipantsMessageIncludesRecoverableRoomState(t *testing.T) { if role, ok := participant["role"].(string); !ok || role != "for" { t.Fatalf("expected role=for, got %#v", participant["role"]) } + if username, ok := participant["username"].(string); !ok || username != "Alice" { + t.Fatalf("expected username=Alice, got %#v", participant["username"]) + } + if avatarURL, ok := participant["avatarUrl"].(string); !ok || avatarURL != "https://example.com/alice.png" { + t.Fatalf("expected participant avatar URL, got %#v", participant["avatarUrl"]) + } + if elo, ok := participant["elo"].(int); !ok || elo != 1425 { + t.Fatalf("expected elo=1425, got %#v", participant["elo"]) + } } From f9849467fba14b669b630a8bb2480e91f11246e0 Mon Sep 17 00:00:00 2001 From: priyanshunitr Date: Sun, 23 Aug 2026 03:29:43 +0530 Subject: [PATCH 3/3] fixed opponent video stream --- frontend/src/Pages/OnlineDebateRoom.tsx | 158 ++++++++++++++++++++---- 1 file changed, 132 insertions(+), 26 deletions(-) diff --git a/frontend/src/Pages/OnlineDebateRoom.tsx b/frontend/src/Pages/OnlineDebateRoom.tsx index 8eefded4..e8c28d99 100644 --- a/frontend/src/Pages/OnlineDebateRoom.tsx +++ b/frontend/src/Pages/OnlineDebateRoom.tsx @@ -170,12 +170,14 @@ const OnlineDebateRoom = (): JSX.Element => { const [roomParticipants, setRoomParticipants] = useState([]); const [roomOwnerId, setRoomOwnerId] = useState(null); const [isWsConnected, setIsWsConnected] = useState(false); + const [isPeerWsConnected, setIsPeerWsConnected] = useState(false); const isRoomOwner = Boolean(roomOwnerId && currentUserId === roomOwnerId); // Refs for WebSocket, PeerConnection, and media elements const wsRef = useRef(null); const pcRef = useRef(null); + const peerOfferStartedRef = useRef(false); const spectatorPCsRef = useRef>(new Map()); const spectatorOfferQueueRef = useRef< { connectionId: string; requestId?: string }[] @@ -201,6 +203,7 @@ const OnlineDebateRoom = (): JSX.Element => { const timerRef = useRef(null); const judgePollRef = useRef(null); const submissionStartedRef = useRef(false); + const debateEndedByConcessionRef = useRef(false); useEffect(() => { return () => { @@ -816,15 +819,29 @@ const OnlineDebateRoom = (): JSX.Element => { ]); const handleConcede = useCallback(() => { - if (window.confirm("Are you sure you want to concede? This will count as a loss.")) { + if ( + window.confirm( + "Are you sure you want to concede? This will count as a loss." + ) + ) { + debateEndedByConcessionRef.current = true; + submissionStartedRef.current = true; + if (judgePollRef.current) { + clearInterval(judgePollRef.current); + judgePollRef.current = null; + } + if (wsRef.current) { - wsRef.current.send(JSON.stringify({ - type: "concede", - room: roomId, - userId: currentUserId, - username: currentUser?.displayName || "User" - })); + wsRef.current.send( + JSON.stringify({ + type: "concede", + room: roomId, + userId: currentUserId, + username: currentUser?.displayName || "User", + }) + ); } + setShowJudgment(false); setDebatePhase(DebatePhase.Finished); setPopup({ show: true, @@ -1150,6 +1167,8 @@ const OnlineDebateRoom = (): JSX.Element => { if (!token || !roomId) return; let participantFetchTimeout: number | undefined; + let pendingPeerOffer: RTCSessionDescriptionInit | null = null; + const pendingPeerCandidates: RTCIceCandidateInit[] = []; const wsUrl = `${WS_BASE_URL}/ws?room=${roomId}&token=${token}`; @@ -1175,10 +1194,35 @@ const OnlineDebateRoom = (): JSX.Element => { rws.onclose = () => { setIsWsConnected(false); + setIsPeerWsConnected(false); }; rws.onerror = () => { setIsWsConnected(false); + setIsPeerWsConnected(false); + }; + + const flushPendingPeerCandidates = async () => { + const pc = pcRef.current; + if (!pc?.remoteDescription) return; + + while (pendingPeerCandidates.length > 0) { + const candidate = pendingPeerCandidates.shift(); + if (candidate) { + await pc.addIceCandidate(candidate); + } + } + }; + + const acceptPeerOffer = async (offer: RTCSessionDescriptionInit) => { + const pc = pcRef.current; + if (!pc) return; + + await pc.setRemoteDescription(offer); + await flushPendingPeerCandidates(); + const answer = await pc.createAnswer(); + await pc.setLocalDescription(answer); + wsRef.current?.send(JSON.stringify({ type: "answer", answer })); }; rws.onmessage = async (event) => { @@ -1264,6 +1308,7 @@ const OnlineDebateRoom = (): JSX.Element => { break; case "roomParticipants": if (data.roomParticipants) { + setIsPeerWsConnected(data.roomParticipants.length >= 2); console.debug( "Received room participants update:", data.roomParticipants @@ -1330,14 +1375,28 @@ const OnlineDebateRoom = (): JSX.Element => { } } break; - case "concede": + case "concede": { + debateEndedByConcessionRef.current = true; + submissionStartedRef.current = true; + if (judgePollRef.current) { + clearInterval(judgePollRef.current); + judgePollRef.current = null; + } + + const localUserConceded = data.userId === currentUserIdRef.current; + setShowJudgment(false); setDebatePhase(DebatePhase.Finished); setPopup({ show: true, - message: `${data.username || "Opponent"} has conceded the debate. You win!`, + message: localUserConceded + ? "You have conceded the debate." + : `${ + data.username || "Opponent" + } has conceded the debate. You win!`, isJudging: false, }); break; + } case "spectatorJoined": if (data.spectator?.connectionId) { queueSpectatorOffer( @@ -1360,10 +1419,11 @@ const OnlineDebateRoom = (): JSX.Element => { break; } if (pcRef.current && data.offer) { - await pcRef.current.setRemoteDescription(data.offer!); - const answer = await pcRef.current.createAnswer(); - await pcRef.current.setLocalDescription(answer); - wsRef.current?.send(JSON.stringify({ type: "answer", answer })); + if (localStreamRef.current) { + await acceptPeerOffer(data.offer); + } else { + pendingPeerOffer = data.offer; + } } break; case "answer": @@ -1404,6 +1464,7 @@ const OnlineDebateRoom = (): JSX.Element => { // Spectator answer meant for the other debater; ignore. } else if (pcRef.current && data.answer) { await pcRef.current.setRemoteDescription(data.answer); + await flushPendingPeerCandidates(); } break; case "candidate": @@ -1436,7 +1497,11 @@ const OnlineDebateRoom = (): JSX.Element => { } } } else if (pcRef.current && data.candidate) { - await pcRef.current.addIceCandidate(data.candidate); + if (pcRef.current.remoteDescription) { + await pcRef.current.addIceCandidate(data.candidate); + } else { + pendingPeerCandidates.push(data.candidate); + } } break; } @@ -1465,8 +1530,14 @@ const OnlineDebateRoom = (): JSX.Element => { video: { width: 1280, height: 720 }, audio: true, }); + localStreamRef.current = stream; setLocalStream(stream); stream.getTracks().forEach((track) => pc.addTrack(track, stream)); + if (pendingPeerOffer) { + const offer = pendingPeerOffer; + pendingPeerOffer = null; + await acceptPeerOffer(offer); + } flushSpectatorOfferQueue(); } catch (err) { setMediaError( @@ -1494,6 +1565,7 @@ const OnlineDebateRoom = (): JSX.Element => { spectatorPCsRef.current.clear(); rws.close(); pc.close(); + peerOfferStartedRef.current = false; }; }, [ cleanupSpectatorConnection, @@ -1504,6 +1576,45 @@ const OnlineDebateRoom = (): JSX.Element => { roomId, ]); + const startPeerVideo = useCallback(async () => { + const pc = pcRef.current; + const ws = wsRef.current; + + if ( + !isRoomOwner || + !isWsConnected || + !isPeerWsConnected || + !localStream || + !pc || + !ws || + ws.readyState !== WebSocket.OPEN || + pc.signalingState !== "stable" || + peerOfferStartedRef.current + ) { + return; + } + + peerOfferStartedRef.current = true; + try { + const offer = await pc.createOffer(); + await pc.setLocalDescription(offer); + ws.send(JSON.stringify({ type: "offer", offer })); + } catch (error) { + peerOfferStartedRef.current = false; + console.error("Failed to start opponent video stream:", error); + } + }, [isPeerWsConnected, isRoomOwner, isWsConnected, localStream]); + + useEffect(() => { + void startPeerVideo(); + }, [startPeerVideo]); + + useEffect(() => { + if (!isPeerWsConnected) { + peerOfferStartedRef.current = false; + } + }, [isPeerWsConnected]); + useEffect(() => { flushSpectatorOfferQueue(); }, [flushSpectatorOfferQueue]); @@ -2042,15 +2153,20 @@ const OnlineDebateRoom = (): JSX.Element => { // Trigger logMessageHistory when debatePhase changes to Finished useEffect(() => { - if (debatePhase === DebatePhase.Finished && localRole) { + if ( + debatePhase === DebatePhase.Finished && + localRole && + !debateEndedByConcessionRef.current + ) { logMessageHistory(); } }, [debatePhase, localRole, logMessageHistory]); - // Reset submissionStartedRef whenever phase moves away from Finished. + // Reset terminal-flow guards whenever phase moves away from Finished. useEffect(() => { if (debatePhase !== DebatePhase.Finished) { submissionStartedRef.current = false; + debateEndedByConcessionRef.current = false; } }, [debatePhase]); @@ -2113,16 +2229,6 @@ const OnlineDebateRoom = (): JSX.Element => { console.debug( `Countdown finished. Starting debate at ${DebatePhase.OpeningFor} for ${localRole}` ); - if (localRole === "for") { - pcRef.current - ?.createOffer() - .then((offer) => - pcRef.current!.setLocalDescription(offer).then(() => offer) - ) - .then((offer) => - wsRef.current?.send(JSON.stringify({ type: "offer", offer })) - ); - } } }, [countdown, localRole]);