11import { initializeApp } from "firebase/app" ;
22import { getAuth , GoogleAuthProvider , onAuthStateChanged } from "firebase/auth" ;
3- import { getFunctions , httpsCallable } from 'firebase/functions' ;
4- import { get , getDatabase , ref , set , onValue , onChildRemoved , onChildAdded , runTransaction } from "firebase/database" ;
3+ import { getFunctions , httpsCallable } from "firebase/functions" ;
4+ import {
5+ get ,
6+ getDatabase ,
7+ ref ,
8+ set ,
9+ onValue ,
10+ onChildRemoved ,
11+ onChildAdded ,
12+ runTransaction ,
13+ } from "firebase/database" ;
514import { reaction , toJS } from "mobx" ;
615
16+ /**
17+ * Firebase configuration and initialization.
18+ * This code connects to Firebase, sets up authentication and allows to save and fetch courses as well as user data.
19+ * Data Synchronization and caching are also handled.
20+ * The firebase realtime database is used to store courses, user data, and reviews.
21+ * If you would like to reuse this project, make sure to configure rules as shown in firebas_rules.json.
22+ */
23+
724// Your web app's Firebase configuration
825const firebaseConfig = {
926 apiKey : "AIzaSyCBckVI9nhAP62u5jZJW3F4SLulUv7znis" ,
@@ -25,6 +42,11 @@ export const googleProvider = new GoogleAuthProvider();
2542googleProvider . addScope ( "profile" ) ;
2643googleProvider . addScope ( "email" ) ;
2744
45+ /**
46+ * Startup hook to connect the model to Firebase.
47+ * This function sets up the Firebase connection, fetches initial data and starts listener for syncing data.
48+ * @param {object } model The reactive model
49+ */
2850export function connectToFirebase ( model ) {
2951 loadCoursesFromCacheOrFirebase ( model ) ;
3052 fetchDepartmentsAndLocations ( model ) ;
@@ -38,14 +60,17 @@ export function connectToFirebase(model) {
3860 console . log ( "Restore options from local storage" ) ;
3961 }
4062
63+ // automaticaly save filter options to local storage whenever they change
4164 reaction (
4265 ( ) => ( { filterOptions : JSON . stringify ( model . filterOptions ) } ) ,
4366 // eslint-disable-next-line no-unused-vars
4467 ( { filterOptions } ) => {
4568 localStorage . setItem ( "filterOptions" , filterOptions ) ;
4669 }
4770 ) ;
48-
71+ /**
72+ * Hook to start synchronization when user is authenticated.
73+ */
4974 onAuthStateChanged ( auth , ( user ) => {
5075 if ( user ) {
5176 model . setUser ( user ) ; // Set the user ID once authenticated
@@ -60,48 +85,59 @@ export function connectToFirebase(model) {
6085
6186// fetches all relevant information to create the model
6287async function firebaseToModel ( model ) {
63- const userRef = ref ( db , `users/${ model . user . uid } ` ) ;
64- onValue ( userRef , async ( snapshot ) => {
65- if ( ! snapshot . exists ( ) ) return ;
66- const data = snapshot . val ( ) ;
67-
68- // Use a transaction to ensure atomicity
69- await runTransaction ( userRef , ( currentData ) => {
70- if ( currentData ) {
71- if ( data ?. favourites ) model . setFavourite ( data . favourites ) ;
72- if ( data ?. currentSearchText ) model . setCurrentSearchText ( data . currentSearchText ) ;
73- // Add other fields as needed
74- }
75- return currentData ; // Return the current data to avoid overwriting
76- } ) ;
77- } ) ;
88+ const userRef = ref ( db , `users/${ model . user . uid } ` ) ;
89+ onValue ( userRef , async ( snapshot ) => {
90+ if ( ! snapshot . exists ( ) ) return ;
91+ const data = snapshot . val ( ) ;
92+
93+ // Use a transaction to ensure atomicity
94+ await runTransaction ( userRef , ( currentData ) => {
95+ if ( currentData ) {
96+ if ( data ?. favourites ) model . setFavourite ( data . favourites ) ;
97+ if ( data ?. currentSearchText )
98+ model . setCurrentSearchText ( data . currentSearchText ) ;
99+ // Add other fields as needed
100+ }
101+ return currentData ; // Return the current data to avoid overwriting
102+ } ) ;
103+ } ) ;
78104}
79105
106+ /**
107+ * If the userid, favourites or search changes, sync to firebase.
108+ * @param {object } model reactive model object
109+ */
80110export function syncModelToFirebase ( model ) {
81- reaction (
82- ( ) => ( {
83- userId : model ?. user . uid ,
84- favourites : toJS ( model . favourites ) ,
85- currentSearchText : toJS ( model . currentSearchText ) ,
86- } ) ,
87- async ( { userId, favourites, currentSearchText } ) => {
88- if ( ! userId ) return ;
89-
90- const userRef = ref ( db , `users/${ userId } ` ) ;
91- await runTransaction ( userRef , ( currentData ) => {
92- // Merge the new data with the existing data
93- return {
94- ...currentData ,
95- favourites,
96- currentSearchText,
97- } ;
98- } ) . catch ( ( error ) => {
99- console . error ( ' Error syncing model to Firebase:' , error ) ;
100- } ) ;
101- }
102- ) ;
111+ reaction (
112+ ( ) => ( {
113+ userId : model ?. user . uid ,
114+ favourites : toJS ( model . favourites ) ,
115+ currentSearchText : toJS ( model . currentSearchText ) ,
116+ } ) ,
117+ async ( { userId, favourites, currentSearchText } ) => {
118+ if ( ! userId ) return ;
119+
120+ const userRef = ref ( db , `users/${ userId } ` ) ;
121+ await runTransaction ( userRef , ( currentData ) => {
122+ // Merge the new data with the existing data
123+ return {
124+ ...currentData ,
125+ favourites,
126+ currentSearchText,
127+ } ;
128+ } ) . catch ( ( error ) => {
129+ console . error ( " Error syncing model to Firebase:" , error ) ;
130+ } ) ;
131+ }
132+ ) ;
103133}
104134
135+ /**
136+ * Synchronizes the scroll position of the container to Firebase / local storage.
137+ * @param {object } model
138+ * @param {* } containerRef
139+ * @returns
140+ */
105141export function syncScrollPositionToFirebase ( model , containerRef ) {
106142 if ( ! containerRef ?. current ) return ;
107143 let lastSavedPosition = 0 ;
@@ -129,13 +165,16 @@ export function syncScrollPositionToFirebase(model, containerRef) {
129165 containerRef . current ?. removeEventListener ( "scroll" , handleScroll ) ;
130166}
131167
132-
133-
168+ /**
169+ * Caches the courses to IndexedDB.
170+ * @param {Array } courses The course array to save
171+ * @param {number } timestamp The last update of the course array
172+ */
134173function saveCoursesToCache ( courses , timestamp ) {
135174 const request = indexedDB . open ( "CourseDB" , 1 ) ;
136175
137176 function validateCourseData ( course ) {
138- return course && typeof course === ' object' && course . code ;
177+ return course && typeof course === " object" && course . code ;
139178 }
140179
141180 request . onupgradeneeded = ( event ) => {
@@ -155,7 +194,9 @@ function saveCoursesToCache(courses, timestamp) {
155194 const metaStore = tx . objectStore ( "metadata" ) ;
156195
157196 courseStore . clear ( ) ;
158- courses . filter ( validateCourseData ) . forEach ( ( course ) => courseStore . put ( course ) ) ;
197+ courses
198+ . filter ( validateCourseData )
199+ . forEach ( ( course ) => courseStore . put ( course ) ) ;
159200 metaStore . put ( { key : "timestamp" , value : timestamp } ) ;
160201
161202 tx . oncomplete = ( ) => console . log ( "Saved courses to IndexedDB" ) ;
@@ -178,18 +219,26 @@ async function fetchLastUpdatedTimestamp() {
178219 return snapshot . exists ( ) ? snapshot . val ( ) : 0 ;
179220}
180221
222+ /**
223+ * Admin function to add a course to the database.
224+ * @param {Array } course
225+ */
181226export async function addCourse ( course ) {
182- if ( ! auth . currentUser )
183- throw new Error ( 'User must be authenticated' ) ;
184- if ( ! course ?. code )
185- throw new Error ( ' Invalid course data' ) ;
186- if ( ! course || typeof course !== 'object' ) throw new Error ( 'Invalid course object' ) ;
187- if ( ! course . code || typeof course . code !== 'string' ) throw new Error ( ' Invalid course code' ) ;
188- const myRef = ref ( db , `courses/${ course . code } ` ) ;
189- await set ( myRef , course ) ;
227+ if ( ! auth . currentUser ) throw new Error ( "User must be authenticated" ) ;
228+ if ( ! course ?. code ) throw new Error ( "Invalid course data" ) ;
229+ if ( ! course || typeof course !== "object" )
230+ throw new Error ( " Invalid course object" ) ;
231+ if ( ! course . code || typeof course . code !== "string" )
232+ throw new Error ( " Invalid course code" ) ;
233+ const myRef = ref ( db , `courses/${ course . code } ` ) ;
234+ await set ( myRef , course ) ;
190235 updateLastUpdatedTimestamp ( ) ;
191236}
192237
238+ /**
239+ * Fetches all courses from the database.
240+ * @returns {Array } Array of courses
241+ */
193242export async function fetchAllCourses ( ) {
194243 const myRef = ref ( db , `courses` ) ;
195244 const snapshot = await get ( myRef ) ;
@@ -204,6 +253,12 @@ export async function fetchAllCourses() {
204253 return courses ;
205254}
206255
256+ /**
257+ * Admin function to upload departments and locations to the database.
258+ * @param { } departments
259+ * @param {* } locations
260+ * @returns
261+ */
207262export async function uploadDepartmentsAndLocations ( departments , locations ) {
208263 if ( departments ) {
209264 const departmentsRef = ref ( db , "departments" ) ;
@@ -228,6 +283,11 @@ export async function uploadDepartmentsAndLocations(departments, locations) {
228283 return true ;
229284}
230285
286+ /**
287+ * Fetches departments and locations from the database.
288+ * @param {object } model
289+ * @returns {Array } Array of departments and locations
290+ */
231291export async function fetchDepartmentsAndLocations ( model ) {
232292 const departmentsRef = ref ( db , "departments" ) ;
233293 const locationsRef = ref ( db , "locations" ) ;
@@ -251,6 +311,12 @@ export async function fetchDepartmentsAndLocations(model) {
251311 }
252312}
253313
314+ /**
315+ * Try to restore the courses from IndexedDB.
316+ * @param {object } model
317+ * @returns void
318+ * @throws Error if IndexedDB is not available or no courses are found
319+ */
254320async function loadCoursesFromCacheOrFirebase ( model ) {
255321 const firebaseTimestamp = await fetchLastUpdatedTimestamp ( ) ;
256322 const dbPromise = new Promise ( ( resolve , reject ) => {
@@ -289,13 +355,15 @@ async function loadCoursesFromCacheOrFirebase(model) {
289355 getAllReq . onsuccess = ( ) => resolve ( getAllReq . result ) ;
290356 getAllReq . onerror = ( ) => resolve ( [ ] ) ;
291357 } ) ;
292- if ( ! cachedCourses )
293- throw new Error ( "No courses found in IndexedDB" ) ;
358+ if ( ! cachedCourses ) throw new Error ( "No courses found in IndexedDB" ) ;
294359 model . setCourses ( cachedCourses ) ;
295360 return ;
296361 }
297362 } catch ( err ) {
298- console . warn ( "IndexedDB unavailable, falling back Posting anonymously is possible. Firebase:" , err ) ;
363+ console . warn (
364+ "IndexedDB unavailable, falling back Posting anonymously is possible. Firebase:" ,
365+ err
366+ ) ;
299367 }
300368
301369 // fallback: fetch from Firebase
@@ -305,19 +373,48 @@ async function loadCoursesFromCacheOrFirebase(model) {
305373 saveCoursesToCache ( courses , firebaseTimestamp ) ;
306374}
307375
376+ /**
377+ * Function to add a review for a course. The firebase rules disallow to have more than one course per person.
378+ * Adding a review will automatically update the average rating of the course.
379+ * @param {string } courseCode The course code
380+ * @param {object } review The review object containing the review data
381+ * @param {string } review.userName The name of the user
382+ * @param {string } review.uid The user ID
383+ * @param {number } review.timestamp The timestamp of the review
384+ * @param {string } review.text The review text
385+ * @param {number } review.overallRating The overall rating
386+ * @param {number } review.difficultyRating The difficulty rating
387+ * @param {string } review.professorName The name of the professor
388+ * @param {string } review.grade The grade received
389+ * @param {boolean } review.recommended Whether the course is recommended
390+ * @returns {Promise<void> } A promise that resolves when the review is added
391+ * @throws {Error } If there is an error adding the review or updating the average rating
392+ */
308393export async function addReviewForCourse ( courseCode , review ) {
309394 try {
310395 const reviewsRef = ref ( db , `reviews/${ courseCode } /${ review . uid } ` ) ;
311396 await set ( reviewsRef , review ) ;
312- const updateCourseAvgRating = httpsCallable ( functions , 'updateCourseAvgRating' ) ;
313- const result = await updateCourseAvgRating ( { courseCode } ) ;
314-
315- console . log ( 'Average rating updated:' , result . data . avgRating ) ;
397+ const updateCourseAvgRating = httpsCallable (
398+ functions ,
399+ "updateCourseAvgRating"
400+ ) ;
401+ const result = await updateCourseAvgRating ( { courseCode } ) ;
402+
403+ console . log ( "Average rating updated:" , result . data . avgRating ) ;
316404 } catch ( error ) {
317- console . error ( "Error when adding a course to firebase or updating the average:" , error ) ;
405+ console . error (
406+ "Error when adding a course to firebase or updating the average:" ,
407+ error
408+ ) ;
318409 }
319410}
320411
412+ /**
413+ * Adding a course triggers a firebase function to update the average rating of the course.
414+ * This function sets up a listener for the average rating of each course, to update the model onChange.
415+ * It also fetches the initial average ratings if the model is not initialized.
416+ * @param {object } model
417+ */
321418function startAverageRatingListener ( model ) {
322419 const coursesRef = ref ( db , "reviews" ) ;
323420
@@ -337,7 +434,7 @@ function startAverageRatingListener(model) {
337434 }
338435 } ) ;
339436 model . setAverageRatings ( initialRatings ) ;
340- } )
437+ } ) ;
341438 }
342439
343440 // Step 2: listener for each courses avgRating
@@ -362,17 +459,22 @@ function startAverageRatingListener(model) {
362459 } ) ;
363460}
364461
462+ /**
463+ * Fetches reviews for a specific course.
464+ * @param {string } courseCode
465+ * @returns
466+ */
365467export async function getReviewsForCourse ( courseCode ) {
366468 const reviewsRef = ref ( db , `reviews/${ courseCode } ` ) ;
367469 const snapshot = await get ( reviewsRef ) ;
368470 if ( ! snapshot . exists ( ) ) return [ ] ;
369471 const reviews = [ ] ;
370472 snapshot . forEach ( ( childSnapshot ) => {
371- if ( childSnapshot . key != "avgRating" )
372- reviews . push ( {
373- id : childSnapshot . key ,
374- ...childSnapshot . val ( ) ,
375- } ) ;
473+ if ( childSnapshot . key != "avgRating" )
474+ reviews . push ( {
475+ id : childSnapshot . key ,
476+ ...childSnapshot . val ( ) ,
477+ } ) ;
376478 } ) ;
377479 return reviews ;
378480}
0 commit comments