From 4eb1b1a22028a2e63917548758891ca75e1cf01a Mon Sep 17 00:00:00 2001 From: Marco Escaleira Date: Wed, 20 Mar 2024 20:02:24 +0000 Subject: [PATCH] feat: update user password UI --- src/__generated__/gql.ts | 5 + src/__generated__/graphql.ts | 28 +++++ src/pages/Profile/UpdatePasswordModal.tsx | 140 ++++++++++++++++++++++ src/pages/Profile/index.tsx | 11 +- src/utils/queries/UpdateUserPassword.ts | 7 ++ 5 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 src/pages/Profile/UpdatePasswordModal.tsx create mode 100644 src/utils/queries/UpdateUserPassword.ts diff --git a/src/__generated__/gql.ts b/src/__generated__/gql.ts index baa67e0..3b4dd7d 100644 --- a/src/__generated__/gql.ts +++ b/src/__generated__/gql.ts @@ -35,6 +35,7 @@ const documents = { "\n mutation UpdateQuizComments($commentId: String!, $text: String!) {\n editComment(commentId: $commentId, text: $text)\n }\n": types.UpdateQuizCommentsDocument, "\n mutation UpdateQuiz($quizId: String!, $quiz: QuizAddInput!) {\n updateQuiz(quizId: $quizId, quiz: $quiz) {\n id\n }\n }\n": types.UpdateQuizDocument, "\n mutation UpdateUser($userId: String!, $user: UserUpdateInput!) {\n updateUser(userId: $userId, user: $user) {\n id\n }\n }\n": types.UpdateUserDocument, + "\n mutation UpdateUserPassword($userPasswordInput: PasswordUpdateInput!) {\n updatePassword(userPasswordInput: $userPasswordInput)\n }\n": types.UpdateUserPasswordDocument, "\n query UserAttempts($userId: String) {\n attempts(userId: $userId) {\n id\n quiz {\n id\n country\n }\n }\n }\n": types.UserAttemptsDocument, "\n query GetMe {\n getCurrentlyLoggedInUser {\n id\n email\n firstName\n lastName\n dateOfBirth\n country\n role\n createdAt\n updatedAt\n }\n }\n": types.GetMeDocument, }; @@ -141,6 +142,10 @@ export function gql(source: "\n mutation UpdateQuiz($quizId: String!, $quiz: Qu * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function gql(source: "\n mutation UpdateUser($userId: String!, $user: UserUpdateInput!) {\n updateUser(userId: $userId, user: $user) {\n id\n }\n }\n"): (typeof documents)["\n mutation UpdateUser($userId: String!, $user: UserUpdateInput!) {\n updateUser(userId: $userId, user: $user) {\n id\n }\n }\n"]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function gql(source: "\n mutation UpdateUserPassword($userPasswordInput: PasswordUpdateInput!) {\n updatePassword(userPasswordInput: $userPasswordInput)\n }\n"): (typeof documents)["\n mutation UpdateUserPassword($userPasswordInput: PasswordUpdateInput!) {\n updatePassword(userPasswordInput: $userPasswordInput)\n }\n"]; /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/src/__generated__/graphql.ts b/src/__generated__/graphql.ts index 1e579cd..9db927d 100644 --- a/src/__generated__/graphql.ts +++ b/src/__generated__/graphql.ts @@ -125,8 +125,11 @@ export type Mutation = { editComment: Scalars['Boolean']['output']; loginUser: LoginResponse; signupUser: UserData; + /** Update user password while logged in */ + updatePassword: Scalars['Boolean']['output']; /** Update a quiz */ updateQuiz: QuizData; + /** Update all user details */ updateUser: UserData; }; @@ -193,6 +196,11 @@ export type MutationSignupUserArgs = { }; +export type MutationUpdatePasswordArgs = { + userPasswordInput: PasswordUpdateInput; +}; + + export type MutationUpdateQuizArgs = { quiz: QuizAddInput; quizId: Scalars['String']['input']; @@ -222,6 +230,18 @@ export type OptionInput = { text: Scalars['String']['input']; }; +/** User update password input mutation data */ +export type PasswordUpdateInput = { + /** Current user password */ + currentPassword: Scalars['String']['input']; + /** New user password */ + password: Scalars['String']['input']; + /** New user password confirmation */ + passwordConfirm: Scalars['String']['input']; + /** User id for identification */ + userId: Scalars['String']['input']; +}; + export type Query = { __typename?: 'Query'; attemptById: AttemptData; @@ -585,6 +605,13 @@ export type UpdateUserMutationVariables = Exact<{ export type UpdateUserMutation = { __typename?: 'Mutation', updateUser: { __typename?: 'UserData', id: string } }; +export type UpdateUserPasswordMutationVariables = Exact<{ + userPasswordInput: PasswordUpdateInput; +}>; + + +export type UpdateUserPasswordMutation = { __typename?: 'Mutation', updatePassword: boolean }; + export type UserAttemptsQueryVariables = Exact<{ userId?: InputMaybe; }>; @@ -620,5 +647,6 @@ export const SubmitAttemptRatingDocument = {"kind":"Document","definitions":[{"k export const UpdateQuizCommentsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateQuizComments"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"commentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"text"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"editComment"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"commentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"commentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"text"},"value":{"kind":"Variable","name":{"kind":"Name","value":"text"}}}]}]}}]} as unknown as DocumentNode; export const UpdateQuizDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateQuiz"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"quizId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"quiz"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"QuizAddInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateQuiz"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"quizId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"quizId"}}},{"kind":"Argument","name":{"kind":"Name","value":"quiz"},"value":{"kind":"Variable","name":{"kind":"Name","value":"quiz"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const UpdateUserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateUser"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"user"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UserUpdateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateUser"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"userId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userId"}}},{"kind":"Argument","name":{"kind":"Name","value":"user"},"value":{"kind":"Variable","name":{"kind":"Name","value":"user"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; +export const UpdateUserPasswordDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateUserPassword"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userPasswordInput"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PasswordUpdateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updatePassword"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"userPasswordInput"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userPasswordInput"}}}]}]}}]} as unknown as DocumentNode; export const UserAttemptsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserAttempts"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"attempts"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"userId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"quiz"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"country"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetMeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetMe"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getCurrentlyLoggedInUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"dateOfBirth"}},{"kind":"Field","name":{"kind":"Name","value":"country"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/src/pages/Profile/UpdatePasswordModal.tsx b/src/pages/Profile/UpdatePasswordModal.tsx new file mode 100644 index 0000000..427e599 --- /dev/null +++ b/src/pages/Profile/UpdatePasswordModal.tsx @@ -0,0 +1,140 @@ +import { useState } from "react"; +import { useMutation } from "@apollo/client"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Button, Dialog, DialogBody, DialogHeader, Typography } from "@material-tailwind/react"; +import { Info } from "lucide-react"; +import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; +import { toast } from "react-toastify"; +import { z } from "zod"; +import { FormInput } from "@components/Form"; +import { useUserStore } from "@state/userStore"; +import { UPDATE_USER_PASSWORD } from "@utils/queries/UpdateUserPassword"; + +const formSchema = z + .object({ + currentPassword: z.string().min(1, { message: "Enter your current password." }), + password: z.string().min(1, { message: "Enter a new password." }), + passwordConfirm: z.string().min(1, { message: "Enter a new confirm password." }), + }) + .superRefine(({ password, passwordConfirm }, ctx) => { + if (password !== passwordConfirm) { + ctx.addIssue({ + code: "custom", + message: "The new passwords did not match", + path: ["passwordConfirm"], + }); + } + }); + +export const UpdatePasswordModal = () => { + const [open, setOpen] = useState(false); + const toggle = () => setOpen(!open); + + const { + user: { userId }, + } = useUserStore(); + + const [updatePassword, { loading, error: mutationError }] = useMutation(UPDATE_USER_PASSWORD); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + currentPassword: "", + password: "", + passwordConfirm: "", + }, + }); + const { + handleSubmit, + reset, + formState: { errors }, + } = form; + + const onSubmit: SubmitHandler> = async (values, event) => { + event?.preventDefault(); + try { + await updatePassword({ + variables: { + userPasswordInput: { + userId, + currentPassword: values.currentPassword, + password: values.password, + passwordConfirm: values.passwordConfirm, + }, + }, + }); + reset(); + toggle(); + toast.success("Password updated successfully!"); + } catch (e) { + console.log("Something went wrong", e); + } + }; + + return ( + <> + + + + Update your password + + + +
+ + + + + + + Use at least 8 characters, one uppercase, one lowercase and one number and one special character. + + + {mutationError?.message && ( + + {mutationError.message} + + )} + +
+ + +
+ +
+
+
+ + ); +}; diff --git a/src/pages/Profile/index.tsx b/src/pages/Profile/index.tsx index e36b6a6..c0da369 100644 --- a/src/pages/Profile/index.tsx +++ b/src/pages/Profile/index.tsx @@ -13,6 +13,7 @@ import { useUserStore } from "@state/userStore.ts"; import { DATE_TIME, DATE_TIME_READ } from "@utils/constants.ts"; import { UPDATE_USER } from "@utils/queries/UpdateUser"; import { DownloadAttempts } from "./DownloadAttempts"; +import { UpdatePasswordModal } from "./UpdatePasswordModal"; const formSchema = z.object({ firstName: z.string().min(1, { message: "Enter a first name." }), @@ -138,9 +139,13 @@ export function Component() { {!isEditing && ( - +
+ + + +
)} diff --git a/src/utils/queries/UpdateUserPassword.ts b/src/utils/queries/UpdateUserPassword.ts new file mode 100644 index 0000000..42a10be --- /dev/null +++ b/src/utils/queries/UpdateUserPassword.ts @@ -0,0 +1,7 @@ +import { gql } from "@generated/gql.ts"; + +export const UPDATE_USER_PASSWORD = gql(/* GraphQL */ ` + mutation UpdateUserPassword($userPasswordInput: PasswordUpdateInput!) { + updatePassword(userPasswordInput: $userPasswordInput) + } +`);