Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/src/styles/inputs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ input[type="checkbox"] {
position: relative;
transition: background 0.125s;
flex-shrink: 0;
cursor: pointer;
&:after {
font-family: "Font Awesome";
content: "\f00c";
Expand Down
162 changes: 119 additions & 43 deletions frontend/src/ts/modals/edit-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export function show(): void {
void modal.show({
beforeAnimation: async () => {
hydrateInputs();
originalState = getProfileState();
updateSaveButtonState();
initializeCharacterCounters();
},
});
Expand All @@ -32,6 +34,10 @@ function hide(): void {
void modal.hide();
}

const saveButton = qsr<HTMLButtonElement>(
"#editProfileModal .edit-profile-submit",
);

const bioInput = qsr<HTMLTextAreaElement>("#editProfileModal .bio");
const keyboardInput = qsr<HTMLTextAreaElement>("#editProfileModal .keyboard");
const twitterInput = qsr<HTMLInputElement>("#editProfileModal .twitter");
Expand All @@ -42,10 +48,41 @@ const showActivityOnPublicProfileInput = qsr<HTMLInputElement>(
"#editProfileModal .editProfileShowActivityOnPublicProfile",
);

bioInput.on("input", () => {
updateSaveButtonState();
});
keyboardInput.on("input", () => {
updateSaveButtonState();
});
twitterInput.on("input", () => {
updateSaveButtonState();
});
githubInput.on("input", () => {
updateSaveButtonState();
});
websiteInput.on("input", () => {
updateSaveButtonState();
});
showActivityOnPublicProfileInput.on("change", () => {
updateSaveButtonState();
});

const indicators = [
addValidation(twitterInput, TwitterProfileSchema),
addValidation(githubInput, GithubProfileSchema),
addValidation(websiteInput, WebsiteSchema),
addValidation(
twitterInput,
TwitterProfileSchema,
() => originalState?.twitter ?? "",
),
addValidation(
githubInput,
GithubProfileSchema,
() => originalState?.github ?? "",
),
addValidation(
websiteInput,
WebsiteSchema,
() => originalState?.website ?? "",
),
];

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

badgeIdsSelect?.qsa(".badgeSelectionItem")?.removeClass("selected");
(currentTarget as HTMLElement).classList.add("selected");
updateSaveButtonState();
});

indicators.forEach((it) => it.hide());
}

let characterCountersInitialized = false;

function initializeCharacterCounters(): void {
if (characterCountersInitialized) return;
new CharacterCounter(bioInput, 250);
new CharacterCounter(keyboardInput, 75);
characterCountersInitialized = true;
}

type ProfileState = {
bio: string;
keyboard: string;
twitter: string;
github: string;
website: string;
badgeId: number;
showActivityOnPublicProfile: boolean;
};

function getProfileState(): ProfileState {
return {
bio: bioInput.getValue() ?? "",
keyboard: keyboardInput.getValue() ?? "",
twitter: twitterInput.getValue() ?? "",
github: githubInput.getValue() ?? "",
website: websiteInput.getValue() ?? "",
badgeId: currentSelectedBadgeId,
showActivityOnPublicProfile:
showActivityOnPublicProfileInput.isChecked() ?? false,
};
}

function buildUpdatesFromInputs(): UserProfileDetails {
const bio = bioInput.getValue() ?? "";
const keyboard = keyboardInput.getValue() ?? "";
const twitter = twitterInput.getValue() ?? "";
const github = githubInput.getValue() ?? "";
const website = websiteInput.getValue() ?? "";
const showActivityOnPublicProfile =
showActivityOnPublicProfileInput.isChecked() ?? false;

const profileUpdates: UserProfileDetails = {
bio,
keyboard,
function buildUpdatesFromState(state: ProfileState): UserProfileDetails {
return {
bio: state.bio,
keyboard: state.keyboard,
socialProfiles: {
twitter,
github,
website,
twitter: state.twitter,
github: state.github,
website: state.website,
},
showActivityOnPublicProfile,
showActivityOnPublicProfile: state.showActivityOnPublicProfile,
};
}

let originalState: ProfileState | null = null;

function hasProfileChanged(
originalState: ProfileState | null,
currentState: ProfileState,
): boolean {
if (originalState === null) return true;

return (
originalState.bio !== currentState.bio ||
originalState.keyboard !== currentState.keyboard ||
originalState.twitter !== currentState.twitter ||
originalState.github !== currentState.github ||
originalState.website !== currentState.website ||
originalState.badgeId !== currentState.badgeId ||
originalState.showActivityOnPublicProfile !==
currentState.showActivityOnPublicProfile
);
}

function updateSaveButtonState(): void {
const currentState = getProfileState();
const hasChanges = hasProfileChanged(originalState, currentState);

return profileUpdates;
const hasValidationErrors = [
{ value: currentState.twitter, schema: TwitterProfileSchema },
{ value: currentState.github, schema: GithubProfileSchema },
{ value: currentState.website, schema: WebsiteSchema },
].some(
({ value, schema }) => value !== "" && !schema.safeParse(value).success,
);

saveButton.native.disabled = !hasChanges || hasValidationErrors;
}

async function updateProfile(): Promise<void> {
const snapshot = DB.getSnapshot();
if (!snapshot) return;
const updates = buildUpdatesFromInputs();

// check for length resctrictions before sending server requests
const githubLengthLimit = 39;
if (
updates.socialProfiles?.github !== undefined &&
updates.socialProfiles?.github.length > githubLengthLimit
) {
showErrorNotification(
`GitHub username exceeds maximum allowed length (${githubLengthLimit} characters).`,
);
return;
}

const twitterLengthLimit = 20;
if (
updates.socialProfiles?.twitter !== undefined &&
updates.socialProfiles?.twitter.length > twitterLengthLimit
) {
showErrorNotification(
`Twitter username exceeds maximum allowed length (${twitterLengthLimit} characters).`,
);
const currentState = getProfileState();

if (!hasProfileChanged(originalState, currentState)) {
updateSaveButtonState();
return;
}

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

DB.setSnapshot(snapshot);

originalState = currentState;
showSuccessNotification("Profile updated");

hide();
Expand All @@ -194,6 +268,7 @@ async function updateProfile(): Promise<void> {
function addValidation(
element: ElementWithUtils<HTMLInputElement>,
schema: Zod.Schema,
getOriginalValue: () => string,
): InputIndicator {
const indicator = new InputIndicator(element, {
valid: {
Expand All @@ -213,10 +288,11 @@ function addValidation(

element.on("input", (event) => {
const value = (event.target as HTMLInputElement).value;
if (value === undefined || value === "") {
if (value === undefined || value === "" || value === getOriginalValue()) {
indicator.hide();
return;
}

const validationResult = schema.safeParse(value);
if (!validationResult.success) {
indicator.show(
Expand Down
Loading