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/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/modals/EditProfileModal.tsx b/frontend/src/ts/components/modals/EditProfileModal.tsx new file mode 100644 index 000000000000..64f487ce72af --- /dev/null +++ b/frontend/src/ts/components/modals/EditProfileModal.tsx @@ -0,0 +1,274 @@ +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 diff --git a/frontend/src/ts/components/modals/SimpleModal.tsx b/frontend/src/ts/components/modals/SimpleModal.tsx index cd53d9a62432..3a07ac6e9c29 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} - skipDirtyCheck={(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 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 6dd15828a218..6eb9a121c8e6 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 "../../modals/EditProfileModal"; 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"); }} /> `; - 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;