Skip to content
6 changes: 4 additions & 2 deletions frontend/src/ts/commandline/lists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ import * as FPSCounter from "../elements/fps-counter";
import { migrateConfig } from "../utils/config";
import { PartialConfigSchema } from "@monkeytype/contracts/schemas/configs";
import { Command, CommandsSubgroup } from "./types";
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";

const layoutsPromise = JSONData.getLayoutsList();
layoutsPromise
Expand Down Expand Up @@ -362,8 +363,9 @@ export const commands: CommandsSubgroup = {
exec: async ({ input }): Promise<void> => {
if (input === undefined || input === "") return;
try {
const parsedConfig = PartialConfigSchema.strip().parse(
JSON.parse(input)
const parsedConfig = parseJsonWithSchema(
input,
PartialConfigSchema.strip()
);
await UpdateConfig.apply(migrateConfig(parsedConfig));
UpdateConfig.saveFullConfigToLocalStorage();
Expand Down
21 changes: 15 additions & 6 deletions frontend/src/ts/controllers/analytics-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ import {
} from "firebase/analytics";
import { app as firebaseApp } from "../firebase";
import { createErrorMessage } from "../utils/misc";
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
import { z } from "zod";

let analytics: AnalyticsType;

type AcceptedCookies = {
security: boolean;
analytics: boolean;
};
const acceptedCookiesSchema = z.object({
security: z.boolean(),
analytics: z.boolean(),
});

type AcceptedCookies = z.infer<typeof acceptedCookiesSchema>; // Updated type inference

export async function log(
eventName: string,
Expand All @@ -26,9 +30,14 @@ export async function log(
}

const lsString = localStorage.getItem("acceptedCookies");
let acceptedCookies;
let acceptedCookies: AcceptedCookies | null;
if (lsString !== undefined && lsString !== null && lsString !== "") {
acceptedCookies = JSON.parse(lsString) as AcceptedCookies;
try {
acceptedCookies = parseJsonWithSchema(lsString, acceptedCookiesSchema);
} catch (e) {
console.error("Failed to parse accepted cookies:", e);
acceptedCookies = null;
}
} else {
acceptedCookies = null;
}
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/ts/event-handlers/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { isAuthenticated } from "../firebase";
import * as Notifications from "../elements/notifications";
import * as EditResultTagsModal from "../modals/edit-result-tags";
import * as AddFilterPresetModal from "../modals/new-filter-preset";
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
import { z } from "zod";

const accountPage = document.querySelector("#pageAccount") as HTMLElement;

Expand Down Expand Up @@ -36,12 +38,15 @@ $(accountPage).on("click", ".editProfileButton", () => {
EditProfileModal.show();
});

const tagsArraySchema = z.array(z.string());

$(accountPage).on("click", ".group.history .resultEditTagsButton", (e) => {
const resultid = $(e.target).attr("data-result-id");
const tags = $(e.target).attr("data-tags");

EditResultTagsModal.show(
resultid ?? "",
JSON.parse(tags ?? "[]") as string[],
parseJsonWithSchema(tags ?? "[]", tagsArraySchema),
"accountPage"
);
});
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/ts/modals/import-export-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as UpdateConfig from "../config";
import * as Notifications from "../elements/notifications";
import AnimatedModal from "../utils/animated-modal";
import { migrateConfig } from "../utils/config";
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";

type State = {
mode: "import" | "export";
Expand Down Expand Up @@ -47,8 +48,9 @@ const modal = new AnimatedModal({
return;
}
try {
const parsedConfig = PartialConfigSchema.strip().parse(
JSON.parse(state.value)
const parsedConfig = parseJsonWithSchema(
state.value,
PartialConfigSchema.strip()
);
await UpdateConfig.apply(migrateConfig(parsedConfig));
} catch (e) {
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/ts/test/custom-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ const customTextSettings = new LocalStorageWithSchema({
},
});

// reassigned to CustomTextSettingsSchema
export const CustomTextDataSchema = CustomTextSettingsSchema;

export function getText(): string[] {
return customTextSettings.get().text;
}
Expand Down
29 changes: 25 additions & 4 deletions frontend/src/ts/test/wikipedia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import * as Loader from "../elements/loader";
import * as Misc from "../utils/misc";
import * as Strings from "../utils/strings";
import * as JSONData from "../utils/json-data";
import { z } from "zod";
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";

export async function getTLD(
languageGroup: JSONData.LanguageGroup
Expand Down Expand Up @@ -241,6 +243,18 @@ type SectionObject = {
author: string;
};

// Section Schema
const sectionSchema = z.object({
query: z.object({
pages: z.record(
z.string(),
z.object({
extract: z.string(),
})
),
}),
});

export async function getSection(language: string): Promise<JSONData.Section> {
// console.log("Getting section");
Loader.show();
Expand Down Expand Up @@ -285,10 +299,17 @@ export async function getSection(language: string): Promise<JSONData.Section> {
sectionReq.onload = (): void => {
if (sectionReq.readyState === 4) {
if (sectionReq.status === 200) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
let sectionText = JSON.parse(sectionReq.responseText).query.pages[
pageid.toString()
].extract as string;
const parsedResponse = parseJsonWithSchema(
sectionReq.responseText,
sectionSchema
);
const page = parsedResponse.query.pages[pageid.toString()];
if (!page) {
Loader.hide();
rej("Page not found");
return;
}
let sectionText = page.extract;

// Converting to one paragraph
sectionText = sectionText.replace(/<\/p><p>+/g, " ");
Expand Down
51 changes: 36 additions & 15 deletions frontend/src/ts/utils/url-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@ import * as Loader from "../elements/loader";
import * as AccountButton from "../elements/account-button";
import { restart as restartTest } from "../test/test-logic";
import * as ChallengeController from "../controllers/challenge-controller";
import { Mode, Mode2 } from "@monkeytype/contracts/schemas/shared";
import {
DifficultySchema,
Mode2Schema,
ModeSchema,
} from "@monkeytype/contracts/schemas/shared";
import {
CustomBackgroundFilter,
CustomBackgroundFilterSchema,
CustomBackgroundSize,
CustomBackgroundSizeSchema,
CustomThemeColors,
CustomThemeColorsSchema,
Difficulty,
} from "@monkeytype/contracts/schemas/configs";
import { z } from "zod";
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
Expand Down Expand Up @@ -129,24 +132,42 @@ export function loadCustomThemeFromUrl(getOverride?: string): void {
}
}

type SharedTestSettings = [
Mode | null,
Mode2<Mode> | null,
CustomText.CustomTextData | null,
boolean | null,
boolean | null,
string | null,
Difficulty | null,
string | null
];
const testSettingsSchema = z.tuple([
ModeSchema.nullable(), // Replaces z.union([ModeSchema, z.null()])
Mode2Schema.nullable(),
CustomText.CustomTextDataSchema.nullable(),
z.boolean().nullable(),
z.boolean().nullable(),
z.string().nullable(),
DifficultySchema.nullable(),
z.string().nullable(),
]);

type SharedTestSettings = z.infer<typeof testSettingsSchema>;

export function loadTestSettingsFromUrl(getOverride?: string): void {
const getValue = Misc.findGetParameter("testSettings", getOverride);
if (getValue === null) return;

const de = JSON.parse(
decompressFromURI(getValue) ?? ""
) as SharedTestSettings;
let de: SharedTestSettings;
try {
const decompressed = decompressFromURI(getValue) ?? "";
const parsed = parseJsonWithSchema(decompressed, testSettingsSchema);

// Ensure the second element (index 1) is a string or null
if (parsed[1] !== null && typeof parsed[1] === "number") {
parsed[1] = parsed[1].toString(); // Convert number to string
}

de = parsed as SharedTestSettings; // Assign after refinement
} catch (e) {
console.error("Failed to parse test settings:", e);
Notifications.add(
"Failed to load test settings from URL: " + (e as Error).message,
0
);
return;
}

const applied: Record<string, string> = {};

Expand Down
Loading