Skip to content
Merged
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
19 changes: 10 additions & 9 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
const location = useLocation();

useEffect(() => {
// Only switch from dark if webex theme explicitly requests light
setDarkMode(webexTheme !== 'light');
}, [webexTheme]);
// Only load Webex on /game routes
const shouldLoadWebex = location.pathname.startsWith('/game');

const theme = createTheme({
palette: {
Expand All @@ -43,7 +40,11 @@ function App() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Navbar darkMode={darkMode} setDarkMode={setDarkMode} />
<Navbar
darkMode={darkMode}
setDarkMode={setDarkMode}
shouldLoadWebex={shouldLoadWebex}
/>
<Container>
<Routes>
<Route path={ROUTES.HOME} element={<LandingPage />} />
Expand Down
43 changes: 40 additions & 3 deletions frontend/src/components/CreateLobby.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
CircularProgress,
Box,
Paper,
FormControlLabel,
Checkbox,
} from '@mui/material';
import { v4 as uuidv4 } from 'uuid';
import { ROUTES } from '../constants';
Expand All @@ -20,20 +22,27 @@ 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);
}
if (username) {
setDisplayName(username);
}
}
}, [isLoading, meetingName, username]);
}, [isLoading, meetingName, username, webexEnabled]);

const handleCreateLobby = async () => {
if (!lobbyName.trim() || !displayName.trim()) return;
Expand All @@ -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) {
Expand Down Expand Up @@ -99,6 +115,27 @@ const CreateLobby = () => {
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
/>
<Box sx={{ mt: 2, textAlign: 'left' }}>
<FormControlLabel
control={
<Checkbox
checked={webexEnabled}
onChange={(e) => setWebexEnabled(e.target.checked)}
sx={{
color: 'primary.main',
'&.Mui-checked': {
color: 'primary.main',
},
}}
/>
}
label={
<Typography variant="body2" color="textSecondary">
Enable Webex Integration
</Typography>
}
/>
</Box>
<Button
type="submit"
variant="contained"
Expand Down
60 changes: 46 additions & 14 deletions frontend/src/components/LandingPage.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import React from 'react';
import { Container, Typography, Button, Box, Paper } 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';
import VideocamIcon from '@mui/icons-material/Videocam';
import VideocamOffIcon from '@mui/icons-material/VideocamOff';

const LandingPage = () => {
const navigate = useNavigate();
Expand Down Expand Up @@ -42,20 +51,43 @@ const LandingPage = () => {
trap?
</Typography>
<Box sx={{ mt: 4 }}>
<Button
variant="contained"
color="primary"
size="large"
onClick={() => navigate(ROUTES.GAME)}
sx={{
boxShadow: '0 0 10px #00ff00',
'&:hover': {
boxShadow: '0 0 15px #00ff00',
},
}}
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={2}
justifyContent="center"
>
Create a Game
</Button>
<Button
variant="contained"
color="primary"
size="large"
startIcon={<VideocamIcon />}
onClick={() => navigate(ROUTES.GAME)}
sx={{
boxShadow: '0 0 10px #00ff00',
'&:hover': {
boxShadow: '0 0 15px #00ff00',
},
}}
>
Launch in Webex
</Button>
<Button
variant="outlined"
color="primary"
size="large"
startIcon={<VideocamOffIcon />}
onClick={() => navigate(`${ROUTES.GAME}?disableWebex=true`)}
sx={{
borderColor: 'primary.main',
'&:hover': {
borderColor: 'primary.main',
backgroundColor: 'rgba(0, 255, 0, 0.1)',
},
}}
>
Standalone Browser
</Button>
</Stack>
</Box>
</Paper>
</Container>
Expand Down
129 changes: 71 additions & 58 deletions frontend/src/components/Navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,15 @@ 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,
Expand Down Expand Up @@ -126,64 +132,70 @@ export default function Navbar({ darkMode, setDarkMode }) {
</IconButton>
</Tooltip>

{/* Webex Info Menu */}
<Tooltip title="Webex Info">
<IconButton
color="inherit"
onClick={handleMenuOpen}
aria-label="Webex Info Menu"
sx={{
'&:hover': {
color: darkMode ? 'primary.main' : 'inherit',
},
}}
>
<MenuIcon />
</IconButton>
</Tooltip>

<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
>
{loading ? (
<MenuItem>
<CircularProgress size={18} />
</MenuItem>
) : (
[
<MenuItem key="status">
{isConnected ? (
<CheckIcon color="success" sx={{ mr: 1 }} />
) : (
<CrossIcon color="error" sx={{ mr: 1 }} />
)}
{isConnected ? 'Connected to Webex' : 'Webex Not Connected'}
</MenuItem>,

!isRunningInWebex && (
<MenuItem key="not-webex">
<ErrorIcon color="warning" sx={{ mr: 1 }} />
Running Outside Webex
</MenuItem>
),

username && <MenuItem key="username">{username}</MenuItem>,

meetingName && (
<MenuItem key="meetingName">{meetingName}</MenuItem>
),

error && (
<MenuItem key="error">
<ErrorIcon color="error" sx={{ mr: 1 }} />
{error}
{/* Webex Info Menu - Only show on /game routes */}
{shouldLoadWebex && (
<>
<Tooltip title="Webex Info">
<IconButton
color="inherit"
onClick={handleMenuOpen}
aria-label="Webex Info Menu"
sx={{
'&:hover': {
color: darkMode ? 'primary.main' : 'inherit',
},
}}
>
<MenuIcon />
</IconButton>
</Tooltip>

<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
>
{loading ? (
<MenuItem>
<CircularProgress size={18} />
</MenuItem>
),
]
)}
</Menu>
) : (
[
<MenuItem key="status">
{isConnected ? (
<CheckIcon color="success" sx={{ mr: 1 }} />
) : (
<CrossIcon color="error" sx={{ mr: 1 }} />
)}
{isConnected
? 'Connected to Webex'
: 'Webex Not Connected'}
</MenuItem>,

!isRunningInWebex && (
<MenuItem key="not-webex">
<ErrorIcon color="warning" sx={{ mr: 1 }} />
Running Outside Webex
</MenuItem>
),

username && <MenuItem key="username">{username}</MenuItem>,

meetingName && (
<MenuItem key="meetingName">{meetingName}</MenuItem>
),

error && (
<MenuItem key="error">
<ErrorIcon color="error" sx={{ mr: 1 }} />
{error}
</MenuItem>
),
]
)}
</Menu>
</>
)}
</Toolbar>
</AppBar>

Expand All @@ -197,4 +209,5 @@ export default function Navbar({ darkMode, setDarkMode }) {
Navbar.propTypes = {
darkMode: PropTypes.bool.isRequired,
setDarkMode: PropTypes.func.isRequired,
shouldLoadWebex: PropTypes.bool,
};
26 changes: 21 additions & 5 deletions frontend/src/components/__tests__/LandingPage.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,31 @@ 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',
);
});
});
Loading