From c7ca37854764c39fc70b40aef4093f86372d8e75 Mon Sep 17 00:00:00 2001 From: Jeet Parikh Date: Sun, 9 Mar 2025 12:35:08 -0700 Subject: [PATCH 1/9] create user document on signInWithGoogle --- src/firebase/auth.ts | 98 +++++++++++++++++++++++++++++--------------- 1 file changed, 64 insertions(+), 34 deletions(-) diff --git a/src/firebase/auth.ts b/src/firebase/auth.ts index 439c6d6..67b1cd6 100644 --- a/src/firebase/auth.ts +++ b/src/firebase/auth.ts @@ -4,22 +4,45 @@ import { MouseEventHandler, useContext } from 'react'; import { getAccountId, getAccountName, + getAccountEmail, + checkIfLoggedIn, updateAnonymousUserToAuthUser, } from './events'; -import { auth, googleProvider } from './firebase'; +import { db, auth, googleProvider } from './firebase'; import { onAuthStateChanged, signInWithPopup, signInWithRedirect, signOut, } from 'firebase/auth'; +import { getFirestore, doc, getDoc, setDoc } from 'firebase/firestore'; import { GAPIContext } from './gapiContext'; import { get } from 'http'; +const handleAddingUserToDB = async () => { + try { + const userId = getAccountId(); + const userEmail = getAccountEmail(); + const userDocRef = doc(db, 'users', userId); + + const userDoc = await getDoc(userDocRef); + if (!userDoc.exists()) { + await setDoc(userDocRef, { + email: userEmail, // Set email from Google profile + name: getAccountName() || '', // Set first name from Google profile + selectedCalendarIDs: [userEmail], // Defaul value in user's email (primary cal) + uid: userId, // Set the UID + userEvents: [], // Initialize as empty array + }); + } + } catch (err) { + console.error(err); + } +}; + // Google sign in // returns error message - const signInWithGoogle = async ( clickEvent?: any, gapi?: any, @@ -46,13 +69,29 @@ const signInWithGoogle = async ( if (formerName !== '') { updateAnonymousUserToAuthUser(formerName) .then(() => { - resolve(true); + // Add user to database after successful sign-in + handleAddingUserToDB() + .then(() => { + resolve(true); + }) + .catch((dbError) => { + console.error('Error adding user to database:', dbError); + resolve(true); // Still resolve true since login succeeded + }); }) .catch((updateError) => { reject(updateError); }); } else { - resolve(true); + // Add user to database after successful sign-in + handleAddingUserToDB() + .then(() => { + resolve(true); + }) + .catch((dbError) => { + console.error('Error adding user to database:', dbError); + resolve(true); // Still resolve true since login succeeded + }); } }) .catch((error: any) => { @@ -71,13 +110,32 @@ const signInWithGoogle = async ( if (formerName !== '') { updateAnonymousUserToAuthUser(formerName) .then(() => { - resolve(true); + // Add user to database after successful sign-in + handleAddingUserToDB() + .then(() => { + resolve(true); + }) + .catch((dbError) => { + console.error( + 'Error adding user to database:', + dbError + ); + resolve(true); // Still resolve true since login succeeded + }); }) .catch((updateError) => { resolve(false); }); } else { - resolve(true); + // Add user to database after successful sign-in + handleAddingUserToDB() + .then(() => { + resolve(true); + }) + .catch((dbError) => { + console.error('Error adding user to database:', dbError); + resolve(true); // Still resolve true since login succeeded + }); } }) .catch((error) => { @@ -96,34 +154,6 @@ const signInWithGoogle = async ( }); }; -// useEffect(() => { -// if (!authInstance) { -// return; - -// } -// if (authInstance.isSignedIn.get()) { -// setUser(authInstance.currentUser.get()); - -// } else { -// const signInButton = document.getElementById('auth'); -// authInstance.attachClickHandler( -// signInButton, -// {}, -// (googleUser) => { -// if (signInButton && signInButton.id == 'auth') { -// setUser(googleUser); -// createCalendarEvent(getEventObjectForGCal()); -// signInButton.id = 'sync'; -// } -// }, -// (error) => { -// console.log("Error: ", error); -// // alert('[GCAL]: Error signing in to Google Calendar: ' + error); -// }, -// ); -// } -// }, [authInstance, user, createCalendarEvent]); - // logout const logout = (loadedGAPI: typeof globalThis.gapi | null) => { if (loadedGAPI === null) { From f9ac52c12bf65162d1a38134a5e51198959dfd35 Mon Sep 17 00:00:00 2001 From: Jeet Parikh Date: Sun, 9 Mar 2025 13:04:26 -0700 Subject: [PATCH 2/9] add event info to admin's users collection when event is created --- src/firebase/events.ts | 733 ++++++++++++++++++++++++----------------- 1 file changed, 438 insertions(+), 295 deletions(-) diff --git a/src/firebase/events.ts b/src/firebase/events.ts index a96596c..49a5b24 100644 --- a/src/firebase/events.ts +++ b/src/firebase/events.ts @@ -1,12 +1,37 @@ /* eslint-disable */ -import { doc, collection, getDoc, setDoc, updateDoc, CollectionReference, DocumentData, getDocs, Timestamp, arrayUnion, query, where, QuerySnapshot, deleteDoc, writeBatch, FieldValue, deleteField } from 'firebase/firestore' -import { Availability, Event, Location, EventDetails, EventId, Participant } from '../types' -import { auth, db } from './firebase' -import { generateTimeBlocks } from '../components/utils/functions/generateTimeBlocks' -import { time } from 'console' -import { start } from 'repl' -import { DateTime } from "luxon"; +import { + doc, + collection, + getDoc, + setDoc, + updateDoc, + CollectionReference, + DocumentData, + getDocs, + Timestamp, + arrayUnion, + query, + where, + QuerySnapshot, + deleteDoc, + writeBatch, + FieldValue, + deleteField, +} from 'firebase/firestore'; +import { + Availability, + Event, + Location, + EventDetails, + EventId, + Participant, +} from '../types'; +import { auth, db } from './firebase'; +import { generateTimeBlocks } from '../components/utils/functions/generateTimeBlocks'; +import { time } from 'console'; +import { start } from 'repl'; +import { DateTime } from 'luxon'; // ASSUME names are unique within an event @@ -23,21 +48,24 @@ let workingEvent: Event = { plausibleLocations: ['HH17', 'Sterling'], timeZone: 'America/New_York', }, - participants: [] -} + participants: [], +}; const checkIfLoggedIn = (): boolean => { - return (auth.currentUser && !auth.currentUser.isAnonymous) || false -} + return (auth.currentUser && !auth.currentUser.isAnonymous) || false; +}; const checkIfAdmin = (): boolean => { if (workingEvent && auth.currentUser) { - if (workingEvent.details.adminAccountId && workingEvent.details.adminAccountId == getAccountId()) { - return true + if ( + workingEvent.details.adminAccountId && + workingEvent.details.adminAccountId == getAccountId() + ) { + return true; } } - return false -} + return false; +}; /** * @@ -45,30 +73,31 @@ const checkIfAdmin = (): boolean => { */ const getAccountId = (): string => { if (auth.currentUser !== null && !auth.currentUser.isAnonymous) { - return auth.currentUser.uid + return auth.currentUser.uid; } else { - return '' + return ''; } -} +}; const getAccountName = (): string => { - return auth.currentUser?.displayName ? auth.currentUser.displayName : '' -} + return auth.currentUser?.displayName ? auth.currentUser.displayName : ''; +}; const getAccountEmail = (): string => { - return auth.currentUser?.email ? auth.currentUser.email : '' -} + return auth.currentUser?.email ? auth.currentUser.email : ''; +}; async function generateUniqueEventKey(): Promise { let uniqueKeyFound = false; - let newKey = ""; + let newKey = ''; while (!uniqueKeyFound) { - newKey = doc(collection(db, "events")).id.substring(0, 6).toUpperCase(); - const docRef = doc(db, "events", newKey); + newKey = doc(collection(db, 'events')).id.substring(0, 6).toUpperCase(); + const docRef = doc(db, 'events', newKey); const existingDoc = await getDoc(docRef); - if (!existingDoc.exists()) { // possible race conditions (with this along with previous iteration) + if (!existingDoc.exists()) { + // possible race conditions (with this along with previous iteration) uniqueKeyFound = true; } } @@ -81,209 +110,277 @@ async function generateUniqueEventKey(): Promise { // Will throw an error if the document cannot be found // Event is not return; rather, it is stored internally // for other getters and settings defined in this file. -async function getEventById (id: EventId): Promise { - const eventsRef = collection(db, 'events') +async function getEventById(id: EventId): Promise { + const eventsRef = collection(db, 'events'); await new Promise((resolve, reject) => { - getDoc(doc(eventsRef, id)).then(async (result) => { - if (result.exists()) { - workingEvent = result.data() as Event - workingEvent.details.startTime = workingEvent.details.startTime ? (workingEvent.details.startTime as unknown as Timestamp).toDate() : workingEvent.details.startTime - workingEvent.details.endTime = workingEvent.details.endTime ? (workingEvent.details.endTime as unknown as Timestamp).toDate() : workingEvent.details.endTime - workingEvent.details.chosenStartDate = workingEvent.details.chosenStartDate ? (workingEvent.details.chosenStartDate as unknown as Timestamp).toDate() : workingEvent.details.chosenStartDate - workingEvent.details.chosenEndDate = workingEvent.details.chosenEndDate ? (workingEvent.details.chosenEndDate as unknown as Timestamp).toDate() : workingEvent.details.chosenEndDate - workingEvent.details.dates = dateToArray(workingEvent.details.dates) - - // Retrieve all participants as sub-collection - await getParticipants(collection(db, 'events', id, 'participants')).then((parts) => { - workingEvent.participants = parts - }).catch((err) => { - reject(err) - }) - resolve() - } else { - reject('Document not found') - } - }).catch((err) => { - console.error('Caught ' + err + ' with message ' + err.msg) - reject(err) - }) - }) + getDoc(doc(eventsRef, id)) + .then(async (result) => { + if (result.exists()) { + workingEvent = result.data() as Event; + workingEvent.details.startTime = workingEvent.details.startTime + ? (workingEvent.details.startTime as unknown as Timestamp).toDate() + : workingEvent.details.startTime; + workingEvent.details.endTime = workingEvent.details.endTime + ? (workingEvent.details.endTime as unknown as Timestamp).toDate() + : workingEvent.details.endTime; + workingEvent.details.chosenStartDate = workingEvent.details + .chosenStartDate + ? ( + workingEvent.details.chosenStartDate as unknown as Timestamp + ).toDate() + : workingEvent.details.chosenStartDate; + workingEvent.details.chosenEndDate = workingEvent.details + .chosenEndDate + ? ( + workingEvent.details.chosenEndDate as unknown as Timestamp + ).toDate() + : workingEvent.details.chosenEndDate; + workingEvent.details.dates = dateToArray(workingEvent.details.dates); + + // Retrieve all participants as sub-collection + await getParticipants(collection(db, 'events', id, 'participants')) + .then((parts) => { + workingEvent.participants = parts; + }) + .catch((err) => { + reject(err); + }); + resolve(); + } else { + reject('Document not found'); + } + }) + .catch((err) => { + console.error('Caught ' + err + ' with message ' + err.msg); + reject(err); + }); + }); } -async function deleteEvent (id: EventId): Promise { +async function deleteEvent(id: EventId): Promise { // Prevent any user other than the admin to delete event - if (getAccountId() !== ((await getDoc(doc(db, 'events', id))).data() as unknown as Event).details.adminAccountId) { - throw Error('Only creator can delete event') + if ( + getAccountId() !== + ((await getDoc(doc(db, 'events', id))).data() as unknown as Event).details + .adminAccountId + ) { + throw Error('Only creator can delete event'); } // Get a new write batch - const batch = writeBatch(db) + const batch = writeBatch(db); // Add each participant doc in the subcollection - const participants = await getDocs(collection(db, 'events', id, 'participants')) + const participants = await getDocs( + collection(db, 'events', id, 'participants') + ); participants.forEach((data) => { - batch.delete(doc(db, 'events', id, 'participants', data.id)) - }) + batch.delete(doc(db, 'events', id, 'participants', data.id)); + }); // delete the event itself - batch.delete(doc(db, 'events', id)) + batch.delete(doc(db, 'events', id)); // Commit the batch await batch.commit().catch((err) => { - console.log('Error: ', err) - }) + console.log('Error: ', err); + }); } // Retrieves all events that this user has submitted availability for -async function getAllEventsForUser (accountID: string): Promise { - const eventsRef = collection(db, 'events') +async function getAllEventsForUser(accountID: string): Promise { + const eventsRef = collection(db, 'events'); return await new Promise(async (resolve, reject) => { - const q = query(eventsRef, where('participants', 'array-contains', accountID)) - const querySnapshot = await getDocs(q) + const q = query( + eventsRef, + where('participants', 'array-contains', accountID) + ); + const querySnapshot = await getDocs(q); - const eventsList: Event[] = [] + const eventsList: Event[] = []; querySnapshot.forEach((doc) => { - const result = doc.data() - result.details.startTime = result.details.startTime ? (result.details.startTime as unknown as Timestamp).toDate() : result.details.startTime - result.details.endTime = result.details.endTime ? (result.details.endTime as unknown as Timestamp).toDate() : result.details.endTime - result.details.chosenStartDate = result.details.chosenStartDate ? (result.details.chosenStartDate as unknown as Timestamp).toDate() : result.details.chosenStartDate - result.details.chosenEndDate = result.details.chosenEndDate ? (result.details.chosenEndDate as unknown as Timestamp).toDate() : result.details.chosenEndDate - result.details.dates = dateToArray(result.details.dates) - - eventsList.push(result as unknown as Event) - }) + const result = doc.data(); + result.details.startTime = result.details.startTime + ? (result.details.startTime as unknown as Timestamp).toDate() + : result.details.startTime; + result.details.endTime = result.details.endTime + ? (result.details.endTime as unknown as Timestamp).toDate() + : result.details.endTime; + result.details.chosenStartDate = result.details.chosenStartDate + ? (result.details.chosenStartDate as unknown as Timestamp).toDate() + : result.details.chosenStartDate; + result.details.chosenEndDate = result.details.chosenEndDate + ? (result.details.chosenEndDate as unknown as Timestamp).toDate() + : result.details.chosenEndDate; + result.details.dates = dateToArray(result.details.dates); + + eventsList.push(result as unknown as Event); + }); - resolve(eventsList) - }) + resolve(eventsList); + }); } // Stores a new event passed in as a parameter to the backend -async function createEvent (eventDetails: EventDetails): Promise { - const id = await generateUniqueEventKey() +async function createEvent(eventDetails: EventDetails): Promise { + const id = await generateUniqueEventKey(); const newEvent: Event = { details: { ...eventDetails, // TODO map dates to JSON - dates: eventDetails.dates + dates: eventDetails.dates, }, publicId: id, - participants: [] - } + participants: [], + }; // Update local copy - workingEvent = newEvent + workingEvent = newEvent; // Update backend return await new Promise((resolve, reject) => { - const eventsRef = collection(db, 'events') + // Add event to events collection + const eventsRef = collection(db, 'events'); setDoc(doc(eventsRef, id), { ...newEvent, - participants: [eventDetails.adminAccountId] - }) // addDoc as overwrite-safe alt + participants: [eventDetails.adminAccountId], + }) .then((result: void) => { - resolve(newEvent) - }).catch((err) => { - reject(err) + // Add event to admin user's events field + const userRef = doc(db, 'users', eventDetails.adminAccountId); // Reference to the user's document + const eventDetailsForUser = { + code: id, // Using event ID as the event code + timestamp: new Date(), // Timestamp when the event is added + isAdmin: true, // Admin status for this event + }; + updateDoc(userRef, { + userEvents: arrayUnion(eventDetailsForUser), + }) + .then(() => { + resolve(newEvent); + }) + .catch((err) => { + reject(err); + }); }) - }) + .catch((err) => { + reject(err); + }); + }); } // For internal use // Returns undefined when participant has not been added yet -const getParticipantIndex = (name: string, accountId: string = ''): number | undefined => { - let index +const getParticipantIndex = ( + name: string, + accountId: string = '' +): number | undefined => { + let index; for (let i = 0; i < workingEvent.participants.length; i++) { - if ((accountId === '' && workingEvent.participants[i].name == name) || - (accountId !== '' && workingEvent.participants[i].accountId == accountId)) { - index = i + if ( + (accountId === '' && workingEvent.participants[i].name == name) || + (accountId !== '' && workingEvent.participants[i].accountId == accountId) + ) { + index = i; } } - return index -} + return index; +}; // For internal use -const getParticipants = async (reference: CollectionReference): Promise => { +const getParticipants = async ( + reference: CollectionReference +): Promise => { return await new Promise((resolve, reject) => { - getDocs(reference).then((docs) => { - const parts: Participant[] = [] - docs.forEach((data) => { - // unstringify availability - const participant = data.data() - participant.availability = JSON.parse(participant.availability) - - parts.push(participant as Participant) + getDocs(reference) + .then((docs) => { + const parts: Participant[] = []; + docs.forEach((data) => { + // unstringify availability + const participant = data.data(); + participant.availability = JSON.parse(participant.availability); + + parts.push(participant as Participant); + }); + + resolve(parts); }) - - resolve(parts) - }).catch((err) => { - console.error('Caught ' + err + ' with message ' + err.msg) - reject(err) - }) - }) -} + .catch((err) => { + console.error('Caught ' + err + ' with message ' + err.msg); + reject(err); + }); + }); +}; // For internal use // Updates (overwrites) the event details of the working event // with the eventDetails parameter -async function saveEventDetails (eventDetails: EventDetails) { +async function saveEventDetails(eventDetails: EventDetails) { // Update local copy - workingEvent.details = eventDetails + workingEvent.details = eventDetails; // Update backend await new Promise((resolve, reject) => { - const eventsRef = collection(db, 'events') + const eventsRef = collection(db, 'events'); updateDoc(doc(eventsRef, workingEvent.publicId), { - details: eventDetails - }).then(() => { - resolve() - }).catch((err) => { - console.error(err.msg) - reject(err) + details: eventDetails, }) - }) + .then(() => { + resolve(); + }) + .catch((err) => { + console.error(err.msg); + reject(err); + }); + }); } // For internal use // Updates the participants list of the working event // with the participant passed in, overwriting if they already exist -async function saveParticipantDetails (participant: Participant): Promise { - participant.email = getAccountEmail() +async function saveParticipantDetails(participant: Participant): Promise { + participant.email = getAccountEmail(); // Update local copy - let flag = false + let flag = false; workingEvent.participants.forEach((part, index) => { - if ((participant.accountId !== '' && part.accountId == participant.accountId) || - ((participant.accountId == '') && (participant.name == part.name))) { - workingEvent.participants[index] = participant - flag = true + if ( + (participant.accountId !== '' && + part.accountId == participant.accountId) || + (participant.accountId == '' && participant.name == part.name) + ) { + workingEvent.participants[index] = participant; + flag = true; } - }) + }); if (!flag) { - workingEvent.participants.push(participant) + workingEvent.participants.push(participant); // Update Backend: add user uid to particpants list of event object - const accountId = getAccountId() + const accountId = getAccountId(); if (accountId && accountId !== '') { - const eventsRef = collection(db, 'events') + const eventsRef = collection(db, 'events'); updateDoc(doc(eventsRef, workingEvent.publicId), { - participants: arrayUnion(accountId) - + participants: arrayUnion(accountId), }).catch((err) => { - console.error(err.msg) - }) + console.error(err.msg); + }); } } // Update backend await new Promise((resolve, reject) => { - const eventsRef = collection(db, 'events') - const participantsRef = collection(doc(eventsRef, workingEvent.publicId), 'participants') - let partRef + const eventsRef = collection(db, 'events'); + const participantsRef = collection( + doc(eventsRef, workingEvent.publicId), + 'participants' + ); + let partRef; if (participant.accountId) { - partRef = doc(participantsRef, participant.accountId) + partRef = doc(participantsRef, participant.accountId); } else { - partRef = doc(participantsRef, participant.name) + partRef = doc(participantsRef, participant.name); } setDoc(partRef, { @@ -291,73 +388,81 @@ async function saveParticipantDetails (participant: Participant): Promise accountId: participant.accountId || '', email: getAccountEmail(), availability: JSON.stringify(participant.availability), - location: participant.location || '' - - }).then(() => { - resolve() - }).catch((err) => { - console.error('Failed to save participant details. Error', err.msg) - reject(err) + location: participant.location || '', }) - }) + .then(() => { + resolve(); + }) + .catch((err) => { + console.error('Failed to save participant details. Error', err.msg); + reject(err); + }); + }); } -async function updateAnonymousUserToAuthUser (name: string) { - const accountName = getAccountName() - const accountId = getAccountId() +async function updateAnonymousUserToAuthUser(name: string) { + const accountName = getAccountName(); + const accountId = getAccountId(); try { - if (accountName === '') Promise.reject('User is not signed in') - const eventsRef = collection(db, 'events') - const participantsRef = collection(doc(eventsRef, workingEvent.publicId), 'participants') + if (accountName === '') Promise.reject('User is not signed in'); + const eventsRef = collection(db, 'events'); + const participantsRef = collection( + doc(eventsRef, workingEvent.publicId), + 'participants' + ); - const anonymousPartRef = doc(participantsRef, name) - const authedPartRef = doc(participantsRef, accountId) + const anonymousPartRef = doc(participantsRef, name); + const authedPartRef = doc(participantsRef, accountId); // Update local - let availability + let availability; workingEvent.participants.forEach((part, index) => { if (part.name == name && part.accountId == '') { - workingEvent.participants[index].accountId = accountName - workingEvent.participants[index].accountId = accountId - availability = workingEvent.participants[index].availability + workingEvent.participants[index].accountId = accountName; + workingEvent.participants[index].accountId = accountId; + availability = workingEvent.participants[index].availability; } - }) + }); // Anonymous user has not submitted their availability yet // So nothing to update. - if (availability === undefined) return + if (availability === undefined) return; // Update Backend - const batch = writeBatch(db) + const batch = writeBatch(db); // Delete old anonymous doc - batch.delete(anonymousPartRef) + batch.delete(anonymousPartRef); // Create (update) new authed doc with old availability batch.set(authedPartRef, { name: accountName, email: getAccountEmail(), accountId, - availability: JSON.stringify(availability) - }) + availability: JSON.stringify(availability), + }); // This updates the participants list in the event details object batch.update(doc(eventsRef, workingEvent.publicId), { - participants: arrayUnion(getAccountId()) - }) + participants: arrayUnion(getAccountId()), + }); - await batch.commit(); return + await batch.commit(); + return; } catch (err) { - console.error(err) - }; + console.error(err); + } } -async function wrappedSaveParticipantDetails (availability: Availability, locations: Location[] | undefined): Promise { - let name = getAccountName() +async function wrappedSaveParticipantDetails( + availability: Availability, + locations: Location[] | undefined +): Promise { + let name = getAccountName(); if (!name) { - console.warn('User not signed in') - name = 'John Doe' + console.warn('User not signed in'); + name = 'John Doe'; } await saveParticipantDetails({ @@ -365,173 +470,199 @@ async function wrappedSaveParticipantDetails (availability: Availability, locati accountId: getAccountId(), email: getAccountEmail(), availability: JSON.stringify(availability), - location: locations !== undefined ? locations : [] - }) + location: locations !== undefined ? locations : [], + }); } // Sets the official date for the event; must be called by the admin -async function setChosenDate (chosenStartDate: Date | undefined, chosenEndDate: Date | undefined): Promise { - if ((chosenStartDate === undefined && chosenEndDate !== undefined) || - (chosenStartDate !== undefined && chosenEndDate === undefined)) { - throw ('Both start and end dates must be defined or undefined, not one of each!') +async function setChosenDate( + chosenStartDate: Date | undefined, + chosenEndDate: Date | undefined +): Promise { + if ( + (chosenStartDate === undefined && chosenEndDate !== undefined) || + (chosenStartDate !== undefined && chosenEndDate === undefined) + ) { + throw 'Both start and end dates must be defined or undefined, not one of each!'; // unselect chosen date } else if (chosenStartDate === undefined && chosenEndDate === undefined) { - delete workingEvent.details.chosenStartDate - delete workingEvent.details.chosenEndDate + delete workingEvent.details.chosenStartDate; + delete workingEvent.details.chosenEndDate; // make sure the order is correct - - } else if (chosenStartDate !== undefined && chosenEndDate !== undefined && chosenEndDate > chosenStartDate) { - workingEvent.details.chosenStartDate = chosenStartDate - workingEvent.details.chosenEndDate = chosenEndDate + } else if ( + chosenStartDate !== undefined && + chosenEndDate !== undefined && + chosenEndDate > chosenStartDate + ) { + workingEvent.details.chosenStartDate = chosenStartDate; + workingEvent.details.chosenEndDate = chosenEndDate; } else { - workingEvent.details.chosenStartDate = chosenEndDate - workingEvent.details.chosenEndDate = chosenStartDate + workingEvent.details.chosenStartDate = chosenEndDate; + workingEvent.details.chosenEndDate = chosenStartDate; } - await saveEventDetails(workingEvent.details) + await saveEventDetails(workingEvent.details); } // Sets the official location for the event; must be called by the admin -async function setChosenLocation (chosenLocation: Location | undefined): Promise { +async function setChosenLocation( + chosenLocation: Location | undefined +): Promise { if (chosenLocation === undefined) { - delete workingEvent.details.chosenLocation + delete workingEvent.details.chosenLocation; } - workingEvent.details.chosenLocation = chosenLocation + workingEvent.details.chosenLocation = chosenLocation; - await saveEventDetails(workingEvent.details) + await saveEventDetails(workingEvent.details); } // Retrieves the list of locations to be considered for the event -function getLocationOptions (): Location[] { - return workingEvent.details.plausibleLocations +function getLocationOptions(): Location[] { + return workingEvent.details.plausibleLocations; } // Retrieves the name of the event -function getEventName (): string { - return workingEvent.details.name +function getEventName(): string { + return workingEvent.details.name; } // Retrieves the description of the event -function getEventDescription (): string { - return workingEvent.details.description +function getEventDescription(): string { + return workingEvent.details.description; } // To be called on render when a page loads with event id in url -async function getEventOnPageload (id: string): Promise { - await getEventById(id.toUpperCase()); return +async function getEventOnPageload(id: string): Promise { + await getEventById(id.toUpperCase()); + return; // To avoid caching (TODO?) if (workingEvent && workingEvent.publicId == id.toUpperCase()) { - await Promise.resolve(); return + await Promise.resolve(); + return; } else { - await getEventById(id.toUpperCase()); return + await getEventById(id.toUpperCase()); + return; } } // Retrieves the availability object of the participant matching `name` -function getAvailabilityByName (name: string): Availability | undefined { +function getAvailabilityByName(name: string): Availability | undefined { for (let i = 0; i < workingEvent.participants.length; i++) { if (workingEvent.participants[i].name == name) { //@ts-expect-error - return JSON.parse(workingEvent.participants[i].availability) + return JSON.parse(workingEvent.participants[i].availability); // this arises because to store an availability object in Firestore, it must be stringified, but the frontend uses a JSON object } } } // Retrieves the availability object of the participant matching `accountId` -function getAvailabilityByAccountId (accountId: string): Availability | undefined { +function getAvailabilityByAccountId( + accountId: string +): Availability | undefined { for (let i = 0; i < workingEvent.participants.length; i++) { if (workingEvent.participants[i].accountId == accountId) { // @ts-expect-error - return JSON.parse(workingEvent.participants[i].availability) + return JSON.parse(workingEvent.participants[i].availability); } } } // Retrieves the list of participants // GUARANTEED to be the in the same order as getAllAvailabilities -function getAllAvailabilitiesNames (): string[] { - const names: string[] = [] +function getAllAvailabilitiesNames(): string[] { + const names: string[] = []; for (let i = 0; i < workingEvent.participants.length; i++) { - names.push(workingEvent.participants[i].name) + names.push(workingEvent.participants[i].name); } - return names + return names; } // Retrieves the list of availabilities // GUARANTEED to be the in the same order as getAllAvailabilitiesNames -function getAllAvailabilities (): Availability[] { - const avails: Availability[] = [] +function getAllAvailabilities(): Availability[] { + const avails: Availability[] = []; for (let i = 0; i < workingEvent.participants.length; i++) { // @ts-expect-error - avails.push(JSON.parse(workingEvent.participants[i].availability)) + avails.push(JSON.parse(workingEvent.participants[i].availability)); } - return avails + return avails; } // Retrieves the official datetime (start and end) of the event as chosen by the admin -function getChosenDayAndTime (): [Date, Date] | undefined { - if (workingEvent.details.chosenStartDate && workingEvent.details.chosenEndDate) { - return [workingEvent.details.chosenStartDate, workingEvent.details.chosenEndDate] +function getChosenDayAndTime(): [Date, Date] | undefined { + if ( + workingEvent.details.chosenStartDate && + workingEvent.details.chosenEndDate + ) { + return [ + workingEvent.details.chosenStartDate, + workingEvent.details.chosenEndDate, + ]; } } // Retrieves the official location of the event as chosen by the admin -function getChosenLocation (): Location | undefined { - return workingEvent.details.chosenLocation +function getChosenLocation(): Location | undefined { + return workingEvent.details.chosenLocation; } // Retrieves the dict objects mapping locations (keys) to number of votes (items) -function getLocationsVotes (): Record | any { - const votes: Record = {} +function getLocationsVotes(): Record | any { + const votes: Record = {}; for (let i = 0; i < workingEvent.details.plausibleLocations.length; i++) { - const location = workingEvent.details.plausibleLocations[i] - votes[location] = 0 + const location = workingEvent.details.plausibleLocations[i]; + votes[location] = 0; for (let i = 0; i < workingEvent.participants.length; i++) { - const participant = workingEvent.participants[i] + const participant = workingEvent.participants[i]; if (participant.location.includes(location)) { - votes[location] += 1 + votes[location] += 1; } } } - return votes + return votes; } // Retrieves the availability object of the participant matching `name` -function getLocationVotesByName (name: string): Location[] | undefined { +function getLocationVotesByName(name: string): Location[] | undefined { for (let i = 0; i < workingEvent.participants.length; i++) { if (workingEvent.participants[i].name == name) { - return workingEvent.participants[i].location + return workingEvent.participants[i].location; } } - return undefined + return undefined; } // Retrieves the availability object of the participant matching `accountId` -function getLocationVotesByAccountId (accountId: string): Location[] | undefined { +function getLocationVotesByAccountId( + accountId: string +): Location[] | undefined { for (let i = 0; i < workingEvent.participants.length; i++) { if (workingEvent.participants[i].accountId == accountId) { - return workingEvent.participants[i].location + return workingEvent.participants[i].location; } } } -function getEmails (): string[] { - const emails: string[] = [] +function getEmails(): string[] { + const emails: string[] = []; for (let i = 0; i < workingEvent.participants.length; i++) { - if (workingEvent.participants[i].email !== undefined && workingEvent.participants[i].email !== '') { - emails.push(workingEvent.participants[i].email || '') + if ( + workingEvent.participants[i].email !== undefined && + workingEvent.participants[i].email !== '' + ) { + emails.push(workingEvent.participants[i].email || ''); } } - return emails + return emails; } -function getZoomLink (): string | undefined { - return workingEvent.details.zoomLink || undefined +function getZoomLink(): string | undefined { + return workingEvent.details.zoomLink || undefined; } function getDates(): Date[] { @@ -543,9 +674,11 @@ function getDates(): Date[] { return dates; } - dates = dates.map(date => { + dates = dates.map((date) => { // Treat the date as UTC and convert to the target timezone - return DateTime.fromJSDate(date, { zone: 'utc' }).setZone(timeZone, { keepLocalTime: true }).toJSDate(); + return DateTime.fromJSDate(date, { zone: 'utc' }) + .setZone(timeZone, { keepLocalTime: true }) + .toJSDate(); }); // Convert the start and end times to the user's time zone @@ -555,24 +688,32 @@ function getDates(): Date[] { // Get the original DateTime objects in the creator's time zone (without converting to ISO date) const startInCreatorZone = DateTime.fromJSDate(startTime).setZone(timeZone); const endInCreatorZone = DateTime.fromJSDate(endTime).setZone(timeZone); - + // Determine if the time range crosses into another day let adjustedDates = [...dates]; const startDateUserZone = startInUserZone?.toISODate(); const startDateCreatorZone = startInCreatorZone?.toISODate(); const endDateUserZone = endInUserZone?.toISODate(); const endDateCreatorZone = endInCreatorZone?.toISODate(); - - if (startDateUserZone && startDateCreatorZone && startDateUserZone < startDateCreatorZone) { + + if ( + startDateUserZone && + startDateCreatorZone && + startDateUserZone < startDateCreatorZone + ) { // If the start date is earlier in the user's time zone (crosses backward) - adjustedDates = adjustedDates.map(date => { + adjustedDates = adjustedDates.map((date) => { const adjustedDate = new Date(date); adjustedDate.setDate(date.getDate() - 1); // Shift the date backward by 1 return adjustedDate; }); - } else if (endDateUserZone && endDateCreatorZone && endDateUserZone < endDateCreatorZone) { + } else if ( + endDateUserZone && + endDateCreatorZone && + endDateUserZone < endDateCreatorZone + ) { // If the end date is later in the user's time zone (crosses forward) - adjustedDates = adjustedDates.map(date => { + adjustedDates = adjustedDates.map((date) => { const adjustedDate = new Date(date); adjustedDate.setDate(date.getDate() + 1); // Shift the date forward by 1 return adjustedDate; @@ -583,8 +724,7 @@ function getDates(): Date[] { return adjustedDates; } - -function getStartAndEndTimes (): Date[] { +function getStartAndEndTimes(): Date[] { let startTime = workingEvent.details.startTime; let endTime = workingEvent.details.endTime; @@ -592,92 +732,97 @@ function getStartAndEndTimes (): Date[] { } function getTimezone(): string { - return workingEvent.details.timeZone + return workingEvent.details.timeZone; } -function getEventObjectForGCal (startDate: Date, endDate: Date, location?: string) { - +function getEventObjectForGCal( + startDate: Date, + endDate: Date, + location?: string +) { return { summary: workingEvent.details.name, - location: location == undefined ? "" : location, + location: location == undefined ? '' : location, description: workingEvent.details.description, start: { dateTime: startDate, - timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, }, end: { dateTime: endDate, - timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone - + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, }, attendees: workingEvent.participants - .filter((participant: Participant) => - participant.email === undefined || participant.email !== '' + .filter( + (participant: Participant) => + participant.email === undefined || participant.email !== '' ) .map((participant: Participant) => ({ email: participant.email, })), - } + }; } async function setNewEventName(newName: string | undefined) { if (newName == undefined) { - return Promise.resolve() + return Promise.resolve(); } workingEvent.details.name = newName; - await saveEventDetails(workingEvent.details) + await saveEventDetails(workingEvent.details); } async function setNewEventDescription(newDescription: string | undefined) { if (newDescription == undefined) { - return Promise.resolve() + return Promise.resolve(); } workingEvent.details.description = newDescription; - await saveEventDetails(workingEvent.details) + await saveEventDetails(workingEvent.details); } async function setNewStartTimes(newStartTime: Date | undefined) { if (newStartTime == undefined) { - return Promise.resolve() + return Promise.resolve(); } workingEvent.details.startTime = newStartTime; - await saveEventDetails(workingEvent.details) + await saveEventDetails(workingEvent.details); } async function setNewEndTimes(newEndTime: Date | undefined) { if (newEndTime == undefined) { - return Promise.resolve() + return Promise.resolve(); } workingEvent.details.endTime = newEndTime; - await saveEventDetails(workingEvent.details) + await saveEventDetails(workingEvent.details); } -async function setNewDates(newDates:Date[] | undefined) { +async function setNewDates(newDates: Date[] | undefined) { if (newDates == undefined) { - return Promise.resolve() + return Promise.resolve(); } for (let p = 0; p < workingEvent.participants.length; p++) { - let timeBlocks = generateTimeBlocks(workingEvent.details.chosenStartDate, workingEvent.details.chosenEndDate); + let timeBlocks = generateTimeBlocks( + workingEvent.details.chosenStartDate, + workingEvent.details.chosenEndDate + ); let availability = []; for (let i = 0; i < timeBlocks.length; i++) { - availability.push(new Array(newDates.length).fill(false)); + availability.push(new Array(newDates.length).fill(false)); } workingEvent.participants[p].availability = availability; } - await saveEventDetails(workingEvent.details) + await saveEventDetails(workingEvent.details); } async function undoAdminSelections() { - workingEvent.details.chosenLocation = ""; + workingEvent.details.chosenLocation = ''; workingEvent.details.chosenStartDate = new Date(1970, 1, 1, 10, 15, 30); workingEvent.details.chosenEndDate = new Date(1970, 1, 1, 10, 15, 30); await saveEventDetails(workingEvent.details); } - -export { workingEvent } // For interal use; use getters and setters below +export { workingEvent }; // For interal use; use getters and setters below export { // Firebase Wrappers @@ -731,25 +876,23 @@ export { setNewStartTimes, setNewEndTimes, setNewDates, + getParticipantIndex, +}; - getParticipantIndex - -} - -function dateToObject (dateArray: number[][]): Record { - const dateObject: Record = {} +function dateToObject(dateArray: number[][]): Record { + const dateObject: Record = {}; dateArray.forEach((date: number[], index: number) => { - dateObject[index + 1] = date - }) - return dateObject + dateObject[index + 1] = date; + }); + return dateObject; } -function dateToArray (obj: Record): Date[] { - const result: Date[] = [] +function dateToArray(obj: Record): Date[] { + const result: Date[] = []; for (const key in obj) { if (obj.hasOwnProperty(key)) { - result.push((obj[key] as unknown as Timestamp).toDate()) + result.push((obj[key] as unknown as Timestamp).toDate()); } } - return result -} \ No newline at end of file + return result; +} From 2ed90eba0e2d5ddfe14faccc2c12dc8bfb53b62d Mon Sep 17 00:00:00 2001 From: Jeet Parikh Date: Sun, 9 Mar 2025 18:15:32 -0700 Subject: [PATCH 3/9] adding event to userEvents on time availabilty submit (works for both admin, non-admin, and anonymous->auth cases) --- src/firebase/events.ts | 65 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/src/firebase/events.ts b/src/firebase/events.ts index 49a5b24..389c792 100644 --- a/src/firebase/events.ts +++ b/src/firebase/events.ts @@ -336,6 +336,59 @@ async function saveEventDetails(eventDetails: EventDetails) { }); } +async function updateUserCollectionEventsWith(accountId: string) { + console.log("Here's the account ID:", accountId); + const userRef = doc(db, 'users', accountId); + + // First, get the current user document to check existing events + getDoc(userRef) + .then((docSnap) => { + if (docSnap.exists()) { + const userData = docSnap.data(); + const userEvents = userData.userEvents || []; + + let existingEventIndex = -1; + // Check if an event with the same code already exists + userEvents.forEach((event: { code: string }) => { + if (event.code === workingEvent.publicId) { + existingEventIndex = userEvents.indexOf(event); + } + }); + + if (existingEventIndex !== -1) { + console.log('Event exists, updating timestamp'); + // Event exists, update its timestamp + userEvents[existingEventIndex].timestamp = new Date(); + + // Update the document with the modified array + updateDoc(userRef, { + userEvents: userEvents, + }).catch((err) => { + console.error('Error updating event timestamp:', err); + }); + } else { + // Event doesn't exist, add it to the array + const eventDetailsForUser = { + code: workingEvent.publicId, + timestamp: new Date(), + isAdmin: false, // safe assumtion, since admins always already event saved on creation + }; + + updateDoc(userRef, { + userEvents: arrayUnion(eventDetailsForUser), + }).catch((err) => { + console.error('Error adding new event:', err); + }); + } + } else { + console.error("User document doesn't exist"); + } + }) + .catch((err) => { + console.error('Error getting user document:', err); + }); +} + // For internal use // Updates the participants list of the working event // with the participant passed in, overwriting if they already exist @@ -354,11 +407,14 @@ async function saveParticipantDetails(participant: Participant): Promise { flag = true; } }); + + const accountId = getAccountId(); + + // !flag = participant does not exists in that event if (!flag) { workingEvent.participants.push(participant); // Update Backend: add user uid to particpants list of event object - const accountId = getAccountId(); if (accountId && accountId !== '') { const eventsRef = collection(db, 'events'); updateDoc(doc(eventsRef, workingEvent.publicId), { @@ -369,6 +425,11 @@ async function saveParticipantDetails(participant: Participant): Promise { } } + // Add or update event in user collection's userEvents field + if (accountId && accountId !== '') { + updateUserCollectionEventsWith(accountId); + } + // Update backend await new Promise((resolve, reject) => { const eventsRef = collection(db, 'events'); @@ -448,6 +509,8 @@ async function updateAnonymousUserToAuthUser(name: string) { participants: arrayUnion(getAccountId()), }); + updateUserCollectionEventsWith(accountId); + await batch.commit(); return; } catch (err) { From d92ca93294bc878980267a3cf7b6d7ac34f7be30 Mon Sep 17 00:00:00 2001 From: Jeet Parikh Date: Sun, 16 Mar 2025 17:19:32 -0700 Subject: [PATCH 4/9] much more efficient way of loading events, pulling from user collection. It's now sorted by most recently edited. not backwards compatible though... --- src/firebase/events.ts | 118 +++++++++++++++++++++++++++++++++-------- 1 file changed, 95 insertions(+), 23 deletions(-) diff --git a/src/firebase/events.ts b/src/firebase/events.ts index 389c792..b7796df 100644 --- a/src/firebase/events.ts +++ b/src/firebase/events.ts @@ -188,35 +188,107 @@ async function deleteEvent(id: EventId): Promise { }); } +// Structure of the userEvents array in the user document +interface UserEvent { + code: string; + timestamp: any; // This could be Timestamp or Date + isAdmin: boolean; +} + +async function getEventCodesSortedByTimestamp( + userID: string +): Promise { + try { + const userRef = doc(db, 'users', userID); + const userDoc = await getDoc(userRef); + + if (!userDoc.exists()) { + console.log( + 'User document not found: User does not exist in the database' + ); + return []; + } + + const userData = userDoc.data(); + const userEvents: UserEvent[] = userData.userEvents || []; + + const sortedEvents = [...userEvents].sort((a, b) => { + const aTime = a.timestamp?.toMillis + ? a.timestamp.toMillis() + : a.timestamp?.getTime?.() || 0; + const bTime = b.timestamp?.toMillis + ? b.timestamp.toMillis() + : b.timestamp?.getTime?.() || 0; + return bTime - aTime; // Descending order (newest first) + }); + + const eventCodes = sortedEvents.map((event) => event.code); + + return eventCodes; + } catch (error) { + console.error('Error getting sorted event codes:', error); + return []; + } +} + // Retrieves all events that this user has submitted availability for async function getAllEventsForUser(accountID: string): Promise { + const sortedEventCodes = await getEventCodesSortedByTimestamp(accountID); + console.log('Event codes sorted by timestamp:', sortedEventCodes); + const eventsRef = collection(db, 'events'); + return await new Promise(async (resolve, reject) => { - const q = query( - eventsRef, - where('participants', 'array-contains', accountID) - ); - const querySnapshot = await getDocs(q); + // const q = query( + // eventsRef, + // where('participants', 'array-contains', accountID) + // ); + // const querySnapshot = await getDocs(q); const eventsList: Event[] = []; - querySnapshot.forEach((doc) => { - const result = doc.data(); - result.details.startTime = result.details.startTime - ? (result.details.startTime as unknown as Timestamp).toDate() - : result.details.startTime; - result.details.endTime = result.details.endTime - ? (result.details.endTime as unknown as Timestamp).toDate() - : result.details.endTime; - result.details.chosenStartDate = result.details.chosenStartDate - ? (result.details.chosenStartDate as unknown as Timestamp).toDate() - : result.details.chosenStartDate; - result.details.chosenEndDate = result.details.chosenEndDate - ? (result.details.chosenEndDate as unknown as Timestamp).toDate() - : result.details.chosenEndDate; - result.details.dates = dateToArray(result.details.dates); - - eventsList.push(result as unknown as Event); - }); + + for (const eventCode of sortedEventCodes) { + const eventDoc = await getDoc(doc(eventsRef, eventCode)); + if (eventDoc.exists()) { + const event = eventDoc.data(); + event.details.startTime = event.details.startTime + ? (event.details.startTime as unknown as Timestamp).toDate() + : event.details.startTime; + event.details.endTime = event.details.endTime + ? (event.details.endTime as unknown as Timestamp).toDate() + : event.details.endTime; + event.details.chosenStartDate = event.details.chosenStartDate + ? (event.details.chosenStartDate as unknown as Timestamp).toDate() + : event.details.chosenStartDate; + event.details.chosenEndDate = event.details.chosenEndDate + ? (event.details.chosenEndDate as unknown as Timestamp).toDate() + : event.details.chosenEndDate; + event.details.dates = dateToArray(event.details.dates); + + eventsList.push(event as unknown as Event); + } else { + // if it doesn't exist (might have been deleted by admin), then it just won't load + } + } + + // querySnapshot.forEach((doc) => { + // const result = doc.data(); + // result.details.startTime = result.details.startTime + // ? (result.details.startTime as unknown as Timestamp).toDate() + // : result.details.startTime; + // result.details.endTime = result.details.endTime + // ? (result.details.endTime as unknown as Timestamp).toDate() + // : result.details.endTime; + // result.details.chosenStartDate = result.details.chosenStartDate + // ? (result.details.chosenStartDate as unknown as Timestamp).toDate() + // : result.details.chosenStartDate; + // result.details.chosenEndDate = result.details.chosenEndDate + // ? (result.details.chosenEndDate as unknown as Timestamp).toDate() + // : result.details.chosenEndDate; + // result.details.dates = dateToArray(result.details.dates); + + // eventsList.push(result as unknown as Event); + // }); resolve(eventsList); }); From 37105eb052122de4bf5f8f103c703f53d29c31b2 Mon Sep 17 00:00:00 2001 From: Jeet Parikh Date: Wed, 19 Mar 2025 01:11:36 -0700 Subject: [PATCH 5/9] event deletion updates backend - (security consideration: it's designed so a user only ever edits their own document) --- src/firebase/events.ts | 57 +++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/src/firebase/events.ts b/src/firebase/events.ts index b7796df..c648e81 100644 --- a/src/firebase/events.ts +++ b/src/firebase/events.ts @@ -29,8 +29,6 @@ import { } from '../types'; import { auth, db } from './firebase'; import { generateTimeBlocks } from '../components/utils/functions/generateTimeBlocks'; -import { time } from 'console'; -import { start } from 'repl'; import { DateTime } from 'luxon'; // ASSUME names are unique within an event @@ -186,6 +184,8 @@ async function deleteEvent(id: EventId): Promise { await batch.commit().catch((err) => { console.log('Error: ', err); }); + + removeEventFromUserCollection(getAccountId(), id); } // Structure of the userEvents array in the user document @@ -231,6 +231,30 @@ async function getEventCodesSortedByTimestamp( } } +async function removeEventFromUserCollection( + userID: string, + eventCode: string +) { + const userRef = doc(db, 'users', userID); + const userDoc = await getDoc(userRef); + + if (!userDoc.exists()) { + console.log('User document not found: User does not exist in the database'); + return; + } + + const userData = userDoc.data(); + const userEvents: UserEvent[] = userData.userEvents || []; + + const updatedEvents = userEvents.filter((event) => event.code !== eventCode); + + await updateDoc(userRef, { + userEvents: updatedEvents, + }).catch((err) => { + console.error('Error updating user document:', err); + }); +} + // Retrieves all events that this user has submitted availability for async function getAllEventsForUser(accountID: string): Promise { const sortedEventCodes = await getEventCodesSortedByTimestamp(accountID); @@ -239,12 +263,6 @@ async function getAllEventsForUser(accountID: string): Promise { const eventsRef = collection(db, 'events'); return await new Promise(async (resolve, reject) => { - // const q = query( - // eventsRef, - // where('participants', 'array-contains', accountID) - // ); - // const querySnapshot = await getDocs(q); - const eventsList: Event[] = []; for (const eventCode of sortedEventCodes) { @@ -267,29 +285,11 @@ async function getAllEventsForUser(accountID: string): Promise { eventsList.push(event as unknown as Event); } else { - // if it doesn't exist (might have been deleted by admin), then it just won't load + // won't load if it doesn't exist (has been deleted by admin) + removeEventFromUserCollection(accountID, eventCode); } } - // querySnapshot.forEach((doc) => { - // const result = doc.data(); - // result.details.startTime = result.details.startTime - // ? (result.details.startTime as unknown as Timestamp).toDate() - // : result.details.startTime; - // result.details.endTime = result.details.endTime - // ? (result.details.endTime as unknown as Timestamp).toDate() - // : result.details.endTime; - // result.details.chosenStartDate = result.details.chosenStartDate - // ? (result.details.chosenStartDate as unknown as Timestamp).toDate() - // : result.details.chosenStartDate; - // result.details.chosenEndDate = result.details.chosenEndDate - // ? (result.details.chosenEndDate as unknown as Timestamp).toDate() - // : result.details.chosenEndDate; - // result.details.dates = dateToArray(result.details.dates); - - // eventsList.push(result as unknown as Event); - // }); - resolve(eventsList); }); } @@ -1012,6 +1012,7 @@ export { setNewEndTimes, setNewDates, getParticipantIndex, + removeEventFromUserCollection, }; function dateToObject(dateArray: number[][]): Record { From 85bfa47d00f454341c7e3b2a01ab782dbabf9858 Mon Sep 17 00:00:00 2001 From: Jeet Parikh Date: Fri, 18 Apr 2025 15:03:56 -0400 Subject: [PATCH 6/9] add dateCreated to events --- .../DaySelect/day_select_component/day_select_component.tsx | 6 ++++-- src/firebase/eventAPI.ts | 4 +++- src/firebase/events.ts | 1 + src/types.ts | 1 + 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/DaySelect/day_select_component/day_select_component.tsx b/src/components/DaySelect/day_select_component/day_select_component.tsx index a4f79c7..0e6339a 100644 --- a/src/components/DaySelect/day_select_component/day_select_component.tsx +++ b/src/components/DaySelect/day_select_component/day_select_component.tsx @@ -195,7 +195,8 @@ export const DaySelectComponent = () => { startDate, endDate, zoomLink, - timezone + timezone, + new Date() // dateCreated ) .then((ev) => { navigate('/timeselect/' + ev?.publicId); @@ -217,7 +218,8 @@ export const DaySelectComponent = () => { startDate, endDate, zoomLink, - timezone + timezone, + new Date() // dateCreated ) .then((ev) => { navigate('/timeselect/' + ev?.publicId); diff --git a/src/firebase/eventAPI.ts b/src/firebase/eventAPI.ts index 3e717ef..e6d194a 100644 --- a/src/firebase/eventAPI.ts +++ b/src/firebase/eventAPI.ts @@ -78,7 +78,8 @@ export default class FrontendEventAPI { startTime: Date, endTime: Date, zoomLink: string = '', - timeZone: string + timeZone: string, + dateCreated: Date ): Promise { try { const ev: Event | null = await createEvent({ @@ -93,6 +94,7 @@ export default class FrontendEventAPI { zoomLink: zoomLink, timeZone: timeZone, participants: [], + dateCreated: dateCreated, }); return ev; diff --git a/src/firebase/events.ts b/src/firebase/events.ts index 58649c0..5e08d34 100644 --- a/src/firebase/events.ts +++ b/src/firebase/events.ts @@ -49,6 +49,7 @@ let workingEvent: Event = { plausibleLocations: ['HH17', 'Sterling'], timeZone: 'America/New_York', participants: [], + dateCreated: new Date(), }, participants: [], }; diff --git a/src/types.ts b/src/types.ts index bb90708..3492e09 100644 --- a/src/types.ts +++ b/src/types.ts @@ -24,6 +24,7 @@ export interface EventDetails { zoomLink?: string | undefined; timeZone: string; participants: string[]; // firebase uids + dateCreated: Date; } export interface Participant { From e7e55388740e9d1b2acd267217c67678882247c3 Mon Sep 17 00:00:00 2001 From: Jeet Parikh Date: Fri, 18 Apr 2025 17:44:56 -0400 Subject: [PATCH 7/9] sorting by lastModified and dateCreated --- src/components/Accounts/AccountsPage.tsx | 137 +++++++++++++---------- src/firebase/events.ts | 100 ++++++++++++++--- 2 files changed, 163 insertions(+), 74 deletions(-) diff --git a/src/components/Accounts/AccountsPage.tsx b/src/components/Accounts/AccountsPage.tsx index 3a1e74c..e28d508 100644 --- a/src/components/Accounts/AccountsPage.tsx +++ b/src/components/Accounts/AccountsPage.tsx @@ -1,11 +1,18 @@ import { useContext, useState, useEffect } from 'react'; -import { IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; +import { + IconPlus, + IconSearch, + IconTrash, + IconClock, + IconCalendar, +} from '@tabler/icons-react'; import { checkIfLoggedIn, getAccountId, getAllEventsForUser, deleteEvent, + getParsedAccountPageEventsForUser, } from '../../firebase/events'; import { logout } from '../../firebase/auth'; @@ -18,7 +25,7 @@ import { LoadingAnim } from '../utils/components/LoadingAnim'; import LoginButton from '../utils/components/LoginButton'; import CopyCodeButton from '../utils/components/CopyCodeButton'; -interface AccountsPageEvent { +export interface AccountsPageEvent { name: string; id: string; dates: string; @@ -26,46 +33,10 @@ interface AccountsPageEvent { endTime: string; location: string; iAmCreator: boolean; + dateCreated: Date; + lastModified: Date; } -/** - * Parses the backend event object into a type that the AccountsPage component understands. - * @param events Event[] - * @returns AccountsPageEvent[] - */ -const parseEventObjectForAccountsPage = ( - events: Event[] -): AccountsPageEvent[] => { - const accountPageEvents: AccountsPageEvent[] = []; - events.forEach((event) => { - accountPageEvents.push({ - name: event.details.name, - id: event.publicId, - dates: event.details.chosenStartDate - ? event.details.chosenStartDate?.toLocaleDateString() - : 'TBD', - startTime: event.details.chosenStartDate - ? event.details.chosenStartDate?.toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit', - hour12: true, - }) - : 'TBD', - endTime: event.details.chosenEndDate - ? event.details.chosenEndDate?.toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit', - hour12: true, - }) - : 'TBD', - location: event.details.chosenLocation || 'TBD', - iAmCreator: event.details.adminAccountId === getAccountId(), - }); - }); - - return accountPageEvents; -}; - /** * Page Component. Renders the events associated with a logged in Google account. * Renders nothing if no events are associated or the user is logged in anonymously @@ -80,9 +51,11 @@ export default function AccountsPage() { const accountID = getAccountId(); if (accountID && accountID !== '') { - await getAllEventsForUser(getAccountId()).then((eventsUnparsed) => { - setEvents(parseEventObjectForAccountsPage(eventsUnparsed) || []); - }); + await getParsedAccountPageEventsForUser(accountID).then( + (parsedEvents) => { + setEvents(parsedEvents); + } + ); } else { setEvents([]); } @@ -95,14 +68,37 @@ export default function AccountsPage() { const nav = useNavigate(); const [filter, setFilter] = useState(''); - const [events, setEvents] = useState(); const [hasDeletedEvent, setHasDeletedEvent] = useState(false); + const [sortBy, setSortBy] = useState<'dateCreated' | 'lastModified'>( + 'lastModified' + ); const handleInputChange = (e: any) => { setFilter(e.target.value.toLowerCase()); }; + const getSortedEvents = (events: AccountsPageEvent[]) => { + // return events; // remove later + + console.log(events); + + return [...events].sort((a, b) => { + let dateA = sortBy === 'dateCreated' ? a.dateCreated : a.lastModified; + let dateB = sortBy === 'dateCreated' ? b.dateCreated : b.lastModified; + + dateA = new Date(dateA); + dateB = new Date(dateB); + + console.log(dateA, dateB); + if (dateA === undefined || dateB === undefined) { + console.log('Date is undefined'); + return 0; + } + return dateB.getTime() - dateA.getTime(); + }); + }; + return (
@@ -112,20 +108,45 @@ export default function AccountsPage() {
-
-
- +
+
+
+ +
+
- + +
+
+ + {/* Mobile Button */} +
+ nav('/dayselect')} + bgColor="primary" + textColor="white" + className="inline-flex items-center gap-2" + > + + Create + +
+
- + {/* Controls Row */} +
+ {/* Search Input */} +
+
+
+ +
-
@@ -170,7 +185,7 @@ export default function AccountsPage() {
) : undefined} {events && events.length != 0 ? ( -
+
{getSortedEvents(events) .filter( (e) => diff --git a/src/components/utils/components/Button.tsx b/src/components/utils/components/Button.tsx index 0b0959f..fa3d37a 100644 --- a/src/components/utils/components/Button.tsx +++ b/src/components/utils/components/Button.tsx @@ -10,6 +10,7 @@ interface Props { py?: string; // Padding-top and padding-bottom themeGradient?: boolean; bolded?: boolean; // Font weight + className?: string; // Add this line } export default function Button({ @@ -22,6 +23,7 @@ export default function Button({ textSize = 'lg', themeGradient = true, bolded = true, + className = '', }: Props) { const borderRadius = rounded === 'full' ? 'rounded-full' : 'rounded-lg'; const textSizeClass = @@ -38,6 +40,7 @@ export default function Button({ ${textSizeClass} text-${textColor} ${borderRadius} ${bgColor === `primary` ? `dark:bg-blue-700` : ``} ${bolded ? `font-bold` : `font-semibold`} + ${className} `} onClick={onClick} disabled={disabled} diff --git a/src/components/utils/components/ButtonSmall.tsx b/src/components/utils/components/ButtonSmall.tsx index 218b125..4148cb9 100644 --- a/src/components/utils/components/ButtonSmall.tsx +++ b/src/components/utils/components/ButtonSmall.tsx @@ -5,6 +5,7 @@ interface Props { children: React.ReactNode; disabled?: boolean; themeGradient?: boolean; + className?: string; } export default function ButtonSmall({ @@ -14,6 +15,7 @@ export default function ButtonSmall({ children, disabled = false, themeGradient = true, + className = ``, }: Props) { return (