Skip to content

Commit 9638545

Browse files
jklugeSailet03
andauthored
Stability, Bug Fixes and Go-To-Top not visible during popup (#137)
* Locations and Departments * Not working functions stuff... * Reviews need login * Only one review per course * We update the averages automatically * Bug Fixes Yippeee * Bug Fixes, handle reviews and show errors * More ratings * Refactoring * Readme update * Even more README updates * Made the firebase safer and wrote more documentation in the README. * Update README.md * Update README.md * Update README.md * Update README.md * Reverted the environment refactoring * no login for ratings * removed dev and some performance / safety improvements * fixes * Documentation for the model and firebase functions * Some linting, fixes and funky stuff --------- Co-authored-by: Sailet03 <52610280+Sailet03@users.noreply.github.com>
1 parent 1d9e6b8 commit 9638545

17 files changed

+620
-632
lines changed

my-app/firebase.js

Lines changed: 166 additions & 61 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,8 +42,13 @@ export const googleProvider = new GoogleAuthProvider();
2542
googleProvider.addScope("profile");
2643
googleProvider.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
6185
async 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+
*/
79108
export 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+
*/
104139
export 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+
*/
133171
function 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+
*/
176224
export 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+
*/
186240
export 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+
*/
200260
export 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+
*/
222287
export 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+
*/
245316
async 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+
*/
295386
export 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+
*/
307407
function 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+
*/
351456
export 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

Comments
 (0)