223 lines
8.4 KiB
TypeScript
223 lines
8.4 KiB
TypeScript
import { useState, useRef } from "react";
|
|
import { CountdownCircleTimer } from "react-countdown-circle-timer";
|
|
import Reward, { RewardElement } from 'react-rewards';
|
|
import Link from "../components/Link";
|
|
import { useUser } from "../user-context";
|
|
import "./timer.css";
|
|
import {
|
|
getPercentageDone,
|
|
} from "../lib/game-utils";
|
|
import {
|
|
saveResult,
|
|
postSavedResults,
|
|
} from "../lib/game-results";
|
|
import {
|
|
AT,
|
|
getTimestamp,
|
|
} from "@lingdocs/lingdocs-main";
|
|
import {
|
|
Types,
|
|
} from "@lingdocs/pashto-inflector";
|
|
import ReactGA from "react-ga";
|
|
const errorVibration = 200;
|
|
|
|
function GameCore<T>({ questions, Display, timeLimit, Instructions, studyLink, id }:{
|
|
id: string,
|
|
studyLink: string,
|
|
Instructions: (props: { opts?: Types.TextOptions }) => JSX.Element,
|
|
questions: () => QuestionGenerator<T>,
|
|
Display: (props: QuestionDisplayProps<T>) => JSX.Element,
|
|
timeLimit: number;
|
|
}) {
|
|
// TODO: report pass with id to user info
|
|
const rewardRef = useRef<RewardElement | null>(null);
|
|
const { user, pullUser, setUser } = useUser();
|
|
const [finish, setFinish] = useState<undefined | "pass" | { msg: "fail", answer: JSX.Element } | "time out">(undefined);
|
|
const [current, setCurrent] = useState<Current<T> | undefined>(undefined);
|
|
const [questionBox, setQuestionBox] = useState<QuestionGenerator<T>>(questions());
|
|
const [timerKey, setTimerKey] = useState<number>(1);
|
|
|
|
function handleCallback(correct: true | JSX.Element) {
|
|
if (correct === true) handleAdvance();
|
|
else {
|
|
ReactGA.event({
|
|
category: "Game",
|
|
action: "fail on game",
|
|
label: id,
|
|
});
|
|
setFinish({ msg: "fail", answer: correct });
|
|
navigator.vibrate(errorVibration);
|
|
}
|
|
}
|
|
function handleAdvance() {
|
|
const next = questionBox.next();
|
|
if (next.done) handleFinish();
|
|
else setCurrent(next.value);
|
|
}
|
|
function handleResult(result: AT.TestResult) {
|
|
// add the test to the user object
|
|
if (!user) return;
|
|
setUser((u) => {
|
|
// pure type safety with the prevUser
|
|
if (!u) return u;
|
|
return {
|
|
...u,
|
|
tests: [...u.tests, result],
|
|
};
|
|
});
|
|
// save the test result in local storage
|
|
saveResult(result, user.userId);
|
|
// try to post the result
|
|
postSavedResults(user.userId).then((r) => {
|
|
if (r === "sent") pullUser();
|
|
}).catch(console.error);
|
|
}
|
|
function handleFinish() {
|
|
ReactGA.event({
|
|
category: "Game",
|
|
action: "passed game",
|
|
label: id,
|
|
});
|
|
setFinish("pass");
|
|
rewardRef.current?.rewardMe();
|
|
if (!user) return;
|
|
const result: AT.TestResult = {
|
|
done: true,
|
|
time: getTimestamp(),
|
|
id,
|
|
};
|
|
handleResult(result);
|
|
}
|
|
function handleQuit() {
|
|
setFinish(undefined);
|
|
setCurrent(undefined);
|
|
}
|
|
function handleRestart() {
|
|
ReactGA.event({
|
|
category: "Game",
|
|
action: "started game",
|
|
label: id,
|
|
});
|
|
const newQuestionBox = questions();
|
|
const { value } = newQuestionBox.next();
|
|
// just for type safety -- the generator will have at least one question
|
|
if (!value) return;
|
|
setQuestionBox(newQuestionBox);
|
|
setFinish(undefined);
|
|
setCurrent(value);
|
|
setTimerKey(prev => prev + 1);
|
|
}
|
|
function handleTimeOut() {
|
|
ReactGA.event({
|
|
category: "Game",
|
|
action: "timeout on game",
|
|
label: id,
|
|
});
|
|
setFinish("time out");
|
|
navigator.vibrate(errorVibration);
|
|
}
|
|
function getProgressWidth(): string {
|
|
const num = !current
|
|
? 0
|
|
: (finish === "pass")
|
|
? 100
|
|
: getPercentageDone(current.progress);
|
|
return `${num}%`;
|
|
}
|
|
const progressColor = finish === "pass"
|
|
? "success"
|
|
: typeof finish === "object"
|
|
? "danger"
|
|
: "primary";
|
|
const gameRunning = current && finish === undefined;
|
|
return <>
|
|
<div className="text-center" style={{ minHeight: "200px", zIndex: 10, position: "relative" }}>
|
|
<div className="progress" style={{ height: "5px" }}>
|
|
<div className={`progress-bar bg-${progressColor}`} role="progressbar" style={{ width: getProgressWidth() }} />
|
|
</div>
|
|
{current && <div className="d-flex flex-row-reverse mt-2">
|
|
<CountdownCircleTimer
|
|
key={timerKey}
|
|
isPlaying={!!current && !finish}
|
|
size={30}
|
|
colors={["#555555", "#F7B801", "#A30000"]}
|
|
colorsTime={[timeLimit, timeLimit*0.33, 0]}
|
|
strokeWidth={4}
|
|
strokeLinecap="square"
|
|
duration={timeLimit}
|
|
onComplete={handleTimeOut}
|
|
/>
|
|
<button onClick={handleQuit} className="btn btn-outline-secondary btn-sm mr-2">Quit</button>
|
|
</div>}
|
|
<Reward ref={rewardRef} config={{ lifetime: 130, spread: 90, elementCount: 150, zIndex: 500 }} type="confetti">
|
|
<div className="py-3">
|
|
{finish === undefined &&
|
|
(current
|
|
? <div>
|
|
<Display question={current.question} callback={handleCallback} />
|
|
</div>
|
|
: <div>
|
|
<div className="pt-3">
|
|
{/* TODO: ADD IN TEXT DISPLAY OPTIONS HERE TOO - WHEN WE START USING THEM*/}
|
|
<Instructions />
|
|
</div>
|
|
<div>
|
|
<button className="btn btn-primary mt-4" onClick={handleRestart}>Start</button>
|
|
</div>
|
|
</div>)
|
|
}
|
|
{finish === "pass" && <div>
|
|
<h4 className="mt-4">
|
|
<span role="img" aria-label="celebration">🎉</span> Finished!
|
|
</h4>
|
|
<button className="btn btn-secondary mt-4" onClick={handleRestart}>Try Again</button>
|
|
</div>}
|
|
{(typeof finish === "object" || finish === "time out") && <div>
|
|
<h4 className="mt-4">{failMessage(current?.progress, finish)}</h4>
|
|
{typeof finish === "object" && <div>
|
|
<div>The correct answer was:</div>
|
|
{finish?.answer}
|
|
</div>}
|
|
<div className="mt-3">
|
|
<button className="btn btn-success mr-2" onClick={handleRestart}>Try Again</button>
|
|
<button className="btn btn-danger ml-2" onClick={handleQuit}>Quit</button>
|
|
</div>
|
|
<div onClick={handleQuit} className="my-3">
|
|
<Link to={studyLink}>
|
|
<button className="btn btn-outline-secondary"><span role="img" aria-label="">📚</span> Study more</button>
|
|
</Link>
|
|
</div>
|
|
</div>}
|
|
</div>
|
|
</Reward>
|
|
</div>
|
|
{gameRunning && <div style={{
|
|
position: "absolute",
|
|
backgroundColor: "rgba(255, 255, 255, 0.3)",
|
|
backdropFilter: "blur(10px)",
|
|
top: "0px",
|
|
left: "0px",
|
|
width: "100%",
|
|
height: "100%",
|
|
zIndex: 6,
|
|
}}></div>}
|
|
</>;
|
|
}
|
|
|
|
function failMessage(progress: Progress | undefined, finish: "time out" | { msg: "fail", answer: JSX.Element }): string {
|
|
const pDone = progress ? getPercentageDone(progress) : 0;
|
|
const { message, face } = pDone < 20
|
|
? { message: "No, sorry", face: "😑" }
|
|
: pDone < 30
|
|
? { message: "Oops, that's wrong", face: "😟" }
|
|
: pDone < 55
|
|
? { message: "Fail", face: "😕" }
|
|
: pDone < 78
|
|
? { message: "You almost got it!", face: "😩" }
|
|
: { message: "Nooo! So close!", face: "😭" };
|
|
return typeof finish === "object"
|
|
? `${message} ${face}`
|
|
: `⏳ Time's Up ${face}`;
|
|
}
|
|
|
|
export default GameCore; |