half functional textOptions sync
This commit is contained in:
parent
5031d64139
commit
c63ab30f1e
|
@ -95,7 +95,8 @@ export async function updateLingdocsUser(uuid: T.UUID, toUpdate:
|
|||
level: "student",
|
||||
wordlistDbName: T.WordlistDbName,
|
||||
couchDbPassword: T.UserDbPassword,
|
||||
}
|
||||
} |
|
||||
{ userTextOptionsRecord: T.UserTextOptionsRecord }
|
||||
): Promise<T.LingdocsUser> {
|
||||
const user = await getLingdocsUser("userId", uuid);
|
||||
if (!user) throw new Error("unable to update - user not found " + uuid);
|
||||
|
|
|
@ -74,6 +74,7 @@ export async function createNewUser(input: {
|
|||
tests: [],
|
||||
lastLogin: now,
|
||||
lastActive: now,
|
||||
textOptions: undefined,
|
||||
};
|
||||
const user = await insertLingdocsUser(newUser);
|
||||
sendVerificationEmail(user, email.token).catch(console.error);
|
||||
|
@ -91,6 +92,7 @@ export async function createNewUser(input: {
|
|||
tests: [],
|
||||
lastLogin: now,
|
||||
lastActive: now,
|
||||
textOptions: undefined,
|
||||
};
|
||||
const user = await insertLingdocsUser(newUser);
|
||||
return user;
|
||||
|
@ -111,6 +113,7 @@ export async function createNewUser(input: {
|
|||
tests: [],
|
||||
lastActive: now,
|
||||
level: "basic",
|
||||
textOptions: undefined,
|
||||
}
|
||||
const user = await insertLingdocsUser(newUser);
|
||||
sendVerificationEmail(user, em.token);
|
||||
|
@ -127,6 +130,7 @@ export async function createNewUser(input: {
|
|||
tests: [],
|
||||
lastActive: now,
|
||||
level: "basic",
|
||||
textOptions: undefined,
|
||||
}
|
||||
const user = await insertLingdocsUser(newUser);
|
||||
return user;
|
||||
|
|
|
@ -93,6 +93,18 @@ apiRouter.put("/email-verification", async (req, res, next) => {
|
|||
}
|
||||
});
|
||||
|
||||
apiRouter.put("/user/userTextOptionsRecord", async (req, res, next) => {
|
||||
if (!req.user) throw new Error("user not found");
|
||||
try {
|
||||
const { userTextOptionsRecord } = req.body as T.UpdateUserTextOptionsRecordBody;
|
||||
const user = await updateLingdocsUser(req.user.userId, { userTextOptionsRecord });
|
||||
const toSend: T.UpdateUserTextOptionsRecordResponse = { ok: true, message: "updated userTextOptionsRecord", user };
|
||||
res.send(toSend);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
})
|
||||
|
||||
apiRouter.put("/user/upgrade", async (req, res, next) => {
|
||||
if (!req.user) throw new Error("user not found");
|
||||
try {
|
||||
|
|
|
@ -32,7 +32,10 @@ import {
|
|||
readUser,
|
||||
} from "./lib/local-storage";
|
||||
import { dictionary, pageSize } from "./lib/dictionary";
|
||||
import optionsReducer 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";
|
||||
|
@ -42,6 +45,7 @@ import {
|
|||
} from "./lib/submissions";
|
||||
import {
|
||||
getUser,
|
||||
updateUserTextOptionsRecord,
|
||||
} from "./lib/backend-calls";
|
||||
import {
|
||||
getWordlist,
|
||||
|
@ -57,6 +61,7 @@ import {
|
|||
import {
|
||||
textBadge,
|
||||
} from "./lib/badges";
|
||||
import * as AT from "./lib/account-types";
|
||||
import ReactGA from "react-ga";
|
||||
// tslint:disable-next-line
|
||||
import "@fortawesome/fontawesome-free/css/all.css";
|
||||
|
@ -91,12 +96,16 @@ class App extends Component<RouteComponentProps, State> {
|
|||
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",
|
||||
theme: /* istanbul ignore next */ (window.matchMedia &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light",
|
||||
textOptions: defaultTextOptions,
|
||||
textOptionsRecord: {
|
||||
lastModified: Date.now() as AT.TimeStamp,
|
||||
textOptions: defaultTextOptions,
|
||||
},
|
||||
wordlistMode: "browse",
|
||||
wordlistReviewLanguage: "Pashto",
|
||||
wordlistReviewBadge: true,
|
||||
|
@ -254,6 +263,7 @@ class App extends Component<RouteComponentProps, State> {
|
|||
if (user === "offline") return;
|
||||
if (user) sendSubmissions();
|
||||
this.setState({ user });
|
||||
// TODO: LOAD AND RESOLVE THE USER'S TEXT OPTIONS ETC.
|
||||
saveUser(user);
|
||||
if (user) {
|
||||
startLocalDbs(user, { wordlist: this.handleRefreshWordlist, reviewTasks: this.handleRefreshReviewTasks });
|
||||
|
@ -282,6 +292,7 @@ class App extends Component<RouteComponentProps, State> {
|
|||
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") {
|
||||
|
@ -300,6 +311,25 @@ class App extends Component<RouteComponentProps, State> {
|
|||
}
|
||||
}
|
||||
|
||||
private handleTextOptionsUpdate(action: TextOptionsAction) {
|
||||
const textOptions = textOptionsReducer(this.state.options.textOptionsRecord.textOptions, action);
|
||||
const lastModified = Date.now() as AT.TimeStamp;
|
||||
const textOptionsRecord: TextOptionsRecord = {
|
||||
lastModified,
|
||||
textOptions,
|
||||
};
|
||||
this.handleOptionsUpdate({ type: "updateTextOptionsRecord", payload: textOptionsRecord });
|
||||
// try to save the new text options to the user
|
||||
if (this.state.user) {
|
||||
const { pTextSize, ...userTextOptions } = textOptions;
|
||||
const userTextOptionsRecord = {
|
||||
userTextOptions,
|
||||
lastModified,
|
||||
};
|
||||
updateUserTextOptionsRecord(userTextOptionsRecord);
|
||||
}
|
||||
}
|
||||
|
||||
private handleSearchValueChange(searchValue: string) {
|
||||
if (this.state.dictionaryStatus !== "ready") return;
|
||||
if (searchValue === "") {
|
||||
|
@ -416,7 +446,12 @@ class App extends Component<RouteComponentProps, State> {
|
|||
<About state={this.state} />
|
||||
</Route>
|
||||
<Route path="/settings">
|
||||
<Options state={this.state} options={this.state.options} optionsDispatch={this.handleOptionsUpdate} />
|
||||
<Options
|
||||
state={this.state}
|
||||
options={this.state.options}
|
||||
optionsDispatch={this.handleOptionsUpdate}
|
||||
textOptionsDispatch={this.handleTextOptionsUpdate}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/search">
|
||||
<Results state={this.state} isolateEntry={this.handleIsolateEntry} />
|
||||
|
|
|
@ -17,6 +17,13 @@ export type TwitterProfile = WoutRJ<import("passport-twitter").Profile> & { toke
|
|||
export type ProviderProfile = GoogleProfile | GitHubProfile | TwitterProfile;
|
||||
export type UserLevel = "basic" | "student" | "editor";
|
||||
|
||||
export type UserTextOptions = Omit<import("@lingdocs/pashto-inflector").Types.TextOptions, "pTextSize">;
|
||||
|
||||
export type UserTextOptionsRecord = {
|
||||
lastModified: TimeStamp,
|
||||
userTextOptions: UserTextOptions,
|
||||
};
|
||||
|
||||
// TODO: TYPE GUARDING SO WE NEVER HAVE A USER WITH NO Id or Password
|
||||
export type LingdocsUser = {
|
||||
userId: UUID,
|
||||
|
@ -34,8 +41,9 @@ export type LingdocsUser = {
|
|||
tests: [],
|
||||
lastLogin: TimeStamp,
|
||||
lastActive: TimeStamp,
|
||||
userTextOptionsRecord: undefined | UserTextOptionsRecord,
|
||||
} & (
|
||||
{ level: "basic"} |
|
||||
{ level: "basic" } |
|
||||
{
|
||||
level: "student" | "editor",
|
||||
couchDbPassword: UserDbPassword,
|
||||
|
@ -58,3 +66,11 @@ export type UpgradeUserResponse = {
|
|||
message: "user already upgraded" | "user upgraded to student",
|
||||
user: LingdocsUser,
|
||||
};
|
||||
|
||||
export type UpdateUserTextOptionsRecordBody = { userTextOptionsRecord: UserTextOptionsRecord };
|
||||
|
||||
export type UpdateUserTextOptionsRecordResponse = {
|
||||
ok: true,
|
||||
message: "updated userTextOptionsRecord",
|
||||
user: LingdocsUser,
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as FT from "./functions-types";
|
||||
import * as AT from "./account-types";
|
||||
import { Types as IT } from "@lingdocs/pashto-inflector";
|
||||
|
||||
type Service = "account" | "functions";
|
||||
|
||||
|
@ -23,6 +24,11 @@ export async function upgradeAccount(password: string): Promise<AT.UpgradeUserRe
|
|||
return response as AT.UpgradeUserResponse;
|
||||
}
|
||||
|
||||
export async function updateUserTextOptionsRecord(userTextOptionsRecord: AT.UserTextOptionsRecord): Promise<AT.UpdateUserTextOptionsRecordResponse> {
|
||||
const response = await myFetch("account", "user/userTextOptionsRecord", "PUT", { userTextOptionsRecord }) as AT.UpdateUserTextOptionsRecordResponse;
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function signOut() {
|
||||
try {
|
||||
await myFetch("account", "sign-out", "POST");
|
||||
|
@ -48,7 +54,7 @@ async function myFetch(
|
|||
service: Service,
|
||||
url: string,
|
||||
method: "GET" | "POST" | "PUT" | "DELETE" = "GET",
|
||||
body?: FT.SubmissionsRequest | { password: string },
|
||||
body?: FT.SubmissionsRequest | { password: string } | AT.UpdateUserTextOptionsRecordBody,
|
||||
): Promise<AT.APIResponse> {
|
||||
const response = await fetch(baseUrl[service] + url, {
|
||||
method,
|
||||
|
|
|
@ -19,6 +19,7 @@ import { fuzzifyPashto } from "./fuzzify-pashto/fuzzify-pashto";
|
|||
// @ts-ignore
|
||||
import relevancy from "relevancy";
|
||||
import { makeAWeeBitFuzzy } from "./wee-bit-fuzzy";
|
||||
import { getTextOptions } from "./get-text-options";
|
||||
|
||||
// const dictionaryBaseUrl = "https://storage.googleapis.com/lingdocs/";
|
||||
const dictionaryUrl = `https://storage.googleapis.com/lingdocs/dictionary`;
|
||||
|
@ -353,7 +354,7 @@ export const dictionary: DictionaryAPI = {
|
|||
search: function(state: State): Types.DictionaryEntry[] {
|
||||
const searchString = convertSpelling(
|
||||
state.searchValue,
|
||||
state.options.textOptions.spelling,
|
||||
getTextOptions(state).spelling,
|
||||
);
|
||||
if (state.searchValue === "") {
|
||||
return [];
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { Types as T } from "@lingdocs/pashto-inflector";
|
||||
|
||||
export function getTextOptions(state: State): T.TextOptions {
|
||||
return state.options.textOptionsRecord.textOptions;
|
||||
}
|
|
@ -8,27 +8,20 @@
|
|||
|
||||
import * as AT from "./account-types";
|
||||
|
||||
export const optionsLocalStorageName = "options2";
|
||||
export const optionsLocalStorageName = "options3";
|
||||
export const userLocalStorageName = "user1";
|
||||
|
||||
export function saveOptions(options: Options): void {
|
||||
localStorage.setItem(optionsLocalStorageName, JSON.stringify(options));
|
||||
};
|
||||
|
||||
export const readOptions = (): Options | undefined => {
|
||||
export const readOptions = (): undefined | Options => {
|
||||
const optionsRaw = localStorage.getItem(optionsLocalStorageName);
|
||||
if (!optionsRaw) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const options = JSON.parse(optionsRaw) as Options;
|
||||
// check for new options here
|
||||
if (options.wordlistReviewBadge === undefined) {
|
||||
options.wordlistReviewBadge = true;
|
||||
}
|
||||
if (options.searchBarPosition === undefined) {
|
||||
options.searchBarPosition = "top";
|
||||
}
|
||||
return options;
|
||||
} catch (e) {
|
||||
console.error("error parsing saved state JSON", e);
|
||||
|
|
|
@ -1,99 +0,0 @@
|
|||
import optionsReducer from "./options-reducer";
|
||||
import { defaultTextOptions } from "@lingdocs/pashto-inflector";
|
||||
|
||||
const options: Options = {
|
||||
textOptions: defaultTextOptions,
|
||||
language: "Pashto",
|
||||
searchType: "fuzzy",
|
||||
theme: "light",
|
||||
wordlistMode: "browse",
|
||||
wordlistReviewLanguage: "Pashto",
|
||||
wordlistReviewBadge: true,
|
||||
searchBarPosition: "top",
|
||||
};
|
||||
|
||||
test("options reducer should work", () => {
|
||||
expect(optionsReducer(options, { type: "toggleLanguage" }))
|
||||
.toEqual({
|
||||
...options,
|
||||
language: "English",
|
||||
});
|
||||
expect(optionsReducer({ ...options, language: "English" }, { type: "toggleLanguage" }))
|
||||
.toEqual(options);
|
||||
expect(optionsReducer(options, { type: "toggleSearchType" }))
|
||||
.toEqual({
|
||||
...options,
|
||||
searchType: "alphabetical",
|
||||
});
|
||||
expect(optionsReducer({ ...options, searchType: "alphabetical" }, { type: "toggleSearchType" }))
|
||||
.toEqual(options);
|
||||
expect(optionsReducer(options, { type: "changeTheme", payload: "dark" }))
|
||||
.toEqual({
|
||||
...options,
|
||||
theme: "dark",
|
||||
});
|
||||
expect(optionsReducer(options, { type: "changeWordlistMode", payload: "review" }))
|
||||
.toEqual({
|
||||
...options,
|
||||
wordlistMode: "review",
|
||||
});
|
||||
expect(optionsReducer(options, { type: "changeWordlistReviewLanguage", payload: "English" }))
|
||||
.toEqual({
|
||||
...options,
|
||||
wordlistReviewLanguage: "English",
|
||||
});
|
||||
expect(optionsReducer(options, { type: "changeWordlistReviewBadge", payload: false }))
|
||||
.toEqual({
|
||||
...options,
|
||||
wordlistReviewBadge: false,
|
||||
});
|
||||
expect(optionsReducer(options, { type: "changeSearchBarPosition", payload: "bottom" }))
|
||||
.toEqual({
|
||||
...options,
|
||||
searchBarPosition: "bottom",
|
||||
});
|
||||
expect(optionsReducer(options, { type: "changePTextSize", payload: "largest" }))
|
||||
.toEqual({
|
||||
...options,
|
||||
textOptions: {
|
||||
...defaultTextOptions,
|
||||
pTextSize: "largest",
|
||||
},
|
||||
});
|
||||
expect(optionsReducer(options, { type: "changeSpelling", payload: "Pakistani ی" }))
|
||||
.toEqual({
|
||||
...options,
|
||||
textOptions: {
|
||||
...defaultTextOptions,
|
||||
spelling: "Pakistani ی",
|
||||
},
|
||||
});
|
||||
expect(optionsReducer(options, { type: "changePhonetics", payload: "ipa" }))
|
||||
.toEqual({
|
||||
...options,
|
||||
textOptions: {
|
||||
...defaultTextOptions,
|
||||
phonetics: "ipa",
|
||||
},
|
||||
});
|
||||
expect(optionsReducer(options, { type: "changeDialect", payload: "southern" }))
|
||||
.toEqual({
|
||||
...options,
|
||||
textOptions: {
|
||||
...defaultTextOptions,
|
||||
dialect: "southern",
|
||||
},
|
||||
});
|
||||
expect(optionsReducer(options, { type: "changeDiacritics", payload: true }))
|
||||
.toEqual({
|
||||
...options,
|
||||
textOptions: {
|
||||
...defaultTextOptions,
|
||||
diacritics: true,
|
||||
},
|
||||
});
|
||||
expect(() => {
|
||||
// @ts-ignore
|
||||
optionsReducer(options, { type: "non existent action" });
|
||||
}).toThrow("action type not recognized in reducer");
|
||||
})
|
|
@ -1,4 +1,6 @@
|
|||
function optionsReducer(options: Options, action: OptionsAction): Options {
|
||||
import { Types as IT } from "@lingdocs/pashto-inflector";
|
||||
|
||||
export function optionsReducer(options: Options, action: OptionsAction): Options {
|
||||
if (action.type === "toggleLanguage") {
|
||||
return {
|
||||
...options,
|
||||
|
@ -41,52 +43,45 @@ function optionsReducer(options: Options, action: OptionsAction): Options {
|
|||
wordlistReviewLanguage: action.payload,
|
||||
};
|
||||
}
|
||||
if (action.type === "changePTextSize") {
|
||||
if (action.type === "updateTextOptionsRecord") {
|
||||
return {
|
||||
...options,
|
||||
textOptions: {
|
||||
...options.textOptions,
|
||||
pTextSize: action.payload,
|
||||
},
|
||||
textOptionsRecord: action.payload,
|
||||
};
|
||||
}
|
||||
if (action.type === "changeSpelling") {
|
||||
return {
|
||||
...options,
|
||||
textOptions: {
|
||||
...options.textOptions,
|
||||
spelling: action.payload,
|
||||
}
|
||||
};
|
||||
}
|
||||
if (action.type === "changePhonetics") {
|
||||
return {
|
||||
...options,
|
||||
textOptions: {
|
||||
...options.textOptions,
|
||||
phonetics: action.payload,
|
||||
}
|
||||
};
|
||||
}
|
||||
if (action.type === "changeDialect") {
|
||||
return {
|
||||
...options,
|
||||
textOptions: {
|
||||
...options.textOptions,
|
||||
dialect: action.payload,
|
||||
}
|
||||
};
|
||||
}
|
||||
if (action.type === "changeDiacritics") {
|
||||
return {
|
||||
...options,
|
||||
textOptions: {
|
||||
...options.textOptions,
|
||||
diacritics: action.payload,
|
||||
}
|
||||
};
|
||||
}
|
||||
throw new Error("action type not recognized in reducer");
|
||||
}
|
||||
throw new Error("action type not recognized in options reducer");
|
||||
}
|
||||
|
||||
export default optionsReducer;
|
||||
export function textOptionsReducer(textOptions: IT.TextOptions, action: TextOptionsAction): IT.TextOptions {
|
||||
if (action.type === "changePTextSize") {
|
||||
return {
|
||||
...textOptions,
|
||||
pTextSize: action.payload,
|
||||
};
|
||||
}
|
||||
if (action.type === "changeSpelling") {
|
||||
return {
|
||||
...textOptions,
|
||||
spelling: action.payload,
|
||||
};
|
||||
}
|
||||
if (action.type === "changePhonetics") {
|
||||
return {
|
||||
...textOptions,
|
||||
phonetics: action.payload,
|
||||
};
|
||||
}
|
||||
if (action.type === "changeDialect") {
|
||||
return {
|
||||
...textOptions,
|
||||
dialect: action.payload,
|
||||
};
|
||||
}
|
||||
if (action.type === "changeDiacritics") {
|
||||
return {
|
||||
...textOptions,
|
||||
diacritics: action.payload,
|
||||
};
|
||||
}
|
||||
throw new Error("action type not recognized in text options reducer");
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
submissionBase,
|
||||
addSubmission,
|
||||
} from "../lib/submissions";
|
||||
import { getTextOptions } from "../lib/get-text-options";
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
const textFields: {field: T.DictionaryEntryTextField, label: string}[] = [
|
||||
|
@ -116,6 +117,7 @@ function EntryEditor({ state, dictionary, searchParams }: {
|
|||
setMatchingEntries(state.isolatedEntry ? searchForMatchingEntries(state.isolatedEntry.p) : []);
|
||||
// eslint-disable-next-line
|
||||
}, [state]);
|
||||
const textOptions = getTextOptions(state);
|
||||
function searchForMatchingEntries(s: string): T.DictionaryEntry[] {
|
||||
return dictionary.exactPashtoSearch(s)
|
||||
.filter((w) => w.ts !== state.isolatedEntry?.ts);
|
||||
|
@ -181,15 +183,15 @@ function EntryEditor({ state, dictionary, searchParams }: {
|
|||
})();
|
||||
const linkField: { field: "l", label: string | JSX.Element } = {
|
||||
field: "l",
|
||||
label: <>link {entry.l ? (complement ? <InlinePs opts={state.options.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" }}>
|
||||
<Helmet>
|
||||
<link rel="canonical" href="https://dictionary.lingdocs.com/edit" />
|
||||
<title>Edit - LingDocs Pashto Dictionary</title>
|
||||
</Helmet>
|
||||
{state.isolatedEntry && <Entry nonClickable entry={state.isolatedEntry} textOptions={state.options.textOptions} isolateEntry={() => null} />}
|
||||
{suggestedWord && <InlinePs opts={state.options.textOptions}>{suggestedWord}</InlinePs>}
|
||||
{state.isolatedEntry && <Entry nonClickable entry={state.isolatedEntry} textOptions={textOptions} isolateEntry={() => null} />}
|
||||
{suggestedWord && <InlinePs opts={textOptions}>{suggestedWord}</InlinePs>}
|
||||
{comment && <p>Comment: "{comment}"</p>}
|
||||
{submitted ? "Edit submitted/saved" : deleted ? "Entry Deleted" :
|
||||
<div>
|
||||
|
@ -198,7 +200,7 @@ function EntryEditor({ state, dictionary, searchParams }: {
|
|||
{matchingEntries.map((entry) => (
|
||||
<div key={entry.ts}>
|
||||
<Link to={`/edit?id=${entry.ts}`} className="plain-link">
|
||||
<InlinePs opts={state.options.textOptions}>{entry}</InlinePs>
|
||||
<InlinePs opts={textOptions}>{entry}</InlinePs>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
|
@ -330,12 +332,12 @@ function EntryEditor({ state, dictionary, searchParams }: {
|
|||
</ul>
|
||||
</div>}
|
||||
</form>
|
||||
{inflections && <InflectionsTable inf={inflections} textOptions={state.options.textOptions} />}
|
||||
{inflections && <InflectionsTable inf={inflections} textOptions={textOptions} />}
|
||||
{/* TODO: aay tail from state options */}
|
||||
<ConjugationViewer
|
||||
entry={entry}
|
||||
complement={complement}
|
||||
textOptions={state.options.textOptions}
|
||||
textOptions={textOptions}
|
||||
/>
|
||||
</div>}
|
||||
</div>;
|
||||
|
|
|
@ -31,6 +31,7 @@ import { wordlistEnabled } from "../lib/level-management";
|
|||
import AudioPlayButton from "../components/AudioPlayButton";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Modal } from "react-bootstrap";
|
||||
import { getTextOptions } from "../lib/get-text-options";
|
||||
|
||||
function IsolatedEntry({ state, dictionary, isolateEntry }: {
|
||||
state: State,
|
||||
|
@ -47,6 +48,7 @@ function IsolatedEntry({ state, dictionary, isolateEntry }: {
|
|||
setEditSubmitted(false);
|
||||
}, [state]);
|
||||
const wordlistWord = state.wordlist.find((w) => w.entry.ts === state.isolatedEntry?.ts);
|
||||
const textOptions = getTextOptions(state);
|
||||
function submitEdit() {
|
||||
if (!state.isolatedEntry) return;
|
||||
if (!state.user) return;
|
||||
|
@ -102,7 +104,7 @@ function IsolatedEntry({ state, dictionary, isolateEntry }: {
|
|||
<Entry
|
||||
nonClickable
|
||||
entry={entry}
|
||||
textOptions={state.options.textOptions}
|
||||
textOptions={textOptions}
|
||||
isolateEntry={isolateEntry}
|
||||
/>
|
||||
</div>
|
||||
|
@ -178,12 +180,12 @@ function IsolatedEntry({ state, dictionary, isolateEntry }: {
|
|||
</div>
|
||||
}
|
||||
{editSubmitted && <p>Thank you for your help!</p>}
|
||||
{inflections && <InflectionsTable inf={inflections} textOptions={state.options.textOptions} />}
|
||||
{inflections && <InflectionsTable inf={inflections} textOptions={textOptions} />}
|
||||
{/* TODO: State options for tail type here */}
|
||||
<ConjugationViewer
|
||||
entry={entry}
|
||||
complement={complement}
|
||||
textOptions={state.options.textOptions}
|
||||
textOptions={textOptions}
|
||||
/>
|
||||
{relatedEntries && <>
|
||||
{relatedEntries.length ?
|
||||
|
|
|
@ -131,10 +131,12 @@ function Options({
|
|||
options,
|
||||
state,
|
||||
optionsDispatch,
|
||||
textOptionsDispatch,
|
||||
}: {
|
||||
options: Options,
|
||||
state: State,
|
||||
optionsDispatch: (action: OptionsAction) => void,
|
||||
textOptionsDispatch: (action: TextOptionsAction) => void,
|
||||
}) {
|
||||
return <div style={{ maxWidth: "700px", marginBottom: "150px" }}>
|
||||
<Helmet>
|
||||
|
@ -188,22 +190,22 @@ function Options({
|
|||
<ButtonSelect
|
||||
small
|
||||
options={fontSizeOptions}
|
||||
value={options.textOptions.pTextSize}
|
||||
handleChange={(p) => optionsDispatch({ type: "changePTextSize", payload: p as PTextSize })}
|
||||
value={options.textOptionsRecord.textOptions.pTextSize}
|
||||
handleChange={(p) => textOptionsDispatch({ type: "changePTextSize", payload: p as PTextSize })}
|
||||
/>
|
||||
<h4 className="mt-3">Diacritics</h4>
|
||||
<ButtonSelect
|
||||
small
|
||||
options={booleanOptions}
|
||||
value={options.textOptions.diacritics.toString()}
|
||||
handleChange={(p) => optionsDispatch({ type: "changeDiacritics", payload: p === "true" })}
|
||||
value={options.textOptionsRecord.textOptions.diacritics.toString()}
|
||||
handleChange={(p) => textOptionsDispatch({ type: "changeDiacritics", payload: p === "true" })}
|
||||
/>
|
||||
<h4 className="mt-3">Pashto Spelling</h4>
|
||||
<ButtonSelect
|
||||
small
|
||||
options={spellingOptions}
|
||||
value={options.textOptions.spelling}
|
||||
handleChange={(p) => optionsDispatch({ type: "changeSpelling", payload: p as T.Spelling })}
|
||||
value={options.textOptionsRecord.textOptions.spelling}
|
||||
handleChange={(p) => textOptionsDispatch({ type: "changeSpelling", payload: p as T.Spelling })}
|
||||
/>
|
||||
{/* NEED TO UPDATE THE PHONETICS DIALECT OPTION THING */}
|
||||
{/* <h4 className="mt-3">Phonetics</h4>
|
||||
|
|
|
@ -41,6 +41,7 @@ function Results({ state, isolateEntry }: {
|
|||
const [pashto, setPashto] = useState<string>("");
|
||||
const [phonetics, setPhonetics] = useState<string>("");
|
||||
const [english, setEnglish] = useState<string>("");
|
||||
const textOptions = state.user?.textOptions ? state.user.textOptions : state.options.textOptions;
|
||||
useEffect(() => {
|
||||
setPowerResults(undefined);
|
||||
}, [state.searchValue])
|
||||
|
@ -82,7 +83,7 @@ function Results({ state, isolateEntry }: {
|
|||
const allDocs = allEntries();
|
||||
const results = searchAllInflections(
|
||||
allDocs,
|
||||
prepValueForSearch(state.searchValue, state.options.textOptions),
|
||||
prepValueForSearch(state.searchValue, textOptions),
|
||||
);
|
||||
setPowerResults(results);
|
||||
}, 20);
|
||||
|
@ -118,14 +119,14 @@ function Results({ state, isolateEntry }: {
|
|||
<Entry
|
||||
key={p.entry.i}
|
||||
entry={p.entry}
|
||||
textOptions={state.options.textOptions}
|
||||
textOptions={textOptions}
|
||||
isolateEntry={isolateEntry}
|
||||
/>
|
||||
<div className="mb-3 ml-2">
|
||||
{p.results.map((result: InflectionSearchResult, i) => (
|
||||
<InflectionSearchResult
|
||||
key={"inf-result" + i}
|
||||
textOptions={state.options.textOptions}
|
||||
textOptions={textOptions}
|
||||
result={result}
|
||||
entry={p.entry}
|
||||
/>
|
||||
|
@ -138,7 +139,7 @@ function Results({ state, isolateEntry }: {
|
|||
<Entry
|
||||
key={entry.i}
|
||||
entry={entry}
|
||||
textOptions={state.options.textOptions}
|
||||
textOptions={textOptions}
|
||||
isolateEntry={isolateEntry}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
Types as T,
|
||||
} from "@lingdocs/pashto-inflector";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { getTextOptions } from "../lib/get-text-options";
|
||||
|
||||
function ReviewTask({ reviewTask, textOptions }: { reviewTask: FT.ReviewTask, textOptions: T.TextOptions }) {
|
||||
function handleDelete() {
|
||||
|
@ -50,13 +51,14 @@ function ReviewTask({ reviewTask, textOptions }: { reviewTask: FT.ReviewTask, te
|
|||
}
|
||||
|
||||
export default function ReviewTasks({ state }: { state: State }) {
|
||||
const textOptions = getTextOptions(state);
|
||||
return <div className="width-limiter" style={{ marginBottom: "70px" }}>
|
||||
<Helmet>
|
||||
<title>Review Tasks - LingDocs Pashto Dictionary</title>
|
||||
</Helmet>
|
||||
<h3 className="mb-4">Review Tasks</h3>
|
||||
{state.reviewTasks.length ?
|
||||
state.reviewTasks.map((reviewTask, i) => <ReviewTask key={i} reviewTask={reviewTask} textOptions={state.options.textOptions} />)
|
||||
state.reviewTasks.map((reviewTask, i) => <ReviewTask key={i} reviewTask={reviewTask} textOptions={textOptions} />)
|
||||
: <p>None</p>
|
||||
}
|
||||
</div>;
|
||||
|
|
|
@ -44,6 +44,8 @@ import AudioPlayButton from "../components/AudioPlayButton";
|
|||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime.js";
|
||||
import hitBottom from "../lib/hitBottom";
|
||||
import { getTextOptions } from "../lib/get-text-options";
|
||||
|
||||
const cleanupIcon = "broom";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
@ -94,6 +96,7 @@ function Wordlist({ state, isolateEntry, optionsDispatch }: {
|
|||
// eslint-disable-next-line
|
||||
}, []);
|
||||
const toReview = forReview(state.wordlist);
|
||||
const textOptions = getTextOptions(state);
|
||||
function handleScroll() {
|
||||
// TODO: DON'T HAVE ENDLESS PAGE INCREASING
|
||||
if (hitBottom() && state.options.wordlistMode === "browse") {
|
||||
|
@ -116,7 +119,7 @@ function Wordlist({ state, isolateEntry, optionsDispatch }: {
|
|||
}
|
||||
function handleSearchValueChange(value: string) {
|
||||
setWordlistSearchValue(value);
|
||||
const results = value ? searchWordlist(value, state.wordlist, state.options.textOptions) : [];
|
||||
const results = value ? searchWordlist(value, state.wordlist, textOptions) : [];
|
||||
setFilteredWords(results);
|
||||
}
|
||||
async function handleGetWordlistCSV() {
|
||||
|
@ -152,7 +155,7 @@ function Wordlist({ state, isolateEntry, optionsDispatch }: {
|
|||
return <div className="mb-4">
|
||||
<Entry
|
||||
entry={word.entry}
|
||||
textOptions={state.options.textOptions}
|
||||
textOptions={textOptions}
|
||||
isolateEntry={() => handleWordClickBrowse(word._id)}
|
||||
/>
|
||||
{hasAttachment(word, "audio") && <AudioPlayButton word={word} />}
|
||||
|
@ -217,14 +220,14 @@ function Wordlist({ state, isolateEntry, optionsDispatch }: {
|
|||
<div className="card-body">
|
||||
<h6 className="card-title text-center">
|
||||
{state.options.wordlistReviewLanguage === "Pashto"
|
||||
? <InlinePs opts={state.options.textOptions}>{{ p: word.entry.p, f: word.entry.f }}</InlinePs>
|
||||
? <InlinePs opts={textOptions}>{{ p: word.entry.p, f: word.entry.f }}</InlinePs>
|
||||
: word.entry.e
|
||||
}
|
||||
</h6>
|
||||
{beingQuizzed && <div className="card-text text-center">
|
||||
{state.options.wordlistReviewLanguage === "Pashto"
|
||||
? <div>{word.entry.e}</div>
|
||||
: <InlinePs opts={state.options.textOptions}>
|
||||
: <InlinePs opts={textOptions}>
|
||||
{{ p: word.entry.p, f: word.entry.f }}
|
||||
</InlinePs>
|
||||
}
|
||||
|
@ -311,7 +314,7 @@ function Wordlist({ state, isolateEntry, optionsDispatch }: {
|
|||
const { e, ...ps } = nextUp.entry;
|
||||
return <div>
|
||||
<div className="lead my-3">None to review</div>
|
||||
<p>Next word up for review <strong>{dayjs().to(nextUp.dueDate)}</strong>: <InlinePs opts={state.options.textOptions}>
|
||||
<p>Next word up for review <strong>{dayjs().to(nextUp.dueDate)}</strong>: <InlinePs opts={textOptions}>
|
||||
{removeFVariants(ps)}
|
||||
</InlinePs></p>
|
||||
</div>;
|
||||
|
|
|
@ -17,11 +17,16 @@ type SearchBarPosition = "top" | "bottom";
|
|||
|
||||
type WordlistMode = "browse" | "review";
|
||||
|
||||
type TextOptionsRecord = {
|
||||
lastModified: import("./lib/account-types").TimeStamp,
|
||||
textOptions: import("@lingdocs/pashto-inflector").Types.TextOptions,
|
||||
};
|
||||
|
||||
type Options = {
|
||||
language: Language,
|
||||
searchType: SearchType,
|
||||
theme: Theme,
|
||||
textOptions: import("@lingdocs/pashto-inflector").Types.TextOptions,
|
||||
textOptionsRecord: TextOptionsRecord,
|
||||
wordlistMode: WordlistMode,
|
||||
wordlistReviewLanguage: Language,
|
||||
wordlistReviewBadge: boolean,
|
||||
|
@ -47,27 +52,12 @@ type OptionsAction = {
|
|||
type: "toggleSearchType",
|
||||
} | {
|
||||
type: "toggleLanguage",
|
||||
} | {
|
||||
type: "changePTextSize",
|
||||
payload: PTextSize,
|
||||
} | {
|
||||
type: "changeTheme",
|
||||
payload: Theme,
|
||||
} | {
|
||||
type: "changeSearchBarPosition",
|
||||
payload: SearchBarPosition,
|
||||
} | {
|
||||
type: "changeSpelling",
|
||||
payload: import("@lingdocs/pashto-inflector").Types.Spelling,
|
||||
} | {
|
||||
type: "changePhonetics",
|
||||
payload: import("@lingdocs/pashto-inflector").Types.Phonetics,
|
||||
} | {
|
||||
type: "changeDialect",
|
||||
payload: import("@lingdocs/pashto-inflector").Types.Dialect,
|
||||
} | {
|
||||
type: "changeDiacritics",
|
||||
payload: boolean,
|
||||
} | {
|
||||
type: "changeUserLevel",
|
||||
payload: UserLevel,
|
||||
|
@ -80,6 +70,26 @@ type OptionsAction = {
|
|||
} | {
|
||||
type: "changeWordlistReviewBadge",
|
||||
payload: boolean,
|
||||
} | {
|
||||
type: "updateTextOptionsRecord",
|
||||
payload: TextOptionsRecord,
|
||||
};
|
||||
|
||||
type TextOptionsAction = {
|
||||
type: "changePTextSize",
|
||||
payload: PTextSize,
|
||||
} | {
|
||||
type: "changeSpelling",
|
||||
payload: import("@lingdocs/pashto-inflector").Types.Spelling,
|
||||
} | {
|
||||
type: "changePhonetics",
|
||||
payload: import("@lingdocs/pashto-inflector").Types.Phonetics,
|
||||
} | {
|
||||
type: "changeDialect",
|
||||
payload: import("@lingdocs/pashto-inflector").Types.Dialect,
|
||||
} | {
|
||||
type: "changeDiacritics",
|
||||
payload: boolean,
|
||||
};
|
||||
|
||||
type DictionaryAPI = {
|
||||
|
|
Loading…
Reference in New Issue