Skip to content

Commit f1f4d17

Browse files
authored
impr(profile): validate inputs for twitter, github and website (@fehmer) (#6544)
fixes #6543
1 parent 8370de1 commit f1f4d17

File tree

2 files changed

+76
-16
lines changed

2 files changed

+76
-16
lines changed

frontend/src/ts/modals/edit-profile.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@ import * as ConnectionState from "../states/connection";
77
import AnimatedModal from "../utils/animated-modal";
88
import * as Profile from "../elements/profile";
99
import { CharacterCounter } from "../elements/character-counter";
10-
import { Badge, UserProfileDetails } from "@monkeytype/contracts/schemas/users";
10+
import {
11+
Badge,
12+
GithubProfileSchema,
13+
TwitterProfileSchema,
14+
UserProfileDetails,
15+
WebsiteSchema,
16+
} from "@monkeytype/contracts/schemas/users";
17+
import { InputIndicator } from "../elements/input-indicator";
1118

1219
export function show(): void {
1320
if (!ConnectionState.get()) {
@@ -44,6 +51,12 @@ const githubInput = $("#editProfileModal .github");
4451
const websiteInput = $("#editProfileModal .website");
4552
const badgeIdsSelect = $("#editProfileModal .badgeSelectionContainer");
4653

54+
const indicators = [
55+
addValidation(twitterInput, TwitterProfileSchema),
56+
addValidation(githubInput, GithubProfileSchema),
57+
addValidation(websiteInput, WebsiteSchema),
58+
];
59+
4760
let currentSelectedBadgeId = -1;
4861

4962
function hydrateInputs(): void {
@@ -90,6 +103,8 @@ function hydrateInputs(): void {
90103
badgeIdsSelect.find(".badgeSelectionItem").removeClass("selected");
91104
$(currentTarget).addClass("selected");
92105
});
106+
107+
indicators.forEach((it) => it.hide());
93108
}
94109

95110
function initializeCharacterCounters(): void {
@@ -175,6 +190,45 @@ async function updateProfile(): Promise<void> {
175190
hide();
176191
}
177192

193+
function addValidation(
194+
element: JQuery<HTMLElement>,
195+
schema: Zod.Schema
196+
): InputIndicator {
197+
const indicator = new InputIndicator(element, {
198+
valid: {
199+
icon: "fa-check",
200+
level: 1,
201+
},
202+
invalid: {
203+
icon: "fa-times",
204+
level: -1,
205+
},
206+
checking: {
207+
icon: "fa-circle-notch",
208+
spinIcon: true,
209+
level: 0,
210+
},
211+
});
212+
213+
element.on("input", (event) => {
214+
const value = (event.target as HTMLInputElement).value;
215+
if (value === undefined || value === "") {
216+
indicator.hide();
217+
return;
218+
}
219+
const validationResult = schema.safeParse(value);
220+
if (!validationResult.success) {
221+
indicator.show(
222+
"invalid",
223+
validationResult.error.errors.map((err) => err.message).join(", ")
224+
);
225+
return;
226+
}
227+
indicator.show("valid");
228+
});
229+
return indicator;
230+
}
231+
178232
const modal = new AnimatedModal({
179233
dialogId: "editProfileModal",
180234
setup: async (modalEl): Promise<void> => {

packages/contracts/src/schemas/users.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -87,27 +87,33 @@ function profileDetailsBase(
8787
.transform((value) => (value === null ? undefined : value));
8888
}
8989

90+
export const TwitterProfileSchema = profileDetailsBase(
91+
z
92+
.string()
93+
.max(20)
94+
.regex(/^[0-9a-zA-Z_.-]+$/)
95+
).or(z.literal(""));
96+
97+
export const GithubProfileSchema = profileDetailsBase(
98+
z
99+
.string()
100+
.max(39)
101+
.regex(/^[0-9a-zA-Z_.-]+$/)
102+
).or(z.literal(""));
103+
104+
export const WebsiteSchema = profileDetailsBase(
105+
z.string().url().max(200).startsWith("https://")
106+
).or(z.literal(""));
107+
90108
export const UserProfileDetailsSchema = z
91109
.object({
92110
bio: profileDetailsBase(z.string().max(250)).or(z.literal("")),
93111
keyboard: profileDetailsBase(z.string().max(75)).or(z.literal("")),
94112
socialProfiles: z
95113
.object({
96-
twitter: profileDetailsBase(
97-
z
98-
.string()
99-
.max(20)
100-
.regex(/^[0-9a-zA-Z_.-]+$/)
101-
).or(z.literal("")),
102-
github: profileDetailsBase(
103-
z
104-
.string()
105-
.max(39)
106-
.regex(/^[0-9a-zA-Z_.-]+$/)
107-
).or(z.literal("")),
108-
website: profileDetailsBase(
109-
z.string().url().max(200).startsWith("https://")
110-
).or(z.literal("")),
114+
twitter: TwitterProfileSchema,
115+
github: GithubProfileSchema,
116+
website: WebsiteSchema,
111117
})
112118
.strict()
113119
.optional(),

0 commit comments

Comments
 (0)