Skip to content

Commit 09eb1a2

Browse files
authored
refactor: use validation on email update modal (@fehmer) (monkeytypegame#6272)
1 parent 14d423e commit 09eb1a2

File tree

3 files changed

+36
-34
lines changed

3 files changed

+36
-34
lines changed

frontend/src/ts/modals/simple-modals.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import {
3535
} from "../utils/simple-modal";
3636
import { ShowOptions } from "../utils/animated-modal";
3737
import { GenerateDataRequest } from "@monkeytype/contracts/dev";
38-
import { UserNameSchema } from "@monkeytype/contracts/users";
38+
import { UserEmailSchema, UserNameSchema } from "@monkeytype/contracts/users";
3939
import { goToPage } from "../pages/leaderboards";
4040

4141
type PopupKey =
@@ -230,11 +230,20 @@ list.updateEmail = new SimpleModal({
230230
type: "text",
231231
placeholder: "New email",
232232
initVal: "",
233+
validation: {
234+
schema: UserEmailSchema,
235+
},
233236
},
234237
{
235238
type: "text",
236239
placeholder: "Confirm new email",
237240
initVal: "",
241+
validation: {
242+
schema: UserEmailSchema,
243+
isValid: async (currentValue, thisPopup) =>
244+
currentValue === thisPopup.inputs?.[1]?.currentValue() ||
245+
"Emails don't match",
246+
},
238247
},
239248
],
240249
buttonText: "update",
@@ -262,7 +271,7 @@ list.updateEmail = new SimpleModal({
262271

263272
const response = await Ape.users.updateEmail({
264273
body: {
265-
newEmail: email.trim(),
274+
newEmail: email,
266275
previousEmail: reauth.user.email as string,
267276
},
268277
});

frontend/src/ts/utils/simple-modal.ts

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ type CommonInput<TType, TValue> = {
3131
* Custom async validation method.
3232
* This is intended to be used for validations that cannot be handled with a Zod schema like server-side validations.
3333
* @param value current input value
34+
* @param thisPopup the current modal
3435
* @returns true if the `value` is valid, an errorMessage as string if it is invalid.
3536
*/
36-
isValid?: (value: string) => Promise<true | string>;
37+
isValid?: (value: string, thisPopup: SimpleModal) => Promise<true | string>;
3738
};
3839
};
3940

@@ -88,8 +89,9 @@ export type ExecReturn = {
8889
afterHide?: () => void;
8990
};
9091

91-
type CommonInputTypeWithIndicator = CommonInputType & {
92+
type FormInput = CommonInputType & {
9293
indicator?: InputIndicator;
94+
currentValue: () => string;
9395
};
9496
type SimpleModalOptions = {
9597
id: string;
@@ -114,7 +116,7 @@ export class SimpleModal {
114116
modal: AnimatedModal;
115117
id: string;
116118
title: string;
117-
inputs: CommonInputTypeWithIndicator[];
119+
inputs: FormInput[];
118120
text?: string;
119121
textAllowHtml: boolean;
120122
buttonText: string;
@@ -130,7 +132,7 @@ export class SimpleModal {
130132
this.id = options.id;
131133
this.execFn = options.execFn;
132134
this.title = options.title;
133-
this.inputs = options.inputs ?? [];
135+
this.inputs = (options.inputs as FormInput[]) ?? [];
134136
this.text = options.text;
135137
this.textAllowHtml = options.textAllowHtml ?? false;
136138
this.wrapper = modal.getWrapper();
@@ -313,9 +315,17 @@ export class SimpleModal {
313315
const element = document.querySelector(
314316
"#" + attributes["id"]
315317
) as HTMLInputElement;
318+
316319
if (input.oninput !== undefined) {
317320
element.oninput = input.oninput;
318321
}
322+
323+
input.currentValue = () => {
324+
if (element.type === "checkbox")
325+
return element.checked ? "true" : "false";
326+
return element.value;
327+
};
328+
319329
if (input.validation !== undefined) {
320330
const indicator = new InputIndicator(element, {
321331
valid: {
@@ -335,7 +345,7 @@ export class SimpleModal {
335345
input.indicator = indicator;
336346

337347
const debouceIsValid = debounce(1000, async (value: string) => {
338-
const result = await input.validation?.isValid?.(value);
348+
const result = await input.validation?.isValid?.(value, this);
339349

340350
if (element.value !== value) {
341351
//value of the input has changed in the meantime. discard
@@ -389,43 +399,24 @@ export class SimpleModal {
389399

390400
exec(): void {
391401
if (!this.canClose) return;
392-
const vals: string[] = [];
393-
for (const el of $("#simpleModal input, #simpleModal textarea")) {
394-
if ($(el).is(":checkbox")) {
395-
vals.push($(el).is(":checked") ? "true" : "false");
396-
} else {
397-
vals.push($(el).val() as string);
398-
}
399-
}
400-
401-
type CommonInputWithCurrentValue = CommonInputTypeWithIndicator & {
402-
currentValue: string | undefined;
403-
};
404-
405-
const inputsWithCurrentValue: CommonInputWithCurrentValue[] = [];
406-
for (let i = 0; i < this.inputs.length; i++) {
407-
inputsWithCurrentValue.push({
408-
...(this.inputs[i] as CommonInputTypeWithIndicator),
409-
currentValue: vals[i],
410-
});
411-
}
412402

413403
if (
414-
inputsWithCurrentValue
404+
this.inputs
415405
.filter((i) => i.hidden !== true && i.optional !== true)
416-
.some((v) => v.currentValue === undefined || v.currentValue === "")
406+
.some((v) => v.currentValue() === undefined || v.currentValue() === "")
417407
) {
418408
Notifications.add("Please fill in all fields", 0);
419409
return;
420410
}
421411

422-
if (inputsWithCurrentValue.some((i) => i.indicator?.get() === "invalid")) {
412+
if (this.inputs.some((i) => i.indicator?.get() === "invalid")) {
423413
Notifications.add("Please solve all validation errors", 0);
424414
return;
425415
}
426416

427417
this.disableInputs();
428418
Loader.show();
419+
const vals: string[] = this.inputs.map((it) => it.currentValue());
429420
void this.execFn(this, ...vals).then((res) => {
430421
Loader.hide();
431422
if (res.showNotification ?? true) {

packages/contracts/src/users.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import { IdSchema, LanguageSchema, StringNumberSchema } from "./schemas/util";
3030
import { CustomThemeColorsSchema } from "./schemas/configs";
3131
import { doesNotContainProfanity } from "./validation/validation";
3232

33+
export const UserEmailSchema = z.string().email();
34+
3335
export const GetUserResponseSchema = responseWithData(
3436
UserSchema.extend({
3537
inboxUnreadSize: z.number().int().nonnegative(),
@@ -50,7 +52,7 @@ export const UserNameSchema = doesNotContainProfanity(
5052
);
5153

5254
export const CreateUserRequestSchema = z.object({
53-
email: z.string().email().optional(),
55+
email: UserEmailSchema.optional(),
5456
name: UserNameSchema,
5557
uid: z.string().optional(), //defined by firebase, no validation should be applied
5658
captcha: z.string(), //defined by google recaptcha, no validation should be applied
@@ -80,8 +82,8 @@ export type UpdateLeaderboardMemoryRequest = z.infer<
8082
>;
8183

8284
export const UpdateEmailRequestSchema = z.object({
83-
newEmail: z.string().email(),
84-
previousEmail: z.string().email(),
85+
newEmail: UserEmailSchema,
86+
previousEmail: UserEmailSchema,
8587
});
8688
export type UpdateEmailRequestSchema = z.infer<typeof UpdateEmailRequestSchema>;
8789

@@ -303,7 +305,7 @@ export type ReportUserRequest = z.infer<typeof ReportUserRequestSchema>;
303305

304306
export const ForgotPasswordEmailRequestSchema = z.object({
305307
captcha: z.string(),
306-
email: z.string().email(),
308+
email: UserEmailSchema,
307309
});
308310
export type ForgotPasswordEmailRequest = z.infer<
309311
typeof ForgotPasswordEmailRequestSchema

0 commit comments

Comments
 (0)