diff --git a/public/images/profile.png b/public/images/profile.png new file mode 100644 index 000000000..7dd8036ea Binary files /dev/null and b/public/images/profile.png differ diff --git a/src/components/Profile/AddPasskey.tsx b/src/components/Profile/AddPasskey.tsx index 2ff8de60f..59b2211d2 100644 --- a/src/components/Profile/AddPasskey.tsx +++ b/src/components/Profile/AddPasskey.tsx @@ -1,52 +1,35 @@ -import { useEffect, useState } from 'react'; +import { Alert, Button } from 'flowbite-react' +import React, { useEffect, useState } from 'react' +import { IDeviceData, IdeviceBody, RegistrationOptionInterface } from './interfaces' +import DeviceDetails from '../../commonComponents/DeviceDetailsCard' +import PasskeyAddDevice from '../../commonComponents/PasseyAddDevicePopup' +import { AxiosError, AxiosResponse } from 'axios' +import { addDeviceDetails, generateRegistrationOption, getUserDeviceDetails, verifyRegistration } from '../../api/Fido' +import { apiStatusCodes, storageKeys } from '../../config/CommonConstant' +import { apiRoutes } from '../../config/apiRoutes' import { startRegistration } from '@simplewebauthn/browser' -import type { AxiosError, AxiosResponse } from 'axios'; -import { addDeviceDetails, generateRegistrationOption, getUserDeviceDetails, verifyRegistration } from '../../api/Fido'; -import DeviceDetails from '../../commonComponents/DeviceDetailsCard'; -import { apiStatusCodes, storageKeys } from '../../config/CommonConstant'; -import { getFromLocalStorage, getUserProfile } from '../../api/Auth'; -import BreadCrumbs from '../BreadCrumbs'; -import { Alert } from 'flowbite-react'; -import type { IDeviceData, IdeviceBody, RegistrationOptionInterface, UserProfile, verifyRegistrationObjInterface } from './interfaces'; -import DisplayUserProfile from './DisplayUserProfile'; -import UpdateUserProfile from './EditUserProfile'; -import CustomSpinner from '../CustomSpinner'; -import PasskeyAddDevice from '../../commonComponents/PasseyAddDevicePopup'; -import { apiRoutes } from '../../config/apiRoutes'; -import React from 'react'; +import { getFromLocalStorage } from '../../api/Auth' +import CustomSpinner from '../CustomSpinner' const AddPasskey = () => { + const [fidoError, setFidoError] = useState("") const [fidoLoader, setFidoLoader] = useState(true) const [OrgUserEmail, setOrgUserEmail] = useState('') const [deviceList, setDeviceList] = useState([]) const [addSuccess, setAddSuccess] = useState(null) const [addfailure, setAddFailur] = useState(null) - const [isEditProfileOpen, setIsEditProfileOpen] = useState(false); - const [prePopulatedUserProfile, setPrePopulatedUserProfile] = useState(null); const [disableFlag, setDisableFlag] = useState(false) const [openModel, setOpenModel] = useState(false) const props = { openModel, setOpenModel }; - const fetchUserProfile = async () => { - try { - const token = await getFromLocalStorage(storageKeys.TOKEN); - const userDetailsResponse = await getUserProfile(token); - const { data } = userDetailsResponse as AxiosResponse; - - if (data?.statusCode === apiStatusCodes.API_STATUS_SUCCESS) { - setPrePopulatedUserProfile(data?.data); - } - } catch (error) { - } - }; - - const toggleEditProfile = async () => { - await fetchUserProfile() - setIsEditProfileOpen(!isEditProfileOpen); - }; + const setProfile = async () => { + const UserEmail = await getFromLocalStorage(storageKeys.USER_EMAIL) + setOrgUserEmail(UserEmail) + return UserEmail + } const showFidoError = (error: unknown): void => { const err = error as AxiosError @@ -65,16 +48,6 @@ const AddPasskey = () => { setFidoLoader(false) } } - const setProfile = async () => { - const UserEmail = await getFromLocalStorage(storageKeys.USER_EMAIL) - setOrgUserEmail(UserEmail) - return UserEmail - } - - useEffect(() => { - fetchUserProfile(); - }, []); - const registerWithPasskey = async (flag: boolean): Promise => { try { const RegistrationOption: RegistrationOptionInterface = { @@ -164,9 +137,9 @@ const AddPasskey = () => { return data }) : [] - if (data?.data?.length === 1){ - setDisableFlag(true) - } + if (data?.data?.length === 1) { + setDisableFlag(true) + } setDeviceList(deviceDetails) } } catch (error) { @@ -174,7 +147,6 @@ const AddPasskey = () => { setFidoLoader(false) } } - useEffect(() => { if (OrgUserEmail) { userDeviceDetails(); @@ -183,102 +155,80 @@ const AddPasskey = () => { } }, [OrgUserEmail]); - const updatePrePopulatedUserProfile = (updatedProfile: UserProfile) => { - setPrePopulatedUserProfile(updatedProfile); - }; - return ( - -
- -

- User's Profile -

- - {fidoLoader +
+
+
+
+ {fidoLoader ?
: -
- - {/* first section */} -
- - {!isEditProfileOpen && prePopulatedUserProfile && ( - - )} - - {isEditProfileOpen && prePopulatedUserProfile && ( - - )} - -
- - {/* second section */} -
-
- -
- {deviceList && deviceList.length > 0 && - deviceList.map((element, key) => ( - - ))} -
- -
- - - { - (addSuccess || addfailure || fidoError) && -
- { - setAddSuccess(null) - setFidoError('') - setAddFailur('') - }} - > - -

- {addSuccess || addfailure || fidoError} -

-
-
-
+
+
+
+

Add Passkey

+

With Passkey, no complex passwords to remember.

+
+ + {deviceList && deviceList.length > 0 && + deviceList.map((element, key) => ( + + ))} + +
+ + + { + (addSuccess || addfailure || fidoError) && +
+ { + setAddSuccess(null) + setFidoError('') + setAddFailur('') + }} + > + +

+ {addSuccess || addfailure || fidoError} +

+
+
+
+ } + +
+ +
-
+ }
- -
- } +
- ); -}; -export default AddPasskey; + ) +} + +export default AddPasskey \ No newline at end of file diff --git a/src/components/Profile/DisplayProfileImg.tsx b/src/components/Profile/DisplayProfileImg.tsx index fe73aecd7..6bab045d2 100644 --- a/src/components/Profile/DisplayProfileImg.tsx +++ b/src/components/Profile/DisplayProfileImg.tsx @@ -6,15 +6,12 @@ import { getFromLocalStorage } from "../../api/Auth"; import { storageKeys } from "../../config/CommonConstant"; const DisplayProfileImg = () => { - const [userObj, setUserObj] = useState(null) - const getUserDetails = async () => { const userProfile = await getFromLocalStorage(storageKeys.USER_PROFILE) const orgRoles = await getFromLocalStorage(storageKeys.ORG_ROLES) const parsedUser = JSON.parse(userProfile) parsedUser.roles = orgRoles - setUserObj(parsedUser) } @@ -22,7 +19,6 @@ const DisplayProfileImg = () => { getUserDetails() }, []) - return ( <> {(userObj?.profileImg) ? diff --git a/src/components/Profile/DisplayUserProfile.tsx b/src/components/Profile/DisplayUserProfile.tsx index cce777def..599ff86b6 100644 --- a/src/components/Profile/DisplayUserProfile.tsx +++ b/src/components/Profile/DisplayUserProfile.tsx @@ -1,82 +1,92 @@ import type { UserProfile } from "./interfaces"; import CustomAvatar from '../Avatar' +import { Button } from "flowbite-react"; interface DisplayUserProfileProps { toggleEditProfile: () => void; userProfileInfo: UserProfile | null; - } - - const DisplayUserProfile = ({ toggleEditProfile, userProfileInfo }: DisplayUserProfileProps) => { - - return ( -
- -
-
    - -
  • - {(userProfileInfo?.profileImg) ? - : - } - - - - - -
  • - -
  • -
    -
    -

    - Name -

    -

    - {userProfileInfo?.firstName} {userProfileInfo?.lastName} -

    -
    - -
    -
  • -
  • -
    - -
    -

    - Email -

    -

    - - {userProfileInfo?.email} -

    -
    +} +const DisplayUserProfile = ({ toggleEditProfile, userProfileInfo }: DisplayUserProfileProps) => { + return ( +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +

    General

    +

    Basic info, like your first name, last name and profile image that will be displayed

    +
    + + + +
    + +
    +
    First Name
    +
    + {userProfileInfo?.firstName ? ( + userProfileInfo.firstName + ) : ( + "N/A" + )} + +
    +
    +
    +
    Last Name
    +
    + {userProfileInfo?.lastName ? ( + userProfileInfo.lastName + ) : ( + "N/A" + )} + +
    +
    +
    +
    Profile Image
    +
    +
    + + {(userProfileInfo?.profileImg) ? + : + } +
    +
    +
    +
    +
    -
  • -
+
+
); diff --git a/src/components/Profile/EditUserProfile.tsx b/src/components/Profile/EditUserProfile.tsx index 16503a359..6b5d695d6 100644 --- a/src/components/Profile/EditUserProfile.tsx +++ b/src/components/Profile/EditUserProfile.tsx @@ -1,19 +1,20 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import type { UserProfile } from "./interfaces"; import { setToLocalStorage, updateUserProfile } from "../../api/Auth"; -import { IMG_MAX_HEIGHT, IMG_MAX_WIDTH, imageSizeAccepted, storageKeys } from "../../config/CommonConstant"; +import { IMG_MAX_HEIGHT, IMG_MAX_WIDTH, imageSizeAccepted, storageKeys} from "../../config/CommonConstant"; import type { AxiosResponse } from "axios"; import CustomAvatar from '../Avatar' import { calculateSize, dataURItoBlob } from "../../utils/CompressImage"; -import { Button, Label } from "flowbite-react"; -import { Field, Form, Formik, FormikHelpers } from "formik"; +import { Alert,Button} from "flowbite-react"; +import { Form, Formik, FormikHelpers } from "formik"; import * as yup from "yup" +import React from "react"; interface Values { profileImg: string; firstName: string; lastName: string; - email:string; + email: string; } interface ILogoImage { @@ -27,10 +28,13 @@ interface EditUserProfileProps { updateProfile: (updatedProfile: UserProfile) => void; } -const UpdateUserProfile = ({ toggleEditProfile, userProfileInfo, updateProfile }: EditUserProfileProps) => { +const EditUserProfile = ({ toggleEditProfile, userProfileInfo, updateProfile}: EditUserProfileProps) => { const [loading, setLoading] = useState(false) const [isImageEmpty, setIsImageEmpty] = useState(true) + const [success, setSuccess] = useState(null) + const [failure, setFailure] = useState(null) + const [initialProfileData, setInitialProfileData] = useState({ profileImg: userProfileInfo?.profileImg || "", firstName: userProfileInfo?.firstName || "", @@ -44,15 +48,23 @@ const UpdateUserProfile = ({ toggleEditProfile, userProfileInfo, updateProfile } fileName: '' }) + const firstNameInputRef = useRef(null); + + useEffect(() => { + if (firstNameInputRef.current) { + firstNameInputRef.current.focus(); + } + }, []); + + useEffect(() => { if (userProfileInfo) { setInitialProfileData({ profileImg: userProfileInfo?.profileImg || "", - firstName: userProfileInfo.firstName || '', - lastName: userProfileInfo.lastName || '', + firstName: userProfileInfo.firstName || "", + lastName: userProfileInfo.lastName || "", email: userProfileInfo?.email, - }); setLogoImage({ @@ -120,7 +132,7 @@ const UpdateUserProfile = ({ toggleEditProfile, userProfileInfo, updateProfile } ...logoImage, imagePreviewUrl: '', }); - + const reader = new FileReader() const file = event?.target?.files @@ -151,188 +163,212 @@ const UpdateUserProfile = ({ toggleEditProfile, userProfileInfo, updateProfile } lastName: values.lastName, email: values.email, profileImg: logoImage?.imagePreviewUrl as string || values?.profileImg - } - const resUpdateUserDetails = await updateUserProfile(userData) const { data } = resUpdateUserDetails as AxiosResponse setToLocalStorage(storageKeys.USER_PROFILE, userData) updateProfile(userData); - window.location.reload() + await setToLocalStorage(storageKeys.USER_PROFILE, userData); + window.location.reload(); setLoading(false) - } const validationSchema = yup.object().shape({ firstName: yup.string() - .required("First Name is required") - .min(2, 'First name must be at least 2 characters') - .max(255, 'First name must be at most 255 characters'), + .required("First Name is required") + .min(2, 'First name must be at least 2 characters') + .max(255, 'First name must be at most 255 characters'), lastName: yup.string() - .required("Last Name is required") - .min(2, 'Last name must be at least 2 characters') - .max(255, 'Last name must be at most 255 characters') + .required("Last Name is required") + .min(2, 'Last name must be at least 2 characters') + .max(255, 'Last name must be at most 255 characters') }); - return ( -
-
- - ) => { - if (!values.firstName || !values.lastName) { - return; - } - - updateUserDetails(values); - toggleEditProfile(); - }} - - validationSchema={validationSchema}> - {(formikHandlers): JSX.Element => ( -
-
-
+
+
+
+
+ + { + (success === "Profile Updated Successfully" || failure) && + { + setSuccess(null) + setFailure(null) + }} > + +

+ {success || failure} +

+
+
+ } + + + + ) => { + await updateUserDetails(values); + toggleEditProfile(); + }} + + validationSchema={validationSchema}> + {(formikHandlers): JSX.Element => ( + + +
+
+ +
+

General

+

Basic info, like your first name, last name and profile image that will be displayed

+
- {logoImage.imagePreviewUrl ? ( - Profile Picture - - ) : ( - )} - -
-

- Profile Image -

-
- JPG, JPEG and PNG . Max size of 1MB +
-
- -
- +
+
+ First Name + *
+
+
+ + {(formikHandlers?.errors?.firstName && formikHandlers?.touched?.firstName) && ( + + {formikHandlers?.errors?.firstName} + + )} +
+
-
- - - -
- - -
-
-
-
- - { - (formikHandlers?.errors?.firstName && formikHandlers?.touched?.firstName) && - {formikHandlers?.errors?.firstName} - } -
- -
-
-
- - { - (formikHandlers?.errors?.lastName && formikHandlers?.touched?.lastName) && - {formikHandlers?.errors?.lastName} - } -
- -
- -
- - )} - +
+
+ Last Name + * +
+ +
+
+ + {(formikHandlers?.errors?.lastName && formikHandlers?.touched?.lastName) && ( + + {formikHandlers?.errors?.lastName} + + )} +
+
+
+
+
Profile Image
+
+
+ {logoImage.imagePreviewUrl ? ( + Profile Picture + ) : ( + )} + +
+ + +
+
+
+
+
+
+ +
+
+ +
+
+ + )} + +
+
+
); }; -export default UpdateUserProfile; +export default EditUserProfile; diff --git a/src/components/Profile/UserProfile.tsx b/src/components/Profile/UserProfile.tsx new file mode 100644 index 000000000..d5c8e3cfc --- /dev/null +++ b/src/components/Profile/UserProfile.tsx @@ -0,0 +1,115 @@ +import { useEffect, useState } from 'react'; +import type { AxiosResponse } from 'axios'; +import { apiStatusCodes, storageKeys } from '../../config/CommonConstant'; +import { getFromLocalStorage, getUserProfile } from '../../api/Auth'; +import BreadCrumbs from '../BreadCrumbs'; +import type { UserProfile } from './interfaces'; +import DisplayUserProfile from './DisplayUserProfile'; +import React from 'react'; +import AddPasskey from './AddPasskey'; +import EditUserProfile from './EditUserProfile'; + +const UserProfile = () => { + const [isEditProfileOpen, setIsEditProfileOpen] = useState(false); + const [prePopulatedUserProfile, setPrePopulatedUserProfile] = useState(null); + + const fetchUserProfile = async () => { + try { + const token = await getFromLocalStorage(storageKeys.TOKEN); + const userDetailsResponse = await getUserProfile(token); + const { data } = userDetailsResponse as AxiosResponse; + + if (data?.statusCode === apiStatusCodes.API_STATUS_SUCCESS) { + setPrePopulatedUserProfile(data?.data); + } + } catch (error) { + } + }; + + const toggleEditProfile = async () => { + await fetchUserProfile() + setIsEditProfileOpen(!isEditProfileOpen); + }; + + + useEffect(() => { + fetchUserProfile(); + }, []); + + + const updatePrePopulatedUserProfile = (updatedProfile: UserProfile) => { + setPrePopulatedUserProfile(updatedProfile); + }; + + return ( + +
+ +

+ User's Profile +

+ +
+
+
+
    +
  • + +
  • +
  • + +
  • +
+
+ +
+
+
+ + {!isEditProfileOpen && prePopulatedUserProfile && ( + + )} + + {isEditProfileOpen && prePopulatedUserProfile && ( + + )} + +
+
+ +
+ +
+
+ +
+ ); +}; + +export default UserProfile; diff --git a/src/components/organization/users/index.tsx b/src/components/organization/users/index.tsx index 6f1d00c49..eec2e9e3e 100644 --- a/src/components/organization/users/index.tsx +++ b/src/components/organization/users/index.tsx @@ -7,6 +7,7 @@ import BreadCrumbs from '../../BreadCrumbs'; import Invitations from '../invitations/Invitations'; import { MdDashboard } from 'react-icons/md'; import Members from './Members'; +import React from 'react'; const initialPageState = { pageNumber: 1, diff --git a/src/pages/profile.astro b/src/pages/profile.astro index 007222d0c..ea58791c9 100644 --- a/src/pages/profile.astro +++ b/src/pages/profile.astro @@ -1,6 +1,6 @@ --- import LayoutSidebar from '../app/LayoutSidebar.astro'; -import AddPasskey from '../components/Profile/AddPasskey'; +import UserProfile from '../components/Profile/UserProfile'; import { pathRoutes } from '../config/pathRoutes'; import { checkUserSession } from '../utils/check-session'; @@ -12,5 +12,5 @@ if (!response) { --- - + diff --git a/tailwind.config.cjs b/tailwind.config.cjs index 4fb0836d4..7614fac1b 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -101,6 +101,9 @@ module.exports = { minWidth: { kanban: '28rem', }, + maxWidth: { + '100/6rem': 'calc(100% - 6rem)' + } }, },