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,8 +42,13 @@ export const googleProvider = new GoogleAuthProvider();
2542googleProvider . addScope ( "profile" ) ;
2643googleProvider . addScope ( "email" ) ;
2744
28- export function connectToFirebase ( model ) {
29- loadCoursesFromCacheOrFirebase ( model ) ;
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+ */
50+ export async function connectToFirebase ( model ) {
51+ await loadCoursesFromCacheOrFirebase ( model ) ;
3052 fetchDepartmentsAndLocations ( model ) ;
3153 startAverageRatingListener ( model ) ;
3254 // setting missing
@@ -37,14 +59,16 @@ export function connectToFirebase(model) {
3759 model . setFilterOptions ( options ) ;
3860 }
3961
62+ // automaticaly save filter options to local storage whenever they change
4063 reaction (
4164 ( ) => ( { filterOptions : JSON . stringify ( model . filterOptions ) } ) ,
42- // eslint-disable-next-line no-unused-vars
4365 ( { filterOptions } ) => {
4466 localStorage . setItem ( "filterOptions" , filterOptions ) ;
4567 }
4668 ) ;
47-
69+ /**
70+ * Hook to start synchronization when user is authenticated.
71+ */
4872 onAuthStateChanged ( auth , ( user ) => {
4973 if ( user ) {
5074 model . setUser ( user ) ; // Set the user ID once authenticated
@@ -59,48 +83,59 @@ export function connectToFirebase(model) {
5983
6084// fetches all relevant information to create the model
6185async function firebaseToModel ( model ) {
62- const userRef = ref ( db , `users/${ model . user . uid } ` ) ;
63- onValue ( userRef , async ( snapshot ) => {
64- if ( ! snapshot . exists ( ) ) return ;
65- const data = snapshot . val ( ) ;
66-
67- // Use a transaction to ensure atomicity
68- await runTransaction ( userRef , ( currentData ) => {
69- if ( currentData ) {
70- if ( data ?. favourites ) model . setFavourite ( data . favourites ) ;
71- if ( data ?. currentSearchText ) model . setCurrentSearchText ( data . currentSearchText ) ;
72- // Add other fields as needed
73- }
74- return currentData ; // Return the current data to avoid overwriting
75- } ) ;
76- } ) ;
86+ const userRef = ref ( db , `users/${ model . user . uid } ` ) ;
87+ onValue ( userRef , async ( snapshot ) => {
88+ if ( ! snapshot . exists ( ) ) return ;
89+ const data = snapshot . val ( ) ;
90+
91+ // Use a transaction to ensure atomicity
92+ await runTransaction ( userRef , ( currentData ) => {
93+ if ( currentData ) {
94+ if ( data ?. favourites ) model . setFavourite ( data . favourites ) ;
95+ if ( data ?. currentSearchText )
96+ model . setCurrentSearchText ( data . currentSearchText ) ;
97+ // Add other fields as needed
98+ }
99+ return currentData ; // Return the current data to avoid overwriting
100+ } ) ;
101+ } ) ;
77102}
78103
104+ /**
105+ * If the userid, favourites or search changes, sync to firebase.
106+ * @param {object } model reactive model object
107+ */
79108export function syncModelToFirebase ( model ) {
80- reaction (
81- ( ) => ( {
82- userId : model ?. user . uid ,
83- favourites : toJS ( model . favourites ) ,
84- currentSearchText : toJS ( model . currentSearchText ) ,
85- } ) ,
86- async ( { userId, favourites, currentSearchText } ) => {
87- if ( ! userId ) return ;
88-
89- const userRef = ref ( db , `users/${ userId } ` ) ;
90- await runTransaction ( userRef , ( currentData ) => {
91- // Merge the new data with the existing data
92- return {
93- ...currentData ,
94- favourites,
95- currentSearchText,
96- } ;
97- } ) . catch ( ( error ) => {
98- console . error ( ' Error syncing model to Firebase:' , error ) ;
99- } ) ;
100- }
101- ) ;
109+ reaction (
110+ ( ) => ( {
111+ userId : model ?. user . uid ,
112+ favourites : toJS ( model . favourites ) ,
113+ currentSearchText : toJS ( model . currentSearchText ) ,
114+ } ) ,
115+ async ( { userId, favourites, currentSearchText } ) => {
116+ if ( ! userId ) return ;
117+
118+ const userRef = ref ( db , `users/${ userId } ` ) ;
119+ await runTransaction ( userRef , ( currentData ) => {
120+ // Merge the new data with the existing data
121+ return {
122+ ...currentData ,
123+ favourites,
124+ currentSearchText,
125+ } ;
126+ } ) . catch ( ( error ) => {
127+ console . error ( " Error syncing model to Firebase:" , error ) ;
128+ } ) ;
129+ }
130+ ) ;
102131}
103132
133+ /**
134+ * Synchronizes the scroll position of the container to Firebase / local storage.
135+ * @param {object } model
136+ * @param {* } containerRef
137+ * @returns
138+ */
104139export function syncScrollPositionToFirebase ( model , containerRef ) {
105140 if ( ! containerRef ?. current ) return ;
106141 let lastSavedPosition = 0 ;
@@ -128,11 +163,18 @@ export function syncScrollPositionToFirebase(model, containerRef) {
128163 containerRef . current ?. removeEventListener ( "scroll" , handleScroll ) ;
129164}
130165
131-
132-
166+ /**
167+ * Caches the courses to IndexedDB.
168+ * @param {Array } courses The course array to save
169+ * @param {number } timestamp The last update of the course array
170+ */
133171function saveCoursesToCache ( courses , timestamp ) {
134172 const request = indexedDB . open ( "CourseDB" , 1 ) ;
135173
174+ function validateCourseData ( course ) {
175+ return course && typeof course === "object" && course . code ;
176+ }
177+
136178 request . onupgradeneeded = ( event ) => {
137179 const db = event . target . result ;
138180 if ( ! db . objectStoreNames . contains ( "courses" ) ) {
@@ -150,7 +192,9 @@ function saveCoursesToCache(courses, timestamp) {
150192 const metaStore = tx . objectStore ( "metadata" ) ;
151193
152194 courseStore . clear ( ) ;
153- courses . forEach ( ( course ) => courseStore . put ( course ) ) ;
195+ courses
196+ . filter ( validateCourseData )
197+ . forEach ( ( course ) => courseStore . put ( course ) ) ;
154198 metaStore . put ( { key : "timestamp" , value : timestamp } ) ;
155199
156200 tx . oncomplete = ( ) => console . log ( "Saved courses to IndexedDB" ) ;
@@ -173,16 +217,26 @@ async function fetchLastUpdatedTimestamp() {
173217 return snapshot . exists ( ) ? snapshot . val ( ) : 0 ;
174218}
175219
220+ /**
221+ * Admin function to add a course to the database.
222+ * @param {Array } course
223+ */
176224export async function addCourse ( course ) {
177- if ( ! auth . currentUser )
178- throw new Error ( 'User must be authenticated' ) ;
179- if ( ! course ?. code )
180- throw new Error ( 'Invalid course data' ) ;
181- const myRef = ref ( db , `courses/${ course . code } ` ) ;
182- await set ( myRef , course ) ;
225+ if ( ! auth . currentUser ) throw new Error ( "User must be authenticated" ) ;
226+ if ( ! course ?. code ) throw new Error ( "Invalid course data" ) ;
227+ if ( ! course || typeof course !== "object" )
228+ throw new Error ( "Invalid course object" ) ;
229+ if ( ! course . code || typeof course . code !== "string" )
230+ throw new Error ( "Invalid course code" ) ;
231+ const myRef = ref ( db , `courses/${ course . code } ` ) ;
232+ await set ( myRef , course ) ;
183233 updateLastUpdatedTimestamp ( ) ;
184234}
185235
236+ /**
237+ * Fetches all courses from the database.
238+ * @returns {Array } Array of courses
239+ */
186240export async function fetchAllCourses ( ) {
187241 const myRef = ref ( db , `courses` ) ;
188242 const snapshot = await get ( myRef ) ;
@@ -197,6 +251,12 @@ export async function fetchAllCourses() {
197251 return courses ;
198252}
199253
254+ /**
255+ * Admin function to upload departments and locations to the database.
256+ * @param { } departments
257+ * @param {* } locations
258+ * @returns
259+ */
200260export async function uploadDepartmentsAndLocations ( departments , locations ) {
201261 if ( departments ) {
202262 const departmentsRef = ref ( db , "departments" ) ;
@@ -219,6 +279,11 @@ export async function uploadDepartmentsAndLocations(departments, locations) {
219279 return true ;
220280}
221281
282+ /**
283+ * Fetches departments and locations from the database.
284+ * @param {object } model
285+ * @returns {Array } Array of departments and locations
286+ */
222287export async function fetchDepartmentsAndLocations ( model ) {
223288 const departmentsRef = ref ( db , "departments" ) ;
224289 const locationsRef = ref ( db , "locations" ) ;
@@ -242,6 +307,12 @@ export async function fetchDepartmentsAndLocations(model) {
242307 }
243308}
244309
310+ /**
311+ * Try to restore the courses from IndexedDB.
312+ * @param {object } model
313+ * @returns void
314+ * @throws Error if IndexedDB is not available or no courses are found
315+ */
245316async function loadCoursesFromCacheOrFirebase ( model ) {
246317 const firebaseTimestamp = await fetchLastUpdatedTimestamp ( ) ;
247318 const dbPromise = new Promise ( ( resolve , reject ) => {
@@ -279,11 +350,15 @@ async function loadCoursesFromCacheOrFirebase(model) {
279350 getAllReq . onsuccess = ( ) => resolve ( getAllReq . result ) ;
280351 getAllReq . onerror = ( ) => resolve ( [ ] ) ;
281352 } ) ;
353+ if ( ! cachedCourses ) throw new Error ( "No courses found in IndexedDB" ) ;
282354 model . setCourses ( cachedCourses ) ;
283355 return ;
284356 }
285357 } catch ( err ) {
286- console . warn ( "IndexedDB unavailable, falling back Posting anonymously is possible. Firebase:" , err ) ;
358+ console . warn (
359+ "IndexedDB unavailable, falling back Posting anonymously is possible. Firebase:" ,
360+ err
361+ ) ;
287362 }
288363
289364 // fallback: fetch from Firebase
@@ -292,18 +367,43 @@ async function loadCoursesFromCacheOrFirebase(model) {
292367 saveCoursesToCache ( courses , firebaseTimestamp ) ;
293368}
294369
370+ /**
371+ * Function to add a review for a course. The firebase rules disallow to have more than one course per person.
372+ * Adding a review will automatically update the average rating of the course.
373+ * @param {string } courseCode The course code
374+ * @param {object } review The review object containing the review data
375+ * @param {string } review.userName The name of the user
376+ * @param {string } review.uid The user ID
377+ * @param {number } review.timestamp The timestamp of the review
378+ * @param {string } review.text The review text
379+ * @param {number } review.overallRating The overall rating
380+ * @param {number } review.difficultyRating The difficulty rating
381+ * @param {string } review.professorName The name of the professor
382+ * @param {string } review.grade The grade received
383+ * @param {boolean } review.recommended Whether the course is recommended
384+ * @throws {Error } If there is an error adding the review or updating the average rating
385+ */
295386export async function addReviewForCourse ( courseCode , review ) {
296387 try {
297388 const reviewsRef = ref ( db , `reviews/${ courseCode } /${ review . uid } ` ) ;
298389 await set ( reviewsRef , review ) ;
299390 const updateCourseAvgRating = httpsCallable ( functions , 'updateCourseAvgRating' ) ;
300- const result = await updateCourseAvgRating ( { courseCode } ) ;
391+ await updateCourseAvgRating ( { courseCode } ) ;
301392
302393 } catch ( error ) {
303- console . error ( "Error when adding a course to firebase or updating the average:" , error ) ;
394+ console . error (
395+ "Error when adding a course to firebase or updating the average:" ,
396+ error
397+ ) ;
304398 }
305399}
306400
401+ /**
402+ * Adding a course triggers a firebase function to update the average rating of the course.
403+ * This function sets up a listener for the average rating of each course, to update the model onChange.
404+ * It also fetches the initial average ratings if the model is not initialized.
405+ * @param {object } model
406+ */
307407function startAverageRatingListener ( model ) {
308408 const coursesRef = ref ( db , "reviews" ) ;
309409
@@ -323,7 +423,7 @@ function startAverageRatingListener(model) {
323423 }
324424 } ) ;
325425 model . setAverageRatings ( initialRatings ) ;
326- } )
426+ } ) ;
327427 }
328428
329429 // Step 2: listener for each courses avgRating
@@ -348,17 +448,22 @@ function startAverageRatingListener(model) {
348448 } ) ;
349449}
350450
451+ /**
452+ * Fetches reviews for a specific course.
453+ * @param {string } courseCode
454+ * @returns
455+ */
351456export async function getReviewsForCourse ( courseCode ) {
352457 const reviewsRef = ref ( db , `reviews/${ courseCode } ` ) ;
353458 const snapshot = await get ( reviewsRef ) ;
354459 if ( ! snapshot . exists ( ) ) return [ ] ;
355460 const reviews = [ ] ;
356461 snapshot . forEach ( ( childSnapshot ) => {
357- if ( childSnapshot . key != "avgRating" )
358- reviews . push ( {
359- id : childSnapshot . key ,
360- ...childSnapshot . val ( ) ,
361- } ) ;
462+ if ( childSnapshot . key != "avgRating" )
463+ reviews . push ( {
464+ id : childSnapshot . key ,
465+ ...childSnapshot . val ( ) ,
466+ } ) ;
362467 } ) ;
363468 return reviews ;
364469}
0 commit comments