diff --git a/src/components/Profile/TimePreferencesPage.jsx b/src/components/Profile/TimePreferencesPage.jsx index 808843c..3dd0831 100644 --- a/src/components/Profile/TimePreferencesPage.jsx +++ b/src/components/Profile/TimePreferencesPage.jsx @@ -1,6 +1,6 @@ import React from 'react'; -import { useTimePreferences } from '@data/useTimePreferences'; +import useTimePreferences from '@data/useTimePreferences'; import { Box, Typography, Button, CircularProgress } from '@mui/material'; import TimePreferencesGrid from './TimePreferencesGrid'; diff --git a/src/hooks/data/useTimePreferences.js b/src/hooks/data/useTimePreferences.js index 980d13d..2db1ff1 100644 --- a/src/hooks/data/useTimePreferences.js +++ b/src/hooks/data/useTimePreferences.js @@ -1,33 +1,24 @@ import { useState, useEffect } from 'react'; import { useAuthState } from '@auth/useAuthState'; -import { fetchTimePreferences, saveTimePreferences } from '@firestore/userProfile'; +import useUserProfile from '@data/useUserProfile'; +import { saveTimePreferences } from '@firestore/userProfile'; import { useNavigate } from 'react-router-dom'; -export const useTimePreferences = () => { +const useTimePreferences = () => { const [selectedTimes, setSelectedTimes] = useState([]); - const [loading, setLoading] = useState(true); const [user] = useAuthState(); const navigate = useNavigate(); const userId = user?.uid; + const { userProfile, loading } = useUserProfile(); - // Check if user is authenticated, if not, redirect to home + // Set selected times from user profile only when userProfile is updated useEffect(() => { - // Fetch saved time preferences when the component loads - const loadPreferences = async () => { - try { - const fetchedTimes = await fetchTimePreferences(userId); - setSelectedTimes(fetchedTimes); - } catch (err) { - console.error('Failed to load time preferences'); - } finally { - setLoading(false); - } - }; - - loadPreferences(); - }, [userId, navigate]); + if (userProfile && userProfile.timePreferences) { + setSelectedTimes(userProfile.timePreferences); + } + }, [userProfile]); // Function to save the selected time preferences const savePreferences = async () => { @@ -46,3 +37,5 @@ export const useTimePreferences = () => { savePreferences, }; }; + +export default useTimePreferences; diff --git a/src/utils/auth.js b/src/utils/auth.js index 092b8c6..1f80cb4 100644 --- a/src/utils/auth.js +++ b/src/utils/auth.js @@ -1,5 +1,5 @@ -import { fetchAndStoreClassData } from '@firestore/classData'; -import { checkUserProfile } from '@firestore/userProfile'; +// import { fetchAndStoreClassData } from '@firestore/classData'; +import { createFirstUserProfile, fetchUserProfile } from '@firestore/userProfile'; import { auth } from '@utils/firebaseConfig'; import { GoogleAuthProvider, signInWithPopup, signOut } from 'firebase/auth'; @@ -20,21 +20,27 @@ const signInWithGoogle = async () => { // Return true: user already exists in the database export const handleSignIn = async () => { const user = await signInWithGoogle(); - let alreadyExist = true; + if (user) { - alreadyExist = await checkUserProfile(user); - } - // ! TEMPORARY REMOVE: Fetch and store class data - // TODO: uncomment this code after the demo + const { profile } = await fetchUserProfile(user.uid); - // const res = await fetchAndStoreClassData(); - // console.clear(); - // if (res) { - // console.warn('Class data fetched and stored:', res); - // } else { - // console.warn('Classes not update:', res); - // } - return alreadyExist; + if (!profile) { + await createFirstUserProfile(user); + return false; + } + // ! TEMPORARY REMOVE: Fetch and store class data + // TODO: uncomment this code after the demo + + // const res = await fetchAndStoreClassData(); + // console.clear(); + // if (res) { + // console.warn('Class data fetched and stored:', res); + // } else { + // console.warn('Classes not update:', res); + // } + return true; + } + return false; }; // Handle Sign-Out diff --git a/src/utils/firestore/general.js b/src/utils/firestore/general.js index 8b4e889..bec96fa 100644 --- a/src/utils/firestore/general.js +++ b/src/utils/firestore/general.js @@ -1,49 +1,119 @@ // General Firestore functions (shared utilities) import { db } from '@utils/firebaseConfig'; -import { collection, getDocs, doc, getDoc } from 'firebase/firestore'; +import { collection, getDocs, doc, getDoc, onSnapshot } from 'firebase/firestore'; -// Get all users from Firestore +// Caches for Firestore data +const usersCache = new Map(); +const majorsCache = { data: null, timestamp: 0 }; +const coursesCache = { data: new Set(), timestamp: 0 }; +// Cache time-to-live (TTL) of 5 days +const cacheTTL = 5 * 24 * 3600 * 1000; + +// Get all users from Firestore with caching and real-time updates export const getAllUsers = async () => { + if (usersCache.size > 0) { + return Array.from(usersCache.values()); + } + try { const usersCollectionRef = collection(db, 'users'); const usersSnapshot = await getDocs(usersCollectionRef); - return usersSnapshot.docs.map((doc) => doc.data()); + + usersSnapshot.docs.forEach((doc) => { + usersCache.set(doc.id, doc.data()); // Cache the user data + }); + + // Set up real-time listener to update cache + onSnapshot(usersCollectionRef, (snapshot) => { + snapshot.docChanges().forEach((change) => { + if (change.type === 'added' || change.type === 'modified') { + usersCache.set(change.doc.id, change.doc.data()); + } else if (change.type === 'removed') { + usersCache.delete(change.doc.id); + } + }); + }); + + return Array.from(usersCache.values()); } catch (error) { console.error('Error fetching user profiles:', error); return []; } }; -// Get list of majors from Firestore +// Get list of majors from Firestore with caching and real-time updates export const getMajors = async () => { + const now = Date.now(); + if (majorsCache.data && now - majorsCache.timestamp < cacheTTL) { + return majorsCache.data; + } + try { const majorsDocRef = doc(collection(db, 'majorsCourses'), 'majors'); const majorsSnapshot = await getDoc(majorsDocRef); - return majorsSnapshot.exists() ? majorsSnapshot.data().majors : []; + if (majorsSnapshot.exists()) { + const majorsData = majorsSnapshot.data().majors; + majorsCache.data = majorsData; + majorsCache.timestamp = now; + + // Set up real-time listener to update cache + onSnapshot(majorsDocRef, (docSnapshot) => { + if (docSnapshot.exists()) { + majorsCache.data = docSnapshot.data().majors; + majorsCache.timestamp = Date.now(); + } + }); + + return majorsData; + } + return []; } catch (error) { console.error('Error fetching majors:', error); return []; } }; -// Get list of courses from Firestore and store them as an set +// Get list of courses from Firestore and store them as a Set with caching and real-time updates export const getCourses = async () => { + const now = Date.now(); + if (coursesCache.data.size > 0 && now - coursesCache.timestamp < cacheTTL) { + return Array.from(coursesCache.data); + } + try { const coursesCollectionRef = collection(db, 'courseData'); const coursesSnapshot = await getDocs(coursesCollectionRef); - const coursesSet = new Set(); coursesSnapshot.docs.forEach((doc) => { const subject = doc.id; const numbers = doc.data().numbers || []; - numbers.forEach((courseNumber) => { - coursesSet.add(`${subject} ${courseNumber}`); // Add unique combination to Set + coursesCache.data.add(`${subject} ${courseNumber}`); + }); + }); + + coursesCache.timestamp = now; + + // Set up real-time listener to update cache + onSnapshot(coursesCollectionRef, (snapshot) => { + snapshot.docChanges().forEach((change) => { + const subject = change.doc.id; + const numbers = change.doc.data().numbers || []; + + if (change.type === 'added' || change.type === 'modified') { + numbers.forEach((courseNumber) => { + coursesCache.data.add(`${subject} ${courseNumber}`); + }); + } else if (change.type === 'removed') { + numbers.forEach((courseNumber) => { + coursesCache.data.delete(`${subject} ${courseNumber}`); + }); + } }); + coursesCache.timestamp = Date.now(); }); - // Convert Set to Array to eliminate duplicates - return Array.from(coursesSet); + return Array.from(coursesCache.data); } catch (error) { console.error('Error fetching courses:', error); return []; diff --git a/src/utils/firestore/matches.js b/src/utils/firestore/matches.js index ae17dbb..1e7b53b 100644 --- a/src/utils/firestore/matches.js +++ b/src/utils/firestore/matches.js @@ -1,23 +1,27 @@ import { fetchUserProfile } from '@firestore/userProfile'; import { db } from '@utils/firebaseConfig'; import { + query, + where, + getDocs, doc, - getDoc, - collection, runTransaction, arrayUnion, arrayRemove, + collection, } from 'firebase/firestore'; -// Utility function to fetch a match document by ID -const fetchMatchDocument = async (matchId) => { +// Batch fetch match documents using getDocs by match IDs +const fetchMatchDocuments = async (matchIds) => { try { - const matchDocRef = doc(db, 'matches', matchId); - const matchSnapshot = await getDoc(matchDocRef); - return matchSnapshot.exists() ? matchSnapshot.data() : null; + const q = query(collection(db, 'matches'), where('__name__', 'in', matchIds)); + const querySnapshot = await getDocs(q); + + // Return all match data from Firestore + return querySnapshot.docs.map((doc) => doc.data()); } catch (error) { - console.error('Error fetching match document:', error); - return null; + console.error('Error fetching match documents:', error); + return []; } }; @@ -78,34 +82,31 @@ export const createMatch = async (users, location, description = '') => { // Get all user matches export const getUserMatches = async (uid) => { try { - const userRef = doc(db, 'users', uid); - const userSnapshot = await getDoc(userRef); - if (!userSnapshot.exists()) { + const { profile: userProfile } = await fetchUserProfile(uid); // Use fetchUserProfile + if (!userProfile) { throw new Error('User profile does not exist'); } - const { currentMatches } = userSnapshot.data(); + const { currentMatches } = userProfile; 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); - const profiles = await Promise.all( - otherUsers.map(async (user) => { - const { profile } = await fetchUserProfile(user.uid); - return profile || null; - }), - ); - return profiles.filter((profile) => profile !== null); - }), + // Fetch match documents in a batch using getDocs + const matchDocs = await fetchMatchDocuments(currentMatches); + + const profiles = await Promise.all( + matchDocs.map((matchData) => + Promise.all( + matchData.users + .filter((user) => user.uid !== uid) + .map(async (user) => { + const { profile } = await fetchUserProfile(user.uid); + return profile || null; + }), + ), + ), ); - return matchProfiles.flat().filter((profile) => profile !== null); + return profiles.flat().filter((profile) => profile !== null); } catch (error) { console.error('Error fetching user matches:', error); return []; @@ -145,23 +146,21 @@ export const resolveMatchRequest = async (requestedUserUid, requestingUserUid, m // Get matched user UIDs for a specific user export const getMatchedUserUids = async (userUid) => { try { - const userRef = doc(db, 'users', userUid); - const userSnapshot = await getDoc(userRef); - - if (!userSnapshot.exists()) { + const { profile: userProfile } = await fetchUserProfile(userUid); // Use fetchUserProfile + if (!userProfile) { throw new Error('User profile does not exist'); } - const { currentMatches } = userSnapshot.data(); + const { currentMatches } = userProfile; if (!currentMatches || currentMatches.length === 0) return []; const matchedUserUids = new Set(); await Promise.all( currentMatches.map(async (matchId) => { - const matchData = await fetchMatchDocument(matchId); - if (matchData) { - matchData.users.forEach((user) => { + const matchData = await fetchMatchDocuments([matchId]); // Use batch fetch here + if (matchData.length > 0) { + matchData[0].users.forEach((user) => { if (user.uid !== userUid) { matchedUserUids.add(user.uid); } diff --git a/src/utils/firestore/userProfile.js b/src/utils/firestore/userProfile.js index 255568e..1e98db1 100644 --- a/src/utils/firestore/userProfile.js +++ b/src/utils/firestore/userProfile.js @@ -1,19 +1,23 @@ // User profile operations (get, update, check) import { db } from '@utils/firebaseConfig'; -import { doc, setDoc, getDoc, updateDoc } from 'firebase/firestore'; -// Unified function to fetch user profile by UID +import { doc, setDoc, getDoc, updateDoc, onSnapshot } from 'firebase/firestore'; + +// Unified function to fetch and listen to user profile changes by UID // (supports both regular and transaction-based fetches) -export const fetchUserProfile = async (uid, transaction) => { +export const fetchUserProfile = async (uid, transaction = null) => { try { const userRef = doc(db, 'users', uid); + + // If transaction is provided, use it to fetch the document const userSnapshot = transaction ? await transaction.get(userRef) : await getDoc(userRef); if (!userSnapshot.exists()) { - console.warn(`User profile for ${uid} does not exist`); + console.error(`User profile for ${uid} does not exist`); return { ref: userRef, profile: null }; // Return consistent format with null profile } - return { ref: userRef, profile: userSnapshot.data() }; + const profile = userSnapshot.data(); + return { ref: userRef, profile }; } catch (error) { console.error('Error fetching user profile:', error); return { ref: null, profile: null }; @@ -21,7 +25,7 @@ export const fetchUserProfile = async (uid, transaction) => { }; // Check or create user profile in Firestore (uses fetchUserProfile to streamline code) -export const checkUserProfile = async (user) => { +export const createFirstUserProfile = async (user) => { try { const { uid, photoURL, displayName, email, phoneNumber } = user; const defaultProfile = { @@ -40,38 +44,14 @@ export const checkUserProfile = async (user) => { outgoingMatches: [], currentMatches: [], pastMatches: [], - timePreferences: [], //Addingdefault empty array for storing time preferences + timePreferences: [], }; - // 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('New user profile created with default data.'); - return false; // Return false indicating a new user profile was created - } - - const existingProfile = profile; - const updates = {}; - - // Check for missing or outdated fields in the user's profile - for (const key in defaultProfile) { - if (!(key in existingProfile) || existingProfile[key] === undefined) { - updates[key] = defaultProfile[key]; // Add missing or undefined attributes to updates - } else if (key === 'profilePic' && photoURL !== existingProfile.profilePic) { - updates.profilePic = photoURL; // Update profile picture if it has changed - } - } - - // 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.'); - } + await setDoc(doc(db, 'users', uid), defaultProfile); + console.warn('New user profile created with default data.'); - return true; // Return true indicating the user profile already existed + return true; } catch (error) { console.error('Error checking or creating user profile:', error); return false; // Return false if an error occurs @@ -90,7 +70,7 @@ export const updateUserProfile = async (uid, updates) => { try { const userDocRef = doc(db, 'users', uid); await updateDoc(userDocRef, updates); - console.log('User profile updated'); + console.warn('User profile updated'); } catch (error) { console.error('Error updating user profile:', error); } @@ -109,26 +89,8 @@ export const saveTimePreferences = async (uid, selectedTimes) => { await updateDoc(userDocRef, { timePreferences: selectedTimes, // Update timePreferences field }); - console.log('Time preferences updated successfully.'); + console.warn('Time preferences updated successfully.'); } catch (error) { console.error('Error updating time preferences:', error); } }; - -// Function to fetch time preferences from Firestore -export const fetchTimePreferences = async (uid) => { - try { - const userDocRef = doc(db, 'users', uid); - // Getting the user's document from firebase - const userDocSnap = await getDoc(userDocRef); - - if (userDocSnap.exists()) { - const data = userDocSnap.data(); - // Return saved timePreferences or empty array if none. - return data.timePreferences || []; - } - } catch (error) { - console.error('Error fetching time preferences:', error); - return []; - } -};