From 211d9b3446bf943e44a9ef2de3f94ecd5e314a55 Mon Sep 17 00:00:00 2001 From: adueck Date: Sat, 15 Jul 2023 00:49:02 +0400 Subject: [PATCH] update ga --- website/package.json | 2 +- website/src/App.tsx | 1349 +++++++++++++++++++++++------------------- website/yarn.lock | 8 +- 3 files changed, 745 insertions(+), 614 deletions(-) diff --git a/website/package.json b/website/package.json index 100d4c6..d4b5c87 100644 --- a/website/package.json +++ b/website/package.json @@ -31,7 +31,7 @@ "react-bootstrap": "^1.5.1", "react-dom": "^17.0.2", "react-dropzone": "^11.3.2", - "react-ga": "^3.3.0", + "react-ga4": "^2.1.0", "react-helmet": "^6.1.0", "react-image-crop": "^8.6.9", "react-image-file-resizer": "^0.4.4", diff --git a/website/src/App.tsx b/website/src/App.tsx index a1b9675..9805b65 100644 --- a/website/src/App.tsx +++ b/website/src/App.tsx @@ -10,7 +10,12 @@ // sync on initialization and cancel sync on de-initialization import { Component } from "react"; -import { defaultTextOptions, revertSpelling, standardizePashto, Types as T } from "@lingdocs/ps-react"; +import { + defaultTextOptions, + revertSpelling, + standardizePashto, + Types as T, +} from "@lingdocs/ps-react"; import { withRouter, Route, RouteComponentProps, Link } from "react-router-dom"; import Helmet from "react-helmet"; import BottomNavItem from "./components/BottomNavItem"; @@ -26,42 +31,29 @@ import IsolatedEntry from "./screens/IsolatedEntry"; import PrivacyPolicy from "./screens/PrivacyPolicy"; import Wordlist from "./screens/Wordlist"; import { - saveOptions, - readOptions, - saveUser, - readUser, + saveOptions, + readOptions, + saveUser, + readUser, } from "./lib/local-storage"; import { allEntries, dictionary, pageSize } from "./lib/dictionary"; -import { - optionsReducer, - textOptionsReducer, -} from "./lib/options-reducer"; +import { optionsReducer, textOptionsReducer } from "./lib/options-reducer"; import hitBottom from "./lib/hitBottom"; import getWordId from "./lib/get-word-id"; import { CronJob } from "cron"; import Mousetrap from "mousetrap"; +import { sendSubmissions } from "./lib/submissions"; +import { getUser } from "./lib/backend-calls"; +import { getWordlist } from "./lib/wordlist-database"; import { - sendSubmissions, -} from "./lib/submissions"; -import { - getUser, -} from "./lib/backend-calls"; -import { - getWordlist, -} from "./lib/wordlist-database"; -import { - startLocalDbs, - stopLocalDbs, - getAllDocsLocalDb, + startLocalDbs, + stopLocalDbs, + getAllDocsLocalDb, } from "./lib/pouch-dbs"; -import { - forReview, -} from "./lib/spaced-repetition"; -import { - textBadge, -} from "./lib/badges"; +import { forReview } from "./lib/spaced-repetition"; +import { textBadge } from "./lib/badges"; import * as AT from "./types/account-types"; -import ReactGA from "react-ga"; +import ReactGA from "react-ga4"; // tslint:disable-next-line import "@fortawesome/fontawesome-free/css/all.css"; import "./custom-bootstrap.css"; @@ -71,623 +63,762 @@ import { getTextOptions } from "./lib/get-text-options"; import { getTextFromShareTarget } from "./lib/share-target"; import { objIsEqual, userObjIsEqual } from "./lib/misc-helpers"; import { - State, - TextOptionsRecord, - TextOptionsAction, - OptionsAction, + State, + TextOptionsRecord, + TextOptionsAction, + OptionsAction, } from "./types/dictionary-types"; import PhraseBuilder from "./screens/PhraseBuilder"; import { searchAllInflections } from "./lib/search-all-inflections"; -import { - addToWordlist, -} from "./lib/wordlist-database"; +import { addToWordlist } from "./lib/wordlist-database"; import ScriptToPhonetics from "./screens/ScriptToPhonetics"; // to allow Moustrap key combos even when input fields are in focus Mousetrap.prototype.stopCallback = function () { - return false; -} + return false; +}; const prod = document.location.hostname === "dictionary.lingdocs.com"; if (prod) { - // TODO: migrate to https://www.npmjs.com/package/react-ga4 - ReactGA.initialize("UA-196576671-1"); - ReactGA.set({ anonymizeIp: true }); + // TODO: migrate to https://www.npmjs.com/package/react-ga4 + ReactGA.initialize("386396674"); + ReactGA.set({ anonymizeIp: true }); } const possibleLandingPages = [ - "/", "/about", "/settings", "/word", "/account", "/new-entries", "/share-target", "/phrase-builder", - "/privacy", "/script-to-phonetics" -]; -const editorOnlyPages = [ - "/edit", "/review-tasks", + "/", + "/about", + "/settings", + "/word", + "/account", + "/new-entries", + "/share-target", + "/phrase-builder", + "/privacy", + "/script-to-phonetics", ]; +const editorOnlyPages = ["/edit", "/review-tasks"]; class App extends Component { - constructor(props: RouteComponentProps) { - super(props); - const savedOptions = readOptions(); - this.state = { - dictionaryStatus: "loading", - dictionaryInfo: undefined, - // TODO: Choose between the saved options and the options in the saved user - options: savedOptions ? savedOptions : { - language: "Pashto", - searchType: "fuzzy", - searchBarStickyFocus: false, - theme: (window.matchMedia?.("(prefers-color-scheme: dark)").matches) ? "dark" : "light", - textOptionsRecord: { - lastModified: Date.now() as AT.TimeStamp, - textOptions: defaultTextOptions, - }, - wordlistMode: "browse", - wordlistReviewLanguage: "Pashto", - wordlistReviewBadge: true, - searchBarPosition: "top", + constructor(props: RouteComponentProps) { + super(props); + const savedOptions = readOptions(); + this.state = { + dictionaryStatus: "loading", + dictionaryInfo: undefined, + // TODO: Choose between the saved options and the options in the saved user + options: savedOptions + ? savedOptions + : { + language: "Pashto", + searchType: "fuzzy", + searchBarStickyFocus: false, + theme: window.matchMedia?.("(prefers-color-scheme: dark)").matches + ? "dark" + : "light", + textOptionsRecord: { + lastModified: Date.now() as AT.TimeStamp, + textOptions: defaultTextOptions, }, - searchValue: "", - page: 1, - isolatedEntry: undefined, - results: [], - wordlist: [], - reviewTasks: [], - user: readUser(), - inflectionSearchResults: undefined - }; - this.handleOptionsUpdate = this.handleOptionsUpdate.bind(this); - this.handleTextOptionsUpdate = this.handleTextOptionsUpdate.bind(this); - this.handleSearchValueChange = this.handleSearchValueChange.bind(this); - this.handleIsolateEntry = this.handleIsolateEntry.bind(this); - this.handleScroll = this.handleScroll.bind(this); - this.handleGoBack = this.handleGoBack.bind(this); - this.handleLoadUser = this.handleLoadUser.bind(this); - this.handleRefreshWordlist = this.handleRefreshWordlist.bind(this); - this.handleRefreshReviewTasks = this.handleRefreshReviewTasks.bind(this); - this.handleDictionaryUpdate = this.handleDictionaryUpdate.bind(this); - this.handleInflectionSearch = this.handleInflectionSearch.bind(this); - } + wordlistMode: "browse", + wordlistReviewLanguage: "Pashto", + wordlistReviewBadge: true, + searchBarPosition: "top", + }, + searchValue: "", + page: 1, + isolatedEntry: undefined, + results: [], + wordlist: [], + reviewTasks: [], + user: readUser(), + inflectionSearchResults: undefined, + }; + this.handleOptionsUpdate = this.handleOptionsUpdate.bind(this); + this.handleTextOptionsUpdate = this.handleTextOptionsUpdate.bind(this); + this.handleSearchValueChange = this.handleSearchValueChange.bind(this); + this.handleIsolateEntry = this.handleIsolateEntry.bind(this); + this.handleScroll = this.handleScroll.bind(this); + this.handleGoBack = this.handleGoBack.bind(this); + this.handleLoadUser = this.handleLoadUser.bind(this); + this.handleRefreshWordlist = this.handleRefreshWordlist.bind(this); + this.handleRefreshReviewTasks = this.handleRefreshReviewTasks.bind(this); + this.handleDictionaryUpdate = this.handleDictionaryUpdate.bind(this); + this.handleInflectionSearch = this.handleInflectionSearch.bind(this); + } - public componentDidMount() { - window.addEventListener("scroll", this.handleScroll); - if (!possibleLandingPages.includes(this.props.location.pathname)) { - this.props.history.replace("/"); - } - if (prod && (!(this.state.user?.level === "editor"))) { - ReactGA.pageview(window.location.pathname + window.location.search); - } - dictionary.initialize().then((r) => { - this.cronJob.start(); - this.setState({ - dictionaryStatus: "ready", - dictionaryInfo: r.dictionaryInfo, - }); - this.handleLoadUser(); - // incase it took forever and timed out - might need to reinitialize the wordlist here ?? - if (this.state.user) { - startLocalDbs(this.state.user, { wordlist: this.handleRefreshWordlist, reviewTasks: this.handleRefreshReviewTasks }); - } - if (this.props.location.pathname === "/word") { - const wordId = getWordId(this.props.location.search); - if (wordId) { - const word = dictionary.findOneByTs(wordId); - if (word) { - this.setState({ searchValue: word.p }); - } - this.handleIsolateEntry(wordId); - } else { - // TODO: Make a word not found screen - console.error("somehow had a word path without a word id param"); - this.props.history.replace("/"); - } - } - if (this.props.location.pathname === "/share-target") { - const searchString = getTextFromShareTarget(window.location); - this.props.history.replace("/"); - if (this.state.options.language === "English") { - this.handleOptionsUpdate({ type: "toggleLanguage" }); - } - if (this.state.options.searchType === "alphabetical") { - this.handleOptionsUpdate({ type: "toggleSearchType" }); - } - this.handleSearchValueChange(searchString); - } - if (this.props.location.pathname === "/new-entries") { - this.setState({ - results: dictionary.getNewWordsThisMonth(), - page: 1, - }); - } - if (r.response === "loaded from saved") { - this.handleDictionaryUpdate(); - } - }).catch((error) => { - console.error(error); - this.setState({ dictionaryStatus: "error loading" }); + public componentDidMount() { + window.addEventListener("scroll", this.handleScroll); + if (!possibleLandingPages.includes(this.props.location.pathname)) { + this.props.history.replace("/"); + } + if (prod && !(this.state.user?.level === "editor")) { + ReactGA.send({ + hitType: "pageview", + page: window.location.pathname + window.location.search, + }); + } + dictionary + .initialize() + .then((r) => { + this.cronJob.start(); + this.setState({ + dictionaryStatus: "ready", + dictionaryInfo: r.dictionaryInfo, }); - document.documentElement.setAttribute("data-theme", this.state.options.theme); - /* istanbul ignore if */ - if (window.matchMedia) { - const prefersDarkQuery = window.matchMedia("(prefers-color-scheme: dark)"); - prefersDarkQuery.addListener((e) => { - if (e.matches) { - this.handleOptionsUpdate({ type: "changeTheme", payload: "dark" }); - } - }); - const prefersLightQuery = window.matchMedia("(prefers-color-scheme: light)"); - prefersLightQuery.addListener((e) => { - if (e.matches) { - this.handleOptionsUpdate({ type: "changeTheme", payload: "light" }); - } - }); - } - // shortcuts to isolote word in search results - Mousetrap.bind(["1", "2", "3", "4", "5", "6", "7", "8", "9"], (e) => { - e.preventDefault(); - if (e.repeat) { - return; - } - if (this.props.location.pathname !== "/search") { - return; - } - const toIsolate = this.state.results[Number(e.key) - 1]; - if (!toIsolate) { - return; - } - this.handleIsolateEntry(toIsolate.ts); - }) - Mousetrap.bind(["ctrl+down", "ctrl+up", "command+down", "command+up"], (e) => { - e.preventDefault(); - if (e.repeat) { - return; - } - this.handleOptionsUpdate({ type: "toggleLanguage" }); - }); - Mousetrap.bind(["ctrl+b", "command+b"], (e) => { - e.preventDefault(); - if (e.repeat) { - return; - } - this.handleSearchValueChange(""); - }); - Mousetrap.bind(["ctrl+i", "command+i"], (e) => { - e.preventDefault(); - if (e.repeat) { - return; - } - if (!this.state.searchValue) { - return; - } - this.handleInflectionSearch(); - }); - Mousetrap.bind(["ctrl+s", "command+s"], (e) => { - if (this.state.user?.level === "basic") { - return; - } - e.preventDefault(); - if (!this.state.isolatedEntry) { - return; - } - const toAdd = { - entry: this.state.isolatedEntry, - notes: "", - }; - addToWordlist(toAdd); - }); - Mousetrap.bind(["ctrl+\\", "command+\\"], (e) => { - e.preventDefault(); - if (e.repeat) { - return; - } - if (this.state.user?.level === "basic") { - return; - } - if (this.props.location.pathname !== "/wordlist") { - this.props.history.push("/wordlist"); - } else { - this.handleGoBack(); - } - }); - } - - public componentWillUnmount() { - window.removeEventListener("scroll", this.handleScroll); - this.cronJob.stop(); - stopLocalDbs(); - Mousetrap.unbind(["ctrl+down", "ctrl+up", "command+down", "command+up"]); - Mousetrap.unbind(["ctrl+b", "command+b"]); - Mousetrap.unbind(["ctrl+\\", "command+\\"]); - Mousetrap.unbind(["ctrl+s", "command+s"]); - Mousetrap.unbind(["ctrl+i", "command+i"]); - Mousetrap.unbind(["1", "2", "3", "4", "5", "6", "7", "8", "9"]); - } - - public componentDidUpdate(prevProps: RouteComponentProps) { - if (this.props.location.pathname !== prevProps.location.pathname) { - if (prod && (!(this.state.user?.level === "editor"))) { - ReactGA.pageview(window.location.pathname + window.location.search); - } - if (this.props.location.pathname === "/") { - this.handleSearchValueChange(""); - } - if (this.props.location.pathname === "/new-entries") { - this.setState({ - results: dictionary.getNewWordsThisMonth(), - page: 1, - }); - } - if (editorOnlyPages.includes(this.props.location.pathname) && !(this.state.user?.level === "editor")) { - this.props.history.replace("/"); - } - } - if (getWordId(this.props.location.search) !== getWordId(prevProps.location.search)) { - if (prod && ((this.state.user?.level !== "editor"))) { - ReactGA.pageview(window.location.pathname + window.location.search); - } - const wordId = getWordId(this.props.location.search); - /* istanbul ignore else */ - if (wordId) { - this.handleIsolateEntry(wordId, true); - } else { - this.setState({ isolatedEntry: undefined }) - } - } - // if (!["/wordlist", "/settings", "/review-tasks"].includes(this.props.location.pathname)) { - // window.scrollTo(0, 0); - // } - } - - private async handleLoadUser(): Promise { - try { - const prevUser = this.state.user; - const user = await getUser(); - if (user === "offline") { - return; - } - if (user) { - sendSubmissions(); - } - if (!user) { - if (this.state.user) { - console.log("setting state user because user is newly undefined"); - this.setState({ user: undefined }); - } - saveUser(undefined); - return; - } - if (!userObjIsEqual(prevUser, user)) { - console.log("setting state user because something is different about the user") - this.setState({ user }); - saveUser(user); - } - if (user) { - startLocalDbs(user, { wordlist: this.handleRefreshWordlist, reviewTasks: this.handleRefreshReviewTasks }); - } else { - stopLocalDbs(); - } - } catch (err) { - console.error("error checking user level", err); - } - } - - private handleDictionaryUpdate() { - // TODO: fix - what the heck happened and what's going on here - dictionary.update(() => { - // this.setState({ dictionaryStatus: "updating" }); - }).then(({ dictionaryInfo }) => { - //if (this.state.dictionaryInfo?.release !== dictionaryInfo?.release) { - // to avoid unnecessary re-rendering that breaks things - this.setState({ - dictionaryStatus: "ready", - dictionaryInfo, - }); - //} - }).catch(() => { - this.setState({ dictionaryStatus: "error loading" }); - }); - } - - private handleOptionsUpdate(action: OptionsAction) { - if (action.type === "changeTheme") { - document.documentElement.setAttribute("data-theme", action.payload); - } - // TODO: use a seperate reducer for changing text options (otherwise you could just be updating the saved text options instead of the user text options that the program is going off of) - const options = optionsReducer(this.state.options, action); - saveOptions(options); - if (action.type === "toggleLanguage" || action.type === "toggleSearchType") { - if (this.props.location.pathname !== "/new-entries") { - this.setState(prevState => ({ - options, - page: 1, - results: dictionary.search({ ...prevState, options }), - })); - window.scrollTo(0, 0); - } else { - this.setState({ options }); - } - } else { - !objIsEqual(this.state.options, options) && this.setState({ options }); - } - } - - private handleTextOptionsUpdate(action: TextOptionsAction) { - const textOptions = textOptionsReducer(getTextOptions(this.state), action); - const lastModified = Date.now() as AT.TimeStamp; - const textOptionsRecord: TextOptionsRecord = { - lastModified, - textOptions, - }; - this.handleOptionsUpdate({ type: "updateTextOptionsRecord", payload: textOptionsRecord }); - } - - private handleSearchValueChange(searchValue: string) { - if (searchValue === " ") { - return; - } - const lastChar = searchValue[searchValue.length-1]; - // don't let people type in a single digit (to allow for number shortcuts) - // but do allow the whole thing to be numbers (to allow for pasting and searching for ts) - if (lastChar >= '0' && lastChar <= '9' && !(/^\d+$/.test(searchValue))) { - return; - } - if (this.state.dictionaryStatus !== "ready") { - return; - } - if (searchValue === "") { - this.setState({ - searchValue: "", - results: [], - page: 1, - inflectionSearchResults: undefined, - }); - if (this.props.location.pathname !== "/") { - this.props.history.replace("/"); - } - return; - } - this.setState(prevState => ({ - searchValue, - results: dictionary.search({ ...prevState, searchValue }), - page: 1, - inflectionSearchResults: undefined, - })); - if (this.props.history.location.pathname !== "/search") { - this.props.history.push("/search"); - } - window.scrollTo(0, 0); - } - - private handleIsolateEntry(ts: number, onlyState?: boolean) { - window.scrollTo(0, 0); - const isolatedEntry = dictionary.findOneByTs(ts); - if (!isolatedEntry) { - console.error("couldn't find word to isolate"); - return; - } - this.setState({ isolatedEntry }); - if (!onlyState && (this.props.location.pathname !== "/word" || (getWordId(this.props.location.search) !== ts))) { - this.props.history.push(`/word?id=${isolatedEntry.ts}`); - } - } - - // TODO: right now not checking user very often cause it messes with the state? - // causes the verb quizzer to reset? - private cronJob = new CronJob("1/10 * * * *", () => { - this.handleDictionaryUpdate(); this.handleLoadUser(); - }) - - /* istanbul ignore next */ - private handleScroll() { - if (hitBottom() && this.props.location.pathname === "/search" && this.state.results.length >= (pageSize * this.state.page)) { - const page = this.state.page + 1; - const moreResults = dictionary.search({ ...this.state, page }); - if (moreResults.length > this.state.results.length) { - this.setState({ - page, - results: moreResults, - }); + // incase it took forever and timed out - might need to reinitialize the wordlist here ?? + if (this.state.user) { + startLocalDbs(this.state.user, { + wordlist: this.handleRefreshWordlist, + reviewTasks: this.handleRefreshReviewTasks, + }); + } + if (this.props.location.pathname === "/word") { + const wordId = getWordId(this.props.location.search); + if (wordId) { + const word = dictionary.findOneByTs(wordId); + if (word) { + this.setState({ searchValue: word.p }); } + this.handleIsolateEntry(wordId); + } else { + // TODO: Make a word not found screen + console.error("somehow had a word path without a word id param"); + this.props.history.replace("/"); + } } - } - - private handleInflectionSearch() { - function prepValueForSearch(searchValue: string, textOptions: T.TextOptions): string { - const s = revertSpelling(searchValue, textOptions.spelling); - return standardizePashto(s.trim()); + if (this.props.location.pathname === "/share-target") { + const searchString = getTextFromShareTarget(window.location); + this.props.history.replace("/"); + if (this.state.options.language === "English") { + this.handleOptionsUpdate({ type: "toggleLanguage" }); + } + if (this.state.options.searchType === "alphabetical") { + this.handleOptionsUpdate({ type: "toggleSearchType" }); + } + this.handleSearchValueChange(searchString); } - this.setState({ inflectionSearchResults: "searching" }); - // need timeout to make sure the "searching" notice gets rendered before things lock up for the big search - setTimeout(() => { - const inflectionSearchResults = searchAllInflections( - allEntries(), - prepValueForSearch(this.state.searchValue, this.state.options.textOptionsRecord.textOptions), - ); - this.setState({ inflectionSearchResults }); - }, 20); + if (this.props.location.pathname === "/new-entries") { + this.setState({ + results: dictionary.getNewWordsThisMonth(), + page: 1, + }); + } + if (r.response === "loaded from saved") { + this.handleDictionaryUpdate(); + } + }) + .catch((error) => { + console.error(error); + this.setState({ dictionaryStatus: "error loading" }); + }); + document.documentElement.setAttribute( + "data-theme", + this.state.options.theme + ); + /* istanbul ignore if */ + if (window.matchMedia) { + const prefersDarkQuery = window.matchMedia( + "(prefers-color-scheme: dark)" + ); + prefersDarkQuery.addListener((e) => { + if (e.matches) { + this.handleOptionsUpdate({ type: "changeTheme", payload: "dark" }); + } + }); + const prefersLightQuery = window.matchMedia( + "(prefers-color-scheme: light)" + ); + prefersLightQuery.addListener((e) => { + if (e.matches) { + this.handleOptionsUpdate({ type: "changeTheme", payload: "light" }); + } + }); } + // shortcuts to isolote word in search results + Mousetrap.bind(["1", "2", "3", "4", "5", "6", "7", "8", "9"], (e) => { + e.preventDefault(); + if (e.repeat) { + return; + } + if (this.props.location.pathname !== "/search") { + return; + } + const toIsolate = this.state.results[Number(e.key) - 1]; + if (!toIsolate) { + return; + } + this.handleIsolateEntry(toIsolate.ts); + }); + Mousetrap.bind( + ["ctrl+down", "ctrl+up", "command+down", "command+up"], + (e) => { + e.preventDefault(); + if (e.repeat) { + return; + } + this.handleOptionsUpdate({ type: "toggleLanguage" }); + } + ); + Mousetrap.bind(["ctrl+b", "command+b"], (e) => { + e.preventDefault(); + if (e.repeat) { + return; + } + this.handleSearchValueChange(""); + }); + Mousetrap.bind(["ctrl+i", "command+i"], (e) => { + e.preventDefault(); + if (e.repeat) { + return; + } + if (!this.state.searchValue) { + return; + } + this.handleInflectionSearch(); + }); + Mousetrap.bind(["ctrl+s", "command+s"], (e) => { + if (this.state.user?.level === "basic") { + return; + } + e.preventDefault(); + if (!this.state.isolatedEntry) { + return; + } + const toAdd = { + entry: this.state.isolatedEntry, + notes: "", + }; + addToWordlist(toAdd); + }); + Mousetrap.bind(["ctrl+\\", "command+\\"], (e) => { + e.preventDefault(); + if (e.repeat) { + return; + } + if (this.state.user?.level === "basic") { + return; + } + if (this.props.location.pathname !== "/wordlist") { + this.props.history.push("/wordlist"); + } else { + this.handleGoBack(); + } + }); + } - private handleGoBack() { - this.props.history.goBack(); + public componentWillUnmount() { + window.removeEventListener("scroll", this.handleScroll); + this.cronJob.stop(); + stopLocalDbs(); + Mousetrap.unbind(["ctrl+down", "ctrl+up", "command+down", "command+up"]); + Mousetrap.unbind(["ctrl+b", "command+b"]); + Mousetrap.unbind(["ctrl+\\", "command+\\"]); + Mousetrap.unbind(["ctrl+s", "command+s"]); + Mousetrap.unbind(["ctrl+i", "command+i"]); + Mousetrap.unbind(["1", "2", "3", "4", "5", "6", "7", "8", "9"]); + } + + public componentDidUpdate(prevProps: RouteComponentProps) { + if (this.props.location.pathname !== prevProps.location.pathname) { + if (prod && !(this.state.user?.level === "editor")) { + ReactGA.send({ + hitType: "pageview", + page: window.location.pathname + window.location.search, + }); + } + if (this.props.location.pathname === "/") { + this.handleSearchValueChange(""); + } + if (this.props.location.pathname === "/new-entries") { + this.setState({ + results: dictionary.getNewWordsThisMonth(), + page: 1, + }); + } + if ( + editorOnlyPages.includes(this.props.location.pathname) && + !(this.state.user?.level === "editor") + ) { + this.props.history.replace("/"); + } + } + if ( + getWordId(this.props.location.search) !== + getWordId(prevProps.location.search) + ) { + if (prod && this.state.user?.level !== "editor") { + ReactGA.send({ + type: "pageview", + page: window.location.pathname + window.location.search, + }); + } + const wordId = getWordId(this.props.location.search); + /* istanbul ignore else */ + if (wordId) { + this.handleIsolateEntry(wordId, true); + } else { + this.setState({ isolatedEntry: undefined }); + } + } + // if (!["/wordlist", "/settings", "/review-tasks"].includes(this.props.location.pathname)) { + // window.scrollTo(0, 0); + // } + } + + private async handleLoadUser(): Promise { + try { + const prevUser = this.state.user; + const user = await getUser(); + if (user === "offline") { + return; + } + if (user) { + sendSubmissions(); + } + if (!user) { + if (this.state.user) { + console.log("setting state user because user is newly undefined"); + this.setState({ user: undefined }); + } + saveUser(undefined); + return; + } + if (!userObjIsEqual(prevUser, user)) { + console.log( + "setting state user because something is different about the user" + ); + this.setState({ user }); + saveUser(user); + } + if (user) { + startLocalDbs(user, { + wordlist: this.handleRefreshWordlist, + reviewTasks: this.handleRefreshReviewTasks, + }); + } else { + stopLocalDbs(); + } + } catch (err) { + console.error("error checking user level", err); + } + } + + private handleDictionaryUpdate() { + // TODO: fix - what the heck happened and what's going on here + dictionary + .update(() => { + // this.setState({ dictionaryStatus: "updating" }); + }) + .then(({ dictionaryInfo }) => { + //if (this.state.dictionaryInfo?.release !== dictionaryInfo?.release) { + // to avoid unnecessary re-rendering that breaks things + this.setState({ + dictionaryStatus: "ready", + dictionaryInfo, + }); + //} + }) + .catch(() => { + this.setState({ dictionaryStatus: "error loading" }); + }); + } + + private handleOptionsUpdate(action: OptionsAction) { + if (action.type === "changeTheme") { + document.documentElement.setAttribute("data-theme", action.payload); + } + // TODO: use a seperate reducer for changing text options (otherwise you could just be updating the saved text options instead of the user text options that the program is going off of) + const options = optionsReducer(this.state.options, action); + saveOptions(options); + if ( + action.type === "toggleLanguage" || + action.type === "toggleSearchType" + ) { + if (this.props.location.pathname !== "/new-entries") { + this.setState((prevState) => ({ + options, + page: 1, + results: dictionary.search({ ...prevState, options }), + })); window.scrollTo(0, 0); + } else { + this.setState({ options }); + } + } else { + !objIsEqual(this.state.options, options) && this.setState({ options }); } + } - private handleRefreshWordlist() { - getWordlist().then((wordlist) => { - this.setState({ wordlist }); + private handleTextOptionsUpdate(action: TextOptionsAction) { + const textOptions = textOptionsReducer(getTextOptions(this.state), action); + const lastModified = Date.now() as AT.TimeStamp; + const textOptionsRecord: TextOptionsRecord = { + lastModified, + textOptions, + }; + this.handleOptionsUpdate({ + type: "updateTextOptionsRecord", + payload: textOptionsRecord, + }); + } + + private handleSearchValueChange(searchValue: string) { + if (searchValue === " ") { + return; + } + const lastChar = searchValue[searchValue.length - 1]; + // don't let people type in a single digit (to allow for number shortcuts) + // but do allow the whole thing to be numbers (to allow for pasting and searching for ts) + if (lastChar >= "0" && lastChar <= "9" && !/^\d+$/.test(searchValue)) { + return; + } + if (this.state.dictionaryStatus !== "ready") { + return; + } + if (searchValue === "") { + this.setState({ + searchValue: "", + results: [], + page: 1, + inflectionSearchResults: undefined, + }); + if (this.props.location.pathname !== "/") { + this.props.history.replace("/"); + } + return; + } + this.setState((prevState) => ({ + searchValue, + results: dictionary.search({ ...prevState, searchValue }), + page: 1, + inflectionSearchResults: undefined, + })); + if (this.props.history.location.pathname !== "/search") { + this.props.history.push("/search"); + } + window.scrollTo(0, 0); + } + + private handleIsolateEntry(ts: number, onlyState?: boolean) { + window.scrollTo(0, 0); + const isolatedEntry = dictionary.findOneByTs(ts); + if (!isolatedEntry) { + console.error("couldn't find word to isolate"); + return; + } + this.setState({ isolatedEntry }); + if ( + !onlyState && + (this.props.location.pathname !== "/word" || + getWordId(this.props.location.search) !== ts) + ) { + this.props.history.push(`/word?id=${isolatedEntry.ts}`); + } + } + + // TODO: right now not checking user very often cause it messes with the state? + // causes the verb quizzer to reset? + private cronJob = new CronJob("1/10 * * * *", () => { + this.handleDictionaryUpdate(); + this.handleLoadUser(); + }); + + /* istanbul ignore next */ + private handleScroll() { + if ( + hitBottom() && + this.props.location.pathname === "/search" && + this.state.results.length >= pageSize * this.state.page + ) { + const page = this.state.page + 1; + const moreResults = dictionary.search({ ...this.state, page }); + if (moreResults.length > this.state.results.length) { + this.setState({ + page, + results: moreResults, }); + } } + } - private handleRefreshReviewTasks() { - getAllDocsLocalDb("reviewTasks").then((reviewTasks) => { - this.setState({ reviewTasks }); - }); + private handleInflectionSearch() { + function prepValueForSearch( + searchValue: string, + textOptions: T.TextOptions + ): string { + const s = revertSpelling(searchValue, textOptions.spelling); + return standardizePashto(s.trim()); } + this.setState({ inflectionSearchResults: "searching" }); + // need timeout to make sure the "searching" notice gets rendered before things lock up for the big search + setTimeout(() => { + const inflectionSearchResults = searchAllInflections( + allEntries(), + prepValueForSearch( + this.state.searchValue, + this.state.options.textOptionsRecord.textOptions + ) + ); + this.setState({ inflectionSearchResults }); + }, 20); + } - render() { - return
- - LingDocs Pashto Dictionary - - {this.state.options.searchBarPosition === "top" && { + this.setState({ wordlist }); + }); + } + + private handleRefreshReviewTasks() { + getAllDocsLocalDb("reviewTasks").then((reviewTasks) => { + this.setState({ reviewTasks }); + }); + } + + render() { + return ( +
+ + LingDocs Pashto Dictionary + + {this.state.options.searchBarPosition === "top" && ( + + )} +
+ {this.state.dictionaryStatus !== "ready" ? ( + + ) : ( + <> + +
+

+ LingDocs Pashto Dictionary +

+
+
+ {this.state.options.searchType === "alphabetical" ? ( + <> + Alphabetical + browsing mode + + ) : ( + <> + Approximate + search mode + + )} +
+
+ {this.state.user?.level === "editor" && ( +
+
Editor privileges active
+ + + +
+ )} + +
New words this month
+ +
+ + Phrase Builder + + + + Grammar + +
+
+
+ + + + + + + + + + + + + + + + +

New Words This Month

+ {this.state.results.length ? ( + } -
- {this.state.dictionaryStatus !== "ready" ? - - : - <> - -
-

LingDocs Pashto Dictionary

-
-
- {this.state.options.searchType === "alphabetical" - ? <> Alphabetical browsing mode - : <> Approximate search mode} -
-
- {this.state.user?.level === "editor" &&
-
Editor privileges active
- - - -
} - -
New words this month
- -
- - Phrase Builder - - - - Grammar - -
-
-
- - - - - - - - - - - - - - - - -

New Words This Month

- {this.state.results.length ? - - : -
No new words added this month 😓
- } -
- - - - - - - - - - - - - {this.state.user?.level === "editor" && - - } - {this.state.user?.level === "editor" && - - } - - } + isolateEntry={this.handleIsolateEntry} + handleInflectionSearch={this.handleInflectionSearch} + /> + ) : ( +
No new words added this month 😓
+ )} + + + + + + + + + + + + + + {this.state.user?.level === "editor" && ( + + + + )} + {this.state.user?.level === "editor" && ( + + + + )} + + )} +
+
+ +
+ + + + + {this.state.user?.level === "editor" && ( + + )}
-
- -
- - - - - {this.state.user?.level === "editor" && - - } -
-
- -
- -
-
- {this.state.options.searchBarPosition === "bottom" && } -
-
; - } + + +
+ +
+
+ {this.state.options.searchBarPosition === "bottom" && ( + + )} + +
+ ); + } } export default withRouter(App); diff --git a/website/yarn.lock b/website/yarn.lock index d638345..46708e4 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -11335,10 +11335,10 @@ react-fast-compare@^3.0.1, react-fast-compare@^3.1.1: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== -react-ga@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-3.3.0.tgz#c91f407198adcb3b49e2bc5c12b3fe460039b3ca" - integrity sha512-o8RScHj6Lb8cwy3GMrVH6NJvL+y0zpJvKtc0+wmH7Bt23rszJmnqEQxRbyrqUzk9DTJIHoP42bfO5rswC9SWBQ== +react-ga4@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/react-ga4/-/react-ga4-2.1.0.tgz#56601f59d95c08466ebd6edfbf8dede55c4678f9" + integrity sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ== react-helmet@^6.1.0: version "6.1.0"