update ga

This commit is contained in:
adueck 2023-07-15 01:03:29 +04:00
parent 0def4b3136
commit 5d21aa142a
7 changed files with 695 additions and 511 deletions

View File

@ -33,7 +33,7 @@
"react-bootstrap": "1.6.4",
"react-countdown-circle-timer": "3.0.9",
"react-dom": "^18.2.0",
"react-ga": "3.3.0",
"react-ga4": "^2.1.0",
"react-media": "1",
"react-player": "2.10.1",
"react-rewards": "1.1.2",

View File

@ -19,19 +19,19 @@ import LandingPage from "./pages/LandingPage";
import AccountPage from "./pages/AccountPage";
import { useEffect } from "react";
import { isProd } from "./lib/isProd";
import ReactGA from "react-ga";
import ReactGA from "react-ga4";
import { useUser } from "./user-context";
import PrivacyPolicy from "./pages/PrivacyPolicy";
import SearchPage from "./pages/SearchPage";
const chapters = content.reduce((chapters, item) => (
item.content
? [...chapters, item]
: [...chapters, ...item.chapters]
), []);
const chapters = content.reduce(
(chapters, item) =>
item.content ? [...chapters, item] : [...chapters, ...item.chapters],
[]
);
if (isProd) {
ReactGA.initialize("UA-196576671-2");
ReactGA.initialize("387148608");
ReactGA.set({ anonymizeIp: true });
}
@ -41,15 +41,18 @@ function App() {
const navigate = useNavigate();
const { user } = useUser();
function logAnalytics() {
if (isProd && !(user?.admin)) {
ReactGA.pageview(window.location.pathname);
};
if (isProd && !user?.admin) {
ReactGA.send({
hitType: "pageview",
page: window.location.pathname,
});
}
}
useEffect(() => {
logAnalytics();
if (window.location.pathname === "/") {
if (localStorage.getItem("visitedOnce")) {
navigate("/table-of-contents", { replace: true })
navigate("/table-of-contents", { replace: true });
} else {
localStorage.setItem("visitedOnce", "true");
}
@ -65,7 +68,10 @@ function App() {
<>
<Header setNavOpen={setNavOpen} />
<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
content={content}
navOpen={navOpen}
@ -73,18 +79,9 @@ function App() {
pathname={window.location.pathname}
/>
<Routes>
<Route
path="/"
element={<LandingPage />}
/>
<Route
path="/search"
element={<SearchPage />}
/>
<Route
path="/privacy"
element={<PrivacyPolicy />}
/>
<Route path="/" element={<LandingPage />} />
<Route path="/search" element={<SearchPage />} />
<Route path="/privacy" element={<PrivacyPolicy />} />
<Route
path="/table-of-contents"
element={<TableOfContentsPage />}
@ -96,15 +93,12 @@ function App() {
element={<Chapter>{chapter}</Chapter>}
/>
))}
<Route
path="/account"
element={<AccountPage />}
/>
<Route path="/account" element={<AccountPage />} />
<Route path="*" element={<Page404 />} />
</Routes>
</div>
</div>
</>
</>
);
}

View File

@ -1,64 +1,84 @@
import {
Types as T,
EPDisplay,
EPPicker,
} from "@lingdocs/ps-react";
import { Types as T, EPDisplay, EPPicker } from "@lingdocs/ps-react";
import entryFeeder from "../../lib/entry-feeder";
import { useState } from "react";
import ReactGA from "react-ga";
import ReactGA from "react-ga4";
import { isProd } from "../../lib/isProd";
import { useUser } from "../../user-context";
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 }) {
const [editing, setEditing] = useState<boolean>(false);
const [eps, setEps] = useState<T.EPSelectionState>(children);
const { user } = useUser();
function handleReset() {
setEditing(false);
setEps(children);
function EditableEPEx({
children,
opts,
hideOmitSubject,
noEdit,
}: {
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)) {
ReactGA.event({
category: "Example",
action: `edit EPex - ${window.location.pathname}`,
label: "edit EPex"
});
}
}
return <div className="mt-2 mb-4">
{!noEdit && <div
className="text-left clickable"
style={{ marginBottom: editing ? "0.5rem" : "-0.5rem" }}
onClick={editing ? handleReset : () => {
setEditing(true);
logEdit();
}}
}
return (
<div className="mt-2 mb-4">
{!noEdit && (
<div
className="text-left clickable"
style={{ marginBottom: editing ? "0.5rem" : "-0.5rem" }}
onClick={
editing
? handleReset
: () => {
setEditing(true);
logEdit();
}
}
>
{!editing ? <EditIcon /> : <i className="fas fa-undo" />}
</div>}
{editing
&& <EPPicker
opts={opts}
entryFeeder={entryFeeder}
eps={eps}
onChange={setEps}
/>}
<EPDisplay
opts={opts}
eps={eps}
setOmitSubject={hideOmitSubject ? false : (value) => setEps(o => ({
...o,
omitSubject: value === "true",
}))}
justify="left"
onlyOne
{!editing ? <EditIcon /> : <i className="fas fa-undo" />}
</div>
)}
{editing && (
<EPPicker
opts={opts}
entryFeeder={entryFeeder}
eps={eps}
onChange={setEps}
/>
</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;

View File

@ -1,84 +1,103 @@
import {
Types as T,
VPDisplay,
VPPicker,
vpsReducer,
Types as T,
VPDisplay,
VPPicker,
vpsReducer,
} from "@lingdocs/ps-react";
import entryFeeder from "../../lib/entry-feeder";
import { useState } from "react";
import ReactGA from "react-ga";
import ReactGA from "react-ga4";
import { isProd } from "../../lib/isProd";
import { useUser } from "../../user-context";
export function EditIcon() {
return <i className="fas fa-edit" />;
return <i className="fas fa-edit" />;
}
// TODO: Ability to show all variations
function EditableVPEx({ children, opts, formChoice, noEdit, length, mode, sub, allVariations }: {
children: T.VPSelectionState,
opts: T.TextOptions,
formChoice?: boolean,
noEdit?: boolean,
length?: "long" | "short",
mode?: "text" | "blocks",
sub?: string | JSX.Element,
allVariations?: boolean,
function EditableVPEx({
children,
opts,
formChoice,
noEdit,
length,
mode,
sub,
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 [selectedLength, setSelectedLength] = useState<"long" | "short">(length || "short");
const [vps, setVps] = useState<T.VPSelectionState>({ ...children });
const { user } = useUser();
function logEdit() {
if (isProd && !(user?.admin)) {
ReactGA.event({
category: "Example",
action: `edit VPex - ${window.location.pathname}`,
label: "edit VPex"
});
}
const [editing, setEditing] = useState<boolean>(false);
const [selectedLength, setSelectedLength] = useState<"long" | "short">(
length || "short"
);
const [vps, setVps] = useState<T.VPSelectionState>({ ...children });
const { user } = useUser();
function logEdit() {
if (isProd && !user?.admin) {
ReactGA.event({
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 ???
setVps(children);
setEditing(false);
}
function handleSetForm(form: T.FormVersion) {
setVps(vpsReducer(vps, { type: "set form", payload: form }));
}
return <div className="mt-2 mb-4">
{!noEdit && <div
className="text-left clickable mb-2"
style={{ marginBottom: editing ? "0.5rem" : "-0.5rem" }}
onClick={editing ? handleReset : () => {
setEditing(true);
logEdit();
}}
}
function handleReset() {
// TODO: this is crazy, how does children get changed after calling setVps ???
setVps(children);
setEditing(false);
}
function handleSetForm(form: T.FormVersion) {
setVps(vpsReducer(vps, { type: "set form", payload: form }));
}
return (
<div className="mt-2 mb-4">
{!noEdit && (
<div
className="text-left clickable mb-2"
style={{ marginBottom: editing ? "0.5rem" : "-0.5rem" }}
onClick={
editing
? handleReset
: () => {
setEditing(true);
logEdit();
}
}
>
{!editing ? <EditIcon /> : <i className="fas fa-undo" />}
</div>}
{editing
&& <VPPicker
opts={opts}
entryFeeder={entryFeeder}
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
{!editing ? <EditIcon /> : <i className="fas fa-undo" />}
</div>
)}
{editing && (
<VPPicker
opts={opts}
entryFeeder={entryFeeder}
vps={vps}
onChange={setVps}
/>
{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;

View File

@ -1,354 +1,452 @@
import { useState, useRef, useEffect } from "react";
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 { 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 {
randFromArray,
Types,
} from "@lingdocs/ps-react";
import ReactGA from "react-ga";
import { getPercentageDone } from "../lib/game-utils";
import { saveResult, postSavedResults } from "../lib/game-results";
import { AT, getTimestamp } from "@lingdocs/lingdocs-main";
import { randFromArray, Types } from "@lingdocs/ps-react";
import ReactGA from "react-ga4";
import { isProd } from "../lib/isProd";
import autoAnimate from "@formkit/auto-animate";
const errorVibration = 200;
const strikesToFail = 3;
type GameState<Question> = ({
mode: "practice",
showAnswer: boolean,
} | {
mode: "intro" | "test" | "fail" | "timeout" | "complete",
showAnswer: false,
}) & {
numberComplete: number,
current: Question,
timerKey: number,
strikes: number,
justStruck: boolean,
}
type GameState<Question> = (
| {
mode: "practice";
showAnswer: boolean;
}
| {
mode: "intro" | "test" | "fail" | "timeout" | "complete";
showAnswer: false;
}
) & {
numberComplete: number;
current: Question;
timerKey: number;
strikes: number;
justStruck: boolean;
};
type GameReducerAction = {
type: "handle question response",
payload: { correct: boolean },
} | {
type: "start",
payload: "practice" | "test",
} | {
type: "quit",
} | {
type: "timeout",
} | {
type: "toggle show answer",
} | {
type: "skip",
}
function GameCore<Question>({ inChapter, getQuestion, amount, Display, DisplayCorrectAnswer, timeLimit, Instructions, studyLink, id }: {
inChapter: boolean,
id: string,
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,
type GameReducerAction =
| {
type: "handle question response";
payload: { correct: boolean };
}
| {
type: "start";
payload: "practice" | "test";
}
| {
type: "quit";
}
| {
type: "timeout";
}
| {
type: "toggle show answer";
}
| {
type: "skip";
};
// 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> => {
if (action.type === "handle question response") {
if (gs.mode === "test") {
if (action.payload.correct) {
const numberComplete = gs.numberComplete + 1;
if (numberComplete === amount) {
logGameEvent("passed");
rewardRef.current?.rewardMe();
handleResult(true);
return {
...gs,
numberComplete,
justStruck: false,
mode: "complete",
function GameCore<Question>({
inChapter,
getQuestion,
amount,
Display,
DisplayCorrectAnswer,
timeLimit,
Instructions,
studyLink,
id,
}: {
inChapter: boolean;
id: string;
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]);
}
} else {
return {
...gs,
numberComplete,
current: getQuestion(),
justStruck: false,
};
}
} else {
punish();
const strikes = gs.strikes + 1;
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}`);
const gameReducer = (
gs: GameState<Question>,
action: GameReducerAction
): GameState<Question> => {
if (action.type === "handle question response") {
if (gs.mode === "test") {
if (action.payload.correct) {
const numberComplete = gs.numberComplete + 1;
if (numberComplete === amount) {
logGameEvent("passed");
rewardRef.current?.rewardMe();
handleResult(true);
return {
...initialState,
mode: action.payload,
current: getQuestion(),
timerKey: gs.timerKey + 1,
}
}
if (action.type === "quit") {
...gs,
numberComplete,
justStruck: false,
mode: "complete",
};
} else {
return {
...initialState,
timerKey: gs.timerKey + 1,
}
}
if (action.type === "timeout") {
logGameEvent("timeout");
...gs,
numberComplete,
current: getQuestion(),
justStruck: false,
};
}
} else {
punish();
const strikes = gs.strikes + 1;
if (strikes === strikesToFail) {
logGameEvent("fail");
handleResult(false);
return {
...gs,
mode: "timeout",
justStruck: false,
showAnswer: false,
...gs,
strikes,
mode: "fail",
justStruck: false,
};
}
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;
} else {
return {
...u,
tests: [...u.tests, result],
...gs,
strikes,
justStruck: true,
};
});
// 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);
}
}
} /* (gs.mode === "practice") */ else {
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,
};
}
}
}
function getProgressWidth(): string {
const num = !state.current
? 0
: (state.mode === "complete")
? 100
: getPercentageDone(state.numberComplete, amount);
return `${num}%`;
if (action.type === "start") {
logGameEvent(`started ${action.payload}`);
return {
...initialState,
mode: action.payload,
current: getQuestion(),
timerKey: gs.timerKey + 1,
};
}
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>;
if (action.type === "quit") {
return {
...initialState,
timerKey: gs.timerKey + 1,
};
}
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 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>
if (action.type === "timeout") {
logGameEvent("timeout");
handleResult(false);
return {
...gs,
mode: "timeout",
justStruck: false,
showAnswer: false,
};
}
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 {
...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 getProgressWidth(): string {
const num = !state.current
? 0
: state.mode === "complete"
? 100
: 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>
{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",
backgroundColor: "rgba(255, 255, 255, 0.3)",
backdropFilter: "blur(10px)",
@ -357,51 +455,75 @@ function GameCore<Question>({ inChapter, getQuestion, amount, Display, DisplayCo
width: "100%",
height: "100%",
zIndex: 6,
}}></div>}
</>;
}}
></div>
)}
</>
);
}
function PracticeStatusDisplay({ correct, incorrect }: { 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>
function PracticeStatusDisplay({
correct,
incorrect,
}: {
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>
);
}
function StrikesDisplay({ strikes }: { strikes: number }) {
return <div>
{[...Array(strikes)].map(_ => <span key={Math.random()} className="mr-2"></span>)}
</div>;
return (
<div>
{[...Array(strikes)].map((_) => (
<span key={Math.random()} className="mr-2">
</span>
))}
</div>
);
}
function getStrikeMessage() {
return randFromArray([
"Not quite! Try again.",
"No sorry, try again",
"Umm, no, try again",
"Try again",
"Oooooooo, sorry no...",
]);
return randFromArray([
"Not quite! Try again.",
"No sorry, try again",
"Umm, no, try again",
"Try again",
"Oooooooo, sorry no...",
]);
}
function failMessage({ numberComplete, amount, type }: {
numberComplete: number,
amount: number,
type: "timeout" | "fail",
function failMessage({
numberComplete,
amount,
type,
}: {
numberComplete: number;
amount: number;
type: "timeout" | "fail";
}): string {
const pDone = getPercentageDone(numberComplete, amount);
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 type === "fail"
? `${message} ${face}`
: `⏳ Time's Up ${face}`;
const pDone = getPercentageDone(numberComplete, amount);
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 type === "fail" ? `${message} ${face}` : `⏳ Time's Up ${face}`;
}
export default GameCore;
export default GameCore;

29
w-grammar.txt Normal file
View File

@ -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)

View File

@ -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"
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
react-ga@3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-3.3.0.tgz#c91f407198adcb3b49e2bc5c12b3fe460039b3ca"
integrity sha512-o8RScHj6Lb8cwy3GMrVH6NJvL+y0zpJvKtc0+wmH7Bt23rszJmnqEQxRbyrqUzk9DTJIHoP42bfO5rswC9SWBQ==
react-ga4@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/react-ga4/-/react-ga4-2.1.0.tgz#56601f59d95c08466ebd6edfbf8dede55c4678f9"
integrity sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ==
react-is@^16.13.1, react-is@^16.3.2, react-is@^16.7.0:
version "16.13.1"