Skip to content

Commit 6f13b18

Browse files
fix(profile): prevent saving profile when no changes are made
1 parent 841dbe3 commit 6f13b18

2 files changed

Lines changed: 120 additions & 43 deletions

File tree

frontend/src/styles/inputs.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ input[type="checkbox"] {
114114
position: relative;
115115
transition: background 0.125s;
116116
flex-shrink: 0;
117+
cursor: pointer;
117118
&:after {
118119
font-family: "Font Awesome";
119120
content: "\f00c";

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

Lines changed: 119 additions & 43 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,41 @@ const showActivityOnPublicProfileInput = qsr<HTMLInputElement>(
4248
"#editProfileModal .editProfileShowActivityOnPublicProfile",
4349
);
4450

51+
bioInput.on("input", () => {
52+
updateSaveButtonState();
53+
});
54+
keyboardInput.on("input", () => {
55+
updateSaveButtonState();
56+
});
57+
twitterInput.on("input", () => {
58+
updateSaveButtonState();
59+
});
60+
githubInput.on("input", () => {
61+
updateSaveButtonState();
62+
});
63+
websiteInput.on("input", () => {
64+
updateSaveButtonState();
65+
});
66+
showActivityOnPublicProfileInput.on("change", () => {
67+
updateSaveButtonState();
68+
});
69+
4570
const indicators = [
46-
addValidation(twitterInput, TwitterProfileSchema),
47-
addValidation(githubInput, GithubProfileSchema),
48-
addValidation(websiteInput, WebsiteSchema),
71+
addValidation(
72+
twitterInput,
73+
TwitterProfileSchema,
74+
() => originalState?.twitter ?? "",
75+
),
76+
addValidation(
77+
githubInput,
78+
GithubProfileSchema,
79+
() => originalState?.github ?? "",
80+
),
81+
addValidation(
82+
websiteInput,
83+
WebsiteSchema,
84+
() => originalState?.website ?? "",
85+
),
4986
];
5087

5188
let currentSelectedBadgeId = -1;
@@ -100,67 +137,104 @@ function hydrateInputs(): void {
100137

101138
badgeIdsSelect?.qsa(".badgeSelectionItem")?.removeClass("selected");
102139
(currentTarget as HTMLElement).classList.add("selected");
140+
updateSaveButtonState();
103141
});
104142

105143
indicators.forEach((it) => it.hide());
106144
}
107145

146+
let characterCountersInitialized = false;
147+
108148
function initializeCharacterCounters(): void {
149+
if (characterCountersInitialized) return;
109150
new CharacterCounter(bioInput, 250);
110151
new CharacterCounter(keyboardInput, 75);
152+
characterCountersInitialized = true;
153+
}
154+
155+
type ProfileState = {
156+
bio: string;
157+
keyboard: string;
158+
twitter: string;
159+
github: string;
160+
website: string;
161+
badgeId: number;
162+
showActivityOnPublicProfile: boolean;
163+
};
164+
165+
function getProfileState(): ProfileState {
166+
return {
167+
bio: bioInput.getValue() ?? "",
168+
keyboard: keyboardInput.getValue() ?? "",
169+
twitter: twitterInput.getValue() ?? "",
170+
github: githubInput.getValue() ?? "",
171+
website: websiteInput.getValue() ?? "",
172+
badgeId: currentSelectedBadgeId,
173+
showActivityOnPublicProfile:
174+
showActivityOnPublicProfileInput.isChecked() ?? false,
175+
};
111176
}
112177

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,
178+
function buildUpdatesFromState(state: ProfileState): UserProfileDetails {
179+
return {
180+
bio: state.bio,
181+
keyboard: state.keyboard,
125182
socialProfiles: {
126-
twitter,
127-
github,
128-
website,
183+
twitter: state.twitter,
184+
github: state.github,
185+
website: state.website,
129186
},
130-
showActivityOnPublicProfile,
187+
showActivityOnPublicProfile: state.showActivityOnPublicProfile,
131188
};
189+
}
190+
191+
let originalState: ProfileState | null = null;
192+
193+
function hasProfileChanged(
194+
originalState: ProfileState | null,
195+
currentState: ProfileState,
196+
): boolean {
197+
if (originalState === null) return true;
198+
199+
return (
200+
originalState.bio !== currentState.bio ||
201+
originalState.keyboard !== currentState.keyboard ||
202+
originalState.twitter !== currentState.twitter ||
203+
originalState.github !== currentState.github ||
204+
originalState.website !== currentState.website ||
205+
originalState.badgeId !== currentState.badgeId ||
206+
originalState.showActivityOnPublicProfile !==
207+
currentState.showActivityOnPublicProfile
208+
);
209+
}
210+
211+
function updateSaveButtonState(): void {
212+
const currentState = getProfileState();
213+
const hasChanges = hasProfileChanged(originalState, currentState);
132214

133-
return profileUpdates;
215+
const hasValidationErrors = [
216+
{ value: currentState.twitter, schema: TwitterProfileSchema },
217+
{ value: currentState.github, schema: GithubProfileSchema },
218+
{ value: currentState.website, schema: WebsiteSchema },
219+
].some(
220+
({ value, schema }) => value !== "" && !schema.safeParse(value).success,
221+
);
222+
223+
saveButton.native.disabled = !hasChanges || hasValidationErrors;
134224
}
135225

136226
async function updateProfile(): Promise<void> {
137227
const snapshot = DB.getSnapshot();
138228
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-
}
152229

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-
);
230+
const currentState = getProfileState();
231+
232+
if (!hasProfileChanged(originalState, currentState)) {
233+
updateSaveButtonState();
161234
return;
162235
}
163236

237+
const updates = buildUpdatesFromState(currentState);
164238
showLoaderBar();
165239
const response = await Ape.users.updateProfile({
166240
body: {
@@ -185,7 +259,7 @@ async function updateProfile(): Promise<void> {
185259
});
186260

187261
DB.setSnapshot(snapshot);
188-
262+
originalState = currentState;
189263
showSuccessNotification("Profile updated");
190264

191265
hide();
@@ -194,6 +268,7 @@ async function updateProfile(): Promise<void> {
194268
function addValidation(
195269
element: ElementWithUtils<HTMLInputElement>,
196270
schema: Zod.Schema,
271+
getOriginalValue: () => string,
197272
): InputIndicator {
198273
const indicator = new InputIndicator(element, {
199274
valid: {
@@ -213,10 +288,11 @@ function addValidation(
213288

214289
element.on("input", (event) => {
215290
const value = (event.target as HTMLInputElement).value;
216-
if (value === undefined || value === "") {
291+
if (value === undefined || value === "" || value === getOriginalValue()) {
217292
indicator.hide();
218293
return;
219294
}
295+
220296
const validationResult = schema.safeParse(value);
221297
if (!validationResult.success) {
222298
indicator.show(

0 commit comments

Comments
 (0)