Skip to content

Commit dc8406f

Browse files
committed
Documentation for the model and firebase functions
1 parent 85d5b37 commit dc8406f

2 files changed

Lines changed: 571 additions & 452 deletions

File tree

my-app/firebase.js

Lines changed: 168 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
11
import { initializeApp } from "firebase/app";
22
import { 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";
514
import { 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
825
const firebaseConfig = {
926
apiKey: "AIzaSyCBckVI9nhAP62u5jZJW3F4SLulUv7znis",
@@ -25,6 +42,11 @@ export const googleProvider = new GoogleAuthProvider();
2542
googleProvider.addScope("profile");
2643
googleProvider.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+
*/
2850
export 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
6287
async 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+
*/
80110
export 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+
*/
105141
export 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+
*/
134173
function 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+
*/
181226
export 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+
*/
193242
export 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+
*/
207262
export 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+
*/
231291
export 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+
*/
254320
async 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+
*/
308393
export 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+
*/
321418
function 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+
*/
365467
export 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

Comments
 (0)