update ga
This commit is contained in:
parent
0def4b3136
commit
5d21aa142a
|
@ -33,7 +33,7 @@
|
||||||
"react-bootstrap": "1.6.4",
|
"react-bootstrap": "1.6.4",
|
||||||
"react-countdown-circle-timer": "3.0.9",
|
"react-countdown-circle-timer": "3.0.9",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-ga": "3.3.0",
|
"react-ga4": "^2.1.0",
|
||||||
"react-media": "1",
|
"react-media": "1",
|
||||||
"react-player": "2.10.1",
|
"react-player": "2.10.1",
|
||||||
"react-rewards": "1.1.2",
|
"react-rewards": "1.1.2",
|
||||||
|
|
52
src/App.tsx
52
src/App.tsx
|
@ -19,19 +19,19 @@ import LandingPage from "./pages/LandingPage";
|
||||||
import AccountPage from "./pages/AccountPage";
|
import AccountPage from "./pages/AccountPage";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { isProd } from "./lib/isProd";
|
import { isProd } from "./lib/isProd";
|
||||||
import ReactGA from "react-ga";
|
import ReactGA from "react-ga4";
|
||||||
import { useUser } from "./user-context";
|
import { useUser } from "./user-context";
|
||||||
import PrivacyPolicy from "./pages/PrivacyPolicy";
|
import PrivacyPolicy from "./pages/PrivacyPolicy";
|
||||||
import SearchPage from "./pages/SearchPage";
|
import SearchPage from "./pages/SearchPage";
|
||||||
|
|
||||||
const chapters = content.reduce((chapters, item) => (
|
const chapters = content.reduce(
|
||||||
item.content
|
(chapters, item) =>
|
||||||
? [...chapters, item]
|
item.content ? [...chapters, item] : [...chapters, ...item.chapters],
|
||||||
: [...chapters, ...item.chapters]
|
[]
|
||||||
), []);
|
);
|
||||||
|
|
||||||
if (isProd) {
|
if (isProd) {
|
||||||
ReactGA.initialize("UA-196576671-2");
|
ReactGA.initialize("387148608");
|
||||||
ReactGA.set({ anonymizeIp: true });
|
ReactGA.set({ anonymizeIp: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,15 +41,18 @@ function App() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
function logAnalytics() {
|
function logAnalytics() {
|
||||||
if (isProd && !(user?.admin)) {
|
if (isProd && !user?.admin) {
|
||||||
ReactGA.pageview(window.location.pathname);
|
ReactGA.send({
|
||||||
};
|
hitType: "pageview",
|
||||||
|
page: window.location.pathname,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
logAnalytics();
|
logAnalytics();
|
||||||
if (window.location.pathname === "/") {
|
if (window.location.pathname === "/") {
|
||||||
if (localStorage.getItem("visitedOnce")) {
|
if (localStorage.getItem("visitedOnce")) {
|
||||||
navigate("/table-of-contents", { replace: true })
|
navigate("/table-of-contents", { replace: true });
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem("visitedOnce", "true");
|
localStorage.setItem("visitedOnce", "true");
|
||||||
}
|
}
|
||||||
|
@ -65,7 +68,10 @@ function App() {
|
||||||
<>
|
<>
|
||||||
<Header setNavOpen={setNavOpen} />
|
<Header setNavOpen={setNavOpen} />
|
||||||
<div className="container-fluid">
|
<div className="container-fluid">
|
||||||
<div className="main-row row" style={{ minHeight: "calc(100vh - 62px)" }}>
|
<div
|
||||||
|
className="main-row row"
|
||||||
|
style={{ minHeight: "calc(100vh - 62px)" }}
|
||||||
|
>
|
||||||
<Sidebar
|
<Sidebar
|
||||||
content={content}
|
content={content}
|
||||||
navOpen={navOpen}
|
navOpen={navOpen}
|
||||||
|
@ -73,18 +79,9 @@ function App() {
|
||||||
pathname={window.location.pathname}
|
pathname={window.location.pathname}
|
||||||
/>
|
/>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route path="/" element={<LandingPage />} />
|
||||||
path="/"
|
<Route path="/search" element={<SearchPage />} />
|
||||||
element={<LandingPage />}
|
<Route path="/privacy" element={<PrivacyPolicy />} />
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/search"
|
|
||||||
element={<SearchPage />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/privacy"
|
|
||||||
element={<PrivacyPolicy />}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="/table-of-contents"
|
path="/table-of-contents"
|
||||||
element={<TableOfContentsPage />}
|
element={<TableOfContentsPage />}
|
||||||
|
@ -96,15 +93,12 @@ function App() {
|
||||||
element={<Chapter>{chapter}</Chapter>}
|
element={<Chapter>{chapter}</Chapter>}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<Route
|
<Route path="/account" element={<AccountPage />} />
|
||||||
path="/account"
|
|
||||||
element={<AccountPage />}
|
|
||||||
/>
|
|
||||||
<Route path="*" element={<Page404 />} />
|
<Route path="*" element={<Page404 />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,64 +1,84 @@
|
||||||
import {
|
import { Types as T, EPDisplay, EPPicker } from "@lingdocs/ps-react";
|
||||||
Types as T,
|
|
||||||
EPDisplay,
|
|
||||||
EPPicker,
|
|
||||||
} from "@lingdocs/ps-react";
|
|
||||||
import entryFeeder from "../../lib/entry-feeder";
|
import entryFeeder from "../../lib/entry-feeder";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import ReactGA from "react-ga";
|
import ReactGA from "react-ga4";
|
||||||
import { isProd } from "../../lib/isProd";
|
import { isProd } from "../../lib/isProd";
|
||||||
import { useUser } from "../../user-context";
|
import { useUser } from "../../user-context";
|
||||||
|
|
||||||
export function EditIcon() {
|
export function EditIcon() {
|
||||||
return <i className="fas fa-edit" />;
|
return <i className="fas fa-edit" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditableEPEx({ children, opts, hideOmitSubject, noEdit }: { children: T.EPSelectionState, opts: T.TextOptions, hideOmitSubject?: boolean, noEdit?: boolean }) {
|
function EditableEPEx({
|
||||||
const [editing, setEditing] = useState<boolean>(false);
|
children,
|
||||||
const [eps, setEps] = useState<T.EPSelectionState>(children);
|
opts,
|
||||||
const { user } = useUser();
|
hideOmitSubject,
|
||||||
function handleReset() {
|
noEdit,
|
||||||
setEditing(false);
|
}: {
|
||||||
setEps(children);
|
children: T.EPSelectionState;
|
||||||
|
opts: T.TextOptions;
|
||||||
|
hideOmitSubject?: boolean;
|
||||||
|
noEdit?: boolean;
|
||||||
|
}) {
|
||||||
|
const [editing, setEditing] = useState<boolean>(false);
|
||||||
|
const [eps, setEps] = useState<T.EPSelectionState>(children);
|
||||||
|
const { user } = useUser();
|
||||||
|
function handleReset() {
|
||||||
|
setEditing(false);
|
||||||
|
setEps(children);
|
||||||
|
}
|
||||||
|
function logEdit() {
|
||||||
|
if (isProd && !user?.admin) {
|
||||||
|
ReactGA.event({
|
||||||
|
category: "Example",
|
||||||
|
action: `edit EPex - ${window.location.pathname}`,
|
||||||
|
label: "edit EPex",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
function logEdit() {
|
}
|
||||||
if (isProd && !(user?.admin)) {
|
return (
|
||||||
ReactGA.event({
|
<div className="mt-2 mb-4">
|
||||||
category: "Example",
|
{!noEdit && (
|
||||||
action: `edit EPex - ${window.location.pathname}`,
|
<div
|
||||||
label: "edit EPex"
|
className="text-left clickable"
|
||||||
});
|
style={{ marginBottom: editing ? "0.5rem" : "-0.5rem" }}
|
||||||
}
|
onClick={
|
||||||
}
|
editing
|
||||||
return <div className="mt-2 mb-4">
|
? handleReset
|
||||||
{!noEdit && <div
|
: () => {
|
||||||
className="text-left clickable"
|
setEditing(true);
|
||||||
style={{ marginBottom: editing ? "0.5rem" : "-0.5rem" }}
|
logEdit();
|
||||||
onClick={editing ? handleReset : () => {
|
}
|
||||||
setEditing(true);
|
}
|
||||||
logEdit();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{!editing ? <EditIcon /> : <i className="fas fa-undo" />}
|
{!editing ? <EditIcon /> : <i className="fas fa-undo" />}
|
||||||
</div>}
|
</div>
|
||||||
{editing
|
)}
|
||||||
&& <EPPicker
|
{editing && (
|
||||||
opts={opts}
|
<EPPicker
|
||||||
entryFeeder={entryFeeder}
|
opts={opts}
|
||||||
eps={eps}
|
entryFeeder={entryFeeder}
|
||||||
onChange={setEps}
|
eps={eps}
|
||||||
/>}
|
onChange={setEps}
|
||||||
<EPDisplay
|
|
||||||
opts={opts}
|
|
||||||
eps={eps}
|
|
||||||
setOmitSubject={hideOmitSubject ? false : (value) => setEps(o => ({
|
|
||||||
...o,
|
|
||||||
omitSubject: value === "true",
|
|
||||||
}))}
|
|
||||||
justify="left"
|
|
||||||
onlyOne
|
|
||||||
/>
|
/>
|
||||||
</div>;
|
)}
|
||||||
|
<EPDisplay
|
||||||
|
opts={opts}
|
||||||
|
eps={eps}
|
||||||
|
setOmitSubject={
|
||||||
|
hideOmitSubject
|
||||||
|
? false
|
||||||
|
: (value) =>
|
||||||
|
setEps((o) => ({
|
||||||
|
...o,
|
||||||
|
omitSubject: value === "true",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
justify="left"
|
||||||
|
onlyOne
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditableEPEx;
|
export default EditableEPEx;
|
||||||
|
|
|
@ -1,84 +1,103 @@
|
||||||
import {
|
import {
|
||||||
Types as T,
|
Types as T,
|
||||||
VPDisplay,
|
VPDisplay,
|
||||||
VPPicker,
|
VPPicker,
|
||||||
vpsReducer,
|
vpsReducer,
|
||||||
} from "@lingdocs/ps-react";
|
} from "@lingdocs/ps-react";
|
||||||
import entryFeeder from "../../lib/entry-feeder";
|
import entryFeeder from "../../lib/entry-feeder";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import ReactGA from "react-ga";
|
import ReactGA from "react-ga4";
|
||||||
import { isProd } from "../../lib/isProd";
|
import { isProd } from "../../lib/isProd";
|
||||||
import { useUser } from "../../user-context";
|
import { useUser } from "../../user-context";
|
||||||
|
|
||||||
export function EditIcon() {
|
export function EditIcon() {
|
||||||
return <i className="fas fa-edit" />;
|
return <i className="fas fa-edit" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Ability to show all variations
|
// TODO: Ability to show all variations
|
||||||
|
|
||||||
function EditableVPEx({ children, opts, formChoice, noEdit, length, mode, sub, allVariations }: {
|
function EditableVPEx({
|
||||||
children: T.VPSelectionState,
|
children,
|
||||||
opts: T.TextOptions,
|
opts,
|
||||||
formChoice?: boolean,
|
formChoice,
|
||||||
noEdit?: boolean,
|
noEdit,
|
||||||
length?: "long" | "short",
|
length,
|
||||||
mode?: "text" | "blocks",
|
mode,
|
||||||
sub?: string | JSX.Element,
|
sub,
|
||||||
allVariations?: boolean,
|
allVariations,
|
||||||
|
}: {
|
||||||
|
children: T.VPSelectionState;
|
||||||
|
opts: T.TextOptions;
|
||||||
|
formChoice?: boolean;
|
||||||
|
noEdit?: boolean;
|
||||||
|
length?: "long" | "short";
|
||||||
|
mode?: "text" | "blocks";
|
||||||
|
sub?: string | JSX.Element;
|
||||||
|
allVariations?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [editing, setEditing] = useState<boolean>(false);
|
const [editing, setEditing] = useState<boolean>(false);
|
||||||
const [selectedLength, setSelectedLength] = useState<"long" | "short">(length || "short");
|
const [selectedLength, setSelectedLength] = useState<"long" | "short">(
|
||||||
const [vps, setVps] = useState<T.VPSelectionState>({ ...children });
|
length || "short"
|
||||||
const { user } = useUser();
|
);
|
||||||
function logEdit() {
|
const [vps, setVps] = useState<T.VPSelectionState>({ ...children });
|
||||||
if (isProd && !(user?.admin)) {
|
const { user } = useUser();
|
||||||
ReactGA.event({
|
function logEdit() {
|
||||||
category: "Example",
|
if (isProd && !user?.admin) {
|
||||||
action: `edit VPex - ${window.location.pathname}`,
|
ReactGA.event({
|
||||||
label: "edit VPex"
|
category: "Example",
|
||||||
});
|
action: `edit VPex - ${window.location.pathname}`,
|
||||||
}
|
label: "edit VPex",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
function handleReset() {
|
}
|
||||||
// TODO: this is crazy, how does children get changed after calling setVps ???
|
function handleReset() {
|
||||||
setVps(children);
|
// TODO: this is crazy, how does children get changed after calling setVps ???
|
||||||
setEditing(false);
|
setVps(children);
|
||||||
}
|
setEditing(false);
|
||||||
function handleSetForm(form: T.FormVersion) {
|
}
|
||||||
setVps(vpsReducer(vps, { type: "set form", payload: form }));
|
function handleSetForm(form: T.FormVersion) {
|
||||||
}
|
setVps(vpsReducer(vps, { type: "set form", payload: form }));
|
||||||
return <div className="mt-2 mb-4">
|
}
|
||||||
{!noEdit && <div
|
return (
|
||||||
className="text-left clickable mb-2"
|
<div className="mt-2 mb-4">
|
||||||
style={{ marginBottom: editing ? "0.5rem" : "-0.5rem" }}
|
{!noEdit && (
|
||||||
onClick={editing ? handleReset : () => {
|
<div
|
||||||
setEditing(true);
|
className="text-left clickable mb-2"
|
||||||
logEdit();
|
style={{ marginBottom: editing ? "0.5rem" : "-0.5rem" }}
|
||||||
}}
|
onClick={
|
||||||
|
editing
|
||||||
|
? handleReset
|
||||||
|
: () => {
|
||||||
|
setEditing(true);
|
||||||
|
logEdit();
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{!editing ? <EditIcon /> : <i className="fas fa-undo" />}
|
{!editing ? <EditIcon /> : <i className="fas fa-undo" />}
|
||||||
</div>}
|
</div>
|
||||||
{editing
|
)}
|
||||||
&& <VPPicker
|
{editing && (
|
||||||
opts={opts}
|
<VPPicker
|
||||||
entryFeeder={entryFeeder}
|
opts={opts}
|
||||||
vps={vps}
|
entryFeeder={entryFeeder}
|
||||||
onChange={setVps}
|
vps={vps}
|
||||||
/>
|
onChange={setVps}
|
||||||
}
|
|
||||||
<VPDisplay
|
|
||||||
opts={opts}
|
|
||||||
VPS={vps}
|
|
||||||
justify="left"
|
|
||||||
onlyOne={allVariations ? false : "concat"}
|
|
||||||
setForm={handleSetForm}
|
|
||||||
onLengthChange={setSelectedLength}
|
|
||||||
length={allVariations ? undefined : selectedLength}
|
|
||||||
mode={mode}
|
|
||||||
inlineFormChoice
|
|
||||||
/>
|
/>
|
||||||
{sub && <div className="text-muted small">{sub}</div>}
|
)}
|
||||||
</div>;
|
<VPDisplay
|
||||||
|
opts={opts}
|
||||||
|
VPS={vps}
|
||||||
|
justify="left"
|
||||||
|
onlyOne={allVariations ? false : "concat"}
|
||||||
|
setForm={handleSetForm}
|
||||||
|
onLengthChange={setSelectedLength}
|
||||||
|
length={allVariations ? undefined : selectedLength}
|
||||||
|
mode={mode}
|
||||||
|
inlineFormChoice
|
||||||
|
/>
|
||||||
|
{sub && <div className="text-muted small">{sub}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditableVPEx;
|
export default EditableVPEx;
|
||||||
|
|
|
@ -1,354 +1,452 @@
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import { CountdownCircleTimer } from "react-countdown-circle-timer";
|
import { CountdownCircleTimer } from "react-countdown-circle-timer";
|
||||||
import Reward, { RewardElement } from 'react-rewards';
|
import Reward, { RewardElement } from "react-rewards";
|
||||||
import Link from "../components/Link";
|
import Link from "../components/Link";
|
||||||
import { useUser } from "../user-context";
|
import { useUser } from "../user-context";
|
||||||
import "./timer.css";
|
import "./timer.css";
|
||||||
import {
|
import { getPercentageDone } from "../lib/game-utils";
|
||||||
getPercentageDone,
|
import { saveResult, postSavedResults } from "../lib/game-results";
|
||||||
} from "../lib/game-utils";
|
import { AT, getTimestamp } from "@lingdocs/lingdocs-main";
|
||||||
import {
|
import { randFromArray, Types } from "@lingdocs/ps-react";
|
||||||
saveResult,
|
import ReactGA from "react-ga4";
|
||||||
postSavedResults,
|
|
||||||
} from "../lib/game-results";
|
|
||||||
import {
|
|
||||||
AT,
|
|
||||||
getTimestamp,
|
|
||||||
} from "@lingdocs/lingdocs-main";
|
|
||||||
import {
|
|
||||||
randFromArray,
|
|
||||||
Types,
|
|
||||||
} from "@lingdocs/ps-react";
|
|
||||||
import ReactGA from "react-ga";
|
|
||||||
import { isProd } from "../lib/isProd";
|
import { isProd } from "../lib/isProd";
|
||||||
import autoAnimate from "@formkit/auto-animate";
|
import autoAnimate from "@formkit/auto-animate";
|
||||||
const errorVibration = 200;
|
const errorVibration = 200;
|
||||||
const strikesToFail = 3;
|
const strikesToFail = 3;
|
||||||
|
|
||||||
type GameState<Question> = ({
|
type GameState<Question> = (
|
||||||
mode: "practice",
|
| {
|
||||||
showAnswer: boolean,
|
mode: "practice";
|
||||||
} | {
|
showAnswer: boolean;
|
||||||
mode: "intro" | "test" | "fail" | "timeout" | "complete",
|
}
|
||||||
showAnswer: false,
|
| {
|
||||||
}) & {
|
mode: "intro" | "test" | "fail" | "timeout" | "complete";
|
||||||
numberComplete: number,
|
showAnswer: false;
|
||||||
current: Question,
|
}
|
||||||
timerKey: number,
|
) & {
|
||||||
strikes: number,
|
numberComplete: number;
|
||||||
justStruck: boolean,
|
current: Question;
|
||||||
}
|
timerKey: number;
|
||||||
|
strikes: number;
|
||||||
|
justStruck: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type GameReducerAction = {
|
type GameReducerAction =
|
||||||
type: "handle question response",
|
| {
|
||||||
payload: { correct: boolean },
|
type: "handle question response";
|
||||||
} | {
|
payload: { correct: boolean };
|
||||||
type: "start",
|
}
|
||||||
payload: "practice" | "test",
|
| {
|
||||||
} | {
|
type: "start";
|
||||||
type: "quit",
|
payload: "practice" | "test";
|
||||||
} | {
|
}
|
||||||
type: "timeout",
|
| {
|
||||||
} | {
|
type: "quit";
|
||||||
type: "toggle show answer",
|
}
|
||||||
} | {
|
| {
|
||||||
type: "skip",
|
type: "timeout";
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
function GameCore<Question>({ inChapter, getQuestion, amount, Display, DisplayCorrectAnswer, timeLimit, Instructions, studyLink, id }: {
|
type: "toggle show answer";
|
||||||
inChapter: boolean,
|
}
|
||||||
id: string,
|
| {
|
||||||
studyLink: string,
|
type: "skip";
|
||||||
Instructions: (props: { opts?: Types.TextOptions }) => JSX.Element,
|
|
||||||
getQuestion: () => Question,
|
|
||||||
DisplayCorrectAnswer: (props: { question: Question }) => JSX.Element,
|
|
||||||
Display: (props: QuestionDisplayProps<Question>) => JSX.Element,
|
|
||||||
timeLimit: number,
|
|
||||||
amount: number,
|
|
||||||
}) {
|
|
||||||
const initialState: GameState<Question> = {
|
|
||||||
mode: "intro",
|
|
||||||
numberComplete: 0,
|
|
||||||
current: getQuestion(),
|
|
||||||
timerKey: 0,
|
|
||||||
strikes: 0,
|
|
||||||
justStruck: false,
|
|
||||||
showAnswer: false,
|
|
||||||
};
|
};
|
||||||
// TODO: report pass with id to user info
|
|
||||||
const rewardRef = useRef<RewardElement | null>(null);
|
|
||||||
const parent = useRef<HTMLDivElement | null>(null);
|
|
||||||
const { user, pullUser, setUser } = useUser();
|
|
||||||
const [state, setStateDangerous] = useState<GameState<Question>>(initialState);
|
|
||||||
useEffect(() => {
|
|
||||||
parent.current && autoAnimate(parent.current)
|
|
||||||
}, [parent]);
|
|
||||||
|
|
||||||
const gameReducer = (gs: GameState<Question>, action: GameReducerAction): GameState<Question> => {
|
function GameCore<Question>({
|
||||||
if (action.type === "handle question response") {
|
inChapter,
|
||||||
if (gs.mode === "test") {
|
getQuestion,
|
||||||
if (action.payload.correct) {
|
amount,
|
||||||
const numberComplete = gs.numberComplete + 1;
|
Display,
|
||||||
if (numberComplete === amount) {
|
DisplayCorrectAnswer,
|
||||||
logGameEvent("passed");
|
timeLimit,
|
||||||
rewardRef.current?.rewardMe();
|
Instructions,
|
||||||
handleResult(true);
|
studyLink,
|
||||||
return {
|
id,
|
||||||
...gs,
|
}: {
|
||||||
numberComplete,
|
inChapter: boolean;
|
||||||
justStruck: false,
|
id: string;
|
||||||
mode: "complete",
|
studyLink: string;
|
||||||
|
Instructions: (props: { opts?: Types.TextOptions }) => JSX.Element;
|
||||||
|
getQuestion: () => Question;
|
||||||
|
DisplayCorrectAnswer: (props: { question: Question }) => JSX.Element;
|
||||||
|
Display: (props: QuestionDisplayProps<Question>) => JSX.Element;
|
||||||
|
timeLimit: number;
|
||||||
|
amount: number;
|
||||||
|
}) {
|
||||||
|
const initialState: GameState<Question> = {
|
||||||
|
mode: "intro",
|
||||||
|
numberComplete: 0,
|
||||||
|
current: getQuestion(),
|
||||||
|
timerKey: 0,
|
||||||
|
strikes: 0,
|
||||||
|
justStruck: false,
|
||||||
|
showAnswer: false,
|
||||||
|
};
|
||||||
|
// TODO: report pass with id to user info
|
||||||
|
const rewardRef = useRef<RewardElement | null>(null);
|
||||||
|
const parent = useRef<HTMLDivElement | null>(null);
|
||||||
|
const { user, pullUser, setUser } = useUser();
|
||||||
|
const [state, setStateDangerous] =
|
||||||
|
useState<GameState<Question>>(initialState);
|
||||||
|
useEffect(() => {
|
||||||
|
parent.current && autoAnimate(parent.current);
|
||||||
|
}, [parent]);
|
||||||
|
|
||||||
}
|
const gameReducer = (
|
||||||
} else {
|
gs: GameState<Question>,
|
||||||
return {
|
action: GameReducerAction
|
||||||
...gs,
|
): GameState<Question> => {
|
||||||
numberComplete,
|
if (action.type === "handle question response") {
|
||||||
current: getQuestion(),
|
if (gs.mode === "test") {
|
||||||
justStruck: false,
|
if (action.payload.correct) {
|
||||||
};
|
const numberComplete = gs.numberComplete + 1;
|
||||||
}
|
if (numberComplete === amount) {
|
||||||
} else {
|
logGameEvent("passed");
|
||||||
punish();
|
rewardRef.current?.rewardMe();
|
||||||
const strikes = gs.strikes + 1;
|
handleResult(true);
|
||||||
if (strikes === strikesToFail) {
|
|
||||||
logGameEvent("fail");
|
|
||||||
handleResult(false);
|
|
||||||
return {
|
|
||||||
...gs,
|
|
||||||
strikes,
|
|
||||||
mode: "fail",
|
|
||||||
justStruck: false,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
...gs,
|
|
||||||
strikes,
|
|
||||||
justStruck: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else /* (gs.mode === "practice") */ {
|
|
||||||
if (action.payload.correct) {
|
|
||||||
const numberComplete = gs.numberComplete + (!gs.showAnswer ? 1 : 0);
|
|
||||||
return {
|
|
||||||
...gs,
|
|
||||||
numberComplete,
|
|
||||||
current: getQuestion(),
|
|
||||||
justStruck: false,
|
|
||||||
showAnswer: false,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
punish();
|
|
||||||
const strikes = gs.strikes + 1;
|
|
||||||
return {
|
|
||||||
...gs,
|
|
||||||
strikes,
|
|
||||||
justStruck: true,
|
|
||||||
showAnswer: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (action.type === "start") {
|
|
||||||
logGameEvent(`started ${action.payload}`);
|
|
||||||
return {
|
return {
|
||||||
...initialState,
|
...gs,
|
||||||
mode: action.payload,
|
numberComplete,
|
||||||
current: getQuestion(),
|
justStruck: false,
|
||||||
timerKey: gs.timerKey + 1,
|
mode: "complete",
|
||||||
}
|
};
|
||||||
}
|
} else {
|
||||||
if (action.type === "quit") {
|
|
||||||
return {
|
return {
|
||||||
...initialState,
|
...gs,
|
||||||
timerKey: gs.timerKey + 1,
|
numberComplete,
|
||||||
}
|
current: getQuestion(),
|
||||||
}
|
justStruck: false,
|
||||||
if (action.type === "timeout") {
|
};
|
||||||
logGameEvent("timeout");
|
}
|
||||||
|
} else {
|
||||||
|
punish();
|
||||||
|
const strikes = gs.strikes + 1;
|
||||||
|
if (strikes === strikesToFail) {
|
||||||
|
logGameEvent("fail");
|
||||||
handleResult(false);
|
handleResult(false);
|
||||||
return {
|
return {
|
||||||
...gs,
|
...gs,
|
||||||
mode: "timeout",
|
strikes,
|
||||||
justStruck: false,
|
mode: "fail",
|
||||||
showAnswer: false,
|
justStruck: false,
|
||||||
};
|
};
|
||||||
}
|
} else {
|
||||||
if (action.type === "toggle show answer") {
|
|
||||||
if (gs.mode === "practice") {
|
|
||||||
return {
|
|
||||||
...gs,
|
|
||||||
justStruck: false,
|
|
||||||
showAnswer: !gs.showAnswer,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return gs;
|
|
||||||
}
|
|
||||||
if (action.type === "skip") {
|
|
||||||
if (gs.mode === "practice") {
|
|
||||||
return {
|
|
||||||
...gs,
|
|
||||||
current: getQuestion(),
|
|
||||||
justStruck: false,
|
|
||||||
showAnswer: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return gs;
|
|
||||||
}
|
|
||||||
throw new Error("unknown GameReducerAction");
|
|
||||||
}
|
|
||||||
|
|
||||||
function dispatch(action: GameReducerAction) {
|
|
||||||
setStateDangerous(gs => gameReducer(gs, action));
|
|
||||||
}
|
|
||||||
|
|
||||||
function logGameEvent(action: string) {
|
|
||||||
if (isProd && !(user?.admin)) {
|
|
||||||
ReactGA.event({
|
|
||||||
category: "Game",
|
|
||||||
action: `${action} - ${id}`,
|
|
||||||
label: id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function punish() {
|
|
||||||
if (navigator.vibrate) {
|
|
||||||
navigator.vibrate(errorVibration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function handleResult(done: boolean) {
|
|
||||||
const result: AT.TestResult = {
|
|
||||||
done,
|
|
||||||
time: getTimestamp(),
|
|
||||||
id,
|
|
||||||
};
|
|
||||||
// add the test to the user object
|
|
||||||
if (!user) return;
|
|
||||||
setUser((u) => {
|
|
||||||
// pure type safety with the prevUser
|
|
||||||
if (!u) return u;
|
|
||||||
return {
|
return {
|
||||||
...u,
|
...gs,
|
||||||
tests: [...u.tests, result],
|
strikes,
|
||||||
|
justStruck: true,
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
// save the test result in local storage
|
}
|
||||||
saveResult(result, user.userId);
|
} /* (gs.mode === "practice") */ else {
|
||||||
// try to post the result
|
if (action.payload.correct) {
|
||||||
postSavedResults(user.userId).then((r) => {
|
const numberComplete = gs.numberComplete + (!gs.showAnswer ? 1 : 0);
|
||||||
if (r === "sent") pullUser();
|
return {
|
||||||
}).catch(console.error);
|
...gs,
|
||||||
|
numberComplete,
|
||||||
|
current: getQuestion(),
|
||||||
|
justStruck: false,
|
||||||
|
showAnswer: false,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
punish();
|
||||||
|
const strikes = gs.strikes + 1;
|
||||||
|
return {
|
||||||
|
...gs,
|
||||||
|
strikes,
|
||||||
|
justStruck: true,
|
||||||
|
showAnswer: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function getProgressWidth(): string {
|
if (action.type === "start") {
|
||||||
const num = !state.current
|
logGameEvent(`started ${action.payload}`);
|
||||||
? 0
|
return {
|
||||||
: (state.mode === "complete")
|
...initialState,
|
||||||
? 100
|
mode: action.payload,
|
||||||
: getPercentageDone(state.numberComplete, amount);
|
current: getQuestion(),
|
||||||
return `${num}%`;
|
timerKey: gs.timerKey + 1,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const progressColor = state.mode === "complete"
|
if (action.type === "quit") {
|
||||||
? "success"
|
return {
|
||||||
: (state.mode === "fail" || state.mode === "timeout")
|
...initialState,
|
||||||
? "danger"
|
timerKey: gs.timerKey + 1,
|
||||||
: "primary";
|
};
|
||||||
const gameRunning = state.mode === "practice" || state.mode === "test";
|
|
||||||
function ActionButtons() {
|
|
||||||
return <div>
|
|
||||||
{!inChapter && <Link to={studyLink}>
|
|
||||||
<button className="btn btn-danger mt-4 mx-3">Study</button>
|
|
||||||
</Link>}
|
|
||||||
<button className="btn btn-warning mt-4 mx-3" onClick={() => dispatch({ type: "start", payload: "practice" })}>Practice</button>
|
|
||||||
<button className="btn btn-success mt-4 mx-3" onClick={() => dispatch({ type: "start", payload: "test" })}>Test</button>
|
|
||||||
</div>;
|
|
||||||
}
|
}
|
||||||
return <>
|
if (action.type === "timeout") {
|
||||||
<div className="text-center" style={{ minHeight: "200px", zIndex: 10, position: "relative" }}>
|
logGameEvent("timeout");
|
||||||
{(state.mode === "test" || state.mode === "intro") && <div className="progress" style={{ height: "5px" }}>
|
handleResult(false);
|
||||||
<div className={`progress-bar bg-${progressColor}`} role="progressbar" style={{ width: getProgressWidth() }} />
|
return {
|
||||||
</div>}
|
...gs,
|
||||||
<div className="d-flex flex-row justify-content-between mt-2">
|
mode: "timeout",
|
||||||
{state.mode === "test"
|
justStruck: false,
|
||||||
? <StrikesDisplay strikes={state.strikes} />
|
showAnswer: false,
|
||||||
: state.mode === "practice" ? <PracticeStatusDisplay
|
};
|
||||||
correct={state.numberComplete}
|
}
|
||||||
incorrect={state.strikes}
|
if (action.type === "toggle show answer") {
|
||||||
/>
|
if (gs.mode === "practice") {
|
||||||
: <div />}
|
return {
|
||||||
<div className="d-flex flex-row justify-content-right">
|
...gs,
|
||||||
{state.mode === "test" && <CountdownCircleTimer
|
justStruck: false,
|
||||||
key={state.timerKey}
|
showAnswer: !gs.showAnswer,
|
||||||
isPlaying={gameRunning}
|
};
|
||||||
size={30}
|
}
|
||||||
colors={["#555555", "#F7B801", "#A30000"]}
|
return gs;
|
||||||
colorsTime={[timeLimit, timeLimit*0.33, 0]}
|
}
|
||||||
strokeWidth={4}
|
if (action.type === "skip") {
|
||||||
strokeLinecap="square"
|
if (gs.mode === "practice") {
|
||||||
duration={timeLimit}
|
return {
|
||||||
onComplete={() => dispatch({ type: "timeout" })}
|
...gs,
|
||||||
/>}
|
current: getQuestion(),
|
||||||
{state.mode !== "intro" && <button onClick={() => dispatch({ type: "quit" })} className="btn btn-outline-secondary btn-sm ml-2">
|
justStruck: false,
|
||||||
Quit
|
showAnswer: false,
|
||||||
</button>}
|
};
|
||||||
</div>
|
}
|
||||||
</div>
|
return gs;
|
||||||
<div ref={parent}>
|
}
|
||||||
{state.justStruck && <div className="alert alert-warning my-2" role="alert" style={{ maxWidth: "300px", margin: "0 auto" }}>
|
throw new Error("unknown GameReducerAction");
|
||||||
{getStrikeMessage()}
|
};
|
||||||
</div>}
|
|
||||||
</div>
|
function dispatch(action: GameReducerAction) {
|
||||||
<Reward ref={rewardRef} config={{ lifetime: 130, spread: 90, elementCount: 150, zIndex: 999999999 }} type="confetti">
|
setStateDangerous((gs) => gameReducer(gs, action));
|
||||||
<div className="mb-2">
|
}
|
||||||
{state.mode === "intro" && <div>
|
|
||||||
<div className="pt-3">
|
function logGameEvent(action: string) {
|
||||||
{/* TODO: ADD IN TEXT DISPLAY OPTIONS HERE TOO - WHEN WE START USING THEM*/}
|
if (isProd && !user?.admin) {
|
||||||
<Instructions />
|
ReactGA.event({
|
||||||
</div>
|
category: "Game",
|
||||||
<ActionButtons />
|
action: `${action} - ${id}`,
|
||||||
</div>}
|
label: id,
|
||||||
{gameRunning && <Display
|
});
|
||||||
question={state.current}
|
}
|
||||||
callback={(correct) => dispatch({ type: "handle question response", payload: { correct }})}
|
}
|
||||||
/>}
|
function punish() {
|
||||||
{(state.mode === "practice" && (state.justStruck || state.showAnswer)) && <div className="my-3">
|
if (navigator.vibrate) {
|
||||||
<button className="btn btn-sm btn-secondary" onClick={() => dispatch({ type: "toggle show answer" })}>
|
navigator.vibrate(errorVibration);
|
||||||
{state.showAnswer ? "Hide" : "Show"} Answer
|
}
|
||||||
</button>
|
}
|
||||||
</div>}
|
function handleResult(done: boolean) {
|
||||||
{(state.showAnswer && state.mode === "practice") && <div className="my-2">
|
const result: AT.TestResult = {
|
||||||
<div className="my-1">
|
done,
|
||||||
<DisplayCorrectAnswer question={state.current} />
|
time: getTimestamp(),
|
||||||
</div>
|
id,
|
||||||
<button className="btn btn-sm btn-primary my-2" onClick={() => dispatch({ type: "skip" })}>
|
};
|
||||||
Next Question
|
// add the test to the user object
|
||||||
</button>
|
if (!user) return;
|
||||||
</div>}
|
setUser((u) => {
|
||||||
{state.mode === "complete" && <div>
|
// pure type safety with the prevUser
|
||||||
<h4 className="mt-4">
|
if (!u) return u;
|
||||||
<span role="img" aria-label="celebration">🎉</span> Finished!
|
return {
|
||||||
</h4>
|
...u,
|
||||||
<button className="btn btn-secondary mt-4" onClick={() => dispatch({ type: "start", payload: "test" })}>Try Again</button>
|
tests: [...u.tests, result],
|
||||||
</div>}
|
};
|
||||||
{(state.mode === "timeout" || state.mode === "fail") && <div className="mb-4">
|
});
|
||||||
<h4 className="mt-4">{failMessage({
|
// save the test result in local storage
|
||||||
numberComplete: state.numberComplete,
|
saveResult(result, user.userId);
|
||||||
amount,
|
// try to post the result
|
||||||
type: state.mode,
|
postSavedResults(user.userId)
|
||||||
})}</h4>
|
.then((r) => {
|
||||||
<div>The correct answer was:</div>
|
if (r === "sent") pullUser();
|
||||||
<div className="my-2">
|
})
|
||||||
<DisplayCorrectAnswer question={state.current} />
|
.catch(console.error);
|
||||||
</div>
|
}
|
||||||
<div className="my-3">
|
function getProgressWidth(): string {
|
||||||
<ActionButtons />
|
const num = !state.current
|
||||||
</div>
|
? 0
|
||||||
</div>}
|
: state.mode === "complete"
|
||||||
</div>
|
? 100
|
||||||
</Reward>
|
: getPercentageDone(state.numberComplete, amount);
|
||||||
|
return `${num}%`;
|
||||||
|
}
|
||||||
|
const progressColor =
|
||||||
|
state.mode === "complete"
|
||||||
|
? "success"
|
||||||
|
: state.mode === "fail" || state.mode === "timeout"
|
||||||
|
? "danger"
|
||||||
|
: "primary";
|
||||||
|
const gameRunning = state.mode === "practice" || state.mode === "test";
|
||||||
|
function ActionButtons() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{!inChapter && (
|
||||||
|
<Link to={studyLink}>
|
||||||
|
<button className="btn btn-danger mt-4 mx-3">Study</button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="btn btn-warning mt-4 mx-3"
|
||||||
|
onClick={() => dispatch({ type: "start", payload: "practice" })}
|
||||||
|
>
|
||||||
|
Practice
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-success mt-4 mx-3"
|
||||||
|
onClick={() => dispatch({ type: "start", payload: "test" })}
|
||||||
|
>
|
||||||
|
Test
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="text-center"
|
||||||
|
style={{ minHeight: "200px", zIndex: 10, position: "relative" }}
|
||||||
|
>
|
||||||
|
{(state.mode === "test" || state.mode === "intro") && (
|
||||||
|
<div className="progress" style={{ height: "5px" }}>
|
||||||
|
<div
|
||||||
|
className={`progress-bar bg-${progressColor}`}
|
||||||
|
role="progressbar"
|
||||||
|
style={{ width: getProgressWidth() }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="d-flex flex-row justify-content-between mt-2">
|
||||||
|
{state.mode === "test" ? (
|
||||||
|
<StrikesDisplay strikes={state.strikes} />
|
||||||
|
) : state.mode === "practice" ? (
|
||||||
|
<PracticeStatusDisplay
|
||||||
|
correct={state.numberComplete}
|
||||||
|
incorrect={state.strikes}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
<div className="d-flex flex-row justify-content-right">
|
||||||
|
{state.mode === "test" && (
|
||||||
|
<CountdownCircleTimer
|
||||||
|
key={state.timerKey}
|
||||||
|
isPlaying={gameRunning}
|
||||||
|
size={30}
|
||||||
|
colors={["#555555", "#F7B801", "#A30000"]}
|
||||||
|
colorsTime={[timeLimit, timeLimit * 0.33, 0]}
|
||||||
|
strokeWidth={4}
|
||||||
|
strokeLinecap="square"
|
||||||
|
duration={timeLimit}
|
||||||
|
onComplete={() => dispatch({ type: "timeout" })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{state.mode !== "intro" && (
|
||||||
|
<button
|
||||||
|
onClick={() => dispatch({ type: "quit" })}
|
||||||
|
className="btn btn-outline-secondary btn-sm ml-2"
|
||||||
|
>
|
||||||
|
Quit
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{gameRunning && <div style={{
|
<div ref={parent}>
|
||||||
|
{state.justStruck && (
|
||||||
|
<div
|
||||||
|
className="alert alert-warning my-2"
|
||||||
|
role="alert"
|
||||||
|
style={{ maxWidth: "300px", margin: "0 auto" }}
|
||||||
|
>
|
||||||
|
{getStrikeMessage()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Reward
|
||||||
|
ref={rewardRef}
|
||||||
|
config={{
|
||||||
|
lifetime: 130,
|
||||||
|
spread: 90,
|
||||||
|
elementCount: 150,
|
||||||
|
zIndex: 999999999,
|
||||||
|
}}
|
||||||
|
type="confetti"
|
||||||
|
>
|
||||||
|
<div className="mb-2">
|
||||||
|
{state.mode === "intro" && (
|
||||||
|
<div>
|
||||||
|
<div className="pt-3">
|
||||||
|
{/* TODO: ADD IN TEXT DISPLAY OPTIONS HERE TOO - WHEN WE START USING THEM*/}
|
||||||
|
<Instructions />
|
||||||
|
</div>
|
||||||
|
<ActionButtons />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{gameRunning && (
|
||||||
|
<Display
|
||||||
|
question={state.current}
|
||||||
|
callback={(correct) =>
|
||||||
|
dispatch({
|
||||||
|
type: "handle question response",
|
||||||
|
payload: { correct },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{state.mode === "practice" &&
|
||||||
|
(state.justStruck || state.showAnswer) && (
|
||||||
|
<div className="my-3">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-secondary"
|
||||||
|
onClick={() => dispatch({ type: "toggle show answer" })}
|
||||||
|
>
|
||||||
|
{state.showAnswer ? "Hide" : "Show"} Answer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{state.showAnswer && state.mode === "practice" && (
|
||||||
|
<div className="my-2">
|
||||||
|
<div className="my-1">
|
||||||
|
<DisplayCorrectAnswer question={state.current} />
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-primary my-2"
|
||||||
|
onClick={() => dispatch({ type: "skip" })}
|
||||||
|
>
|
||||||
|
Next Question
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{state.mode === "complete" && (
|
||||||
|
<div>
|
||||||
|
<h4 className="mt-4">
|
||||||
|
<span role="img" aria-label="celebration">
|
||||||
|
🎉
|
||||||
|
</span>{" "}
|
||||||
|
Finished!
|
||||||
|
</h4>
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary mt-4"
|
||||||
|
onClick={() => dispatch({ type: "start", payload: "test" })}
|
||||||
|
>
|
||||||
|
Try Again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(state.mode === "timeout" || state.mode === "fail") && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<h4 className="mt-4">
|
||||||
|
{failMessage({
|
||||||
|
numberComplete: state.numberComplete,
|
||||||
|
amount,
|
||||||
|
type: state.mode,
|
||||||
|
})}
|
||||||
|
</h4>
|
||||||
|
<div>The correct answer was:</div>
|
||||||
|
<div className="my-2">
|
||||||
|
<DisplayCorrectAnswer question={state.current} />
|
||||||
|
</div>
|
||||||
|
<div className="my-3">
|
||||||
|
<ActionButtons />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Reward>
|
||||||
|
</div>
|
||||||
|
{gameRunning && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.3)",
|
backgroundColor: "rgba(255, 255, 255, 0.3)",
|
||||||
backdropFilter: "blur(10px)",
|
backdropFilter: "blur(10px)",
|
||||||
|
@ -357,51 +455,75 @@ function GameCore<Question>({ inChapter, getQuestion, amount, Display, DisplayCo
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
zIndex: 6,
|
zIndex: 6,
|
||||||
}}></div>}
|
}}
|
||||||
</>;
|
></div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PracticeStatusDisplay({ correct, incorrect }: { correct: number, incorrect: number }) {
|
function PracticeStatusDisplay({
|
||||||
return <div className="d-flex flex-row justify-content-between align-items-center small">
|
correct,
|
||||||
<div className="mr-3">✅ <samp>Correct: {correct}</samp></div>
|
incorrect,
|
||||||
<div>❌ <samp>Incorrect: {incorrect}</samp></div>
|
}: {
|
||||||
|
correct: number;
|
||||||
|
incorrect: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="d-flex flex-row justify-content-between align-items-center small">
|
||||||
|
<div className="mr-3">
|
||||||
|
✅ <samp>Correct: {correct}</samp>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
❌ <samp>Incorrect: {incorrect}</samp>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StrikesDisplay({ strikes }: { strikes: number }) {
|
function StrikesDisplay({ strikes }: { strikes: number }) {
|
||||||
return <div>
|
return (
|
||||||
{[...Array(strikes)].map(_ => <span key={Math.random()} className="mr-2">❌</span>)}
|
<div>
|
||||||
</div>;
|
{[...Array(strikes)].map((_) => (
|
||||||
|
<span key={Math.random()} className="mr-2">
|
||||||
|
❌
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStrikeMessage() {
|
function getStrikeMessage() {
|
||||||
return randFromArray([
|
return randFromArray([
|
||||||
"Not quite! Try again.",
|
"Not quite! Try again.",
|
||||||
"No sorry, try again",
|
"No sorry, try again",
|
||||||
"Umm, no, try again",
|
"Umm, no, try again",
|
||||||
"Try again",
|
"Try again",
|
||||||
"Oooooooo, sorry no...",
|
"Oooooooo, sorry no...",
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function failMessage({ numberComplete, amount, type }: {
|
function failMessage({
|
||||||
numberComplete: number,
|
numberComplete,
|
||||||
amount: number,
|
amount,
|
||||||
type: "timeout" | "fail",
|
type,
|
||||||
|
}: {
|
||||||
|
numberComplete: number;
|
||||||
|
amount: number;
|
||||||
|
type: "timeout" | "fail";
|
||||||
}): string {
|
}): string {
|
||||||
const pDone = getPercentageDone(numberComplete, amount);
|
const pDone = getPercentageDone(numberComplete, amount);
|
||||||
const { message, face } = pDone < 20
|
const { message, face } =
|
||||||
? { message: "No, sorry", face: "😑" }
|
pDone < 20
|
||||||
: pDone < 30
|
? { message: "No, sorry", face: "😑" }
|
||||||
? { message: "Oops, that's wrong", face: "😟" }
|
: pDone < 30
|
||||||
: pDone < 55
|
? { message: "Oops, that's wrong", face: "😟" }
|
||||||
? { message: "Fail", face: "😕" }
|
: pDone < 55
|
||||||
: pDone < 78
|
? { message: "Fail", face: "😕" }
|
||||||
? { message: "You almost got it!", face: "😩" }
|
: pDone < 78
|
||||||
: { message: "Nooo! So close!", face: "😭" };
|
? { message: "You almost got it!", face: "😩" }
|
||||||
return type === "fail"
|
: { message: "Nooo! So close!", face: "😭" };
|
||||||
? `${message} ${face}`
|
return type === "fail" ? `${message} ${face}` : `⏳ Time's Up ${face}`;
|
||||||
: `⏳ Time's Up ${face}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GameCore;
|
export default GameCore;
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
Metaproductions
|
||||||
|
|
||||||
|
PERSON :: 0 .. 11
|
||||||
|
|
||||||
|
Hyper-rules
|
||||||
|
|
||||||
|
s ()
|
||||||
|
=> es
|
||||||
|
=> vs
|
||||||
|
|
||||||
|
es (subj: PERSON)
|
||||||
|
=> np(subj), adj(subj), eq(subj)
|
||||||
|
es (subj: PERSON, pred: PERSON)
|
||||||
|
=> np(subj), np(pred), eq(pred)
|
||||||
|
|
||||||
|
vs ()
|
||||||
|
=> vs-intrans
|
||||||
|
=> vs-trans-past
|
||||||
|
=> vs-trans-non-past
|
||||||
|
|
||||||
|
vs-trans-non-past (subj: PERSON, obj: PERSON)
|
||||||
|
=> np(subj), np(obj), v-trans-non-past(subj, obj)
|
||||||
|
=> np(obj), np(subj), v-trans-non-past(subj, obj)
|
||||||
|
|
||||||
|
vs-trans-past (subj: PERSON, obj: PERSON)
|
||||||
|
=> np(subj), np(obj), v-trans-past(obj, obj)
|
||||||
|
=> np(obj), np(subj), v-trans-past(obj, obj)
|
||||||
|
|
||||||
|
vs-intrans-past (subj: PERSON) => np(subj), v-intrans(subj)
|
|
@ -4523,10 +4523,10 @@ react-fast-compare@^3.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
|
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
|
||||||
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
|
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
|
||||||
|
|
||||||
react-ga@3.3.0:
|
react-ga4@^2.1.0:
|
||||||
version "3.3.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-3.3.0.tgz#c91f407198adcb3b49e2bc5c12b3fe460039b3ca"
|
resolved "https://registry.yarnpkg.com/react-ga4/-/react-ga4-2.1.0.tgz#56601f59d95c08466ebd6edfbf8dede55c4678f9"
|
||||||
integrity sha512-o8RScHj6Lb8cwy3GMrVH6NJvL+y0zpJvKtc0+wmH7Bt23rszJmnqEQxRbyrqUzk9DTJIHoP42bfO5rswC9SWBQ==
|
integrity sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ==
|
||||||
|
|
||||||
react-is@^16.13.1, react-is@^16.3.2, react-is@^16.7.0:
|
react-is@^16.13.1, react-is@^16.3.2, react-is@^16.7.0:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
|
|
Loading…
Reference in New Issue