From c1834845c6aee3dec1fd277e3684dbe4af3d8cee Mon Sep 17 00:00:00 2001 From: Adam Weeks Date: Thu, 18 Dec 2025 14:07:55 -0500 Subject: [PATCH 1/2] Refactor Webex integration and enhance UI components - Updated App component to conditionally load Webex based on route. - Modified CreateLobby to include a checkbox for enabling/disabling Webex integration. - Enhanced LandingPage with two buttons for launching a game: one for Webex and one for standalone browser. - Updated Navbar to display Webex info only on game routes. - Adjusted LobbyProvider to preserve the disableWebex query parameter in the lobby URL. - Updated useWebex hook to manage Webex loading based on the current route and query parameters. - Added tests for new button functionality in LandingPage. --- frontend/src/App.jsx | 17 +-- frontend/src/components/CreateLobby.jsx | 43 +++++- frontend/src/components/LandingPage.jsx | 29 ++++- frontend/src/components/Navbar.jsx | 123 +++++++++--------- .../components/__tests__/LandingPage.test.jsx | 20 ++- frontend/src/context/LobbyProvider.jsx | 13 +- frontend/src/hooks/useWebex.jsx | 17 ++- 7 files changed, 174 insertions(+), 88 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index ca35823..7292886 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,6 +1,6 @@ // src/App.jsx -import React, { useEffect, useState } from 'react'; -import { Routes, Route } from 'react-router-dom'; +import React, { useState } from 'react'; +import { Routes, Route, useLocation } from 'react-router-dom'; import { CssBaseline, ThemeProvider, @@ -12,17 +12,14 @@ import Lobby from './components/Lobby'; import Navbar from './components/Navbar'; import LandingPage from './components/LandingPage'; import About from './components/About'; -import useWebex from './hooks/useWebex'; import { ROUTES } from './constants'; function App() { - const { theme: webexTheme } = useWebex(); const [darkMode, setDarkMode] = useState(true); // Set dark mode as default - - useEffect(() => { - // Only switch from dark if webex theme explicitly requests light - setDarkMode(webexTheme !== 'light'); - }, [webexTheme]); + const location = useLocation(); + + // Only load Webex on /game routes + const shouldLoadWebex = location.pathname.startsWith('/game'); const theme = createTheme({ palette: { @@ -43,7 +40,7 @@ function App() { return ( - + } /> diff --git a/frontend/src/components/CreateLobby.jsx b/frontend/src/components/CreateLobby.jsx index 38211df..fe20bdf 100644 --- a/frontend/src/components/CreateLobby.jsx +++ b/frontend/src/components/CreateLobby.jsx @@ -8,6 +8,8 @@ import { CircularProgress, Box, Paper, + FormControlLabel, + Checkbox, } from '@mui/material'; import { v4 as uuidv4 } from 'uuid'; import { ROUTES } from '../constants'; @@ -20,12 +22,19 @@ const CreateLobby = () => { const [lobbyName, setLobbyName] = useState(''); const [displayName, setDisplayName] = useState(''); const [loading, setLoading] = useState(false); + + // Initialize webexEnabled based on URL parameter + const [webexEnabled, setWebexEnabled] = useState(() => { + const urlParams = new URLSearchParams(window.location.search); + return urlParams.get('disableWebex') !== 'true'; + }); + const { isLoading, username, meetingName } = useWebex(); // Once isLoading is false, set default values from Webex SDK // This is a workaround to avoid setting default values before Webex SDK is ready useEffect(() => { - if (!isLoading) { + if (!isLoading && webexEnabled) { if (meetingName) { setLobbyName(meetingName); } @@ -33,7 +42,7 @@ const CreateLobby = () => { setDisplayName(username); } } - }, [isLoading, meetingName, username]); + }, [isLoading, meetingName, username, webexEnabled]); const handleCreateLobby = async () => { if (!lobbyName.trim() || !displayName.trim()) return; @@ -42,7 +51,14 @@ const CreateLobby = () => { try { const hostId = uuidv4(); const data = await api.createLobby(hostId, displayName, lobbyName); - navigate(ROUTES.GAME_WITH_ID(data.lobby_id), { + + // Build the game URL with optional disableWebex parameter + let gameUrl = ROUTES.GAME_WITH_ID(data.lobby_id); + if (!webexEnabled) { + gameUrl += '?disableWebex=true'; + } + + navigate(gameUrl, { state: { user: { id: hostId, display_name: displayName } }, }); } catch (error) { @@ -99,6 +115,27 @@ const CreateLobby = () => { value={displayName} onChange={(e) => setDisplayName(e.target.value)} /> + + setWebexEnabled(e.target.checked)} + sx={{ + color: 'primary.main', + '&.Mui-checked': { + color: 'primary.main', + }, + }} + /> + } + label={ + + Enable Webex Integration + + } + /> + + + diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 03f4496..7e96746 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -26,9 +26,11 @@ import { Link as RouterLink } from 'react-router-dom'; import { ROUTES } from '../constants'; import AboutModal from './About/AboutModal'; -export default function Navbar({ darkMode, setDarkMode }) { +export default function Navbar({ darkMode, setDarkMode, shouldLoadWebex = false }) { const [anchorEl, setAnchorEl] = useState(null); const [aboutModalOpen, setAboutModalOpen] = useState(false); + + // Always call the hook, but it will internally skip initialization if disabled const { isConnected, isRunningInWebex, @@ -126,64 +128,68 @@ export default function Navbar({ darkMode, setDarkMode }) { - {/* Webex Info Menu */} - - - - - - - - {loading ? ( - - - - ) : ( - [ - - {isConnected ? ( - - ) : ( - - )} - {isConnected ? 'Connected to Webex' : 'Webex Not Connected'} - , - - !isRunningInWebex && ( - - - Running Outside Webex - - ), - - username && {username}, - - meetingName && ( - {meetingName} - ), - - error && ( - - - {error} + {/* Webex Info Menu - Only show on /game routes */} + {shouldLoadWebex && ( + <> + + + + + + + + {loading ? ( + + - ), - ] - )} - + ) : ( + [ + + {isConnected ? ( + + ) : ( + + )} + {isConnected ? 'Connected to Webex' : 'Webex Not Connected'} + , + + !isRunningInWebex && ( + + + Running Outside Webex + + ), + + username && {username}, + + meetingName && ( + {meetingName} + ), + + error && ( + + + {error} + + ), + ] + )} + + + )} @@ -197,4 +203,5 @@ export default function Navbar({ darkMode, setDarkMode }) { Navbar.propTypes = { darkMode: PropTypes.bool.isRequired, setDarkMode: PropTypes.func.isRequired, + shouldLoadWebex: PropTypes.bool, }; diff --git a/frontend/src/components/__tests__/LandingPage.test.jsx b/frontend/src/components/__tests__/LandingPage.test.jsx index 67f8903..49c109d 100644 --- a/frontend/src/components/__tests__/LandingPage.test.jsx +++ b/frontend/src/components/__tests__/LandingPage.test.jsx @@ -23,15 +23,25 @@ describe('LandingPage', () => { expect(screen.getByText(/redshift/i)).toBeInTheDocument(); }); - it('renders Create a Game button', () => { - const button = screen.getByRole('button', { name: /create a game/i }); - expect(button).toBeInTheDocument(); + it('renders both launch buttons', () => { + const webexButton = screen.getByRole('button', { name: /launch in webex/i }); + const standaloneButton = screen.getByRole('button', { name: /standalone browser/i }); + + expect(webexButton).toBeInTheDocument(); + expect(standaloneButton).toBeInTheDocument(); }); - it('calls navigate("/game") on button click', () => { - const button = screen.getByRole('button', { name: /create a game/i }); + it('calls navigate("/game") when Launch in Webex is clicked', () => { + const button = screen.getByRole('button', { name: /launch in webex/i }); fireEvent.click(button); expect(globalThis.mockNavigate).toHaveBeenCalledWith('/game'); }); + + it('calls navigate("/game?disableWebex=true") when Standalone Browser is clicked', () => { + const button = screen.getByRole('button', { name: /standalone browser/i }); + fireEvent.click(button); + + expect(globalThis.mockNavigate).toHaveBeenCalledWith('/game?disableWebex=true'); + }); }); diff --git a/frontend/src/context/LobbyProvider.jsx b/frontend/src/context/LobbyProvider.jsx index 692afe4..c899fe1 100644 --- a/frontend/src/context/LobbyProvider.jsx +++ b/frontend/src/context/LobbyProvider.jsx @@ -30,10 +30,15 @@ export const LobbyProvider = ({ lobbyId, initialUser, children }) => { [], ); - const lobbyUrl = useMemo( - () => `${window.location.origin}${ROUTES.GAME_WITH_ID(lobbyId)}`, - [lobbyId], - ); + const lobbyUrl = useMemo(() => { + const baseUrl = `${window.location.origin}${ROUTES.GAME_WITH_ID(lobbyId)}`; + // Preserve disableWebex query parameter if present + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.get('disableWebex') === 'true') { + return `${baseUrl}?disableWebex=true`; + } + return baseUrl; + }, [lobbyId]); // Fetch initial lobby data useEffect(() => { diff --git a/frontend/src/hooks/useWebex.jsx b/frontend/src/hooks/useWebex.jsx index bf623e6..f63b4cb 100644 --- a/frontend/src/hooks/useWebex.jsx +++ b/frontend/src/hooks/useWebex.jsx @@ -27,19 +27,24 @@ const useWebex = () => { const [meetingName, setMeetingName] = useState(null); const [theme, setTheme] = useState('dark'); // Default to dark theme for hacker aesthetic + // Only load Webex SDK on /game routes + const shouldLoadWebex = window.location.pathname.startsWith('/game'); + // Check for query parameter to disable Webex const isWebexDisabled = new URLSearchParams(window.location.search).get('disableWebex') === 'true'; useEffect(() => { const initializeWebex = async () => { - // If Webex is disabled via query parameter, skip initialization - if (isWebexDisabled) { + // Skip initialization if not on /game route or if disabled via query parameter + if (!shouldLoadWebex || isWebexDisabled) { setIsRunningInWebex(false); setLoading(false); - setUsername('Unknown User (Webex Disabled)'); - setMeetingName('No Active Meeting'); - setTheme('dark'); // Default to dark theme even when Webex is disabled + if (isWebexDisabled) { + setUsername('Unknown User (Webex Disabled)'); + setMeetingName('No Active Meeting'); + } + setTheme('dark'); // Default to dark theme return; } @@ -98,7 +103,7 @@ const useWebex = () => { }; initializeWebex(); - }, [isWebexDisabled]); + }, [isWebexDisabled, shouldLoadWebex]); /** * Toggles the shared state of the lobby. From f0e937119c65b02f213ef156201674dadc4a01cb Mon Sep 17 00:00:00 2001 From: Adam Weeks Date: Thu, 18 Dec 2025 14:41:02 -0500 Subject: [PATCH 2/2] Refactor UI components for improved readability and maintainability - Cleaned up spacing in App, CreateLobby, and Navbar components for better code clarity. - Enhanced LandingPage button structure for improved formatting. - Updated tests for LandingPage to reflect changes in button structure and ensure functionality. - Adjusted useWebex hook to maintain consistent formatting and improve readability. --- frontend/src/App.jsx | 8 +++- frontend/src/components/CreateLobby.jsx | 8 ++-- frontend/src/components/LandingPage.jsx | 35 ++++++++++------- frontend/src/components/Navbar.jsx | 12 ++++-- .../components/__tests__/LandingPage.test.jsx | 14 +++++-- frontend/src/hooks/__tests__/useWebex.test.js | 38 ++++++++++++++++--- frontend/src/hooks/useWebex.jsx | 2 +- 7 files changed, 83 insertions(+), 34 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7292886..3a81963 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -17,7 +17,7 @@ import { ROUTES } from './constants'; function App() { const [darkMode, setDarkMode] = useState(true); // Set dark mode as default const location = useLocation(); - + // Only load Webex on /game routes const shouldLoadWebex = location.pathname.startsWith('/game'); @@ -40,7 +40,11 @@ function App() { return ( - + } /> diff --git a/frontend/src/components/CreateLobby.jsx b/frontend/src/components/CreateLobby.jsx index fe20bdf..dd68dfd 100644 --- a/frontend/src/components/CreateLobby.jsx +++ b/frontend/src/components/CreateLobby.jsx @@ -22,13 +22,13 @@ const CreateLobby = () => { const [lobbyName, setLobbyName] = useState(''); const [displayName, setDisplayName] = useState(''); const [loading, setLoading] = useState(false); - + // Initialize webexEnabled based on URL parameter const [webexEnabled, setWebexEnabled] = useState(() => { const urlParams = new URLSearchParams(window.location.search); return urlParams.get('disableWebex') !== 'true'; }); - + const { isLoading, username, meetingName } = useWebex(); // Once isLoading is false, set default values from Webex SDK @@ -51,13 +51,13 @@ const CreateLobby = () => { try { const hostId = uuidv4(); const data = await api.createLobby(hostId, displayName, lobbyName); - + // Build the game URL with optional disableWebex parameter let gameUrl = ROUTES.GAME_WITH_ID(data.lobby_id); if (!webexEnabled) { gameUrl += '?disableWebex=true'; } - + navigate(gameUrl, { state: { user: { id: hostId, display_name: displayName } }, }); diff --git a/frontend/src/components/LandingPage.jsx b/frontend/src/components/LandingPage.jsx index c4a311d..834a6dc 100644 --- a/frontend/src/components/LandingPage.jsx +++ b/frontend/src/components/LandingPage.jsx @@ -1,5 +1,12 @@ import React from 'react'; -import { Container, Typography, Button, Box, Paper, Stack } from '@mui/material'; +import { + Container, + Typography, + Button, + Box, + Paper, + Stack, +} from '@mui/material'; import { useNavigate } from 'react-router-dom'; import { ROUTES } from '../constants'; import lockoutImage from '../lockout.png'; @@ -49,19 +56,19 @@ const LandingPage = () => { spacing={2} justifyContent="center" > - + diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 7e96746..9c172be 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -26,10 +26,14 @@ import { Link as RouterLink } from 'react-router-dom'; import { ROUTES } from '../constants'; import AboutModal from './About/AboutModal'; -export default function Navbar({ darkMode, setDarkMode, shouldLoadWebex = false }) { +export default function Navbar({ + darkMode, + setDarkMode, + shouldLoadWebex = false, +}) { const [anchorEl, setAnchorEl] = useState(null); const [aboutModalOpen, setAboutModalOpen] = useState(false); - + // Always call the hook, but it will internally skip initialization if disabled const { isConnected, @@ -163,7 +167,9 @@ export default function Navbar({ darkMode, setDarkMode, shouldLoadWebex = false ) : ( )} - {isConnected ? 'Connected to Webex' : 'Webex Not Connected'} + {isConnected + ? 'Connected to Webex' + : 'Webex Not Connected'} , !isRunningInWebex && ( diff --git a/frontend/src/components/__tests__/LandingPage.test.jsx b/frontend/src/components/__tests__/LandingPage.test.jsx index 49c109d..f16e208 100644 --- a/frontend/src/components/__tests__/LandingPage.test.jsx +++ b/frontend/src/components/__tests__/LandingPage.test.jsx @@ -24,9 +24,13 @@ describe('LandingPage', () => { }); it('renders both launch buttons', () => { - const webexButton = screen.getByRole('button', { name: /launch in webex/i }); - const standaloneButton = screen.getByRole('button', { name: /standalone browser/i }); - + const webexButton = screen.getByRole('button', { + name: /launch in webex/i, + }); + const standaloneButton = screen.getByRole('button', { + name: /standalone browser/i, + }); + expect(webexButton).toBeInTheDocument(); expect(standaloneButton).toBeInTheDocument(); }); @@ -42,6 +46,8 @@ describe('LandingPage', () => { const button = screen.getByRole('button', { name: /standalone browser/i }); fireEvent.click(button); - expect(globalThis.mockNavigate).toHaveBeenCalledWith('/game?disableWebex=true'); + expect(globalThis.mockNavigate).toHaveBeenCalledWith( + '/game?disableWebex=true', + ); }); }); diff --git a/frontend/src/hooks/__tests__/useWebex.test.js b/frontend/src/hooks/__tests__/useWebex.test.js index 1756eec..a262466 100644 --- a/frontend/src/hooks/__tests__/useWebex.test.js +++ b/frontend/src/hooks/__tests__/useWebex.test.js @@ -44,6 +44,16 @@ describe('useWebex (real hook, mocked SDK)', () => { }); globalThis.mockAppInstance.clearShareUrl.mockResolvedValue(); globalThis.mockAppInstance.setShareUrl.mockResolvedValue(); + + // Mock window.location to be on /game route by default + Object.defineProperty(window, 'location', { + writable: true, + value: { + pathname: '/game', + search: '', + origin: 'http://localhost', + }, + }); }); it('sets Webex state and returns expected values after init', async () => { @@ -68,16 +78,18 @@ describe('useWebex (real hook, mocked SDK)', () => { await waitFor(() => expect(result.current.loading).toBe(false)); - expect(result.current.error).toMatch(/Webex init failed/); + expect(result.current.error).toBe('Webex init failed'); expect(result.current.isConnected).toBe(false); expect(result.current.isRunningInWebex).toBe(false); }); it('disables Webex SDK initialization when query parameter is set', async () => { // Mock the URL to include the disableWebex query parameter - const originalLocation = window.location; - delete window.location; - window.location = new URL('http://localhost?disableWebex=true'); + window.location = { + pathname: '/game', + search: '?disableWebex=true', + origin: 'http://localhost', + }; const { result } = renderHook(() => useWebex()); @@ -87,8 +99,22 @@ describe('useWebex (real hook, mocked SDK)', () => { expect(result.current.username).toBe('Unknown User (Webex Disabled)'); expect(result.current.meetingName).toBe('No Active Meeting'); expect(result.current.theme).toBe('dark'); // Updated to expect dark theme + }); + + it('skips Webex SDK initialization when not on /game route', async () => { + // Mock the URL to be on the landing page + window.location = { + pathname: '/', + search: '', + origin: 'http://localhost', + }; + + const { result } = renderHook(() => useWebex()); + + await waitFor(() => expect(result.current.loading).toBe(false)); - // Restore the original location object - window.location = originalLocation; + expect(result.current.isRunningInWebex).toBe(false); + expect(result.current.isConnected).toBe(false); + expect(result.current.theme).toBe('dark'); }); }); diff --git a/frontend/src/hooks/useWebex.jsx b/frontend/src/hooks/useWebex.jsx index f63b4cb..fd057fb 100644 --- a/frontend/src/hooks/useWebex.jsx +++ b/frontend/src/hooks/useWebex.jsx @@ -29,7 +29,7 @@ const useWebex = () => { // Only load Webex SDK on /game routes const shouldLoadWebex = window.location.pathname.startsWith('/game'); - + // Check for query parameter to disable Webex const isWebexDisabled = new URLSearchParams(window.location.search).get('disableWebex') === 'true';