From b74f4adc976b0920e8355cf55e54295f78bd5e11 Mon Sep 17 00:00:00 2001 From: swh00tw Date: Wed, 6 Nov 2024 21:04:08 -0500 Subject: [PATCH 01/14] refactor: move component to dedicated folder and refactor interface --- apps/recnet/src/app/[handle]/Profile.tsx | 21 +++++++++++++++---- .../setting}/UserSettingDialog.tsx | 15 ++++++------- 2 files changed, 25 insertions(+), 11 deletions(-) rename apps/recnet/src/{app/[handle] => components/setting}/UserSettingDialog.tsx (98%) diff --git a/apps/recnet/src/app/[handle]/Profile.tsx b/apps/recnet/src/app/[handle]/Profile.tsx index f33d8b93..185bbf51 100644 --- a/apps/recnet/src/app/[handle]/Profile.tsx +++ b/apps/recnet/src/app/[handle]/Profile.tsx @@ -11,11 +11,10 @@ import { Avatar } from "@recnet/recnet-web/components/Avatar"; import { FollowButton } from "@recnet/recnet-web/components/FollowButton"; import { RecNetLink } from "@recnet/recnet-web/components/Link"; import { Skeleton, SkeletonText } from "@recnet/recnet-web/components/Skeleton"; +import { UserSettingDialog } from "@recnet/recnet-web/components/setting/UserSettingDialog"; import { cn } from "@recnet/recnet-web/utils/cn"; import { interleaveWithValue } from "@recnet/recnet-web/utils/interleaveWithValue"; -import { UserSettingDialog } from "./UserSettingDialog"; - function StatDivider() { return
; } @@ -184,7 +183,14 @@ export function Profile(props: { handle: string }) { {isMe ? ( - + + Settings + + } + /> ) : ( )} @@ -196,7 +202,14 @@ export function Profile(props: { handle: string }) {
{userInfo}
{isMe ? ( - + + Settings + + } + /> ) : ( )} diff --git a/apps/recnet/src/app/[handle]/UserSettingDialog.tsx b/apps/recnet/src/components/setting/UserSettingDialog.tsx similarity index 98% rename from apps/recnet/src/app/[handle]/UserSettingDialog.tsx rename to apps/recnet/src/components/setting/UserSettingDialog.tsx index 06e00caf..5ac7782f 100644 --- a/apps/recnet/src/app/[handle]/UserSettingDialog.tsx +++ b/apps/recnet/src/components/setting/UserSettingDialog.tsx @@ -378,8 +378,13 @@ const tabs = { } as const; type TabKey = keyof typeof tabs; -export function UserSettingDialog(props: { handle: string }) { - const { handle } = props; +interface UserSettingDialogProps { + handle: string; + trigger: React.ReactNode; +} + +export function UserSettingDialog(props: UserSettingDialogProps) { + const { handle, trigger } = props; const utils = trpc.useUtils(); const router = useRouter(); const [open, setOpen] = useState(false); @@ -415,11 +420,7 @@ export function UserSettingDialog(props: { handle: string }) { return ( - - - + {trigger} Date: Wed, 6 Nov 2024 21:06:21 -0500 Subject: [PATCH 02/14] chore: remove redundant button --- apps/recnet/src/components/setting/UserSettingDialog.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/recnet/src/components/setting/UserSettingDialog.tsx b/apps/recnet/src/components/setting/UserSettingDialog.tsx index 5ac7782f..1e02d03b 100644 --- a/apps/recnet/src/components/setting/UserSettingDialog.tsx +++ b/apps/recnet/src/components/setting/UserSettingDialog.tsx @@ -301,11 +301,6 @@ function EditProfileForm(props: TabProps) { - - - - -
-
- {Object.entries(tabs).map(([key, { label }]) => ( -
setActiveTab(key as TabKey)} - > - {tabs[key as TabKey].icon} - + + {trigger} + + + + +
+
+ {Object.entries(tabs).map(([key, { label }]) => ( +
setActiveTab(key as TabKey)} > - {label} - -
- ))} -
-
- + {tabs[key as TabKey].icon} + + {label} + +
+ ))} +
+
+ +
-
- - + + + ); } From 9e6d207de82bca10693336924b03d70068614bb2 Mon Sep 17 00:00:00 2001 From: swh00tw Date: Wed, 6 Nov 2024 21:38:19 -0500 Subject: [PATCH 04/14] refactor: move ProfileEditForm to dedicated file --- .../components/setting/UserSettingDialog.tsx | 314 +---------------- .../setting/profile/ProfileEditForm.tsx | 319 ++++++++++++++++++ 2 files changed, 322 insertions(+), 311 deletions(-) create mode 100644 apps/recnet/src/components/setting/profile/ProfileEditForm.tsx diff --git a/apps/recnet/src/components/setting/UserSettingDialog.tsx b/apps/recnet/src/components/setting/UserSettingDialog.tsx index ea666f63..884bb4c1 100644 --- a/apps/recnet/src/components/setting/UserSettingDialog.tsx +++ b/apps/recnet/src/components/setting/UserSettingDialog.tsx @@ -1,325 +1,17 @@ "use client"; -import { zodResolver } from "@hookform/resolvers/zod"; import { PersonIcon, Cross1Icon } from "@radix-ui/react-icons"; -import { - Dialog, - Button, - Flex, - Text, - TextField, - TextArea, -} from "@radix-ui/themes"; -import { TRPCClientError } from "@trpc/client"; +import { Dialog, Button, Text } from "@radix-ui/themes"; import { Settings } from "lucide-react"; import { useRouter } from "next/navigation"; import React, { useMemo, useState, createContext, useContext } from "react"; -import { useForm, useFormState } from "react-hook-form"; -import { toast } from "sonner"; -import * as z from "zod"; -import { useAuth } from "@recnet/recnet-web/app/AuthContext"; import { trpc } from "@recnet/recnet-web/app/_trpc/client"; import { DoubleConfirmButton } from "@recnet/recnet-web/components/DoubleConfirmButton"; -import { RecNetLink } from "@recnet/recnet-web/components/Link"; -import { ErrorMessages } from "@recnet/recnet-web/constant"; import { logout } from "@recnet/recnet-web/firebase/auth"; import { cn } from "@recnet/recnet-web/utils/cn"; -const HandleBlacklist = [ - "about", - "api", - "all-users", - "feeds", - "help", - "onboard", - "search", - "user", -]; - -const EditUserProfileSchema = z.object({ - displayName: z.string().min(1, "Name cannot be blank."), - handle: z - .string() - .min(4) - .max(15) - .regex( - /^[A-Za-z0-9_]+$/, - "User handle should be between 4 to 15 characters and contain only letters (A-Z, a-z), numbers, and underscores (_)." - ) - .refine( - (name) => { - return !HandleBlacklist.includes(name); - }, - { - message: "User handle is not allowed.", - } - ), - affiliation: z - .string() - .max(64, "Affiliation must contain at most 64 character(s)") - .nullable(), - bio: z - .string() - .max(200, "Bio must contain at most 200 character(s)") - .nullable(), - url: z.string().url().nullable(), - googleScholarLink: z.string().url().nullable(), - semanticScholarLink: z.string().url().nullable(), - openReviewUserName: z.string().nullable(), -}); - -function EditProfileForm() { - const utils = trpc.useUtils(); - const router = useRouter(); - const { setOpen, userHandle } = useUserSettingDialogContext(); - const { user, revalidateUser } = useAuth(); - const oldHandle = user?.handle; - - const { register, handleSubmit, formState, setError, control, watch } = - useForm({ - resolver: zodResolver(EditUserProfileSchema), - defaultValues: { - displayName: user?.displayName ?? null, - handle: user?.handle ?? null, - affiliation: user?.affiliation ?? null, - bio: user?.bio ?? null, - url: user?.url ?? null, - googleScholarLink: user?.googleScholarLink ?? null, - semanticScholarLink: user?.semanticScholarLink ?? null, - openReviewUserName: user?.openReviewUserName ?? null, - }, - mode: "onTouched", - }); - const { isDirty } = useFormState({ control: control }); - - const updateProfileMutation = trpc.updateUser.useMutation(); - - return ( -
{ - e?.preventDefault(); - const res = EditUserProfileSchema.safeParse(data); - if (!res.success || !user?.id) { - // should not happen, just in case and for typescript to narrow down type - console.error("Invalid form data."); - return; - } - // if no changes, close dialog - if (!isDirty) { - setOpen(false); - return; - } - try { - const updatedData = await updateProfileMutation.mutateAsync( - { - ...res.data, - }, - { - onError: (error) => { - if ( - error instanceof TRPCClientError && - error.data.code === "CONFLICT" && - error.message === ErrorMessages.USER_HANDLE_USED - ) { - setError("handle", { - type: "manual", - message: "User handle already exists.", - }); - } - }, - } - ); - toast.success("Profile updated successfully!"); - // revaildate user profile - revalidateUser(); - // revalidate cache for user profile or redirect to new user profile if handle changed - const updatedUser = updatedData.user; - if (updatedUser.handle !== oldHandle) { - // if user change user handle, redirect to new user profile - router.replace(`/${updatedUser.handle}`); - } else { - utils.getUserByHandle.invalidate({ handle: userHandle }); - setOpen(false); - } - } catch (error) { - console.log(error); - } - })} - > - Edit profile - - Make changes to your profile. - - - - - - - - - - -