Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: crazy reads. #14

Merged
merged 1 commit into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/Profile/TimePreferencesPage.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
29 changes: 11 additions & 18 deletions src/hooks/data/useTimePreferences.js
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand All @@ -46,3 +37,5 @@ export const useTimePreferences = () => {
savePreferences,
};
};

export default useTimePreferences;
36 changes: 21 additions & 15 deletions src/utils/auth.js
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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
Expand Down
92 changes: 81 additions & 11 deletions src/utils/firestore/general.js
Original file line number Diff line number Diff line change
@@ -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 [];
Expand Down
75 changes: 37 additions & 38 deletions src/utils/firestore/matches.js
Original file line number Diff line number Diff line change
@@ -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 [];
}
};

Expand Down Expand Up @@ -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 [];
Expand Down Expand Up @@ -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);
}
Expand Down
Loading
Loading