From 840bbc4e8c88db52ab9a1d8aef1e1b843e8afb8c Mon Sep 17 00:00:00 2001 From: ZL Asica <40444637+ZL-Asica@users.noreply.github.com> Date: Fri, 11 Oct 2024 18:07:01 -0500 Subject: [PATCH] refactor: Fix multiple major bugs, refactor ProfileCard, add useCopyToClipboard hook fix: 1. GroupsPage.jsx: Resolved issue with current matches not displaying. 2. matches.js: Add error handling for fetchMatchDocument by match ID. 3. userProfile.js: Corrected new user profile creation logic. 4. HomePage.jsx: Prevented rendering of StudentFilter and StudentList when the user's profile is incomplete. refactor: 1. ProfileCard.jsx: Replaced HTML tags with MUI components and added renderProfileDetail helper function for rendering profiles. feat: 1. useCopyToClipboard.jsx: Implemented a new hook for clipboard copy functionality with Snackbar. BREAKING CHANGE: Major bugs fixed. --- src/components/GroupsPage.jsx | 186 ++++++++++++++++------------- src/components/HomePage.jsx | 16 ++- src/components/ProfileCard.jsx | 151 +++++++++++------------ src/hooks/useCopyToClipboard.jsx | 26 ++++ src/utils/firestore/matches.js | 31 ++--- src/utils/firestore/userProfile.js | 19 +-- 6 files changed, 249 insertions(+), 180 deletions(-) create mode 100644 src/hooks/useCopyToClipboard.jsx diff --git a/src/components/GroupsPage.jsx b/src/components/GroupsPage.jsx index 6f3a0d2..cd2b1d4 100644 --- a/src/components/GroupsPage.jsx +++ b/src/components/GroupsPage.jsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import { Box, Stack, Typography } from '@mui/material'; +import ProfileCard from './ProfileCard'; import StudentCard from './UserCard'; import { useAuthState } from '../hooks/useAuthState'; import useUserProfile from '../hooks/useUserProfile'; @@ -15,6 +16,7 @@ function GroupsPage() { const [outgoingRequestProfiles, setOutgoingRequestProfiles] = useState([]); const [matchProfiles, setMatchProfiles] = useState([]); const [selectedProfile, setSelectedProfile] = useState(null); // State for selected user profile + const [openProfileModal, setOpenProfileModal] = useState(false); // State for modal visibility // Combined useEffect for fetching incoming, outgoing, and current matches useEffect(() => { @@ -51,10 +53,6 @@ function GroupsPage() { setIncomingRequestProfiles(incomingProfiles); setOutgoingRequestProfiles(outgoingProfiles); setMatchProfiles(matches); - - // Update match profiles and set selected profile to null when closing modal - const updatedMatches = matches.filter((m) => m.uid !== selectedProfile?.uid); - setMatchProfiles(updatedMatches); } catch (error) { console.error('Error fetching request profiles:', error); } @@ -67,95 +65,119 @@ function GroupsPage() { try { await resolveMatchRequest(userProfile.uid, requestingUserUid, matchId, accept); // Update the UI after resolving the request - setUserProfile((prevProfile) => ({ - ...prevProfile, - incomingMatches: prevProfile.incomingMatches.filter((req) => req.matchId !== matchId), - })); + setIncomingRequestProfiles((prevProfiles) => + prevProfiles.filter((profile) => profile.matchId !== matchId), + ); + setMatchProfiles((prevProfiles) => + prevProfiles.filter((profile) => profile.uid !== requestingUserUid), + ); } catch (error) { console.error(`Error ${accept ? 'accepting' : 'denying'} request:`, error); } }; + const handleOpenProfileModal = (profile) => { + setSelectedProfile(profile); + setOpenProfileModal(true); + }; + + const handleCloseProfileModal = () => { + setOpenProfileModal(false); + }; + if (loading) { return Loading...; } - return ( - <> - {userProfile ? ( - -

Matches

- - {matchProfiles.length > 0 ? ( - matchProfiles.map((profile, index) => { - const actions = [ - { - label: 'View Profile', - // Allow users to see profile of matches - onClick: () => setSelectedProfile(profile), - }, - ]; - return ; - }) - ) : ( - - You don't currently have any matches. - - )} - - -

Incoming Requests

- - {incomingRequestProfiles.length > 0 ? ( - incomingRequestProfiles.map((profile, index) => { - const actions = [ - { - label: 'Accept', - onClick: () => handleRequestResolution(profile.uid, profile.matchId, true), - }, - { - label: 'Deny', - onClick: () => handleRequestResolution(profile.uid, profile.matchId, false), - variant: 'outlined', - color: 'secondary', - }, - ]; - return ; - }) - ) : ( - - You don't have any incoming requests. - - )} - - -

Outgoing Requests

- - {outgoingRequestProfiles.length > 0 ? ( - outgoingRequestProfiles.map((profile, index) => { - const actions = [ - { - label: 'Requested', - variant: 'outlined', - color: 'default', - onClick: () => {}, // No functionality - }, - ]; - return ; - }) - ) : ( - - You don't have any outgoing requests. - - )} - -
- ) : ( - + if (!user) { + return ( + + Please log in to view your groups. - )} - + + ); + } + + // Render the StudentFilter and StudentList only if the userProfile is complete + if (!userProfile || !userProfile.major || !userProfile.year || !userProfile.phoneNumber) { + return null; + } + + return ( + +

Matches

+ + {matchProfiles.length > 0 ? ( + matchProfiles.map((profile, index) => { + const actions = [ + { + label: 'View Profile', + onClick: () => handleOpenProfileModal(profile), + }, + ]; + return ; + }) + ) : ( + + You don't currently have any matches. + + )} + + +

Incoming Requests

+ + {incomingRequestProfiles.length > 0 ? ( + incomingRequestProfiles.map((profile, index) => { + const actions = [ + { + label: 'Accept', + onClick: () => handleRequestResolution(profile.uid, profile.matchId, true), + }, + { + label: 'Deny', + onClick: () => handleRequestResolution(profile.uid, profile.matchId, false), + variant: 'outlined', + color: 'secondary', + }, + ]; + return ; + }) + ) : ( + + You don't have any incoming requests. + + )} + + +

Outgoing Requests

+ + {outgoingRequestProfiles.length > 0 ? ( + outgoingRequestProfiles.map((profile, index) => { + const actions = [ + { + label: 'Requested', + variant: 'outlined', + color: 'default', + onClick: () => {}, // No functionality + }, + ]; + return ; + }) + ) : ( + + You don't have any outgoing requests. + + )} + + + {/* Modal for displaying the selected profile */} + +
); } diff --git a/src/components/HomePage.jsx b/src/components/HomePage.jsx index a8e82bd..b6565ee 100644 --- a/src/components/HomePage.jsx +++ b/src/components/HomePage.jsx @@ -1,6 +1,7 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Box, CircularProgress, Typography } from '@mui/material'; +import { useNavigate } from 'react-router-dom'; import StudentFilter from './Home/StudentFilter'; import StudentList from './Home/StudentList'; @@ -13,6 +14,14 @@ export default function HomePage() { useUserProfile(user); const [selectedMajors, setSelectedMajors] = useState([]); const [selectedYears, setSelectedYears] = useState([]); + const navigate = useNavigate(); + + // Redirect user to edit-profile if the profile is incomplete + useEffect(() => { + if (userProfile && (!userProfile.major || !userProfile.year || !userProfile.phoneNumber)) { + navigate('/edit-profile'); + } + }, [userProfile, navigate]); if (loading) { return ( @@ -32,6 +41,11 @@ export default function HomePage() { ); } + // Render the StudentFilter and StudentList only if the userProfile is complete + if (!userProfile || !userProfile.major || !userProfile.year || !userProfile.phoneNumber) { + return null; + } + return ( - ( + + + {label}: + {' '} + isCopyable && value && handleCopyToClipboard(value)} + > + {value || 'N/A'} + + + ); + + return ( + <> + - - {!profileData?.photoURL && (profileData?.name?.[0] || '')}{' '} - {/* Display initial if there is no Google photo */} - - } - title={ - - {profileData?.name} - - } - // subheader={ - // - // {profileData?.major} - // - // } - /> - - - Email: {profileData?.email} - - - Phone Number: {profileData?.phoneNumber} - - - Major: {profileData?.major} - - - Year: {profileData?.year} - - - Description: {profileData?.description} - - - - + + + {profileData?.name?.[0] || ''} + + } + title={ + + {profileData?.name || 'Unknown User'} + + } + /> + + {renderProfileDetail('Email', profileData?.email, true)} + {renderProfileDetail('Phone Number', profileData?.phoneNumber, true)} + {renderProfileDetail('Major', profileData?.major)} + {renderProfileDetail('Year', profileData?.year)} + {renderProfileDetail('Bio', profileData?.description)} + + + + + {/* Snackbar for copy confirmation */} + + ); } diff --git a/src/hooks/useCopyToClipboard.jsx b/src/hooks/useCopyToClipboard.jsx new file mode 100644 index 0000000..ad5afa1 --- /dev/null +++ b/src/hooks/useCopyToClipboard.jsx @@ -0,0 +1,26 @@ +import { useState } from 'react'; + +import { Snackbar } from '@mui/material'; + +export const useCopyToClipboard = () => { + const [snackbarOpen, setSnackbarOpen] = useState(false); + const [snackbarMessage, setSnackbarMessage] = useState(''); + + const handleCopyToClipboard = (text) => { + navigator.clipboard.writeText(text); + setSnackbarMessage(`Copied to clipboard: ${text}`); + setSnackbarOpen(true); + }; + + const SnackbarComponent = () => ( + setSnackbarOpen(false)} + message={snackbarMessage} + anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} + /> + ); + + return { handleCopyToClipboard, SnackbarComponent }; +}; diff --git a/src/utils/firestore/matches.js b/src/utils/firestore/matches.js index e2f4cf8..9f7bcf0 100644 --- a/src/utils/firestore/matches.js +++ b/src/utils/firestore/matches.js @@ -12,9 +12,14 @@ import { db } from '../firebaseConfig'; // Utility function to fetch a match document by ID const fetchMatchDocument = async (matchId) => { - const matchDocRef = doc(db, 'matches', matchId); - const matchSnapshot = await getDoc(matchDocRef); - return matchSnapshot.exists() ? matchSnapshot.data() : null; + try { + const matchDocRef = doc(db, 'matches', matchId); + const matchSnapshot = await getDoc(matchDocRef); + return matchSnapshot.exists() ? matchSnapshot.data() : null; + } catch (error) { + console.error('Error fetching match document:', error); + return null; + } }; // Create a new match @@ -71,15 +76,6 @@ export const createMatch = async (users, location, description = '') => { } }; -// Fetch match by ID -export const getMatch = async (matchId) => { - try { - return await fetchMatchDocument(matchId); - } catch (error) { - console.error('Error fetching match:', error); - } -}; - // Get all user matches export const getUserMatches = async (uid) => { try { @@ -92,16 +88,21 @@ export const getUserMatches = async (uid) => { const { currentMatches } = userSnapshot.data(); if (!currentMatches || currentMatches.length === 0) return []; + // Fetch each match and gather the profiles of other users in the match const matchProfiles = await Promise.all( currentMatches.map(async (matchId) => { const matchData = await fetchMatchDocument(matchId); if (!matchData) return null; + // Get all users in the match except the current user const otherUsers = matchData.users.filter((user) => user.uid !== uid); - return await Promise.all( - // Pass empty transaction object if not using transactions - otherUsers.map(async (user) => fetchUserProfile(user.uid, {})), + const profiles = await Promise.all( + otherUsers.map(async (user) => { + const { profile } = await fetchUserProfile(user.uid); + return profile || null; + }), ); + return profiles.filter((profile) => profile !== null); }), ); diff --git a/src/utils/firestore/userProfile.js b/src/utils/firestore/userProfile.js index 6f59efd..7a59b20 100644 --- a/src/utils/firestore/userProfile.js +++ b/src/utils/firestore/userProfile.js @@ -12,7 +12,7 @@ export const fetchUserProfile = async (uid, transaction) => { if (!userSnapshot.exists()) { console.warn(`User profile for ${uid} does not exist`); - return { ref: userRef, profile: null }; // Ensure a consistent return format + return { ref: userRef, profile: null }; // Return consistent format with null profile } return { ref: userRef, profile: userSnapshot.data() }; @@ -29,7 +29,7 @@ export const checkUserProfile = async (user) => { const defaultProfile = { uid, profilePic: photoURL || '', - name: displayName || 'Anonymous', + name: displayName || '', email: email || '', phoneNumber: phoneNumber || '', major: '', @@ -43,15 +43,17 @@ export const checkUserProfile = async (user) => { pastMatches: [], }; - const fetchedUser = await fetchUserProfile(uid); - if (!fetchedUser) { - // Create a new profile if it doesn't exist + // Fetch the user profile to check if it exists + const { profile } = await fetchUserProfile(uid); + + // If the profile does not exist, create it with the default data + if (!profile) { await setDoc(doc(db, 'users', uid), defaultProfile); - console.log('User profile created'); + console.log('New user profile created with default data.'); return false; // Return false indicating a new user profile was created } - const existingProfile = fetchedUser.profile; + const existingProfile = profile; const updates = {}; // Check for missing or outdated fields in the user's profile @@ -66,8 +68,9 @@ export const checkUserProfile = async (user) => { // If updates are required, update the user profile in Firestore if (Object.keys(updates).length > 0) { await updateUserProfile(uid, updates); - console.log('User profile updated with missing attributes'); + console.log('User profile updated with missing attributes.'); } + return true; // Return true indicating the user profile already existed } catch (error) { console.error('Error checking or creating user profile:', error);