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-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",

View File

@ -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,10 +93,7 @@ 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>

View File

@ -1,11 +1,7 @@
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";
@ -13,7 +9,17 @@ 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({
children,
opts,
hideOmitSubject,
noEdit,
}: {
children: T.EPSelectionState;
opts: T.TextOptions;
hideOmitSubject?: boolean;
noEdit?: boolean;
}) {
const [editing, setEditing] = useState<boolean>(false); const [editing, setEditing] = useState<boolean>(false);
const [eps, setEps] = useState<T.EPSelectionState>(children); const [eps, setEps] = useState<T.EPSelectionState>(children);
const { user } = useUser(); const { user } = useUser();
@ -22,43 +28,57 @@ function EditableEPEx({ children, opts, hideOmitSubject, noEdit }: { children: T
setEps(children); setEps(children);
} }
function logEdit() { function logEdit() {
if (isProd && !(user?.admin)) { if (isProd && !user?.admin) {
ReactGA.event({ ReactGA.event({
category: "Example", category: "Example",
action: `edit EPex - ${window.location.pathname}`, action: `edit EPex - ${window.location.pathname}`,
label: "edit EPex" label: "edit EPex",
}); });
} }
} }
return <div className="mt-2 mb-4"> return (
{!noEdit && <div <div className="mt-2 mb-4">
{!noEdit && (
<div
className="text-left clickable" className="text-left clickable"
style={{ marginBottom: editing ? "0.5rem" : "-0.5rem" }} style={{ marginBottom: editing ? "0.5rem" : "-0.5rem" }}
onClick={editing ? handleReset : () => { onClick={
editing
? handleReset
: () => {
setEditing(true); setEditing(true);
logEdit(); logEdit();
}} }
}
> >
{!editing ? <EditIcon /> : <i className="fas fa-undo" />} {!editing ? <EditIcon /> : <i className="fas fa-undo" />}
</div>} </div>
{editing )}
&& <EPPicker {editing && (
<EPPicker
opts={opts} opts={opts}
entryFeeder={entryFeeder} entryFeeder={entryFeeder}
eps={eps} eps={eps}
onChange={setEps} onChange={setEps}
/>} />
)}
<EPDisplay <EPDisplay
opts={opts} opts={opts}
eps={eps} eps={eps}
setOmitSubject={hideOmitSubject ? false : (value) => setEps(o => ({ setOmitSubject={
hideOmitSubject
? false
: (value) =>
setEps((o) => ({
...o, ...o,
omitSubject: value === "true", omitSubject: value === "true",
}))} }))
}
justify="left" justify="left"
onlyOne onlyOne
/> />
</div>; </div>
);
} }
export default EditableEPEx; export default EditableEPEx;

View File

@ -6,7 +6,7 @@ import {
} 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";
@ -16,26 +16,37 @@ export function EditIcon() {
// 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">(
length || "short"
);
const [vps, setVps] = useState<T.VPSelectionState>({ ...children }); const [vps, setVps] = useState<T.VPSelectionState>({ ...children });
const { user } = useUser(); const { user } = useUser();
function logEdit() { function logEdit() {
if (isProd && !(user?.admin)) { if (isProd && !user?.admin) {
ReactGA.event({ ReactGA.event({
category: "Example", category: "Example",
action: `edit VPex - ${window.location.pathname}`, action: `edit VPex - ${window.location.pathname}`,
label: "edit VPex" label: "edit VPex",
}); });
} }
} }
@ -47,25 +58,32 @@ function EditableVPEx({ children, opts, formChoice, noEdit, length, mode, sub, a
function handleSetForm(form: T.FormVersion) { function handleSetForm(form: T.FormVersion) {
setVps(vpsReducer(vps, { type: "set form", payload: form })); setVps(vpsReducer(vps, { type: "set form", payload: form }));
} }
return <div className="mt-2 mb-4"> return (
{!noEdit && <div <div className="mt-2 mb-4">
{!noEdit && (
<div
className="text-left clickable mb-2" className="text-left clickable mb-2"
style={{ marginBottom: editing ? "0.5rem" : "-0.5rem" }} style={{ marginBottom: editing ? "0.5rem" : "-0.5rem" }}
onClick={editing ? handleReset : () => { onClick={
editing
? handleReset
: () => {
setEditing(true); setEditing(true);
logEdit(); logEdit();
}} }
}
> >
{!editing ? <EditIcon /> : <i className="fas fa-undo" />} {!editing ? <EditIcon /> : <i className="fas fa-undo" />}
</div>} </div>
{editing )}
&& <VPPicker {editing && (
<VPPicker
opts={opts} opts={opts}
entryFeeder={entryFeeder} entryFeeder={entryFeeder}
vps={vps} vps={vps}
onChange={setVps} onChange={setVps}
/> />
} )}
<VPDisplay <VPDisplay
opts={opts} opts={opts}
VPS={vps} VPS={vps}
@ -78,7 +96,8 @@ function EditableVPEx({ children, opts, formChoice, noEdit, length, mode, sub, a
inlineFormChoice inlineFormChoice
/> />
{sub && <div className="text-muted small">{sub}</div>} {sub && <div className="text-muted small">{sub}</div>}
</div>; </div>
);
} }
export default EditableVPEx; export default EditableVPEx;

View File

@ -1,70 +1,78 @@
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,
}) & {
numberComplete: number,
current: Question,
timerKey: number,
strikes: number,
justStruck: boolean,
} }
| {
type GameReducerAction = { mode: "intro" | "test" | "fail" | "timeout" | "complete";
type: "handle question response", showAnswer: false;
payload: { correct: boolean },
} | {
type: "start",
payload: "practice" | "test",
} | {
type: "quit",
} | {
type: "timeout",
} | {
type: "toggle show answer",
} | {
type: "skip",
} }
) & {
numberComplete: number;
current: Question;
timerKey: number;
strikes: number;
justStruck: boolean;
};
function GameCore<Question>({ inChapter, getQuestion, amount, Display, DisplayCorrectAnswer, timeLimit, Instructions, studyLink, id }: { type GameReducerAction =
inChapter: boolean, | {
id: string, type: "handle question response";
studyLink: string, payload: { correct: boolean };
Instructions: (props: { opts?: Types.TextOptions }) => JSX.Element, }
getQuestion: () => Question, | {
DisplayCorrectAnswer: (props: { question: Question }) => JSX.Element, type: "start";
Display: (props: QuestionDisplayProps<Question>) => JSX.Element, payload: "practice" | "test";
timeLimit: number, }
amount: number, | {
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> = { const initialState: GameState<Question> = {
mode: "intro", mode: "intro",
@ -79,12 +87,16 @@ function GameCore<Question>({ inChapter, getQuestion, amount, Display, DisplayCo
const rewardRef = useRef<RewardElement | null>(null); const rewardRef = useRef<RewardElement | null>(null);
const parent = useRef<HTMLDivElement | null>(null); const parent = useRef<HTMLDivElement | null>(null);
const { user, pullUser, setUser } = useUser(); const { user, pullUser, setUser } = useUser();
const [state, setStateDangerous] = useState<GameState<Question>>(initialState); const [state, setStateDangerous] =
useState<GameState<Question>>(initialState);
useEffect(() => { useEffect(() => {
parent.current && autoAnimate(parent.current) parent.current && autoAnimate(parent.current);
}, [parent]); }, [parent]);
const gameReducer = (gs: GameState<Question>, action: GameReducerAction): GameState<Question> => { const gameReducer = (
gs: GameState<Question>,
action: GameReducerAction
): GameState<Question> => {
if (action.type === "handle question response") { if (action.type === "handle question response") {
if (gs.mode === "test") { if (gs.mode === "test") {
if (action.payload.correct) { if (action.payload.correct) {
@ -98,8 +110,7 @@ function GameCore<Question>({ inChapter, getQuestion, amount, Display, DisplayCo
numberComplete, numberComplete,
justStruck: false, justStruck: false,
mode: "complete", mode: "complete",
};
}
} else { } else {
return { return {
...gs, ...gs,
@ -128,8 +139,7 @@ function GameCore<Question>({ inChapter, getQuestion, amount, Display, DisplayCo
}; };
} }
} }
} } /* (gs.mode === "practice") */ else {
else /* (gs.mode === "practice") */ {
if (action.payload.correct) { if (action.payload.correct) {
const numberComplete = gs.numberComplete + (!gs.showAnswer ? 1 : 0); const numberComplete = gs.numberComplete + (!gs.showAnswer ? 1 : 0);
return { return {
@ -158,13 +168,13 @@ function GameCore<Question>({ inChapter, getQuestion, amount, Display, DisplayCo
mode: action.payload, mode: action.payload,
current: getQuestion(), current: getQuestion(),
timerKey: gs.timerKey + 1, timerKey: gs.timerKey + 1,
} };
} }
if (action.type === "quit") { if (action.type === "quit") {
return { return {
...initialState, ...initialState,
timerKey: gs.timerKey + 1, timerKey: gs.timerKey + 1,
} };
} }
if (action.type === "timeout") { if (action.type === "timeout") {
logGameEvent("timeout"); logGameEvent("timeout");
@ -198,14 +208,14 @@ function GameCore<Question>({ inChapter, getQuestion, amount, Display, DisplayCo
return gs; return gs;
} }
throw new Error("unknown GameReducerAction"); throw new Error("unknown GameReducerAction");
} };
function dispatch(action: GameReducerAction) { function dispatch(action: GameReducerAction) {
setStateDangerous(gs => gameReducer(gs, action)); setStateDangerous((gs) => gameReducer(gs, action));
} }
function logGameEvent(action: string) { function logGameEvent(action: string) {
if (isProd && !(user?.admin)) { if (isProd && !user?.admin) {
ReactGA.event({ ReactGA.event({
category: "Game", category: "Game",
action: `${action} - ${id}`, action: `${action} - ${id}`,
@ -237,48 +247,79 @@ function GameCore<Question>({ inChapter, getQuestion, amount, Display, DisplayCo
// save the test result in local storage // save the test result in local storage
saveResult(result, user.userId); saveResult(result, user.userId);
// try to post the result // try to post the result
postSavedResults(user.userId).then((r) => { postSavedResults(user.userId)
.then((r) => {
if (r === "sent") pullUser(); if (r === "sent") pullUser();
}).catch(console.error); })
.catch(console.error);
} }
function getProgressWidth(): string { function getProgressWidth(): string {
const num = !state.current const num = !state.current
? 0 ? 0
: (state.mode === "complete") : state.mode === "complete"
? 100 ? 100
: getPercentageDone(state.numberComplete, amount); : getPercentageDone(state.numberComplete, amount);
return `${num}%`; return `${num}%`;
} }
const progressColor = state.mode === "complete" const progressColor =
state.mode === "complete"
? "success" ? "success"
: (state.mode === "fail" || state.mode === "timeout") : state.mode === "fail" || state.mode === "timeout"
? "danger" ? "danger"
: "primary"; : "primary";
const gameRunning = state.mode === "practice" || state.mode === "test"; const gameRunning = state.mode === "practice" || state.mode === "test";
function ActionButtons() { function ActionButtons() {
return <div> return (
{!inChapter && <Link to={studyLink}> <div>
{!inChapter && (
<Link to={studyLink}>
<button className="btn btn-danger mt-4 mx-3">Study</button> <button className="btn btn-danger mt-4 mx-3">Study</button>
</Link>} </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> <button
</div>; 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 <> 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
<div className={`progress-bar bg-${progressColor}`} role="progressbar" style={{ width: getProgressWidth() }} /> className="text-center"
</div>} 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"> <div className="d-flex flex-row justify-content-between mt-2">
{state.mode === "test" {state.mode === "test" ? (
? <StrikesDisplay strikes={state.strikes} /> <StrikesDisplay strikes={state.strikes} />
: state.mode === "practice" ? <PracticeStatusDisplay ) : state.mode === "practice" ? (
<PracticeStatusDisplay
correct={state.numberComplete} correct={state.numberComplete}
incorrect={state.strikes} incorrect={state.strikes}
/> />
: <div />} ) : (
<div />
)}
<div className="d-flex flex-row justify-content-right"> <div className="d-flex flex-row justify-content-right">
{state.mode === "test" && <CountdownCircleTimer {state.mode === "test" && (
<CountdownCircleTimer
key={state.timerKey} key={state.timerKey}
isPlaying={gameRunning} isPlaying={gameRunning}
size={30} size={30}
@ -288,55 +329,109 @@ function GameCore<Question>({ inChapter, getQuestion, amount, Display, DisplayCo
strokeLinecap="square" strokeLinecap="square"
duration={timeLimit} duration={timeLimit}
onComplete={() => dispatch({ type: "timeout" })} onComplete={() => dispatch({ type: "timeout" })}
/>} />
{state.mode !== "intro" && <button onClick={() => dispatch({ type: "quit" })} className="btn btn-outline-secondary btn-sm ml-2"> )}
{state.mode !== "intro" && (
<button
onClick={() => dispatch({ type: "quit" })}
className="btn btn-outline-secondary btn-sm ml-2"
>
Quit Quit
</button>} </button>
)}
</div> </div>
</div> </div>
<div ref={parent}> <div ref={parent}>
{state.justStruck && <div className="alert alert-warning my-2" role="alert" style={{ maxWidth: "300px", margin: "0 auto" }}> {state.justStruck && (
<div
className="alert alert-warning my-2"
role="alert"
style={{ maxWidth: "300px", margin: "0 auto" }}
>
{getStrikeMessage()} {getStrikeMessage()}
</div>}
</div> </div>
<Reward ref={rewardRef} config={{ lifetime: 130, spread: 90, elementCount: 150, zIndex: 999999999 }} type="confetti"> )}
</div>
<Reward
ref={rewardRef}
config={{
lifetime: 130,
spread: 90,
elementCount: 150,
zIndex: 999999999,
}}
type="confetti"
>
<div className="mb-2"> <div className="mb-2">
{state.mode === "intro" && <div> {state.mode === "intro" && (
<div>
<div className="pt-3"> <div className="pt-3">
{/* TODO: ADD IN TEXT DISPLAY OPTIONS HERE TOO - WHEN WE START USING THEM*/} {/* TODO: ADD IN TEXT DISPLAY OPTIONS HERE TOO - WHEN WE START USING THEM*/}
<Instructions /> <Instructions />
</div> </div>
<ActionButtons /> <ActionButtons />
</div>} </div>
{gameRunning && <Display )}
{gameRunning && (
<Display
question={state.current} question={state.current}
callback={(correct) => dispatch({ type: "handle question response", payload: { correct }})} callback={(correct) =>
/>} dispatch({
{(state.mode === "practice" && (state.justStruck || state.showAnswer)) && <div className="my-3"> type: "handle question response",
<button className="btn btn-sm btn-secondary" onClick={() => dispatch({ type: "toggle show answer" })}> 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 {state.showAnswer ? "Hide" : "Show"} Answer
</button> </button>
</div>} </div>
{(state.showAnswer && state.mode === "practice") && <div className="my-2"> )}
{state.showAnswer && state.mode === "practice" && (
<div className="my-2">
<div className="my-1"> <div className="my-1">
<DisplayCorrectAnswer question={state.current} /> <DisplayCorrectAnswer question={state.current} />
</div> </div>
<button className="btn btn-sm btn-primary my-2" onClick={() => dispatch({ type: "skip" })}> <button
className="btn btn-sm btn-primary my-2"
onClick={() => dispatch({ type: "skip" })}
>
Next Question Next Question
</button> </button>
</div>} </div>
{state.mode === "complete" && <div> )}
{state.mode === "complete" && (
<div>
<h4 className="mt-4"> <h4 className="mt-4">
<span role="img" aria-label="celebration">🎉</span> Finished! <span role="img" aria-label="celebration">
🎉
</span>{" "}
Finished!
</h4> </h4>
<button className="btn btn-secondary mt-4" onClick={() => dispatch({ type: "start", payload: "test" })}>Try Again</button> <button
</div>} className="btn btn-secondary mt-4"
{(state.mode === "timeout" || state.mode === "fail") && <div className="mb-4"> onClick={() => dispatch({ type: "start", payload: "test" })}
<h4 className="mt-4">{failMessage({ >
Try Again
</button>
</div>
)}
{(state.mode === "timeout" || state.mode === "fail") && (
<div className="mb-4">
<h4 className="mt-4">
{failMessage({
numberComplete: state.numberComplete, numberComplete: state.numberComplete,
amount, amount,
type: state.mode, type: state.mode,
})}</h4> })}
</h4>
<div>The correct answer was:</div> <div>The correct answer was:</div>
<div className="my-2"> <div className="my-2">
<DisplayCorrectAnswer question={state.current} /> <DisplayCorrectAnswer question={state.current} />
@ -344,11 +439,14 @@ function GameCore<Question>({ inChapter, getQuestion, amount, Display, DisplayCo
<div className="my-3"> <div className="my-3">
<ActionButtons /> <ActionButtons />
</div> </div>
</div>} </div>
)}
</div> </div>
</Reward> </Reward>
</div> </div>
{gameRunning && <div style={{ {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,21 +455,42 @@ 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>
<div>
<samp>Incorrect: {incorrect}</samp>
</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() {
@ -384,13 +503,18 @@ function getStrikeMessage() {
]); ]);
} }
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 } =
pDone < 20
? { message: "No, sorry", face: "😑" } ? { message: "No, sorry", face: "😑" }
: pDone < 30 : pDone < 30
? { message: "Oops, that's wrong", face: "😟" } ? { message: "Oops, that's wrong", face: "😟" }
@ -399,9 +523,7 @@ function failMessage({ numberComplete, amount, type }: {
: pDone < 78 : pDone < 78
? { message: "You almost got it!", face: "😩" } ? { message: "You almost got it!", face: "😩" }
: { message: "Nooo! So close!", face: "😭" }; : { message: "Nooo! So close!", face: "😭" };
return type === "fail" return type === "fail" ? `${message} ${face}` : `⏳ Time's Up ${face}`;
? `${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" 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"