diff --git a/.github/workflows/firebase-deploy.yml b/.github/workflows/firebase-deploy.yml index 9efcd72..a74b50a 100644 --- a/.github/workflows/firebase-deploy.yml +++ b/.github/workflows/firebase-deploy.yml @@ -24,35 +24,63 @@ jobs: runs-on: ubuntu-latest steps: + # Step 1: Checkout the code - name: ๐Ÿ“ฅ Checkout Code uses: actions/checkout@v4 + # Step 2: Setup Node.js and enable caching - name: ๐Ÿ› ๏ธ Setup Node uses: actions/setup-node@v4 with: node-version: 'lts/*' cache: 'npm' + # Step 3: Restore dependencies cache + - name: ๐Ÿ“‚ Restore npm cache + uses: actions/cache@v4 + with: + path: ~/.npm + key: ${{ runner.os }}-npm-cache-${{ hashFiles('package-lock.json') }} + restore-keys: | + ${{ runner.os }}-npm-cache- + + # Step 4: Install dependencies - name: ๐Ÿ“‚ Install Packages run: npm ci + # Step 5: Run linter - name: ๐Ÿšจ Lint Code run: npm run lint:fix + # Step 6: Format code - name: ๐ŸŽจ Format Code run: npm run format + # Step 7: Build the project - name: ๐Ÿงฑ Build Project run: npm run build + # Step 8: Cache the build output + - name: ๐Ÿ“ฆ Cache build output + uses: actions/cache@v4 + with: + path: | + dist + key: ${{ runner.os }}-build-cache-${{ hashFiles('src/**', 'public/**') }} + restore-keys: | + ${{ runner.os }}-build-cache- + + # Step 9: Run tests - name: ๐Ÿงช Run Tests with Vitest if: success() run: npm run test:ci + # Step 10: Install Firebase CLI (use cache for global installs) - name: ๐ŸŒ Install Firebase Tools if: success() run: npm install -g firebase-tools + # Step 11: Deploy to Firebase - name: ๐Ÿš€ Deploy to Firebase if: success() env: diff --git a/package-lock.json b/package-lock.json index 39627f0..5d17165 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,20 +12,20 @@ "@emotion/styled": "^11.13.0", "@mui/icons-material": "^6.1.10", "@mui/material": "^6.1.10", - "@mui/x-date-pickers": "^7.23.0", + "@mui/x-date-pickers": "^7.23.1", "@zl-asica/react": "^0.3.10", "dayjs": "^1.11.13", "es-toolkit": "^1.29.0", "firebase": "^11.0.2", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", "react-router-dom": "^7.0.2", "sonner": "^1.7.0", "zustand": "^5.0.2" }, "devDependencies": { - "@types/react": "^18.3.13", - "@types/react-dom": "^18.3.1", + "@types/react": "^19.0.1", + "@types/react-dom": "^19.0.1", "@types/react-router-dom": "^5.3.3", "@vitejs/plugin-react-swc": "^3.7.2", "@vitest/ui": "^2.1.8", @@ -37,9 +37,9 @@ "lint-staged": "^15.2.10", "typescript": "^5.7.2", "typescript-eslint": "^8.17.0", - "vite": "^6.0.2", + "vite": "^6.0.3", "vite-plugin-checker": "^0.8.0", - "vite-tsconfig-paths": "^5.1.2", + "vite-tsconfig-paths": "^5.1.4", "vitest": "^2.1.8" }, "engines": { @@ -1925,9 +1925,9 @@ } }, "node_modules/@mui/x-date-pickers": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.23.0.tgz", - "integrity": "sha512-Db9ElibVYHluXLVsRLfFwlYkL6/3NNE5AosSZiTx+Gw7uix/Z3pdjyHeA3ab65fU1tCk08XHY0PU6LQFifYB2g==", + "version": "7.23.1", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.23.1.tgz", + "integrity": "sha512-Jr4beZ7r2lvWBaFnkIAg9BgiNFcfeJy4AUe3MbG10BBSZyB++odGqhOUAIGqkP7MpUzEGlTv4NUaaD7gYTAQPg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", @@ -2681,19 +2681,18 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.13", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.13.tgz", - "integrity": "sha512-ii/gswMmOievxAJed4PAHT949bpYjPKXvXo1v6cRB/kqc2ZR4n+SgyCyvyc5Fec5ez8VnUumI1Vk7j6fRyRogg==", + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.1.tgz", + "integrity": "sha512-YW6614BDhqbpR5KtUYzTA+zlA7nayzJRA9ljz9CQoxthR0sDisYZLuvSMsil36t4EH/uAt8T52Xb4sVw17G+SQ==", "license": "MIT", "dependencies": { - "@types/prop-types": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.1.tgz", + "integrity": "sha512-hljHij7MpWPKF6u5vojuyfV0YA4YURsQG7KT6SzV0Zs2BXAtgdTxG6A229Ub/xiWV4w/7JL8fi6aAyjshH4meA==", "dev": true, "license": "MIT", "dependencies": { @@ -7470,28 +7469,24 @@ "license": "MIT" }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.25.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.0.0" } }, "node_modules/react-is": { @@ -7995,13 +7990,10 @@ } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "license": "MIT" }, "node_modules/semver": { "version": "7.6.3", @@ -8960,9 +8952,9 @@ } }, "node_modules/vite": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.2.tgz", - "integrity": "sha512-XdQ+VsY2tJpBsKGs0wf3U/+azx8BBpYRHFAyKm5VeEZNOJZRB63q7Sc8Iup3k0TrN3KO6QgyzFf+opSbfY1y0g==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.3.tgz", + "integrity": "sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==", "dev": true, "license": "MIT", "dependencies": { @@ -9687,9 +9679,9 @@ } }, "node_modules/vite-tsconfig-paths": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.3.tgz", - "integrity": "sha512-0bz+PDlLpGfP2CigeSKL9NFTF1KtXkeHGZSSaGQSuPZH77GhoiQaA8IjYgOaynSuwlDTolSUEU0ErVvju3NURg==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 73f9aac..94bffba 100644 --- a/package.json +++ b/package.json @@ -29,20 +29,20 @@ "@emotion/styled": "^11.13.0", "@mui/icons-material": "^6.1.10", "@mui/material": "^6.1.10", - "@mui/x-date-pickers": "^7.23.0", + "@mui/x-date-pickers": "^7.23.1", "@zl-asica/react": "^0.3.10", "dayjs": "^1.11.13", "es-toolkit": "^1.29.0", "firebase": "^11.0.2", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", "react-router-dom": "^7.0.2", "sonner": "^1.7.0", "zustand": "^5.0.2" }, "devDependencies": { - "@types/react": "^18.3.13", - "@types/react-dom": "^18.3.1", + "@types/react": "^19.0.1", + "@types/react-dom": "^19.0.1", "@types/react-router-dom": "^5.3.3", "@vitejs/plugin-react-swc": "^3.7.2", "@vitest/ui": "^2.1.8", @@ -54,9 +54,9 @@ "lint-staged": "^15.2.10", "typescript": "^5.7.2", "typescript-eslint": "^8.17.0", - "vite": "^6.0.2", + "vite": "^6.0.3", "vite-plugin-checker": "^0.8.0", - "vite-tsconfig-paths": "^5.1.2", + "vite-tsconfig-paths": "^5.1.4", "vitest": "^2.1.8" }, "engines": { diff --git a/src/components/Home/DonorDashboard/index.tsx b/src/components/Home/DonorDashboard/index.tsx index 0170d2a..5f9e4f7 100644 --- a/src/components/Home/DonorDashboard/index.tsx +++ b/src/components/Home/DonorDashboard/index.tsx @@ -3,9 +3,9 @@ import { useState, useMemo } from 'react'; import OrganizationCard from './OrganizationCard'; -import { useOrganizationStore } from '@/stores'; +import { useOrganizationStore, useUserStore } from '@/stores'; -import { SearchBar, Filters } from '@/components/common'; +import { SearchBar, Filters, LoadingCircle } from '@/components/common'; const DonorDashboard = () => { const [searchQuery, setSearchQuery] = useState(''); @@ -13,6 +13,9 @@ const DonorDashboard = () => { const [descriptionQuery, setDescriptionQuery] = useState(''); const [locationQuery, setLocationQuery] = useState(''); + const loading = useUserStore((state) => state.loading); + if (loading) return ; + const organizationProfiles = useOrganizationStore( (state) => state.organizationProfiles ); diff --git a/src/components/Schedule/index.tsx b/src/components/Schedule/index.tsx index 756bfe2..0c711c5 100644 --- a/src/components/Schedule/index.tsx +++ b/src/components/Schedule/index.tsx @@ -15,18 +15,21 @@ import dayjs from 'dayjs'; import EventsCalendar from './EventsCalendar'; +import { useEventStore } from '@/stores'; + import { generateICSFile, generateCombinedICSFile, } from '@/utils/generateICSFile'; interface ScheduleBaseProps { - events: DonationEvent[]; title: string; description: string; } -const ScheduleBase = ({ events, title, description }: ScheduleBaseProps) => { +const ScheduleBase = ({ title, description }: ScheduleBaseProps) => { + const events = useEventStore((store) => store.events); + const [selectedDate, setSelectedDate] = useState(dayjs()); const [selectedEvent, setSelectedEvent] = useState( null diff --git a/src/pages/Schedule.tsx b/src/pages/Schedule.tsx index 4191080..716577c 100644 --- a/src/pages/Schedule.tsx +++ b/src/pages/Schedule.tsx @@ -1,4 +1,4 @@ -import { useUserStore, useEventStore } from '@/stores'; +import { useUserStore } from '@/stores'; import ScheduleBase from '@/components/Schedule'; @@ -7,8 +7,6 @@ const DonorSchedule = () => { if (!user) return null; - const events = useEventStore((store) => store.events); - const title = user.role === 'donor' ? 'Your Donation Schedule' @@ -20,7 +18,6 @@ const DonorSchedule = () => { return ( diff --git a/src/routes.tsx b/src/routes.tsx index f7ed885..02fbe04 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -9,7 +9,7 @@ import Schedule from '@/pages/Schedule'; import Saved from '@/pages/Saved'; import Alerts from '@/pages/Alerts'; -import { ProtectedRoute, Layout } from '@/components/common'; +import { ProtectedRoute, Layout, LoadingCircle } from '@/components/common'; const AppRoutes = () => { const routeConfig = [ @@ -20,6 +20,7 @@ const AppRoutes = () => { const loading = useUserStore((state) => state.loading); const user = useUserStore((state) => state.user); + const fetchProfiles = useOrganizationStore((state) => state.fetchProfiles); const subscribeToProfiles = useOrganizationStore( (state) => state.subscribeToProfiles ); @@ -27,6 +28,8 @@ const AppRoutes = () => { useEffect(() => { if (!loading && (!user || user.role !== 'organization')) { + fetchProfiles(); + const unsubscribe = subscribeToProfiles; return () => unsubscribe && unsubscribe(); @@ -39,6 +42,8 @@ const AppRoutes = () => { } }, [error]); + if (loading) return ; + return ( Promise; } -const useEventStore = create()( - persist( - (set, get) => ({ - events: [], - - setEvents: (events) => set({ events }), - - fetchEventsByIds: async (eventIds: string[]) => { - if (!eventIds?.length) { - console.info('No event IDs provided.'); - return; - } - - try { - const eventsCollection = collection(db, 'events'); - const snapshot = await getDocs(eventsCollection); - - if (!snapshot.docs?.length) { - console.info('No events found in the collection.'); - return; - } - - const events: DonationEvent[] = snapshot.docs - .filter((doc) => eventIds.includes(doc.id)) - .map((doc) => ({ - ...doc.data(), - eventId: doc.id, - })) as DonationEvent[]; - - set({ events }); - } catch (error) { - console.error('Error fetching events by IDs:', error); - throw error; - } - }, - - addDonationEvent: async ( - event, - donorUpdates, - organizationUpdates - ): Promise => { - try { - if (organizationUpdates.uid) { - await updateDocument( - 'organization', - organizationUpdates.uid, - organizationUpdates - ); - } - - if (donorUpdates.uid) { - const userStore = useUserStore.getState(); - await userStore.updateProfile(donorUpdates); - } - - await updateEvent(event); - - const currentEvents = get().events; - set({ events: [...currentEvents, event] }); - - console.info(`Donation event ${event.eventId} created successfully.`); - } catch (error) { - console.error('Error adding donation event:', error); - throw new Error('Failed to create donation event.'); - } - }, - }), - { - name: 'event-store', - storage: createJSONStorage(() => sessionStorage), +const useEventStore = create()((set, get) => ({ + events: [], + + setEvents: (events) => set({ events }), + + fetchEventsByIds: async (eventIds: string[]) => { + if (!eventIds?.length) { + console.info('No event IDs provided.'); + return; + } + + try { + const eventsCollection = collection(db, 'events'); + const snapshot = await getDocs(eventsCollection); + + if (!snapshot.docs?.length) { + console.info('No events found in the collection.'); + return; + } + + const events: DonationEvent[] = snapshot.docs + .filter((doc) => eventIds.includes(doc.id)) + .map((doc) => ({ + ...doc.data(), + eventId: doc.id, + })) as DonationEvent[]; + + set({ events }); + } catch (error) { + console.error('Error fetching events by IDs:', error); + throw error; + } + }, + + addDonationEvent: async ( + event, + donorUpdates, + organizationUpdates + ): Promise => { + try { + if (organizationUpdates.uid) { + await updateDocument( + 'organization', + organizationUpdates.uid, + organizationUpdates + ); + } + + if (donorUpdates.uid) { + const userStore = useUserStore.getState(); + await userStore.updateProfile(donorUpdates); + } + + await updateEvent(event); + + const currentEvents = get().events; + set({ events: [...currentEvents, event] }); + + console.info(`Donation event ${event.eventId} created successfully.`); + } catch (error) { + console.error('Error adding donation event:', error); + throw new Error('Failed to create donation event.'); } - ) -); + }, +})); export default useEventStore; diff --git a/src/stores/organizationStore.ts b/src/stores/organizationStore.ts index b20f010..5fb1632 100644 --- a/src/stores/organizationStore.ts +++ b/src/stores/organizationStore.ts @@ -1,5 +1,4 @@ import { create } from 'zustand'; -import { createJSONStorage, persist } from 'zustand/middleware'; import { collection, onSnapshot, getDocs } from 'firebase/firestore'; import { db } from '@/utils/firebase'; @@ -12,72 +11,64 @@ interface OrganizationState { subscribeToProfiles: () => void; } -const useOrganizationStore = create()( - persist( - (set) => ({ - organizationProfiles: [], - loading: true, - error: null, +const useOrganizationStore = create()((set) => ({ + organizationProfiles: [], + loading: true, + error: null, - fetchProfiles: async () => { - try { - set({ loading: true }); - const organizationsCollection = collection(db, 'organization'); - const snapshot = await getDocs(organizationsCollection); + fetchProfiles: async () => { + try { + set({ loading: true }); + const organizationsCollection = collection(db, 'organization'); + const snapshot = await getDocs(organizationsCollection); - if (snapshot.empty) { - console.info('No organization profiles found.'); - set({ organizationProfiles: [], loading: false }); - return; - } + if (snapshot.empty) { + console.info('No organization profiles found.'); + set({ organizationProfiles: [], loading: false }); + return; + } - const profiles: OrganizationProfile[] = snapshot.docs.map((doc) => ({ - ...doc.data(), - uid: doc.id, - })) as OrganizationProfile[]; + const profiles: OrganizationProfile[] = snapshot.docs.map((doc) => ({ + ...doc.data(), + uid: doc.id, + })) as OrganizationProfile[]; - set({ organizationProfiles: profiles, loading: false }); - } catch (error) { - console.error('Error fetching organization profiles:', error); - set({ error: (error as Error).message, loading: false }); - } - }, + set({ organizationProfiles: profiles, loading: false }); + } catch (error) { + console.error('Error fetching organization profiles:', error); + set({ error: (error as Error).message, loading: false }); + } + }, - subscribeToProfiles: () => { - const organizationsCollection = collection(db, 'organization'); + subscribeToProfiles: () => { + const organizationsCollection = collection(db, 'organization'); - const unsubscribe = onSnapshot( - organizationsCollection, - (snapshot) => { - if (snapshot.empty) { - console.info('No organization profiles found.'); - set({ organizationProfiles: [], loading: false }); - return; - } + const unsubscribe = onSnapshot( + organizationsCollection, + (snapshot) => { + set({ loading: true }); - const profiles: OrganizationProfile[] = snapshot.docs.map( - (doc) => ({ - ...doc.data(), - uid: doc.id, - }) - ) as OrganizationProfile[]; + if (snapshot.empty) { + console.info('No organization profiles found.'); + set({ organizationProfiles: [], loading: false }); + return; + } - set({ organizationProfiles: profiles, loading: false }); - }, - (error) => { - console.error('Error subscribing to organization profiles:', error); - set({ error: error.message, loading: false }); - } - ); + const profiles: OrganizationProfile[] = snapshot.docs.map((doc) => ({ + ...doc.data(), + uid: doc.id, + })) as OrganizationProfile[]; - return unsubscribe; + set({ organizationProfiles: profiles, loading: false }); }, - }), - { - name: 'organization-store', - storage: createJSONStorage(() => sessionStorage), - } - ) -); + (error) => { + console.error('Error subscribing to organization profiles:', error); + set({ error: error.message, loading: false }); + } + ); + + return unsubscribe; + }, +})); export default useOrganizationStore; diff --git a/src/stores/userStore.ts b/src/stores/userStore.ts index 40c3079..37cce8b 100644 --- a/src/stores/userStore.ts +++ b/src/stores/userStore.ts @@ -21,7 +21,7 @@ const useUserStore = create()( persist( (set, get) => ({ user: undefined, - loading: true, + loading: false, error: null, initializeAuthListener: () => { @@ -30,6 +30,7 @@ const useUserStore = create()( const unsubscribeAuth = onAuthStateChanged( auth, async (firebaseUser) => { + set({ loading: true }); if (firebaseUser) { const currentUser = get().user; @@ -53,6 +54,8 @@ const useUserStore = create()( }, login: async (userType, navigate) => { + const fetchEventsByIds = useEventStore.getState().fetchEventsByIds; + try { set({ loading: true }); const profile = await loginUser(navigate, userType); @@ -61,6 +64,10 @@ const useUserStore = create()( user: { ...profile, role: userType }, error: null, }); + const currentUser = get().user; + if (currentUser && currentUser.joinedEvents?.length > 0) { + await fetchEventsByIds(currentUser.joinedEvents); + } } } catch (error) { console.error('Error during login:', error); @@ -76,6 +83,7 @@ const useUserStore = create()( await logoutUser(navigate); set({ user: undefined, error: null }); + useEventStore.getState().setEvents([]); } catch (error) { console.error('Error during logout:', error); set({ error: 'Logout failed. Please try again.' });