Skip to content

Commit

Permalink
refactor timer code to hook and TimeViewer component, add it to mahjong
Browse files Browse the repository at this point in the history
  • Loading branch information
ayan4m1 committed Jan 1, 2024
1 parent 2f0e255 commit fcacf22
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 107 deletions.
64 changes: 48 additions & 16 deletions src/components/mahjongBoard.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,36 @@ import {
ButtonGroup,
Button
} from 'react-bootstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faExclamationTriangle,
faFloppyDisk,
faFolderOpen,
faHighlighter,
faRecycle,
faTrash
} from '@fortawesome/free-solid-svg-icons';

import MahjongTile from 'components/mahjongTile';
import TimeViewer from 'components/timeViewer';
import {
generateLayout,
getTileImagePath,
getAvailableMatches,
isOpen,
isMatch
} from 'utils/mahjong';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faExclamationTriangle,
faFloppyDisk,
faFolderOpen,
faRecycle,
faTrash
} from '@fortawesome/free-solid-svg-icons';
import useTimer from 'hooks/useTimer';

export default function MahjongBoard({ images }) {
const {
elapsedTime,
running,
startTimer,
stopTimer,
toggleTimer,
resetTimer
} = useTimer();
const boardRef = useRef(null);
const [solved, setSolved] = useState(false);
const [failed, setFailed] = useState(false);
Expand Down Expand Up @@ -70,7 +81,7 @@ export default function MahjongBoard({ images }) {
[setLayout]
);
const handleTileClick = useCallback(
(tile) =>
(tile) => {
setActiveTile((prevVal) => {
if (prevVal === tile.index) {
return null;
Expand All @@ -80,9 +91,15 @@ export default function MahjongBoard({ images }) {
} else if (isOpen(tile, layout)) {
return tile.index;
}
}),
[setActiveTile, handleMatch, layout]
});
startTimer();
},
[setActiveTile, handleMatch, layout, startTimer]
);
const handleNew = useCallback(() => {
setLayout(generateLayout('turtle'));
resetTimer();
}, [setLayout, resetTimer]);

useEffect(() => {
if (boardRef.current) {
Expand All @@ -91,7 +108,10 @@ export default function MahjongBoard({ images }) {
}, [boardRef, solved, failed]);

useEffect(() => {
setSolved(!layout.length);
if (!layout.length) {
setSolved(true);
stopTimer();
}
}, [layout]);

useEffect(() => {
Expand Down Expand Up @@ -119,11 +139,13 @@ export default function MahjongBoard({ images }) {
<Col xs={4}>
<h4 className="mt-0 mb-2">Progress</h4>
</Col>

<Col xs={2}>
<h4 className="mt-0 mb-2">Matches Left</h4>
</Col>
<Col xs={6}>
<Col xs={2}>
<h4 className="mt-0 mb-2">Time</h4>
</Col>
<Col xs={4}>
<h4 className="mt-0 mb-2">Options</h4>
</Col>
</Row>
Expand All @@ -144,9 +166,16 @@ export default function MahjongBoard({ images }) {
)}
</p>
</Col>
<Col xs={6}>
<Col xs={2}>
<TimeViewer
elapsedTime={elapsedTime}
running={running}
onToggle={toggleTimer}
/>
</Col>
<Col xs={4}>
<ButtonGroup className="me-4">
<Button onClick={() => setLayout(generateLayout('turtle'))}>
<Button onClick={handleNew}>
<FontAwesomeIcon icon={faRecycle} /> New
</Button>
<Button disabled>
Expand All @@ -158,6 +187,9 @@ export default function MahjongBoard({ images }) {
<Button disabled>
<FontAwesomeIcon icon={faFolderOpen} /> Load
</Button>
<Button disabled>
<FontAwesomeIcon icon={faHighlighter} /> Hint
</Button>
</ButtonGroup>
</Col>
</Row>
Expand Down
90 changes: 25 additions & 65 deletions src/components/sudokuBoard.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import { hsl } from 'd3-color';
import { chunk } from 'lodash-es';
import { getSudoku } from 'sudoku-gen';
import { differenceInSeconds } from 'date-fns';
import useLocalStorageState from 'use-local-storage-state';
import {
Alert,
Expand All @@ -15,39 +14,35 @@ import {
Dropdown,
DropdownButton
} from 'react-bootstrap';
import {
Fragment,
useCallback,
useEffect,
useMemo,
useRef,
useState
} from 'react';
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faRecycle,
faFloppyDisk,
faFolderOpen,
faTrash,
faPause,
faPlay
faTrash
} from '@fortawesome/free-solid-svg-icons';

import SudokuCell from 'components/sudokuCell';
import TimeViewer from 'components/timeViewer';
import useRainbow from 'hooks/useRainbow';
import useTimer from 'hooks/useTimer';
import {
formatTime,
checkSolution,
getInvalids,
getInvalidArray,
difficulties
} from 'utils/sudoku';
import useRainbow from 'hooks/useRainbow';

export default function SudokuBoard({ mode }) {
const intervalRef = useRef(null);
const startTime = useMemo(() => Date.now(), []);
const [currentTime, setCurrentTime] = useState(Date.now());
const [paused, setPaused] = useState(false);
const {
elapsedTime,
running,
startTimer,
resetTimer,
incrementTimer,
toggleTimer
} = useTimer();
const [solved, setSolved] = useState(false);
const [solveRate, setSolveRate] = useState(null);
const [activeCell, setActiveCell] = useState([-1, -1]);
Expand Down Expand Up @@ -85,10 +80,10 @@ export default function SudokuBoard({ mode }) {

return newVal;
});
setPaused(false);
startTimer();
}, []);
const handleNew = useCallback((difficulty) => {
setCurrentTime(Date.now());
resetTimer();
setPuzzle(getSudoku(difficulty));
setValues(getInvalidArray());
}, []);
Expand All @@ -97,9 +92,9 @@ export default function SudokuBoard({ mode }) {
setSavedState({
puzzle,
values,
elapsedTime: differenceInSeconds(currentTime, startTime)
elapsedTime
}),
[puzzle, values, currentTime, startTime]
[puzzle, values, elapsedTime]
);
const handleLoad = useCallback(() => {
if (!savedState) {
Expand All @@ -108,14 +103,9 @@ export default function SudokuBoard({ mode }) {

setPuzzle(savedState.puzzle);
setValues(savedState.values);
setCurrentTime(startTime + savedState.elapsedTime * 1000);
incrementTimer(savedState.elapsedTime * 1000);
}, [savedState]);
const handleClear = useCallback(() => setSavedState(null), []);
const handlePause = useCallback(() => setPaused((prevVal) => !prevVal), []);
const handleDocumentVisibilityChange = useCallback(
() => setPaused(document.hidden),
[]
);
const { color: animationColor, start, stop } = useRainbow(false, false);

useEffect(() => {
Expand All @@ -124,7 +114,7 @@ export default function SudokuBoard({ mode }) {
if (solved) {
setSolved(solved);
setSolveRate(
differenceInSeconds(currentTime, startTime) /
elapsedTime /
cells.reduce(
(result, row) =>
result +
Expand All @@ -141,27 +131,6 @@ export default function SudokuBoard({ mode }) {
}
}, [cells, values, puzzle]);

useEffect(() => {
if (!paused) {
intervalRef.current = setInterval(
() => setCurrentTime((prevVal) => prevVal + 1000),
1000
);

return () => clearInterval(intervalRef.current);
} else if (intervalRef.current) {
clearInterval(intervalRef.current);
}
}, [paused]);

useEffect(() => {
// if we remove visibilitychange on unmount we lose it, so only set it up once
document.addEventListener(
'visibilitychange',
handleDocumentVisibilityChange
);
});

return (
<Card body>
<Container fluid>
Expand All @@ -170,21 +139,12 @@ export default function SudokuBoard({ mode }) {
<h1>Sudoku</h1>
</Col>
{mode === 'timed' && (
<Col
xs={4}
className="d-flex justify-content-center align-items-center"
>
<span className="font-monospace">
{formatTime(currentTime, startTime)}
</span>
<Button
variant="info"
size="sm"
className="ms-2"
onClick={handlePause}
>
<FontAwesomeIcon icon={paused ? faPlay : faPause} fixedWidth />
</Button>
<Col xs={4} className="d-flex align-items-center">
<TimeViewer
elapsedTime={elapsedTime}
running={running}
onToggle={toggleTimer}
/>
</Col>
)}
</Row>
Expand Down
25 changes: 25 additions & 0 deletions src/components/timeViewer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import PropTypes from 'prop-types';
import { faPause, faPlay } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button, InputGroup } from 'react-bootstrap';

import { formatTime } from 'utils/timer';

export default function TimeViewer({ elapsedTime, running, onToggle }) {
return (
<InputGroup>
<InputGroup.Text className="font-monospace">
{formatTime(elapsedTime)}
</InputGroup.Text>
<Button variant="info" size="sm" onClick={onToggle}>
<FontAwesomeIcon icon={running ? faPause : faPlay} fixedWidth />
</Button>
</InputGroup>
);
}

TimeViewer.propTypes = {
elapsedTime: PropTypes.number.isRequired,
running: PropTypes.bool.isRequired,
onToggle: PropTypes.func.isRequired
};
55 changes: 55 additions & 0 deletions src/hooks/useTimer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { differenceInSeconds } from 'date-fns';
import { useCallback, useEffect, useState, useMemo, useRef } from 'react';

export default function useTimer(run = true, interval = 1000) {
const intervalRef = useRef(null);
const startTime = useMemo(() => Date.now(), []);
const [running, setRunning] = useState(run);
const [currentTime, setCurrentTime] = useState(startTime);
const startTimer = useCallback(() => setRunning(true), [setRunning]);
const stopTimer = useCallback(() => setRunning(false), [setRunning]);
const toggleTimer = useCallback(
() => setRunning((prevVal) => !prevVal),
[setRunning]
);
const resetTimer = useCallback(
() => setCurrentTime(startTime),
[setCurrentTime, startTime]
);
const incrementTimer = useCallback(
(amount = interval) => setCurrentTime((prevVal) => prevVal + amount),
[setCurrentTime]
);
const handleDocumentVisibilityChange = useCallback(
() => (document.hidden ? stopTimer() : startTimer()),
[]
);

useEffect(() => {
if (running) {
intervalRef.current = setInterval(incrementTimer, interval);

return () => clearInterval(intervalRef.current);
} else if (intervalRef.current) {
clearInterval(intervalRef.current);
}
}, [running]);

useEffect(() => {
// if we remove visibilitychange on unmount we lose it, so only set it up once
document.addEventListener(
'visibilitychange',
handleDocumentVisibilityChange
);
});

return {
elapsedTime: differenceInSeconds(currentTime, startTime),
running,
startTimer,
stopTimer,
toggleTimer,
resetTimer,
incrementTimer
};
}
Loading

0 comments on commit fcacf22

Please sign in to comment.