pashto-dictionary/website/src/screens/EntryEditor.tsx

448 lines
15 KiB
TypeScript
Raw Normal View History

2024-08-14 19:30:10 +00:00
/**
* Copyright (c) 2021 lingdocs.com
*
* This source code is licensed under the GPL3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { useEffect, useState } from "react";
import classNames from "classnames";
import { Link } from "react-router-dom";
import { VPExplorer } from "@lingdocs/ps-react";
import { entryFeeder } from "../lib/dictionary";
import {
InflectionsTable,
inflectWord,
Types as T,
InlinePs,
validateEntry,
typePredicates,
} from "@lingdocs/ps-react";
import Entry from "../components/Entry";
import * as FT from "../types/functions-types";
import { submissionBase, addSubmission } from "../lib/submissions";
import { Helmet } from "react-helmet";
import { TextOptions } from "@lingdocs/ps-react/dist/types";
import * as AT from "../types/account-types";
import { DictionaryAPI } from "../types/dictionary-types";
import { ErrorBoundary } from "react-error-boundary";
const textFields: { field: T.DictionaryEntryTextField; label: string }[] = [
{ field: "p", label: "Pashto" },
{ field: "f", label: "Phonetics" },
{ field: "e", label: "English" },
{ field: "c", label: "Part of Speech" },
{ field: "infap", label: "1st Masc. Irreg. Inflect. P" },
{ field: "infaf", label: "1st Masc. Irreg. Inflect. F" },
{ field: "infbp", label: "2nd Irreg. Inflect. Base P" },
{ field: "infbf", label: "2nd Irreg. Inflect. Base F" },
{ field: "app", label: "Arabic Plural P" },
{ field: "apf", label: "Arabic Plural F" },
{ field: "ppp", label: "Pashto Plural P" },
{ field: "ppf", label: "Pashto Plural F" },
{ field: "psp", label: "Imperf. Stem P" },
{ field: "psf", label: "Imperf. Stem F" },
{ field: "ssp", label: "Perf. Stem P" },
{ field: "ssf", label: "Perf. Stem F" },
{ field: "prp", label: "Perf. Root P" },
{ field: "prf", label: "Perf. Root F" },
{ field: "pprtp", label: "Past Part. P" },
{ field: "pprtf", label: "Past Part. F" },
{ field: "tppp", label: "3rd Pers. Masc. Sing P." },
{ field: "tppf", label: "3rd Pers. Masc. Sing F." },
{ field: "ec", label: "English Verb Conjugation" },
{ field: "ep", label: "English Verb Particle" },
];
const booleanFields: { field: T.DictionaryEntryBooleanField; label: string }[] =
[
{ field: "noInf", label: "no inflection" },
{ field: "shortIntrans", label: "short intrans" },
{ field: "noOo", label: "no oo prefix" },
{ field: "sepOo", label: "sep. oo prefix" },
{ field: "diacExcept", label: "diacritics except." },
];
const numberFields: { field: T.DictionaryEntryNumberField; label: string }[] = [
{ field: "l", label: "link" },
{ field: "separationAtP", label: "seperation at P" },
{ field: "separationAtF", label: "seperation at F" },
];
function OneField(props: {
value: string | number | undefined;
field: { field: T.DictionaryEntryField; label: string | JSX.Element };
errored?: boolean;
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}) {
return (
<div className="form-group">
<label htmlFor={props.field.field} className="small">
{props.field.label}
</label>
<input
type="text"
id={props.field.field}
className={classNames("form-control", { "is-invalid": props.errored })}
name={props.field.field}
value={props.value ?? ""}
dir={props.field.field.slice(-1) === "p" ? "rtl" : "ltr"}
onChange={props.handleChange}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
/>
</div>
);
}
function EntryEditor({
isolatedEntry,
dictionary,
searchParams,
textOptions,
user,
}: {
isolatedEntry: T.DictionaryEntry | undefined;
textOptions: TextOptions;
dictionary: DictionaryAPI;
searchParams: URLSearchParams;
user: AT.LingdocsUser | undefined;
// removeFromSuggestions: (sTs: number) => void,
}) {
const [entry, setEntry] = useState<T.DictionaryEntry>(
isolatedEntry ?? { ts: 0, i: 0, p: "", f: "", g: "", e: "" }
);
const [matchingEntries, setMatchingEntries] = useState<T.DictionaryEntry[]>(
isolatedEntry ? searchForMatchingEntries(isolatedEntry.p) : []
);
const [erroneusFields, setErroneousFields] = useState<
T.DictionaryEntryField[]
>([]);
const [errors, setErrors] = useState<string[]>([]);
const [submitted, setSubmitted] = useState<boolean>(false);
const [deleted, setDeleted] = useState<boolean>(false);
const [willDeleteSuggestion, setWillDeleteSuggestion] =
useState<boolean>(true);
const comment = searchParams.get("comment");
const sTsString = searchParams.get("sTs");
const sTs = sTsString && sTsString !== "0" ? parseInt(sTsString) : undefined;
const suggestedWord =
searchParams.get("p") || searchParams.get("f")
? {
p: searchParams.get("p") || "",
f: searchParams.get("f") || "",
}
: undefined;
useEffect(() => {
setEntry(isolatedEntry ?? { ts: 1, i: 0, p: "", f: "", g: "", e: "" });
setMatchingEntries(
isolatedEntry ? searchForMatchingEntries(isolatedEntry.p) : []
);
// eslint-disable-next-line
}, [isolatedEntry]);
function searchForMatchingEntries(s: string): T.DictionaryEntry[] {
return dictionary
.exactPashtoSearch(s)
.filter((w) => w.ts !== isolatedEntry?.ts);
}
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
const target = event.target;
const value = target.type === "checkbox" ? target.checked : target.value;
const name = target.name;
setEntry({
...entry,
[name]:
value &&
numberFields.find((x) => x.field === name) &&
typeof value === "string"
? parseInt(value)
: value,
});
if (erroneusFields.length) setErroneousFields([]);
if (name === "f" || name === "p") {
setMatchingEntries(searchForMatchingEntries(value as string));
}
}
function handleDelete() {
if (!user) return;
const submission: FT.EntryDeletion = {
...submissionBase(user),
type: "entry deletion",
ts: entry.ts,
};
addSubmission(submission, user);
setDeleted(true);
}
function handleSubmit(e: any) {
setErroneousFields([]);
setErrors([]);
e.preventDefault();
if (!user) return;
const result = validateEntry(entry);
if ("errors" in result) {
setErroneousFields(result.erroneousFields);
setErrors(result.errors);
return;
}
// TODO: Check complement if checkComplement
const submission: FT.NewEntry | FT.EntryEdit = {
...submissionBase(user),
type: entry.ts === 1 ? "new entry" : "entry edit",
entry: { ...entry, ts: entry.ts === 1 ? Date.now() : entry.ts },
};
addSubmission(submission, user);
setSubmitted(true);
// TODO: Remove from suggestions
// if (willDeleteSuggestion && sTs) {
// removeFromSuggestions(sTs);
// }
}
const complement = entry.l ? dictionary.findOneByTs(entry.l) : undefined;
const inf = ((): T.InflectorOutput | false => {
try {
return inflectWord(entry);
} catch (e) {
console.error("error inflecting entry", entry);
return false;
}
})();
const linkField: { field: "l"; label: string | JSX.Element } = {
field: "l",
label: (
<>
link{" "}
{entry.l ? (
complement ? (
<InlinePs opts={textOptions} ps={complement} />
) : (
"not found"
)
) : (
""
)}
</>
),
};
return (
<div className="width-limiter" style={{ marginBottom: "70px" }}>
<Helmet>
<link rel="canonical" href="https://dictionary.lingdocs.com/edit" />
<title>Edit - LingDocs Pashto Dictionary</title>
</Helmet>
{isolatedEntry && (
<Entry
admin={!!user?.admin}
nonClickable
entry={isolatedEntry}
textOptions={textOptions}
isolateEntry={() => null}
/>
)}
{suggestedWord && <InlinePs opts={textOptions} ps={suggestedWord} />}
{comment && <p>Comment: "{comment}"</p>}
{submitted ? (
"Edit submitted/saved"
) : deleted ? (
"Entry Deleted"
) : (
<div>
{matchingEntries.length > 0 && (
<div className="mb-1 text-center">
<strong>Matching Entries:</strong>
{matchingEntries.map((entry) => (
<div key={entry.ts}>
<Link to={`/edit?id=${entry.ts}`} className="plain-link">
<InlinePs opts={textOptions} ps={entry} />
</Link>
</div>
))}
</div>
)}
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col">
{[textFields[0]].map((field) => (
<OneField
key={field.field}
errored={erroneusFields.includes(field.field)}
field={field}
value={entry[field.field]}
handleChange={handleInputChange}
/>
))}
</div>
<div className="col">
{[textFields[1]].map((field) => (
<OneField
key={field.field}
field={field}
errored={erroneusFields.includes(field.field)}
value={entry[field.field]}
handleChange={handleInputChange}
/>
))}
</div>
</div>
{[textFields[2]].map((field) => (
<OneField
key={field.field}
field={field}
errored={erroneusFields.includes(field.field)}
value={entry[field.field]}
handleChange={handleInputChange}
/>
))}
<div className="row">
<div className="col">
{[textFields[3]].map((field) => (
<OneField
key={field.field}
field={field}
errored={erroneusFields.includes(field.field)}
value={entry[field.field]}
handleChange={handleInputChange}
/>
))}
</div>
<div className="col">
{[numberFields[0]].map((field) => (
<OneField
key={field.field}
field={linkField}
errored={erroneusFields.includes(field.field)}
value={entry[field.field]}
handleChange={handleInputChange}
/>
))}
</div>
</div>
<div className="row">
<div className="col">
{textFields.slice(4, 13).map((field) => (
<OneField
key={field.field}
field={field}
errored={erroneusFields.includes(field.field)}
value={entry[field.field]}
handleChange={handleInputChange}
/>
))}
{numberFields.slice(1).map((field) => (
<OneField
key={field.field}
field={field}
errored={erroneusFields.includes(field.field)}
value={entry[field.field]}
handleChange={handleInputChange}
/>
))}
</div>
<div className="col">
{textFields.slice(12, 23).map((field) => (
<OneField
key={field.field}
field={field}
errored={erroneusFields.includes(field.field)}
value={entry[field.field]}
handleChange={handleInputChange}
/>
))}
</div>
</div>
{booleanFields.map((field) => (
<div className="form-group form-check-inline" key={field.field}>
<input
id={field.field}
type="checkbox"
className={classNames("form-check-input", {
"is-invalid": erroneusFields.includes(field.field),
})}
name={field.field}
checked={entry[field.field] || false}
onChange={handleInputChange}
/>
<label htmlFor={field.field} className="form-check-label">
{field.label}
</label>
</div>
))}
<div className="form-group">
<button
type="submit"
className="btn btn-primary mr-4"
onClick={handleSubmit}
>
Submit
</button>
<button
type="button"
className="btn btn-danger"
onClick={handleDelete}
>
Delete Entry
</button>
{sTs && (
<div className="ml-3 form-group form-check-inline">
<input
id={"deleteSts"}
type="checkbox"
className="form-check-input"
name="deleteSts"
checked={willDeleteSuggestion}
onChange={(e) => setWillDeleteSuggestion(e.target.checked)}
/>
<label htmlFor="deleteSts" className="form-check-label">
Delete suggestion?
</label>
</div>
)}
</div>
{errors.length > 0 && (
<div className="alert alert-warning">
<ul className="mt-2">
{errors.map((error) => (
<li key={error}>{error}</li>
))}
</ul>
</div>
)}
</form>
{inf && inf.inflections && (
<InflectionsTable inf={inf.inflections} textOptions={textOptions} />
)}
{inf && "plural" in inf && inf.plural !== undefined && (
<InflectionsTable inf={inf.plural} textOptions={textOptions} />
)}
{inf && "arabicPlural" in inf && inf.arabicPlural !== undefined && (
<InflectionsTable
inf={inf.arabicPlural}
textOptions={textOptions}
/>
)}
{/* TODO: aay tail from state options */}
{typePredicates.isVerbEntry({ entry, complement }) && (
<div className="pb-4">
<ErrorBoundary fallback={<h5>Error conjugating verb</h5>}>
<VPExplorer
verb={{
entry: entry as T.VerbDictionaryEntry,
complement,
}}
opts={textOptions}
entryFeeder={entryFeeder}
handleLinkClick="none"
/>
</ErrorBoundary>
</div>
)}
</div>
)}
</div>
);
}
export default EntryEditor;