Skip to content

Commit

Permalink
refactor(savedOrgs): clean up codes
Browse files Browse the repository at this point in the history
- Remove extra `SavedContext` and corresponde `useSaved` hook.
- Create `useSavedOrgs` hook to directly use `UserContext` to handle global state.

- Add auto-fetch `organizationProfiles` into `UserContext` every time session refresh and listen to firebase updates.
- Directly saved to `Session storage` which will save quotas and ensure speed.
  • Loading branch information
ZL-Asica committed Nov 21, 2024
1 parent 05996d9 commit 9e4e06f
Show file tree
Hide file tree
Showing 14 changed files with 138 additions and 142 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"@mui/lab": "^6.0.0-beta.15",
"@mui/material": "^6.1.7",
"@mui/x-date-pickers": "^7.22.2",
"@zl-asica/react": "^0.3.0",
"@zl-asica/react": "^0.3.4",
"date-fns": "^4.1.0",
"es-toolkit": "^1.27.0",
"firebase": "^11.0.2",
Expand Down
35 changes: 16 additions & 19 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { ThemeProvider } from '@mui/material';
import { BrowserRouter as Router } from 'react-router-dom';

import { UserProvider } from '@/context/UserContext';
import { SavedProvider } from '@/context/SavedContext';
import { EventsProvider } from '@/context/EventsContext';
import AppRoutes from '@/routes';

Expand All @@ -17,25 +16,23 @@ const App = () => {
<ThemeProvider theme={theme}>
<UserProvider>
<EventsProvider>
<SavedProvider>
<div className='App'>
<Router
future={{
v7_relativeSplatPath: true,
v7_startTransition: true,
}}
>
<Header />
<div className='content'>
{/* Main content area where pages will render */}
<AppRoutes />
</div>
<div className='App'>
<Router
future={{
v7_relativeSplatPath: true,
v7_startTransition: true,
}}
>
<Header />
<div className='content'>
{/* Main content area where pages will render */}
<AppRoutes />
</div>

{/* Bottom Navigation with React Router links */}
<Footer />
</Router>
</div>
</SavedProvider>
{/* Bottom Navigation with React Router links */}
<Footer />
</Router>
</div>
</EventsProvider>
</UserProvider>
</ThemeProvider>
Expand Down
6 changes: 3 additions & 3 deletions src/components/Home/OrganizationCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { lighten, useTheme } from '@mui/material/styles';
import { useEffect, useState } from 'react';
import { useToggle } from '@zl-asica/react';

import { useSaved } from '@/hooks';
import { useSavedOrgs } from '@/hooks';

import { DonationModal } from '@/components/common';

Expand All @@ -25,7 +25,7 @@ const OrganizationCard: React.FC<{ organization: OrganizationProfile }> = ({
}) => {
const theme = useTheme();
const [isExpanded, toggleExpand] = useToggle();
const { savedOrgs, toggleSavedOrg } = useSaved();
const { savedOrgs, updateSavedOrgs } = useSavedOrgs();
const isSaved = savedOrgs.some((org) => org.uid === organization.uid);
const [isModalOpen, toggleModal] = useToggle();
const [checkedItems, setCheckedItems] = useState<boolean[]>([]);
Expand Down Expand Up @@ -65,7 +65,7 @@ const OrganizationCard: React.FC<{ organization: OrganizationProfile }> = ({
action={
<Button
color={isSaved ? 'secondary' : 'primary'}
onClick={() => toggleSavedOrg(organization)}
onClick={() => updateSavedOrgs(organization)}
>
{isSaved ? 'Unsave' : 'Save'}
</Button>
Expand Down
53 changes: 0 additions & 53 deletions src/context/SavedContext.tsx

This file was deleted.

43 changes: 34 additions & 9 deletions src/context/UserContext.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { createContext, useState, useEffect } from 'react';
import { onAuthStateChanged } from 'firebase/auth';
import type { NavigateFunction } from 'react-router-dom';
import { useLocalStorage } from '@zl-asica/react';
import { useLocalStorage, useSessionStorage } from '@zl-asica/react';

import LoadingCircle from '@/components/common/LoadingCircle';

import { loginUser, logoutUser, auth, updateDocument } from '@/utils/firebase';
import {
loginUser,
logoutUser,
auth,
updateDocument,
getAllOrganizationProfiles,
} from '@/utils/firebase';

interface UserContextType {
user: User | undefined;
organizationProfiles: OrganizationProfile[];
loading: boolean;
login: (userType: UserType, navigate: NavigateFunction) => Promise<void>;
logout: (navigate: NavigateFunction) => Promise<void>;
Expand All @@ -20,14 +27,21 @@ const UserContext = createContext<UserContextType>({} as UserContextType);
const UserProvider = ({ children }: { children: React.ReactNode }) => {
const { value: user, setValue: setUser } = useLocalStorage<User | undefined>(
'user',
// eslint-disable-next-line
// eslint-disable-next-line unicorn/no-useless-undefined
undefined
);
const { value: organizationProfiles, setValue: setOrganizationProfiles } =
useSessionStorage<OrganizationProfile[]>('organizationProfiles', []);
const [loading, setLoading] = useState(true);

// Monitor auth state changes
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (firebaseUser) => {
let unsubscribeProfiles: (() => void) | undefined;

if (organizationProfiles.length === 0) {
unsubscribeProfiles = getAllOrganizationProfiles(setOrganizationProfiles);
}
const unsubscribeAuth = onAuthStateChanged(auth, async (firebaseUser) => {
if (firebaseUser) {
if (!user) {
// if user logged in but not in local storage
Expand All @@ -40,7 +54,10 @@ const UserProvider = ({ children }: { children: React.ReactNode }) => {
setLoading(false); // Stop loading spinner
});

return () => unsubscribe();
return () => {
if (unsubscribeProfiles) unsubscribeProfiles();
unsubscribeAuth();
};
}, []);

// Handle login
Expand Down Expand Up @@ -78,15 +95,16 @@ const UserProvider = ({ children }: { children: React.ReactNode }) => {
};

// Handle profile update
const updateProfile = async (updates: Partial<User>): Promise<void> => {
const updateProfile = async <T extends User>(
updates: Partial<T>
): Promise<void> => {
if (!user) {
console.error('No user is currently logged in.');
return;
}

try {
const collectionName = user.role === 'donor' ? 'users' : 'organizations';
await updateDocument<User>(collectionName, user.uid, updates);
await updateDocument<T>(user.role, user.uid, updates);
setUser((prev) => (prev ? { ...prev, ...updates } : undefined)); // Update local state
} catch (error) {
console.error('Error updating profile:', error);
Expand All @@ -96,7 +114,14 @@ const UserProvider = ({ children }: { children: React.ReactNode }) => {

return (
<UserContext.Provider
value={{ user, loading, login, logout, updateProfile }}
value={{
user,
organizationProfiles,
loading,
login,
logout,
updateProfile,
}}
>
{loading ? <LoadingCircle /> : children}
</UserContext.Provider>
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { default as useUser } from './useUser';
export { default as useSaved } from './useSaved';
export { default as useEvents } from './useEvents';
export { default as useSavedOrgs } from './useSavedOrgs';
13 changes: 0 additions & 13 deletions src/hooks/useSaved.ts

This file was deleted.

41 changes: 41 additions & 0 deletions src/hooks/useSavedOrgs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import useUser from './useUser';

const useSavedOrgs = () => {
const { user, updateProfile } = useUser();

if (!user || user.role !== 'donor') {
return {
savedOrgs: [],
updateSavedOrgs: async () => {
console.warn('This user is not a donor.');
},
};
}

const donorProfile = user as DonorProfile;
const savedOrgs = donorProfile.saved || [];

const updateSavedOrgs = async (org: OrganizationProfile): Promise<void> => {
const isAlreadySaved = savedOrgs.some(
(organization: OrganizationProfile) => organization.uid === org.uid
);

// If exists, remove; else add
const updatedSaved = isAlreadySaved
? savedOrgs.filter(
(organization: OrganizationProfile) => organization.uid !== org.uid
)
: [...savedOrgs, org];

try {
await updateProfile({ saved: updatedSaved });
} catch (error) {
console.error('Error toggling saved organization:', error);
alert('Failed to update saved organizations. Please try again.');
}
};

return { savedOrgs, updateSavedOrgs };
};

export default useSavedOrgs;
21 changes: 6 additions & 15 deletions src/pages/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,18 @@
import { Box, Typography } from '@mui/material';
import { filter, lowerCase, some } from 'es-toolkit/compat';
import { useState, useEffect } from 'react';
import { useState } from 'react';

import { useUser } from '@/hooks';

import { SearchBar } from '@/components/common';
import OrganizationCard from '@/components/Home/OrganizationCard';

import { getAllOrganizationProfiles } from '@/utils/firebase/firebaseUtils';

const Home = () => {
const [searchQuery, setSearchQuery] = useState(''); // New state for search query
const [organizations, setOrganizations] = useState<OrganizationProfile[]>([]);

useEffect(() => {
const fetchOrganizations = async () => {
const data = await getAllOrganizationProfiles();
setOrganizations(data);
};

fetchOrganizations();
}, []);
const { organizationProfiles } = useUser(); // Get organization profiles from context

// Filtered organizations based on search query
const filteredOrganizations = filter(organizations, (org) => {
const filteredOrganizations = filter(organizationProfiles, (org) => {
const searchTerm = lowerCase(searchQuery);
return (
lowerCase(org.name).includes(searchTerm) ||
Expand All @@ -30,7 +21,7 @@ const Home = () => {
);
});

return organizations.length > 0 ? (
return organizationProfiles.length > 0 ? (
<div>
<SearchBar
searchQuery={searchQuery}
Expand Down
6 changes: 3 additions & 3 deletions src/pages/Saved.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Box, Typography } from '@mui/material';

import { useSaved } from '@/hooks';
import { useSavedOrgs } from '@/hooks';

import SavedOrganizationCard from '@/components/Home/SavedCard';

const Saved = () => {
const { savedOrgs, toggleSavedOrg } = useSaved();
const { savedOrgs, updateSavedOrgs } = useSavedOrgs();

return (
<Box>
Expand All @@ -15,7 +15,7 @@ const Saved = () => {
<SavedOrganizationCard
key={org.uid}
organization={org}
onRemove={() => toggleSavedOrg(org)}
onRemove={() => updateSavedOrgs(org)}
/>
))
) : (
Expand Down
Loading

0 comments on commit 9e4e06f

Please sign in to comment.