update ga

This commit is contained in:
adueck 2023-07-15 00:49:02 +04:00
parent ab0916aadd
commit 211d9b3446
3 changed files with 745 additions and 614 deletions

View File

@ -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",

View File

@ -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";
@ -32,36 +37,23 @@ import {
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,
} 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";
@ -78,31 +70,35 @@ import {
} 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;
}
};
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.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<RouteComponentProps, State> {
constructor(props: RouteComponentProps) {
@ -112,11 +108,15 @@ class App extends Component<RouteComponentProps, State> {
dictionaryStatus: "loading",
dictionaryInfo: undefined,
// TODO: Choose between the saved options and the options in the saved user
options: savedOptions ? savedOptions : {
options: savedOptions
? savedOptions
: {
language: "Pashto",
searchType: "fuzzy",
searchBarStickyFocus: false,
theme: (window.matchMedia?.("(prefers-color-scheme: dark)").matches) ? "dark" : "light",
theme: window.matchMedia?.("(prefers-color-scheme: dark)").matches
? "dark"
: "light",
textOptionsRecord: {
lastModified: Date.now() as AT.TimeStamp,
textOptions: defaultTextOptions,
@ -133,7 +133,7 @@ class App extends Component<RouteComponentProps, State> {
wordlist: [],
reviewTasks: [],
user: readUser(),
inflectionSearchResults: undefined
inflectionSearchResults: undefined,
};
this.handleOptionsUpdate = this.handleOptionsUpdate.bind(this);
this.handleTextOptionsUpdate = this.handleTextOptionsUpdate.bind(this);
@ -153,10 +153,15 @@ class App extends Component<RouteComponentProps, State> {
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);
if (prod && !(this.state.user?.level === "editor")) {
ReactGA.send({
hitType: "pageview",
page: window.location.pathname + window.location.search,
});
}
dictionary.initialize().then((r) => {
dictionary
.initialize()
.then((r) => {
this.cronJob.start();
this.setState({
dictionaryStatus: "ready",
@ -165,7 +170,10 @@ class App extends Component<RouteComponentProps, State> {
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 });
startLocalDbs(this.state.user, {
wordlist: this.handleRefreshWordlist,
reviewTasks: this.handleRefreshReviewTasks,
});
}
if (this.props.location.pathname === "/word") {
const wordId = getWordId(this.props.location.search);
@ -201,20 +209,28 @@ class App extends Component<RouteComponentProps, State> {
if (r.response === "loaded from saved") {
this.handleDictionaryUpdate();
}
}).catch((error) => {
})
.catch((error) => {
console.error(error);
this.setState({ dictionaryStatus: "error loading" });
});
document.documentElement.setAttribute("data-theme", this.state.options.theme);
document.documentElement.setAttribute(
"data-theme",
this.state.options.theme
);
/* istanbul ignore if */
if (window.matchMedia) {
const prefersDarkQuery = window.matchMedia("(prefers-color-scheme: dark)");
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)");
const prefersLightQuery = window.matchMedia(
"(prefers-color-scheme: light)"
);
prefersLightQuery.addListener((e) => {
if (e.matches) {
this.handleOptionsUpdate({ type: "changeTheme", payload: "light" });
@ -235,14 +251,17 @@ class App extends Component<RouteComponentProps, State> {
return;
}
this.handleIsolateEntry(toIsolate.ts);
})
Mousetrap.bind(["ctrl+down", "ctrl+up", "command+down", "command+up"], (e) => {
});
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) {
@ -304,8 +323,11 @@ class App extends Component<RouteComponentProps, State> {
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 (prod && !(this.state.user?.level === "editor")) {
ReactGA.send({
hitType: "pageview",
page: window.location.pathname + window.location.search,
});
}
if (this.props.location.pathname === "/") {
this.handleSearchValueChange("");
@ -316,20 +338,29 @@ class App extends Component<RouteComponentProps, State> {
page: 1,
});
}
if (editorOnlyPages.includes(this.props.location.pathname) && !(this.state.user?.level === "editor")) {
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);
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 })
this.setState({ isolatedEntry: undefined });
}
}
// if (!["/wordlist", "/settings", "/review-tasks"].includes(this.props.location.pathname)) {
@ -356,12 +387,17 @@ class App extends Component<RouteComponentProps, State> {
return;
}
if (!userObjIsEqual(prevUser, user)) {
console.log("setting state user because something is different about the 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 });
startLocalDbs(user, {
wordlist: this.handleRefreshWordlist,
reviewTasks: this.handleRefreshReviewTasks,
});
} else {
stopLocalDbs();
}
@ -372,9 +408,11 @@ class App extends Component<RouteComponentProps, State> {
private handleDictionaryUpdate() {
// TODO: fix - what the heck happened and what's going on here
dictionary.update(() => {
dictionary
.update(() => {
// this.setState({ dictionaryStatus: "updating" });
}).then(({ dictionaryInfo }) => {
})
.then(({ dictionaryInfo }) => {
//if (this.state.dictionaryInfo?.release !== dictionaryInfo?.release) {
// to avoid unnecessary re-rendering that breaks things
this.setState({
@ -382,7 +420,8 @@ class App extends Component<RouteComponentProps, State> {
dictionaryInfo,
});
//}
}).catch(() => {
})
.catch(() => {
this.setState({ dictionaryStatus: "error loading" });
});
}
@ -394,9 +433,12 @@ class App extends Component<RouteComponentProps, State> {
// 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 (
action.type === "toggleLanguage" ||
action.type === "toggleSearchType"
) {
if (this.props.location.pathname !== "/new-entries") {
this.setState(prevState => ({
this.setState((prevState) => ({
options,
page: 1,
results: dictionary.search({ ...prevState, options }),
@ -417,7 +459,10 @@ class App extends Component<RouteComponentProps, State> {
lastModified,
textOptions,
};
this.handleOptionsUpdate({ type: "updateTextOptionsRecord", payload: textOptionsRecord });
this.handleOptionsUpdate({
type: "updateTextOptionsRecord",
payload: textOptionsRecord,
});
}
private handleSearchValueChange(searchValue: string) {
@ -427,7 +472,7 @@ class App extends Component<RouteComponentProps, State> {
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))) {
if (lastChar >= "0" && lastChar <= "9" && !/^\d+$/.test(searchValue)) {
return;
}
if (this.state.dictionaryStatus !== "ready") {
@ -445,7 +490,7 @@ class App extends Component<RouteComponentProps, State> {
}
return;
}
this.setState(prevState => ({
this.setState((prevState) => ({
searchValue,
results: dictionary.search({ ...prevState, searchValue }),
page: 1,
@ -465,7 +510,11 @@ class App extends Component<RouteComponentProps, State> {
return;
}
this.setState({ isolatedEntry });
if (!onlyState && (this.props.location.pathname !== "/word" || (getWordId(this.props.location.search) !== ts))) {
if (
!onlyState &&
(this.props.location.pathname !== "/word" ||
getWordId(this.props.location.search) !== ts)
) {
this.props.history.push(`/word?id=${isolatedEntry.ts}`);
}
}
@ -475,11 +524,15 @@ class App extends Component<RouteComponentProps, State> {
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)) {
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) {
@ -492,7 +545,10 @@ class App extends Component<RouteComponentProps, State> {
}
private handleInflectionSearch() {
function prepValueForSearch(searchValue: string, textOptions: T.TextOptions): string {
function prepValueForSearch(
searchValue: string,
textOptions: T.TextOptions
): string {
const s = revertSpelling(searchValue, textOptions.spelling);
return standardizePashto(s.trim());
}
@ -501,7 +557,10 @@ class App extends Component<RouteComponentProps, State> {
setTimeout(() => {
const inflectionSearchResults = searchAllInflections(
allEntries(),
prepValueForSearch(this.state.searchValue, this.state.options.textOptionsRecord.textOptions),
prepValueForSearch(
this.state.searchValue,
this.state.options.textOptionsRecord.textOptions
)
);
this.setState({ inflectionSearchResults });
}, 20);
@ -525,48 +584,75 @@ class App extends Component<RouteComponentProps, State> {
}
render() {
return <div style={{
paddingTop: this.state.options.searchBarPosition === "top" ? "75px" : "7px",
return (
<div
style={{
paddingTop:
this.state.options.searchBarPosition === "top" ? "75px" : "7px",
paddingBottom: "60px",
}}>
}}
>
<Helmet>
<title>LingDocs Pashto Dictionary</title>
</Helmet>
{this.state.options.searchBarPosition === "top" && <SearchBar
{this.state.options.searchBarPosition === "top" && (
<SearchBar
state={this.state}
optionsDispatch={this.handleOptionsUpdate}
handleSearchValueChange={this.handleSearchValueChange}
/>}
/>
)}
<div className="container-fluid" data-testid="body">
{this.state.dictionaryStatus !== "ready" ?
{this.state.dictionaryStatus !== "ready" ? (
<DictionaryStatusDisplay status={this.state.dictionaryStatus} />
:
) : (
<>
<Route path="/" exact={true}>
<div className="text-center mt-4">
<h4 className="font-weight-light p-3 mb-4">LingDocs Pashto Dictionary</h4>
<h4 className="font-weight-light p-3 mb-4">
LingDocs Pashto Dictionary
</h4>
<div className="mt-4 font-weight-light">
<div className="mb-4 small">
{this.state.options.searchType === "alphabetical"
? <><span className="fa fa-book mr-2" /> Alphabetical browsing mode</>
: <><span className="fa fa-bolt mr-2" /> Approximate search mode</>}
{this.state.options.searchType === "alphabetical" ? (
<>
<span className="fa fa-book mr-2" /> Alphabetical
browsing mode
</>
) : (
<>
<span className="fa fa-bolt mr-2" /> Approximate
search mode
</>
)}
</div>
</div>
{this.state.user?.level === "editor" && <div className="mt-4 font-weight-light">
{this.state.user?.level === "editor" && (
<div className="mt-4 font-weight-light">
<div className="mb-3">Editor privileges active</div>
<Link to="/edit">
<button className="btn btn-secondary">New Entry</button>
</Link>
</div>}
<Link to="/new-entries" className="plain-link font-weight-light">
</div>
)}
<Link
to="/new-entries"
className="plain-link font-weight-light"
>
<div className="my-4">New words this month</div>
</Link>
<div className="mt-4 pt-3">
<Link to="/phrase-builder" className="plain-link h5 font-weight-light">
<Link
to="/phrase-builder"
className="plain-link h5 font-weight-light"
>
Phrase Builder
</Link>
<span className="mx-1"> </span>
<a href="https://grammar.lingdocs.com" className="plain-link h5 font-weight-light">
<a
href="https://grammar.lingdocs.com"
className="plain-link h5 font-weight-light"
>
Grammar
</a>
</div>
@ -601,18 +687,21 @@ class App extends Component<RouteComponentProps, State> {
</Route>
<Route path="/new-entries">
<h4 className="mb-3">New Words This Month</h4>
{this.state.results.length ?
{this.state.results.length ? (
<Results
state={this.state}
isolateEntry={this.handleIsolateEntry}
handleInflectionSearch={this.handleInflectionSearch}
/>
:
) : (
<div>No new words added this month 😓</div>
}
)}
</Route>
<Route path="/account">
<Account user={this.state.user} loadUser={this.handleLoadUser} />
<Account
user={this.state.user}
loadUser={this.handleLoadUser}
/>
</Route>
<Route path="/word">
<IsolatedEntry
@ -634,59 +723,101 @@ class App extends Component<RouteComponentProps, State> {
<Route path="/script-to-phonetics">
<ScriptToPhonetics />
</Route>
{this.state.user?.level === "editor" && <Route path="/edit">
{this.state.user?.level === "editor" && (
<Route path="/edit">
<EntryEditor
isolatedEntry={this.state.isolatedEntry}
user={this.state.user}
textOptions={getTextOptions(this.state)}
dictionary={dictionary}
searchParams={new URLSearchParams(this.props.history.location.search)}
/>
</Route>}
{this.state.user?.level === "editor" && <Route path="/review-tasks">
<ReviewTasks state={this.state} />
</Route>}
</>
searchParams={
new URLSearchParams(this.props.history.location.search)
}
/>
</Route>
)}
{this.state.user?.level === "editor" && (
<Route path="/review-tasks">
<ReviewTasks state={this.state} />
</Route>
)}
</>
)}
</div>
<footer className={classNames(
<footer
className={classNames(
"footer",
{ "bg-white": !["/search", "/word"].includes(this.props.location.pathname) },
{ "footer-thick": this.state.options.searchBarPosition === "bottom" && !["/search", "/word"].includes(this.props.location.pathname) },
{ "wee-less-footer": this.state.options.searchBarPosition === "bottom" && ["/search", "/word"].includes(this.props.location.pathname) },
)}>
{
"bg-white": !["/search", "/word"].includes(
this.props.location.pathname
),
},
{
"footer-thick":
this.state.options.searchBarPosition === "bottom" &&
!["/search", "/word"].includes(this.props.location.pathname),
},
{
"wee-less-footer":
this.state.options.searchBarPosition === "bottom" &&
["/search", "/word"].includes(this.props.location.pathname),
}
)}
>
<Route path="/" exact={true}>
<div className="buttons-footer">
<BottomNavItem label="About" icon="info-circle" page="/about" />
<BottomNavItem label="Settings" icon="cog" page="/settings" />
<BottomNavItem label={this.state.user ? "Account" : "Sign In"} icon="user" page="/account" />
<BottomNavItem
label={`Wordlist ${this.state.options.wordlistReviewBadge ? textBadge(forReview(this.state.wordlist).length) : ""}`}
label={this.state.user ? "Account" : "Sign In"}
icon="user"
page="/account"
/>
<BottomNavItem
label={`Wordlist ${
this.state.options.wordlistReviewBadge
? textBadge(forReview(this.state.wordlist).length)
: ""
}`}
icon="list"
page="/wordlist"
/>
{this.state.user?.level === "editor" &&
{this.state.user?.level === "editor" && (
<BottomNavItem
label={`Tasks ${textBadge(this.state.reviewTasks.length)}`}
icon="edit"
page="/review-tasks"
/>
}
)}
</div>
</Route>
<Route path={["/about", "/settings", "/new-entries", "/account", "/wordlist", "/edit", "/review-tasks", "/phrase-builder"]}>
<Route
path={[
"/about",
"/settings",
"/new-entries",
"/account",
"/wordlist",
"/edit",
"/review-tasks",
"/phrase-builder",
]}
>
<div className="buttons-footer">
<BottomNavItem label="Home" icon="home" page="/" />
</div>
</Route>
{this.state.options.searchBarPosition === "bottom" && <SearchBar
{this.state.options.searchBarPosition === "bottom" && (
<SearchBar
state={this.state}
optionsDispatch={this.handleOptionsUpdate}
handleSearchValueChange={this.handleSearchValueChange}
onBottom={true}
/>}
/>
)}
</footer>
</div>;
</div>
);
}
}

View File

@ -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"