From 208229e86392ca49b2a7ee22965d77491b9dceec Mon Sep 17 00:00:00 2001 From: Ochieng Paul Date: Sat, 8 Mar 2025 17:42:40 +0300 Subject: [PATCH 1/4] Enhancements on JSON errors --- .../Dropdowns/OrganizationDropdown.jsx | 30 +- .../dataDownload/modules/AddLocations.jsx | 14 +- .../Modal/dataDownload/modules/SelectMore.jsx | 14 +- .../components/Settings/Teams/InviteForm.jsx | 100 +++++-- .../SideBar/AuthenticatedSidebar.jsx | 24 +- .../components/SideBar/SideBarDrawer.jsx | 18 +- src/platform/src/core/utils/protectedRoute.js | 30 +- src/platform/src/pages/Home/index.jsx | 35 ++- .../settings/Tabs/OrganizationProfile.jsx | 238 +++++++++------ .../src/pages/settings/Tabs/Profile.jsx | 275 ++++++++++++------ src/platform/src/pages/settings/index.jsx | 52 ++-- 11 files changed, 579 insertions(+), 251 deletions(-) diff --git a/src/platform/src/common/components/Dropdowns/OrganizationDropdown.jsx b/src/platform/src/common/components/Dropdowns/OrganizationDropdown.jsx index f69a688875..32e875f188 100644 --- a/src/platform/src/common/components/Dropdowns/OrganizationDropdown.jsx +++ b/src/platform/src/common/components/Dropdowns/OrganizationDropdown.jsx @@ -38,17 +38,39 @@ const OrganizationDropdown = () => { // Initialize active group if missing useEffect(() => { + // If we're still fetching, do nothing yet if (isFetchingActiveGroup) return; + const storedGroup = localStorage.getItem('activeGroup'); if (storedGroup) { - const defaultGroup = JSON.parse(storedGroup); - dispatch(setOrganizationName(defaultGroup.grp_title)); + try { + // Attempt to parse the stored group + const defaultGroup = JSON.parse(storedGroup); + + // Check if defaultGroup and its properties exist + if (defaultGroup && defaultGroup.grp_title) { + dispatch(setOrganizationName(defaultGroup.grp_title)); + } else { + // If the stored data is missing expected fields, remove it + localStorage.removeItem('activeGroup'); + console.warn( + 'activeGroup in localStorage is missing grp_title, removing it...', + ); + } + } catch (error) { + // If JSON parsing fails, remove the invalid item + console.error('Error parsing activeGroup from localStorage:', error); + localStorage.removeItem('activeGroup'); + } } else if (!activeGroupId && activeGroups.length > 0) { + // No activeGroup in localStorage, so pick the first available group const defaultGroup = activeGroups[0]; localStorage.setItem('activeGroup', JSON.stringify(defaultGroup)); - dispatch(setOrganizationName(defaultGroup.grp_title)); + if (defaultGroup && defaultGroup.grp_title) { + dispatch(setOrganizationName(defaultGroup.grp_title)); + } } - }, [activeGroupId, activeGroups, dispatch]); + }, [isFetchingActiveGroup, activeGroupId, activeGroups, dispatch]); const handleUpdatePreferences = useCallback( async (group) => { diff --git a/src/platform/src/common/components/Modal/dataDownload/modules/AddLocations.jsx b/src/platform/src/common/components/Modal/dataDownload/modules/AddLocations.jsx index 787c672f05..122e45bf5b 100644 --- a/src/platform/src/common/components/Modal/dataDownload/modules/AddLocations.jsx +++ b/src/platform/src/common/components/Modal/dataDownload/modules/AddLocations.jsx @@ -60,8 +60,18 @@ const AddLocations = ({ onClose }) => { // Retrieve user ID from localStorage and memoize it const userID = useMemo(() => { - const user = localStorage.getItem('loggedUser'); - return user ? JSON.parse(user)?._id : null; + const storedUser = localStorage.getItem('loggedUser'); + if (!storedUser) { + return null; + } + + try { + const parsedUser = JSON.parse(storedUser); + return parsedUser?._id ?? null; + } catch (error) { + console.error('Error parsing loggedUser from localStorage:', error); + return null; + } }, []); /** diff --git a/src/platform/src/common/components/Modal/dataDownload/modules/SelectMore.jsx b/src/platform/src/common/components/Modal/dataDownload/modules/SelectMore.jsx index 89bb6c6f30..2b8a6570b7 100644 --- a/src/platform/src/common/components/Modal/dataDownload/modules/SelectMore.jsx +++ b/src/platform/src/common/components/Modal/dataDownload/modules/SelectMore.jsx @@ -60,8 +60,18 @@ const SelectMore = ({ onClose }) => { // Retrieve user ID from localStorage and memoize it const userID = useMemo(() => { - const user = localStorage.getItem('loggedUser'); - return user ? JSON.parse(user)?._id : null; + const storedUser = localStorage.getItem('loggedUser'); + if (!storedUser) { + return null; + } + + try { + const parsedUser = JSON.parse(storedUser); + return parsedUser?._id ?? null; + } catch (error) { + console.error('Error parsing "loggedUser" from localStorage:', error); + return null; + } }, []); // Extract selected site IDs from user preferences diff --git a/src/platform/src/common/components/Settings/Teams/InviteForm.jsx b/src/platform/src/common/components/Settings/Teams/InviteForm.jsx index 82730159e2..73c80b22d3 100644 --- a/src/platform/src/common/components/Settings/Teams/InviteForm.jsx +++ b/src/platform/src/common/components/Settings/Teams/InviteForm.jsx @@ -61,7 +61,8 @@ const TeamInviteForm = ({ open, closeModal }) => { const handleSubmit = (e) => { e.preventDefault(); - if (emails[0] === '') { + // Basic validation: check if the first email input is empty + if (!emails[0]) { setIsError({ isError: true, message: 'Please enter an email', @@ -71,45 +72,84 @@ const TeamInviteForm = ({ open, closeModal }) => { } setLoading(true); + + // Check if all emails are valid const isValid = emails.every((email) => isValidEmail(email)); if (isValid) { + // Safely retrieve and parse 'activeGroup' from localStorage + const storedActiveGroup = localStorage.getItem('activeGroup'); + if (!storedActiveGroup) { + // If it's not found, handle accordingly + setIsError({ + isError: true, + message: 'No active group found in localStorage', + type: 'error', + }); + setLoading(false); + return; + } + + let activeGroup; try { - const activeGroup = JSON.parse(localStorage.getItem('activeGroup')); - if (!activeGroup) { - throw new Error('No active group found'); - } - inviteUserToGroupTeam(activeGroup._id, emails) - .then((response) => { - setIsError({ - isError: true, - message: response.message, - type: 'success', - }); - - setTimeout(() => { - setLoading(false); - setEmails(['']); - setEmailErrors([]); - closeModal(); - }, 3000); - }) - .catch((error) => { - setIsError({ - isError: true, - message: error?.response?.data?.errors?.message, - type: 'error', - }); - setLoading(false); - }); + activeGroup = JSON.parse(storedActiveGroup); } catch (error) { - console.error(error); + console.error('Error parsing "activeGroup" from localStorage:', error); + setIsError({ + isError: true, + message: 'Invalid data in localStorage for "activeGroup"', + type: 'error', + }); + // Optionally remove the invalid item to prevent repeated errors + // localStorage.removeItem('activeGroup'); + setLoading(false); + return; + } + + if (!activeGroup || !activeGroup._id) { + setIsError({ + isError: true, + message: 'No valid active group found', + type: 'error', + }); setLoading(false); return; } + + // Proceed with your invite action + inviteUserToGroupTeam(activeGroup._id, emails) + .then((response) => { + setIsError({ + isError: true, + message: response.message, + type: 'success', + }); + + setTimeout(() => { + setLoading(false); + setEmails(['']); + setEmailErrors([]); + closeModal(); + }, 3000); + }) + .catch((error) => { + setIsError({ + isError: true, + message: + error?.response?.data?.errors?.message || 'Something went wrong', + type: 'error', + }); + setLoading(false); + }); } else { - // Display an error message or handle invalid emails + // Handle invalid emails + setLoading(false); console.log('Invalid emails:', emailErrors); + setIsError({ + isError: true, + message: 'One or more emails are invalid', + type: 'error', + }); } }; diff --git a/src/platform/src/common/components/SideBar/AuthenticatedSidebar.jsx b/src/platform/src/common/components/SideBar/AuthenticatedSidebar.jsx index 248f4bdd24..d4e2d5f978 100644 --- a/src/platform/src/common/components/SideBar/AuthenticatedSidebar.jsx +++ b/src/platform/src/common/components/SideBar/AuthenticatedSidebar.jsx @@ -72,9 +72,27 @@ const AuthenticatedSideBar = () => { const collocationOpenState = localStorage.getItem('collocationOpen'); const analyticsOpenState = localStorage.getItem('analyticsOpen'); - if (collocationOpenState) - setCollocationOpen(JSON.parse(collocationOpenState)); - if (analyticsOpenState) setAnalyticsOpen(JSON.parse(analyticsOpenState)); + if (collocationOpenState) { + try { + setCollocationOpen(JSON.parse(collocationOpenState)); + } catch (error) { + console.error( + 'Error parsing "collocationOpen" from localStorage:', + error, + ); + } + } + + if (analyticsOpenState) { + try { + setAnalyticsOpen(JSON.parse(analyticsOpenState)); + } catch (error) { + console.error( + 'Error parsing "analyticsOpen" from localStorage:', + error, + ); + } + } }, []); // Save dropdown states to localStorage diff --git a/src/platform/src/common/components/SideBar/SideBarDrawer.jsx b/src/platform/src/common/components/SideBar/SideBarDrawer.jsx index 0c8f97eb36..5be505e844 100644 --- a/src/platform/src/common/components/SideBar/SideBarDrawer.jsx +++ b/src/platform/src/common/components/SideBar/SideBarDrawer.jsx @@ -25,9 +25,21 @@ const SideBarDrawer = () => { const router = useRouter(); const userInfo = useSelector((state) => state.login.userInfo); const [isLoading, setIsLoading] = useState(false); - const [collocationOpen, setCollocationOpen] = useState(() => - JSON.parse(localStorage.getItem('collocationOpen') || 'false'), - ); + const [collocationOpen, setCollocationOpen] = useState(() => { + try { + const storedValue = localStorage.getItem('collocationOpen'); + if (!storedValue || storedValue === 'undefined') { + return false; + } + return JSON.parse(storedValue); + } catch (error) { + console.error( + 'Error parsing "collocationOpen" from localStorage:', + error, + ); + return false; + } + }); const drawerClasses = useMemo( () => (togglingDrawer ? 'w-72' : 'w-0'), diff --git a/src/platform/src/core/utils/protectedRoute.js b/src/platform/src/core/utils/protectedRoute.js index 7df4e81298..902c9f2296 100644 --- a/src/platform/src/core/utils/protectedRoute.js +++ b/src/platform/src/core/utils/protectedRoute.js @@ -35,11 +35,20 @@ export const withPermission = (Component, requiredPermission) => { useEffect(() => { if (typeof window !== 'undefined') { const storedUserGroup = localStorage.getItem('activeGroup'); - const parsedUserGroup = storedUserGroup - ? JSON.parse(storedUserGroup) - : {}; - const currentRole = parsedUserGroup?.role; + let parsedUserGroup = {}; + + if (storedUserGroup) { + try { + parsedUserGroup = JSON.parse(storedUserGroup); + } catch (error) { + console.error( + 'Error parsing "activeGroup" from localStorage:', + error, + ); + } + } + const currentRole = parsedUserGroup?.role; const hasPermission = currentRole?.role_permissions?.some( (permission) => permission.permission === requiredPermission, ); @@ -57,13 +66,22 @@ export const withPermission = (Component, requiredPermission) => { export const checkAccess = (requiredPermission) => { if (requiredPermission && typeof window !== 'undefined') { const storedGroupObj = localStorage.getItem('activeGroup'); - const currentRole = storedGroupObj ? JSON.parse(storedGroupObj).role : null; + let currentRole = null; + + if (storedGroupObj) { + try { + const parsedGroup = JSON.parse(storedGroupObj); + currentRole = parsedGroup?.role || null; + } catch (error) { + console.error('Error parsing "activeGroup" from localStorage:', error); + } + } const permissions = currentRole?.role_permissions?.map( (item) => item.permission, ); - return permissions?.includes(requiredPermission) ?? false; } + return false; }; diff --git a/src/platform/src/pages/Home/index.jsx b/src/platform/src/pages/Home/index.jsx index d2457eb2cf..b5d9f6775a 100644 --- a/src/platform/src/pages/Home/index.jsx +++ b/src/platform/src/pages/Home/index.jsx @@ -58,6 +58,28 @@ const createSteps = (handleModal, handleCardClick) => [ }, ]; +const useUserData = () => { + const userData = useMemo(() => { + if (typeof window === 'undefined') { + return null; + } + + const storedUser = localStorage.getItem('loggedUser'); + if (!storedUser || storedUser === 'undefined') { + return null; + } + + try { + return JSON.parse(storedUser); + } catch (error) { + console.error('Error parsing "loggedUser" from localStorage:', error); + return null; + } + }, []); + + return userData; +}; + const Home = () => { const dispatch = useDispatch(); @@ -72,18 +94,7 @@ const Home = () => { const totalSteps = 4; // Safely retrieve user data from localStorage - const userData = useMemo(() => { - if (typeof window !== 'undefined') { - const storedUser = localStorage.getItem('loggedUser'); - try { - return storedUser ? JSON.parse(storedUser) : null; - } catch (error) { - console.error('Error parsing user data from localStorage:', error); - return null; - } - } - return null; - }, []); + const userData = useUserData(); // Handlers const handleModal = useCallback(() => { diff --git a/src/platform/src/pages/settings/Tabs/OrganizationProfile.jsx b/src/platform/src/pages/settings/Tabs/OrganizationProfile.jsx index 09b6551f50..d0258cb28a 100644 --- a/src/platform/src/pages/settings/Tabs/OrganizationProfile.jsx +++ b/src/platform/src/pages/settings/Tabs/OrganizationProfile.jsx @@ -83,19 +83,30 @@ const OrganizationProfile = () => { useEffect(() => { setLoading(true); + + let activeGroupId = null; const storedActiveGroup = localStorage.getItem('activeGroup'); - const storedActiveGroupID = - storedActiveGroup && JSON.parse(storedActiveGroup)._id; - // get group information - try { - dispatch(fetchGroupInfo(storedActiveGroupID)); - } catch (error) { - console.error(`Error fetching group info: ${error}`); - } finally { - setLoading(false); + if (storedActiveGroup) { + try { + const parsedActiveGroup = JSON.parse(storedActiveGroup); + activeGroupId = parsedActiveGroup?._id || null; + } catch (error) { + console.error('Error parsing "activeGroup" from localStorage:', error); + } } - }, []); + + // If we have a valid ID, fetch group info + if (activeGroupId) { + try { + dispatch(fetchGroupInfo(activeGroupId)); + } catch (error) { + console.error(`Error fetching group info: ${error}`); + } + } + + setLoading(false); + }, [dispatch]); useEffect(() => { if (orgInfo) { @@ -118,19 +129,36 @@ const OrganizationProfile = () => { const handleSubmit = (e) => { e.preventDefault(); setLoading(true); + + // Safely parse 'activeGroup' from localStorage + let activeGroupId = null; const storedActiveGroup = localStorage.getItem('activeGroup'); - const storedActiveGroupID = - storedActiveGroup && JSON.parse(storedActiveGroup)._id; - if (!storedActiveGroupID) { + + if (storedActiveGroup) { + try { + const parsedGroup = JSON.parse(storedActiveGroup); + activeGroupId = parsedGroup?._id || null; + } catch (error) { + console.error('Error parsing "activeGroup" from localStorage:', error); + + setLoading(false); + return; + } + } + + // If no valid ID, stop here + if (!activeGroupId) { setLoading(false); return; } + try { - updateGroupDetailsApi(storedActiveGroupID, orgData) - .then((response) => { + // Update group details with the parsed ID + updateGroupDetailsApi(activeGroupId, orgData) + .then(() => { try { - dispatch(fetchGroupInfo(storedActiveGroupID)); - + // Fetch updated group info + dispatch(fetchGroupInfo(activeGroupId)); setIsError({ isError: true, message: 'Organization details successfully updated', @@ -152,6 +180,7 @@ const OrganizationProfile = () => { setLoading(false); }); } catch (error) { + // Catch any unexpected errors in the try block console.error(`Error updating user cloudinary photo: ${error}`); setIsError({ isError: true, @@ -235,7 +264,7 @@ const OrganizationProfile = () => { setUpdatedProfilePicture(croppedUrl); setOrgData({ ...orgData, grp_image: croppedUrl }); }) - .catch((error) => { + .catch(() => { setIsError({ isError: true, message: 'Something went wrong', @@ -245,78 +274,125 @@ const OrganizationProfile = () => { }; const handleProfileImageUpdate = async () => { - if (updatedProfilePicture) { - const formData = new FormData(); - formData.append('file', updatedProfilePicture); - formData.append( - 'upload_preset', - process.env.NEXT_PUBLIC_CLOUDINARY_PRESET, - ); - formData.append('folder', 'organization_profiles'); - - setProfileUploading(true); - await cloudinaryImageUpload(formData) - .then(async (responseData) => { - setOrgData({ ...orgData, grp_image: responseData.secure_url }); - const storedActiveGroup = localStorage.getItem('activeGroup'); - const storedActiveGroupID = - storedActiveGroup && JSON.parse(storedActiveGroup)._id; - - return await updateGroupDetailsApi(storedActiveGroupID, { - grp_image: responseData.secure_url, - }) - .then((responseData) => { - try { - dispatch(fetchGroupInfo(storedActiveGroupID)); - // updated user alert - setIsError({ - isError: true, - message: 'Organization image successfully added', - type: 'success', - }); - setUpdatedProfilePicture(''); - } catch (error) { - console.log(error); - } finally { - setProfileUploading(false); - } - }) - .catch((err) => { - // updated user failure alert - setIsError({ - isError: true, - message: err.message, - type: 'error', - }); - setUpdatedProfilePicture(''); - setProfileUploading(false); - }); - }) - .catch((err) => { - // unable to save image error - setUpdatedProfilePicture(''); - setProfileUploading(false); - setIsError({ - isError: true, - message: err.message, - type: 'error', - }); + if (!updatedProfilePicture) return; + + const formData = new FormData(); + formData.append('file', updatedProfilePicture); + formData.append('upload_preset', process.env.NEXT_PUBLIC_CLOUDINARY_PRESET); + formData.append('folder', 'organization_profiles'); + + setProfileUploading(true); + + try { + // 1. Upload the image to Cloudinary + const responseData = await cloudinaryImageUpload(formData); + + // 2. Update the orgData state with the new image URL + setOrgData((prev) => ({ + ...prev, + grp_image: responseData.secure_url, + })); + + // 3. Safely parse 'activeGroup' from localStorage + let activeGroupId = null; + const storedActiveGroup = localStorage.getItem('activeGroup'); + if (storedActiveGroup) { + try { + const parsedGroup = JSON.parse(storedActiveGroup); + activeGroupId = parsedGroup?._id || null; + } catch (error) { + console.error( + 'Error parsing "activeGroup" from localStorage:', + error, + ); + } + } + + // If there's no valid ID, stop here + if (!activeGroupId) { + setProfileUploading(false); + setIsError({ + isError: true, + message: 'No valid group ID found in localStorage.', + type: 'error', + }); + return; + } + + // 4. Update group details with the new image + try { + await updateGroupDetailsApi(activeGroupId, { + grp_image: responseData.secure_url, + }); + + // 5. Fetch updated group info + dispatch(fetchGroupInfo(activeGroupId)); + + // 6. Show success message + setIsError({ + isError: true, + message: 'Organization image successfully added', + type: 'success', + }); + + // 7. Reset local states + setUpdatedProfilePicture(''); + } catch (error) { + console.error('Error updating organization details:', error); + setIsError({ + isError: true, + message: error.message, + type: 'error', }); + setUpdatedProfilePicture(''); + } finally { + setProfileUploading(false); + } + } catch (error) { + // Handle any error from cloudinaryImageUpload + console.error('Error uploading to Cloudinary:', error); + setUpdatedProfilePicture(''); + setProfileUploading(false); + setIsError({ + isError: true, + message: error.message, + type: 'error', + }); } }; const deleteProfileImage = () => { + // Reset local states setUpdatedProfilePicture(''); - setOrgData({ ...orgData, grp_image: '' }); + setOrgData((prev) => ({ ...prev, grp_image: '' })); + // Safely retrieve and parse 'activeGroup' + let activeGroupId = null; const storedActiveGroup = localStorage.getItem('activeGroup'); - const storedActiveGroupID = - storedActiveGroup && JSON.parse(storedActiveGroup)._id; + if (storedActiveGroup) { + try { + const parsedGroup = JSON.parse(storedActiveGroup); + activeGroupId = parsedGroup?._id || null; + } catch (error) { + console.error('Error parsing "activeGroup" from localStorage:', error); + } + } + + // If no valid ID, we can’t proceed with API call + if (!activeGroupId) { + setIsError({ + isError: true, + message: 'No valid group ID found in localStorage.', + type: 'error', + }); + return; + } - updateGroupDetailsApi(storedActiveGroupID, { grp_image: '' }) - .then((response) => { + // Update group details with an empty image + updateGroupDetailsApi(activeGroupId, { grp_image: '' }) + .then(() => { try { - dispatch(fetchGroupInfo(storedActiveGroupID)); + dispatch(fetchGroupInfo(activeGroupId)); setShowDeleteProfileModal(false); setIsError({ isError: true, @@ -324,7 +400,7 @@ const OrganizationProfile = () => { type: 'success', }); } catch (error) { - console.log(error); + console.log('Error fetching group info:', error); } }) .catch((error) => { diff --git a/src/platform/src/pages/settings/Tabs/Profile.jsx b/src/platform/src/pages/settings/Tabs/Profile.jsx index 0f71043677..82c39450fc 100644 --- a/src/platform/src/pages/settings/Tabs/Profile.jsx +++ b/src/platform/src/pages/settings/Tabs/Profile.jsx @@ -87,23 +87,36 @@ const Profile = () => { }; useEffect(() => { - const user = JSON.parse(localStorage.getItem('loggedUser')); + // Prevent running on the server + if (typeof window === 'undefined') return; + + // Attempt to retrieve the "loggedUser" from localStorage + const storedUser = localStorage.getItem('loggedUser'); + let parsedUser = null; + + if (storedUser && storedUser !== 'undefined') { + try { + parsedUser = JSON.parse(storedUser); + } catch (error) { + console.error('Error parsing "loggedUser" from localStorage:', error); + } + } - if (user) { + // If parsing succeeded and we have user data + if (parsedUser) { if (!userInfo) { - dispatch(setUserInfo(user)); + dispatch(setUserInfo(parsedUser)); } - setUserData({ - firstName: user.firstName || '', - lastName: user.lastName || '', - email: user.email || '', - phone: user.phone || '', - jobTitle: user.jobTitle || '', - country: user.country || '', - timezone: user.timezone || '', - description: user.description || '', - profilePicture: user.profilePicture || '', + firstName: parsedUser.firstName || '', + lastName: parsedUser.lastName || '', + email: parsedUser.email || '', + phone: parsedUser.phone || '', + jobTitle: parsedUser.jobTitle || '', + country: parsedUser.country || '', + timezone: parsedUser.timezone || '', + description: parsedUser.description || '', + profilePicture: parsedUser.profilePicture || '', }); } else { setIsError({ @@ -112,7 +125,7 @@ const Profile = () => { type: 'error', }); } - }, []); + }, [userInfo, dispatch]); const handleChange = (e) => { setUserData({ ...userData, [e.target.id]: e.target.value }); @@ -122,27 +135,49 @@ const Profile = () => { e.preventDefault(); setLoading(true); - const loggedUser = JSON.parse(localStorage.getItem('loggedUser')); + // Safely parse the "loggedUser" from localStorage + let loggedUser = null; + const storedUser = localStorage.getItem('loggedUser'); + + if (storedUser && storedUser !== 'undefined') { + try { + loggedUser = JSON.parse(storedUser); + } catch (error) { + console.error('Error parsing "loggedUser" from localStorage:', error); + + setLoading(false); + return; + } + } + + // If we still don't have a valid user, stop here if (!loggedUser) { setLoading(false); return; } const { _id: userID } = loggedUser; + try { + // 1. Update user creation details await updateUserCreationDetails(userData, userID); + // 2. Retrieve updated user info from the server const res = await getUserDetails(userID, userToken); - const updatedUser = res.users[0]; + const updatedUser = res?.users?.[0]; if (!updatedUser) { throw new Error('User details not updated'); } + // 3. Merge the updated user info with the ID const updatedData = { _id: userID, ...updatedUser }; + + // 4. Store the updated user data in localStorage and Redux localStorage.setItem('loggedUser', JSON.stringify(updatedData)); dispatch(setUserInfo(updatedData)); + // 5. Optional: Check if certain fields are present for profile completion if ( userData.firstName && userData.lastName && @@ -153,6 +188,7 @@ const Profile = () => { handleProfileCompletion(3); } + // 6. Show success message setIsError({ isError: true, message: 'User details successfully updated', @@ -171,17 +207,44 @@ const Profile = () => { }; const handleCancel = () => { - const user = JSON.parse(localStorage.getItem('loggedUser')); + // Safely parse the "loggedUser" from localStorage + let parsedUser = null; + const storedUser = localStorage.getItem('loggedUser'); + if (storedUser && storedUser !== 'undefined') { + try { + parsedUser = JSON.parse(storedUser); + } catch (error) { + console.error('Error parsing "loggedUser" from localStorage:', error); + } + } + + // If no valid user, reset to empty fields or handle accordingly + if (!parsedUser) { + setUserData({ + firstName: '', + lastName: '', + email: '', + phone: '', + jobTitle: '', + country: '', + timezone: '', + description: '', + profilePicture: '', + }); + return; + } + + // Update local state with user data setUserData({ - firstName: user.firstName || '', - lastName: user.lastName || '', - email: user.email || '', - phone: user.phone || '', - jobTitle: user.jobTitle || '', - country: user.country || '', - timezone: user.timezone || '', - description: user.description || '', - profilePicture: user.profilePicture || '', + firstName: parsedUser.firstName || '', + lastName: parsedUser.lastName || '', + email: parsedUser.email || '', + phone: parsedUser.phone || '', + jobTitle: parsedUser.jobTitle || '', + country: parsedUser.country || '', + timezone: parsedUser.timezone || '', + description: parsedUser.description || '', + profilePicture: parsedUser.profilePicture || '', }); }; @@ -246,7 +309,7 @@ const Profile = () => { setUpdatedProfilePicture(croppedUrl); setUserData({ ...userData, profilePicture: croppedUrl }); }) - .catch((error) => { + .catch(() => { setIsError({ isError: true, message: 'Something went wrong', @@ -256,75 +319,111 @@ const Profile = () => { }; const handleProfileImageUpdate = async () => { - if (updatedProfilePicture) { - const formData = new FormData(); - formData.append('file', updatedProfilePicture); - formData.append( - 'upload_preset', - process.env.NEXT_PUBLIC_CLOUDINARY_PRESET, + if (!updatedProfilePicture) return; + + const formData = new FormData(); + formData.append('file', updatedProfilePicture); + formData.append('upload_preset', process.env.NEXT_PUBLIC_CLOUDINARY_PRESET); + formData.append('folder', 'profiles'); + + setProfileUploading(true); + + try { + // 1. Upload to Cloudinary + const responseData = await cloudinaryImageUpload(formData); + + // 2. Update local userData state + setUserData((prev) => ({ + ...prev, + profilePicture: responseData.secure_url, + })); + + // 3. Safely parse "loggedUser" for user ID + let userID = null; + const storedUser = localStorage.getItem('loggedUser'); + if (storedUser && storedUser !== 'undefined') { + try { + const parsedUser = JSON.parse(storedUser); + userID = parsedUser?._id || null; + } catch (error) { + console.error('Error parsing "loggedUser" from localStorage:', error); + // localStorage.removeItem('loggedUser'); + } + } + + if (!userID) { + throw new Error('No valid user ID found in localStorage'); + } + + // 4. Update user details with the new profile picture + await updateUserCreationDetails( + { profilePicture: responseData.secure_url }, + userID, ); - formData.append('folder', 'profiles'); - - setProfileUploading(true); - await cloudinaryImageUpload(formData) - .then(async (responseData) => { - setUserData({ ...userData, profilePicture: responseData.secure_url }); - const userID = JSON.parse(localStorage.getItem('loggedUser'))?._id; - return await updateUserCreationDetails( - { profilePicture: responseData.secure_url }, - userID, - ) - .then((responseData) => { - localStorage.setItem( - 'loggedUser', - JSON.stringify({ _id: userID, ...userData }), - ); - dispatch(setUserInfo({ _id: userID, ...userData })); - // updated user alert - setIsError({ - isError: true, - message: 'Profile image successfully added', - type: 'success', - }); - setUpdatedProfilePicture(''); - setProfileUploading(false); - }) - .catch((err) => { - // updated user failure alert - setIsError({ - isError: true, - message: err.message, - type: 'error', - }); - setUpdatedProfilePicture(''); - setProfileUploading(false); - }); - }) - .catch((err) => { - // unable to save image error - setUpdatedProfilePicture(''); - setProfileUploading(false); - setIsError({ - isError: true, - message: err.message, - type: 'error', - }); - }); + + // 5. Update localStorage and Redux store with new data + const updatedData = { + _id: userID, + ...userData, + profilePicture: responseData.secure_url, + }; + localStorage.setItem('loggedUser', JSON.stringify(updatedData)); + dispatch(setUserInfo(updatedData)); + + // 6. Success message + setIsError({ + isError: true, + message: 'Profile image successfully added', + type: 'success', + }); + + setUpdatedProfilePicture(''); + setProfileUploading(false); + } catch (err) { + console.error('Error uploading/updating profile image:', err); + setUpdatedProfilePicture(''); + setProfileUploading(false); + setIsError({ + isError: true, + message: err.message, + type: 'error', + }); } }; const deleteProfileImage = () => { setUpdatedProfilePicture(''); - setUserData({ ...userData, profilePicture: '' }); + setUserData((prev) => ({ ...prev, profilePicture: '' })); + + // Safely parse "loggedUser" for user ID + let userID = null; + const storedUser = localStorage.getItem('loggedUser'); + if (storedUser && storedUser !== 'undefined') { + try { + const parsedUser = JSON.parse(storedUser); + userID = parsedUser?._id || null; + } catch (error) { + console.error('Error parsing "loggedUser" from localStorage:', error); + } + } - const userID = JSON.parse(localStorage.getItem('loggedUser'))?._id; + if (!userID) { + setIsError({ + isError: true, + message: 'No valid user ID found in localStorage.', + type: 'error', + }); + return; + } + + // Update the user profile image to empty updateUserCreationDetails({ profilePicture: '' }, userID) - .then((response) => { - localStorage.setItem( - 'loggedUser', - JSON.stringify({ ...userData, profilePicture: '', _id: userID }), - ); - dispatch(setUserInfo({ ...userData, profilePicture: '', _id: userID })); + .then(() => { + // Update localStorage and Redux + const updatedData = { ...userData, profilePicture: '', _id: userID }; + localStorage.setItem('loggedUser', JSON.stringify(updatedData)); + dispatch(setUserInfo(updatedData)); + setShowDeleteProfileModal(false); setIsError({ isError: true, diff --git a/src/platform/src/pages/settings/index.jsx b/src/platform/src/pages/settings/index.jsx index 23d2bb91eb..ceb0998c55 100644 --- a/src/platform/src/pages/settings/index.jsx +++ b/src/platform/src/pages/settings/index.jsx @@ -32,36 +32,48 @@ const Settings = () => { useEffect(() => { setLoading(true); + const storedActiveGroup = localStorage.getItem('activeGroup'); - if (!storedActiveGroup) return setLoading(false); + if (!storedActiveGroup) { + setLoading(false); + return; + } + + let parsedActiveGroup = null; + try { + parsedActiveGroup = JSON.parse(storedActiveGroup); + } catch (error) { + console.error('Error parsing "activeGroup" from localStorage:', error); + + setLoading(false); + return; + } - setUserGroup(JSON.parse(storedActiveGroup)); - const activeGroupId = - storedActiveGroup && JSON.parse(storedActiveGroup)._id; + // Now we have a valid parsedActiveGroup object + setUserGroup(parsedActiveGroup); + + const activeGroupId = parsedActiveGroup?._id; const storedUserPermissions = - storedActiveGroup && JSON.parse(storedActiveGroup).role.role_permissions; + parsedActiveGroup?.role?.role_permissions || []; - if (storedUserPermissions && storedUserPermissions.length > 0) { + if (storedUserPermissions.length > 0) { setUserPermissions(storedUserPermissions); } else { setUserPermissions([]); dispatch(setChartTab(0)); } - try { - getAssignedGroupMembers(activeGroupId) - .then((response) => { - setTeamMembers(response.group_members); - }) - .catch((error) => { - console.error(`Error fetching user details: ${error}`); - }); - } catch (error) { - console.error(`Error fetching user details: ${error}`); - } finally { - setLoading(false); - } - }, [userInfo, preferences]); + getAssignedGroupMembers(activeGroupId) + .then((response) => { + setTeamMembers(response.group_members); + }) + .catch((error) => { + console.error(`Error fetching user details: ${error}`); + }) + .finally(() => { + setLoading(false); + }); + }, [userInfo, preferences, dispatch]); return ( From 5e5fe1324e1e582a357f33124502c0ccde8333c7 Mon Sep 17 00:00:00 2001 From: Ochieng Paul Date: Sat, 8 Mar 2025 18:46:06 +0300 Subject: [PATCH 2/4] updates --- .../dataDownload/components/CustomFields.jsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/platform/src/common/components/Modal/dataDownload/components/CustomFields.jsx b/src/platform/src/common/components/Modal/dataDownload/components/CustomFields.jsx index 4133a7c0aa..ed7ae5cb59 100644 --- a/src/platform/src/common/components/Modal/dataDownload/components/CustomFields.jsx +++ b/src/platform/src/common/components/Modal/dataDownload/components/CustomFields.jsx @@ -20,12 +20,24 @@ const formatName = (name, textFormat = 'lowercase') => { */ const FIELD_FORMAT_RULES = { organization: { - display: (value) => formatName(value.replace(/[_-]/g, ' '), 'uppercase'), + display: (value) => { + // Fallback to an empty string if `value` is null or undefined + const safeValue = typeof value === 'string' ? value : ''; + return formatName(safeValue.replace(/[_-]/g, ' '), 'uppercase'); + }, store: (value) => value, }, default: { - display: (value, textFormat) => formatName(value, textFormat), - store: (value, textFormat) => formatName(value, textFormat), + display: (value, textFormat) => { + // Fallback to an empty string if `value` is null or undefined + const safeValue = typeof value === 'string' ? value : ''; + return formatName(safeValue, textFormat); + }, + store: (value, textFormat) => { + // Same logic here if you need to ensure `value` is a string + const safeValue = typeof value === 'string' ? value : ''; + return formatName(safeValue, textFormat); + }, }, }; From abc6afc7291159ce2b71c91b849283e0794edc40 Mon Sep 17 00:00:00 2001 From: Ochieng Paul Date: Sat, 8 Mar 2025 19:01:13 +0300 Subject: [PATCH 3/4] updates --- .../components/Map/components/MapNodes.js | 217 +++++++++--------- .../sidebar/components/CountryList.jsx | 32 ++- .../sidebar/components/LocationAlertCard.jsx | 17 +- .../sidebar/components/LocationCards.jsx | 26 +-- .../sidebar/components/PollutantCard.jsx | 16 +- .../sidebar/components/Predictions.jsx | 9 +- .../components/SearchResultsSkeleton.jsx | 14 +- .../sidebar/components/Sections.jsx | 9 - .../sidebar/components/SidebarHeader.jsx | 2 - .../sidebar/components/TabSelector.jsx | 6 - .../Map/components/sidebar/index.jsx | 55 ++--- 11 files changed, 162 insertions(+), 241 deletions(-) diff --git a/src/platform/src/common/components/Map/components/MapNodes.js b/src/platform/src/common/components/Map/components/MapNodes.js index 01d4a34a0e..62aad322e2 100644 --- a/src/platform/src/common/components/Map/components/MapNodes.js +++ b/src/platform/src/common/components/Map/components/MapNodes.js @@ -10,30 +10,14 @@ import Invalid from '@/icons/Charts/Invalid'; // icon images export const images = { - GoodAir: `data:image/svg+xml,${encodeURIComponent( - renderToString(), - )}`, - ModerateAir: `data:image/svg+xml,${encodeURIComponent( - renderToString(), - )}`, - UnhealthyForSensitiveGroups: `data:image/svg+xml,${encodeURIComponent( - renderToString(), - )}`, - Unhealthy: `data:image/svg+xml,${encodeURIComponent( - renderToString(), - )}`, - VeryUnhealthy: `data:image/svg+xml,${encodeURIComponent( - renderToString(), - )}`, - Hazardous: `data:image/svg+xml,${encodeURIComponent( - renderToString(), - )}`, - Invalid: `data:image/svg+xml,${encodeURIComponent( - renderToString(), - )}`, - undefined: `data:image/svg+xml,${encodeURIComponent( - renderToString(), - )}`, + GoodAir: `data:image/svg+xml,${encodeURIComponent(renderToString())}`, + ModerateAir: `data:image/svg+xml,${encodeURIComponent(renderToString())}`, + UnhealthyForSensitiveGroups: `data:image/svg+xml,${encodeURIComponent(renderToString())}`, + Unhealthy: `data:image/svg+xml,${encodeURIComponent(renderToString())}`, + VeryUnhealthy: `data:image/svg+xml,${encodeURIComponent(renderToString())}`, + Hazardous: `data:image/svg+xml,${encodeURIComponent(renderToString())}`, + Invalid: `data:image/svg+xml,${encodeURIComponent(renderToString())}`, + undefined: `data:image/svg+xml,${encodeURIComponent(renderToString())}`, }; const markerDetails = { @@ -111,24 +95,31 @@ const colors = { * @returns {Object} */ export const getAQICategory = (pollutant, value) => { - if (!Object.prototype.hasOwnProperty.call(markerDetails, pollutant)) { + if (!markerDetails[pollutant]) { throw new Error(`Invalid pollutant: ${pollutant}`); } const categories = markerDetails[pollutant]; + // Loop through categories assuming they are ordered from highest threshold to lowest for (let i = 0; i < categories.length; i++) { if (value >= categories[i].limit) { return { icon: categories[i].category, - color: colors[categories[i].category], + color: colors[categories[i].category] || colors.undefined, category: categories[i].category, }; } } + // Fallback in case no category matched (should not happen if thresholds are complete) + return { + icon: 'Invalid', + color: colors.Invalid, + category: 'Invalid', + }; }; export const getAQIcon = (pollutant, value) => { - if (!Object.prototype.hasOwnProperty.call(markerDetails, pollutant)) { + if (!markerDetails[pollutant]) { throw new Error(`Invalid pollutant: ${pollutant}`); } @@ -138,41 +129,35 @@ export const getAQIcon = (pollutant, value) => { return categories[i].category; } } + return 'Invalid'; }; export const getAQIMessage = (pollutant, timePeriod, value) => { - if (!Object.prototype.hasOwnProperty.call(markerDetails, pollutant)) { + if (!markerDetails[pollutant]) { throw new Error(`Invalid pollutant: ${pollutant}`); } const aqiCategory = getAQICategory(pollutant, value); - if (aqiCategory?.icon === 'GoodAir') { - return 'Enjoy the day with confidence in the clean air around you.'; - } else if (aqiCategory?.icon === 'ModerateAir') { - return `${ - timePeriod === 'this week' + switch (aqiCategory.icon) { + case 'GoodAir': + return 'Enjoy the day with confidence in the clean air around you.'; + case 'ModerateAir': + return timePeriod === 'this week' ? 'This week is a great time to be outdoors.' - : `${ - timePeriod.charAt(0).toUpperCase() + timePeriod.slice(1) - } is a great day for an outdoor activity.` - }`; - } else if (aqiCategory?.icon === 'UnhealthyForSensitiveGroups') { - return 'Reduce the intensity of your outdoor activities.'; - } else if (aqiCategory?.icon === 'Unhealthy') { - return `${ - timePeriod === 'this week' + : `${timePeriod.charAt(0).toUpperCase() + timePeriod.slice(1)} is a great day for an outdoor activity.`; + case 'UnhealthyForSensitiveGroups': + return 'Reduce the intensity of your outdoor activities.'; + case 'Unhealthy': + return timePeriod === 'this week' ? 'Avoid activities that make you breathe more rapidly. This week is the perfect time to spend indoors reading.' - : `Avoid activities that make you breathe more rapidly. ${ - timePeriod.charAt(0).toUpperCase() + timePeriod.slice(1) - } is the perfect time to spend indoors reading.` - }`; - } else if (aqiCategory?.icon === 'VeryUnhealthy') { - return 'Reduce the intensity of your outdoor activities. Try to stay indoors until the air quality improves.'; - } else if (aqiCategory?.icon === 'Hazardous') { - return 'If you have to spend a lot of time outside, disposable masks like the N95 are helpful.'; - } else { - return ''; + : `Avoid activities that make you breathe more rapidly. ${timePeriod.charAt(0).toUpperCase() + timePeriod.slice(1)} is the perfect time to spend indoors reading.`; + case 'VeryUnhealthy': + return 'Reduce the intensity of your outdoor activities. Try to stay indoors until the air quality improves.'; + case 'Hazardous': + return 'If you have to spend a lot of time outside, disposable masks like the N95 are helpful.'; + default: + return ''; } }; @@ -180,14 +165,16 @@ export const getAQIMessage = (pollutant, timePeriod, value) => { * Create HTML for unClustered nodes * @param {Object} feature * @param {String} NodeType + * @param {String} selectedNode * @returns {String} */ export const UnclusteredNode = ({ feature, NodeType, selectedNode }) => { - if (!feature?.properties?.aqi?.icon) { - console.error('feature.properties.aqi.icon is not defined', feature); + if (!feature?.properties?.aqi) { + console.error('feature.properties.aqi is not defined', feature); return ''; } + // Use a fallback to the 'Invalid' icon if the desired one isn’t available const Icon = images[feature.properties.aqi.icon] || images['Invalid']; const isActive = selectedNode && selectedNode === feature.properties._id ? 'active' : ''; @@ -197,7 +184,7 @@ export const UnclusteredNode = ({ feature, NodeType, selectedNode }) => {
-

${feature.properties.pm2_5.toFixed(2)}

+

${Number(feature.properties.pm2_5)?.toFixed(2) || 'N/A'}

`; @@ -223,65 +210,81 @@ export const UnclusteredNode = ({ feature, NodeType, selectedNode }) => { /** * Create HTML for Clustered nodes - * @param {Object} feature - * @param {String} NodeType + * @param {Object} params + * @param {Object} params.feature + * @param {String} params.NodeType * @returns {String} */ export const createClusterNode = ({ feature, NodeType }) => { - // Get the two most common AQIs from the feature properties + // Validate that feature and expected properties exist + if (!feature || !feature.properties) { + console.error( + 'Invalid feature or feature.properties is undefined', + feature, + ); + return ''; + } + + // Check that feature.properties.aqi exists and is an array with at least 2 elements + if ( + !Array.isArray(feature.properties.aqi) || + feature.properties.aqi.length < 2 + ) { + console.error( + 'feature.properties.aqi is not an array with at least 2 elements', + feature.properties.aqi, + ); + return ''; + } + const [firstAQI, secondAQI] = feature.properties.aqi; - // Get the corresponding colors and icons for the AQIs - const firstColor = colors[firstAQI.aqi.icon]; - const secondColor = colors[secondAQI.aqi.icon]; - const FirstIcon = images[firstAQI.aqi.icon]; - const SecondIcon = images[secondAQI.aqi.icon]; + // Use default fallbacks if any expected nested data is missing + const firstColor = colors[firstAQI?.aqi?.icon] || colors.undefined; + const secondColor = colors[secondAQI?.aqi?.icon] || colors.undefined; + const FirstIcon = images[firstAQI?.aqi?.icon] || images['Invalid']; + const SecondIcon = images[secondAQI?.aqi?.icon] || images['Invalid']; - const firstAQIValue = ( - firstAQI.pm2_5 || - firstAQI.no2 || - firstAQI.pm10 - ).toFixed(2); - const secondAQIValue = ( - secondAQI.pm2_5 || - secondAQI.no2 || - secondAQI.pm10 - ).toFixed(2); + const firstAQIValue = firstAQI.pm2_5 || firstAQI.no2 || firstAQI.pm10; + const secondAQIValue = secondAQI.pm2_5 || secondAQI.no2 || secondAQI.pm10; - // Get the correct count for the nodes in the cluster - const count = feature.properties.point_count; + // Ensure numeric values for display, fallback to "N/A" if missing + const formattedFirstAQI = + typeof firstAQIValue === 'number' ? firstAQIValue.toFixed(2) : 'N/A'; + const formattedSecondAQI = + typeof secondAQIValue === 'number' ? secondAQIValue.toFixed(2) : 'N/A'; + + const count = feature.properties.point_count || 0; const countDisplay = count > 2 ? `${count - 2} + ` : ''; if (NodeType === 'Number' || NodeType === 'Node') { return `
-
${ - NodeType !== 'Node' ? firstAQIValue : '' - }
-
${ - NodeType !== 'Node' ? secondAQIValue : '' - }
+
+ ${NodeType !== 'Node' ? formattedFirstAQI : ''} +
+
+ ${NodeType !== 'Node' ? formattedSecondAQI : ''} +
-
${countDisplay}
`; } return `
- ${FirstIcon} - ${SecondIcon} + AQI Icon + AQI Icon
-
${countDisplay}
+
${countDisplay}
`; }; /** * Create HTML for Popup - * @param {Object} feature - * @param {Object} images + * @param {Object} params + * @param {Object} params.feature + * @param {Object} params.images * @returns {String} */ export const createPopupHTML = ({ feature, images }) => { @@ -290,44 +293,42 @@ export const createPopupHTML = ({ feature, images }) => { return ''; } - // Check if feature.properties.pm2_5 and feature.properties.aqi are defined - if (!feature.properties.pm2_5 || !feature.properties.aqi) { - console.error('Invalid AQI or PM2.5 data'); + // Validate necessary data before proceeding + if (typeof feature.properties.pm2_5 !== 'number' || !feature.properties.aqi) { + console.error('Invalid AQI or PM2.5 data', feature.properties); return ''; } + const formattedDate = new Date( + feature.properties.createdAt, + ).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: '2-digit', + }); + return `
- ${new Date(feature.properties.createdAt).toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: '2-digit', - })} + ${formattedDate}
- ${feature.properties.location} + ${feature.properties.location || 'Unknown Location'}
-
${feature.properties.pm2_5.toFixed( - 2, - )} µg/m³
+
${feature.properties.pm2_5.toFixed(2)} µg/m³
-

- Air Quality is ${feature.properties.airQuality +

+ Air Quality is ${String(feature.properties.airQuality) .replace(/([A-Z])/g, ' $1') .trim()}

- AQI Icon + AQI Icon
`; diff --git a/src/platform/src/common/components/Map/components/sidebar/components/CountryList.jsx b/src/platform/src/common/components/Map/components/sidebar/components/CountryList.jsx index d77d4727b3..ff95c7c28e 100644 --- a/src/platform/src/common/components/Map/components/sidebar/components/CountryList.jsx +++ b/src/platform/src/common/components/Map/components/sidebar/components/CountryList.jsx @@ -1,15 +1,12 @@ import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; +import PropTypes from 'prop-types'; import Button from '@/components/Button'; import { setLocation, addSuggestedSites, } from '@/lib/store/services/map/MapSlice'; -/** - * CountryList - * @description Optimized Country list component - */ const CountryList = ({ siteDetails, data, @@ -18,35 +15,30 @@ const CountryList = ({ }) => { const dispatch = useDispatch(); - // Memoize sorted data to prevent re-sorting on each render + // Memoize sorted data to avoid re-sorting on every render const sortedData = useMemo(() => { - if (!data || !Array.isArray(data) || data.length === 0) return []; + if (!Array.isArray(data) || data.length === 0) return []; return [...data].sort((a, b) => a.country.localeCompare(b.country)); }, [data]); // Handle click event for country selection const handleClick = useCallback( (country) => { - if (!country) return; // Guard clause for invalid country data + if (!country) return; setSelectedCountry(country); - - // Update selected location in the Redux store dispatch(setLocation({ country: country.country, city: '' })); - // Filter and sort siteDetails based on the selected country const selectedSites = siteDetails .filter((site) => site.country === country.country) .sort((a, b) => a.name.localeCompare(b.name)); - // Dispatch filtered sites to the Redux store dispatch(addSuggestedSites(selectedSites)); }, [dispatch, siteDetails, setSelectedCountry], ); - // Show loading skeleton if no data - if (!sortedData.length) { + if (sortedData.length === 0) { return (
{Array.from({ length: 4 }).map((_, index) => ( @@ -62,7 +54,6 @@ const CountryList = ({ return (
{sortedData.map((country, index) => { - // Check if country and flag properties exist if (!country || !country.flag) { return (
@@ -70,7 +61,6 @@ const CountryList = ({
); } - return (
- {children} - -); +
+ {children} +
+ {children} + + ); +}); + +Option.displayName = 'Option'; Option.propTypes = { isSelected: PropTypes.bool.isRequired, @@ -44,7 +66,7 @@ Option.defaultProps = { }; /** - * LayerModal component for selecting map style and details + * LayerModal component for selecting map style and details. */ const LayerModal = ({ isOpen, @@ -58,19 +80,21 @@ const LayerModal = ({ const [selectedStyle, setSelectedStyle] = useState(mapStyles[0]); const [selectedMapDetail, setSelectedMapDetail] = useState(mapDetails[0]); + // Reset selections when the provided options change useEffect(() => { - if (mapStyles[0]) { - setSelectedStyle(mapStyles[0]); - } - if (mapDetails[0]) { - setSelectedMapDetail(mapDetails[0]); - } + if (mapStyles[0]) setSelectedStyle(mapStyles[0]); + if (mapDetails[0]) setSelectedMapDetail(mapDetails[0]); }, [mapStyles, mapDetails]); const handleApply = useCallback(() => { - onStyleSelect(selectedStyle); - onMapDetailsSelect(selectedMapDetail.name); - onClose(); + try { + onStyleSelect(selectedStyle); + onMapDetailsSelect(selectedMapDetail.name); + onClose(); + } catch (error) { + console.error('Error applying layer modal selections:', error); + // Optionally, you can display an error notification here + } }, [ selectedStyle, selectedMapDetail, @@ -79,28 +103,22 @@ const LayerModal = ({ onClose, ]); - const handleSelectStyle = useCallback( - (style) => { - setSelectedStyle(style); - }, - [setSelectedStyle], - ); + const handleSelectStyle = useCallback((style) => { + setSelectedStyle(style); + }, []); - const handleSelectDetail = useCallback( - (detail) => { - setSelectedMapDetail(detail); - }, - [setSelectedMapDetail], - ); + const handleSelectDetail = useCallback((detail) => { + setSelectedMapDetail(detail); + }, []); if (!isOpen) return null; return (
-
+
e.stopPropagation()} - className="relative z-50 bg-white rounded-lg overflow-hidden shadow-xl sm:max-w-lg sm:w-full" + className="relative z-[1000] bg-white rounded-lg overflow-hidden shadow-xl sm:max-w-lg sm:w-full" >

Map Details

@@ -151,7 +169,8 @@ const LayerModal = ({ ); }; -// Add PropTypes for the LayerModal component +LayerModal.displayName = 'LayerModal'; + LayerModal.propTypes = { isOpen: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, diff --git a/src/platform/src/common/components/Map/functions/useLocationBoundaries.jsx b/src/platform/src/common/components/Map/functions/useLocationBoundaries.jsx index 573a5d2d85..8fb7da143f 100644 --- a/src/platform/src/common/components/Map/functions/useLocationBoundaries.jsx +++ b/src/platform/src/common/components/Map/functions/useLocationBoundaries.jsx @@ -1,22 +1,30 @@ +// useLocationBoundaries.jsx import { useEffect } from 'react'; import axios from 'axios'; import { BOUNDARY_URL } from '../data/constants'; const useLocationBoundaries = ({ mapRef, mapData, setLoading }) => { useEffect(() => { + const source = axios.CancelToken.source(); const fetchLocationBoundaries = async () => { - setLoading(true); + if (!mapRef.current) return; const map = mapRef.current; + setLoading(true); - if (!map) return; - - // Remove existing boundaries if any + // Remove existing boundaries safely if (map.getLayer('location-boundaries')) { - map.removeLayer('location-boundaries'); + try { + map.removeLayer('location-boundaries'); + } catch (err) { + console.error('Error removing layer:', err); + } } - if (map.getSource('location-boundaries')) { - map.removeSource('location-boundaries'); + try { + map.removeSource('location-boundaries'); + } catch (err) { + console.error('Error removing source:', err); + } } let queryString = mapData.location.country; @@ -31,18 +39,16 @@ const useLocationBoundaries = ({ mapRef, mapData, setLoading }) => { polygon_geojson: 1, format: 'json', }, + cancelToken: source.token, }); const data = response.data; - if (data && data.length > 0) { const boundaryData = data[0].geojson; - map.addSource('location-boundaries', { type: 'geojson', data: boundaryData, }); - map.addLayer({ id: 'location-boundaries', type: 'fill', @@ -57,10 +63,11 @@ const useLocationBoundaries = ({ mapRef, mapData, setLoading }) => { const { lat: boundaryLat, lon: boundaryLon } = data[0]; map.flyTo({ center: [parseFloat(boundaryLon), parseFloat(boundaryLat)], - zoom: mapData.location.city && mapData.location.country ? 10 : 5, + zoom: mapData.location.city ? 10 : 5, }); - map.on('zoomend', function () { + // Ensure the zoomend event is attached only once + const zoomHandler = () => { const zoom = map.getZoom(); const opacity = zoom > 10 ? 0 : 0.2; map.setPaintProperty( @@ -68,17 +75,32 @@ const useLocationBoundaries = ({ mapRef, mapData, setLoading }) => { 'fill-opacity', opacity, ); - }); + }; + map.on('zoomend', zoomHandler); + + // Cleanup the event listener on unmount + return () => { + map.off('zoomend', zoomHandler); + }; } } catch (error) { - console.error('Error fetching location boundaries:', error); + if (axios.isCancel(error)) { + console.log('Boundary fetch cancelled'); + } else { + console.error('Error fetching location boundaries:', error); + } } finally { setLoading(false); } }; - fetchLocationBoundaries(); - }, [mapData.location]); + const cleanup = fetchLocationBoundaries(); + + return () => { + source.cancel('Component unmounted, cancelling request.'); + if (cleanup instanceof Function) cleanup(); + }; + }, [mapData.location, mapRef, setLoading]); }; export default useLocationBoundaries; diff --git a/src/platform/src/common/components/Map/functions/useMapControls.jsx b/src/platform/src/common/components/Map/functions/useMapControls.jsx index b72bb7f123..c3d2f20fb6 100644 --- a/src/platform/src/common/components/Map/functions/useMapControls.jsx +++ b/src/platform/src/common/components/Map/functions/useMapControls.jsx @@ -1,3 +1,4 @@ +// useMapControls.jsx import React, { useCallback } from 'react'; import GeoIcon from '@/icons/map/gpsIcon'; import PlusIcon from '@/icons/map/plusIcon'; @@ -8,8 +9,7 @@ import { setSelectedNode } from '@/lib/store/services/map/MapSlice'; /** * CustomZoomControl - * @description Custom mapbox zoom control with zoom in and zoom out buttons - * @returns {HTMLElement} container + * Custom mapbox zoom control with zoom in and zoom out buttons */ export class CustomZoomControl { constructor() { @@ -17,16 +17,24 @@ export class CustomZoomControl { this.container = this.createContainer(); this.zoomInButton = this.createButton('Zoom In', , () => { if (this.map) { - this.map.zoomIn(); + try { + this.map.zoomIn(); + } catch (error) { + console.error('Zoom in failed:', error); + } } }); this.zoomOutButton = this.createButton('Zoom Out', , () => { if (this.map) { - this.map.zoomOut(); + try { + this.map.zoomOut(); + } catch (error) { + console.error('Zoom out failed:', error); + } } }); - // Append buttons to the container + // Append buttons and separator to the container this.container.append( this.zoomInButton, this.createSeparator(), @@ -77,18 +85,22 @@ export class CustomZoomControl { return this.container; } - updateUrlWithMapState = () => { + updateUrlWithMapState() { if (!this.map) return; - const center = this.map.getCenter(); - const zoom = this.map.getZoom(); - if (!center || isNaN(zoom)) return; + try { + const center = this.map.getCenter(); + const zoom = this.map.getZoom(); + if (!center || isNaN(zoom)) return; - const url = new URL(window.location); - url.searchParams.set('lat', center.lat.toFixed(4)); - url.searchParams.set('lng', center.lng.toFixed(4)); - url.searchParams.set('zm', zoom.toFixed(2)); - window.history.pushState({}, '', url); - }; + const url = new URL(window.location); + url.searchParams.set('lat', center.lat.toFixed(4)); + url.searchParams.set('lng', center.lng.toFixed(4)); + url.searchParams.set('zm', zoom.toFixed(2)); + window.history.pushState({}, '', url); + } catch (error) { + console.error('Error updating URL:', error); + } + } onRemove() { if (this.map) { @@ -103,14 +115,13 @@ export class CustomZoomControl { /** * CustomGeolocateControl - * @description Custom mapbox geolocate control to find the user's location - * @returns {HTMLElement} container - * @param {Function} setToastMessage - Function to display feedback to the user + * Custom mapbox geolocate control to find the user's location */ export class CustomGeolocateControl { constructor(setToastMessage) { this.map = null; - this.setToastMessage = setToastMessage || (() => {}); + this.setToastMessage = + typeof setToastMessage === 'function' ? setToastMessage : () => {}; this.container = this.createContainer(); this.geolocateButton = this.createButton('Locate Me', , () => { this.locateUser(); @@ -183,7 +194,6 @@ export class CustomGeolocateControl { handleGeolocationSuccess(position) { if (!this.map) return; - const { longitude, latitude } = position.coords; this.setToastMessage({ message: 'Location tracked successfully.', @@ -191,55 +201,59 @@ export class CustomGeolocateControl { bgColor: 'bg-blue-600', }); - this.map.flyTo({ - center: [longitude, latitude], - zoom: 14, - speed: 1, - }); + try { + this.map.flyTo({ + center: [longitude, latitude], + zoom: 14, + speed: 1, + }); - new mapboxgl.Marker().setLngLat([longitude, latitude]).addTo(this.map); - - if (!this.map.getSource('circle-source')) { - this.map.addSource('circle-source', { - type: 'geojson', - data: { - type: 'FeatureCollection', - features: [ - { - type: 'Feature', - geometry: { - type: 'Point', - coordinates: [longitude, latitude], + new mapboxgl.Marker().setLngLat([longitude, latitude]).addTo(this.map); + + if (!this.map.getSource('circle-source')) { + this.map.addSource('circle-source', { + type: 'geojson', + data: { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [longitude, latitude], + }, }, - }, - ], - }, - }); - } + ], + }, + }); + } - if (!this.map.getLayer('circle-layer')) { - this.map.addLayer({ - id: 'circle-layer', - type: 'circle', - source: 'circle-source', - paint: { - 'circle-radius': [ - 'step', - ['zoom'], - 20, - 14, - 50, - 16, - 100, - 18, - 200, - 20, - 400, - ], - 'circle-color': '#0000ff', - 'circle-opacity': 0.2, - }, - }); + if (!this.map.getLayer('circle-layer')) { + this.map.addLayer({ + id: 'circle-layer', + type: 'circle', + source: 'circle-source', + paint: { + 'circle-radius': [ + 'step', + ['zoom'], + 20, + 14, + 50, + 16, + 100, + 18, + 200, + 20, + 400, + ], + 'circle-color': '#0000ff', + 'circle-opacity': 0.2, + }, + }); + } + } catch (error) { + console.error('Error updating map on geolocation success:', error); } } @@ -253,7 +267,7 @@ export class CustomGeolocateControl { } /** - * Function to refresh the map + * Refresh the map style and state. */ export const useRefreshMap = ( setToastMessage, @@ -263,14 +277,12 @@ export const useRefreshMap = ( ) => useCallback(() => { const map = mapRef.current; - if (map) { try { const originalStyle = map.getStyle().sprite.split('/').slice(0, -1).join('/') + '/style.json'; map.setStyle(originalStyle); - setToastMessage({ message: 'Map refreshed successfully', type: 'success', @@ -288,16 +300,21 @@ export const useRefreshMap = ( if (selectedNode) { dispatch(setSelectedNode(null)); } + } else { + setToastMessage({ + message: 'Map reference is not available.', + type: 'error', + bgColor: 'bg-red-600', + }); } }, [mapRef, dispatch, setToastMessage, selectedNode]); /** - * Custom hook to share the current map location by copying the URL with updated parameters. + * Share the current map location by copying the URL with updated parameters. */ export const useShareLocation = (setToastMessage, mapRef) => { return useCallback(async () => { const map = mapRef.current; - if (!map) { setToastMessage({ message: 'Map is not available.', @@ -308,7 +325,6 @@ export const useShareLocation = (setToastMessage, mapRef) => { } try { - // Ensure window and navigator are available (to avoid SSR issues) if (typeof window === 'undefined' || typeof navigator === 'undefined') { setToastMessage({ message: 'This feature is only available in the browser.', @@ -318,25 +334,18 @@ export const useShareLocation = (setToastMessage, mapRef) => { return; } - // Get the current center and zoom level of the map const center = map.getCenter(); const zoom = map.getZoom(); - - // Construct a new URL based on the current location without existing search params const currentUrl = new URL(window.location.href); const baseUrl = `${currentUrl.origin}${currentUrl.pathname}`; const url = new URL(baseUrl); - - // Update or set the search parameters for latitude, longitude, and zoom url.searchParams.set('lat', center.lat.toFixed(4)); url.searchParams.set('lng', center.lng.toFixed(4)); url.searchParams.set('zm', zoom.toFixed(2)); const shareUrl = url.toString(); - // Check if the Clipboard API is available if (navigator.clipboard && navigator.clipboard.writeText) { - // Use the Clipboard API to copy the URL await navigator.clipboard.writeText(shareUrl); setToastMessage({ message: 'Location URL copied to clipboard!', @@ -344,7 +353,6 @@ export const useShareLocation = (setToastMessage, mapRef) => { bgColor: 'bg-blue-600', }); } else { - // Fallback method for browsers that do not support the Clipboard API const textArea = document.createElement('textarea'); textArea.value = shareUrl; textArea.style.position = 'fixed'; @@ -365,7 +373,7 @@ export const useShareLocation = (setToastMessage, mapRef) => { throw new Error('Copy command was unsuccessful'); } } catch (err) { - console.error('Fallback: Oops, unable to copy', err); + console.error('Fallback copy failed:', err); setToastMessage({ message: 'Failed to copy location URL.', type: 'error', @@ -388,11 +396,7 @@ export const useShareLocation = (setToastMessage, mapRef) => { /** * IconButton - * @description Reusable button component with customizable icons - * @param {Function} onClick - Click event handler - * @param {string} title - Button title for accessibility - * @param {ReactNode} icon - Icon to be displayed inside the button - * @returns JSX Element + * Reusable button component with customizable icons */ export const IconButton = ({ onClick, title, icon }) => (