Skip to content

Commit 9c2e4e9

Browse files
fix(profile): prevent saving profile when no changes are made
1 parent 82bf095 commit 9c2e4e9

1 file changed

Lines changed: 100 additions & 45 deletions

File tree

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

Lines changed: 100 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export function show(): void {
2323
void modal.show({
2424
beforeAnimation: async () => {
2525
hydrateInputs();
26+
originalState = getProfileState();
27+
updateSaveButtonState();
2628
initializeCharacterCounters();
2729
},
2830
});
@@ -32,6 +34,10 @@ function hide(): void {
3234
void modal.hide();
3335
}
3436

37+
const saveButton = qsr<HTMLButtonElement>(
38+
"#editProfileModal .edit-profile-submit",
39+
);
40+
3541
const bioInput = qsr<HTMLTextAreaElement>("#editProfileModal .bio");
3642
const keyboardInput = qsr<HTMLTextAreaElement>("#editProfileModal .keyboard");
3743
const twitterInput = qsr<HTMLInputElement>("#editProfileModal .twitter");
@@ -42,10 +48,28 @@ const showActivityOnPublicProfileInput = qsr<HTMLInputElement>(
4248
"#editProfileModal .editProfileShowActivityOnPublicProfile",
4349
);
4450

51+
const inputs = [
52+
bioInput,
53+
keyboardInput,
54+
twitterInput,
55+
githubInput,
56+
websiteInput,
57+
];
58+
59+
inputs.forEach((input) => {
60+
input.on("input", updateSaveButtonState);
61+
});
62+
63+
showActivityOnPublicProfileInput.on("change", updateSaveButtonState);
64+
4565
const indicators = [
46-
addValidation(twitterInput, TwitterProfileSchema),
47-
addValidation(githubInput, GithubProfileSchema),
48-
addValidation(websiteInput, WebsiteSchema),
66+
addValidation(
67+
twitterInput,
68+
TwitterProfileSchema,
69+
() => originalState.twitter,
70+
),
71+
addValidation(githubInput, GithubProfileSchema, () => originalState.github),
72+
addValidation(websiteInput, WebsiteSchema, () => originalState.website),
4973
];
5074

5175
let currentSelectedBadgeId = -1;
@@ -100,66 +124,96 @@ function hydrateInputs(): void {
100124

101125
badgeIdsSelect?.qsa(".badgeSelectionItem")?.removeClass("selected");
102126
(currentTarget as HTMLElement).classList.add("selected");
127+
updateSaveButtonState();
103128
});
104129

105130
indicators.forEach((it) => it.hide());
106131
}
107132

133+
let characterCountersInitialized = false;
134+
108135
function initializeCharacterCounters(): void {
136+
if (characterCountersInitialized) return;
109137
new CharacterCounter(bioInput, 250);
110138
new CharacterCounter(keyboardInput, 75);
139+
characterCountersInitialized = true;
140+
}
141+
142+
type ProfileState = {
143+
bio: string;
144+
keyboard: string;
145+
twitter: string;
146+
github: string;
147+
website: string;
148+
badgeId: number;
149+
showActivityOnPublicProfile: boolean;
150+
};
151+
152+
function getProfileState(): ProfileState {
153+
return {
154+
bio: bioInput.getValue() ?? "",
155+
keyboard: keyboardInput.getValue() ?? "",
156+
twitter: twitterInput.getValue() ?? "",
157+
github: githubInput.getValue() ?? "",
158+
website: websiteInput.getValue() ?? "",
159+
badgeId: currentSelectedBadgeId,
160+
showActivityOnPublicProfile:
161+
showActivityOnPublicProfileInput.isChecked() ?? false,
162+
};
111163
}
112164

113-
function buildUpdatesFromInputs(): UserProfileDetails {
114-
const bio = bioInput.getValue() ?? "";
115-
const keyboard = keyboardInput.getValue() ?? "";
116-
const twitter = twitterInput.getValue() ?? "";
117-
const github = githubInput.getValue() ?? "";
118-
const website = websiteInput.getValue() ?? "";
119-
const showActivityOnPublicProfile =
120-
showActivityOnPublicProfileInput.isChecked() ?? false;
121-
122-
const profileUpdates: UserProfileDetails = {
123-
bio,
124-
keyboard,
165+
function buildUpdatesFromState(state: ProfileState): UserProfileDetails {
166+
return {
167+
bio: state.bio,
168+
keyboard: state.keyboard,
125169
socialProfiles: {
126-
twitter,
127-
github,
128-
website,
170+
twitter: state.twitter,
171+
github: state.github,
172+
website: state.website,
129173
},
130-
showActivityOnPublicProfile,
174+
showActivityOnPublicProfile: state.showActivityOnPublicProfile,
131175
};
176+
}
132177

133-
return profileUpdates;
178+
let originalState: ProfileState;
179+
180+
function hasProfileChanged(
181+
originalProfile: ProfileState,
182+
currentProfile: ProfileState,
183+
): boolean {
184+
return (
185+
originalProfile.bio !== currentProfile.bio ||
186+
originalProfile.keyboard !== currentProfile.keyboard ||
187+
originalProfile.twitter !== currentProfile.twitter ||
188+
originalProfile.github !== currentProfile.github ||
189+
originalProfile.website !== currentProfile.website ||
190+
originalProfile.badgeId !== currentProfile.badgeId ||
191+
originalProfile.showActivityOnPublicProfile !==
192+
currentProfile.showActivityOnPublicProfile
193+
);
194+
}
195+
196+
function updateSaveButtonState(): void {
197+
const currentState = getProfileState();
198+
const hasChanges = hasProfileChanged(originalState, currentState);
199+
200+
const hasValidationErrors = [
201+
{ value: currentState.twitter, schema: TwitterProfileSchema },
202+
{ value: currentState.github, schema: GithubProfileSchema },
203+
{ value: currentState.website, schema: WebsiteSchema },
204+
].some(
205+
({ value, schema }) => value !== "" && !schema.safeParse(value).success,
206+
);
207+
208+
saveButton.native.disabled = !hasChanges || hasValidationErrors;
134209
}
135210

136211
async function updateProfile(): Promise<void> {
137212
const snapshot = DB.getSnapshot();
138213
if (!snapshot) return;
139-
const updates = buildUpdatesFromInputs();
140-
141-
// check for length resctrictions before sending server requests
142-
const githubLengthLimit = 39;
143-
if (
144-
updates.socialProfiles?.github !== undefined &&
145-
updates.socialProfiles?.github.length > githubLengthLimit
146-
) {
147-
showErrorNotification(
148-
`GitHub username exceeds maximum allowed length (${githubLengthLimit} characters).`,
149-
);
150-
return;
151-
}
152214

153-
const twitterLengthLimit = 20;
154-
if (
155-
updates.socialProfiles?.twitter !== undefined &&
156-
updates.socialProfiles?.twitter.length > twitterLengthLimit
157-
) {
158-
showErrorNotification(
159-
`Twitter username exceeds maximum allowed length (${twitterLengthLimit} characters).`,
160-
);
161-
return;
162-
}
215+
const currentState = getProfileState();
216+
const updates = buildUpdatesFromState(currentState);
163217

164218
showLoaderBar();
165219
const response = await Ape.users.updateProfile({
@@ -185,7 +239,7 @@ async function updateProfile(): Promise<void> {
185239
});
186240

187241
DB.setSnapshot(snapshot);
188-
242+
originalState = currentState;
189243
showSuccessNotification("Profile updated");
190244

191245
hide();
@@ -194,6 +248,7 @@ async function updateProfile(): Promise<void> {
194248
function addValidation(
195249
element: ElementWithUtils<HTMLInputElement>,
196250
schema: Zod.Schema,
251+
getOriginalValue: () => string,
197252
): InputIndicator {
198253
const indicator = new InputIndicator(element, {
199254
valid: {
@@ -213,7 +268,7 @@ function addValidation(
213268

214269
element.on("input", (event) => {
215270
const value = (event.target as HTMLInputElement).value;
216-
if (value === undefined || value === "") {
271+
if (value === undefined || value === "" || value === getOriginalValue()) {
217272
indicator.hide();
218273
return;
219274
}

0 commit comments

Comments
 (0)