Skip to content

Commit e2abe18

Browse files
Johnwz123RichDom2185martin-henzlhw-1
authored
Add a fullscreen button for the game div container (#2855)
* Add a fullscreen button for the game div container * Modify code to use the useFullscreen hook from the @mantine/hooks package * Bump dependencies * Use `IconNames` instead of magic strings --------- Co-authored-by: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Co-authored-by: Martin Henz <henz@comp.nus.edu.sg> Co-authored-by: Lee Hyung Woon / 이형운 <leehyungwoonsamuel@gmail.com>
1 parent f4d6e78 commit e2abe18

File tree

4 files changed

+100
-1
lines changed

4 files changed

+100
-1
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@blueprintjs/icons": "^5.5.0",
3232
"@blueprintjs/popover2": "^2.0.0",
3333
"@blueprintjs/select": "^5.0.0",
34+
"@mantine/hooks": "^7.7.0",
3435
"@octokit/rest": "^20.0.0",
3536
"@reduxjs/toolkit": "^1.9.7",
3637
"@sentry/browser": "^7.57.0",

src/pages/academy/game/Game.tsx

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { Icon } from '@blueprintjs/core';
2+
import { IconNames } from '@blueprintjs/icons';
3+
import { useFullscreen } from '@mantine/hooks';
14
import React from 'react';
25
import { useDispatch } from 'react-redux';
36
import { useTypedSelector } from 'src/commons/utils/Hooks';
@@ -50,9 +53,93 @@ function Game() {
5053
}
5154
}, [session, achievements, goals]);
5255

56+
// This is a custom hook imported from @mantine/hooks that handles the fullscreen logic
57+
// It returns a ref to attach to the element that should be fullscreened,
58+
// a function to toggle fullscreen and a boolean indicating whether the element is fullscreen
59+
const {
60+
ref: fullscreenRef,
61+
toggle: toggleFullscreen,
62+
fullscreen: isFullscreen
63+
} = useFullscreen<HTMLDivElement>();
64+
65+
// This function is a wrapper around toggleFullscreen that also locks the screen orientation
66+
// to landscape when entering fullscreen and unlocks it when exiting fullscreen
67+
const enhancedToggleFullscreen = async () => {
68+
toggleFullscreen();
69+
70+
if (window.screen.orientation) {
71+
if (!isFullscreen) {
72+
window.screen.orientation.lock('landscape');
73+
} else {
74+
window.screen.orientation.unlock();
75+
}
76+
}
77+
};
78+
79+
const gameDisplayRef = React.useRef<HTMLDivElement | null>(null);
80+
81+
// This function sets the gameDisplayRef and also calls the ref callback from useFullscreen
82+
// to attach the fullscreen logic to the game display element
83+
const setGameDisplayRefs = React.useCallback(
84+
(node: HTMLDivElement | null) => {
85+
// Refs returned by useRef()
86+
gameDisplayRef.current = node;
87+
88+
// Ref callback from useFullscreen
89+
fullscreenRef(node);
90+
},
91+
[fullscreenRef]
92+
);
93+
94+
// Logic for the fullscreen button to dynamically adjust its size, position and padding
95+
// based on the size of the game display.
96+
const [iconSize, setIconSize] = React.useState(0);
97+
const [iconLeft, setIconLeft] = React.useState('0px');
98+
const [iconPadding, setIconPadding] = React.useState('0px');
99+
100+
React.useEffect(() => {
101+
const handleResize = () => {
102+
if (gameDisplayRef.current) {
103+
const aspectRatio = 16 / 9;
104+
const height = gameDisplayRef.current.offsetHeight;
105+
const width = gameDisplayRef.current.offsetWidth;
106+
const size = height / 40;
107+
const padding = height / 50;
108+
const leftOffset =
109+
isFullscreen || height * aspectRatio > width ? 0 : (width - height * aspectRatio) / 2;
110+
setIconSize(size);
111+
setIconPadding(`${padding}px`);
112+
setIconLeft(`${leftOffset}px`);
113+
}
114+
};
115+
116+
// When exiting fullscreen, the browser might not have completed the transition
117+
// at the time handleResize is called, so the height of gameDisplayRef.current
118+
// is still the fullscreen height.
119+
// To fix this, we delay handleResize by 100ms.
120+
const delayedHandleResize = () => {
121+
setTimeout(handleResize, 100);
122+
};
123+
124+
window.addEventListener('resize', delayedHandleResize);
125+
delayedHandleResize();
126+
127+
return () => window.removeEventListener('resize', delayedHandleResize);
128+
}, [isFullscreen]);
129+
53130
return (
54131
<>
55-
<div id="game-display"></div>
132+
<div id="game-display" ref={setGameDisplayRefs}>
133+
<Icon
134+
id="fullscreen-button"
135+
icon={isFullscreen ? IconNames.MINIMIZE : IconNames.FULLSCREEN}
136+
color="white"
137+
htmlTitle={isFullscreen ? 'Exit full screen' : 'Full screen'}
138+
size={iconSize}
139+
onClick={enhancedToggleFullscreen}
140+
style={{ left: iconLeft, padding: iconPadding }}
141+
/>
142+
</div>
56143
{isTestStudent && (
57144
<div className="Horizontal">
58145
<button

src/styles/_game.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,9 @@
44
width: 100%;
55
align-items: center;
66
}
7+
8+
#fullscreen-button {
9+
position: absolute;
10+
cursor: pointer;
11+
z-index: 100;
12+
}

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1919,6 +1919,11 @@
19191919
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
19201920
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
19211921

1922+
"@mantine/hooks@^7.7.0":
1923+
version "7.7.0"
1924+
resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-7.7.0.tgz#52f0fdc97e953798d2e632aa5e90959f389cbd1e"
1925+
integrity sha512-m99vMzeONMpBLv0Rcb2LD88xAhpvwVdTMBo/7WohBDYtk1shJKHAc/WbQ/cJPcNk11Bzp/mhx/EPNZfs9+NwZA==
1926+
19221927
"@mapbox/node-pre-gyp@^1.0.0":
19231928
version "1.0.10"
19241929
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz#8e6735ccebbb1581e5a7e652244cadc8a844d03c"

0 commit comments

Comments
 (0)