From 97cf1fbd73e65c9bbcc5a4a61faf3997d24d12f8 Mon Sep 17 00:00:00 2001 From: MC2b6 Date: Fri, 6 Dec 2024 11:46:32 -0600 Subject: [PATCH 1/3] feat: add donor email to organization alerts --- src/components/Alerts/OrganizationAlerts.tsx | 44 ++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/components/Alerts/OrganizationAlerts.tsx b/src/components/Alerts/OrganizationAlerts.tsx index 49d94b4..b946845 100644 --- a/src/components/Alerts/OrganizationAlerts.tsx +++ b/src/components/Alerts/OrganizationAlerts.tsx @@ -8,11 +8,47 @@ import { ListItemText, } from '@mui/material'; import dayjs from 'dayjs'; +import { useState, useEffect } from 'react'; import { useEventStore } from '@/stores'; +import { getOrCreateDocument } from '@/utils/firebase/firebaseUtils'; + +const defaultDonorProfile: DonorProfile = { + uid: '', + name: '', + email: '', + profilePic: '', + joinedEvents: [], + createdAt: new Date().toISOString(), + role: 'donor', + providedSupplies: [], + saved: [], +}; + const OrganizationAlerts = () => { const events = useEventStore((store) => store.events); + const [donorEmails, setDonorEmails] = useState>({}); + + useEffect(() => { + const fetchDonorEmails = async () => { + const emails: Record = {}; + for (const event of events) { + if (event.donorId) { + const donorDoc = await getOrCreateDocument( + event.donorId, + defaultDonorProfile + ); + emails[event.donorId] = donorDoc?.email || 'No email available'; + } + } + setDonorEmails(emails); + }; + + if (events.length > 0) { + fetchDonorEmails(); + } + }, [events]); return ( @@ -43,6 +79,14 @@ const OrganizationAlerts = () => { {event.title.split('from')[1].trim() || 'Loading...'}{' '} {/* Display donor name */} + + Donor Email:{' '} + {donorEmails[event.donorId] || 'Loading...'}{' '} + {/* Display donor name */} + Date: Fri, 6 Dec 2024 12:00:46 -0600 Subject: [PATCH 2/3] fix: fetching org profiles on mount --- src/components/Home/DonorDashboard/index.tsx | 5 ++- src/components/common/ProtectedRoute.tsx | 41 ++++---------------- src/routes.tsx | 23 +++++++++++ 3 files changed, 34 insertions(+), 35 deletions(-) diff --git a/src/components/Home/DonorDashboard/index.tsx b/src/components/Home/DonorDashboard/index.tsx index 3e2985a..5a4e7ec 100644 --- a/src/components/Home/DonorDashboard/index.tsx +++ b/src/components/Home/DonorDashboard/index.tsx @@ -6,10 +6,13 @@ import OrganizationCard from './OrganizationCard'; import { useOrganizationStore } from '@/stores'; -import SearchBar from '@/components/common/SearchBar'; +import { SearchBar, LoadingCircle } from '@/components/common'; const DonorDashboard = () => { const [searchQuery, setSearchQuery] = useState(''); + const loading = useOrganizationStore((state) => state.loading); + if (loading) return ; + const organizationProfiles = useOrganizationStore( (state) => state.organizationProfiles ); diff --git a/src/components/common/ProtectedRoute.tsx b/src/components/common/ProtectedRoute.tsx index e2140f0..3da9f3e 100644 --- a/src/components/common/ProtectedRoute.tsx +++ b/src/components/common/ProtectedRoute.tsx @@ -1,8 +1,7 @@ import { Typography } from '@mui/material'; -import { useEffect, type ReactElement } from 'react'; -import { toast } from 'sonner'; +import { type ReactElement } from 'react'; -import { useOrganizationStore, useUserStore } from '@/stores'; +import { useUserStore, useOrganizationStore } from '@/stores'; import LoadingCircle from '@/components/common/LoadingCircle'; @@ -13,8 +12,12 @@ type ProtectedRouteProps = { const ProtectedRoute = ({ element }: ProtectedRouteProps) => { const user = useUserStore((state) => state.user); const loading = useUserStore((state) => state.loading); + const orgLoading = useOrganizationStore((state) => state.loading); + + if (loading || (user && user.role !== 'organization' && orgLoading)) { + return ; + } - if (loading) return ; if (!user) { return ( { ); } - if (user.role !== 'organization') { - const orgLoading = useOrganizationStore((state) => state.loading); - const subscribeToProfiles = useOrganizationStore( - (state) => state.subscribeToProfiles - ); - const error = useOrganizationStore((state) => state.error); - - useEffect(() => { - const unsubscribe = subscribeToProfiles; - - return () => unsubscribe && unsubscribe(); - }, [subscribeToProfiles]); - - if (orgLoading) return ; - - if (error) { - toast.error(error); - return ( - - {error} - - ); - } - } - return element; }; diff --git a/src/routes.tsx b/src/routes.tsx index 11b4a17..c3de8b2 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -1,4 +1,8 @@ import { Routes, Route } from 'react-router-dom'; +import { useEffect } from 'react'; +import { toast } from 'sonner'; + +import { useOrganizationStore, useUserStore } from './stores'; import Home from '@/pages/Home'; import Schedule from '@/pages/Schedule'; @@ -14,6 +18,25 @@ const AppRoutes = () => { { path: 'alerts', element: }, ]; + const loading = useUserStore((state) => state.loading); + const user = useUserStore((state) => state.user); + + if (!loading && (!user || user.role !== 'organization')) { + const subscribeToProfiles = useOrganizationStore( + (state) => state.subscribeToProfiles + ); + const error = useOrganizationStore((state) => state.error); + + useEffect(() => { + const unsubscribe = subscribeToProfiles; + + return () => unsubscribe && unsubscribe(); + }, [subscribeToProfiles]); + if (error) { + toast.error(error); + } + } + return ( Date: Fri, 6 Dec 2024 12:21:33 -0600 Subject: [PATCH 3/3] feat: fetch in docs to save quotas. --- src/components/Alerts/OrganizationAlerts.tsx | 18 +++--- src/utils/firebase/firebaseUtils.ts | 58 +++++++++++++++++++- src/utils/firebase/index.ts | 2 +- 3 files changed, 66 insertions(+), 12 deletions(-) diff --git a/src/components/Alerts/OrganizationAlerts.tsx b/src/components/Alerts/OrganizationAlerts.tsx index b946845..4e6c503 100644 --- a/src/components/Alerts/OrganizationAlerts.tsx +++ b/src/components/Alerts/OrganizationAlerts.tsx @@ -12,7 +12,7 @@ import { useState, useEffect } from 'react'; import { useEventStore } from '@/stores'; -import { getOrCreateDocument } from '@/utils/firebase/firebaseUtils'; +import { getOrDefaultDocuments } from '@/utils/firebase'; const defaultDonorProfile: DonorProfile = { uid: '', @@ -32,15 +32,15 @@ const OrganizationAlerts = () => { useEffect(() => { const fetchDonorEmails = async () => { + const donorIds = events.map((event) => event.donorId); + const donorProfiles = await getOrDefaultDocuments( + donorIds, + defaultDonorProfile + ); + const emails: Record = {}; - for (const event of events) { - if (event.donorId) { - const donorDoc = await getOrCreateDocument( - event.donorId, - defaultDonorProfile - ); - emails[event.donorId] = donorDoc?.email || 'No email available'; - } + for (const profile of donorProfiles) { + emails[profile.uid] = profile.email; } setDonorEmails(emails); }; diff --git a/src/utils/firebase/firebaseUtils.ts b/src/utils/firebase/firebaseUtils.ts index fe306f4..af6c01e 100644 --- a/src/utils/firebase/firebaseUtils.ts +++ b/src/utils/firebase/firebaseUtils.ts @@ -1,5 +1,13 @@ import type { DocumentData, WithFieldValue } from 'firebase/firestore'; -import { doc, getDoc, setDoc } from 'firebase/firestore'; +import { + collection, + doc, + getDoc, + getDocs, + query, + setDoc, + where, +} from 'firebase/firestore'; import { db } from './firebaseConfig'; @@ -57,4 +65,50 @@ const updateDocument = async ( } }; -export { getOrCreateDocument, updateDocument }; +const getOrDefaultDocuments = async >( + uids: string[], + defaultData: T +): Promise => { + const results: T[] = []; + const BATCH_SIZE = 10; // Firestore `in` query limit + const collectionName = defaultData.role; + + try { + const existingDocs: Record = {}; + + // Split UIDs into batches to respect Firestore's `in` query limit + const batches = []; + for (let i = 0; i < uids.length; i += BATCH_SIZE) { + batches.push(uids.slice(i, i + BATCH_SIZE)); + } + + // Fetch existing documents in batches + for (const batch of batches) { + const docsQuery = query( + collection(db, collectionName), + where('__name__', 'in', batch) + ); + const querySnapshot = await getDocs(docsQuery); + // eslint-disable-next-line unicorn/no-array-for-each + querySnapshot.forEach((docSnap) => { + if (docSnap.exists()) { + existingDocs[docSnap.id] = docSnap.data() as T; + } + }); + } + + // Map UIDs to their corresponding documents or default data + for (const uid of uids) { + results.push(existingDocs[uid] || { ...defaultData, id: uid }); + } + } catch (error) { + console.error( + `Error in getOrDefaultDocuments for ${collectionName}:`, + error + ); + } + + return results; +}; + +export { getOrCreateDocument, updateDocument, getOrDefaultDocuments }; diff --git a/src/utils/firebase/index.ts b/src/utils/firebase/index.ts index e492942..22bd71d 100644 --- a/src/utils/firebase/index.ts +++ b/src/utils/firebase/index.ts @@ -1,4 +1,4 @@ export { loginUser, logoutUser } from './auth'; export { auth, db } from './firebaseConfig'; -export { updateDocument } from './firebaseUtils'; +export { updateDocument, getOrDefaultDocuments } from './firebaseUtils'; export { updateEvent, removeEvent } from './eventsUtils';