392 lines
14 KiB
TypeScript
392 lines
14 KiB
TypeScript
/**
|
|
* Copyright (c) 2021 lingdocs.com
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
*/
|
|
|
|
import { useEffect, useReducer } from "react";
|
|
import VerbInfo from "./verb-info/VerbInfo";
|
|
import VerbFormDisplay from "./VerbFormDisplay";
|
|
import ButtonSelect from "./ButtonSelect";
|
|
import Hider from "./Hider";
|
|
import { getForms } from "../lib/conjugation-forms";
|
|
import PersonSelection from "./PersonSelection";
|
|
import {
|
|
personIsAllowed,
|
|
randomPerson,
|
|
incrementPerson,
|
|
parseEc,
|
|
} from "../lib/misc-helpers";
|
|
import * as T from "../types";
|
|
|
|
const VerbChoiceWarning = () => (
|
|
<>
|
|
<div className="alert alert-warning d-block mx-auto mt-3" role="alert" style={{ maxWidth: "500px" }}>
|
|
<i className="fas fa-exclamation-triangle" /> This verb can be used in different ways!
|
|
</div>
|
|
<p>Choose which way to use it:</p>
|
|
</>
|
|
);
|
|
|
|
const stateLocalStorageName = "explorerState6";
|
|
|
|
type Difficulty = "beginner" | "advanced";
|
|
// remember to increment the stateLocalStorageName whenever changing
|
|
// the State type
|
|
type State = {
|
|
mode: "chart" | "sentence";
|
|
subject: T.Person,
|
|
object: T.Person,
|
|
negative: boolean,
|
|
compoundTypeSelected: "stative" | "dynamic";
|
|
transitivitySelected: "transitive" | "grammatically transitive";
|
|
compoundComplementVersionSelected: "sing" | "plur";
|
|
showingStemsAndRoots: boolean;
|
|
difficulty: Difficulty;
|
|
formsOpened: string[];
|
|
showingFormInfo: boolean;
|
|
}
|
|
|
|
type Action = {
|
|
type: "choose compound type",
|
|
payload: "stative" | "dynamic",
|
|
} | {
|
|
type: "set forms opened",
|
|
payload: string,
|
|
} | {
|
|
type: "set difficulty",
|
|
payload: Difficulty,
|
|
} | {
|
|
type: "set compound complement version",
|
|
payload: "sing" | "plur",
|
|
} | {
|
|
type: "toggle showingStemsAndRoots",
|
|
} | {
|
|
type: "setState",
|
|
payload: State,
|
|
} | {
|
|
type: "setMode",
|
|
payload: "chart" | "sentence",
|
|
} | {
|
|
type: "setPerson",
|
|
payload: { setting: "subject" | "object", person: T.Person },
|
|
} | {
|
|
type: "randomPerson",
|
|
payload: "subject" | "object",
|
|
} | {
|
|
type: "setShowingFormInfo",
|
|
payload: boolean,
|
|
} | {
|
|
type: "setTransitivitySelected",
|
|
payload: "transitive" | "grammatically transitive",
|
|
} | {
|
|
type: "setNegative",
|
|
payload: boolean,
|
|
};
|
|
|
|
function oppositeRole(x: "subject" | "object"): "subject" | "object" {
|
|
return x === "subject" ? "object" : "subject";
|
|
}
|
|
|
|
function reducer(state: State, action: Action): State {
|
|
function applyFormOpen(
|
|
payload: string,
|
|
formsOpened: string[],
|
|
): string[] {
|
|
if (formsOpened.includes(payload)) {
|
|
return formsOpened.filter((f) => f !== payload);
|
|
}
|
|
return [...formsOpened, payload];
|
|
}
|
|
function setPerson({ setting, person }: { setting: "subject" | "object", person: T.Person }): State {
|
|
let newPerson = person;
|
|
let otherPerson = state[oppositeRole(setting)];
|
|
let otherSetting = oppositeRole(setting);
|
|
while (!personIsAllowed(newPerson, otherPerson)) {
|
|
otherPerson = incrementPerson(otherPerson);
|
|
}
|
|
return { ...state, [setting]: newPerson, [otherSetting]: otherPerson };
|
|
}
|
|
|
|
switch(action.type) {
|
|
case "choose compound type":
|
|
return { ...state, compoundTypeSelected: action.payload };
|
|
case "set forms opened":
|
|
return {
|
|
...state,
|
|
formsOpened: applyFormOpen(action.payload, state.formsOpened),
|
|
};
|
|
case "set difficulty":
|
|
return { ...state, difficulty: action.payload };
|
|
case "set compound complement version":
|
|
return { ...state, compoundComplementVersionSelected: action.payload };
|
|
case "toggle showingStemsAndRoots":
|
|
return { ...state, showingStemsAndRoots: !state.showingStemsAndRoots };
|
|
case "setState":
|
|
return action.payload;
|
|
case "setMode":
|
|
return { ...state, mode: action.payload };
|
|
case "setPerson":
|
|
return setPerson(action.payload);
|
|
case "randomPerson":
|
|
return {
|
|
...state,
|
|
[action.payload]: randomPerson(
|
|
state[action.payload === "subject" ? "object" : "subject"]
|
|
),
|
|
};
|
|
case "setShowingFormInfo":
|
|
return {
|
|
...state,
|
|
showingFormInfo: action.payload,
|
|
};
|
|
case "setTransitivitySelected":
|
|
return {
|
|
...state,
|
|
transitivitySelected: action.payload,
|
|
};
|
|
case "setNegative":
|
|
return {
|
|
...state,
|
|
negative: action.payload,
|
|
};
|
|
default:
|
|
throw new Error();
|
|
}
|
|
}
|
|
|
|
const initialState: State = {
|
|
subject: 0,
|
|
object: 2,
|
|
negative: false,
|
|
compoundTypeSelected: "stative",
|
|
transitivitySelected: "transitive",
|
|
mode: "chart",
|
|
compoundComplementVersionSelected: "plur",
|
|
difficulty: "beginner",
|
|
showingStemsAndRoots: false,
|
|
showingFormInfo: false,
|
|
formsOpened: [],
|
|
};
|
|
|
|
function ConjugationViewer({ conjugation, textOptions, ec, ep }: {
|
|
conjugation: T.VerbOutput,
|
|
textOptions: T.TextOptions,
|
|
ec?: string | undefined,
|
|
ep?: string | undefined,
|
|
}) {
|
|
const [state, dispatch] = useReducer(reducer, initialState);
|
|
useEffect(() => {
|
|
const stateRaw = localStorage.getItem(stateLocalStorageName);
|
|
if (stateRaw) {
|
|
try {
|
|
const payload = JSON.parse(stateRaw) as State;
|
|
dispatch({ type: "setState", payload });
|
|
} catch (e) {
|
|
console.error("error parsing saved state JSON", e);
|
|
}
|
|
}
|
|
}, []);
|
|
const verbConj1 = ("dynamic" in conjugation)
|
|
? conjugation[state.compoundTypeSelected]
|
|
: ("transitive" in conjugation)
|
|
? conjugation[state.transitivitySelected === "transitive" ? "transitive" : "grammaticallyTransitive"]
|
|
: conjugation;
|
|
const verbConj = (verbConj1.singularForm && state.compoundComplementVersionSelected === "sing")
|
|
? verbConj1.singularForm
|
|
: verbConj1;
|
|
const englishConjugation: T.EnglishVerbConjugation | undefined = ec ? {
|
|
ec: parseEc(ec),
|
|
ep: ep,
|
|
} : undefined;
|
|
|
|
useEffect(() => {
|
|
localStorage.setItem(stateLocalStorageName, JSON.stringify(state));
|
|
});
|
|
|
|
const filterDifficulty = (f: T.DisplayForm): boolean => (
|
|
state.difficulty === "advanced" || !f.advanced
|
|
);
|
|
const forms = getForms({
|
|
conj: verbConj,
|
|
filterFunc: filterDifficulty,
|
|
mode: state.mode,
|
|
subject: state.subject,
|
|
object: state.object,
|
|
negative: state.negative,
|
|
englishConjugation,
|
|
});
|
|
return <div className="mb-4">
|
|
{"transitive" in conjugation && <div className="text-center my-2">
|
|
<VerbChoiceWarning />
|
|
<div>
|
|
<ButtonSelect
|
|
options={[
|
|
{
|
|
label: "Transitive", value: "transitive" },
|
|
{
|
|
label: "Grammatically Transitive",
|
|
value: "grammatically transitive",
|
|
},
|
|
]}
|
|
value={state.transitivitySelected}
|
|
handleChange={(p) => dispatch({ type: "setTransitivitySelected", payload: p as "transitive" | "grammatically transitive" })}
|
|
/>
|
|
</div>
|
|
</div>}
|
|
{"dynamic" in conjugation && <div className="text-center my-2">
|
|
<VerbChoiceWarning />
|
|
<div>
|
|
<ButtonSelect
|
|
options={[
|
|
{ label: "Dynamic", value: "dynamic" },
|
|
{
|
|
label: `${conjugation.info.type === "dynamic or generative stative compound" ? "Generative " : ""}Stative`,
|
|
value: "stative",
|
|
},
|
|
]}
|
|
value={state.compoundTypeSelected}
|
|
handleChange={(p) => dispatch({ type: "choose compound type", payload: p as "dynamic" | "stative" })}
|
|
/>
|
|
</div>
|
|
</div>}
|
|
{verbConj1.singularForm && <div className="text-center my-2">
|
|
<VerbChoiceWarning />
|
|
<div>
|
|
<ButtonSelect
|
|
small
|
|
options={[
|
|
// @ts-ignore - TODO - make this a bit safer
|
|
{ label: `Sing. ${verbConj1.info.objComplement.entry.p}`, value: "sing" },
|
|
// @ts-ignore - TODO - make this a bit safer
|
|
{ label: `Plur. ${verbConj1.info.objComplement.plural.p}`, value: "plur" },
|
|
]}
|
|
value={state.compoundComplementVersionSelected}
|
|
handleChange={(p) => dispatch({ type: "set compound complement version", payload: p as "sing" | "plur" })}
|
|
/>
|
|
</div>
|
|
</div>}
|
|
<VerbInfo
|
|
info={verbConj.info}
|
|
textOptions={textOptions}
|
|
showingStemsAndRoots={state.showingStemsAndRoots}
|
|
toggleShowingSar={() => dispatch({ type: "toggle showingStemsAndRoots" })}
|
|
/>
|
|
<div className="d-flex flex-row align-items-center justify-content-around flex-wrap mt-4 mb-2">
|
|
<div className="mb-3">
|
|
<ButtonSelect
|
|
options={[
|
|
{ label: `Charts`, value: "chart" },
|
|
{ label: `Sentences`, value: "sentence" },
|
|
]}
|
|
value={state.mode}
|
|
handleChange={(p) => dispatch({ type: "setMode", payload: p as "chart" | "sentence" })}
|
|
/>
|
|
</div>
|
|
<div className="mb-3">
|
|
<ButtonSelect
|
|
options={[
|
|
{ label: "👶 Beginner", value: "beginner" },
|
|
{ label: "🤓 Advanced", value: "advanced" },
|
|
]}
|
|
value={state.difficulty}
|
|
handleChange={(p) => dispatch({ type: "set difficulty", payload: p as Difficulty })}
|
|
/>
|
|
</div>
|
|
<div className="form-group form-check">
|
|
<input
|
|
type="checkbox"
|
|
className="form-check-input"
|
|
checked={state.showingFormInfo}
|
|
onChange={(e) => {
|
|
dispatch({ type: "setShowingFormInfo", payload: e.target.checked })
|
|
}}
|
|
/>
|
|
<label className="form-check-label">Show Form Info</label>
|
|
</div>
|
|
</div>
|
|
{state.mode === "sentence" &&
|
|
<div className="position-sticky pb-1" style={{ top: 0, background: "var(--theme-shade)", zIndex: 1000 }}>
|
|
<PersonSelection
|
|
subject={state.subject}
|
|
object={state.object}
|
|
info={verbConj.info}
|
|
handleChange={(payload: { setting: "subject" | "object", person: T.Person }) => dispatch({
|
|
type: "setPerson", payload,
|
|
})}
|
|
handleRandom={(payload: "subject" | "object") => dispatch({
|
|
type: "randomPerson", payload,
|
|
})}
|
|
textOptions={textOptions}
|
|
/>
|
|
<div className="mt-2 text-center">
|
|
<ButtonSelect
|
|
options={[
|
|
{ label: "Pos.", value: "false" },
|
|
{ label: "Neg.", value: "true" },
|
|
]}
|
|
value={state.negative.toString()}
|
|
handleChange={(p) => dispatch({ type: "setNegative", payload: p === "true" })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
}
|
|
<FormsDisplay
|
|
forms={forms}
|
|
state={state}
|
|
handleChange={(payload: string) => dispatch({ type: "set forms opened", payload })}
|
|
verbConj={verbConj}
|
|
textOptions={textOptions}
|
|
/>
|
|
</div>;
|
|
}
|
|
|
|
function FormsDisplay({ forms, state, handleChange, textOptions, verbConj }: {
|
|
verbConj: T.VerbConjugation,
|
|
forms: T.DisplayFormItem[],
|
|
state: State,
|
|
handleChange: (p: string) => void,
|
|
textOptions: T.TextOptions,
|
|
}) {
|
|
function drawLevel(forms: T.DisplayFormItem[], level: number) {
|
|
return <div className="mt-3">
|
|
{forms.map((f, i) => {
|
|
if ("content" in f && f.content.length === 0) {
|
|
return null;
|
|
}
|
|
const showDividerLine = (item: T.DisplayFormItem, index: number): boolean => {
|
|
return (index !== 0) && ("content" in item || !item.aspect || (item.aspect === "imperfective"));
|
|
};
|
|
return <div key={`level1-${i}`}>
|
|
{showDividerLine(f, i) && <hr />}
|
|
<Hider
|
|
label={f.label}
|
|
hLevel={5+level}
|
|
aspect={"aspect" in f ? f.aspect : undefined}
|
|
showing={state.formsOpened.includes(f.label)}
|
|
handleChange={() => handleChange(f.label)}
|
|
>
|
|
{"content" in f ?
|
|
drawLevel(f.content, level + 1)
|
|
:
|
|
<VerbFormDisplay
|
|
displayForm={f}
|
|
textOptions={textOptions}
|
|
showingFormInfo={state.showingFormInfo}
|
|
info={verbConj.info}
|
|
/>
|
|
}
|
|
</Hider>
|
|
</div>;
|
|
})}
|
|
</div>
|
|
}
|
|
return <div style={{ marginBottom: "5rem" }}>
|
|
{drawLevel(forms, 0)}
|
|
</div>;
|
|
}
|
|
|
|
export default ConjugationViewer;
|