diff --git a/src/frontend/src/components/GoogleAuth/index.tsx b/src/frontend/src/components/GoogleAuth/index.tsx index 1763a886..0d8c38b4 100644 --- a/src/frontend/src/components/GoogleAuth/index.tsx +++ b/src/frontend/src/components/GoogleAuth/index.tsx @@ -25,24 +25,32 @@ function GoogleAuth() { const userDetailsUrl = `${BASE_URL}/users/my-info/`; const completeLogin = async () => { + // fetch callback api const response = await fetch(callbackUrl, { credentials: 'include' }); const token = await response.json(); - localStorage.setItem('token', token); + localStorage.setItem('token', token.access_token); + + // fetch user details const response2 = await fetch(userDetailsUrl, { credentials: 'include', headers: { 'access-token': token.access_token }, }); const userDetails = await response2.json(); - localStorage.setItem('userprofile', userDetails); + + // stringify the response and set it to local storage + const userDetailsString = JSON.stringify(userDetails); + localStorage.setItem('userprofile', userDetailsString); setUserProfileDetails(userDetails); + + // navigate according the user + if (userDetails?.has_user_profile) { + navigate('/projects'); + } else { + navigate('/user-profile'); + } }; await completeLogin(); toast.success('Logged In Successfully'); - if (userProfileDetails?.has_user_profile) { - navigate('/projects'); - } else { - navigate('/user-profile'); - } } setIsReadyToRedirect(true); }; diff --git a/src/frontend/src/components/LandingPage/SignInOverlay/index.tsx b/src/frontend/src/components/LandingPage/SignInOverlay/index.tsx index c02ebd84..81bf58e5 100644 --- a/src/frontend/src/components/LandingPage/SignInOverlay/index.tsx +++ b/src/frontend/src/components/LandingPage/SignInOverlay/index.tsx @@ -46,7 +46,7 @@ export default function SignInOverlay() { className="!naxatw-bg-landing-red" rightIcon="east" onClick={() => { - dispatch(setCommonState({ signInAs: 'Project Creator' })); + localStorage.setItem('signedInAs', 'Project Creator'); if (isAuthenticated()) { navigate('/projects'); } else { @@ -67,7 +67,7 @@ export default function SignInOverlay() { className="!naxatw-bg-landing-red" rightIcon="east" onClick={() => { - dispatch(setCommonState({ signInAs: 'Drone Operator' })); + localStorage.setItem('signedInAs', 'Drone Operator'); if (isAuthenticated()) { navigate('/projects'); } else { diff --git a/src/frontend/src/components/UserProfile/FormContents/BasicDetails/index.tsx b/src/frontend/src/components/UserProfile/FormContents/BasicDetails/index.tsx index 6679e457..677d9ae6 100644 --- a/src/frontend/src/components/UserProfile/FormContents/BasicDetails/index.tsx +++ b/src/frontend/src/components/UserProfile/FormContents/BasicDetails/index.tsx @@ -1,18 +1,18 @@ import { Flex, FlexColumn } from '@Components/common/Layouts'; -import { FormControl, Input, Label } from '@Components/common/FormUI'; +import { FormControl, Select, Input, Label } from '@Components/common/FormUI'; import ErrorMessage from '@Components/common/ErrorMessage'; +import { countriesWithPhoneCodes } from '@Constants/countryCode'; +import { Controller } from 'react-hook-form'; export default function BasicDetails({ formProps }: { formProps: any }) { - const { register } = formProps; + const { register, formState, control } = formProps; - // const userProfile: UserProfileDetailsType = - // localStorage.getItem('userprofile'); return ( -
+
-

Basic Details

+

Basic Details

- +

SK

@@ -22,45 +22,43 @@ export default function BasicDetails({ formProps }: { formProps: any }) { placeholder="Enter Name" className="naxatw-mt-1" {...register('name', { - required: 'Required', + required: 'Name is Required', })} /> - + - - ( + - */} - + @@ -69,20 +67,19 @@ export default function BasicDetails({ formProps }: { formProps: any }) { placeholder="+977" className="naxatw-mt-1 naxatw-w-14" {...register('country_code', { - required: 'Required', + required: 'Phone Number is Required', })} /> - +
diff --git a/src/frontend/src/components/UserProfile/FormContents/OrganizationDetails/index.tsx b/src/frontend/src/components/UserProfile/FormContents/OrganizationDetails/index.tsx index d963c813..eac54968 100644 --- a/src/frontend/src/components/UserProfile/FormContents/OrganizationDetails/index.tsx +++ b/src/frontend/src/components/UserProfile/FormContents/OrganizationDetails/index.tsx @@ -1,12 +1,15 @@ import ErrorMessage from '@Components/common/ErrorMessage'; import { FormControl, Input, Label } from '@Components/common/FormUI'; -import { FlexColumn } from '@Components/common/Layouts'; +import { Flex, FlexColumn } from '@Components/common/Layouts'; export default function OrganizationDetails({ formProps }: { formProps: any }) { const { register } = formProps; return ( -
+
+ +

Organization Details

+
@@ -14,7 +17,7 @@ export default function OrganizationDetails({ formProps }: { formProps: any }) { placeholder="Enter Organization Name" className="naxatw-mt-1" {...register('organization_name', { - required: 'Required', + required: 'Organization name is Required', })} /> state.common.isCertifiedDroneUser, + ); + + const { register, setValue } = formProps; + + return ( +
+ +

+ Other Details +

+
+ + + + + + + + + + + + + + + + + + { + dispatch(setCommonState({ isCertifiedDroneUser: val })); + setValue('certified_drone_operator', val === 'yes'); + }} + value={isCertifiedDroneOperator} + /> + + + {}} + onChange={() => {}} + setValue={() => {}} + placeholder="*The supported file formats are pdf, .jpeg, .png" + /> + +
+ ); +} diff --git a/src/frontend/src/components/UserProfile/FormContents/Password/index.tsx b/src/frontend/src/components/UserProfile/FormContents/Password/index.tsx index 17572791..ff26b892 100644 --- a/src/frontend/src/components/UserProfile/FormContents/Password/index.tsx +++ b/src/frontend/src/components/UserProfile/FormContents/Password/index.tsx @@ -1,37 +1,42 @@ import ErrorMessage from '@Components/common/ErrorMessage'; import { FormControl, Input, Label } from '@Components/common/FormUI'; -import { FlexColumn } from '@Components/common/Layouts'; +import { Flex, FlexColumn } from '@Components/common/Layouts'; export default function PasswordSection({ formProps }: { formProps: any }) { - const { register } = formProps; + const { register, formState } = formProps; return ( -
+
+ +

Change Password

+
- + - +
diff --git a/src/frontend/src/components/UserProfile/FormContents/index.tsx b/src/frontend/src/components/UserProfile/FormContents/index.tsx index 8695bbca..5c6cb321 100644 --- a/src/frontend/src/components/UserProfile/FormContents/index.tsx +++ b/src/frontend/src/components/UserProfile/FormContents/index.tsx @@ -1,5 +1,6 @@ import BasicDetails from './BasicDetails'; import OrganizationDetails from './OrganizationDetails'; +import OtherDetails from './OtherDetails'; import PasswordSection from './Password'; -export { BasicDetails, OrganizationDetails, PasswordSection }; +export { BasicDetails, OrganizationDetails, OtherDetails, PasswordSection }; diff --git a/src/frontend/src/components/common/ErrorMessage/index.tsx b/src/frontend/src/components/common/ErrorMessage/index.tsx index 715ae264..dfb8d3b7 100644 --- a/src/frontend/src/components/common/ErrorMessage/index.tsx +++ b/src/frontend/src/components/common/ErrorMessage/index.tsx @@ -13,7 +13,7 @@ export default function ErrorMessage({ return (

{message} diff --git a/src/frontend/src/components/common/FormUI/Input/index.tsx b/src/frontend/src/components/common/FormUI/Input/index.tsx index 37d3866b..2f5002d4 100644 --- a/src/frontend/src/components/common/FormUI/Input/index.tsx +++ b/src/frontend/src/components/common/FormUI/Input/index.tsx @@ -11,7 +11,7 @@ const Input = React.forwardRef( type={type} placeholder={placeholder || 'Search'} className={cn( - `hover:naxatw-border-primary-600 focus:naxatw-border-primary-600 naxatw-flex naxatw-rounded-[4px] naxatw-border naxatw-border-[#555555] naxatw-bg-transparent naxatw-p-2 naxatw-text-body-md file:naxatw-font-medium focus:naxatw-bg-transparent focus:naxatw-outline-none disabled:naxatw-cursor-not-allowed`, + `naxatw-flex naxatw-rounded-[4px] naxatw-border naxatw-border-[#555555] naxatw-bg-transparent naxatw-p-2 naxatw-text-body-md file:naxatw-font-medium hover:naxatw-border-red focus:naxatw-border-red focus:naxatw-bg-transparent focus:naxatw-outline-none disabled:naxatw-cursor-not-allowed`, className, )} ref={ref} diff --git a/src/frontend/src/components/common/UserProfile/index.tsx b/src/frontend/src/components/common/UserProfile/index.tsx index 13ee9d0a..d5378d16 100644 --- a/src/frontend/src/components/common/UserProfile/index.tsx +++ b/src/frontend/src/components/common/UserProfile/index.tsx @@ -26,7 +26,7 @@ export default function UserProfile() { icon: 'logout', onClick: () => { localStorage.clear(); - navigate('/login'); + navigate('/'); toast.success('Logged Out Successfully'); }, }, diff --git a/src/frontend/src/constants/countryCode.ts b/src/frontend/src/constants/countryCode.ts new file mode 100644 index 00000000..f7948716 --- /dev/null +++ b/src/frontend/src/constants/countryCode.ts @@ -0,0 +1,103 @@ +/* eslint-disable import/prefer-default-export */ +export const countriesWithPhoneCodes = [ + { + id: 1, + label: 'Nepal', + value: '+977', + }, + { + id: 2, + label: 'United States', + value: '+1', + }, + { + id: 3, + label: 'United Kingdom', + value: '+44', + }, + { + id: 4, + label: 'India', + value: '+91', + }, + { + id: 5, + label: 'China', + value: '+86', + }, + { + id: 6, + label: 'Bangladesh', + value: '+880', + }, + { + id: 7, + label: 'Bhutan', + value: '+975', + }, + { + id: 8, + label: 'Pakistan', + value: '+92', + }, + { + id: 9, + label: 'Myanmar', + value: '+95', + }, + { + id: 10, + label: 'Sri Lanka', + value: '+94', + }, + { + id: 11, + label: 'Canada', + value: '+1', + }, + { + id: 12, + label: 'Mexico', + value: '+52', + }, + { + id: 13, + label: 'Ireland', + value: '+353', + }, + { + id: 14, + label: 'France', + value: '+33', + }, + { + id: 15, + label: 'Mongolia', + value: '+976', + }, + { + id: 16, + label: 'Russia', + value: '+7', + }, + { + id: 17, + label: 'Kazakhstan', + value: '+7', + }, + { + id: 18, + label: 'Kyrgyzstan', + value: '+996', + }, + { + id: 19, + label: 'Tajikistan', + value: '+992', + }, + { + id: 20, + label: 'Afghanistan', + value: '+93', + }, +]; diff --git a/src/frontend/src/constants/index.ts b/src/frontend/src/constants/index.ts index f48c75b5..e384cb8a 100644 --- a/src/frontend/src/constants/index.ts +++ b/src/frontend/src/constants/index.ts @@ -29,3 +29,35 @@ export const tabOptions = [ value: 3, }, ]; + +export const droneOperatorOptions = [ + { + name: 'Yes', + value: 'yes', + label: 'Yes', + }, + { + name: 'No', + value: 'no', + label: 'No', + }, +]; + +// keys only present in project creator form +export const projectCreatorKeys = [ + 'organization_name', + 'organization_address', + 'job_title', + 'confirm_password', + 'country_code', +]; + +// keys only present in drone operator form +export const droneOperatorKeys = [ + 'notify_for_projects_within_km', + 'experience_years', + 'certified_drone_operator', + 'drone_you_own', + 'confirm_password', + 'country_code', +]; diff --git a/src/frontend/src/modules/user-auth-module/src/components/Authentication/Login/index.tsx b/src/frontend/src/modules/user-auth-module/src/components/Authentication/Login/index.tsx index 01ea5428..25ffa35e 100644 --- a/src/frontend/src/modules/user-auth-module/src/components/Authentication/Login/index.tsx +++ b/src/frontend/src/modules/user-auth-module/src/components/Authentication/Login/index.tsx @@ -11,7 +11,7 @@ import { Button } from '@Components/RadixComponents/Button'; import Icon from '@Components/common/Icon'; import { Flex, FlexRow } from '@Components/common/Layouts'; import Person from '@Assets/images/person.svg'; -import { useTypedDispatch, useTypedSelector } from '@Store/hooks'; +import { useTypedDispatch } from '@Store/hooks'; import { signInGoogle, signInUser } from '@Services/common'; import { setUserState } from '@UserModule/store/actions/user'; import googleIcon from '@Assets/images/google-icon.svg'; @@ -32,7 +32,8 @@ export default function Login() { const handleShow = () => { return setShowPassword(prev => !prev); }; - const signInAs = useTypedSelector(state => state.common.signInAs); + + const signedInAs = localStorage.getItem('signedInAs') || 'Project Creator'; const { mutate, isLoading } = useMutation({ mutationFn: signInUser, @@ -72,7 +73,7 @@ export default function Login() { className="naxatw-h-screen naxatw-w-full naxatw-flex-col naxatw-items-center naxatw-justify-center" > -

{signInAs}

+

{signedInAs}

{/* google login button */}
api.post('/users/login/', data); @@ -11,8 +11,11 @@ export const logoutUser = () => api.post('/user/logout/'); export const postUserProfile = ({ userId, - userProfile, + data, }: { userId: number; - userProfile: UserProfileDetailsType; -}) => api.post(`/users/${userId}/profile`, { data: userProfile }); + data: UserProfileDetailsType; +}) => + authenticated(api).post(`/users/${userId}/profile`, data, { + headers: { 'Content-Type': 'application/json' }, + }); diff --git a/src/frontend/src/services/index.ts b/src/frontend/src/services/index.ts index f9279f76..b1bda49e 100644 --- a/src/frontend/src/services/index.ts +++ b/src/frontend/src/services/index.ts @@ -19,9 +19,9 @@ export const authenticated = (apiInstance: AxiosInstance) => { const token = localStorage.getItem('token'); if (!token) return apiInstance; if (process.env.NODE_ENV === 'development') { - apiInstance.defaults.headers.common.Authorization = `Token ${token}`; + apiInstance.defaults.headers.common['Access-Token'] = `${token}`; } else { - apiInstance.defaults.headers.common.Authorization = `Token ${token}`; + apiInstance.defaults.headers.common['Access-Token'] = `${token}`; apiInstance.defaults.withCredentials = false; } return apiInstance; diff --git a/src/frontend/src/store/slices/common.ts b/src/frontend/src/store/slices/common.ts index 9b050e1b..f3574193 100644 --- a/src/frontend/src/store/slices/common.ts +++ b/src/frontend/src/store/slices/common.ts @@ -13,8 +13,8 @@ export interface CommonState { promptDialogContent: PromptDialogContentsType; showMap: boolean; openSignInMenu: boolean; - signInAs: 'Project Creator' | 'Drone Operator'; userProfileActiveTab: number; + isCertifiedDroneUser: 'yes' | 'no'; } const initialState: CommonState = { @@ -24,8 +24,8 @@ const initialState: CommonState = { promptDialogContent: null, showMap: false, openSignInMenu: false, - signInAs: 'Project Creator', userProfileActiveTab: 1, + isCertifiedDroneUser: 'no', }; const setCommonState: CaseReducer< diff --git a/src/frontend/src/utils/index.ts b/src/frontend/src/utils/index.ts index b5bc37a5..e7730caa 100644 --- a/src/frontend/src/utils/index.ts +++ b/src/frontend/src/utils/index.ts @@ -5,3 +5,25 @@ import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +/** + * Removes specified keys from an object. + * + * @param {Object} obj - The original object to remove keys from. + * @param {Array} keysToRemove - An array of key names to be removed from the object. + * @returns {Object} A new object with the specified keys removed. + * + * @example + * const originalObj = { a: 1, b: 2, c: 3, d: 4 }; + * const keysToRemove = ['b', 'd']; + * const result = removeKeys(originalObj, keysToRemove); + * // result is { a: 1, c: 3 } + */ +export function removeKeysFromObject( + obj: Record, + keysToRemove: any[], +) { + return Object.fromEntries( + Object.entries(obj).filter(([key]) => !keysToRemove.includes(key)), + ); +} diff --git a/src/frontend/src/views/UserProfile/UserProfileTabs/index.tsx b/src/frontend/src/views/UserProfile/UserProfileTabs/index.tsx index 50125b3b..701d9771 100644 --- a/src/frontend/src/views/UserProfile/UserProfileTabs/index.tsx +++ b/src/frontend/src/views/UserProfile/UserProfileTabs/index.tsx @@ -13,9 +13,15 @@ interface TabProps { tabOptions: ITabOptions[]; onTabChange: (index: number) => void; activeTab?: number; + clickable?: boolean; } -const Tab: React.FC = ({ tabOptions, onTabChange, activeTab }) => { +const Tab: React.FC = ({ + tabOptions, + onTabChange, + activeTab, + clickable = false, +}) => { const [activeTabx, setActiveTab] = useState(activeTab); useEffect(() => { @@ -23,6 +29,7 @@ const Tab: React.FC = ({ tabOptions, onTabChange, activeTab }) => { }, [activeTab]); const handleTabClick = (index: number) => { + if (!clickable) return; setActiveTab(index); onTabChange(index); }; diff --git a/src/frontend/src/views/UserProfile/index.tsx b/src/frontend/src/views/UserProfile/index.tsx index 3598ab33..53806020 100644 --- a/src/frontend/src/views/UserProfile/index.tsx +++ b/src/frontend/src/views/UserProfile/index.tsx @@ -1,27 +1,42 @@ import { useTypedDispatch, useTypedSelector } from '@Store/hooks'; +import { useNavigate } from 'react-router-dom'; import { UserProfileHeader } from '@Components/UserProfile'; import { useForm } from 'react-hook-form'; -import { tabOptions } from '@Constants/index'; -import { setCommonState } from '@Store/actions/common'; -import { Button } from '@Components/RadixComponents/Button'; import { BasicDetails, OrganizationDetails, - // PasswordSection, + OtherDetails, + PasswordSection, } from '@Components/UserProfile/FormContents'; +import { + tabOptions, + projectCreatorKeys, + droneOperatorKeys, +} from '@Constants/index'; +import { setCommonState } from '@Store/actions/common'; +import { Button } from '@Components/RadixComponents/Button'; import { useMutation } from '@tanstack/react-query'; import { postUserProfile } from '@Services/common'; -import { IUserProfileDetailsType } from '@Components/GoogleAuth/types'; +import { toast } from 'react-toastify'; +import { removeKeysFromObject } from '@Utils/index'; import Tab from './UserProfileTabs'; -const getActiveFormContent = (activeTab: number, formProps: any) => { +const getActiveFormContent = ( + activeTab: number, + userType: string, + formProps: any, +) => { switch (activeTab) { case 1: return ; case 2: - return ; - // case 3: - // return ; + return userType === 'Project Creator' ? ( + + ) : ( + + ); + case 3: + return ; default: return <>; } @@ -29,26 +44,39 @@ const getActiveFormContent = (activeTab: number, formProps: any) => { export default function UserProfile() { const dispatch = useTypedDispatch(); + const navigate = useNavigate(); + + const signedInAs = localStorage.getItem('signedInAs') || 'Project Creator'; + const isDroneOperator = + localStorage.getItem('signedInAs') === 'Drone Operator'; const userProfileActiveTab = useTypedSelector( state => state.common.userProfileActiveTab, ); - const userProfile = localStorage.getItem( - 'userprofile', - ) as IUserProfileDetailsType; + + const userProfileString = localStorage.getItem('userprofile'); + const userProfile = userProfileString && JSON.parse(userProfileString); const initialState = { - name: null, - country: null, + name: '', + country: '', city: null, + password: null, + confirm_password: null, + phone_number: null, + // for project creators organization_name: null, organization_address: null, job_title: null, - password: null, - confirm_password: null, + // for drone operators + notify_for_projects_within_km: null, + experience_years: null, + certified_drone_operator: false, + drone_you_own: null, + role: isDroneOperator ? 2 : 1, }; - const { register, setValue, handleSubmit, formState } = useForm({ + const { register, setValue, handleSubmit, formState, control } = useForm({ defaultValues: initialState, }); @@ -56,36 +84,34 @@ export default function UserProfile() { register, setValue, formState, + control, }; const { mutate: updateUserProfile } = useMutation({ - mutationFn: postUserProfile, + mutationFn: payloadDataObject => postUserProfile(payloadDataObject), onSuccess: () => { - alert('updated'); - // toast.success('UserProfile Updated Successfully'); + toast.success('UserProfile Updated Successfully'); + navigate('/projects'); }, onError: err => { // eslint-disable-next-line no-console console.log(err); - // toast.error(err.message); }, }); - const onSubmit = (data: any) => { - updateUserProfile(userProfile?.id, data); - // if (userProfileActiveTab < 3) return; - // createProject(data); - // reset(); - // alert('test'); - }; - const onNextBtnClick = () => { - handleSubmit(onSubmit)(); - if (Object.keys(formState.errors).length > 0) return; - if (userProfileActiveTab === 2) return; - dispatch( - setCommonState({ userProfileActiveTab: userProfileActiveTab + 1 }), - ); + const onSubmit = (formData: Record) => { + if (userProfileActiveTab !== 3) { + dispatch( + setCommonState({ userProfileActiveTab: userProfileActiveTab + 1 }), + ); + return; + } + const finalFormData = isDroneOperator + ? removeKeysFromObject(formData, projectCreatorKeys) + : removeKeysFromObject(formData, droneOperatorKeys); + updateUserProfile({ userId: userProfile?.id, data: finalFormData }); }; + const onBackBtnClick = () => { if (userProfileActiveTab === 1) return; dispatch( @@ -96,36 +122,39 @@ export default function UserProfile() { return (
-
-
+
+
{ - dispatch(setCommonState({ userProfileActiveTab: info })); - }} + onTabChange={() => {}} tabOptions={tabOptions} activeTab={userProfileActiveTab} />
- {getActiveFormContent(userProfileActiveTab, formProps)} + {getActiveFormContent(userProfileActiveTab, signedInAs, formProps)} {userProfileActiveTab !== 1 && ( )}