touch up
This commit is contained in:
parent
ea21348c5c
commit
eb9ffc5c6f
|
@ -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
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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;
|
||||||
|
if (user && !user.admin) {
|
||||||
ReactGA.event({
|
ReactGA.event({
|
||||||
category: "sounds",
|
category: "sounds",
|
||||||
action: `play ${ts} - ${p}`,
|
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();
|
||||||
|
|
|
@ -21,16 +21,13 @@ import {
|
||||||
} 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" },
|
||||||
|
@ -57,7 +54,8 @@ const textFields: {field: T.DictionaryEntryTextField, label: string}[] = [
|
||||||
{ 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" },
|
||||||
|
@ -65,21 +63,23 @@ const booleanFields: {field: T.DictionaryEntryBooleanField, label: string}[] = [
|
||||||
{ 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">
|
||||||
|
{props.field.label}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id={props.field.field}
|
id={props.field.field}
|
||||||
|
@ -96,35 +96,54 @@ function OneField(props: {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
}: {
|
||||||
|
isolatedEntry: T.DictionaryEntry | undefined;
|
||||||
|
textOptions: TextOptions;
|
||||||
|
dictionary: DictionaryAPI;
|
||||||
|
searchParams: URLSearchParams;
|
||||||
|
user: AT.LingdocsUser | undefined;
|
||||||
// removeFromSuggestions: (sTs: number) => void,
|
// 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 [matchingEntries, setMatchingEntries] = useState<T.DictionaryEntry[]>(
|
||||||
|
isolatedEntry ? searchForMatchingEntries(isolatedEntry.p) : []
|
||||||
|
);
|
||||||
|
const [erroneusFields, setErroneousFields] = useState<
|
||||||
|
T.DictionaryEntryField[]
|
||||||
|
>([]);
|
||||||
const [errors, setErrors] = useState<string[]>([]);
|
const [errors, setErrors] = useState<string[]>([]);
|
||||||
const [submitted, setSubmitted] = useState<boolean>(false);
|
const [submitted, setSubmitted] = useState<boolean>(false);
|
||||||
const [deleted, setDeleted] = useState<boolean>(false);
|
const [deleted, setDeleted] = useState<boolean>(false);
|
||||||
const [willDeleteSuggestion, setWillDeleteSuggestion] = useState<boolean>(true);
|
const [willDeleteSuggestion, setWillDeleteSuggestion] =
|
||||||
|
useState<boolean>(true);
|
||||||
const comment = searchParams.get("comment");
|
const comment = searchParams.get("comment");
|
||||||
const sTsString = searchParams.get("sTs");
|
const sTsString = searchParams.get("sTs");
|
||||||
const sTs = (sTsString && sTsString !== "0") ? parseInt(sTsString) : undefined;
|
const sTs = sTsString && sTsString !== "0" ? parseInt(sTsString) : undefined;
|
||||||
const suggestedWord = (searchParams.get("p") || searchParams.get("f")) ? {
|
const suggestedWord =
|
||||||
|
searchParams.get("p") || searchParams.get("f")
|
||||||
|
? {
|
||||||
p: searchParams.get("p") || "",
|
p: searchParams.get("p") || "",
|
||||||
f: searchParams.get("f") || "",
|
f: searchParams.get("f") || "",
|
||||||
} : undefined;
|
}
|
||||||
|
: undefined;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEntry((isolatedEntry) ?? { ts: 1, i: 0, p: "", f: "", g: "", e: "" });
|
setEntry(isolatedEntry ?? { ts: 1, i: 0, p: "", f: "", g: "", e: "" });
|
||||||
setMatchingEntries(isolatedEntry ? searchForMatchingEntries(isolatedEntry.p) : []);
|
setMatchingEntries(
|
||||||
|
isolatedEntry ? searchForMatchingEntries(isolatedEntry.p) : []
|
||||||
|
);
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, [isolatedEntry]);
|
}, [isolatedEntry]);
|
||||||
function searchForMatchingEntries(s: string): T.DictionaryEntry[] {
|
function searchForMatchingEntries(s: string): T.DictionaryEntry[] {
|
||||||
return dictionary.exactPashtoSearch(s)
|
return dictionary
|
||||||
|
.exactPashtoSearch(s)
|
||||||
.filter((w) => w.ts !== isolatedEntry?.ts);
|
.filter((w) => w.ts !== isolatedEntry?.ts);
|
||||||
}
|
}
|
||||||
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
|
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
@ -133,7 +152,10 @@ function EntryEditor({ isolatedEntry, dictionary, searchParams, textOptions, use
|
||||||
const name = target.name;
|
const name = target.name;
|
||||||
setEntry({
|
setEntry({
|
||||||
...entry,
|
...entry,
|
||||||
[name]: (value && numberFields.find((x) => x.field === name) && typeof value === "string")
|
[name]:
|
||||||
|
value &&
|
||||||
|
numberFields.find((x) => x.field === name) &&
|
||||||
|
typeof value === "string"
|
||||||
? parseInt(value)
|
? parseInt(value)
|
||||||
: value,
|
: value,
|
||||||
});
|
});
|
||||||
|
@ -186,21 +208,48 @@ function EntryEditor({ isolatedEntry, dictionary, searchParams, textOptions, use
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
const linkField: { field: "l", label: string | JSX.Element } = {
|
const linkField: { field: "l"; label: string | JSX.Element } = {
|
||||||
field: "l",
|
field: "l",
|
||||||
label: <>link {entry.l ? (complement ? <InlinePs opts={textOptions}>{complement}</InlinePs> : "not found") : ""}</>,
|
label: (
|
||||||
|
<>
|
||||||
|
link{" "}
|
||||||
|
{entry.l ? (
|
||||||
|
complement ? (
|
||||||
|
<InlinePs opts={textOptions}>{complement}</InlinePs>
|
||||||
|
) : (
|
||||||
|
"not found"
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
return <div className="width-limiter" style={{ marginBottom: "70px" }}>
|
return (
|
||||||
|
<div className="width-limiter" style={{ marginBottom: "70px" }}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<link rel="canonical" href="https://dictionary.lingdocs.com/edit" />
|
<link rel="canonical" href="https://dictionary.lingdocs.com/edit" />
|
||||||
<title>Edit - LingDocs Pashto Dictionary</title>
|
<title>Edit - LingDocs Pashto Dictionary</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
{isolatedEntry && <Entry nonClickable entry={isolatedEntry} textOptions={textOptions} isolateEntry={() => null} />}
|
{isolatedEntry && (
|
||||||
|
<Entry
|
||||||
|
user={user}
|
||||||
|
nonClickable
|
||||||
|
entry={isolatedEntry}
|
||||||
|
textOptions={textOptions}
|
||||||
|
isolateEntry={() => null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{suggestedWord && <InlinePs opts={textOptions}>{suggestedWord}</InlinePs>}
|
{suggestedWord && <InlinePs opts={textOptions}>{suggestedWord}</InlinePs>}
|
||||||
{comment && <p>Comment: "{comment}"</p>}
|
{comment && <p>Comment: "{comment}"</p>}
|
||||||
{submitted ? "Edit submitted/saved" : deleted ? "Entry Deleted" :
|
{submitted ? (
|
||||||
|
"Edit submitted/saved"
|
||||||
|
) : deleted ? (
|
||||||
|
"Entry Deleted"
|
||||||
|
) : (
|
||||||
<div>
|
<div>
|
||||||
{matchingEntries.length > 0 && <div className="mb-1 text-center">
|
{matchingEntries.length > 0 && (
|
||||||
|
<div className="mb-1 text-center">
|
||||||
<strong>Matching Entries:</strong>
|
<strong>Matching Entries:</strong>
|
||||||
{matchingEntries.map((entry) => (
|
{matchingEntries.map((entry) => (
|
||||||
<div key={entry.ts}>
|
<div key={entry.ts}>
|
||||||
|
@ -209,7 +258,8 @@ function EntryEditor({ isolatedEntry, dictionary, searchParams, textOptions, use
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>}
|
</div>
|
||||||
|
)}
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
|
@ -306,18 +356,35 @@ function EntryEditor({ isolatedEntry, dictionary, searchParams, textOptions, use
|
||||||
<input
|
<input
|
||||||
id={field.field}
|
id={field.field}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className={classNames("form-check-input", { "is-invalid": erroneusFields.includes(field.field) })}
|
className={classNames("form-check-input", {
|
||||||
|
"is-invalid": erroneusFields.includes(field.field),
|
||||||
|
})}
|
||||||
name={field.field}
|
name={field.field}
|
||||||
checked={entry[field.field] || false}
|
checked={entry[field.field] || false}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
<label htmlFor={field.field} className="form-check-label">{field.label}</label>
|
<label htmlFor={field.field} className="form-check-label">
|
||||||
|
{field.label}
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<button type="submit" className="btn btn-primary mr-4" onClick={handleSubmit}>Submit</button>
|
<button
|
||||||
<button type="button" className="btn btn-danger" onClick={handleDelete}>Delete Entry</button>
|
type="submit"
|
||||||
{sTs && <div className="ml-3 form-group form-check-inline">
|
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
|
<input
|
||||||
id={"deleteSts"}
|
id={"deleteSts"}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
@ -326,22 +393,37 @@ function EntryEditor({ isolatedEntry, dictionary, searchParams, textOptions, use
|
||||||
checked={willDeleteSuggestion}
|
checked={willDeleteSuggestion}
|
||||||
onChange={(e) => setWillDeleteSuggestion(e.target.checked)}
|
onChange={(e) => setWillDeleteSuggestion(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="deleteSts" className="form-check-label">Delete suggestion?</label>
|
<label htmlFor="deleteSts" className="form-check-label">
|
||||||
</div>}
|
Delete suggestion?
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{errors.length > 0 && <div className="alert alert-warning">
|
)}
|
||||||
|
</div>
|
||||||
|
{errors.length > 0 && (
|
||||||
|
<div className="alert alert-warning">
|
||||||
<ul className="mt-2">
|
<ul className="mt-2">
|
||||||
{errors.map((error) => (
|
{errors.map((error) => (
|
||||||
<li key={error}>{error}</li>
|
<li key={error}>{error}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>}
|
</div>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
{inf && inf.inflections && <InflectionsTable inf={inf.inflections} textOptions={textOptions} />}
|
{inf && inf.inflections && (
|
||||||
{inf && "plural" in inf && inf.plural !== undefined && <InflectionsTable inf={inf.plural} textOptions={textOptions} />}
|
<InflectionsTable inf={inf.inflections} textOptions={textOptions} />
|
||||||
{inf && "arabicPlural" in inf && inf.arabicPlural !== undefined && <InflectionsTable inf={inf.arabicPlural} 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 */}
|
{/* TODO: aay tail from state options */}
|
||||||
{typePredicates.isVerbEntry({ entry, complement }) && <div className="pb-4">
|
{typePredicates.isVerbEntry({ entry, complement }) && (
|
||||||
|
<div className="pb-4">
|
||||||
<VPExplorer
|
<VPExplorer
|
||||||
verb={{
|
verb={{
|
||||||
// TODO: CLEAN THIS UP!
|
// TODO: CLEAN THIS UP!
|
||||||
|
@ -353,11 +435,14 @@ function EntryEditor({ isolatedEntry, dictionary, searchParams, textOptions, use
|
||||||
entryFeeder={entryFeeder}
|
entryFeeder={entryFeeder}
|
||||||
handleLinkClick="none"
|
handleLinkClick="none"
|
||||||
/>
|
/>
|
||||||
</div>}
|
</div>
|
||||||
{typePredicates.isVerbEntry({ entry, complement }) && <div className="pb-4">
|
)}
|
||||||
|
{typePredicates.isVerbEntry({ entry, complement }) && (
|
||||||
|
<div className="pb-4">
|
||||||
{(() => {
|
{(() => {
|
||||||
try {
|
try {
|
||||||
return <VPExplorer
|
return (
|
||||||
|
<VPExplorer
|
||||||
verb={{
|
verb={{
|
||||||
// TODO: CLEAN THIS UP!
|
// TODO: CLEAN THIS UP!
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -368,14 +453,18 @@ function EntryEditor({ isolatedEntry, dictionary, searchParams, textOptions, use
|
||||||
entryFeeder={entryFeeder}
|
entryFeeder={entryFeeder}
|
||||||
handleLinkClick={"none"}
|
handleLinkClick={"none"}
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
return <h5>Error conjugating verb</h5>
|
return <h5>Error conjugating verb</h5>;
|
||||||
}
|
}
|
||||||
})()}
|
})()}
|
||||||
</div>}
|
</div>
|
||||||
</div>}
|
)}
|
||||||
</div>;
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EntryEditor;
|
export default EntryEditor;
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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({
|
||||||
|
reviewTask,
|
||||||
|
textOptions,
|
||||||
|
}: {
|
||||||
|
reviewTask: FT.ReviewTask;
|
||||||
|
textOptions: T.TextOptions;
|
||||||
|
}) {
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
deleteFromLocalDb("reviewTasks", reviewTask._id);
|
deleteFromLocalDb("reviewTasks", reviewTask._id);
|
||||||
}
|
}
|
||||||
const queryParamData = {
|
const queryParamData = {
|
||||||
...reviewTask.sTs ? {
|
...(reviewTask.sTs
|
||||||
|
? {
|
||||||
sTs: reviewTask.sTs,
|
sTs: reviewTask.sTs,
|
||||||
} : {},
|
}
|
||||||
..."comment" in reviewTask ? {
|
: {}),
|
||||||
|
...("comment" in reviewTask
|
||||||
|
? {
|
||||||
comment: reviewTask.comment,
|
comment: reviewTask.comment,
|
||||||
} : {},
|
}
|
||||||
..."entry" in reviewTask ? {
|
: {}),
|
||||||
|
...("entry" in reviewTask
|
||||||
|
? {
|
||||||
id: reviewTask.entry.ts,
|
id: reviewTask.entry.ts,
|
||||||
} : {},
|
}
|
||||||
|
: {}),
|
||||||
} as URLSearchParams;
|
} as URLSearchParams;
|
||||||
const queryString = new URLSearchParams(queryParamData).toString();
|
const queryString = new URLSearchParams(queryParamData).toString();
|
||||||
return <div className="d-flex flex-row align-items-center">
|
return (
|
||||||
|
<div className="d-flex flex-row align-items-center">
|
||||||
<div className="mr-3">
|
<div className="mr-3">
|
||||||
<div onClick={handleDelete} className="clickable">
|
<div onClick={handleDelete} className="clickable">
|
||||||
<i className="fa fa-trash" />
|
<i className="fa fa-trash" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{reviewTask.type !== "issue" &&
|
{reviewTask.type !== "issue" && (
|
||||||
<Link to={`/edit?${queryString}`} className="plain-link">
|
<Link to={`/edit?${queryString}`} className="plain-link">
|
||||||
<div className="card mb-2">
|
<div className="card mb-2">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
{reviewTask.type === "entry suggestion" && <div>
|
{reviewTask.type === "entry suggestion" && (
|
||||||
New Entry Suggestion
|
<div>New Entry Suggestion</div>
|
||||||
</div>}
|
)}
|
||||||
<Entry textOptions={textOptions} entry={reviewTask.entry} />
|
<Entry
|
||||||
|
user={undefined}
|
||||||
|
textOptions={textOptions}
|
||||||
|
entry={reviewTask.entry}
|
||||||
|
/>
|
||||||
<div className="mb-2">"{reviewTask.comment}"</div>
|
<div className="mb-2">"{reviewTask.comment}"</div>
|
||||||
<div className="small">{reviewTask.user.name} - {reviewTask.user.email}</div>
|
<div className="small">
|
||||||
|
{reviewTask.user.name} - {reviewTask.user.email}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</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 (
|
||||||
|
<div className="width-limiter" style={{ marginBottom: "70px" }}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>Review Tasks - LingDocs Pashto Dictionary</title>
|
<title>Review Tasks - LingDocs Pashto Dictionary</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<h3 className="mb-4">Review Tasks</h3>
|
<h3 className="mb-4">Review Tasks</h3>
|
||||||
{state.reviewTasks.length ?
|
{state.reviewTasks.length ? (
|
||||||
state.reviewTasks.map((reviewTask, i) => <ReviewTask key={i} reviewTask={reviewTask} textOptions={textOptions} />)
|
state.reviewTasks.map((reviewTask, i) => (
|
||||||
: <p>None</p>
|
<ReviewTask
|
||||||
}
|
key={i}
|
||||||
</div>;
|
reviewTask={reviewTask}
|
||||||
|
textOptions={textOptions}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p>None</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
|
@ -19,15 +19,8 @@ import {
|
||||||
calculateWordsToDelete,
|
calculateWordsToDelete,
|
||||||
hasAttachment,
|
hasAttachment,
|
||||||
} from "../lib/wordlist-database";
|
} from "../lib/wordlist-database";
|
||||||
import {
|
import { ButtonSelect, InlinePs, removeFVarients } from "@lingdocs/ps-react";
|
||||||
ButtonSelect,
|
import { Modal, Button } from "react-bootstrap";
|
||||||
InlinePs,
|
|
||||||
removeFVarients,
|
|
||||||
} from "@lingdocs/ps-react";
|
|
||||||
import {
|
|
||||||
Modal,
|
|
||||||
Button,
|
|
||||||
} from "react-bootstrap";
|
|
||||||
import WordlistImage from "../components/WordlistImage";
|
import WordlistImage from "../components/WordlistImage";
|
||||||
import ReviewScoreInput from "../components/ReviewScoreInput";
|
import ReviewScoreInput from "../components/ReviewScoreInput";
|
||||||
import { isPashtoScript } from "../lib/is-pashto";
|
import { isPashtoScript } from "../lib/is-pashto";
|
||||||
|
@ -36,9 +29,7 @@ import {
|
||||||
nextUpForReview,
|
nextUpForReview,
|
||||||
practiceWord,
|
practiceWord,
|
||||||
} from "../lib/spaced-repetition";
|
} from "../lib/spaced-repetition";
|
||||||
import {
|
import { pageSize } from "../lib/dictionary";
|
||||||
pageSize,
|
|
||||||
} from "../lib/dictionary";
|
|
||||||
import { textBadge } from "../lib/badges";
|
import { textBadge } from "../lib/badges";
|
||||||
import { SuperMemoGrade } from "supermemo";
|
import { SuperMemoGrade } from "supermemo";
|
||||||
import AudioPlayButton from "../components/AudioPlayButton";
|
import AudioPlayButton from "../components/AudioPlayButton";
|
||||||
|
@ -60,8 +51,8 @@ const cleanupIcon = "broom";
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
const reviewLanguageOptions: {
|
const reviewLanguageOptions: {
|
||||||
label: string,
|
label: string;
|
||||||
value: Language,
|
value: Language;
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
label: "Pashto",
|
label: "Pashto",
|
||||||
|
@ -86,27 +77,40 @@ function amountOfWords(number: number): string {
|
||||||
|
|
||||||
let popupRef: Window | null = null;
|
let popupRef: Window | null = null;
|
||||||
|
|
||||||
function Wordlist({ options, wordlist, isolateEntry, optionsDispatch, user, loadUser }: {
|
function Wordlist({
|
||||||
options: Options,
|
options,
|
||||||
wordlist: WordlistWord[],
|
wordlist,
|
||||||
isolateEntry: (ts: number) => void,
|
isolateEntry,
|
||||||
optionsDispatch: (action: OptionsAction) => void,
|
optionsDispatch,
|
||||||
user: AT.LingdocsUser | undefined,
|
user,
|
||||||
loadUser: () => void,
|
loadUser,
|
||||||
|
}: {
|
||||||
|
options: Options;
|
||||||
|
wordlist: WordlistWord[];
|
||||||
|
isolateEntry: (ts: number) => void;
|
||||||
|
optionsDispatch: (action: OptionsAction) => void;
|
||||||
|
user: AT.LingdocsUser | undefined;
|
||||||
|
loadUser: () => void;
|
||||||
}) {
|
}) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const [wordOpen, setWordOpen] = useState<string | undefined>(undefined);
|
const [wordOpen, setWordOpen] = useState<string | undefined>(undefined);
|
||||||
const [wordQuizzing, setWordQuizzing] = useState<string | undefined>(undefined);
|
const [wordQuizzing, setWordQuizzing] = useState<string | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
const [showGuide, setShowGuide] = useState<boolean>(true);
|
const [showGuide, setShowGuide] = useState<boolean>(true);
|
||||||
const [showingDownload, setShowingDownload] = useState<boolean>(false);
|
const [showingDownload, setShowingDownload] = useState<boolean>(false);
|
||||||
const [wordlistDownloadSort, setWordlistDownloadSort] = useState<"alphabetical" | "time">("time");
|
const [wordlistDownloadSort, setWordlistDownloadSort] = useState<
|
||||||
|
"alphabetical" | "time"
|
||||||
|
>("time");
|
||||||
const [page, setPage] = useState<number>(1);
|
const [page, setPage] = useState<number>(1);
|
||||||
const [wordlistSearchValue, setWordlistSearchValue] = useState<string>("");
|
const [wordlistSearchValue, setWordlistSearchValue] = useState<string>("");
|
||||||
const [filteredWords, setFilteredWords] = useState<WordlistWord[]>([]);
|
const [filteredWords, setFilteredWords] = useState<WordlistWord[]>([]);
|
||||||
const [showingCleanup, setShowingCleanup] = useState<boolean>(false);
|
const [showingCleanup, setShowingCleanup] = useState<boolean>(false);
|
||||||
const [monthsBackToKeep, setMonthsBackToKeep] = useState<number>(6);
|
const [monthsBackToKeep, setMonthsBackToKeep] = useState<number>(6);
|
||||||
const [wordsToDelete, setWordsToDelete] = useState<string[]>([]);
|
const [wordsToDelete, setWordsToDelete] = useState<string[]>([]);
|
||||||
const [startedWithWordsToReview] = useState<boolean>(forReview(wordlist).length !== 0);
|
const [startedWithWordsToReview] = useState<boolean>(
|
||||||
|
forReview(wordlist).length !== 0
|
||||||
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener("message", handleIncomingMessage);
|
window.addEventListener("message", handleIncomingMessage);
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -117,9 +121,9 @@ function Wordlist({ options, wordlist, isolateEntry, optionsDispatch, user, load
|
||||||
// TODO put the account url in an imported constant
|
// TODO put the account url in an imported constant
|
||||||
function handleIncomingMessage(event: MessageEvent<any>) {
|
function handleIncomingMessage(event: MessageEvent<any>) {
|
||||||
if (
|
if (
|
||||||
event.origin === "https://account.lingdocs.com"
|
event.origin === "https://account.lingdocs.com" &&
|
||||||
&& (event.data === "signed in")
|
event.data === "signed in" &&
|
||||||
&& popupRef
|
popupRef
|
||||||
) {
|
) {
|
||||||
loadUser();
|
loadUser();
|
||||||
popupRef.close();
|
popupRef.close();
|
||||||
|
@ -135,7 +139,7 @@ function Wordlist({ options, wordlist, isolateEntry, optionsDispatch, user, load
|
||||||
function handleScroll() {
|
function handleScroll() {
|
||||||
// TODO: DON'T HAVE ENDLESS PAGE INCREASING
|
// TODO: DON'T HAVE ENDLESS PAGE INCREASING
|
||||||
if (hitBottom() && options.wordlistMode === "browse") {
|
if (hitBottom() && options.wordlistMode === "browse") {
|
||||||
setPage(page => page + 1);
|
setPage((page) => page + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function handleWordClickBrowse(id: string) {
|
function handleWordClickBrowse(id: string) {
|
||||||
|
@ -171,9 +175,7 @@ function Wordlist({ options, wordlist, isolateEntry, optionsDispatch, user, load
|
||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
function handleShowCleanup() {
|
function handleShowCleanup() {
|
||||||
setWordsToDelete(
|
setWordsToDelete(calculateWordsToDelete(wordlist, monthsBackToKeep));
|
||||||
calculateWordsToDelete(wordlist, monthsBackToKeep)
|
|
||||||
);
|
|
||||||
setShowingCleanup(true);
|
setShowingCleanup(true);
|
||||||
}
|
}
|
||||||
function handleCloseCleanup() {
|
function handleCloseCleanup() {
|
||||||
|
@ -186,103 +188,140 @@ function Wordlist({ options, wordlist, isolateEntry, optionsDispatch, user, load
|
||||||
setShowingCleanup(false);
|
setShowingCleanup(false);
|
||||||
}
|
}
|
||||||
function handleOpenSignup() {
|
function handleOpenSignup() {
|
||||||
popupRef = window.open("https://account.lingdocs.com", "account", "height=800,width=500,top=50,left=400");
|
popupRef = window.open(
|
||||||
|
"https://account.lingdocs.com",
|
||||||
|
"account",
|
||||||
|
"height=800,width=500,top=50,left=400"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
function WordlistBrowsingWord({ word }: { word: WordlistWord }) {
|
function WordlistBrowsingWord({ word }: { word: WordlistWord }) {
|
||||||
const [confirmDelete, setConfirmDelete] = useState<boolean>(false);
|
const [confirmDelete, setConfirmDelete] = useState<boolean>(false);
|
||||||
return <div className="mb-4">
|
return (
|
||||||
|
<div className="mb-4">
|
||||||
<Entry
|
<Entry
|
||||||
|
user={user}
|
||||||
entry={word.entry}
|
entry={word.entry}
|
||||||
textOptions={textOptions}
|
textOptions={textOptions}
|
||||||
isolateEntry={() => handleWordClickBrowse(word._id)}
|
isolateEntry={() => handleWordClickBrowse(word._id)}
|
||||||
/>
|
/>
|
||||||
{hasAttachment(word, "audio") && <AudioPlayButton word={word} />}
|
{hasAttachment(word, "audio") && <AudioPlayButton word={word} />}
|
||||||
{word._id === wordOpen ? <>
|
{word._id === wordOpen ? (
|
||||||
|
<>
|
||||||
<div className="mb-3 d-flex flex-row justify-content-between">
|
<div className="mb-3 d-flex flex-row justify-content-between">
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-outline-secondary"
|
className="btn btn-sm btn-outline-secondary"
|
||||||
onClick={() => isolateEntry(word.entry.ts)}>
|
onClick={() => isolateEntry(word.entry.ts)}
|
||||||
<i className="fas fa-book"/>{` `}View Entry
|
>
|
||||||
|
<i className="fas fa-book" />
|
||||||
|
{` `}View Entry
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{!confirmDelete ?
|
{!confirmDelete ? (
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-outline-danger"
|
className="btn btn-sm btn-outline-danger"
|
||||||
onClick={() => setConfirmDelete(true)}
|
onClick={() => setConfirmDelete(true)}
|
||||||
>
|
>
|
||||||
<i className="fas fa-trash"/>{` `}Delete
|
<i className="fas fa-trash" />
|
||||||
|
{` `}Delete
|
||||||
</button>
|
</button>
|
||||||
:
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
className="btn mr-2 btn-sm btn-outline-secondary"
|
className="btn mr-2 btn-sm btn-outline-secondary"
|
||||||
onClick={() => setConfirmDelete(false)}
|
onClick={() => setConfirmDelete(false)}
|
||||||
>
|
>
|
||||||
<i className="fas fa-times"/>{` `}Cancel
|
<i className="fas fa-times" />
|
||||||
|
{` `}Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-outline-danger"
|
className="btn btn-sm btn-outline-danger"
|
||||||
onClick={() => deleteWord(word._id)}
|
onClick={() => deleteWord(word._id)}
|
||||||
>
|
>
|
||||||
<i className="fas fa-check"/>{` `}Delete
|
<i className="fas fa-check" />
|
||||||
|
{` `}Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{(Date.now() < word.dueDate) && <div className="text-muted small text-center mt-2 mb-1">
|
{Date.now() < word.dueDate && (
|
||||||
|
<div className="text-muted small text-center mt-2 mb-1">
|
||||||
up for review {dayjs().to(dayjs(word.dueDate))}
|
up for review {dayjs().to(dayjs(word.dueDate))}
|
||||||
</div>}
|
</div>
|
||||||
|
)}
|
||||||
{/* possible "next review" in x days, mins etc. calculated from milliseconds */}
|
{/* possible "next review" in x days, mins etc. calculated from milliseconds */}
|
||||||
<WordlistWordEditor word={word} />
|
<WordlistWordEditor word={word} />
|
||||||
</>
|
</>
|
||||||
:
|
) : (
|
||||||
word.notes && <div
|
word.notes && (
|
||||||
|
<div
|
||||||
className="clickable text-muted mb-3"
|
className="clickable text-muted mb-3"
|
||||||
onClick={() => handleWordClickBrowse(word._id)}
|
onClick={() => handleWordClickBrowse(word._id)}
|
||||||
dir="auto"
|
dir="auto"
|
||||||
style={{ textAlign: isPashtoScript(word.notes[0]) ? "right" : "left" }}
|
style={{
|
||||||
|
textAlign: isPashtoScript(word.notes[0]) ? "right" : "left",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{word.notes}
|
{word.notes}
|
||||||
</div>
|
</div>
|
||||||
}
|
)
|
||||||
{(hasAttachment(word, "image") && word._id !== wordOpen) && <WordlistImage word={word} />}
|
)}
|
||||||
</div>;
|
{hasAttachment(word, "image") && word._id !== wordOpen && (
|
||||||
|
<WordlistImage word={word} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
function WordlistReviewWord({ word }: { word: WordlistWord }) {
|
function WordlistReviewWord({ word }: { word: WordlistWord }) {
|
||||||
const beingQuizzed = word._id === wordQuizzing;
|
const beingQuizzed = word._id === wordQuizzing;
|
||||||
return <div className="mb-4">
|
return (
|
||||||
<div className="card mb-3 clickable" onClick={() => handleWordClickReview(word._id)}>
|
<div className="mb-4">
|
||||||
|
<div
|
||||||
|
className="card mb-3 clickable"
|
||||||
|
onClick={() => handleWordClickReview(word._id)}
|
||||||
|
>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h6 className="card-title text-center">
|
<h6 className="card-title text-center">
|
||||||
{options.wordlistReviewLanguage === "Pashto"
|
{options.wordlistReviewLanguage === "Pashto" ? (
|
||||||
? <InlinePs opts={textOptions}>{{ p: word.entry.p, f: word.entry.f }}</InlinePs>
|
<InlinePs opts={textOptions}>
|
||||||
: word.entry.e
|
|
||||||
}
|
|
||||||
</h6>
|
|
||||||
{beingQuizzed && <div className="card-text text-center">
|
|
||||||
{options.wordlistReviewLanguage === "Pashto"
|
|
||||||
? <div>{word.entry.e}</div>
|
|
||||||
: <InlinePs opts={textOptions}>
|
|
||||||
{{ p: word.entry.p, f: word.entry.f }}
|
{{ p: word.entry.p, f: word.entry.f }}
|
||||||
</InlinePs>
|
</InlinePs>
|
||||||
}
|
) : (
|
||||||
|
word.entry.e
|
||||||
|
)}
|
||||||
|
</h6>
|
||||||
|
{beingQuizzed && (
|
||||||
|
<div className="card-text text-center">
|
||||||
|
{options.wordlistReviewLanguage === "Pashto" ? (
|
||||||
|
<div>{word.entry.e}</div>
|
||||||
|
) : (
|
||||||
|
<InlinePs opts={textOptions}>
|
||||||
|
{{ p: word.entry.p, f: word.entry.f }}
|
||||||
|
</InlinePs>
|
||||||
|
)}
|
||||||
<div className="text-muted mb-2">{word.notes}</div>
|
<div className="text-muted mb-2">{word.notes}</div>
|
||||||
{hasAttachment(word, "audio") && <AudioPlayButton word={word} />}
|
{hasAttachment(word, "audio") && (
|
||||||
|
<AudioPlayButton word={word} />
|
||||||
|
)}
|
||||||
{hasAttachment(word, "image") && <WordlistImage word={word} />}
|
{hasAttachment(word, "image") && <WordlistImage word={word} />}
|
||||||
</div>}
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{beingQuizzed && <ReviewScoreInput
|
{beingQuizzed && (
|
||||||
|
<ReviewScoreInput
|
||||||
handleGrade={(grade) => handleAnswer(word, grade)}
|
handleGrade={(grade) => handleAnswer(word, grade)}
|
||||||
guide={showGuide}
|
guide={showGuide}
|
||||||
/>}
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!user || user.level === "basic") {
|
if (!user || user.level === "basic") {
|
||||||
return <div className="width-limiter" style={{ marginBottom: "120px" }}>
|
return (
|
||||||
|
<div className="width-limiter" style={{ marginBottom: "120px" }}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>Wordlist - LingDocs Pashto Dictionary</title>
|
<title>Wordlist - LingDocs Pashto Dictionary</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
@ -290,9 +329,16 @@ function Wordlist({ options, wordlist, isolateEntry, optionsDispatch, user, load
|
||||||
<h4 className="mb-3">Wordlist</h4>
|
<h4 className="mb-3">Wordlist</h4>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: "2rem" }}>
|
<div style={{ marginTop: "2rem" }}>
|
||||||
{!user
|
{!user ? (
|
||||||
? <p className="lead"><Link to="/account">Sign in</Link> to upgrade and enable wordlist</p>
|
<p className="lead">
|
||||||
: <p className="lead">Upgrade to a <strong>student account</strong> to enable the wordlist</p>}
|
<Link to="/account">Sign in</Link> to upgrade and enable wordlist
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="lead">
|
||||||
|
Upgrade to a <strong>student account</strong> to enable the
|
||||||
|
wordlist
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<p>Features:</p>
|
<p>Features:</p>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -300,41 +346,62 @@ function Wordlist({ options, wordlist, isolateEntry, optionsDispatch, user, load
|
||||||
<li>Save text, audio, or visual context for words</li>
|
<li>Save text, audio, or visual context for words</li>
|
||||||
<li>Review words with Anki-style spaced repetition</li>
|
<li>Review words with Anki-style spaced repetition</li>
|
||||||
</ul>
|
</ul>
|
||||||
{!user ? <>
|
{!user ? (
|
||||||
|
<>
|
||||||
<p>Cost:</p>
|
<p>Cost:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>$1/month or $10/year - cancel any time</li>
|
<li>$1/month or $10/year - cancel any time</li>
|
||||||
</ul>
|
</ul>
|
||||||
</> : <UpgradePrices source="wordlist" />}
|
</>
|
||||||
{!user && <button className="btn btn-lg btn-primary my-4" onClick={handleOpenSignup}><i className="fas fa-sign-in-alt mr-2" /> Sign In</button>}
|
) : (
|
||||||
|
<UpgradePrices source="wordlist" />
|
||||||
|
)}
|
||||||
|
{!user && (
|
||||||
|
<button
|
||||||
|
className="btn btn-lg btn-primary my-4"
|
||||||
|
onClick={handleOpenSignup}
|
||||||
|
>
|
||||||
|
<i className="fas fa-sign-in-alt mr-2" /> Sign In
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return <div className="width-limiter" style={{ marginBottom: "120px" }}>
|
return (
|
||||||
|
<div className="width-limiter" style={{ marginBottom: "120px" }}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>Wordlist - LingDocs Pashto Dictionary</title>
|
<title>Wordlist - LingDocs Pashto Dictionary</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="d-flex flex-row justify-content-between mb-2">
|
<div className="d-flex flex-row justify-content-between mb-2">
|
||||||
<h4 className="mb-3">Wordlist</h4>
|
<h4 className="mb-3">Wordlist</h4>
|
||||||
{wordlist.length > 0 &&
|
{wordlist.length > 0 && (
|
||||||
<div className="d-flex flex-row justify-content-between mb-2">
|
<div className="d-flex flex-row justify-content-between mb-2">
|
||||||
<div>
|
<div>
|
||||||
<button className="btn btn-sm btn-outline-secondary mr-3" onClick={handleShowCleanup}>
|
<button
|
||||||
<i className={`fas fa-${cleanupIcon} mx-1`} /> <span className="show-on-desktop">Cleanup</span>
|
className="btn btn-sm btn-outline-secondary mr-3"
|
||||||
|
onClick={handleShowCleanup}
|
||||||
|
>
|
||||||
|
<i className={`fas fa-${cleanupIcon} mx-1`} />{" "}
|
||||||
|
<span className="show-on-desktop">Cleanup</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button className="btn btn-sm btn-outline-secondary" onClick={() => setShowingDownload(true)}>
|
<button
|
||||||
<i className="fas fa-download mx-1" /> <span className="show-on-desktop">CSV</span>
|
className="btn btn-sm btn-outline-secondary"
|
||||||
|
onClick={() => setShowingDownload(true)}
|
||||||
|
>
|
||||||
|
<i className="fas fa-download mx-1" />{" "}
|
||||||
|
<span className="show-on-desktop">CSV</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!wordlist.length ?
|
{!wordlist.length ? (
|
||||||
<EmptyWordlistNotice />
|
<EmptyWordlistNotice />
|
||||||
:
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="mb-3 text-center">
|
<div className="mb-3 text-center">
|
||||||
<ButtonSelect
|
<ButtonSelect
|
||||||
|
@ -350,62 +417,91 @@ function Wordlist({ options, wordlist, isolateEntry, optionsDispatch, user, load
|
||||||
]}
|
]}
|
||||||
value={options.wordlistMode || "browse"}
|
value={options.wordlistMode || "browse"}
|
||||||
handleChange={(p) => {
|
handleChange={(p) => {
|
||||||
optionsDispatch({ type: "changeWordlistMode", payload: p as WordlistMode });
|
optionsDispatch({
|
||||||
|
type: "changeWordlistMode",
|
||||||
|
payload: p as WordlistMode,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{options.wordlistMode === "browse"
|
{options.wordlistMode === "browse" ? (
|
||||||
? <div className="mt-4">
|
<div className="mt-4">
|
||||||
<WordlistSearchBar value={wordlistSearchValue} handleChange={handleSearchValueChange} />
|
<WordlistSearchBar
|
||||||
{paginate(wordlistSearchValue ? filteredWords : wordlist, page).map((word) => (
|
value={wordlistSearchValue}
|
||||||
|
handleChange={handleSearchValueChange}
|
||||||
|
/>
|
||||||
|
{paginate(
|
||||||
|
wordlistSearchValue ? filteredWords : wordlist,
|
||||||
|
page
|
||||||
|
).map((word) => (
|
||||||
<WordlistBrowsingWord word={word} key={word._id} />
|
<WordlistBrowsingWord word={word} key={word._id} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
: <div>
|
) : (
|
||||||
|
<div>
|
||||||
<div className="mb-2 text-center">Show:</div>
|
<div className="mb-2 text-center">Show:</div>
|
||||||
<div className="mb-4 text-center" style={{ width: "100%" }}>
|
<div className="mb-4 text-center" style={{ width: "100%" }}>
|
||||||
<ButtonSelect
|
<ButtonSelect
|
||||||
options={reviewLanguageOptions}
|
options={reviewLanguageOptions}
|
||||||
value={options.wordlistReviewLanguage || "Pashto"}
|
value={options.wordlistReviewLanguage || "Pashto"}
|
||||||
handleChange={(p) => {
|
handleChange={(p) => {
|
||||||
optionsDispatch({ type: "changeWordlistReviewLanguage", payload: p as Language });
|
optionsDispatch({
|
||||||
|
type: "changeWordlistReviewLanguage",
|
||||||
|
payload: p as Language,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{/* TODO: ISSUE WITH NOT USING PAGINATE HERE BECAUSE OF IMAGE RELOADING BUGINESS WHEN HITTING BOTTOM */}
|
{/* TODO: ISSUE WITH NOT USING PAGINATE HERE BECAUSE OF IMAGE RELOADING BUGINESS WHEN HITTING BOTTOM */}
|
||||||
{toReview.length === 0
|
{toReview.length === 0 ? (
|
||||||
? (startedWithWordsToReview
|
startedWithWordsToReview ? (
|
||||||
? <p className="lead my-3">All done review 🎉</p>
|
<p className="lead my-3">All done review 🎉</p>
|
||||||
: (() => {
|
) : (
|
||||||
|
(() => {
|
||||||
const nextUp = nextUpForReview(wordlist);
|
const nextUp = nextUpForReview(wordlist);
|
||||||
const { e, ...ps } = nextUp.entry;
|
const { e, ...ps } = nextUp.entry;
|
||||||
return <div>
|
return (
|
||||||
|
<div>
|
||||||
<div className="lead my-3">None to review</div>
|
<div className="lead my-3">None to review</div>
|
||||||
<p>Next word up for review <strong>{dayjs().to(nextUp.dueDate)}</strong>: <InlinePs opts={textOptions}>
|
<p>
|
||||||
|
Next word up for review{" "}
|
||||||
|
<strong>{dayjs().to(nextUp.dueDate)}</strong>:{" "}
|
||||||
|
<InlinePs opts={textOptions}>
|
||||||
{removeFVarients(ps)}
|
{removeFVarients(ps)}
|
||||||
</InlinePs></p>
|
</InlinePs>
|
||||||
</div>;
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
})()
|
})()
|
||||||
)
|
)
|
||||||
: toReview.map((word) => (
|
) : (
|
||||||
|
toReview.map((word) => (
|
||||||
<WordlistReviewWord word={word} key={word._id} />
|
<WordlistReviewWord word={word} key={word._id} />
|
||||||
))
|
))
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
{(wordlistSearchValue && filteredWords.length === 0) && <div>
|
{wordlistSearchValue && filteredWords.length === 0 && (
|
||||||
|
<div>
|
||||||
<h6 className="my-4 ml-1">None found</h6>
|
<h6 className="my-4 ml-1">None found</h6>
|
||||||
</div>}
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
<Modal show={showingDownload} onHide={() => setShowingDownload(false)}>
|
<Modal show={showingDownload} onHide={() => setShowingDownload(false)}>
|
||||||
<Modal.Header closeButton>
|
<Modal.Header closeButton>
|
||||||
<Modal.Title><i className={`fas fa-download mr-1`} /> Download Wordlist CSV</Modal.Title>
|
<Modal.Title>
|
||||||
|
<i className={`fas fa-download mr-1`} /> Download Wordlist CSV
|
||||||
|
</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<p>You can download your wordlist in CSV format. Then you can open it with a spreadsheet program or import it into Anki. Pictures will not be included.</p>
|
<p>
|
||||||
|
You can download your wordlist in CSV format. Then you can open it
|
||||||
|
with a spreadsheet program or import it into Anki. Pictures will not
|
||||||
|
be included.
|
||||||
|
</p>
|
||||||
<p>How should we sort your wordlist?</p>
|
<p>How should we sort your wordlist?</p>
|
||||||
<ButtonSelect
|
<ButtonSelect
|
||||||
options={[
|
options={[
|
||||||
|
@ -435,7 +531,9 @@ function Wordlist({ options, wordlist, isolateEntry, optionsDispatch, user, load
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal show={showingCleanup} onHide={handleCloseCleanup}>
|
<Modal show={showingCleanup} onHide={handleCloseCleanup}>
|
||||||
<Modal.Header closeButton>
|
<Modal.Header closeButton>
|
||||||
<Modal.Title><i className={`fas fa-${cleanupIcon} mr-1`} /> Wordlist Cleanup</Modal.Title>
|
<Modal.Title>
|
||||||
|
<i className={`fas fa-${cleanupIcon} mr-1`} /> Wordlist Cleanup
|
||||||
|
</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<p>You have {amountOfWords(wordlist.length)} in your wordlist.</p>
|
<p>You have {amountOfWords(wordlist.length)} in your wordlist.</p>
|
||||||
|
@ -462,38 +560,52 @@ function Wordlist({ options, wordlist, isolateEntry, optionsDispatch, user, load
|
||||||
value={monthsBackToKeep.toString()}
|
value={monthsBackToKeep.toString()}
|
||||||
handleChange={(p: string) => {
|
handleChange={(p: string) => {
|
||||||
const months = parseInt(p);
|
const months = parseInt(p);
|
||||||
setWordsToDelete(
|
setWordsToDelete(calculateWordsToDelete(wordlist, months));
|
||||||
calculateWordsToDelete(wordlist, months),
|
|
||||||
);
|
|
||||||
setMonthsBackToKeep(months);
|
setMonthsBackToKeep(months);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="mt-3">This will delete {amountOfWords(wordsToDelete.length)}.</p>
|
<p className="mt-3">
|
||||||
|
This will delete {amountOfWords(wordsToDelete.length)}.
|
||||||
|
</p>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button variant="secondary" onClick={handleCloseCleanup}>
|
<Button variant="secondary" onClick={handleCloseCleanup}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="danger" onClick={handleCleanup} disabled={wordsToDelete.length === 0}>
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
onClick={handleCleanup}
|
||||||
|
disabled={wordsToDelete.length === 0}
|
||||||
|
>
|
||||||
<i className={`fas fa-${cleanupIcon} mr-1`} /> Delete
|
<i className={`fas fa-${cleanupIcon} mr-1`} /> Delete
|
||||||
</Button>
|
</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmptyWordlistNotice() {
|
function EmptyWordlistNotice() {
|
||||||
return <div className="pt-4">
|
return (
|
||||||
|
<div className="pt-4">
|
||||||
<p>Your wordlist is empty.</p>
|
<p>Your wordlist is empty.</p>
|
||||||
<p>To add a word to your wordlist, choose a word while searching and click on the <i className="far fa-star"/> icon.</p>
|
<p>
|
||||||
</div>;
|
To add a word to your wordlist, choose a word while searching and click
|
||||||
|
on the <i className="far fa-star" /> icon.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function WordlistSearchBar({ handleChange, value }: {
|
function WordlistSearchBar({
|
||||||
value: string,
|
handleChange,
|
||||||
handleChange: (value: string) => void,
|
value,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
handleChange: (value: string) => void;
|
||||||
}) {
|
}) {
|
||||||
return <div className="input-group mb-3">
|
return (
|
||||||
|
<div className="input-group mb-3">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
style={{ borderRight: "0px", zIndex: 200 }}
|
style={{ borderRight: "0px", zIndex: 200 }}
|
||||||
|
@ -517,10 +629,14 @@ function WordlistSearchBar({ handleChange, value }: {
|
||||||
onClick={() => handleChange("")}
|
onClick={() => handleChange("")}
|
||||||
data-testid="wordlistClearButton"
|
data-testid="wordlistClearButton"
|
||||||
>
|
>
|
||||||
<i className="fa fa-times" style={!value ? { visibility: "hidden" } : {}}></i>
|
<i
|
||||||
|
className="fa fa-times"
|
||||||
|
style={!value ? { visibility: "hidden" } : {}}
|
||||||
|
></i>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>;
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Wordlist;
|
export default Wordlist;
|
Loading…
Reference in New Issue