From e15ce22460352ae469d86f30a7c00559a4b287e2 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 7 Sep 2024 14:32:24 -0500 Subject: [PATCH 1/4] add local storage to points,membership, and shirt-tracking pages --- shpe-app-web/app/(main)/membership/page.tsx | 61 +++++++++++---- shpe-app-web/app/(main)/points/page.tsx | 78 ++++++++++++------- .../app/(main)/tools/shirt-tracker/page.tsx | 37 +++++++-- 3 files changed, 129 insertions(+), 47 deletions(-) diff --git a/shpe-app-web/app/(main)/membership/page.tsx b/shpe-app-web/app/(main)/membership/page.tsx index 0f007882..93a56b4c 100644 --- a/shpe-app-web/app/(main)/membership/page.tsx +++ b/shpe-app-web/app/(main)/membership/page.tsx @@ -27,19 +27,55 @@ const Membership = () => { const fetchMembers = async () => { setLoading(true); - const response = await getMembers(); - setStudents(response); - const filteredMembers = response.filter((member) => { - console.log(member.publicInfo?.chapterExpiration); - return isMemberVerified(member.publicInfo?.chapterExpiration, member.publicInfo?.nationalExpiration); - }); - setMembers(filteredMembers); + try { + const response = await getMembers(); + setStudents(response); + const filteredMembers = response.filter((member) => { + console.log(member.publicInfo?.chapterExpiration); + return isMemberVerified(member.publicInfo?.chapterExpiration, member.publicInfo?.nationalExpiration); + }); + setMembers(filteredMembers); + + localStorage.setItem('cachedMembers', JSON.stringify(filteredMembers)); + localStorage.setItem('cachedMembersTimestamp', Date.now().toString()); + + const incomingReqs = await getMembersToVerify(); + setRequestsWithDocuments(incomingReqs); + localStorage.setItem('cachedRequests', JSON.stringify(incomingReqs)); + localStorage.setItem('cachedRequestsTimestamp', Date.now().toString()); + + } catch (error) { + console.error('Error fetching members:', error); + } finally { + setLoading(false); + } + }; + + const checkCacheAndFetchMembers = () => { + const cachedMembers = localStorage.getItem('cachedMembers'); + const cachedMembersTimestamp = localStorage.getItem('cachedMembersTimestamp'); + const cachedRequests = localStorage.getItem('cachedRequests'); + const cachedRequestsTimestamp = localStorage.getItem('cachedRequestsTimestamp'); + + const isCacheValid = (timestamp: string): boolean => { + return Date.now() - parseInt(timestamp, 10) < 24 * 60 * 60 * 1000; + }; - const incomingReqs = await getMembersToVerify(); - setRequestsWithDocuments(incomingReqs); - setLoading(false); + if (cachedMembers && cachedMembersTimestamp && isCacheValid(cachedMembersTimestamp) && + cachedRequests && cachedRequestsTimestamp && isCacheValid(cachedRequestsTimestamp)) { + + setMembers(JSON.parse(cachedMembers)); + setRequestsWithDocuments(JSON.parse(cachedRequests)); + setLoading(false); + } else { + fetchMembers(); + } }; + useEffect(() => { + checkCacheAndFetchMembers(); + }, []); + const handleApprove = async (member: RequestWithDoc) => { const userDocRef = doc(db, 'users', member.uid); @@ -110,11 +146,6 @@ const Membership = () => { } }; - useEffect(() => { - fetchMembers(); - setLoading(false); - }, []); - useEffect(() => { const unsubscribe = onAuthStateChanged(auth, (currentUser) => { if (currentUser) { diff --git a/shpe-app-web/app/(main)/points/page.tsx b/shpe-app-web/app/(main)/points/page.tsx index 22461d5e..495f3745 100644 --- a/shpe-app-web/app/(main)/points/page.tsx +++ b/shpe-app-web/app/(main)/points/page.tsx @@ -35,7 +35,6 @@ const Points = () => { const months = generateSchoolYearMonths(); const updateAllUserPoints = httpsCallable(functions, 'updateAllUserPoints'); - // Set the initial current month index to the real current month const currentMonthDate = new Date(); const realCurrentMonthIndex = months.findIndex( (month) => month.getFullYear() === currentMonthDate.getFullYear() && month.getMonth() === currentMonthDate.getMonth() @@ -43,43 +42,70 @@ const Points = () => { const [currentMonthIndex, setCurrentMonthIndex] = useState(realCurrentMonthIndex !== -1 ? realCurrentMonthIndex : 0); const fetchMembers = async () => { - const response = await getMembers() as UserWithLogs[]; - setMembers(response); - - // Initialize the original points state - const initialPoints = response.reduce((acc: PointsRecord, member) => { - if (member.publicInfo?.uid && member.eventLogs) { - member.eventLogs.forEach(log => { - if (log.eventId) { - acc[`${member.publicInfo?.uid}-${log.eventId}`] = log.points || 0; - } + try { + const response = await getMembers() as UserWithLogs[]; + setMembers(response); + + const initialPoints = response.reduce((acc: PointsRecord, member) => { + if (member.publicInfo?.uid && member.eventLogs) { + member.eventLogs.forEach(log => { + if (log.eventId) { + acc[`${member.publicInfo?.uid}-${log.eventId}`] = log.points || 0; + } + + if (log.instagramLogs) { + months.forEach((month, index) => { + const pointsForInstagram = calculateInstagramPoints(log.instagramLogs!, month); + acc[`${member.publicInfo?.uid}-instagram-${index}`] = pointsForInstagram; + }); + } + }); + } - // Calculate Instagram points per month - if (log.instagramLogs) { - months.forEach((month, index) => { - const pointsForInstagram = calculateInstagramPoints(log.instagramLogs!, month); - acc[`${member.publicInfo?.uid}-instagram-${index}`] = pointsForInstagram; - }); - } - }); - } + return acc; + }, {}); - return acc; - }, {}); + setOriginalPoints(initialPoints); + localStorage.setItem('cachedMembers', JSON.stringify(response)); + localStorage.setItem('cachedPoints', JSON.stringify(initialPoints)); + localStorage.setItem('cachedMembersTimestamp', Date.now().toString()); + } catch (error) { + console.error('Error fetching members:', error); + } + }; + + const checkCacheAndFetchMembers = () => { + const cachedMembers = localStorage.getItem('cachedMembers'); + const cachedPoints = localStorage.getItem('cachedPoints'); + const cachedTimestamp = localStorage.getItem('cachedMembersTimestamp'); - setOriginalPoints(initialPoints); + const isCacheValid = (timestamp: string): boolean => { + return Date.now() - parseInt(timestamp, 10) < 24 * 60 * 60 * 1000; + }; + + if (cachedMembers && cachedPoints && cachedTimestamp && isCacheValid(cachedTimestamp)) { + setMembers(JSON.parse(cachedMembers)); + setOriginalPoints(JSON.parse(cachedPoints)); + } else { + fetchMembers(); + } }; const fetchEvents = async () => { - const response = await getEvents(); - setEvents(response); + try { + const response = await getEvents(); + setEvents(response); + } catch (error) { + console.error('Error fetching events:', error); + } }; useEffect(() => { - fetchMembers(); + checkCacheAndFetchMembers(); fetchEvents(); }, []); + useEffect(() => { if (isEditing && inputRef.current) { inputRef.current.focus(); diff --git a/shpe-app-web/app/(main)/tools/shirt-tracker/page.tsx b/shpe-app-web/app/(main)/tools/shirt-tracker/page.tsx index 9832a840..ff2df10f 100644 --- a/shpe-app-web/app/(main)/tools/shirt-tracker/page.tsx +++ b/shpe-app-web/app/(main)/tools/shirt-tracker/page.tsx @@ -16,11 +16,6 @@ const ShirtTracker = () => { const [members, setMembers] = useState([]); const [shirtList, setShirtList] = useState([]); - const fetchMembers = async () => { - setLoading(true); - const response = await getMembers(); - setMembers(response); - }; const fetchShirts = async () => { const shirts = await getShirtsToVerify(); @@ -78,8 +73,38 @@ const ShirtTracker = () => { } }; + const fetchMembers = async () => { + setLoading(true); + try { + const response = await getMembers() as UserWithLogs[]; + setMembers(response); + + localStorage.setItem('cachedMembers', JSON.stringify(response)); + localStorage.setItem('cachedMembersTimestamp', Date.now().toString()); + + } catch (error) { + console.error('Error fetching members:', error); + } finally { + setLoading(false); + } + }; + + const checkCacheAndFetchMembers = () => { + const cachedMembers = localStorage.getItem('cachedMembers'); + const cachedTimestamp = localStorage.getItem('cachedMembersTimestamp'); + + if (cachedMembers && cachedTimestamp && Date.now() - parseInt(cachedTimestamp, 10) < 24 * 60 * 60 * 1000) { + const membersData = JSON.parse(cachedMembers) as UserWithLogs[]; + setMembers(membersData); + + setLoading(false); + } else { + fetchMembers(); + } + }; + useEffect(() => { - fetchMembers(); + checkCacheAndFetchMembers(); }, []); useEffect(() => { From ebb9873361b8721ddf0b6bdf5cf3f3635f414a76 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 7 Sep 2024 15:09:21 -0500 Subject: [PATCH 2/4] bug fix with timestamp --- shpe-app-web/app/(main)/points/page.tsx | 54 +++++++++++++- .../app/(main)/tools/shirt-tracker/page.tsx | 74 +++++++++++++++++-- 2 files changed, 118 insertions(+), 10 deletions(-) diff --git a/shpe-app-web/app/(main)/points/page.tsx b/shpe-app-web/app/(main)/points/page.tsx index 495f3745..a1b2b204 100644 --- a/shpe-app-web/app/(main)/points/page.tsx +++ b/shpe-app-web/app/(main)/points/page.tsx @@ -84,13 +84,14 @@ const Points = () => { }; if (cachedMembers && cachedPoints && cachedTimestamp && isCacheValid(cachedTimestamp)) { - setMembers(JSON.parse(cachedMembers)); + const members = JSON.parse(cachedMembers); + const convertedMembers = convertMembersLogsToTimestamps(members); + setMembers(convertedMembers); setOriginalPoints(JSON.parse(cachedPoints)); } else { fetchMembers(); } }; - const fetchEvents = async () => { try { const response = await getEvents(); @@ -708,8 +709,55 @@ const getColumnColor = (index: number): string => { return colors[index % colors.length]; }; -const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); +const isPlainDateObject = (obj: any): obj is Date => { + return ( + obj && + typeof obj === 'object' && + typeof obj.getFullYear === 'function' && + typeof obj.getMonth === 'function' && + typeof obj.getDate === 'function' + ); +}; + +// Helper function to check if an object is structured like a Firebase Timestamp +const isPlainTimestampObject = (obj: any): obj is { seconds: number; nanoseconds: number } => { + return ( + obj && + typeof obj === 'object' && + typeof obj.seconds === 'number' && + typeof obj.nanoseconds === 'number' + ); +}; + +// Convert the object back to a Timestamp if it's structured like one +const convertToTimestamp = (obj: any): Timestamp | null => { + if (isPlainDateObject(obj)) { + return Timestamp.fromDate(obj); + } else if (isPlainTimestampObject(obj)) { + return new Timestamp(obj.seconds, obj.nanoseconds); + } + return null; +}; + +// Conversion function to convert Date objects or Timestamp-like objects back to Timestamps +const convertDatesToTimestamps = (log: SHPEEventLog): SHPEEventLog => { + return { + ...log, + signInTime: convertToTimestamp(log.signInTime) || log.signInTime, + signOutTime: convertToTimestamp(log.signOutTime) || log.signOutTime, + creationTime: convertToTimestamp(log.creationTime) || log.creationTime, + instagramLogs: log.instagramLogs?.map(log => convertToTimestamp(log) || log), + }; +}; + +// Utility function to convert all event logs in members +const convertMembersLogsToTimestamps = (members: UserWithLogs[]): UserWithLogs[] => { + return members.map(member => ({ + ...member, + eventLogs: member.eventLogs?.map(convertDatesToTimestamps), + })); +}; export default Points; diff --git a/shpe-app-web/app/(main)/tools/shirt-tracker/page.tsx b/shpe-app-web/app/(main)/tools/shirt-tracker/page.tsx index ff2df10f..7d60922e 100644 --- a/shpe-app-web/app/(main)/tools/shirt-tracker/page.tsx +++ b/shpe-app-web/app/(main)/tools/shirt-tracker/page.tsx @@ -5,7 +5,7 @@ import Header from '@/components/Header'; import { onAuthStateChanged } from 'firebase/auth'; import { db, auth } from '@/config/firebaseConfig'; import { getMembers, getShirtsToVerify } from '@/api/firebaseUtils'; -import { User } from '@/types/user'; +import { PublicUserInfo, User } from '@/types/user'; import { SHPEEventLog } from '@/types/events'; import { doc, updateDoc, Timestamp } from 'firebase/firestore'; import { isMemberVerified } from '@/types/membership'; @@ -15,19 +15,23 @@ const ShirtTracker = () => { const [loading, setLoading] = useState(true); const [members, setMembers] = useState([]); const [shirtList, setShirtList] = useState([]); - - const fetchShirts = async () => { const shirts = await getShirtsToVerify(); const updatedShirtList: ShirtWithMember[] = shirts.map((shirt) => { - const matchedMember = members.find((member) => member.publicInfo?.uid === shirt.uid); + const shirtUid = shirt.uid?.trim().toLowerCase(); + const matchedMember = members.find((member) => + member.publicInfo?.uid?.trim().toLowerCase() === shirtUid + ); const email = matchedMember?.publicInfo?.email?.trim() ? matchedMember.publicInfo.email : matchedMember?.private?.privateInfo?.email || 'N/A'; const isOfficialMember = matchedMember - ? isMemberVerified(matchedMember.publicInfo?.chapterExpiration, matchedMember.publicInfo?.nationalExpiration) + ? isMemberVerified( + matchedMember.publicInfo?.chapterExpiration, + matchedMember.publicInfo?.nationalExpiration + ) ? 'Yes' : 'No' : 'No'; @@ -95,8 +99,8 @@ const ShirtTracker = () => { if (cachedMembers && cachedTimestamp && Date.now() - parseInt(cachedTimestamp, 10) < 24 * 60 * 60 * 1000) { const membersData = JSON.parse(cachedMembers) as UserWithLogs[]; - setMembers(membersData); - + const convertedMembers = convertMembersLogsAndPublicInfoToTimestamps(membersData); + setMembers(convertedMembers); setLoading(false); } else { fetchMembers(); @@ -199,5 +203,61 @@ interface ShirtWithMember extends ShirtData { isOfficialMember: string; } +const isPlainDateObject = (obj: any): obj is Date => { + return ( + obj && + typeof obj === 'object' && + typeof obj.getFullYear === 'function' && + typeof obj.getMonth === 'function' && + typeof obj.getDate === 'function' + ); +}; + + +const isPlainTimestampObject = (obj: any): obj is { seconds: number; nanoseconds: number } => { + return ( + obj && + typeof obj === 'object' && + typeof obj.seconds === 'number' && + typeof obj.nanoseconds === 'number' + ); +}; + +const convertToTimestamp = (obj: any): Timestamp | null => { + if (isPlainDateObject(obj)) { + return Timestamp.fromDate(obj); + } else if (isPlainTimestampObject(obj)) { + return new Timestamp(obj.seconds, obj.nanoseconds); + } + return null; +}; + +const convertPublicUserInfoDatesToTimestamps = (publicInfo: PublicUserInfo): PublicUserInfo => { + return { + ...publicInfo, + chapterExpiration: convertToTimestamp(publicInfo.chapterExpiration) || publicInfo.chapterExpiration, + nationalExpiration: convertToTimestamp(publicInfo.nationalExpiration) || publicInfo.nationalExpiration, + }; +}; + +const convertDatesToTimestamps = (log: SHPEEventLog): SHPEEventLog => { + return { + ...log, + signInTime: convertToTimestamp(log.signInTime) || log.signInTime, + signOutTime: convertToTimestamp(log.signOutTime) || log.signOutTime, + creationTime: convertToTimestamp(log.creationTime) || log.creationTime, + instagramLogs: log.instagramLogs?.map(log => convertToTimestamp(log) || log), + }; +}; + +const convertMembersLogsAndPublicInfoToTimestamps = (members: UserWithLogs[]): UserWithLogs[] => { + return members.map(member => ({ + ...member, + publicInfo: member.publicInfo ? convertPublicUserInfoDatesToTimestamps(member.publicInfo) : undefined, + eventLogs: member.eventLogs?.map(convertDatesToTimestamps), + })); +}; + + export default ShirtTracker; From 6cdace9065d45d46b6bbf8998d00e682ceb13b63 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 7 Sep 2024 15:19:05 -0500 Subject: [PATCH 3/4] fix membership caching --- shpe-app-web/app/(main)/membership/page.tsx | 26 +++++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/shpe-app-web/app/(main)/membership/page.tsx b/shpe-app-web/app/(main)/membership/page.tsx index 93a56b4c..d8b744cc 100644 --- a/shpe-app-web/app/(main)/membership/page.tsx +++ b/shpe-app-web/app/(main)/membership/page.tsx @@ -30,13 +30,18 @@ const Membership = () => { try { const response = await getMembers(); setStudents(response); + const filteredMembers = response.filter((member) => { console.log(member.publicInfo?.chapterExpiration); - return isMemberVerified(member.publicInfo?.chapterExpiration, member.publicInfo?.nationalExpiration); + return isMemberVerified( + member.publicInfo?.chapterExpiration, + member.publicInfo?.nationalExpiration + ); }); setMembers(filteredMembers); - localStorage.setItem('cachedMembers', JSON.stringify(filteredMembers)); + localStorage.setItem('cachedMembers', JSON.stringify(response)); + localStorage.setItem('cachedOfficialMembers', JSON.stringify(filteredMembers)); localStorage.setItem('cachedMembersTimestamp', Date.now().toString()); const incomingReqs = await getMembersToVerify(); @@ -53,6 +58,7 @@ const Membership = () => { const checkCacheAndFetchMembers = () => { const cachedMembers = localStorage.getItem('cachedMembers'); + const cachedOfficialMembers = localStorage.getItem('cachedOfficialMembers'); const cachedMembersTimestamp = localStorage.getItem('cachedMembersTimestamp'); const cachedRequests = localStorage.getItem('cachedRequests'); const cachedRequestsTimestamp = localStorage.getItem('cachedRequestsTimestamp'); @@ -61,10 +67,20 @@ const Membership = () => { return Date.now() - parseInt(timestamp, 10) < 24 * 60 * 60 * 1000; }; - if (cachedMembers && cachedMembersTimestamp && isCacheValid(cachedMembersTimestamp) && - cachedRequests && cachedRequestsTimestamp && isCacheValid(cachedRequestsTimestamp)) { + if ( + cachedMembers && + cachedOfficialMembers && + cachedMembersTimestamp && + isCacheValid(cachedMembersTimestamp) && + cachedRequests && + cachedRequestsTimestamp && + isCacheValid(cachedRequestsTimestamp) + ) { + const studentsData = JSON.parse(cachedMembers); + setStudents(studentsData); + + setMembers(JSON.parse(cachedOfficialMembers)); - setMembers(JSON.parse(cachedMembers)); setRequestsWithDocuments(JSON.parse(cachedRequests)); setLoading(false); } else { From 981c570180f5e849824f3a057567a9cd1767129c Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 7 Sep 2024 15:31:19 -0500 Subject: [PATCH 4/4] add manually fetch data --- shpe-app-web/app/(main)/membership/page.tsx | 17 +++++++++++++- shpe-app-web/app/(main)/points/page.tsx | 23 ++++++++++++++++--- .../app/(main)/tools/shirt-tracker/page.tsx | 17 ++++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/shpe-app-web/app/(main)/membership/page.tsx b/shpe-app-web/app/(main)/membership/page.tsx index d8b744cc..51beddef 100644 --- a/shpe-app-web/app/(main)/membership/page.tsx +++ b/shpe-app-web/app/(main)/membership/page.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import Header from '@/components/Header'; import { getMembers, getMembersToVerify } from '@/api/firebaseUtils'; +import { FaSync } from "react-icons/fa"; import { SHPEEventLog } from '@/types/events'; import { User } from '@/types/user'; import { isMemberVerified, RequestWithDoc } from '@/types/membership'; @@ -162,6 +163,13 @@ const Membership = () => { } }; + const handleReload = async () => { + if (window.confirm("Are you sure you want to reload the members?")) { + setLoading(true); + await fetchMembers(); + } + }; + useEffect(() => { const unsubscribe = onAuthStateChanged(auth, (currentUser) => { if (currentUser) { @@ -189,7 +197,6 @@ const Membership = () => { return (
-
); }; diff --git a/shpe-app-web/app/(main)/points/page.tsx b/shpe-app-web/app/(main)/points/page.tsx index a1b2b204..826c91aa 100644 --- a/shpe-app-web/app/(main)/points/page.tsx +++ b/shpe-app-web/app/(main)/points/page.tsx @@ -5,7 +5,7 @@ import { getEvents, getMembers, updatePointsInFirebase } from "@/api/firebaseUti import { SHPEEvent, SHPEEventLog } from '@/types/events'; import { format } from 'date-fns'; import { User } from "@/types/user"; -import { FaChevronLeft, FaChevronRight, FaFilter, FaUndo, FaSave } from "react-icons/fa"; +import { FaChevronLeft, FaChevronRight, FaFilter, FaUndo, FaSave, FaSync } from "react-icons/fa"; import { httpsCallable } from "firebase/functions"; import { auth, functions } from "@/config/firebaseConfig"; import { onAuthStateChanged } from "firebase/auth"; @@ -40,8 +40,8 @@ const Points = () => { (month) => month.getFullYear() === currentMonthDate.getFullYear() && month.getMonth() === currentMonthDate.getMonth() ); const [currentMonthIndex, setCurrentMonthIndex] = useState(realCurrentMonthIndex !== -1 ? realCurrentMonthIndex : 0); - const fetchMembers = async () => { + setLoading(true); try { const response = await getMembers() as UserWithLogs[]; setMembers(response); @@ -61,7 +61,6 @@ const Points = () => { } }); } - return acc; }, {}); @@ -71,6 +70,8 @@ const Points = () => { localStorage.setItem('cachedMembersTimestamp', Date.now().toString()); } catch (error) { console.error('Error fetching members:', error); + } finally { + setLoading(false); } }; @@ -438,6 +439,14 @@ const Points = () => { }; + const handleReload = async () => { + if (window.confirm("Are you sure you want to reload the points?")) { + setLoading(true); + await fetchMembers(); + } + }; + + if (loading) { return (
@@ -663,6 +672,14 @@ const Points = () => {
+ + {/* Reload Button */} +
); } diff --git a/shpe-app-web/app/(main)/tools/shirt-tracker/page.tsx b/shpe-app-web/app/(main)/tools/shirt-tracker/page.tsx index 7d60922e..fb67c912 100644 --- a/shpe-app-web/app/(main)/tools/shirt-tracker/page.tsx +++ b/shpe-app-web/app/(main)/tools/shirt-tracker/page.tsx @@ -9,6 +9,7 @@ import { PublicUserInfo, User } from '@/types/user'; import { SHPEEventLog } from '@/types/events'; import { doc, updateDoc, Timestamp } from 'firebase/firestore'; import { isMemberVerified } from '@/types/membership'; +import { FaSync } from 'react-icons/fa'; const ShirtTracker = () => { const router = useRouter(); @@ -129,6 +130,14 @@ const ShirtTracker = () => { return () => unsubscribe(); }, [router]); + const handleReload = async () => { + if (window.confirm("Are you sure you want to reload the points?")) { + setLoading(true); + await fetchMembers(); + } + }; + + if (loading) { return (
@@ -182,6 +191,14 @@ const ShirtTracker = () => {
+ + {/* Reload Button */} + ); };