diff --git a/package.json b/package.json index afe2b3dded..40c3ee9e69 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@blueprintjs/icons": "^5.5.0", "@blueprintjs/popover2": "^2.0.0", "@blueprintjs/select": "^5.0.0", + "@mantine/hooks": "^7.7.0", "@octokit/rest": "^20.0.0", "@reduxjs/toolkit": "^1.9.7", "@sentry/browser": "^7.57.0", diff --git a/src/pages/academy/game/Game.tsx b/src/pages/academy/game/Game.tsx index 65adf5495f..f82c6264dd 100644 --- a/src/pages/academy/game/Game.tsx +++ b/src/pages/academy/game/Game.tsx @@ -1,3 +1,6 @@ +import { Icon } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { useFullscreen } from '@mantine/hooks'; import React from 'react'; import { useDispatch } from 'react-redux'; import { useTypedSelector } from 'src/commons/utils/Hooks'; @@ -50,9 +53,93 @@ function Game() { } }, [session, achievements, goals]); + // This is a custom hook imported from @mantine/hooks that handles the fullscreen logic + // It returns a ref to attach to the element that should be fullscreened, + // a function to toggle fullscreen and a boolean indicating whether the element is fullscreen + const { + ref: fullscreenRef, + toggle: toggleFullscreen, + fullscreen: isFullscreen + } = useFullscreen(); + + // This function is a wrapper around toggleFullscreen that also locks the screen orientation + // to landscape when entering fullscreen and unlocks it when exiting fullscreen + const enhancedToggleFullscreen = async () => { + toggleFullscreen(); + + if (window.screen.orientation) { + if (!isFullscreen) { + window.screen.orientation.lock('landscape'); + } else { + window.screen.orientation.unlock(); + } + } + }; + + const gameDisplayRef = React.useRef(null); + + // This function sets the gameDisplayRef and also calls the ref callback from useFullscreen + // to attach the fullscreen logic to the game display element + const setGameDisplayRefs = React.useCallback( + (node: HTMLDivElement | null) => { + // Refs returned by useRef() + gameDisplayRef.current = node; + + // Ref callback from useFullscreen + fullscreenRef(node); + }, + [fullscreenRef] + ); + + // Logic for the fullscreen button to dynamically adjust its size, position and padding + // based on the size of the game display. + const [iconSize, setIconSize] = React.useState(0); + const [iconLeft, setIconLeft] = React.useState('0px'); + const [iconPadding, setIconPadding] = React.useState('0px'); + + React.useEffect(() => { + const handleResize = () => { + if (gameDisplayRef.current) { + const aspectRatio = 16 / 9; + const height = gameDisplayRef.current.offsetHeight; + const width = gameDisplayRef.current.offsetWidth; + const size = height / 40; + const padding = height / 50; + const leftOffset = + isFullscreen || height * aspectRatio > width ? 0 : (width - height * aspectRatio) / 2; + setIconSize(size); + setIconPadding(`${padding}px`); + setIconLeft(`${leftOffset}px`); + } + }; + + // When exiting fullscreen, the browser might not have completed the transition + // at the time handleResize is called, so the height of gameDisplayRef.current + // is still the fullscreen height. + // To fix this, we delay handleResize by 100ms. + const delayedHandleResize = () => { + setTimeout(handleResize, 100); + }; + + window.addEventListener('resize', delayedHandleResize); + delayedHandleResize(); + + return () => window.removeEventListener('resize', delayedHandleResize); + }, [isFullscreen]); + return ( <> -
+
+ +
{isTestStudent && (