Skip to content

Commit

Permalink
feat: implement fullscreen mode in timer app
Browse files Browse the repository at this point in the history
  • Loading branch information
fivestar committed Jan 2, 2025
1 parent 8ebc097 commit 414ddde
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 15 deletions.
11 changes: 8 additions & 3 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
*/

--zindex-site-header: 100;
--zindex-fullscreen: 500;
--zindex-time-picker: 1000;

--layout-padding: 16px;
Expand All @@ -61,6 +62,9 @@
--brand-size: var(--font-size-large);
--site-header-height: calc(var(--layout-padding) * 2 + var(--brand-size));
--content-header-flex-wrap: wrap;

--timer-display-progress-bar-height: 8px;
--timer-display-padding: 8px;
}

/* mobile */
Expand Down Expand Up @@ -467,19 +471,19 @@ textarea {
& {
border-color: transparent;
background-color: transparent;
color: var(--gray-600);
color: var(--gray-500);
}

&:hover {
border-color: transparent;
background-color: transparent;
color: var(--gray-500);
color: var(--gray-400);
}

&:active {
border-color: transparent;
background-color: transparent;
color: var(--gray-400);
color: var(--gray-300);
}
}
}
Expand Down Expand Up @@ -510,6 +514,7 @@ textarea {
display: flex;
align-items: center;
padding: 3px 10px;
background-color: var(--white);
border: 1px solid var(--gray-100);
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
Expand Down
4 changes: 2 additions & 2 deletions src/app/timer/TimeField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ export function TimeField({ value, onInput }: TimeFieldProps) {
}

const rect = inputRef.current.getBoundingClientRect();
pickerRef.current.style.top = rect.bottom + window.scrollY + 'px';
pickerRef.current.style.left = rect.left + window.scrollX + 'px';
pickerRef.current.style.top = rect.height + 'px';
pickerRef.current.style.right = '0';
setShowTimePicker(true);
};

Expand Down
64 changes: 60 additions & 4 deletions src/app/timer/TimerApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,43 @@ import { StartButton, ResetButton, GongButton } from './Button';
import { TimerDisplay } from './TimerDisplay';
import { toSeconds, isValidTimeString } from './utils';

const CONTROL_HIDE_SECONDS = 5;

export default function TimerApp() {
const [timer, setTimer] = useState<TimerState>({
state: 'STOPPED',
startTime: '5:00',
secondsAtStart: 5 * 60,
secondsRemaining: 5 * 60,
});
const [isFullscreen, setIsFullscreen] = useState(false);
const [isRotated, setIsRotated] = useState(false);
const [isControlVisible, setIsControlVisible] = useState(true);
const audioContextRef = useRef<AudioContext | null>(null);
const audioBufferRef = useRef<AudioBuffer | null>(null);
const controlHideTimer = useRef<NodeJS.Timeout | undefined>(undefined);

const checkOrientation = () => {
const width = window.innerWidth;
const height = window.innerHeight;
setIsRotated(height > width);
};

const resetControlHideTimer = () => {
clearTimeout(controlHideTimer.current);
setIsControlVisible(true);

if (isFullscreen && timer.state == 'STARTED') {
controlHideTimer.current = setTimeout(
() => setIsControlVisible(false),
CONTROL_HIDE_SECONDS * 1000
);
}
};

const handleInteraction = () => {
resetControlHideTimer();
};

useEffect(() => {
audioContextRef.current = new AudioContext();
Expand All @@ -27,6 +55,16 @@ export default function TimerApp() {
audioBufferRef.current = audioBuffer || null;
})
.catch((err) => console.error('Error loading audio file', err));

checkOrientation();
window.addEventListener('resize', checkOrientation);

return () => {
window.removeEventListener('resize', checkOrientation);
if (controlHideTimer.current) {
clearTimeout(controlHideTimer.current);
}
};
}, []);

useInterval(
Expand All @@ -47,6 +85,10 @@ export default function TimerApp() {
}
}, [timer.secondsRemaining]);

useEffect(() => {
resetControlHideTimer();
}, [timer.state, isFullscreen]);

const handleInput = (value: string) => {
setTimer({
...timer,
Expand Down Expand Up @@ -114,11 +156,21 @@ export default function TimerApp() {
source.start(0);
};

const handleToggleFullscreen = () => {
setIsFullscreen((prev) => !prev);
};

const startTimeInvalid = !isValidTimeString(timer.startTime);

return (
<>
<div className="timer-controls">
<div
className="timer-layout"
data-fullscreen={isFullscreen}
data-rotated={isRotated}
onClick={handleInteraction}
onTouchStart={handleInteraction}
>
<div className="timer-controls" data-standby={!isControlVisible}>
<TimeField value={timer.startTime} onInput={handleInput} />
<StartButton
timerState={timer.state}
Expand All @@ -131,7 +183,11 @@ export default function TimerApp() {
<GongButton disabled={startTimeInvalid} onGong={playGong} />
</div>

<TimerDisplay {...timer} />
</>
<TimerDisplay
{...timer}
isFullscreen={isFullscreen}
onToggleFullscreen={handleToggleFullscreen}
/>
</div>
);
}
25 changes: 23 additions & 2 deletions src/app/timer/TimerDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCompress, faExpand } from '@fortawesome/free-solid-svg-icons';
import { toTimeString } from './utils';
import { TimerState } from './TimerState';

export function TimerDisplay({ state, secondsRemaining, secondsAtStart }: TimerState) {
type TimerDisplayProps = TimerState & {
isFullscreen: boolean;
onToggleFullscreen: () => void;
};

export function TimerDisplay({
state,
secondsRemaining,
secondsAtStart,
isFullscreen,
onToggleFullscreen,
}: TimerDisplayProps) {
const renderVariant = () => {
if (secondsRemaining <= 0) {
return 'timeup';
Expand All @@ -19,7 +32,7 @@ export function TimerDisplay({ state, secondsRemaining, secondsAtStart }: TimerS
<div
className="timer-display"
data-variant={renderVariant()}
aria-label={secondsRemaining + ' remaining...'}
aria-label={secondsRemaining + ' seconds remaining...'}
>
<progress
className="timer-display__progress"
Expand All @@ -28,6 +41,14 @@ export function TimerDisplay({ state, secondsRemaining, secondsAtStart }: TimerS
hidden={state == 'STOPPED'}
></progress>
<div className="timer-display__time">{toTimeString(secondsRemaining)}</div>
<div className="timer-display__toggle">
<button className="btn" data-variant="transparent" onClick={(e) => onToggleFullscreen()}>
<FontAwesomeIcon icon={isFullscreen ? faCompress : faExpand} size="lg" />
<span className="visually-hidden">
{isFullscreen ? 'Compress' : 'Expand '} Fullscreen
</span>
</button>
</div>
</div>
);
}
88 changes: 84 additions & 4 deletions src/app/timer/timer.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
.timer-layout {
position: relative;
width: 100%;
transition: transform 0.3s ease;
transform-origin: center center;
}

.timer-controls {
display: flex;
margin-bottom: 30px;
transition: opacity 0.1s ease;
opacity: 1;

&[data-standby='true'] {
opacity: 0;
pointer-events: none;
}
}

.timer-control {
Expand All @@ -9,6 +23,7 @@
}

&.timer-control--time {
position: relative;
display: inline-block;

.timer-control__input {
Expand Down Expand Up @@ -70,7 +85,6 @@
margin-left: -50vw;
margin-right: -50vw;

&,
&[data-variant='default'] {
background-color: var(--gray-100);

Expand Down Expand Up @@ -146,10 +160,14 @@
}

.timer-display__time {
display: flex;
width: 100%;
height: 100%;
font-family: var(--font-noto-sans-display), sans-serif;
font-weight: var(--font-weight-semi-bold);
font-size: calc(20vw + 9vh);
text-align: center;
font-size: 36vw;
align-items: center;
justify-content: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
Expand All @@ -161,7 +179,7 @@
left: 0;
right: 0;
width: 100%;
height: 8px;
height: var(--timer-display-progress-bar-height);
background-color: var(--gray-200);
border-radius: 0;

Expand All @@ -174,4 +192,66 @@
visibility: hidden;
}
}

.timer-display__toggle {
position: absolute;
bottom: 0;
right: 0;
margin: var(--timer-display-padding);
color: var(--gray-300);
}
}

/* Fullscreen mode */
.timer-layout[data-fullscreen='true'] {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
z-index: var(--zindex-fullscreen);

.timer-controls {
position: fixed;
top: var(--timer-display-progress-bar-height);
left: 0;
margin: var(--timer-display-padding);
z-index: 1;
}

.timer-display {
width: 100%;
height: 100%;
left: 0;
right: 0;
margin: 0;

&[data-variant='default'] {
background-color: var(--white);
}

.timer-display__time {
height: 100%;
font-size: 36vw;
}
}

&[data-rotated='true'] {
width: 100vh;
height: 100vw;
top: calc((100vh - 100vw) / 2);
left: calc((100vw - 100vh) / 2);
transform: rotate(90deg);

.timer-display {
.timer-display__time {
font-size: 36vh;
}
}
}
}

0 comments on commit 414ddde

Please sign in to comment.