diff --git a/website/src/components/Entry.tsx b/website/src/components/Entry.tsx index aacf165..60e5432 100644 --- a/website/src/components/Entry.tsx +++ b/website/src/components/Entry.tsx @@ -9,23 +9,26 @@ import ExtraEntryInfo from "../components/ExtraEntryInfo"; import classNames from "classnames"; import { Types as T, InlinePs } from "@lingdocs/ps-react"; import playStorageAudio from "./PlayStorageAudio"; +import { LingdocsUser } from "../types/account-types"; function Entry({ entry, textOptions, nonClickable, isolateEntry, + user, }: { entry: T.DictionaryEntry; textOptions: T.TextOptions; nonClickable?: boolean; isolateEntry?: (ts: number) => void; + user: LingdocsUser | undefined; }) { function handlePlayStorageAudio( e: React.MouseEvent ) { e.stopPropagation(); - playStorageAudio(entry.ts, entry.p, () => null); + playStorageAudio(entry.ts, entry.p, user, () => null); } return (
void ) { if (!ts) return; - ReactGA.event({ - category: "sounds", - action: `play ${ts} - ${p}`, - }); + if (user && !user.admin) { + ReactGA.event({ + category: "sounds", + action: `quick play ${p} - ${ts}`, + }); + } let audio = new Audio(getAudioPath(ts)); audio.addEventListener("ended", () => { callback(); diff --git a/website/src/screens/EntryEditor.tsx b/website/src/screens/EntryEditor.tsx index c673118..dd97c78 100644 --- a/website/src/screens/EntryEditor.tsx +++ b/website/src/screens/EntryEditor.tsx @@ -12,370 +12,459 @@ 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, + 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 { 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"; -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 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}[] = [ +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" }, +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) => void, + value: string | number | undefined; + field: { field: T.DictionaryEntryField; label: string | JSX.Element }; + errored?: boolean; + handleChange: (e: React.ChangeEvent) => void; }) { - return ( -
- - -
- ); + return ( +
+ + +
+ ); } -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, +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((isolatedEntry) ?? { ts: 0, i: 0, p: "", f: "", g: "", e: "" }); - const [matchingEntries, setMatchingEntries] = useState(isolatedEntry ? searchForMatchingEntries(isolatedEntry.p) : []); - const [erroneusFields, setErroneousFields] = useState([]); - const [errors, setErrors] = useState([]); - const [submitted, setSubmitted] = useState(false); - const [deleted, setDeleted] = useState(false); - const [willDeleteSuggestion, setWillDeleteSuggestion] = useState(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) { - 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)); + const [entry, setEntry] = useState( + isolatedEntry ?? { ts: 0, i: 0, p: "", f: "", g: "", e: "" } + ); + const [matchingEntries, setMatchingEntries] = useState( + isolatedEntry ? searchForMatchingEntries(isolatedEntry.p) : [] + ); + const [erroneusFields, setErroneousFields] = useState< + T.DictionaryEntryField[] + >([]); + const [errors, setErrors] = useState([]); + const [submitted, setSubmitted] = useState(false); + const [deleted, setDeleted] = useState(false); + const [willDeleteSuggestion, setWillDeleteSuggestion] = + useState(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) { + 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 ? {complement} : "not found") : ""}, + } + function handleDelete() { + if (!user) return; + const submission: FT.EntryDeletion = { + ...submissionBase(user), + type: "entry deletion", + ts: entry.ts, }; - return
- - - Edit - LingDocs Pashto Dictionary - - {isolatedEntry && null} />} - {suggestedWord && {suggestedWord}} - {comment &&

Comment: "{comment}"

} - {submitted ? "Edit submitted/saved" : deleted ? "Entry Deleted" : -
- {matchingEntries.length > 0 &&
- Matching Entries: - {matchingEntries.map((entry) => ( -
- - {entry} - -
- ))} -
} -
-
-
- {[textFields[0]].map((field) => ( - - ))} -
-
- {[textFields[1]].map((field) => ( - - ))} -
-
- {[textFields[2]].map((field) => ( - - ))} -
-
- {[textFields[3]].map((field) => ( - - ))} -
-
- {[numberFields[0]].map((field) => ( - - ))} -
-
-
-
- {textFields.slice(4, 13).map((field) => ( - - ))} - {numberFields.slice(1).map((field) => ( - - ))} -
-
- {textFields.slice(12, 23).map((field) => ( - - ))} -
-
- {booleanFields.map((field) => ( -
- - -
- ))} -
- - - {sTs &&
- setWillDeleteSuggestion(e.target.checked)} - /> - -
} -
- {errors.length > 0 &&
-
    - {errors.map((error) => ( -
  • {error}
  • - ))} -
-
} - - {inf && inf.inflections && } - {inf && "plural" in inf && inf.plural !== undefined && } - {inf && "arabicPlural" in inf && inf.arabicPlural !== undefined && } - {/* TODO: aay tail from state options */} - {typePredicates.isVerbEntry({ entry, complement }) &&
- { + 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 ? ( + {complement} + ) : ( + "not found" + ) + ) : ( + "" + )} + + ), + }; + return ( +
+ + + Edit - LingDocs Pashto Dictionary + + {isolatedEntry && ( + null} + /> + )} + {suggestedWord && {suggestedWord}} + {comment &&

Comment: "{comment}"

} + {submitted ? ( + "Edit submitted/saved" + ) : deleted ? ( + "Entry Deleted" + ) : ( +
+ {matchingEntries.length > 0 && ( +
+ Matching Entries: + {matchingEntries.map((entry) => ( +
+ + {entry} + +
+ ))} +
+ )} +
+
+
+ {[textFields[0]].map((field) => ( + + ))} +
+
+ {[textFields[1]].map((field) => ( + + ))} +
+
+ {[textFields[2]].map((field) => ( + + ))} +
+
+ {[textFields[3]].map((field) => ( + + ))} +
+
+ {[numberFields[0]].map((field) => ( + + ))} +
+
+
+
+ {textFields.slice(4, 13).map((field) => ( + + ))} + {numberFields.slice(1).map((field) => ( + + ))} +
+
+ {textFields.slice(12, 23).map((field) => ( + + ))} +
+
+ {booleanFields.map((field) => ( +
+ + +
+ ))} +
+ + + {sTs && ( +
+ setWillDeleteSuggestion(e.target.checked)} + /> + +
+ )} +
+ {errors.length > 0 && ( +
+
    + {errors.map((error) => ( +
  • {error}
  • + ))} +
+
+ )} + + {inf && inf.inflections && ( + + )} + {inf && "plural" in inf && inf.plural !== undefined && ( + + )} + {inf && "arabicPlural" in inf && inf.arabicPlural !== undefined && ( + + )} + {/* TODO: aay tail from state options */} + {typePredicates.isVerbEntry({ entry, complement }) && ( +
+ +
+ )} + {typePredicates.isVerbEntry({ entry, complement }) && ( +
+ {(() => { + try { + return ( + -
} - {typePredicates.isVerbEntry({ entry, complement }) &&
- {(() => { - try { - return - } catch(e) { - console.error(e); - return
Error conjugating verb
- } - })()} -
} -
} -
; + }} + opts={textOptions} + entryFeeder={entryFeeder} + handleLinkClick={"none"} + /> + ); + } catch (e) { + console.error(e); + return
Error conjugating verb
; + } + })()} +
+ )} +
+ )} +
+ ); } -export default EntryEditor; \ No newline at end of file +export default EntryEditor; diff --git a/website/src/screens/IsolatedEntry.tsx b/website/src/screens/IsolatedEntry.tsx index d0df49f..2e2e4a3 100644 --- a/website/src/screens/IsolatedEntry.tsx +++ b/website/src/screens/IsolatedEntry.tsx @@ -164,6 +164,7 @@ function IsolatedEntry({
{p.forms.map((form, i) => ( @@ -169,6 +170,7 @@ function Results({ entry={entry} textOptions={textOptions} isolateEntry={isolateEntry} + user={state.user} /> ))}
diff --git a/website/src/screens/ReviewTasks.tsx b/website/src/screens/ReviewTasks.tsx index 2b65ce3..7cbc464 100644 --- a/website/src/screens/ReviewTasks.tsx +++ b/website/src/screens/ReviewTasks.tsx @@ -1,68 +1,90 @@ import Entry from "../components/Entry"; import { Link } from "react-router-dom"; import * as FT from "../types/functions-types"; -import { - deleteFromLocalDb, -} from "../lib/pouch-dbs"; -import { - Types as T, -} from "@lingdocs/ps-react"; +import { deleteFromLocalDb } from "../lib/pouch-dbs"; +import { Types as T } from "@lingdocs/ps-react"; import { Helmet } from "react-helmet"; import { getTextOptions } from "../lib/get-text-options"; -import { - State, -} from "../types/dictionary-types"; +import { State } from "../types/dictionary-types"; -function ReviewTask({ reviewTask, textOptions }: { reviewTask: FT.ReviewTask, textOptions: T.TextOptions }) { - function handleDelete() { - deleteFromLocalDb("reviewTasks", reviewTask._id); - } - const queryParamData = { - ...reviewTask.sTs ? { - sTs: reviewTask.sTs, - } : {}, - ..."comment" in reviewTask ? { - comment: reviewTask.comment, - } : {}, - ..."entry" in reviewTask ? { - id: reviewTask.entry.ts, - } : {}, - } as URLSearchParams; - const queryString = new URLSearchParams(queryParamData).toString(); - return
-
-
- -
-
- {reviewTask.type !== "issue" && - -
-
- {reviewTask.type === "entry suggestion" &&
- New Entry Suggestion -
} - -
"{reviewTask.comment}"
-
{reviewTask.user.name} - {reviewTask.user.email}
-
-
- +function ReviewTask({ + reviewTask, + textOptions, +}: { + reviewTask: FT.ReviewTask; + textOptions: T.TextOptions; +}) { + function handleDelete() { + deleteFromLocalDb("reviewTasks", reviewTask._id); + } + const queryParamData = { + ...(reviewTask.sTs + ? { + sTs: reviewTask.sTs, } + : {}), + ...("comment" in reviewTask + ? { + comment: reviewTask.comment, + } + : {}), + ...("entry" in reviewTask + ? { + id: reviewTask.entry.ts, + } + : {}), + } as URLSearchParams; + const queryString = new URLSearchParams(queryParamData).toString(); + return ( +
+
+
+ +
+
+ {reviewTask.type !== "issue" && ( + +
+
+ {reviewTask.type === "entry suggestion" && ( +
New Entry Suggestion
+ )} + +
"{reviewTask.comment}"
+
+ {reviewTask.user.name} - {reviewTask.user.email} +
+
+
+ + )}
- + ); } export default function ReviewTasks({ state }: { state: State }) { - const textOptions = getTextOptions(state); - return
- - Review Tasks - LingDocs Pashto Dictionary - -

Review Tasks

- {state.reviewTasks.length ? - state.reviewTasks.map((reviewTask, i) => ) - :

None

- } -
; -} \ No newline at end of file + const textOptions = getTextOptions(state); + return ( +
+ + Review Tasks - LingDocs Pashto Dictionary + +

Review Tasks

+ {state.reviewTasks.length ? ( + state.reviewTasks.map((reviewTask, i) => ( + + )) + ) : ( +

None

+ )} +
+ ); +} diff --git a/website/src/screens/Wordlist.tsx b/website/src/screens/Wordlist.tsx index 13a8933..26926d8 100644 --- a/website/src/screens/Wordlist.tsx +++ b/website/src/screens/Wordlist.tsx @@ -12,33 +12,24 @@ import WordlistWordEditor from "../components/WordlistWordEditor"; import { useState, useEffect } from "react"; import Helmet from "react-helmet"; import { - deleteWordFromWordlist, - updateWordlistWord, - getWordlistCsv, - searchWordlist, - calculateWordsToDelete, - hasAttachment, + deleteWordFromWordlist, + updateWordlistWord, + getWordlistCsv, + searchWordlist, + calculateWordsToDelete, + hasAttachment, } from "../lib/wordlist-database"; -import { - ButtonSelect, - InlinePs, - removeFVarients, -} from "@lingdocs/ps-react"; -import { - Modal, - Button, -} from "react-bootstrap"; +import { ButtonSelect, InlinePs, removeFVarients } from "@lingdocs/ps-react"; +import { Modal, Button } from "react-bootstrap"; import WordlistImage from "../components/WordlistImage"; import ReviewScoreInput from "../components/ReviewScoreInput"; import { isPashtoScript } from "../lib/is-pashto"; import { - forReview, - nextUpForReview, - practiceWord, + forReview, + nextUpForReview, + practiceWord, } from "../lib/spaced-repetition"; -import { - pageSize, -} from "../lib/dictionary"; +import { pageSize } from "../lib/dictionary"; import { textBadge } from "../lib/badges"; import { SuperMemoGrade } from "supermemo"; import AudioPlayButton from "../components/AudioPlayButton"; @@ -47,11 +38,11 @@ import relativeTime from "dayjs/plugin/relativeTime.js"; import hitBottom from "../lib/hitBottom"; import { Link } from "react-router-dom"; import { - Options, - WordlistWord, - Language, - OptionsAction, - WordlistMode, + Options, + WordlistWord, + Language, + OptionsAction, + WordlistMode, } from "../types/dictionary-types"; import UpgradePrices from "../components/UpgradePrices"; @@ -60,467 +51,592 @@ const cleanupIcon = "broom"; dayjs.extend(relativeTime); const reviewLanguageOptions: { - label: string, - value: Language, + label: string; + value: Language; }[] = [ - { - label: "Pashto", - value: "Pashto", - }, - { - label: "English", - value: "English", - }, + { + label: "Pashto", + value: "Pashto", + }, + { + label: "English", + value: "English", + }, ]; function paginate(arr: T[], page: number): T[] { - return arr.slice(0, page * pageSize); + return arr.slice(0, page * pageSize); } function amountOfWords(number: number): string { - if (number === 0) { - return "ALL your words"; - } - return `${number} word${number !== 1 ? "s" : ""}`; + if (number === 0) { + return "ALL your words"; + } + return `${number} word${number !== 1 ? "s" : ""}`; } let popupRef: Window | null = null; -function Wordlist({ options, wordlist, isolateEntry, optionsDispatch, user, loadUser }: { - options: Options, - wordlist: WordlistWord[], - isolateEntry: (ts: number) => void, - optionsDispatch: (action: OptionsAction) => void, - user: AT.LingdocsUser | undefined, - loadUser: () => void, +function Wordlist({ + options, + wordlist, + isolateEntry, + optionsDispatch, + user, + loadUser, +}: { + options: Options; + wordlist: WordlistWord[]; + isolateEntry: (ts: number) => void; + optionsDispatch: (action: OptionsAction) => void; + user: AT.LingdocsUser | undefined; + loadUser: () => void; }) { + // @ts-ignore + const [wordOpen, setWordOpen] = useState(undefined); + const [wordQuizzing, setWordQuizzing] = useState( + undefined + ); + const [showGuide, setShowGuide] = useState(true); + const [showingDownload, setShowingDownload] = useState(false); + const [wordlistDownloadSort, setWordlistDownloadSort] = useState< + "alphabetical" | "time" + >("time"); + const [page, setPage] = useState(1); + const [wordlistSearchValue, setWordlistSearchValue] = useState(""); + const [filteredWords, setFilteredWords] = useState([]); + const [showingCleanup, setShowingCleanup] = useState(false); + const [monthsBackToKeep, setMonthsBackToKeep] = useState(6); + const [wordsToDelete, setWordsToDelete] = useState([]); + const [startedWithWordsToReview] = useState( + forReview(wordlist).length !== 0 + ); + useEffect(() => { + window.addEventListener("message", handleIncomingMessage); + return () => { + window.removeEventListener("message", handleIncomingMessage); + }; + // eslint-disable-next-line + }, []); + // TODO put the account url in an imported constant + function handleIncomingMessage(event: MessageEvent) { + if ( + event.origin === "https://account.lingdocs.com" && + event.data === "signed in" && + popupRef + ) { + loadUser(); + popupRef.close(); + } + } + useEffect(() => { + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + // eslint-disable-next-line + }, []); + const toReview = forReview(wordlist); + const textOptions = options.textOptionsRecord.textOptions; + function handleScroll() { + // TODO: DON'T HAVE ENDLESS PAGE INCREASING + if (hitBottom() && options.wordlistMode === "browse") { + setPage((page) => page + 1); + } + } + function handleWordClickBrowse(id: string) { + setWordOpen(id === wordOpen ? undefined : id); + } + function handleWordClickReview(id: string) { + setWordQuizzing(id === wordQuizzing ? undefined : id); + } + function deleteWord(id: string) { + deleteWordFromWordlist(id); + } + function handleAnswer(word: WordlistWord, grade: SuperMemoGrade) { + setWordQuizzing(undefined); + setShowGuide(false); + updateWordlistWord(practiceWord(word, grade)); + } + function handleSearchValueChange(value: string) { + setWordlistSearchValue(value); + const results = value ? searchWordlist(value, wordlist, textOptions) : []; + setFilteredWords(results); + } + async function handleGetWordlistCSV() { + const blob = await getWordlistCsv(wordlistDownloadSort); + setShowingDownload(false); + var a = document.createElement("a"); + document.body.appendChild(a); // @ts-ignore - const [wordOpen, setWordOpen] = useState(undefined); - const [wordQuizzing, setWordQuizzing] = useState(undefined); - const [showGuide, setShowGuide] = useState(true); - const [showingDownload, setShowingDownload] = useState(false); - const [wordlistDownloadSort, setWordlistDownloadSort] = useState<"alphabetical" | "time">("time"); - const [page, setPage] = useState(1); - const [wordlistSearchValue, setWordlistSearchValue] = useState(""); - const [filteredWords, setFilteredWords] = useState([]); - const [showingCleanup, setShowingCleanup] = useState(false); - const [monthsBackToKeep, setMonthsBackToKeep] = useState(6); - const [wordsToDelete, setWordsToDelete] = useState([]); - const [startedWithWordsToReview] = useState(forReview(wordlist).length !== 0); - useEffect(() => { - window.addEventListener("message", handleIncomingMessage); - return () => { - window.removeEventListener("message", handleIncomingMessage); - }; - // eslint-disable-next-line - }, []); - // TODO put the account url in an imported constant - function handleIncomingMessage(event: MessageEvent) { - if ( - event.origin === "https://account.lingdocs.com" - && (event.data === "signed in") - && popupRef - ) { - loadUser(); - popupRef.close(); - } - } - useEffect(() => { - window.addEventListener("scroll", handleScroll); - return () => window.removeEventListener("scroll", handleScroll); - // eslint-disable-next-line - }, []); - const toReview = forReview(wordlist); - const textOptions = options.textOptionsRecord.textOptions; - function handleScroll() { - // TODO: DON'T HAVE ENDLESS PAGE INCREASING - if (hitBottom() && options.wordlistMode === "browse") { - setPage(page => page + 1); - } - } - function handleWordClickBrowse(id: string) { - setWordOpen(id === wordOpen ? undefined : id); - } - function handleWordClickReview(id: string) { - setWordQuizzing(id === wordQuizzing ? undefined : id); - } - function deleteWord(id: string) { - deleteWordFromWordlist(id); - } - function handleAnswer(word: WordlistWord, grade: SuperMemoGrade) { - setWordQuizzing(undefined); - setShowGuide(false); - updateWordlistWord(practiceWord(word, grade)); - } - function handleSearchValueChange(value: string) { - setWordlistSearchValue(value); - const results = value ? searchWordlist(value, wordlist, textOptions) : []; - setFilteredWords(results); - } - async function handleGetWordlistCSV() { - const blob = await getWordlistCsv(wordlistDownloadSort); - setShowingDownload(false); - var a = document.createElement("a"); - document.body.appendChild(a); - // @ts-ignore - a.style = "display: none"; - const url = window.URL.createObjectURL(blob); - a.href = url; - a.download = "wordlist.csv"; - a.click(); - window.URL.revokeObjectURL(url); - } - function handleShowCleanup() { - setWordsToDelete( - calculateWordsToDelete(wordlist, monthsBackToKeep) - ); - setShowingCleanup(true); - } - function handleCloseCleanup() { - setShowingCleanup(false); - setWordsToDelete([]); - } - function handleCleanup() { - const toDelete = calculateWordsToDelete(wordlist, monthsBackToKeep); - deleteWordFromWordlist(toDelete); - setShowingCleanup(false); - } - function handleOpenSignup() { - popupRef = window.open("https://account.lingdocs.com", "account", "height=800,width=500,top=50,left=400"); - } - function WordlistBrowsingWord({ word } : { word: WordlistWord }) { - const [confirmDelete, setConfirmDelete] = useState(false); - return
- handleWordClickBrowse(word._id)} - /> - {hasAttachment(word, "audio") && } - {word._id === wordOpen ? <> -
-
- -
-
- {!confirmDelete ? - - : -
- - -
- } -
-
- {(Date.now() < word.dueDate) &&
- up for review {dayjs().to(dayjs(word.dueDate))} -
} - {/* possible "next review" in x days, mins etc. calculated from milliseconds */} - - - : - word.notes &&
handleWordClickBrowse(word._id)} - dir="auto" - style={{ textAlign: isPashtoScript(word.notes[0]) ? "right" : "left" }} + a.style = "display: none"; + const url = window.URL.createObjectURL(blob); + a.href = url; + a.download = "wordlist.csv"; + a.click(); + window.URL.revokeObjectURL(url); + } + function handleShowCleanup() { + setWordsToDelete(calculateWordsToDelete(wordlist, monthsBackToKeep)); + setShowingCleanup(true); + } + function handleCloseCleanup() { + setShowingCleanup(false); + setWordsToDelete([]); + } + function handleCleanup() { + const toDelete = calculateWordsToDelete(wordlist, monthsBackToKeep); + deleteWordFromWordlist(toDelete); + setShowingCleanup(false); + } + function handleOpenSignup() { + popupRef = window.open( + "https://account.lingdocs.com", + "account", + "height=800,width=500,top=50,left=400" + ); + } + function WordlistBrowsingWord({ word }: { word: WordlistWord }) { + const [confirmDelete, setConfirmDelete] = useState(false); + return ( +
+ handleWordClickBrowse(word._id)} + /> + {hasAttachment(word, "audio") && } + {word._id === wordOpen ? ( + <> +
+
+
- } - {(hasAttachment(word, "image") && word._id !== wordOpen) && } -
; - } - function WordlistReviewWord({ word } : { word: WordlistWord }) { - const beingQuizzed = word._id === wordQuizzing; - return
-
handleWordClickReview(word._id)}> -
-
- {options.wordlistReviewLanguage === "Pashto" - ? {{ p: word.entry.p, f: word.entry.f }} - : word.entry.e - } -
- {beingQuizzed &&
- {options.wordlistReviewLanguage === "Pashto" - ?
{word.entry.e}
- : - {{ p: word.entry.p, f: word.entry.f }} + + {` `}View Entry + +
+
+ {!confirmDelete ? ( + + ) : ( +
+ + +
+ )} +
+
+ {Date.now() < word.dueDate && ( +
+ up for review {dayjs().to(dayjs(word.dueDate))} +
+ )} + {/* possible "next review" in x days, mins etc. calculated from milliseconds */} + + + ) : ( + word.notes && ( +
handleWordClickBrowse(word._id)} + dir="auto" + style={{ + textAlign: isPashtoScript(word.notes[0]) ? "right" : "left", + }} + > + {word.notes} +
+ ) + )} + {hasAttachment(word, "image") && word._id !== wordOpen && ( + + )} +
+ ); + } + function WordlistReviewWord({ word }: { word: WordlistWord }) { + const beingQuizzed = word._id === wordQuizzing; + return ( +
+
handleWordClickReview(word._id)} + > +
+
+ {options.wordlistReviewLanguage === "Pashto" ? ( + + {{ p: word.entry.p, f: word.entry.f }} + + ) : ( + word.entry.e + )} +
+ {beingQuizzed && ( +
+ {options.wordlistReviewLanguage === "Pashto" ? ( +
{word.entry.e}
+ ) : ( + + {{ p: word.entry.p, f: word.entry.f }} + + )} +
{word.notes}
+ {hasAttachment(word, "audio") && ( + + )} + {hasAttachment(word, "image") && } +
+ )} +
+
+ {beingQuizzed && ( + handleAnswer(word, grade)} + guide={showGuide} + /> + )} +
+ ); + } + if (!user || user.level === "basic") { + return ( +
+ + Wordlist - LingDocs Pashto Dictionary + +
+

Wordlist

+
+
+ {!user ? ( +

+ Sign in to upgrade and enable wordlist +

+ ) : ( +

+ Upgrade to a student account to enable the + wordlist +

+ )} +
+

Features:

+
    +
  • Save your wordlist and sync across devices
  • +
  • Save text, audio, or visual context for words
  • +
  • Review words with Anki-style spaced repetition
  • +
+ {!user ? ( + <> +

Cost:

+
    +
  • $1/month or $10/year - cancel any time
  • +
+ + ) : ( + + )} + {!user && ( + + )} +
+
+
+ ); + } + return ( +
+ + Wordlist - LingDocs Pashto Dictionary + +
+

Wordlist

+ {wordlist.length > 0 && ( +
+
+ +
+
+ +
+
+ )} +
+ {!wordlist.length ? ( + + ) : ( + <> +
+ { + optionsDispatch({ + type: "changeWordlistMode", + payload: p as WordlistMode, + }); + }} + /> +
+ {options.wordlistMode === "browse" ? ( +
+ + {paginate( + wordlistSearchValue ? filteredWords : wordlist, + page + ).map((word) => ( + + ))} +
+ ) : ( +
+
Show:
+
+ { + optionsDispatch({ + type: "changeWordlistReviewLanguage", + payload: p as Language, + }); + }} + /> +
+
+ {/* TODO: ISSUE WITH NOT USING PAGINATE HERE BECAUSE OF IMAGE RELOADING BUGINESS WHEN HITTING BOTTOM */} + {toReview.length === 0 ? ( + startedWithWordsToReview ? ( +

All done review 🎉

+ ) : ( + (() => { + const nextUp = nextUpForReview(wordlist); + const { e, ...ps } = nextUp.entry; + return ( +
+
None to review
+

+ Next word up for review{" "} + {dayjs().to(nextUp.dueDate)}:{" "} + + {removeFVarients(ps)} - } -

{word.notes}
- {hasAttachment(word, "audio") && } - {hasAttachment(word, "image") && } -
} -
-
- {beingQuizzed && handleAnswer(word, grade)} - guide={showGuide} - />} -
- } - if (!user || user.level === "basic") { - return
- - Wordlist - LingDocs Pashto Dictionary - -
-

Wordlist

-
-
- {!user - ?

Sign in to upgrade and enable wordlist

- :

Upgrade to a student account to enable the wordlist

} -
-

Features:

-
    -
  • Save your wordlist and sync across devices
  • -
  • Save text, audio, or visual context for words
  • -
  • Review words with Anki-style spaced repetition
  • -
- {!user ? <> -

Cost:

-
    -
  • $1/month or $10/year - cancel any time
  • -
- : } - {!user && } -
-
-
; - } - return
- - Wordlist - LingDocs Pashto Dictionary - -
-

Wordlist

- {wordlist.length > 0 && -
-
- +

-
- -
-
- } + ); + })() + ) + ) : ( + toReview.map((word) => ( + + )) + )} +
- {!wordlist.length ? - - : - <> -
- { - optionsDispatch({ type: "changeWordlistMode", payload: p as WordlistMode }); - }} - /> -
- {options.wordlistMode === "browse" - ?
- - {paginate(wordlistSearchValue ? filteredWords : wordlist, page).map((word) => ( - - ))} -
- :
-
Show:
-
- { - optionsDispatch({ type: "changeWordlistReviewLanguage", payload: p as Language }); - }} - /> -
-
- {/* TODO: ISSUE WITH NOT USING PAGINATE HERE BECAUSE OF IMAGE RELOADING BUGINESS WHEN HITTING BOTTOM */} - {toReview.length === 0 - ? (startedWithWordsToReview - ?

All done review 🎉

- : (() => { - const nextUp = nextUpForReview(wordlist); - const { e, ...ps } = nextUp.entry; - return
-
None to review
-

Next word up for review {dayjs().to(nextUp.dueDate)}: - {removeFVarients(ps)} -

-
; - })() - ) - : toReview.map((word) => ( - - )) - } -
-
- } - {(wordlistSearchValue && filteredWords.length === 0) &&
-
None found
-
} - - } - setShowingDownload(false)}> - - Download Wordlist CSV - - -

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.

-

How should we sort your wordlist?

- { - setWordlistDownloadSort(p as "alphabetical" | "time"); - }} - /> -
- - - - -
- - - Wordlist Cleanup - - -

You have {amountOfWords(wordlist.length)} in your wordlist.

-

Delete:

- { - const months = parseInt(p); - setWordsToDelete( - calculateWordsToDelete(wordlist, months), - ); - setMonthsBackToKeep(months); - }} - /> -

This will delete {amountOfWords(wordsToDelete.length)}.

-
- - - - -
-
+ )} + {wordlistSearchValue && filteredWords.length === 0 && ( +
+
None found
+
+ )} + + )} + setShowingDownload(false)}> + + + Download Wordlist CSV + + + +

+ 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. +

+

How should we sort your wordlist?

+ { + setWordlistDownloadSort(p as "alphabetical" | "time"); + }} + /> +
+ + + + +
+ + + + Wordlist Cleanup + + + +

You have {amountOfWords(wordlist.length)} in your wordlist.

+

Delete:

+ { + const months = parseInt(p); + setWordsToDelete(calculateWordsToDelete(wordlist, months)); + setMonthsBackToKeep(months); + }} + /> +

+ This will delete {amountOfWords(wordsToDelete.length)}. +

+
+ + + + +
+
+ ); } function EmptyWordlistNotice() { - return
-

Your wordlist is empty.

-

To add a word to your wordlist, choose a word while searching and click on the icon.

-
; + return ( +
+

Your wordlist is empty.

+

+ To add a word to your wordlist, choose a word while searching and click + on the icon. +

+
+ ); } -function WordlistSearchBar({ handleChange, value }: { - value: string, - handleChange: (value: string) => void, +function WordlistSearchBar({ + handleChange, + value, +}: { + value: string; + handleChange: (value: string) => void; }) { - return
- handleChange(e.target.value)} - name="search" - className="form-control py-2 border-right-0 border" - autoFocus={false} - autoComplete="off" - autoCorrect="off" - autoCapitalize="off" - spellCheck={false} - dir="auto" - data-testid="wordlistSearchInput" - data-lpignore="true" - /> - - handleChange("")} - data-testid="wordlistClearButton" - > - - + return ( +
+ handleChange(e.target.value)} + name="search" + className="form-control py-2 border-right-0 border" + autoFocus={false} + autoComplete="off" + autoCorrect="off" + autoCapitalize="off" + spellCheck={false} + dir="auto" + data-testid="wordlistSearchInput" + data-lpignore="true" + /> + + handleChange("")} + data-testid="wordlistClearButton" + > + -
; +
+
+ ); } -export default Wordlist; \ No newline at end of file +export default Wordlist;