update ga
This commit is contained in:
parent
0def4b3136
commit
5d21aa142a
|
@ -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",
|
||||
|
|
52
src/App.tsx
52
src/App.tsx
|
@ -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>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
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"
|
||||
|
|
Loading…
Reference in New Issue