From c623967cebf559f1fa1aab809da630f0877181d2 Mon Sep 17 00:00:00 2001 From: Aaron Benmohan John Date: Sun, 3 May 2026 17:32:32 +0530 Subject: [PATCH 01/10] refactor(profile): migrate edit profile modal to SolidJS --- frontend/src/html/popups.html | 75 ----- frontend/src/styles/popups.scss | 61 +--- .../components/pages/profile/UserDetails.tsx | 8 +- .../src/ts/components/popups/EditProfile.tsx | 274 ++++++++++++++++++ .../ts/components/ui/form/TextareaField.tsx | 2 + frontend/src/ts/event-handlers/account.ts | 2 - frontend/src/ts/modals/edit-profile.ts | 241 --------------- frontend/src/ts/states/modals.ts | 3 +- 8 files changed, 285 insertions(+), 381 deletions(-) create mode 100644 frontend/src/ts/components/popups/EditProfile.tsx delete mode 100644 frontend/src/ts/modals/edit-profile.ts diff --git a/frontend/src/html/popups.html b/frontend/src/html/popups.html index 4dd3c50507ed..12bee1f6faf9 100644 --- a/frontend/src/html/popups.html +++ b/frontend/src/html/popups.html @@ -355,78 +355,3 @@
- - diff --git a/frontend/src/styles/popups.scss b/frontend/src/styles/popups.scss index 304c7a6d87e0..e1979360b33a 100644 --- a/frontend/src/styles/popups.scss +++ b/frontend/src/styles/popups.scss @@ -667,63 +667,4 @@ body.darkMode { } } } -} - -#editProfileModal { - .modal { - max-width: 600px; - max-height: 100%; - label { - color: var(--sub-color); - margin-bottom: 0.25em; - display: block; - } - input:not([type="checkbox"]) { - width: 100%; - } - input[type="checkbox"] { - vertical-align: text-bottom; - } - textarea { - resize: vertical; - width: 100%; - padding: 10px; - line-height: 1.2rem; - min-height: 5rem; - max-height: 10rem; - } - - .socialURL { - display: flex; - } - - .socialURL > p { - margin-block: 0.5rem; - margin-inline-end: 0.5rem; - } - - .badgeSelectionContainer { - display: flex; - flex-wrap: wrap; - } - - .badgeSelectionItem { - width: max-content; - opacity: 25%; - cursor: pointer; - margin-right: 0.5rem; - margin-bottom: 0.5rem; - padding: 0; - border-radius: calc(var(--roundness) / 2); - } - - .badgeSelectionItem.selected, - .badgeSelectionItem:hover { - opacity: 100%; - } - - span { - color: var(--text-color); - } - } -} +} \ No newline at end of file diff --git a/frontend/src/ts/components/pages/profile/UserDetails.tsx b/frontend/src/ts/components/pages/profile/UserDetails.tsx index 6dd15828a218..441652fc8dda 100644 --- a/frontend/src/ts/components/pages/profile/UserDetails.tsx +++ b/frontend/src/ts/components/pages/profile/UserDetails.tsx @@ -16,10 +16,10 @@ import { createEffect, createSignal, For, JSXElement, Show } from "solid-js"; import { Snapshot } from "../../../constants/default-snapshot"; import { addFriend, isFriend } from "../../../db"; -import * as EditProfileModal from "../../../modals/edit-profile"; import * as UserReportModal from "../../../modals/user-report"; import { bp } from "../../../states/breakpoints"; import { getUserId, isAuthenticated } from "../../../states/core"; +import { showModal } from "../../../states/modals"; import { showNoticeNotification, showErrorNotification, @@ -36,6 +36,7 @@ import { Button } from "../../common/Button"; import { DiscordAvatar } from "../../common/DiscordAvatar"; import { UserBadge } from "../../common/UserBadge"; import { UserFlags } from "../../common/UserFlags"; +import { EditProfile } from "../../popups/EditProfile"; type Variant = "basic" | "hasSocials" | "hasBioOrKeyboard" | "full"; @@ -98,6 +99,9 @@ export function UserDetails(props: { isAccountPage={props.isAccountPage} /> + + + ); } @@ -177,7 +181,7 @@ function ActionButtons(props: { showNoticeNotification("Banned users cannot edit their profile"); return; } - EditProfileModal.show(); + showModal("EditProfile"); }} /> + + {(badge) => ( + + )} + + + )} + + + +
+ + + {(field) => ( + + )} + +
+ + save + + + ); +} \ No newline at end of file diff --git a/frontend/src/ts/components/ui/form/TextareaField.tsx b/frontend/src/ts/components/ui/form/TextareaField.tsx index 3e67c542b858..ad0537f7e81b 100644 --- a/frontend/src/ts/components/ui/form/TextareaField.tsx +++ b/frontend/src/ts/components/ui/form/TextareaField.tsx @@ -9,6 +9,7 @@ export function TextareaField(props: { placeholder?: string; disabled?: boolean; class?: string; + maxLength?: number; onKeyDown?: (e: KeyboardEvent) => void; onKeyPress?: (e: KeyboardEvent) => void; }): JSXElement { @@ -28,6 +29,7 @@ export function TextareaField(props: { onBlur={() => props.field().handleBlur()} onInput={(e) => props.field().handleChange(e.currentTarget.value)} disabled={props.disabled} + maxLength={props.maxLength} onKeyDown={(e) => props.onKeyDown?.(e)} onKeyPress={(e) => props.onKeyPress?.(e)} > diff --git a/frontend/src/ts/event-handlers/account.ts b/frontend/src/ts/event-handlers/account.ts index e75072fae276..b8a87b7fbc43 100644 --- a/frontend/src/ts/event-handlers/account.ts +++ b/frontend/src/ts/event-handlers/account.ts @@ -1,5 +1,4 @@ import * as PbTablesModal from "../modals/pb-tables"; -import * as EditProfileModal from "../modals/edit-profile"; import { getSnapshot } from "../db"; import { isAuthenticated } from "../states/core"; import { @@ -38,7 +37,6 @@ accountPage?.onChild("click", ".editProfileButton", () => { showNoticeNotification("Banned users cannot edit their profile"); return; } - EditProfileModal.show(); }); const TagsArraySchema = z.array(z.string()); diff --git a/frontend/src/ts/modals/edit-profile.ts b/frontend/src/ts/modals/edit-profile.ts deleted file mode 100644 index fcc31ccafefe..000000000000 --- a/frontend/src/ts/modals/edit-profile.ts +++ /dev/null @@ -1,241 +0,0 @@ -import Ape from "../ape"; -import { getHTMLById } from "../controllers/badge-controller"; -import * as DB from "../db"; - -import { showLoaderBar, hideLoaderBar } from "../states/loader-bar"; -import { - showErrorNotification, - showSuccessNotification, -} from "../states/notifications"; -import AnimatedModal from "../utils/animated-modal"; -import { CharacterCounter } from "../elements/character-counter"; -import { - Badge, - GithubProfileSchema, - TwitterProfileSchema, - UserProfileDetails, - WebsiteSchema, -} from "@monkeytype/schemas/users"; -import { InputIndicator } from "../elements/input-indicator"; -import { ElementWithUtils, qsr } from "../utils/dom"; - -export function show(): void { - void modal.show({ - beforeAnimation: async () => { - hydrateInputs(); - initializeCharacterCounters(); - }, - }); -} - -function hide(): void { - void modal.hide(); -} - -const bioInput = qsr("#editProfileModal .bio"); -const keyboardInput = qsr("#editProfileModal .keyboard"); -const twitterInput = qsr("#editProfileModal .twitter"); -const githubInput = qsr("#editProfileModal .github"); -const websiteInput = qsr("#editProfileModal .website"); -const badgeIdsSelect = qsr("#editProfileModal .badgeSelectionContainer"); -const showActivityOnPublicProfileInput = qsr( - "#editProfileModal .editProfileShowActivityOnPublicProfile", -); - -const indicators = [ - addValidation(twitterInput, TwitterProfileSchema), - addValidation(githubInput, GithubProfileSchema), - addValidation(websiteInput, WebsiteSchema), -]; - -let currentSelectedBadgeId = -1; - -function hydrateInputs(): void { - const snapshot = DB.getSnapshot(); - if (!snapshot) return; - const badges = snapshot.inventory?.badges ?? []; - const { bio, keyboard, socialProfiles, showActivityOnPublicProfile } = - snapshot.details ?? {}; - currentSelectedBadgeId = -1; - - bioInput.setValue(bio ?? ""); - keyboardInput.setValue(keyboard ?? ""); - twitterInput.setValue(socialProfiles?.twitter ?? ""); - githubInput.setValue(socialProfiles?.github ?? ""); - websiteInput.setValue(socialProfiles?.website ?? ""); - badgeIdsSelect.setHtml(""); - showActivityOnPublicProfileInput.native.checked = - showActivityOnPublicProfile ?? false; - - badges?.forEach((badge: Badge) => { - if (badge.selected) { - currentSelectedBadgeId = badge.id; - } - - const badgeOption = getHTMLById(badge.id, false, true); - const badgeWrapper = ``; - badgeIdsSelect?.appendHtml(badgeWrapper); - }); - - badgeIdsSelect?.prependHtml( - ``, - ); - - badgeIdsSelect - ?.qsa(".badgeSelectionItem") - ?.on("click", ({ currentTarget }) => { - const selectionId = (currentTarget as HTMLElement).getAttribute( - "selection-id", - ) as string; - currentSelectedBadgeId = parseInt(selectionId, 10); - - badgeIdsSelect?.qsa(".badgeSelectionItem")?.removeClass("selected"); - (currentTarget as HTMLElement).classList.add("selected"); - }); - - indicators.forEach((it) => it.hide()); -} - -function initializeCharacterCounters(): void { - new CharacterCounter(bioInput, 250); - new CharacterCounter(keyboardInput, 75); -} - -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, - socialProfiles: { - twitter, - github, - website, - }, - showActivityOnPublicProfile, - }; - - return profileUpdates; -} - -async function updateProfile(): Promise { - 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).`, - ); - return; - } - - showLoaderBar(); - const response = await Ape.users.updateProfile({ - body: { - ...updates, - selectedBadgeId: currentSelectedBadgeId, - }, - }); - hideLoaderBar(); - - if (response.status !== 200) { - showErrorNotification("Failed to update profile", { response }); - return; - } - - snapshot.details = response.body.data ?? updates; - snapshot.inventory?.badges.forEach((badge) => { - if (badge.id === currentSelectedBadgeId) { - badge.selected = true; - } else { - delete badge.selected; - } - }); - - DB.setSnapshot(snapshot); - - showSuccessNotification("Profile updated"); - - hide(); -} - -function addValidation( - element: ElementWithUtils, - schema: Zod.Schema, -): InputIndicator { - const indicator = new InputIndicator(element, { - valid: { - icon: "fa-check", - level: 1, - }, - invalid: { - icon: "fa-times", - level: -1, - }, - checking: { - icon: "fa-circle-notch", - spinIcon: true, - level: 0, - }, - }); - - element.on("input", (event) => { - const value = (event.target as HTMLInputElement).value; - if (value === undefined || value === "") { - indicator.hide(); - return; - } - const validationResult = schema.safeParse(value); - if (!validationResult.success) { - indicator.show( - "invalid", - validationResult.error.errors.map((err) => err.message).join(", "), - ); - return; - } - indicator.show("valid"); - }); - return indicator; -} - -const modal = new AnimatedModal({ - dialogId: "editProfileModal", - setup: async (modalEl): Promise => { - modalEl.on("submit", async (e) => { - e.preventDefault(); - await updateProfile(); - }); - }, -}); diff --git a/frontend/src/ts/states/modals.ts b/frontend/src/ts/states/modals.ts index fa2fac25505e..02ff77e9950d 100644 --- a/frontend/src/ts/states/modals.ts +++ b/frontend/src/ts/states/modals.ts @@ -23,7 +23,8 @@ export type ModalId = | "TestDuration" | "ShareTestSettings" | "CustomWordAmount" - | "MobileTestConfig"; + | "MobileTestConfig" + | "EditProfile"; export type ModalVisibility = { visible: boolean; From 26e124cfe2587a4ed2876bf51d79a97813ca913c Mon Sep 17 00:00:00 2001 From: Aaron Benmohan John Date: Thu, 7 May 2026 17:58:17 +0530 Subject: [PATCH 02/10] impr(submit-button): use isDefaultValue instead of isDirty --- .../components/modals/CustomTestDurationModal.tsx | 2 +- .../src/ts/components/modals/CustomTextModal.tsx | 2 +- .../components/modals/CustomWordAmountModal.tsx | 2 +- .../EditProfileModal.tsx} | 12 ++++++------ frontend/src/ts/components/modals/SimpleModal.tsx | 2 +- .../src/ts/components/modals/WordFilterModal.tsx | 4 ++-- .../ts/components/pages/profile/UserDetails.tsx | 2 +- .../src/ts/components/ui/form/SubmitButton.tsx | 15 +++++++++------ 8 files changed, 22 insertions(+), 19 deletions(-) rename frontend/src/ts/components/{popups/EditProfile.tsx => modals/EditProfileModal.tsx} (93%) diff --git a/frontend/src/ts/components/modals/CustomTestDurationModal.tsx b/frontend/src/ts/components/modals/CustomTestDurationModal.tsx index 39156334f7ca..4cc214a2565e 100644 --- a/frontend/src/ts/components/modals/CustomTestDurationModal.tsx +++ b/frontend/src/ts/components/modals/CustomTestDurationModal.tsx @@ -98,7 +98,7 @@ export function CustomTestDurationModal(): JSXElement { form={form} variant="button" text="apply" - skipDirtyCheck + skipUnchangedCheck /> diff --git a/frontend/src/ts/components/modals/CustomTextModal.tsx b/frontend/src/ts/components/modals/CustomTextModal.tsx index ae856a66868d..1e12ed3d9769 100644 --- a/frontend/src/ts/components/modals/CustomTextModal.tsx +++ b/frontend/src/ts/components/modals/CustomTextModal.tsx @@ -491,7 +491,7 @@ export function CustomTextModal(): JSXElement { diff --git a/frontend/src/ts/components/popups/EditProfile.tsx b/frontend/src/ts/components/modals/EditProfileModal.tsx similarity index 93% rename from frontend/src/ts/components/popups/EditProfile.tsx rename to frontend/src/ts/components/modals/EditProfileModal.tsx index 682cc90b820f..64f487ce72af 100644 --- a/frontend/src/ts/components/popups/EditProfile.tsx +++ b/frontend/src/ts/components/modals/EditProfileModal.tsx @@ -118,8 +118,8 @@ export function EditProfile() { > {(field) => ( <> - -
+ +
{field().state.value.length}/250
@@ -137,8 +137,8 @@ export function EditProfile() { > {(field) => ( <> - -
+ +
{field().state.value.length}/75
@@ -150,7 +150,7 @@ export function EditProfile() {

https://github.com/

-
+
twitter

https://x.com/

-
+
diff --git a/frontend/src/ts/components/modals/WordFilterModal.tsx b/frontend/src/ts/components/modals/WordFilterModal.tsx index 010a23071722..c2b26701f610 100644 --- a/frontend/src/ts/components/modals/WordFilterModal.tsx +++ b/frontend/src/ts/components/modals/WordFilterModal.tsx @@ -333,7 +333,7 @@ export function WordFilterModal(props: { variant="button" text="set" class="flex-1" - skipDirtyCheck + skipUnchangedCheck disabled={loading()} onClick={() => (submitAction = "set")} /> @@ -342,7 +342,7 @@ export function WordFilterModal(props: { variant="button" text="add" class="flex-1" - skipDirtyCheck + skipUnchangedCheck disabled={loading()} onClick={() => (submitAction = "add")} /> diff --git a/frontend/src/ts/components/pages/profile/UserDetails.tsx b/frontend/src/ts/components/pages/profile/UserDetails.tsx index 441652fc8dda..6eb9a121c8e6 100644 --- a/frontend/src/ts/components/pages/profile/UserDetails.tsx +++ b/frontend/src/ts/components/pages/profile/UserDetails.tsx @@ -36,7 +36,7 @@ import { Button } from "../../common/Button"; import { DiscordAvatar } from "../../common/DiscordAvatar"; import { UserBadge } from "../../common/UserBadge"; import { UserFlags } from "../../common/UserFlags"; -import { EditProfile } from "../../popups/EditProfile"; +import { EditProfile } from "../../modals/EditProfileModal"; type Variant = "basic" | "hasSocials" | "hasBioOrKeyboard" | "full"; diff --git a/frontend/src/ts/components/ui/form/SubmitButton.tsx b/frontend/src/ts/components/ui/form/SubmitButton.tsx index 1e2f02db111e..7d0ce06486a3 100644 --- a/frontend/src/ts/components/ui/form/SubmitButton.tsx +++ b/frontend/src/ts/components/ui/form/SubmitButton.tsx @@ -6,7 +6,7 @@ type FormStateSlice = { canSubmit: boolean; isSubmitting: boolean; isValid: boolean; - isDirty: boolean; + isDefaultValue: boolean; }; type SubscribableForm = { @@ -15,7 +15,7 @@ type SubscribableForm = { canSubmit: boolean; isSubmitting: boolean; isValid: boolean; - isDirty: boolean; + isDefaultValue: boolean; }) => FormStateSlice; children: (state: () => FormStateSlice) => JSXElement; }) => JSXElement; @@ -24,17 +24,20 @@ type SubscribableForm = { export function SubmitButton( props: { form: SubscribableForm; - skipDirtyCheck?: boolean; + skipUnchangedCheck?: boolean; } & Omit, ): JSXElement { - const [local, others] = splitProps(props, ["disabled", "skipDirtyCheck"]); + const [local, others] = splitProps(props, [ + "disabled", + "skipUnchangedCheck", + ]); return ( ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting, isValid: state.isValid, - isDirty: state.isDirty, + isDefaultValue: state.isDefaultValue, })} children={(state) => ( + ); +} diff --git a/frontend/src/ts/components/modals/CustomTestDurationModal.tsx b/frontend/src/ts/components/modals/CustomTestDurationModal.tsx index 4cc214a2565e..b1f949b8766b 100644 --- a/frontend/src/ts/components/modals/CustomTestDurationModal.tsx +++ b/frontend/src/ts/components/modals/CustomTestDurationModal.tsx @@ -98,7 +98,7 @@ export function CustomTestDurationModal(): JSXElement { form={form} variant="button" text="apply" - skipUnchangedCheck + skipUnchangedCheck /> diff --git a/frontend/src/ts/components/modals/CustomTextModal.tsx b/frontend/src/ts/components/modals/CustomTextModal.tsx index 1e12ed3d9769..eb5e2ded3288 100644 --- a/frontend/src/ts/components/modals/CustomTextModal.tsx +++ b/frontend/src/ts/components/modals/CustomTextModal.tsx @@ -491,7 +491,7 @@ export function CustomTextModal(): JSXElement {
diff --git a/frontend/src/ts/components/modals/EditProfileModal.tsx b/frontend/src/ts/components/modals/EditProfileModal.tsx index 9182e0530eb9..6d0dedc6c124 100644 --- a/frontend/src/ts/components/modals/EditProfileModal.tsx +++ b/frontend/src/ts/components/modals/EditProfileModal.tsx @@ -1,279 +1,256 @@ -import { - GithubProfileSchema, - TwitterProfileSchema, - UserProfileDetailsSchema, - WebsiteSchema, -} from "@monkeytype/schemas/users"; -import { For } from "solid-js"; -import Ape from "../../ape"; -import { getHTMLById } from "../../controllers/badge-controller"; -import * as DB from "../../db"; -import { - showSuccessNotification, - showErrorNotification, -} from "../../states/notifications"; - -import { AnimatedModal } from "../common/AnimatedModal"; -import { hideModal } from "../../states/modals"; -import { Checkbox } from "../ui/form/Checkbox"; -import { InputField } from "../ui/form/InputField"; -import { TextareaField } from "../ui/form/TextareaField"; -import { createForm } from "@tanstack/solid-form"; -import { SubmitButton } from "../ui/form/SubmitButton"; -import { fromSchema } from "../ui/form/utils"; - -export function EditProfile() { - const snapshot = DB.getSnapshot(); - if (!snapshot) return; - - const badges = snapshot.inventory?.badges ?? []; - const form = createForm(() => ({ - defaultValues: { - bio: snapshot.details?.bio ?? "", - keyboard: snapshot.details?.keyboard ?? "", - github: snapshot.details?.socialProfiles?.github ?? "", - twitter: snapshot.details?.socialProfiles?.twitter ?? "", - website: snapshot.details?.socialProfiles?.website ?? "", - showActivityOnPublicProfile: - snapshot.details?.showActivityOnPublicProfile ?? true, - badgeId: badges.find((b) => b.selected)?.id ?? -1, - }, - onSubmit: async ({ value }) => { - const updates = { - bio: value.bio, - keyboard: value.keyboard, - socialProfiles: { - twitter: value.twitter || undefined, - github: value.github || undefined, - website: value.website || undefined, - }, - showActivityOnPublicProfile: value.showActivityOnPublicProfile, - }; - - const response = await Ape.users.updateProfile({ - body: { - ...updates, - selectedBadgeId: value.badgeId, - }, - }); - - if (response.status !== 200) { - showErrorNotification("Failed to update profile", { response }); - return; - } - - snapshot.details = response.body.data ?? updates; - snapshot.inventory?.badges.forEach((badge) => { - if (badge.id === value.badgeId) { - badge.selected = true; - } else { - delete badge.selected; - } - }); - - form.reset(value); - hideModal("EditProfile"); - DB.setSnapshot(snapshot); - showSuccessNotification("Profile updated"); - }, - })); - - return ( - -
{ - e.preventDefault(); - void form.handleSubmit(); - }} - > -
- -
- To update your name, go to Account Settings > Account > Update - account name -
-
- -
- -
- To update your avatar make sure your Discord account is linked, then - go to Account Settings > Account > Discord Integration and - click "Update Avatar" -
-
- -
- - - {(field) => ( - <> - -
- {field().state.value.length}/250 -
- - )} -
-
- -
- - - {(field) => ( - <> - -
- {field().state.value.length}/75 -
- - )} -
-
- -
- -
-

https://github.com/

-
- - {(field) => ( - - )} - -
-
-
- -
- -
-

https://x.com/

-
- - {(field) => ( - -
-
-
- -
- - - {(field) => ( - - )} - -
- -
- - - {(field) => ( -
- - - {(badge) => ( - - )} - -
- )} -
-
- -
- - - {(field) => ( - - )} - -
- - save -
-
- ); -} \ No newline at end of file +import { + GithubProfileSchema, + TwitterProfileSchema, + UserProfileDetailsSchema, + WebsiteSchema, +} from "@monkeytype/schemas/users"; +import { createForm } from "@tanstack/solid-form"; +import { For } from "solid-js"; + +import Ape from "../../ape"; +import * as DB from "../../db"; +import { hideModal } from "../../states/modals"; +import { + showSuccessNotification, + showErrorNotification, +} from "../../states/notifications"; +import { AnimatedModal } from "../common/AnimatedModal"; +import { Checkbox } from "../ui/form/Checkbox"; +import { InputField } from "../ui/form/InputField"; +import { SubmitButton } from "../ui/form/SubmitButton"; +import { TextareaField } from "../ui/form/TextareaField"; +import { fromSchema } from "../ui/form/utils"; +import { BadgeButton } from "../common/BadgeButton"; + +export function EditProfile() { + const snapshot = DB.getSnapshot(); + if (!snapshot) return; + + const badges = snapshot.inventory?.badges ?? []; + const form = createForm(() => ({ + defaultValues: { + bio: snapshot.details?.bio ?? "", + keyboard: snapshot.details?.keyboard ?? "", + github: snapshot.details?.socialProfiles?.github ?? "", + twitter: snapshot.details?.socialProfiles?.twitter ?? "", + website: snapshot.details?.socialProfiles?.website ?? "", + showActivityOnPublicProfile: + snapshot.details?.showActivityOnPublicProfile ?? true, + badgeId: badges.find((b) => b.selected)?.id ?? -1, + }, + onSubmit: async ({ value }) => { + const updates = { + bio: value.bio, + keyboard: value.keyboard, + socialProfiles: { + twitter: value.twitter || undefined, + github: value.github || undefined, + website: value.website || undefined, + }, + showActivityOnPublicProfile: value.showActivityOnPublicProfile, + }; + + const response = await Ape.users.updateProfile({ + body: { + ...updates, + selectedBadgeId: value.badgeId, + }, + }); + + if (response.status !== 200) { + showErrorNotification("Failed to update profile", { response }); + return; + } + + snapshot.details = response.body.data ?? updates; + snapshot.inventory?.badges.forEach((badge) => { + if (badge.id === value.badgeId) { + badge.selected = true; + } else { + delete badge.selected; + } + }); + + form.reset(value); + hideModal("EditProfile"); + DB.setSnapshot(snapshot); + showSuccessNotification("Profile updated"); + }, + })); + + return ( + +
{ + e.preventDefault(); + void form.handleSubmit(); + }} + > +
+ +
+ To update your name, go to Account Settings > Account > Update + account name +
+
+ +
+ +
+ To update your avatar make sure your Discord account is linked, then + go to Account Settings > Account > Discord Integration and + click "Update Avatar" +
+
+ +
+ + + {(field) => ( + <> + +
+ {field().state.value.length}/250 +
+ + )} +
+
+ +
+ + + {(field) => ( + <> + +
+ {field().state.value.length}/75 +
+ + )} +
+
+ +
+ +
+

https://github.com/

+
+ + {(field) => ( + + )} + +
+
+
+ +
+ +
+

https://x.com/

+
+ + {(field) => ( + +
+
+
+ +
+ + + {(field) => ( + + )} + +
+ +
+ + + {(field) => ( +
+ field().handleChange(-1)} + /> + + {(badge) => ( + field().handleChange(badge.id)} + /> + )} + +
+ )} +
+
+ +
+ + + {(field) => ( + + )} + +
+ + save +
+
+ ); +} diff --git a/frontend/src/ts/components/modals/SimpleModal.tsx b/frontend/src/ts/components/modals/SimpleModal.tsx index 3a07ac6e9c29..d40fefa3029e 100644 --- a/frontend/src/ts/components/modals/SimpleModal.tsx +++ b/frontend/src/ts/components/modals/SimpleModal.tsx @@ -291,7 +291,7 @@ export function SimpleModal(): JSXElement { variant="button" class="w-full" text={config()?.buttonText} - skipUnchangedCheck ={(config()?.inputs?.length ?? 0) === 0} + skipUnchangedCheck={(config()?.inputs?.length ?? 0) === 0} /> diff --git a/frontend/src/ts/components/modals/WordFilterModal.tsx b/frontend/src/ts/components/modals/WordFilterModal.tsx index c2b26701f610..2439351ea7a2 100644 --- a/frontend/src/ts/components/modals/WordFilterModal.tsx +++ b/frontend/src/ts/components/modals/WordFilterModal.tsx @@ -333,7 +333,7 @@ export function WordFilterModal(props: { variant="button" text="set" class="flex-1" - skipUnchangedCheck + skipUnchangedCheck disabled={loading()} onClick={() => (submitAction = "set")} /> @@ -342,7 +342,7 @@ export function WordFilterModal(props: { variant="button" text="add" class="flex-1" - skipUnchangedCheck + skipUnchangedCheck disabled={loading()} onClick={() => (submitAction = "add")} /> diff --git a/frontend/src/ts/components/ui/form/SubmitButton.tsx b/frontend/src/ts/components/ui/form/SubmitButton.tsx index 7d0ce06486a3..fee956ca1b2c 100644 --- a/frontend/src/ts/components/ui/form/SubmitButton.tsx +++ b/frontend/src/ts/components/ui/form/SubmitButton.tsx @@ -2,7 +2,7 @@ import { JSXElement, splitProps } from "solid-js"; import { Button, ButtonProps } from "../../common/Button"; -type FormStateSlice = { +export type FormStateSlice = { canSubmit: boolean; isSubmitting: boolean; isValid: boolean; @@ -11,12 +11,7 @@ type FormStateSlice = { type SubscribableForm = { Subscribe: (props: { - selector: (state: { - canSubmit: boolean; - isSubmitting: boolean; - isValid: boolean; - isDefaultValue: boolean; - }) => FormStateSlice; + selector: (state: FormStateSlice) => FormStateSlice; children: (state: () => FormStateSlice) => JSXElement; }) => JSXElement; }; @@ -27,10 +22,7 @@ export function SubmitButton( skipUnchangedCheck?: boolean; } & Omit, ): JSXElement { - const [local, others] = splitProps(props, [ - "disabled", - "skipUnchangedCheck", - ]); + const [local, others] = splitProps(props, ["disabled", "skipUnchangedCheck"]); return ( ({ diff --git a/frontend/src/ts/components/ui/form/TextareaField.tsx b/frontend/src/ts/components/ui/form/TextareaField.tsx index 6e2945d6510d..394069d40645 100644 --- a/frontend/src/ts/components/ui/form/TextareaField.tsx +++ b/frontend/src/ts/components/ui/form/TextareaField.tsx @@ -11,7 +11,6 @@ export function TextareaField(props: { disabled?: boolean; class?: string; maxLength?: number; - showIndicator?: boolean; onKeyDown?: (e: KeyboardEvent) => void; onKeyPress?: (e: KeyboardEvent) => void; }): JSXElement { @@ -23,6 +22,7 @@ export function TextareaField(props: { "col-start-1 row-start-1 w-full resize-y", "rounded border-none bg-sub-alt p-[0.5em] text-em-base leading-[1.25em] caret-main outline-none", "focus-visible:shadow-[0_0_0_0.1rem_var(--bg-color),0_0_0_0.2rem_var(--text-color)]", + props.field().options.validators ? "pr-[1.85em]" : "", props.class, )} id={props.field().name as string} @@ -36,7 +36,7 @@ export function TextareaField(props: { onKeyDown={(e) => props.onKeyDown?.(e)} onKeyPress={(e) => props.onKeyPress?.(e)} > - +
From e08acdf508c7961da50ce38775a80a90e171e386 Mon Sep 17 00:00:00 2001 From: Aaron Benmohan John Date: Tue, 12 May 2026 12:25:32 +0530 Subject: [PATCH 07/10] lint fix --- frontend/src/ts/components/modals/EditProfileModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/ts/components/modals/EditProfileModal.tsx b/frontend/src/ts/components/modals/EditProfileModal.tsx index 6d0dedc6c124..9c670de10d51 100644 --- a/frontend/src/ts/components/modals/EditProfileModal.tsx +++ b/frontend/src/ts/components/modals/EditProfileModal.tsx @@ -15,12 +15,12 @@ import { showErrorNotification, } from "../../states/notifications"; import { AnimatedModal } from "../common/AnimatedModal"; +import { BadgeButton } from "../common/BadgeButton"; import { Checkbox } from "../ui/form/Checkbox"; import { InputField } from "../ui/form/InputField"; import { SubmitButton } from "../ui/form/SubmitButton"; import { TextareaField } from "../ui/form/TextareaField"; import { fromSchema } from "../ui/form/utils"; -import { BadgeButton } from "../common/BadgeButton"; export function EditProfile() { const snapshot = DB.getSnapshot(); From 22b496a94265a41880557a2fe1764dc352fcdbe3 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 12 May 2026 13:21:57 +0200 Subject: [PATCH 08/10] refactor badge selection --- .../src/ts/components/common/BadgeButton.tsx | 38 ------------------- .../ts/components/modals/EditProfileModal.tsx | 34 +++++++++++------ 2 files changed, 23 insertions(+), 49 deletions(-) delete mode 100644 frontend/src/ts/components/common/BadgeButton.tsx diff --git a/frontend/src/ts/components/common/BadgeButton.tsx b/frontend/src/ts/components/common/BadgeButton.tsx deleted file mode 100644 index df134702b4ed..000000000000 --- a/frontend/src/ts/components/common/BadgeButton.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { badges, UserBadge } from "../../controllers/badge-controller"; - -export function BadgeButton(props: { - id: number; - selected: boolean; - onClick: () => void; -}) { - const badge = (): UserBadge | undefined => - props.id !== -1 ? badges[props.id] : undefined; - - return ( - - ); -} diff --git a/frontend/src/ts/components/modals/EditProfileModal.tsx b/frontend/src/ts/components/modals/EditProfileModal.tsx index 9c670de10d51..f49aeec56250 100644 --- a/frontend/src/ts/components/modals/EditProfileModal.tsx +++ b/frontend/src/ts/components/modals/EditProfileModal.tsx @@ -11,11 +11,14 @@ import Ape from "../../ape"; import * as DB from "../../db"; import { hideModal } from "../../states/modals"; import { - showSuccessNotification, showErrorNotification, + showSuccessNotification, } from "../../states/notifications"; +import { cn } from "../../utils/cn"; import { AnimatedModal } from "../common/AnimatedModal"; -import { BadgeButton } from "../common/BadgeButton"; +import { Button } from "../common/Button"; +import { Fa } from "../common/Fa"; +import { UserBadge } from "../common/UserBadge"; import { Checkbox } from "../ui/form/Checkbox"; import { InputField } from "../ui/form/InputField"; import { SubmitButton } from "../ui/form/SubmitButton"; @@ -217,19 +220,28 @@ export function EditProfile() { {(field) => ( -
- + {(badge) => ( - field().handleChange(badge.id)} - /> + > + + )}
From 666c43084cd6bebb7731e1c50b1d7a84a57e7b42 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 12 May 2026 13:33:42 +0200 Subject: [PATCH 09/10] fix roundness on none badge, fix snapshot usage --- .../src/ts/components/modals/EditProfileModal.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/frontend/src/ts/components/modals/EditProfileModal.tsx b/frontend/src/ts/components/modals/EditProfileModal.tsx index f49aeec56250..ba0b5e81dfe9 100644 --- a/frontend/src/ts/components/modals/EditProfileModal.tsx +++ b/frontend/src/ts/components/modals/EditProfileModal.tsx @@ -8,12 +8,12 @@ import { createForm } from "@tanstack/solid-form"; import { For } from "solid-js"; import Ape from "../../ape"; -import * as DB from "../../db"; import { hideModal } from "../../states/modals"; import { showErrorNotification, showSuccessNotification, } from "../../states/notifications"; +import { getSnapshot, setSnapshot } from "../../states/snapshot"; import { cn } from "../../utils/cn"; import { AnimatedModal } from "../common/AnimatedModal"; import { Button } from "../common/Button"; @@ -26,9 +26,10 @@ import { TextareaField } from "../ui/form/TextareaField"; import { fromSchema } from "../ui/form/utils"; export function EditProfile() { - const snapshot = DB.getSnapshot(); - if (!snapshot) return; - + const snapshot = getSnapshot(); + if (snapshot === undefined) { + throw new Error("missing snapshot in EditProfile"); + } const badges = snapshot.inventory?.badges ?? []; const form = createForm(() => ({ defaultValues: { @@ -76,7 +77,7 @@ export function EditProfile() { form.reset(value); hideModal("EditProfile"); - DB.setSnapshot(snapshot); + setSnapshot(snapshot); showSuccessNotification("Profile updated"); }, })); @@ -222,7 +223,7 @@ export function EditProfile() { {(field) => (