fix build

This commit is contained in:
adueck 2024-12-06 18:28:08 +05:00
parent a254592fd1
commit 35f29e8048
13 changed files with 1108 additions and 804 deletions

View File

@ -17,7 +17,7 @@
"@formkit/auto-animate": "^1.0.0-beta.6", "@formkit/auto-animate": "^1.0.0-beta.6",
"@fortawesome/fontawesome-free": "5.15.4", "@fortawesome/fontawesome-free": "5.15.4",
"@lingdocs/lingdocs-main": "^0.3.3", "@lingdocs/lingdocs-main": "^0.3.3",
"@lingdocs/ps-react": "^7.7.3", "@lingdocs/ps-react": "^7.7.4",
"@mdx-js/rollup": "^2.2.1", "@mdx-js/rollup": "^2.2.1",
"@stefanprobst/rehype-extract-toc": "^2.2.0", "@stefanprobst/rehype-extract-toc": "^2.2.0",
"@types/mdx": "^2.0.3", "@types/mdx": "^2.0.3",

View File

@ -49,12 +49,13 @@ function BasicVerbShowCase({
render={(item) => { render={(item) => {
return { return {
title: ( title: (
<InlinePs opts={opts}> <InlinePs
{{ opts={opts}
ps={{
...removeFVarients(item.entry), ...removeFVarients(item.entry),
e: undefined, e: undefined,
}} }}
</InlinePs> />
), ),
body: ( body: (
<BasicVerbChart <BasicVerbChart

View File

@ -20,10 +20,10 @@ export default function GenderTable({ rows }) {
return <td key={`row-${i}-${gender}`} style={{ background: genderColors[gender === "masc" ? "m" : "f"] }}> return <td key={`row-${i}-${gender}`} style={{ background: genderColors[gender === "masc" ? "m" : "f"] }}>
{item.ending && <div> {item.ending && <div>
{typeof item.ending === "string" ? item.ending {typeof item.ending === "string" ? item.ending
: <InlinePs opts={opts}>{item.ending}</InlinePs>} : <InlinePs opts={opts} ps={item.ending} />}
</div>} </div>}
<div> <div>
{item.ending ? "eg. " : ""}<InlinePs opts={opts}>{item.ex}</InlinePs> {item.ending ? "eg. " : ""}<InlinePs opts={opts} ps={item.ex} />
</div> </div>
</td> </td>
})} })}

View File

@ -9,15 +9,22 @@ import {
Types as T, Types as T,
} from "@lingdocs/ps-react"; } from "@lingdocs/ps-react";
function InflectionCarousel({ items }: { items: (T.NounEntry | T.AdjectiveEntry)[] }) { function InflectionCarousel({
items,
}: {
items: (T.NounEntry | T.AdjectiveEntry)[];
}) {
if (!items.length) { if (!items.length) {
return "no items for carousel"; return "no items for carousel";
} }
return ( return (
<div className="mt-3"> <div className="mt-3">
<Carousel items={items} render={item => { <Carousel
items={items}
render={(item) => {
const e = getEnglishWord(item); const e = getEnglishWord(item);
const english = e === undefined const english =
e === undefined
? item.e ? item.e
: typeof e === "string" : typeof e === "string"
? e ? e
@ -29,21 +36,30 @@ function InflectionCarousel({ items }: { items: (T.NounEntry | T.AdjectiveEntry)
return { return {
title: "Oops! 🤷‍♂️", title: "Oops! 🤷‍♂️",
// @ts-ignore // @ts-ignore
body: <div>Oops! No inflections for <InlinePs opts={opts}>{item}</InlinePs></div>, body: (
<div>
Oops! No inflections for <InlinePs opts={opts} ps={item} />
</div>
),
}; };
} }
return { return {
// @ts-ignore // @ts-ignore
title: <InlinePs opts={opts} ps={{ title: (
<InlinePs
opts={opts}
ps={{
...removeFVarients(item), ...removeFVarients(item),
e: english, e: english,
}} />, }}
body: <InflectionsTable />
inf={infOut.inflections} ),
textOptions={opts} body: (
/>, <InflectionsTable inf={infOut.inflections} textOptions={opts} />
),
}; };
}}/> }}
/>
</div> </div>
); );
} }

View File

@ -7,27 +7,41 @@ import {
getEnglishFromRendered, getEnglishFromRendered,
} from "@lingdocs/ps-react"; } from "@lingdocs/ps-react";
function BlockDiagram({ opts, children }: { function BlockDiagram({
opts: T.TextOptions, opts,
children: T.NPSelection | T.APSelection, children,
}: {
opts: T.TextOptions;
children: T.NPSelection | T.APSelection;
}) { }) {
try { try {
const rendered = children.type === "AP" const rendered =
children.type === "AP"
? renderAPSelection(children, 0) ? renderAPSelection(children, 0)
: renderNPSelection(children, false, false, "subject", "none", false); : renderNPSelection(children, false, false, "subject", "none", false);
const english = getEnglishFromRendered(rendered); const english = getEnglishFromRendered(rendered);
return <div className="mb-3"> return (
<div className="d-flex flex-row justify-content-center text-center" style={{ maxWidth: "100%" }}> <div className="mb-3">
{rendered.type === "NP" <div
? <NPBlock script="f" opts={opts} english={english}>{rendered}</NPBlock> className="d-flex flex-row justify-content-center text-center"
: <APBlock script="f" opts={opts} english={english}>{rendered}</APBlock>} style={{ maxWidth: "100%" }}
>
{rendered.type === "NP" ? (
<NPBlock script="f" opts={opts} english={english}>
{rendered}
</NPBlock>
) : (
<APBlock script="f" opts={opts} english={english}>
{rendered}
</APBlock>
)}
</div> </div>
</div>; </div>
} catch(e) { );
} catch (e) {
console.log(e); console.log(e);
return <div>ERROR</div>; return <div>ERROR</div>;
} }
} }
export default BlockDiagram; export default BlockDiagram;

View File

@ -1,12 +1,5 @@
import { import { Types as T, NPPicker, APPicker } from "@lingdocs/ps-react";
Types as T, import { useEffect, useRef } from "react";
NPPicker,
APPicker,
} from "@lingdocs/ps-react";
import {
useEffect,
useRef,
} from "react";
import { useState } from "react"; import { useState } from "react";
import BlockDiagram from "./BlockDiagram"; import BlockDiagram from "./BlockDiagram";
import entryFeeder from "../../lib/entry-feeder"; import entryFeeder from "../../lib/entry-feeder";
@ -16,28 +9,36 @@ export function EditIcon() {
return <i className="fas fa-edit" />; return <i className="fas fa-edit" />;
} }
function selectionToBlock(s: T.NPSelection | T.APSelection): { type: "NP", block: T.NPSelection | undefined } | { type: "AP", block: T.APSelection | undefined } { function selectionToBlock(
return (s.type === "AP") s: T.NPSelection | T.APSelection
? { type: "AP", block: s } ):
: { type: "NP", block: s }; | { type: "NP"; block: T.NPSelection | undefined }
| { type: "AP"; block: T.APSelection | undefined } {
return s.type === "AP" ? { type: "AP", block: s } : { type: "NP", block: s };
} }
function EditableBlock({ opts, children: block }: { function EditableBlock({
opts: T.TextOptions, opts,
children: T.NPSelection | T.APSelection, children: block,
}: {
opts: T.TextOptions;
children: T.NPSelection | T.APSelection;
}) { }) {
const parent = useRef<HTMLDivElement>(null); const parent = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
parent.current && autoAnimate(parent.current) parent.current && autoAnimate(parent.current);
}, [parent]); }, [parent]);
const [editing, setEditing] = useState<boolean>(false); const [editing, setEditing] = useState<boolean>(false);
const [edited, setEdited] = useState<{ const [edited, setEdited] = useState<
type: "NP", | {
block: T.NPSelection | undefined, type: "NP";
} | { block: T.NPSelection | undefined;
type: "AP", }
block: T.APSelection | undefined, | {
}>(selectionToBlock(block)); type: "AP";
block: T.APSelection | undefined;
}
>(selectionToBlock(block));
function handleNPChange(np: T.NPSelection | undefined) { function handleNPChange(np: T.NPSelection | undefined) {
setEdited({ type: "NP", block: np }); setEdited({ type: "NP", block: np });
} }
@ -48,7 +49,8 @@ function EditableBlock({ opts, children: block }: {
setEdited(selectionToBlock(block)); setEdited(selectionToBlock(block));
setEditing(false); setEditing(false);
} }
return <div> return (
<div>
<div <div
className="text-right clickable" className="text-right clickable"
onClick={editing ? handleReset : () => setEditing(true)} onClick={editing ? handleReset : () => setEditing(true)}
@ -56,9 +58,10 @@ function EditableBlock({ opts, children: block }: {
{!editing ? <EditIcon /> : <i className="fas fa-undo" />} {!editing ? <EditIcon /> : <i className="fas fa-undo" />}
</div> </div>
<div ref={parent} className="d-flex flex-column align-items-center"> <div ref={parent} className="d-flex flex-column align-items-center">
{editing && <div style={{ maxWidth: "225px", marginBottom: "2rem" }}> {editing && (
{edited.type === "NP" <div style={{ maxWidth: "225px", marginBottom: "2rem" }}>
? <NPPicker {edited.type === "NP" ? (
<NPPicker
opts={opts} opts={opts}
np={edited.block} np={edited.block}
onChange={handleNPChange} onChange={handleNPChange}
@ -66,23 +69,27 @@ function EditableBlock({ opts, children: block }: {
role="subject" role="subject"
counterPart={undefined} counterPart={undefined}
phraseIsComplete={false} phraseIsComplete={false}
negative={false}
/> />
: <APPicker ) : (
<APPicker
opts={opts} opts={opts}
AP={edited.block} AP={edited.block}
onChange={handleAPChange} onChange={handleAPChange}
entryFeeder={entryFeeder} entryFeeder={entryFeeder}
phraseIsComplete={false} phraseIsComplete={false}
onRemove={() => null} onRemove={() => null}
negative={false}
/> />
} )}
</div>}
{edited.block
&& <BlockDiagram opts={opts}>
{edited.block}
</BlockDiagram>}
</div> </div>
</div>; )}
{edited.block && (
<BlockDiagram opts={opts}>{edited.block}</BlockDiagram>
)}
</div>
</div>
);
} }
export default EditableBlock; export default EditableBlock;

View File

@ -11,26 +11,113 @@ import {
export default function GoingStems() { export default function GoingStems() {
const items = [ const items = [
{"ts":1527815348,"i":3666,"p":"تلل","f":"tlul","g":"tlul","e":"to go","c":"v. intrans. irreg.","psp":"ځ","psf":"dz","ssp":"لاړ ش","ssf":"láaR sh","prp":"لاړ","prf":"láaR","ec":"go,goes,going,went,gone"}, {
{"ts":1527815216,"i":6642,"p":"راتلل","f":"raatlúl","g":"raatlul","e":"to come","c":"v. intrans. irreg.","psp":"راځ","psf":"raadz","ssp":"راش","ssf":"ráash","prp":"راغلل","prf":"ráaghlul","pprtp":"راغلی","pprtf":"raaghúley","tppp":"راغی","tppf":"ráaghey","noOo":true,"separationAtP":2,"separationAtF":3,"ec":"come,comes,coming,came,come"}, ts: 1527815348,
{"ts":1585228551150,"i":6062,"p":"درتلل","f":"dărtlul","g":"dartlul","e":"to come (to you / second person)","c":"v. intrans. irreg.","psp":"درځ","psf":"dărdz","ssp":"درش","ssf":"dársh","prp":"درغلل","prf":"dáraghlul","pprtp":"درغلی","pprtf":"dăraghúley","tppp":"درغی","tppf":"dáraghey","noOo":true,"separationAtP":2,"separationAtF":3,"ec":"come,comes,coming,came,come"}, i: 3666,
{"ts":1585228579997,"i":14282,"p":"ورتلل","f":"wărtlul","g":"wartlul","e":"to come / go over to (third person or place)","c":"v. intrans. irreg.","psp":"ورځ","psf":"wărdz","ssp":"ورش","ssf":"wársh","prp":"ورغلل","prf":"wáraghlul","pprtp":"ورغلی","pprtf":"wăraghúley","tppp":"ورغی","tppf":"wáraghey","noOo":true,"separationAtP":2,"separationAtF":3,"ec":"come,comes,coming,came,come"}, p: "تلل",
].map(entry => ({ entry })); f: "tlul",
return <Carousel stickyTitle items={items} render={(item) => { g: "tlul",
e: "to go",
c: "v. intrans. irreg.",
psp: "ځ",
psf: "dz",
ssp: "لاړ ش",
ssf: "láaR sh",
prp: "لاړ",
prf: "láaR",
ec: "go,goes,going,went,gone",
},
{
ts: 1527815216,
i: 6642,
p: "راتلل",
f: "raatlúl",
g: "raatlul",
e: "to come",
c: "v. intrans. irreg.",
psp: "راځ",
psf: "raadz",
ssp: "راش",
ssf: "ráash",
prp: "راغلل",
prf: "ráaghlul",
pprtp: "راغلی",
pprtf: "raaghúley",
tppp: "راغی",
tppf: "ráaghey",
noOo: true,
separationAtP: 2,
separationAtF: 3,
ec: "come,comes,coming,came,come",
},
{
ts: 1585228551150,
i: 6062,
p: "درتلل",
f: "dărtlul",
g: "dartlul",
e: "to come (to you / second person)",
c: "v. intrans. irreg.",
psp: "درځ",
psf: "dărdz",
ssp: "درش",
ssf: "dársh",
prp: "درغلل",
prf: "dáraghlul",
pprtp: "درغلی",
pprtf: "dăraghúley",
tppp: "درغی",
tppf: "dáraghey",
noOo: true,
separationAtP: 2,
separationAtF: 3,
ec: "come,comes,coming,came,come",
},
{
ts: 1585228579997,
i: 14282,
p: "ورتلل",
f: "wărtlul",
g: "wartlul",
e: "to come / go over to (third person or place)",
c: "v. intrans. irreg.",
psp: "ورځ",
psf: "wărdz",
ssp: "ورش",
ssf: "wársh",
prp: "ورغلل",
prf: "wáraghlul",
pprtp: "ورغلی",
pprtf: "wăraghúley",
tppp: "ورغی",
tppf: "wáraghey",
noOo: true,
separationAtP: 2,
separationAtF: 3,
ec: "come,comes,coming,came,come",
},
].map((entry) => ({ entry }));
return (
<Carousel
stickyTitle
items={items}
render={(item) => {
const rs = getAbilityRootsAndStems( const rs = getAbilityRootsAndStems(
ensureNonComboVerbInfo(getVerbInfo(item.entry)) ensureNonComboVerbInfo(getVerbInfo(item.entry))
); );
return { return {
title: <InlinePs opts={opts}> title: (
{{ <InlinePs
opts={opts}
ps={{
...removeFVarients(item.entry), ...removeFVarients(item.entry),
e: undefined, e: undefined,
}} }}
</InlinePs>, />
body: <RootsAndStems ),
textOptions={opts} body: <RootsAndStems textOptions={opts} info={rs} />,
info={rs}
/>,
}; };
}}/> }}
/>
);
} }

View File

@ -1,7 +1,5 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import { comparePs } from "../../lib/game-utils";
comparePs,
} from "../../lib/game-utils";
import GameCore from "../GameCore"; import GameCore from "../GameCore";
import { import {
Types as T, Types as T,
@ -21,21 +19,27 @@ const amount = 12;
const timeLimit = 100; const timeLimit = 100;
type Question = { type Question = {
EPS: T.EPSelectionComplete, EPS: T.EPSelectionComplete;
phrase: { ps: T.PsString[], e?: string[] }, phrase: { ps: T.PsString[]; e?: string[] };
equative: T.EquativeRendered, equative: T.EquativeRendered;
}; };
export default function EquativeGame({ inChapter, id, link, level }: { inChapter: boolean, id: string, link: string, level: T.EquativeTense | "allTenses" }) { export default function EquativeGame({
inChapter,
id,
link,
level,
}: {
inChapter: boolean;
id: string;
link: string;
level: T.EquativeTense | "allTenses";
}) {
const epsPool = randomEPSPool(level); const epsPool = randomEPSPool(level);
function getQuestion(): Question { function getQuestion(): Question {
const EPS = epsPool(); const EPS = epsPool();
const EP = renderEP(EPS); const EP = renderEP(EPS);
const compiled = compileEP( const compiled = compileEP(EP, true, { equative: true, kidsSection: true });
EP,
true,
{ equative: true, kidsSection: true },
);
const phrase = { const phrase = {
ps: compiled.ps, ps: compiled.ps,
e: compiled.e, e: compiled.e,
@ -45,35 +49,42 @@ export default function EquativeGame({ inChapter, id, link, level }: { inChapter
phrase, phrase,
equative: getEqFromRendered(EP), equative: getEqFromRendered(EP),
}; };
}; }
function Display({ question, callback }: QuestionDisplayProps<Question>) { function Display({ question, callback }: QuestionDisplayProps<Question>) {
const [answer, setAnswer] = useState<string>(""); const [answer, setAnswer] = useState<string>("");
const [withBa, setWithBa] = useState<boolean>(false); const [withBa, setWithBa] = useState<boolean>(false);
const handleInput = ({ target: { value }}: React.ChangeEvent<HTMLInputElement>) => { const handleInput = ({
target: { value },
}: React.ChangeEvent<HTMLInputElement>) => {
setAnswer(value); setAnswer(value);
} };
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
const correct = comparePs(answer, question.equative.ps) const correct =
&& (withBa === question.equative.hasBa); comparePs(answer, question.equative.ps) &&
withBa === question.equative.hasBa;
if (correct) { if (correct) {
setAnswer(""); setAnswer("");
} }
callback(correct); callback(correct);
} };
useEffect(() => { useEffect(() => {
if (level === "allTenses") setWithBa(false); if (level === "allTenses") setWithBa(false);
}, [question]); }, [question]);
return <div> return (
<div>
<div className="mb-2" style={{ maxWidth: "300px", margin: "0 auto" }}> <div className="mb-2" style={{ maxWidth: "300px", margin: "0 auto" }}>
<Examples lineHeight={1} opts={opts}> <Examples lineHeight={1} opts={opts}>
{/* @ts-ignore TODO: REMOVE AS P_INFLE */} {/* @ts-ignore TODO: REMOVE AS P_INFLE */}
{modExs(question.phrase.ps, withBa)[0]} {modExs(question.phrase.ps, withBa)[0]}
</Examples> </Examples>
{question.phrase.e && question.phrase.e.map((e, i) => ( {question.phrase.e &&
<div key={e+i} className="text-muted">{e}</div> question.phrase.e.map((e, i) => (
<div key={e + i} className="text-muted">
{e}
</div>
))} ))}
<div>{humanReadableTense(question.EPS.equative.tense)} equative</div> <div>{humanReadableTense(question.EPS.equative.tense)} equative</div>
</div> </div>
@ -84,10 +95,11 @@ export default function EquativeGame({ inChapter, id, link, level }: { inChapter
className="form-check-input" className="form-check-input"
type="checkbox" type="checkbox"
checked={withBa} checked={withBa}
onChange={e => setWithBa(e.target.checked)} onChange={(e) => setWithBa(e.target.checked)}
/> />
<label className="form-check-label text-muted" htmlFor="baCheckbox"> <label className="form-check-label text-muted" htmlFor="baCheckbox">
with <InlinePs opts={opts}>{grammarUnits.baParticle}</InlinePs> in the <span style={{ color: kidsColor }}>kids' section</span> with <InlinePs opts={opts} ps={grammarUnits.baParticle} /> in the{" "}
<span style={{ color: kidsColor }}>kids' section</span>
</label> </label>
</div> </div>
<div className="my-1" style={{ maxWidth: "200px", margin: "0 auto" }}> <div className="my-1" style={{ maxWidth: "200px", margin: "0 auto" }}>
@ -104,7 +116,9 @@ export default function EquativeGame({ inChapter, id, link, level }: { inChapter
</div> </div>
<div className="text-center my-2"> <div className="text-center my-2">
{/* <div> */} {/* <div> */}
<button className="btn btn-primary" type="submit">submit </button> <button className="btn btn-primary" type="submit">
submit
</button>
{/* </div> */} {/* </div> */}
{/* <div className="text-muted small text-center mt-2"> {/* <div className="text-muted small text-center mt-2">
Type <kbd>Enter</kbd> to check Type <kbd>Enter</kbd> to check
@ -112,18 +126,23 @@ export default function EquativeGame({ inChapter, id, link, level }: { inChapter
</div> </div>
</form> </form>
</div> </div>
);
} }
function Instructions() { function Instructions() {
return <div> return (
<div>
<p className="lead"> <p className="lead">
Fill in the blank with the correct {level === "allTenses" ? "" : humanReadableTense(level)} equative Fill in the blank with the correct{" "}
{level === "allTenses" ? "" : humanReadableTense(level)} equative
</p> </p>
{level === "allTenses" && <div> All tenses included...</div>} {level === "allTenses" && <div> All tenses included...</div>}
</div>; </div>
);
} }
return <GameCore return (
<GameCore
inChapter={inChapter} inChapter={inChapter}
studyLink={link} studyLink={link}
getQuestion={getQuestion} getQuestion={getQuestion}
@ -134,24 +153,39 @@ export default function EquativeGame({ inChapter, id, link, level }: { inChapter
amount={amount} amount={amount}
Instructions={Instructions} Instructions={Instructions}
/> />
}; );
function DisplayCorrectAnswer({ question }: { question: Question }): JSX.Element {
return <div>
<div>
{flattenLengths(question.equative.ps).reduce(((accum, curr, i): JSX.Element[] => (
[
...accum,
...i > 0 ? [<span className="text-muted"> or </span>] : [],
<span key={i}>{curr.p}</span>,
]
)), [] as JSX.Element[])}
</div>
<div><strong>{question.equative.hasBa ? "with" : "without"}</strong> a <InlinePs opts={opts}>{grammarUnits.baParticle}</InlinePs> in the kids' section.</div>
</div>;
} }
function modExs(exs: T.PsString[], withBa: boolean): { p: JSX.Element, f: JSX.Element }[] {
return exs.map(ps => { function DisplayCorrectAnswer({
question,
}: {
question: Question;
}): JSX.Element {
return (
<div>
<div>
{flattenLengths(question.equative.ps).reduce(
(accum, curr, i): JSX.Element[] => [
...accum,
...(i > 0 ? [<span className="text-muted"> or </span>] : []),
<span key={i}>{curr.p}</span>,
],
[] as JSX.Element[]
)}
</div>
<div>
<strong>{question.equative.hasBa ? "with" : "without"}</strong> a{" "}
<InlinePs opts={opts} ps={grammarUnits.baParticle} /> in the kids'
section.
</div>
</div>
);
}
function modExs(
exs: T.PsString[],
withBa: boolean
): { p: JSX.Element; f: JSX.Element }[] {
return exs.map((ps) => {
if (!ps.p.includes(" ___ ")) { if (!ps.p.includes(" ___ ")) {
return { return {
p: <>{ps.p}</>, p: <>{ps.p}</>,
@ -161,8 +195,20 @@ function modExs(exs: T.PsString[], withBa: boolean): { p: JSX.Element, f: JSX.El
const splitP = ps.p.split(" ___ "); const splitP = ps.p.split(" ___ ");
const splitF = ps.f.split(" ___ "); const splitF = ps.f.split(" ___ ");
return { return {
p: <>{splitP[0]} <span style={{ color: kidsColor }}>{withBa ? "به" : "__"}</span> {splitP[1]}</>, p: (
f: <>{splitF[0]} <span style={{ color: kidsColor }}>{withBa ? "ba" : "__"}</span> {splitF[1]}</>, <>
{splitP[0]}{" "}
<span style={{ color: kidsColor }}>{withBa ? "به" : "__"}</span>{" "}
{splitP[1]}
</>
),
f: (
<>
{splitF[0]}{" "}
<span style={{ color: kidsColor }}>{withBa ? "ba" : "__"}</span>{" "}
{splitF[1]}
</>
),
}; };
}); });
} }
@ -179,10 +225,9 @@ function humanReadableTense(tense: T.EquativeTense | "allProduce"): string {
: tense; : tense;
} }
function getEqFromRendered(e: T.EPRendered): T.EquativeRendered { function getEqFromRendered(e: T.EPRendered): T.EquativeRendered {
const eblock = e.blocks[0].find(x => x.block.type === "equative"); const eblock = e.blocks[0].find((x) => x.block.type === "equative");
if (!eblock || eblock.block.type !== "equative") throw new Error("Error getting equative block"); if (!eblock || eblock.block.type !== "equative")
throw new Error("Error getting equative block");
return eblock.block.equative; return eblock.block.equative;
} }

View File

@ -19,26 +19,32 @@ const amount = 16;
const timeLimit = 270; const timeLimit = 270;
type Question = { type Question = {
selection: T.NPSelection | T.APSelection, selection: T.NPSelection | T.APSelection;
answer: T.PsString[], answer: T.PsString[];
english: string, english: string;
}; };
type Level = "hints" | "no-hints" | "sandwiches"; type Level = "hints" | "no-hints" | "sandwiches";
const NPAdjWriting: GameSubCore<Level> = ({ inChapter, id, link, level }: { const NPAdjWriting: GameSubCore<Level> = ({
inChapter: boolean, inChapter,
id: string, id,
link: string, link,
level: Level, level,
}: {
inChapter: boolean;
id: string;
link: string;
level: Level;
}) => { }) => {
const npPool = makeNPAdjGenerator(level === "sandwiches" ? "low" : "high"); const npPool = makeNPAdjGenerator(level === "sandwiches" ? "low" : "high");
function getQuestion(): Question { function getQuestion(): Question {
const np = npPool(); const np = npPool();
const selection: T.NPSelection | T.APSelection = level === "sandwiches" const selection: T.NPSelection | T.APSelection =
? makeSandwich(np) : np; level === "sandwiches" ? makeSandwich(np) : np;
const rendered: T.Rendered<T.NPSelection> | T.Rendered<T.APSelection> = selection.type === "AP" const rendered: T.Rendered<T.NPSelection> | T.Rendered<T.APSelection> =
selection.type === "AP"
? renderAPSelection(selection, 0) // WOULD BE CLEANER IF THIS WAS JUST A PURE SANDWICH, NOT AT AP ? renderAPSelection(selection, 0) // WOULD BE CLEANER IF THIS WAS JUST A PURE SANDWICH, NOT AT AP
: renderNPSelection(np, false, false, "subject", "none", false); : renderNPSelection(np, false, false, "subject", "none", false);
const answer = getPashtoFromRendered(rendered, false); const answer = getPashtoFromRendered(rendered, false);
@ -47,7 +53,7 @@ const NPAdjWriting: GameSubCore<Level> = ({ inChapter, id, link, level }: {
answer, answer,
english: getEnglishFromRendered(rendered) || "ERROR", english: getEnglishFromRendered(rendered) || "ERROR",
}; };
}; }
function Display({ question, callback }: QuestionDisplayProps<Question>) { function Display({ question, callback }: QuestionDisplayProps<Question>) {
const [answer, setAnswer] = useState<string>(""); const [answer, setAnswer] = useState<string>("");
@ -58,30 +64,43 @@ const NPAdjWriting: GameSubCore<Level> = ({ inChapter, id, link, level }: {
setAnswer(""); setAnswer("");
} }
callback(correct); callback(correct);
};
if (
!(
(question.selection.type === "AP" &&
question.selection.selection.type === "sandwich" &&
question.selection.selection.inside.selection.type === "noun") ||
(question.selection.type === "NP" &&
question.selection.selection.type === "noun")
)
) {
throw new Error("QUESTION ERROR - BAD SELECTION");
} }
if (!( const nounSelection: T.NounSelection =
(question.selection.type === "AP" && question.selection.selection.type === "sandwich" && question.selection.selection.inside.selection.type === "noun") question.selection.type === "AP"
|| ? (question.selection.selection.inside.selection as T.NounSelection) // ts being dumb
(question.selection.type === "NP" && question.selection.selection.type === "noun")
)) {
throw new Error("QUESTION ERROR - BAD SELECTION")
}
const nounSelection: T.NounSelection = question.selection.type === "AP"
? question.selection.selection.inside.selection as T.NounSelection // ts being dumb
: question.selection.selection; : question.selection.selection;
const adjEntry: T.AdjectiveEntry | undefined = nounSelection.adjectives[0]?.entry; const adjEntry: T.AdjectiveEntry | undefined =
nounSelection.adjectives[0]?.entry;
if (!adjEntry) { if (!adjEntry) {
throw new Error("QUESTION ERROR - MISSING ADJECTIVE"); throw new Error("QUESTION ERROR - MISSING ADJECTIVE");
} }
const handleInput = ({ target: { value }}: React.ChangeEvent<HTMLInputElement>) => { const handleInput = ({
target: { value },
}: React.ChangeEvent<HTMLInputElement>) => {
setAnswer(value); setAnswer(value);
} };
const sandwich = question.selection.type === "AP" const sandwich =
question.selection.type === "AP"
? question.selection.selection ? question.selection.selection
: undefined; : undefined;
return <div> return (
<div>
<div className="my-2" style={{ maxWidth: "300px", margin: "0 auto" }}> <div className="my-2" style={{ maxWidth: "300px", margin: "0 auto" }}>
<div className="d-flex flex-row justify-content-center" style={{ gap: "1rem"}}> <div
className="d-flex flex-row justify-content-center"
style={{ gap: "1rem" }}
>
<WordCard <WordCard
showHint={level === "hints"} showHint={level === "hints"}
entry={adjEntry} entry={adjEntry}
@ -93,16 +112,20 @@ const NPAdjWriting: GameSubCore<Level> = ({ inChapter, id, link, level }: {
selection={nounSelection} selection={nounSelection}
/> />
</div> </div>
{sandwich && <div className="mt-2"> {sandwich && (
<InlinePs opts={opts}> <div className="mt-2">
{concatPsString(sandwich.before, " ... ", sandwich.after)} <InlinePs
</InlinePs> opts={opts}
</div>} ps={concatPsString(sandwich.before, " ... ", sandwich.after)}
<div className="my-3 h5"> />
{question.english}
</div> </div>
)}
<div className="my-3 h5">{question.english}</div>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="mt-2 mb-1" style={{ maxWidth: "200px", margin: "0 auto" }}> <div
className="mt-2 mb-1"
style={{ maxWidth: "200px", margin: "0 auto" }}
>
<input <input
type="text" type="text"
className="form-control" className="form-control"
@ -115,20 +138,29 @@ const NPAdjWriting: GameSubCore<Level> = ({ inChapter, id, link, level }: {
/> />
</div> </div>
<div className="text-center my-3"> <div className="text-center my-3">
<button className="btn btn-primary" type="submit">submit </button> <button className="btn btn-primary" type="submit">
submit
</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
);
} }
function Instructions() { function Instructions() {
return <div> return (
<p className="lead">Write the {level === "sandwiches" ? "sandwich including the" : ""} adjective and noun together with the proper inflections.</p> <div>
<p className="lead">
Write the {level === "sandwiches" ? "sandwich including the" : ""}{" "}
adjective and noun together with the proper inflections.
</p>
</div> </div>
);
} }
return <GameCore return (
<GameCore
inChapter={inChapter} inChapter={inChapter}
studyLink={link} studyLink={link}
getQuestion={getQuestion} getQuestion={getQuestion}
@ -139,21 +171,31 @@ const NPAdjWriting: GameSubCore<Level> = ({ inChapter, id, link, level }: {
amount={amount} amount={amount}
Instructions={Instructions} Instructions={Instructions}
/> />
);
}; };
function DisplayCorrectAnswer({ question }: { question: Question }): JSX.Element { function DisplayCorrectAnswer({
question,
}: {
question: Question;
}): JSX.Element {
// TODO: extract this reduces for the or, or different variations, used in VerbGame (etc.?) // TODO: extract this reduces for the or, or different variations, used in VerbGame (etc.?)
return <div> return (
<div> <div>
{question.answer.reduce(((accum, curr, i): JSX.Element[] => ( <div>
[ {question.answer.reduce(
(accum, curr, i): JSX.Element[] => [
...accum, ...accum,
...i > 0 ? [<span className="text-muted"> or </span>] : [], ...(i > 0 ? [<span className="text-muted"> or </span>] : []),
<span key={i}>{curr.p} - {curr.f}</span>, <span key={i}>
] {curr.p} - {curr.f}
)), [] as JSX.Element[])} </span>,
],
[] as JSX.Element[]
)}
</div> </div>
</div>; </div>
);
} }
export default NPAdjWriting; export default NPAdjWriting;

View File

@ -270,8 +270,8 @@ const VerbGame: GameSubCore<PerfectGameLevel> = ({
className="form-check-label text-muted" className="form-check-label text-muted"
htmlFor="baCheckbox" htmlFor="baCheckbox"
> >
with <InlinePs opts={opts}>{grammarUnits.baParticle}</InlinePs>{" "} with <InlinePs opts={opts} ps={grammarUnits.baParticle} /> in
in the <span style={{ color: kidsColor }}>kids' section</span> the <span style={{ color: kidsColor }}>kids' section</span>
</label> </label>
</div> </div>
)} )}
@ -352,7 +352,7 @@ function DisplayCorrectAnswer({
</div> </div>
<div> <div>
<strong>{verbHasBa(question.rendered) ? "with" : "without"}</strong> a{" "} <strong>{verbHasBa(question.rendered) ? "with" : "without"}</strong> a{" "}
<InlinePs opts={opts}>{grammarUnits.baParticle}</InlinePs> in the kids' <InlinePs opts={opts} ps={grammarUnits.baParticle} /> in the kids'
section. section.
</div> </div>
</div> </div>

View File

@ -14,19 +14,23 @@ import { isImperativeTense } from "@lingdocs/ps-react/dist/lib/src/type-predicat
const amount = 12; const amount = 12;
const timeLimit = 125; const timeLimit = 125;
type StemRoot = "imperfective stem" | "perfective stem" | "imperfective root" | "perfective root"; type StemRoot =
| "imperfective stem"
| "perfective stem"
| "imperfective root"
| "perfective root";
type Ending = "present" | "past" | "imperative"; type Ending = "present" | "past" | "imperative";
type Formula = { type Formula = {
ba: boolean, ba: boolean;
mu: boolean, mu: boolean;
stemRoot: StemRoot, stemRoot: StemRoot;
ending: Ending, ending: Ending;
}; };
type Question = { type Question = {
tense: T.VerbTense | T.ImperativeTense | "negative imperative", tense: T.VerbTense | T.ImperativeTense | "negative imperative";
formula: Formula, formula: Formula;
} };
const questions: Question[] = [ const questions: Question[] = [
{ {
@ -127,10 +131,20 @@ const questions: Question[] = [
stemRoot: "imperfective stem", stemRoot: "imperfective stem",
ending: "imperative", ending: "imperative",
}, },
} },
]; ];
export default function VerbFormulas({ inChapter, id, link, level }: { inChapter: boolean, id: string, link: string, level: "all" }) { export default function VerbFormulas({
inChapter,
id,
link,
level,
}: {
inChapter: boolean;
id: string;
link: string;
level: "all";
}) {
const questionsPool = makePool(questions); const questionsPool = makePool(questions);
function getQuestion() { function getQuestion() {
return questionsPool(); return questionsPool();
@ -138,7 +152,8 @@ export default function VerbFormulas({ inChapter, id, link, level }: { inChapter
function Instructions() { function Instructions() {
return <p className="lead">Pick the formula for each verb tense</p>; return <p className="lead">Pick the formula for each verb tense</p>;
} }
return <GameCore return (
<GameCore
inChapter={inChapter} inChapter={inChapter}
studyLink={link} studyLink={link}
getQuestion={getQuestion} getQuestion={getQuestion}
@ -149,7 +164,8 @@ export default function VerbFormulas({ inChapter, id, link, level }: { inChapter
amount={amount} amount={amount}
Instructions={Instructions} Instructions={Instructions}
/> />
}; );
}
function Display({ question, callback }: QuestionDisplayProps<Question>) { function Display({ question, callback }: QuestionDisplayProps<Question>) {
const [ba, setBa] = useState<boolean>(false); const [ba, setBa] = useState<boolean>(false);
@ -163,24 +179,22 @@ function Display({ question, callback }: QuestionDisplayProps<Question>) {
setMu(false); setMu(false);
}, [question]); }, [question]);
const canSubmit = !!(stemRoot && ending); const canSubmit = !!(stemRoot && ending);
const showMu = question.tense === "negative imperative" || isImperativeTense(question.tense); const showMu =
question.tense === "negative imperative" ||
isImperativeTense(question.tense);
function handleSubmit() { function handleSubmit() {
const { formula } = question; const { formula } = question;
callback( callback(
(ba === formula.ba) ba === formula.ba &&
&& mu === formula.mu &&
(mu === formula.mu) stemRoot === formula.stemRoot &&
&& ending === formula.ending
(stemRoot === formula.stemRoot)
&&
(ending === formula.ending)
); );
} }
return <div> return (
<div>
<div className="my-4" style={{ maxWidth: "300px", margin: "0 auto" }}> <div className="my-4" style={{ maxWidth: "300px", margin: "0 auto" }}>
<p className="lead"> <p className="lead">{humanReadableT(question.tense)}</p>
{humanReadableT(question.tense)}
</p>
</div> </div>
<div className="text-center mb-2"> <div className="text-center mb-2">
<BaPicker value={ba} onChange={setBa} /> <BaPicker value={ba} onChange={setBa} />
@ -197,38 +211,58 @@ function Display({ question, callback }: QuestionDisplayProps<Question>) {
</div> </div>
<samp>{printFormula({ ba, stemRoot, ending, mu })}</samp> <samp>{printFormula({ ba, stemRoot, ending, mu })}</samp>
</div> </div>
);
} }
function humanReadableT(tense: T.VerbTense | T.ImperativeTense | "negative imperative"): string { function humanReadableT(
tense: T.VerbTense | T.ImperativeTense | "negative imperative"
): string {
if (tense === "negative imperative") { if (tense === "negative imperative") {
return tense; return tense;
} }
return humanReadableVerbForm(tense); return humanReadableVerbForm(tense);
} }
function printFormula({ mu, ba, stemRoot, ending }: { function printFormula({
mu: boolean, mu,
ba: boolean, ba,
stemRoot: StemRoot | "", stemRoot,
ending: Ending | "", ending,
}: {
mu: boolean;
ba: boolean;
stemRoot: StemRoot | "";
ending: Ending | "";
}): string { }): string {
const elements = [ const elements = [
mu ? "mu" : undefined, mu ? "mu" : undefined,
ba ? "ba" : undefined, ba ? "ba" : undefined,
stemRoot, stemRoot,
ending ? `${ending} ending` : undefined, ending ? `${ending} ending` : undefined,
].filter(x => x) as string[]; ].filter((x) => x) as string[];
return elements.join(" + "); return elements.join(" + ");
} }
function DisplayCorrectAnswer({ question }: { question: Question }): JSX.Element { function DisplayCorrectAnswer({
return <div> question,
}: {
question: Question;
}): JSX.Element {
return (
<div>
<samp>{printFormula(question.formula)}</samp> <samp>{printFormula(question.formula)}</samp>
</div>; </div>
);
} }
function EndingPicker({ onChange, value }: { value: Ending | "", onChange: (e: Ending | "") => void }) { function EndingPicker({
const options: { label: string, value: Ending }[] = [ onChange,
value,
}: {
value: Ending | "";
onChange: (e: Ending | "") => void;
}) {
const options: { label: string; value: Ending }[] = [
{ label: "Present", value: "present" }, { label: "Present", value: "present" },
{ label: "Past", value: "past" }, { label: "Past", value: "past" },
{ label: "Imperative", value: "imperative" }, { label: "Imperative", value: "imperative" },
@ -237,58 +271,81 @@ function EndingPicker({ onChange, value }: { value: Ending | "", onChange: (e: E
// onChange(ending === e ? "" : e); // onChange(ending === e ? "" : e);
onChange(e); onChange(e);
} }
return <div className="my-3"> return (
<div className="my-3">
<span className="mr-2">Ending:</span> <span className="mr-2">Ending:</span>
<div className="btn-group"> <div className="btn-group">
{options.map((option) => ( {options.map((option) => (
<button <button
key={option.value} key={option.value}
type="button" type="button"
className={classNames( className={classNames("btn", "btn-outline-secondary", {
"btn", active: value === option.value,
"btn-outline-secondary", })}
{ active: value === option.value },
)}
onClick={() => handleClick(option.value)} onClick={() => handleClick(option.value)}
> >
{option.label} {option.label}
</button> </button>
))} ))}
</div> </div>
</div>; </div>
);
} }
function BaPicker({ onChange, value }: { value: boolean, onChange: (b: boolean) => void }) { function BaPicker({
return <div className="form-check my-3" style={{ fontSize: "larger" }}> onChange,
value,
}: {
value: boolean;
onChange: (b: boolean) => void;
}) {
return (
<div className="form-check my-3" style={{ fontSize: "larger" }}>
<input <input
id="baCheckbox" id="baCheckbox"
className="form-check-input" className="form-check-input"
type="checkbox" type="checkbox"
checked={value} checked={value}
onChange={e => onChange(e.target.checked)} onChange={(e) => onChange(e.target.checked)}
/> />
<label className="form-check-label text-muted" htmlFor="baCheckbox"> <label className="form-check-label text-muted" htmlFor="baCheckbox">
with <InlinePs opts={opts}>{grammarUnits.baParticle}</InlinePs> in the kids' section with <InlinePs opts={opts} ps={grammarUnits.baParticle} /> in the kids'
section
</label> </label>
</div> </div>
);
} }
function MuPicker({ onChange, value }: { value: boolean, onChange: (b: boolean) => void }) { function MuPicker({
return <div className="form-check my-3" style={{ fontSize: "larger" }}> onChange,
value,
}: {
value: boolean;
onChange: (b: boolean) => void;
}) {
return (
<div className="form-check my-3" style={{ fontSize: "larger" }}>
<input <input
id="baCheckbox" id="baCheckbox"
className="form-check-input" className="form-check-input"
type="checkbox" type="checkbox"
checked={value} checked={value}
onChange={e => onChange(e.target.checked)} onChange={(e) => onChange(e.target.checked)}
/> />
<label className="form-check-label text-muted" htmlFor="baCheckbox"> <label className="form-check-label text-muted" htmlFor="baCheckbox">
with <InlinePs opts={opts}>{{ p: "مه", f: "mú" }}</InlinePs> with <InlinePs opts={opts} ps={{ p: "مه", f: "mú" }} />
</label> </label>
</div> </div>
);
} }
function RootsAndStemsPicker({ onChange, value }: { value: StemRoot | "", onChange: (s: StemRoot | "" ) => void }) { function RootsAndStemsPicker({
onChange,
value,
}: {
value: StemRoot | "";
onChange: (s: StemRoot | "") => void;
}) {
const colClass = "col col-md-5 px-0 mb-1"; const colClass = "col col-md-5 px-0 mb-1";
const rowClass = "row justify-content-between"; const rowClass = "row justify-content-between";
const title: CSSProperties = { const title: CSSProperties = {
@ -302,7 +359,10 @@ function RootsAndStemsPicker({ onChange, value }: { value: StemRoot | "", onChan
function handleStemRootClick(s: StemRoot) { function handleStemRootClick(s: StemRoot) {
onChange(value === s ? "" : s); onChange(value === s ? "" : s);
} }
return <div className="verb-info" style={{ return (
<div
className="verb-info"
style={{
textAlign: "center", textAlign: "center",
maxWidth: "400px", maxWidth: "400px",
margin: "0 auto", margin: "0 auto",
@ -310,12 +370,16 @@ function RootsAndStemsPicker({ onChange, value }: { value: StemRoot | "", onChan
backgroundRepeat: "no-repeat", backgroundRepeat: "no-repeat",
backgroundPosition: "50% 45%", backgroundPosition: "50% 45%",
backgroundSize: "50%", backgroundSize: "50%",
}}> }}
<div style={{ >
<div
style={{
border: "2px solid black", border: "2px solid black",
padding: "1rem", padding: "1rem",
margin: "0.25rem", margin: "0.25rem",
}} className="container"> }}
className="container"
>
<div className={rowClass + " align-items-center"}> <div className={rowClass + " align-items-center"}>
<div className={colClass}> <div className={colClass}>
<i className="fas fa-video fa-lg" /> <i className="fas fa-video fa-lg" />
@ -329,32 +393,60 @@ function RootsAndStemsPicker({ onChange, value }: { value: StemRoot | "", onChan
</div> </div>
</div> </div>
<div className={rowClass}> <div className={rowClass}>
<div className={colClass} style={value === "imperfective stem" ? highlight : {}}> <div
<div className="clickable" style={title} onClick={() => handleStemRootClick("imperfective stem")}> className={colClass}
style={value === "imperfective stem" ? highlight : {}}
>
<div
className="clickable"
style={title}
onClick={() => handleStemRootClick("imperfective stem")}
>
<div>Imperfective Stem</div> <div>Imperfective Stem</div>
</div> </div>
</div> </div>
<div className={colClass} style={value === "perfective stem" ? highlight : {}}> <div
<div className="clickable" style={title} onClick={() => handleStemRootClick("perfective stem")}> className={colClass}
style={value === "perfective stem" ? highlight : {}}
>
<div
className="clickable"
style={title}
onClick={() => handleStemRootClick("perfective stem")}
>
<div>Perfective Stem</div> <div>Perfective Stem</div>
</div> </div>
</div> </div>
</div> </div>
<div className={rowClass}> <div className={rowClass}>
<div className={colClass} style={value === "imperfective root" ? highlight : {}}> <div
<div className="clickable" style={title} onClick={() => handleStemRootClick("imperfective root")}> className={colClass}
style={value === "imperfective root" ? highlight : {}}
>
<div
className="clickable"
style={title}
onClick={() => handleStemRootClick("imperfective root")}
>
<div>Imperfective Root</div> <div>Imperfective Root</div>
</div> </div>
</div> </div>
<div className={colClass} style={value === "perfective root" ? highlight : {}}> <div
className={colClass}
style={value === "perfective root" ? highlight : {}}
>
<div> <div>
<div className="clickable" style={title} onClick={() => handleStemRootClick("perfective root")}> <div
className="clickable"
style={title}
onClick={() => handleStemRootClick("perfective root")}
>
<div>Perfective Root</div> <div>Perfective Root</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>; </div>
);
} }

View File

@ -276,8 +276,8 @@ const VerbGame: GameSubCore<VerbGameLevel> = ({
onChange={(e) => setWithBa(e.target.checked)} onChange={(e) => setWithBa(e.target.checked)}
/> />
<label className="form-check-label text-muted" htmlFor="baCheckbox"> <label className="form-check-label text-muted" htmlFor="baCheckbox">
with <InlinePs opts={opts}>{grammarUnits.baParticle}</InlinePs> in with <InlinePs opts={opts} ps={grammarUnits.baParticle} /> in the{" "}
the <span style={{ color: kidsColor }}>kids' section</span> <span style={{ color: kidsColor }}>kids' section</span>
</label> </label>
</div> </div>
<div className="my-1" style={{ maxWidth: "200px", margin: "0 auto" }}> <div className="my-1" style={{ maxWidth: "200px", margin: "0 auto" }}>
@ -400,7 +400,7 @@ function DisplayCorrectAnswer({
</div> </div>
<div> <div>
<strong>{verbHasBa(question.rendered) ? "with" : "without"}</strong> a{" "} <strong>{verbHasBa(question.rendered) ? "with" : "without"}</strong> a{" "}
<InlinePs opts={opts}>{grammarUnits.baParticle}</InlinePs> in the kids' <InlinePs opts={opts} ps={grammarUnits.baParticle} /> in the kids'
section. section.
</div> </div>
</div> </div>

View File

@ -1597,10 +1597,10 @@
rambda "^6.7.0" rambda "^6.7.0"
react-select "^5.2.2" react-select "^5.2.2"
"@lingdocs/ps-react@^7.7.3": "@lingdocs/ps-react@^7.7.4":
version "7.7.3" version "7.7.4"
resolved "https://npm.lingdocs.com/@lingdocs/ps-react/-/ps-react-7.7.3.tgz#353b6ddabdfabb4eae45a1d8554adb360f7238fc" resolved "https://npm.lingdocs.com/@lingdocs/ps-react/-/ps-react-7.7.4.tgz#6529c1728828cce6d9e550a0323463bd2ae55ffc"
integrity sha512-TSK5kgyCEHTmJRH+9XfchVPUVvsjOfsLlrZlW4PL95LhvBD1dh5CazZDQlPV99CLZGz3jVf9VKpiQP7RdGYTrg== integrity sha512-K7SnvOXITl3DCY9l4ifKKnjC6Uw3AwvjWlV87l+YtboCG7B277iYhdVrb8/6bxzUhGuSAtSGx982bN7gSuKIxA==
dependencies: dependencies:
"@formkit/auto-animate" "^1.0.0-beta.3" "@formkit/auto-animate" "^1.0.0-beta.3"
classnames "^2.5.1" classnames "^2.5.1"