This commit is contained in:
adueck 2024-05-04 10:33:22 +04:00
parent ea21348c5c
commit eb9ffc5c6f
8 changed files with 1099 additions and 862 deletions

View File

@ -9,23 +9,26 @@ import ExtraEntryInfo from "../components/ExtraEntryInfo";
import classNames from "classnames"; import classNames from "classnames";
import { Types as T, InlinePs } from "@lingdocs/ps-react"; import { Types as T, InlinePs } from "@lingdocs/ps-react";
import playStorageAudio from "./PlayStorageAudio"; import playStorageAudio from "./PlayStorageAudio";
import { LingdocsUser } from "../types/account-types";
function Entry({ function Entry({
entry, entry,
textOptions, textOptions,
nonClickable, nonClickable,
isolateEntry, isolateEntry,
user,
}: { }: {
entry: T.DictionaryEntry; entry: T.DictionaryEntry;
textOptions: T.TextOptions; textOptions: T.TextOptions;
nonClickable?: boolean; nonClickable?: boolean;
isolateEntry?: (ts: number) => void; isolateEntry?: (ts: number) => void;
user: LingdocsUser | undefined;
}) { }) {
function handlePlayStorageAudio( function handlePlayStorageAudio(
e: React.MouseEvent<HTMLElement, MouseEvent> e: React.MouseEvent<HTMLElement, MouseEvent>
) { ) {
e.stopPropagation(); e.stopPropagation();
playStorageAudio(entry.ts, entry.p, () => null); playStorageAudio(entry.ts, entry.p, user, () => null);
} }
return ( return (
<div <div

View File

@ -21,7 +21,7 @@ export function EntryAudioDisplay({
} }
ReactGA.event({ ReactGA.event({
category: "sounds", category: "sounds",
action: `play ${entry.ts} - ${entry.p}`, action: `play ${entry.p} - ${entry.ts}`,
}); });
} }
return ( return (

View File

@ -1,4 +1,5 @@
import ReactGA from "react-ga4"; import ReactGA from "react-ga4";
import { LingdocsUser } from "../types/account-types";
export function getAudioPath(ts: number): string { export function getAudioPath(ts: number): string {
return `https://storage.lingdocs.com/audio/${ts}.mp3`; return `https://storage.lingdocs.com/audio/${ts}.mp3`;
@ -7,13 +8,16 @@ export function getAudioPath(ts: number): string {
export default function playStorageAudio( export default function playStorageAudio(
ts: number, ts: number,
p: string, p: string,
user: LingdocsUser | undefined,
callback: () => void callback: () => void
) { ) {
if (!ts) return; if (!ts) return;
ReactGA.event({ if (user && !user.admin) {
category: "sounds", ReactGA.event({
action: `play ${ts} - ${p}`, category: "sounds",
}); action: `quick play ${p} - ${ts}`,
});
}
let audio = new Audio(getAudioPath(ts)); let audio = new Audio(getAudioPath(ts));
audio.addEventListener("ended", () => { audio.addEventListener("ended", () => {
callback(); callback();

View File

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

View File

@ -164,6 +164,7 @@ function IsolatedEntry({
<dl className="row mb-1"> <dl className="row mb-1">
<div className="col-8"> <div className="col-8">
<Entry <Entry
user={state.user}
nonClickable nonClickable
entry={exploded ? explodeEntry(entry) : entry} entry={exploded ? explodeEntry(entry) : entry}
textOptions={textOptions} textOptions={textOptions}

View File

@ -143,6 +143,7 @@ function Results({
entry={p.entry} entry={p.entry}
textOptions={textOptions} textOptions={textOptions}
isolateEntry={isolateEntry} isolateEntry={isolateEntry}
user={state.user}
/> />
<div className="mb-3 ml-2"> <div className="mb-3 ml-2">
{p.forms.map((form, i) => ( {p.forms.map((form, i) => (
@ -169,6 +170,7 @@ function Results({
entry={entry} entry={entry}
textOptions={textOptions} textOptions={textOptions}
isolateEntry={isolateEntry} isolateEntry={isolateEntry}
user={state.user}
/> />
))} ))}
</dl> </dl>

View File

@ -1,68 +1,90 @@
import Entry from "../components/Entry"; import Entry from "../components/Entry";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import * as FT from "../types/functions-types"; import * as FT from "../types/functions-types";
import { import { deleteFromLocalDb } from "../lib/pouch-dbs";
deleteFromLocalDb, import { Types as T } from "@lingdocs/ps-react";
} from "../lib/pouch-dbs";
import {
Types as T,
} from "@lingdocs/ps-react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { getTextOptions } from "../lib/get-text-options"; import { getTextOptions } from "../lib/get-text-options";
import { import { State } from "../types/dictionary-types";
State,
} from "../types/dictionary-types";
function ReviewTask({ reviewTask, textOptions }: { reviewTask: FT.ReviewTask, textOptions: T.TextOptions }) { function ReviewTask({
function handleDelete() { reviewTask,
deleteFromLocalDb("reviewTasks", reviewTask._id); textOptions,
} }: {
const queryParamData = { reviewTask: FT.ReviewTask;
...reviewTask.sTs ? { textOptions: T.TextOptions;
sTs: reviewTask.sTs, }) {
} : {}, function handleDelete() {
..."comment" in reviewTask ? { deleteFromLocalDb("reviewTasks", reviewTask._id);
comment: reviewTask.comment, }
} : {}, const queryParamData = {
..."entry" in reviewTask ? { ...(reviewTask.sTs
id: reviewTask.entry.ts, ? {
} : {}, sTs: reviewTask.sTs,
} as URLSearchParams;
const queryString = new URLSearchParams(queryParamData).toString();
return <div className="d-flex flex-row align-items-center">
<div className="mr-3">
<div onClick={handleDelete} className="clickable">
<i className="fa fa-trash" />
</div>
</div>
{reviewTask.type !== "issue" &&
<Link to={`/edit?${queryString}`} className="plain-link">
<div className="card mb-2">
<div className="card-body">
{reviewTask.type === "entry suggestion" && <div>
New Entry Suggestion
</div>}
<Entry textOptions={textOptions} entry={reviewTask.entry} />
<div className="mb-2">"{reviewTask.comment}"</div>
<div className="small">{reviewTask.user.name} - {reviewTask.user.email}</div>
</div>
</div>
</Link>
} }
: {}),
...("comment" in reviewTask
? {
comment: reviewTask.comment,
}
: {}),
...("entry" in reviewTask
? {
id: reviewTask.entry.ts,
}
: {}),
} as URLSearchParams;
const queryString = new URLSearchParams(queryParamData).toString();
return (
<div className="d-flex flex-row align-items-center">
<div className="mr-3">
<div onClick={handleDelete} className="clickable">
<i className="fa fa-trash" />
</div>
</div>
{reviewTask.type !== "issue" && (
<Link to={`/edit?${queryString}`} className="plain-link">
<div className="card mb-2">
<div className="card-body">
{reviewTask.type === "entry suggestion" && (
<div>New Entry Suggestion</div>
)}
<Entry
user={undefined}
textOptions={textOptions}
entry={reviewTask.entry}
/>
<div className="mb-2">"{reviewTask.comment}"</div>
<div className="small">
{reviewTask.user.name} - {reviewTask.user.email}
</div>
</div>
</div>
</Link>
)}
</div> </div>
);
} }
export default function ReviewTasks({ state }: { state: State }) { export default function ReviewTasks({ state }: { state: State }) {
const textOptions = getTextOptions(state); const textOptions = getTextOptions(state);
return <div className="width-limiter" style={{ marginBottom: "70px" }}> return (
<Helmet> <div className="width-limiter" style={{ marginBottom: "70px" }}>
<title>Review Tasks - LingDocs Pashto Dictionary</title> <Helmet>
</Helmet> <title>Review Tasks - LingDocs Pashto Dictionary</title>
<h3 className="mb-4">Review Tasks</h3> </Helmet>
{state.reviewTasks.length ? <h3 className="mb-4">Review Tasks</h3>
state.reviewTasks.map((reviewTask, i) => <ReviewTask key={i} reviewTask={reviewTask} textOptions={textOptions} />) {state.reviewTasks.length ? (
: <p>None</p> state.reviewTasks.map((reviewTask, i) => (
} <ReviewTask
</div>; key={i}
} reviewTask={reviewTask}
textOptions={textOptions}
/>
))
) : (
<p>None</p>
)}
</div>
);
}

File diff suppressed because it is too large Load Diff