mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-11-11 06:31:51 +08:00
impr: add local storage with schema class to improve type safety (@miodec) (#5763)
!nuf
This commit is contained in:
parent
38a8529808
commit
55e183e7bb
24 changed files with 642 additions and 264 deletions
|
|
@ -21,7 +21,6 @@ import {
|
|||
CustomTheme,
|
||||
DBResult,
|
||||
MonkeyMail,
|
||||
ResultFilters,
|
||||
UserInventory,
|
||||
UserProfileDetails,
|
||||
UserQuoteRatings,
|
||||
|
|
@ -33,6 +32,7 @@ import {
|
|||
PersonalBest,
|
||||
} from "@monkeytype/contracts/schemas/shared";
|
||||
import { addImportantLog } from "./logs";
|
||||
import { ResultFilters } from "@monkeytype/contracts/schemas/users";
|
||||
|
||||
const SECONDS_PER_HOUR = 3600;
|
||||
|
||||
|
|
|
|||
73
frontend/__tests__/elements/account/result-filters.spec.ts
Normal file
73
frontend/__tests__/elements/account/result-filters.spec.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import defaultResultFilters from "../../../src/ts/constants/default-result-filters";
|
||||
import { mergeWithDefaultFilters } from "../../../src/ts/elements/account/result-filters";
|
||||
|
||||
describe("result-filters.ts", () => {
|
||||
describe("mergeWithDefaultFilters", () => {
|
||||
it("should merge with default filters correctly", () => {
|
||||
const tests = [
|
||||
{
|
||||
input: {
|
||||
pb: {
|
||||
no: false,
|
||||
yes: false,
|
||||
},
|
||||
},
|
||||
expected: () => {
|
||||
const expected = defaultResultFilters;
|
||||
expected.pb.no = false;
|
||||
expected.pb.yes = false;
|
||||
return expected;
|
||||
},
|
||||
},
|
||||
{
|
||||
input: {
|
||||
words: {
|
||||
"10": false,
|
||||
},
|
||||
},
|
||||
expected: () => {
|
||||
const expected = defaultResultFilters;
|
||||
expected.words["10"] = false;
|
||||
return expected;
|
||||
},
|
||||
},
|
||||
{
|
||||
input: {
|
||||
blah: true,
|
||||
},
|
||||
expected: () => {
|
||||
return defaultResultFilters;
|
||||
},
|
||||
},
|
||||
{
|
||||
input: 1,
|
||||
expected: () => {
|
||||
return defaultResultFilters;
|
||||
},
|
||||
},
|
||||
{
|
||||
input: null,
|
||||
expected: () => {
|
||||
return defaultResultFilters;
|
||||
},
|
||||
},
|
||||
{
|
||||
input: undefined,
|
||||
expected: () => {
|
||||
return defaultResultFilters;
|
||||
},
|
||||
},
|
||||
{
|
||||
input: {},
|
||||
expected: () => {
|
||||
return defaultResultFilters;
|
||||
},
|
||||
},
|
||||
];
|
||||
tests.forEach((test) => {
|
||||
const merged = mergeWithDefaultFilters(test.input as any);
|
||||
expect(merged).toEqual(test.expected());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
import { isObject } from "../../src/ts/utils/misc";
|
||||
import {
|
||||
getLanguageDisplayString,
|
||||
removeLanguageSize,
|
||||
} from "../../src/ts/utils/strings";
|
||||
|
||||
//todo this file is in the wrong place
|
||||
|
||||
describe("misc.ts", () => {
|
||||
describe("getLanguageDisplayString", () => {
|
||||
it("should return correctly formatted strings", () => {
|
||||
|
|
@ -72,4 +75,47 @@ describe("misc.ts", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
describe("isObject", () => {
|
||||
it("should correctly identify objects", () => {
|
||||
const tests = [
|
||||
{
|
||||
input: {},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
input: { a: 1 },
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
input: [],
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
input: [1, 2, 3],
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
input: "string",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
input: 1,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
input: null,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
input: undefined,
|
||||
expected: false,
|
||||
},
|
||||
];
|
||||
|
||||
tests.forEach((test) => {
|
||||
const result = isObject(test.input);
|
||||
expect(result).toBe(test.expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
126
frontend/__tests__/utils/local-storage-with-schema.spec.ts
Normal file
126
frontend/__tests__/utils/local-storage-with-schema.spec.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import { z } from "zod";
|
||||
import { LocalStorageWithSchema } from "../../src/ts/utils/local-storage-with-schema";
|
||||
|
||||
describe("local-storage-with-schema.ts", () => {
|
||||
describe("LocalStorageWithSchema", () => {
|
||||
const objectSchema = z.object({
|
||||
punctuation: z.boolean(),
|
||||
mode: z.enum(["words", "time"]),
|
||||
fontSize: z.number(),
|
||||
});
|
||||
|
||||
const defaultObject: z.infer<typeof objectSchema> = {
|
||||
punctuation: true,
|
||||
mode: "words",
|
||||
fontSize: 16,
|
||||
};
|
||||
|
||||
const ls = new LocalStorageWithSchema({
|
||||
key: "config",
|
||||
schema: objectSchema,
|
||||
fallback: defaultObject,
|
||||
});
|
||||
|
||||
const getItemMock = vi.fn();
|
||||
const setItemMock = vi.fn();
|
||||
const removeItemMock = vi.fn();
|
||||
|
||||
vi.stubGlobal("localStorage", {
|
||||
getItem: getItemMock,
|
||||
setItem: setItemMock,
|
||||
removeItem: removeItemMock,
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
getItemMock.mockReset();
|
||||
setItemMock.mockReset();
|
||||
removeItemMock.mockReset();
|
||||
});
|
||||
|
||||
it("should save to localStorage if schema is correct and return true", () => {
|
||||
const res = ls.set(defaultObject);
|
||||
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith(
|
||||
"config",
|
||||
JSON.stringify(defaultObject)
|
||||
);
|
||||
expect(res).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail to save to localStorage if schema is incorrect and return false", () => {
|
||||
const obj = {
|
||||
hi: "hello",
|
||||
};
|
||||
|
||||
const res = ls.set(obj as any);
|
||||
|
||||
expect(localStorage.setItem).not.toHaveBeenCalled();
|
||||
expect(res).toBe(false);
|
||||
});
|
||||
|
||||
it("should revert to the fallback value if localstorage is null", () => {
|
||||
getItemMock.mockReturnValue(null);
|
||||
|
||||
const res = ls.get();
|
||||
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("config");
|
||||
expect(res).toEqual(defaultObject);
|
||||
});
|
||||
|
||||
it("should revert to the fallback value and remove if localstorage json is malformed", () => {
|
||||
getItemMock.mockReturnValue("badjson");
|
||||
|
||||
const res = ls.get();
|
||||
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("config");
|
||||
expect(localStorage.removeItem).toHaveBeenCalledWith("config");
|
||||
expect(res).toEqual(defaultObject);
|
||||
});
|
||||
|
||||
it("should get from localStorage", () => {
|
||||
getItemMock.mockReturnValue(JSON.stringify(defaultObject));
|
||||
|
||||
const res = ls.get();
|
||||
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("config");
|
||||
expect(res).toEqual(defaultObject);
|
||||
});
|
||||
|
||||
it("should revert to fallback value if no migrate function and schema failed", () => {
|
||||
getItemMock.mockReturnValue(JSON.stringify({ hi: "hello" }));
|
||||
const ls = new LocalStorageWithSchema({
|
||||
key: "config",
|
||||
schema: objectSchema,
|
||||
fallback: defaultObject,
|
||||
});
|
||||
|
||||
const res = ls.get();
|
||||
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("config");
|
||||
expect(res).toEqual(defaultObject);
|
||||
});
|
||||
|
||||
it("should migrate (when function is provided) if schema failed", () => {
|
||||
getItemMock.mockReturnValue(JSON.stringify({ hi: "hello" }));
|
||||
|
||||
const migrated = {
|
||||
punctuation: false,
|
||||
mode: "time",
|
||||
fontSize: 1,
|
||||
};
|
||||
const ls = new LocalStorageWithSchema({
|
||||
key: "config",
|
||||
schema: objectSchema,
|
||||
fallback: defaultObject,
|
||||
migrate: () => {
|
||||
return migrated;
|
||||
},
|
||||
});
|
||||
|
||||
const res = ls.get();
|
||||
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("config");
|
||||
expect(res).toEqual(migrated);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import {
|
||||
CountByYearAndDay,
|
||||
CustomTheme,
|
||||
ResultFilters,
|
||||
UserProfile,
|
||||
UserProfileDetails,
|
||||
UserTag,
|
||||
} from "@monkeytype/shared-types";
|
||||
import { Mode, Mode2 } from "@monkeytype/contracts/schemas/shared";
|
||||
import { ResultFilters } from "@monkeytype/contracts/schemas/users";
|
||||
|
||||
const BASE_PATH = "/users";
|
||||
|
||||
|
|
|
|||
|
|
@ -16,14 +16,35 @@ import {
|
|||
canSetConfigWithCurrentFunboxes,
|
||||
canSetFunboxWithConfig,
|
||||
} from "./test/funbox/funbox-validation";
|
||||
import { isDevEnvironment, reloadAfter, typedKeys } from "./utils/misc";
|
||||
import {
|
||||
isDevEnvironment,
|
||||
isObject,
|
||||
reloadAfter,
|
||||
typedKeys,
|
||||
} from "./utils/misc";
|
||||
import * as ConfigSchemas from "@monkeytype/contracts/schemas/configs";
|
||||
import { Config } from "@monkeytype/contracts/schemas/configs";
|
||||
import { roundTo1 } from "./utils/numbers";
|
||||
import { Mode, ModeSchema } from "@monkeytype/contracts/schemas/shared";
|
||||
import { Language, LanguageSchema } from "@monkeytype/contracts/schemas/util";
|
||||
import { LocalStorageWithSchema } from "./utils/local-storage-with-schema";
|
||||
import { mergeWithDefaultConfig } from "./utils/config";
|
||||
|
||||
export let localStorageConfig: Config;
|
||||
const configLS = new LocalStorageWithSchema({
|
||||
key: "config",
|
||||
schema: ConfigSchemas.ConfigSchema,
|
||||
fallback: DefaultConfig,
|
||||
migrate: (value, _issues) => {
|
||||
if (!isObject(value)) {
|
||||
return DefaultConfig;
|
||||
}
|
||||
|
||||
const configWithoutLegacyValues = replaceLegacyValues(value);
|
||||
const merged = mergeWithDefaultConfig(configWithoutLegacyValues);
|
||||
|
||||
return merged;
|
||||
},
|
||||
});
|
||||
|
||||
let loadDone: (value?: unknown) => void;
|
||||
|
||||
|
|
@ -48,29 +69,25 @@ function saveToLocalStorage(
|
|||
noDbCheck = false
|
||||
): void {
|
||||
if (nosave) return;
|
||||
|
||||
const localToSave = config;
|
||||
|
||||
const localToSaveStringified = JSON.stringify(localToSave);
|
||||
window.localStorage.setItem("config", localToSaveStringified);
|
||||
configLS.set(config);
|
||||
if (!noDbCheck) {
|
||||
//@ts-expect-error this is fine
|
||||
configToSend[key] = config[key];
|
||||
saveToDatabase();
|
||||
}
|
||||
const localToSaveStringified = JSON.stringify(config);
|
||||
ConfigEvent.dispatch("saveToLocalStorage", localToSaveStringified);
|
||||
}
|
||||
|
||||
export function saveFullConfigToLocalStorage(noDbCheck = false): void {
|
||||
console.log("saving full config to localStorage");
|
||||
const save = config;
|
||||
const stringified = JSON.stringify(save);
|
||||
window.localStorage.setItem("config", stringified);
|
||||
configLS.set(config);
|
||||
if (!noDbCheck) {
|
||||
AccountButton.loading(true);
|
||||
void DB.saveConfig(save);
|
||||
void DB.saveConfig(config);
|
||||
AccountButton.loading(false);
|
||||
}
|
||||
const stringified = JSON.stringify(config);
|
||||
ConfigEvent.dispatch("saveToLocalStorage", stringified);
|
||||
}
|
||||
|
||||
|
|
@ -1977,8 +1994,6 @@ export async function apply(
|
|||
|
||||
ConfigEvent.dispatch("fullConfigChange");
|
||||
|
||||
configToApply = replaceLegacyValues(configToApply);
|
||||
|
||||
const configObj = configToApply as Config;
|
||||
(Object.keys(DefaultConfig) as (keyof Config)[]).forEach((configKey) => {
|
||||
if (configObj[configKey] === undefined) {
|
||||
|
|
@ -2095,33 +2110,19 @@ export async function reset(): Promise<void> {
|
|||
|
||||
export async function loadFromLocalStorage(): Promise<void> {
|
||||
console.log("loading localStorage config");
|
||||
const newConfigString = window.localStorage.getItem("config");
|
||||
let newConfig: Config;
|
||||
if (
|
||||
newConfigString !== undefined &&
|
||||
newConfigString !== null &&
|
||||
newConfigString !== ""
|
||||
) {
|
||||
try {
|
||||
newConfig = JSON.parse(newConfigString);
|
||||
} catch (e) {
|
||||
newConfig = {} as Config;
|
||||
}
|
||||
await apply(newConfig);
|
||||
localStorageConfig = newConfig;
|
||||
saveFullConfigToLocalStorage(true);
|
||||
} else {
|
||||
const newConfig = configLS.get();
|
||||
if (newConfig === undefined) {
|
||||
await reset();
|
||||
} else {
|
||||
await apply(newConfig);
|
||||
saveFullConfigToLocalStorage(true);
|
||||
}
|
||||
// TestLogic.restart(false, true);
|
||||
loadDone();
|
||||
}
|
||||
|
||||
function replaceLegacyValues(
|
||||
configToApply: ConfigSchemas.PartialConfig | MonkeyTypes.ConfigChanges
|
||||
): ConfigSchemas.Config | MonkeyTypes.ConfigChanges {
|
||||
const configObj = configToApply as ConfigSchemas.Config;
|
||||
|
||||
export function replaceLegacyValues(
|
||||
configObj: ConfigSchemas.PartialConfig
|
||||
): ConfigSchemas.PartialConfig {
|
||||
//@ts-expect-error
|
||||
if (configObj.quickTab === true) {
|
||||
configObj.quickRestart = "tab";
|
||||
|
|
@ -2159,7 +2160,7 @@ function replaceLegacyValues(
|
|||
if (configObj.showLiveWpm === true) {
|
||||
let val: ConfigSchemas.LiveSpeedAccBurstStyle = "mini";
|
||||
if (configObj.timerStyle !== "bar" && configObj.timerStyle !== "off") {
|
||||
val = configObj.timerStyle;
|
||||
val = configObj.timerStyle as ConfigSchemas.LiveSpeedAccBurstStyle;
|
||||
}
|
||||
configObj.liveSpeedStyle = val;
|
||||
}
|
||||
|
|
@ -2168,7 +2169,7 @@ function replaceLegacyValues(
|
|||
if (configObj.showLiveBurst === true) {
|
||||
let val: ConfigSchemas.LiveSpeedAccBurstStyle = "mini";
|
||||
if (configObj.timerStyle !== "bar" && configObj.timerStyle !== "off") {
|
||||
val = configObj.timerStyle;
|
||||
val = configObj.timerStyle as ConfigSchemas.LiveSpeedAccBurstStyle;
|
||||
}
|
||||
configObj.liveBurstStyle = val;
|
||||
}
|
||||
|
|
@ -2177,7 +2178,7 @@ function replaceLegacyValues(
|
|||
if (configObj.showLiveAcc === true) {
|
||||
let val: ConfigSchemas.LiveSpeedAccBurstStyle = "mini";
|
||||
if (configObj.timerStyle !== "bar" && configObj.timerStyle !== "off") {
|
||||
val = configObj.timerStyle;
|
||||
val = configObj.timerStyle as ConfigSchemas.LiveSpeedAccBurstStyle;
|
||||
}
|
||||
configObj.liveAccStyle = val;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ResultFilters } from "@monkeytype/shared-types";
|
||||
import { ResultFilters } from "@monkeytype/contracts/schemas/users";
|
||||
|
||||
const object: ResultFilters = {
|
||||
_id: "default-result-filters-id",
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ async function getDataAndInit(): Promise<boolean> {
|
|||
const areConfigsEqual =
|
||||
JSON.stringify(Config) === JSON.stringify(snapshot.config);
|
||||
|
||||
if (UpdateConfig.localStorageConfig === undefined || !areConfigsEqual) {
|
||||
if (Config === undefined || !areConfigsEqual) {
|
||||
console.log(
|
||||
"no local config or local and db configs are different - applying db"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,25 @@
|
|||
import { z } from "zod";
|
||||
import * as DB from "../db";
|
||||
import * as ModesNotice from "../elements/modes-notice";
|
||||
import { LocalStorageWithSchema } from "../utils/local-storage-with-schema";
|
||||
import { IdSchema } from "@monkeytype/contracts/schemas/util";
|
||||
|
||||
const activeTagsLS = new LocalStorageWithSchema({
|
||||
key: "activeTags",
|
||||
schema: z.array(IdSchema),
|
||||
fallback: [],
|
||||
});
|
||||
|
||||
export function saveActiveToLocalStorage(): void {
|
||||
const tags: string[] = [];
|
||||
|
||||
try {
|
||||
DB.getSnapshot()?.tags?.forEach((tag) => {
|
||||
if (tag.active === true) {
|
||||
tags.push(tag._id);
|
||||
}
|
||||
});
|
||||
window.localStorage.setItem("activeTags", JSON.stringify(tags));
|
||||
} catch (e) {}
|
||||
DB.getSnapshot()?.tags?.forEach((tag) => {
|
||||
if (tag.active === true) {
|
||||
tags.push(tag._id);
|
||||
}
|
||||
});
|
||||
|
||||
activeTagsLS.set(tags);
|
||||
}
|
||||
|
||||
export function clear(nosave = false): void {
|
||||
|
|
@ -61,18 +69,9 @@ export function toggle(tagid: string, nosave = false): void {
|
|||
}
|
||||
|
||||
export function loadActiveFromLocalStorage(): void {
|
||||
let newTags: string[] | string = window.localStorage.getItem(
|
||||
"activeTags"
|
||||
) as string;
|
||||
if (newTags != undefined && newTags !== "") {
|
||||
try {
|
||||
newTags = JSON.parse(newTags) ?? [];
|
||||
} catch (e) {
|
||||
newTags = [];
|
||||
}
|
||||
(newTags as string[]).forEach((ntag) => {
|
||||
toggle(ntag, true);
|
||||
});
|
||||
saveActiveToLocalStorage();
|
||||
const newTags = activeTagsLS.get();
|
||||
for (const tag of newTags) {
|
||||
toggle(tag, true);
|
||||
}
|
||||
saveActiveToLocalStorage();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,50 @@ import Ape from "../../ape/index";
|
|||
import * as Loader from "../loader";
|
||||
// @ts-expect-error TODO: update slim-select
|
||||
import SlimSelect from "slim-select";
|
||||
import { ResultFilters } from "@monkeytype/shared-types";
|
||||
import { QuoteLength } from "@monkeytype/contracts/schemas/configs";
|
||||
import {
|
||||
ResultFilters,
|
||||
ResultFiltersSchema,
|
||||
ResultFiltersGroup,
|
||||
ResultFiltersGroupItem,
|
||||
} from "@monkeytype/contracts/schemas/users";
|
||||
import { LocalStorageWithSchema } from "../../utils/local-storage-with-schema";
|
||||
import defaultResultFilters from "../../constants/default-result-filters";
|
||||
|
||||
export function mergeWithDefaultFilters(
|
||||
filters: Partial<ResultFilters>
|
||||
): ResultFilters {
|
||||
try {
|
||||
const merged = {} as ResultFilters;
|
||||
for (const groupKey of Misc.typedKeys(defaultResultFilters)) {
|
||||
if (groupKey === "_id" || groupKey === "name") {
|
||||
merged[groupKey] = filters[groupKey] ?? defaultResultFilters[groupKey];
|
||||
} else {
|
||||
// @ts-expect-error i cant figure this out
|
||||
merged[groupKey] = {
|
||||
...defaultResultFilters[groupKey],
|
||||
...filters[groupKey],
|
||||
};
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
} catch (e) {
|
||||
return defaultResultFilters;
|
||||
}
|
||||
}
|
||||
|
||||
const resultFiltersLS = new LocalStorageWithSchema({
|
||||
key: "resultFilters",
|
||||
schema: ResultFiltersSchema,
|
||||
fallback: defaultResultFilters,
|
||||
migrate: (unknown, _issues) => {
|
||||
if (!Misc.isObject(unknown)) {
|
||||
return defaultResultFilters;
|
||||
}
|
||||
return mergeWithDefaultFilters(unknown as ResultFilters);
|
||||
},
|
||||
});
|
||||
|
||||
type Option = {
|
||||
id: string;
|
||||
value: string;
|
||||
|
|
@ -36,51 +76,14 @@ const groupSelects: Partial<Record<keyof ResultFilters, SlimSelect>> = {};
|
|||
let filters = defaultResultFilters;
|
||||
|
||||
function save(): void {
|
||||
window.localStorage.setItem("resultFilters", JSON.stringify(filters));
|
||||
resultFiltersLS.set(filters);
|
||||
}
|
||||
|
||||
export async function load(): Promise<void> {
|
||||
try {
|
||||
const newResultFilters = window.localStorage.getItem("resultFilters") ?? "";
|
||||
|
||||
if (!newResultFilters) {
|
||||
filters = defaultResultFilters;
|
||||
} else {
|
||||
const newFiltersObject = JSON.parse(newResultFilters);
|
||||
|
||||
let reset = false;
|
||||
for (const key of Object.keys(defaultResultFilters)) {
|
||||
if (reset) break;
|
||||
if (newFiltersObject[key] === undefined) {
|
||||
reset = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof defaultResultFilters[
|
||||
key as keyof typeof defaultResultFilters
|
||||
] === "object"
|
||||
) {
|
||||
for (const subKey of Object.keys(
|
||||
defaultResultFilters[key as keyof typeof defaultResultFilters]
|
||||
)) {
|
||||
if (newFiltersObject[key][subKey] === undefined) {
|
||||
reset = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reset) {
|
||||
filters = defaultResultFilters;
|
||||
} else {
|
||||
filters = newFiltersObject;
|
||||
}
|
||||
}
|
||||
const filters = resultFiltersLS.get();
|
||||
|
||||
const newTags: Record<string, boolean> = { none: false };
|
||||
|
||||
Object.keys(defaultResultFilters.tags).forEach((tag) => {
|
||||
if (filters.tags[tag] !== undefined) {
|
||||
newTags[tag] = filters.tags[tag];
|
||||
|
|
@ -90,7 +93,6 @@ export async function load(): Promise<void> {
|
|||
});
|
||||
|
||||
filters.tags = newTags;
|
||||
// await updateFilterPresets();
|
||||
save();
|
||||
} catch {
|
||||
console.log("error in loading result filters");
|
||||
|
|
@ -226,7 +228,7 @@ function getFilters(): ResultFilters {
|
|||
return filters;
|
||||
}
|
||||
|
||||
function getGroup<G extends keyof ResultFilters>(group: G): ResultFilters[G] {
|
||||
function getGroup<G extends ResultFiltersGroup>(group: G): ResultFilters[G] {
|
||||
return filters[group];
|
||||
}
|
||||
|
||||
|
|
@ -234,22 +236,22 @@ function getGroup<G extends keyof ResultFilters>(group: G): ResultFilters[G] {
|
|||
// filters[group][filter] = value;
|
||||
// }
|
||||
|
||||
export function getFilter<G extends keyof ResultFilters>(
|
||||
export function getFilter<G extends ResultFiltersGroup>(
|
||||
group: G,
|
||||
filter: MonkeyTypes.Filter<G>
|
||||
): ResultFilters[G][MonkeyTypes.Filter<G>] {
|
||||
filter: ResultFiltersGroupItem<G>
|
||||
): ResultFilters[G][ResultFiltersGroupItem<G>] {
|
||||
return filters[group][filter];
|
||||
}
|
||||
|
||||
function setFilter(
|
||||
group: keyof ResultFilters,
|
||||
filter: MonkeyTypes.Filter<typeof group>,
|
||||
function setFilter<G extends ResultFiltersGroup>(
|
||||
group: G,
|
||||
filter: ResultFiltersGroupItem<G>,
|
||||
value: boolean
|
||||
): void {
|
||||
filters[group][filter as keyof typeof filters[typeof group]] = value as never;
|
||||
filters[group][filter] = value as typeof filters[G][typeof filter];
|
||||
}
|
||||
|
||||
function setAllFilters(group: keyof ResultFilters, value: boolean): void {
|
||||
function setAllFilters(group: ResultFiltersGroup, value: boolean): void {
|
||||
Object.keys(getGroup(group)).forEach((filter) => {
|
||||
filters[group][filter as keyof typeof filters[typeof group]] =
|
||||
value as never;
|
||||
|
|
@ -268,7 +270,7 @@ export function reset(): void {
|
|||
}
|
||||
|
||||
type AboveChartDisplay = Partial<
|
||||
Record<keyof ResultFilters, { all: boolean; array?: string[] }>
|
||||
Record<ResultFiltersGroup, { all: boolean; array?: string[] }>
|
||||
>;
|
||||
|
||||
export function updateActive(): void {
|
||||
|
|
@ -290,7 +292,10 @@ export function updateActive(): void {
|
|||
|
||||
if (groupAboveChartDisplay === undefined) continue;
|
||||
|
||||
const filterValue = getFilter(group, filter);
|
||||
const filterValue = getFilter(
|
||||
group,
|
||||
filter as ResultFiltersGroupItem<typeof group>
|
||||
);
|
||||
if (filterValue === true) {
|
||||
groupAboveChartDisplay.array?.push(filter);
|
||||
} else {
|
||||
|
|
@ -330,7 +335,7 @@ export function updateActive(): void {
|
|||
|
||||
for (const [id, select] of Object.entries(groupSelects)) {
|
||||
const ss = select;
|
||||
const group = getGroup(id as keyof ResultFilters);
|
||||
const group = getGroup(id as ResultFiltersGroup);
|
||||
const everythingSelected = Object.values(group).every((v) => v === true);
|
||||
|
||||
const newData = ss.store.getData();
|
||||
|
|
@ -374,7 +379,7 @@ export function updateActive(): void {
|
|||
}, 0);
|
||||
}
|
||||
|
||||
function addText(group: keyof ResultFilters): string {
|
||||
function addText(group: ResultFiltersGroup): string {
|
||||
let ret = "";
|
||||
ret += "<div class='group'>";
|
||||
if (group === "difficulty") {
|
||||
|
|
@ -472,9 +477,9 @@ export function updateActive(): void {
|
|||
}, 0);
|
||||
}
|
||||
|
||||
function toggle<G extends keyof ResultFilters>(
|
||||
function toggle<G extends ResultFiltersGroup>(
|
||||
group: G,
|
||||
filter: MonkeyTypes.Filter<G>
|
||||
filter: ResultFiltersGroupItem<G>
|
||||
): void {
|
||||
// user is changing the filters -> current filter is no longer a filter preset
|
||||
deSelectFilterPreset();
|
||||
|
|
@ -486,7 +491,7 @@ function toggle<G extends keyof ResultFilters>(
|
|||
const currentValue = filters[group][filter] as unknown as boolean;
|
||||
const newValue = !currentValue;
|
||||
filters[group][filter] =
|
||||
newValue as unknown as ResultFilters[G][MonkeyTypes.Filter<G>];
|
||||
newValue as ResultFilters[G][ResultFiltersGroupItem<G>];
|
||||
save();
|
||||
} catch (e) {
|
||||
Notifications.add(
|
||||
|
|
@ -505,8 +510,10 @@ $(
|
|||
).on("click", "button", (e) => {
|
||||
const group = $(e.target)
|
||||
.parents(".buttons")
|
||||
.attr("group") as keyof ResultFilters;
|
||||
const filter = $(e.target).attr("filter") as MonkeyTypes.Filter<typeof group>;
|
||||
.attr("group") as ResultFiltersGroup;
|
||||
const filter = $(e.target).attr("filter") as ResultFiltersGroupItem<
|
||||
typeof group
|
||||
>;
|
||||
if ($(e.target).hasClass("allFilters")) {
|
||||
Misc.typedKeys(getFilters()).forEach((group) => {
|
||||
// id and name field do not correspond to any ui elements, no need to update
|
||||
|
|
@ -532,8 +539,8 @@ $(
|
|||
} else if ($(e.target).is("button")) {
|
||||
if (e.shiftKey) {
|
||||
setAllFilters(group, false);
|
||||
filters[group][filter as keyof typeof filters[typeof group]] =
|
||||
true as never;
|
||||
filters[group][filter] =
|
||||
true as ResultFilters[typeof group][typeof filter];
|
||||
} else {
|
||||
toggle(group, filter);
|
||||
// filters[group][filter] = !filters[group][filter];
|
||||
|
|
@ -596,7 +603,7 @@ $(".pageAccount .topFilters button.currentConfigFilter").on("click", () => {
|
|||
filters.words.custom = true;
|
||||
}
|
||||
} else if (Config.mode === "quote") {
|
||||
const filterName: MonkeyTypes.Filter<"quoteLength">[] = [
|
||||
const filterName: ResultFiltersGroupItem<"quoteLength">[] = [
|
||||
"short",
|
||||
"medium",
|
||||
"long",
|
||||
|
|
@ -627,7 +634,7 @@ $(".pageAccount .topFilters button.currentConfigFilter").on("click", () => {
|
|||
}
|
||||
|
||||
if (Config.funbox === "none") {
|
||||
filters.funbox.none = true;
|
||||
filters.funbox["none"] = true;
|
||||
} else {
|
||||
for (const f of Config.funbox.split("#")) {
|
||||
filters.funbox[f] = true;
|
||||
|
|
@ -656,7 +663,7 @@ $(".pageAccount .topFilters button.toggleAdvancedFilters").on("click", () => {
|
|||
});
|
||||
|
||||
function adjustScrollposition(
|
||||
group: keyof ResultFilters,
|
||||
group: ResultFiltersGroup,
|
||||
topItem: number = 0
|
||||
): void {
|
||||
const slimSelect = groupSelects[group];
|
||||
|
|
@ -668,7 +675,7 @@ function adjustScrollposition(
|
|||
}
|
||||
|
||||
function selectBeforeChangeFn(
|
||||
group: keyof ResultFilters,
|
||||
group: ResultFiltersGroup,
|
||||
selectedOptions: Option[],
|
||||
oldSelectedOptions: Option[]
|
||||
): void | boolean {
|
||||
|
|
@ -705,7 +712,11 @@ function selectBeforeChangeFn(
|
|||
break;
|
||||
}
|
||||
|
||||
setFilter(group, selectedOption.value, true);
|
||||
setFilter(
|
||||
group,
|
||||
selectedOption.value as ResultFiltersGroupItem<typeof group>,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
updateActive();
|
||||
|
|
@ -925,7 +936,7 @@ $(".group.presetFilterButtons .filterBtns").on(
|
|||
function verifyResultFiltersStructure(filterIn: ResultFilters): ResultFilters {
|
||||
const filter = deepCopyFilter(filterIn);
|
||||
Object.entries(defaultResultFilters).forEach((entry) => {
|
||||
const key = entry[0] as keyof ResultFilters;
|
||||
const key = entry[0] as ResultFiltersGroup;
|
||||
const value = entry[1];
|
||||
if (filter[key] === undefined) {
|
||||
// @ts-expect-error key and value is based on default filter so this is safe to ignore
|
||||
|
|
|
|||
24
frontend/src/ts/elements/merch-banner.ts
Normal file
24
frontend/src/ts/elements/merch-banner.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { z } from "zod";
|
||||
import { LocalStorageWithSchema } from "../utils/local-storage-with-schema";
|
||||
import * as Notifications from "./notifications";
|
||||
|
||||
const closed = new LocalStorageWithSchema({
|
||||
key: "merchBannerClosed",
|
||||
schema: z.boolean(),
|
||||
fallback: false,
|
||||
});
|
||||
|
||||
export function showIfNotClosedBefore(): void {
|
||||
if (!closed.get()) {
|
||||
Notifications.addBanner(
|
||||
`Check out our merchandise, available at <a target="_blank" rel="noopener" href="https://monkeytype.store/">monkeytype.store</a>`,
|
||||
1,
|
||||
"./images/merch2.png",
|
||||
false,
|
||||
() => {
|
||||
closed.set(true);
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,24 +5,28 @@ import * as Notifications from "./notifications";
|
|||
import { format } from "date-fns/format";
|
||||
import * as Alerts from "./alerts";
|
||||
import { PSA } from "@monkeytype/contracts/schemas/psas";
|
||||
import { z } from "zod";
|
||||
import { LocalStorageWithSchema } from "../utils/local-storage-with-schema";
|
||||
import { IdSchema } from "@monkeytype/contracts/schemas/util";
|
||||
|
||||
const confirmedPSAs = new LocalStorageWithSchema({
|
||||
key: "confirmedPSAs",
|
||||
schema: z.array(IdSchema),
|
||||
fallback: [],
|
||||
});
|
||||
|
||||
function clearMemory(): void {
|
||||
window.localStorage.setItem("confirmedPSAs", JSON.stringify([]));
|
||||
confirmedPSAs.set([]);
|
||||
}
|
||||
|
||||
function getMemory(): string[] {
|
||||
//TODO verify with zod?
|
||||
return (
|
||||
(JSON.parse(
|
||||
window.localStorage.getItem("confirmedPSAs") ?? "[]"
|
||||
) as string[]) ?? []
|
||||
);
|
||||
return confirmedPSAs.get();
|
||||
}
|
||||
|
||||
function setMemory(id: string): void {
|
||||
const list = getMemory();
|
||||
list.push(id);
|
||||
window.localStorage.setItem("confirmedPSAs", JSON.stringify(list));
|
||||
confirmedPSAs.set(list);
|
||||
}
|
||||
|
||||
async function getLatest(): Promise<PSA[] | null> {
|
||||
|
|
|
|||
|
|
@ -4,29 +4,30 @@ import { isPopupVisible } from "../utils/misc";
|
|||
import * as AdController from "../controllers/ad-controller";
|
||||
import AnimatedModal from "../utils/animated-modal";
|
||||
import { focusWords } from "../test/test-ui";
|
||||
import { LocalStorageWithSchema } from "../utils/local-storage-with-schema";
|
||||
import { z } from "zod";
|
||||
|
||||
type Accepted = {
|
||||
security: boolean;
|
||||
analytics: boolean;
|
||||
};
|
||||
const AcceptedSchema = z.object({
|
||||
security: z.boolean(),
|
||||
analytics: z.boolean(),
|
||||
});
|
||||
type Accepted = z.infer<typeof AcceptedSchema>;
|
||||
|
||||
function getAcceptedObject(): Accepted | null {
|
||||
const acceptedCookies = localStorage.getItem("acceptedCookies") ?? "";
|
||||
if (acceptedCookies) {
|
||||
//TODO verify with zod?
|
||||
return JSON.parse(acceptedCookies) as Accepted;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const acceptedCookiesLS = new LocalStorageWithSchema({
|
||||
key: "acceptedCookies",
|
||||
schema: AcceptedSchema,
|
||||
fallback: {
|
||||
security: false,
|
||||
analytics: false,
|
||||
},
|
||||
});
|
||||
|
||||
function setAcceptedObject(obj: Accepted): void {
|
||||
localStorage.setItem("acceptedCookies", JSON.stringify(obj));
|
||||
acceptedCookiesLS.set(obj);
|
||||
}
|
||||
|
||||
export function check(): void {
|
||||
const accepted = getAcceptedObject();
|
||||
if (accepted === null) {
|
||||
if (acceptedCookiesLS.get() === undefined) {
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import {
|
|||
Mode2Custom,
|
||||
PersonalBests,
|
||||
} from "@monkeytype/contracts/schemas/shared";
|
||||
import { ResultFiltersGroupItem } from "@monkeytype/contracts/schemas/users";
|
||||
|
||||
let filterDebug = false;
|
||||
//toggle filterdebug
|
||||
|
|
@ -386,7 +387,7 @@ async function fillContent(): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
let puncfilter: MonkeyTypes.Filter<"punctuation"> = "off";
|
||||
let puncfilter: ResultFiltersGroupItem<"punctuation"> = "off";
|
||||
if (result.punctuation) {
|
||||
puncfilter = "on";
|
||||
}
|
||||
|
|
@ -397,7 +398,7 @@ async function fillContent(): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
let numfilter: MonkeyTypes.Filter<"numbers"> = "off";
|
||||
let numfilter: ResultFiltersGroupItem<"numbers"> = "off";
|
||||
if (result.numbers) {
|
||||
numfilter = "on";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import Config from "./config";
|
||||
import * as Misc from "./utils/misc";
|
||||
import * as MonkeyPower from "./elements/monkey-power";
|
||||
import * as Notifications from "./elements/notifications";
|
||||
import * as MerchBanner from "./elements/merch-banner";
|
||||
import * as CookiesModal from "./modals/cookies";
|
||||
import * as ConnectionState from "./states/connection";
|
||||
import * as FunboxList from "./test/funbox/funbox-list";
|
||||
|
|
@ -18,21 +18,7 @@ $((): void => {
|
|||
//this line goes back to pretty much the beginning of the project and im pretty sure its here
|
||||
//to make sure the initial theme application doesnt animate the background color
|
||||
$("body").css("transition", "background .25s, transform .05s");
|
||||
const merchBannerClosed =
|
||||
window.localStorage.getItem("merchbannerclosed") === "true";
|
||||
if (!merchBannerClosed) {
|
||||
Notifications.addBanner(
|
||||
`Check out our merchandise, available at <a target="_blank" rel="noopener" href="https://monkeytype.store/">monkeytype.store</a>`,
|
||||
1,
|
||||
"./images/merch2.png",
|
||||
false,
|
||||
() => {
|
||||
window.localStorage.setItem("merchbannerclosed", "true");
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
MerchBanner.showIfNotClosedBefore();
|
||||
setTimeout(() => {
|
||||
FunboxList.get(Config.funbox).forEach((it) =>
|
||||
it.functions?.applyGlobalCSS?.()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,16 @@
|
|||
import { z } from "zod";
|
||||
import { LocalStorageWithSchema } from "../utils/local-storage-with-schema";
|
||||
|
||||
const ls = new LocalStorageWithSchema({
|
||||
key: "prefersArabicLazyMode",
|
||||
schema: z.boolean(),
|
||||
fallback: true,
|
||||
});
|
||||
|
||||
export function get(): boolean {
|
||||
return (localStorage.getItem("prefersArabicLazyMode") ?? "true") === "true";
|
||||
return ls.get();
|
||||
}
|
||||
|
||||
export function set(value: boolean): void {
|
||||
localStorage.setItem("prefersArabicLazyMode", value ? "true" : "false");
|
||||
ls.set(value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,22 @@
|
|||
import { z } from "zod";
|
||||
import { getLatestReleaseFromGitHub } from "../utils/json-data";
|
||||
import { LocalStorageWithSchema } from "../utils/local-storage-with-schema";
|
||||
|
||||
const LOCALSTORAGE_KEY = "lastSeenVersion";
|
||||
const memoryLS = new LocalStorageWithSchema({
|
||||
key: "lastSeenVersion",
|
||||
schema: z.string(),
|
||||
fallback: "",
|
||||
});
|
||||
|
||||
let version: null | string = null;
|
||||
let isVersionNew: null | boolean = null;
|
||||
|
||||
function setMemory(v: string): void {
|
||||
window.localStorage.setItem(LOCALSTORAGE_KEY, v);
|
||||
memoryLS.set(v);
|
||||
}
|
||||
|
||||
function getMemory(): string {
|
||||
return window.localStorage.getItem(LOCALSTORAGE_KEY) ?? "";
|
||||
return memoryLS.get();
|
||||
}
|
||||
|
||||
async function check(): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,36 @@ import {
|
|||
CustomTextLimitMode,
|
||||
CustomTextMode,
|
||||
} from "@monkeytype/shared-types";
|
||||
import { LocalStorageWithSchema } from "../utils/local-storage-with-schema";
|
||||
import { z } from "zod";
|
||||
|
||||
//zod schema for an object with string keys and string values
|
||||
const CustomTextObjectSchema = z.record(z.string(), z.string());
|
||||
type CustomTextObject = z.infer<typeof CustomTextObjectSchema>;
|
||||
|
||||
const CustomTextLongObjectSchema = z.record(
|
||||
z.string(),
|
||||
z.object({ text: z.string(), progress: z.number() })
|
||||
);
|
||||
type CustomTextLongObject = z.infer<typeof CustomTextLongObjectSchema>;
|
||||
|
||||
const customTextLS = new LocalStorageWithSchema({
|
||||
key: "customText",
|
||||
schema: CustomTextObjectSchema,
|
||||
fallback: {},
|
||||
});
|
||||
//todo maybe add migrations here?
|
||||
const customTextLongLS = new LocalStorageWithSchema({
|
||||
key: "customTextLong",
|
||||
schema: CustomTextLongObjectSchema,
|
||||
fallback: {},
|
||||
});
|
||||
|
||||
// function setLocalStorage(data: CustomTextObject): void {
|
||||
// window.localStorage.setItem("customText", JSON.stringify(data));
|
||||
// }
|
||||
|
||||
// function setLocalStorageLong(data: CustomTextLongObject): void {
|
||||
|
||||
let text: string[] = [
|
||||
"The",
|
||||
|
|
@ -79,10 +109,6 @@ export function getData(): CustomTextData {
|
|||
};
|
||||
}
|
||||
|
||||
type CustomTextObject = Record<string, string>;
|
||||
|
||||
type CustomTextLongObject = Record<string, { text: string; progress: number }>;
|
||||
|
||||
export function getCustomText(name: string, long = false): string[] {
|
||||
if (long) {
|
||||
const customTextLong = getLocalStorageLong();
|
||||
|
|
@ -169,23 +195,19 @@ export function setCustomTextLongProgress(
|
|||
}
|
||||
|
||||
function getLocalStorage(): CustomTextObject {
|
||||
return JSON.parse(
|
||||
window.localStorage.getItem("customText") ?? "{}"
|
||||
) as CustomTextObject;
|
||||
return customTextLS.get();
|
||||
}
|
||||
|
||||
function getLocalStorageLong(): CustomTextLongObject {
|
||||
return JSON.parse(
|
||||
window.localStorage.getItem("customTextLong") ?? "{}"
|
||||
) as CustomTextLongObject;
|
||||
return customTextLongLS.get();
|
||||
}
|
||||
|
||||
function setLocalStorage(data: CustomTextObject): void {
|
||||
window.localStorage.setItem("customText", JSON.stringify(data));
|
||||
customTextLS.set(data);
|
||||
}
|
||||
|
||||
function setLocalStorageLong(data: CustomTextLongObject): void {
|
||||
window.localStorage.setItem("customTextLong", JSON.stringify(data));
|
||||
customTextLongLS.set(data);
|
||||
}
|
||||
|
||||
export function getCustomTextNames(long = false): string[] {
|
||||
|
|
|
|||
11
frontend/src/ts/types/types.d.ts
vendored
11
frontend/src/ts/types/types.d.ts
vendored
|
|
@ -230,7 +230,7 @@ declare namespace MonkeyTypes {
|
|||
inboxUnreadSize: number;
|
||||
streak: number;
|
||||
maxStreak: number;
|
||||
filterPresets: import("@monkeytype/shared-types").ResultFilters[];
|
||||
filterPresets: import("@monkeytype/contracts/schemas/users").ResultFilters[];
|
||||
isPremium: boolean;
|
||||
streakHourOffset?: number;
|
||||
config: import("@monkeytype/contracts/schemas/configs").Config;
|
||||
|
|
@ -244,15 +244,6 @@ declare namespace MonkeyTypes {
|
|||
testActivityByYear?: { [key: string]: TestActivityCalendar };
|
||||
};
|
||||
|
||||
type Group<
|
||||
G extends keyof import("@monkeytype/shared-types").ResultFilters = keyof import("@monkeytype/shared-types").ResultFilters
|
||||
> = G extends G ? import("@monkeytype/shared-types").ResultFilters[G] : never;
|
||||
|
||||
type Filter<G extends Group = Group> =
|
||||
G extends keyof import("@monkeytype/shared-types").ResultFilters
|
||||
? keyof import("@monkeytype/shared-types").ResultFilters[G]
|
||||
: never;
|
||||
|
||||
type TimerStats = {
|
||||
dateNow: number;
|
||||
now: number;
|
||||
|
|
|
|||
66
frontend/src/ts/utils/local-storage-with-schema.ts
Normal file
66
frontend/src/ts/utils/local-storage-with-schema.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { ZodIssue } from "zod";
|
||||
|
||||
export class LocalStorageWithSchema<T> {
|
||||
private key: string;
|
||||
private schema: Zod.Schema<T>;
|
||||
private fallback: T;
|
||||
private migrate?: (value: unknown, zodIssues: ZodIssue[]) => T;
|
||||
|
||||
constructor(options: {
|
||||
key: string;
|
||||
schema: Zod.Schema<T>;
|
||||
fallback: T;
|
||||
migrate?: (value: unknown, zodIssues: ZodIssue[]) => T;
|
||||
}) {
|
||||
this.key = options.key;
|
||||
this.schema = options.schema;
|
||||
this.fallback = options.fallback;
|
||||
this.migrate = options.migrate;
|
||||
}
|
||||
|
||||
public get(): T {
|
||||
const value = window.localStorage.getItem(this.key);
|
||||
|
||||
if (value === null) {
|
||||
return this.fallback;
|
||||
}
|
||||
|
||||
let jsonParsed;
|
||||
try {
|
||||
jsonParsed = JSON.parse(value);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Value from localStorage ${this.key} was not a valid JSON, using fallback`,
|
||||
e
|
||||
);
|
||||
window.localStorage.removeItem(this.key);
|
||||
return this.fallback;
|
||||
}
|
||||
|
||||
const schemaParsed = this.schema.safeParse(jsonParsed);
|
||||
|
||||
if (schemaParsed.success) {
|
||||
return schemaParsed.data;
|
||||
}
|
||||
|
||||
console.error(
|
||||
`Value from localStorage ${this.key} failed schema validation, migrating`,
|
||||
schemaParsed.error
|
||||
);
|
||||
const newValue =
|
||||
this.migrate?.(jsonParsed, schemaParsed.error.issues) ?? this.fallback;
|
||||
window.localStorage.setItem(this.key, JSON.stringify(newValue));
|
||||
return newValue;
|
||||
}
|
||||
|
||||
public set(data: T): boolean {
|
||||
try {
|
||||
const parsed = this.schema.parse(data);
|
||||
window.localStorage.setItem(this.key, JSON.stringify(parsed));
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(`Failed to set ${this.key} in localStorage`, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,18 @@
|
|||
import { z } from "zod";
|
||||
import { LocalStorageWithSchema } from "./local-storage-with-schema";
|
||||
import { isDevEnvironment } from "./misc";
|
||||
|
||||
const nativeLog = console.log;
|
||||
const nativeWarn = console.warn;
|
||||
const nativeError = console.error;
|
||||
|
||||
let debugLogs = localStorage.getItem("debugLogs") === "true";
|
||||
const debugLogsLS = new LocalStorageWithSchema({
|
||||
key: "debugLogs",
|
||||
schema: z.boolean(),
|
||||
fallback: false,
|
||||
});
|
||||
|
||||
let debugLogs = debugLogsLS.get();
|
||||
|
||||
if (isDevEnvironment()) {
|
||||
debugLogs = true;
|
||||
|
|
@ -14,7 +22,7 @@ if (isDevEnvironment()) {
|
|||
export function toggleDebugLogs(): void {
|
||||
debugLogs = !debugLogs;
|
||||
info(`Debug logs ${debugLogs ? "enabled" : "disabled"}`);
|
||||
localStorage.setItem("debugLogs", debugLogs.toString());
|
||||
debugLogsLS.set(debugLogs);
|
||||
}
|
||||
|
||||
function info(...args: unknown[]): void {
|
||||
|
|
|
|||
|
|
@ -676,4 +676,8 @@ export function updateTitle(title?: string): void {
|
|||
}
|
||||
}
|
||||
|
||||
export function isObject(obj: unknown): obj is Record<string, unknown> {
|
||||
return typeof obj === "object" && !Array.isArray(obj) && obj !== null;
|
||||
}
|
||||
|
||||
// DO NOT ALTER GLOBAL OBJECTSONSTRUCTOR, IT WILL BREAK RESULT HASHES
|
||||
|
|
|
|||
|
|
@ -1 +1,62 @@
|
|||
//tbd
|
||||
import { z } from "zod";
|
||||
import { IdSchema } from "./util";
|
||||
import { ModeSchema } from "./shared";
|
||||
|
||||
export const ResultFiltersSchema = z.object({
|
||||
_id: IdSchema,
|
||||
name: z.string(),
|
||||
pb: z.object({
|
||||
no: z.boolean(),
|
||||
yes: z.boolean(),
|
||||
}),
|
||||
difficulty: z.object({
|
||||
normal: z.boolean(),
|
||||
expert: z.boolean(),
|
||||
master: z.boolean(),
|
||||
}),
|
||||
mode: z.record(ModeSchema, z.boolean()),
|
||||
words: z.object({
|
||||
"10": z.boolean(),
|
||||
"25": z.boolean(),
|
||||
"50": z.boolean(),
|
||||
"100": z.boolean(),
|
||||
custom: z.boolean(),
|
||||
}),
|
||||
time: z.object({
|
||||
"15": z.boolean(),
|
||||
"30": z.boolean(),
|
||||
"60": z.boolean(),
|
||||
"120": z.boolean(),
|
||||
custom: z.boolean(),
|
||||
}),
|
||||
quoteLength: z.object({
|
||||
short: z.boolean(),
|
||||
medium: z.boolean(),
|
||||
long: z.boolean(),
|
||||
thicc: z.boolean(),
|
||||
}),
|
||||
punctuation: z.object({
|
||||
on: z.boolean(),
|
||||
off: z.boolean(),
|
||||
}),
|
||||
numbers: z.object({
|
||||
on: z.boolean(),
|
||||
off: z.boolean(),
|
||||
}),
|
||||
date: z.object({
|
||||
last_day: z.boolean(),
|
||||
last_week: z.boolean(),
|
||||
last_month: z.boolean(),
|
||||
last_3months: z.boolean(),
|
||||
all: z.boolean(),
|
||||
}),
|
||||
tags: z.record(z.boolean()),
|
||||
language: z.record(z.boolean()),
|
||||
funbox: z.record(z.boolean()),
|
||||
});
|
||||
export type ResultFilters = z.infer<typeof ResultFiltersSchema>;
|
||||
|
||||
export type ResultFiltersGroup = keyof ResultFilters;
|
||||
|
||||
export type ResultFiltersGroupItem<T extends ResultFiltersGroup> =
|
||||
keyof ResultFilters[T];
|
||||
|
|
|
|||
|
|
@ -240,67 +240,6 @@ export type CustomTextDataWithTextLen = Omit<CustomTextData, "text"> & {
|
|||
textLen: number;
|
||||
};
|
||||
|
||||
export type ResultFilters = {
|
||||
_id: string;
|
||||
name: string;
|
||||
pb: {
|
||||
no: boolean;
|
||||
yes: boolean;
|
||||
};
|
||||
difficulty: {
|
||||
normal: boolean;
|
||||
expert: boolean;
|
||||
master: boolean;
|
||||
};
|
||||
mode: {
|
||||
words: boolean;
|
||||
time: boolean;
|
||||
quote: boolean;
|
||||
zen: boolean;
|
||||
custom: boolean;
|
||||
};
|
||||
words: {
|
||||
"10": boolean;
|
||||
"25": boolean;
|
||||
"50": boolean;
|
||||
"100": boolean;
|
||||
custom: boolean;
|
||||
};
|
||||
time: {
|
||||
"15": boolean;
|
||||
"30": boolean;
|
||||
"60": boolean;
|
||||
"120": boolean;
|
||||
custom: boolean;
|
||||
};
|
||||
quoteLength: {
|
||||
short: boolean;
|
||||
medium: boolean;
|
||||
long: boolean;
|
||||
thicc: boolean;
|
||||
};
|
||||
punctuation: {
|
||||
on: boolean;
|
||||
off: boolean;
|
||||
};
|
||||
numbers: {
|
||||
on: boolean;
|
||||
off: boolean;
|
||||
};
|
||||
date: {
|
||||
last_day: boolean;
|
||||
last_week: boolean;
|
||||
last_month: boolean;
|
||||
last_3months: boolean;
|
||||
all: boolean;
|
||||
};
|
||||
tags: Record<string, boolean>;
|
||||
language: Record<string, boolean>;
|
||||
funbox: {
|
||||
none?: boolean;
|
||||
} & Record<string, boolean>;
|
||||
};
|
||||
|
||||
export type PostResultResponse = {
|
||||
isPb: boolean;
|
||||
tagPbs: string[];
|
||||
|
|
@ -392,7 +331,7 @@ export type User = {
|
|||
verified?: boolean;
|
||||
needsToChangeName?: boolean;
|
||||
quoteMod?: boolean | string;
|
||||
resultFilterPresets?: ResultFilters[];
|
||||
resultFilterPresets?: import("@monkeytype/contracts/schemas/users").ResultFilters[];
|
||||
testActivity?: TestActivity;
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue