From 6a5c64984e927e4acac20b813807ed67a127f7d8 Mon Sep 17 00:00:00 2001 From: bombies Date: Tue, 31 Oct 2023 12:37:49 -0500 Subject: [PATCH] Implement member profile information updates --- .../components/EditableUserProfile.tsx | 60 ++++++++++++++++--- .../inputs/editable/EditableAvatar.tsx | 2 + .../inputs/editable/EditableInput.tsx | 7 ++- src/app/(site)/hooks/s3/useCloudFrontUrl.tsx | 2 + src/app/api/me/route.ts | 5 +- src/app/api/utils/api-utils.ts | 34 ++++++++--- 6 files changed, 89 insertions(+), 21 deletions(-) diff --git a/src/app/(site)/(internal)/settings/account/components/EditableUserProfile.tsx b/src/app/(site)/(internal)/settings/account/components/EditableUserProfile.tsx index 9daf28f..c793358 100644 --- a/src/app/(site)/(internal)/settings/account/components/EditableUserProfile.tsx +++ b/src/app/(site)/(internal)/settings/account/components/EditableUserProfile.tsx @@ -13,25 +13,28 @@ import toast from "react-hot-toast"; import UpdateSelfMember from "@/app/(site)/hooks/user/UpdateSelfMember"; import {PatchSelfDto} from "@/app/api/me/self-user.dto"; import {handleAxiosError} from "@/utils/client/client-utils"; +import {Member} from "@prisma/client"; +import {AxiosError} from "axios"; const EditableUserProfile: FC = () => { - const {trigger: update, isMutating: isUpdating} = UpdateSelfMember() + const {trigger: update} = UpdateSelfMember() const [optimisticAvatarSrc, setOptimisticAvatarSrc] = useState() const { memberData: { data: member, loading: memberDataLoading, + mutateData: mutateMemberData, optimisticData: { editOptimisticData: editMemberData } } } = useMemberData() - const doUpdate = useCallback(async (dto: PatchSelfDto) => ( - update({body: dto}) + const doUpdate = useCallback(async (dto: PatchSelfDto, handleError?: boolean): Promise => { + const work = update({body: dto}) .then(res => res.data) - .catch(handleAxiosError) - ), [update]) + return handleError ? work.catch(handleAxiosError) : work + }, [update]) return ( { onUploadSuccess={async (key) => { if (editMemberData) await editMemberData( - () => doUpdate({image: key}), + () => doUpdate({image: key}, true), { ...member!, image: key @@ -105,8 +108,27 @@ const EditableUserProfile: FC = () => { }, errorMsg: "Invalid username!" }} - onEdit={(newValue) => { + onEdit={async (newValue) => { + if (!newValue) + return + const newUsername = newValue.toLowerCase() + if (newUsername === member!.username) + return; + + await toast.promise(doUpdate({username: newUsername}) + .then(async (res) => { + if (!res || !mutateMemberData) + return + await mutateMemberData(res) + }) + .catch((err: AxiosError) => Promise.reject(err.response?.statusText ?? "Something went wrong!")), { + loading: "Updating username...", + success: "Successfully updated your username!", + error(err: string) { + return err + } + }) }} >

{member?.username} { minLength={1} maxLength={60} size="sm" - onEdit={(newValue) => { + onEdit={async (newValue) => { + if (!newValue) + return + if (editMemberData) + await editMemberData( + () => doUpdate({firstName: newValue}, true), + { + ...member!, + firstName: newValue + } + ) }} >

{member?.firstName} { minLength={1} maxLength={60} size="sm" - onEdit={(newValue) => { + onEdit={async (newValue) => { + if (!newValue) + return + if (editMemberData) + await editMemberData( + () => doUpdate({lastName: newValue}, true), + { + ...member!, + lastName: newValue + } + ) }} >

{member?.lastName} = ({isEditable, value, children, onEdit, ...input else setEditToggled(false) }, [isEditable, value]) - const onSubmit: SubmitHandler = useCallback((data) => { + const onSubmit: SubmitHandler = useCallback((data, e) => { + if (inputProps.validate && !inputProps.validate.predicate(data.value)) + return; + if (data.value === value) return setEditToggled(false) @@ -37,7 +40,7 @@ const EditableInput: FC = ({isEditable, value, children, onEdit, ...input onEdit(data.value.length === 0 ? undefined : data.value) setEditToggled(false) setCurrentValue(value ?? "") - }, [onEdit, value]) + }, [inputProps.validate, onEdit, value]) return ( diff --git a/src/app/(site)/hooks/s3/useCloudFrontUrl.tsx b/src/app/(site)/hooks/s3/useCloudFrontUrl.tsx index 682f5c2..53b1699 100644 --- a/src/app/(site)/hooks/s3/useCloudFrontUrl.tsx +++ b/src/app/(site)/hooks/s3/useCloudFrontUrl.tsx @@ -1,3 +1,5 @@ +"use client" + import {fetcher} from "@/utils/client/client-utils"; import useSWR from "swr"; diff --git a/src/app/api/me/route.ts b/src/app/api/me/route.ts index 73d5545..386da5b 100644 --- a/src/app/api/me/route.ts +++ b/src/app/api/me/route.ts @@ -11,7 +11,10 @@ export const PATCH = async (req: Request) => { return authenticated(async (session) => selfUserService.update(session, await req.json()), { prismaErrors: { - recordNotFoundMessage: "Couldn't find your information!" + recordNotFoundMessage: "Couldn't find your information!", + uniqueConstraintFailed(target) { + return `There is already a user with that ${target}!` + } } } ) diff --git a/src/app/api/utils/api-utils.ts b/src/app/api/utils/api-utils.ts index 8ddb065..a658052 100644 --- a/src/app/api/utils/api-utils.ts +++ b/src/app/api/utils/api-utils.ts @@ -1,7 +1,7 @@ import {NextResponse} from "next/server"; import {getServerSession, Session} from "next-auth"; import authOptions from "@/app/api/auth/[...nextauth]/utils"; -import {buildResponse, RouteResponseType} from "@/app/api/utils/types"; +import {buildResponse} from "@/app/api/utils/types"; import {Member, Prisma} from "@prisma/client"; import prisma from "@/libs/prisma"; import PrismaClientKnownRequestError = Prisma.PrismaClientKnownRequestError; @@ -10,10 +10,11 @@ export type RouteContext = { params: T } -export type IdObject = {id: string} +export type IdObject = { id: string } type PrismaErrorOptions = { - recordNotFoundMessage?: string + recordNotFoundMessage?: string, + uniqueConstraintFailed?: string | ((target: string) => string), } @@ -43,9 +44,11 @@ export const authenticated = async (logic: (session: Session, member?: Member) = status: 404, message: "Couldn't find information for your user!" }) + + return await logic(session, member) } - return logic(session) + return await logic(session) } catch (e) { const prismaError = prismaErrorHandler(e, options?.prismaErrors) if (prismaError) @@ -61,11 +64,24 @@ export const authenticated = async (logic: (session: Session, member?: Member) = const prismaErrorHandler = (e: any, options?: PrismaErrorOptions): NextResponse | undefined => { if (e instanceof PrismaClientKnownRequestError) { - if (e.code === "P2001") - return buildResponse({ - status: 404, - message: options?.recordNotFoundMessage ?? `Couldn't find any records to complete execution!` - }) + switch (e.code) { + case "P2001": { + return buildResponse({ + status: 404, + message: options?.recordNotFoundMessage ?? `Couldn't find any records to complete execution!` + }) + } + case "P2002": { + return buildResponse({ + status: 400, + message: options?.uniqueConstraintFailed ? + (options.uniqueConstraintFailed instanceof Function + ? options.uniqueConstraintFailed((e.meta!.target as string[])[0]) + : options.uniqueConstraintFailed as string) + : `There is already a record with the value of that unique constraint!` + }) + } + } } return undefined } \ No newline at end of file