Skip to content

Commit 459dfb0

Browse files
authored
Reviews are safer and show errors (#119)
* 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
1 parent fa10687 commit 459dfb0

File tree

13 files changed

+7631
-201
lines changed

13 files changed

+7631
-201
lines changed

my-app/firebase.js

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { initializeApp } from "firebase/app";
22
import { getAuth, GoogleAuthProvider, onAuthStateChanged } from "firebase/auth";
3-
import { get, getDatabase, ref, set, onValue, push } from "firebase/database";
3+
import { getFunctions, httpsCallable } from 'firebase/functions';
4+
import { get, getDatabase, ref, set, onValue, onChildRemoved, onChildAdded } from "firebase/database";
45
import { reaction, toJS } from "mobx";
56

67
// Your web app's Firebase configuration
@@ -17,6 +18,7 @@ const firebaseConfig = {
1718

1819
// Initialize Firebase
1920
const app = initializeApp(firebaseConfig);
21+
const functions = getFunctions(app);
2022
export const auth = getAuth(app);
2123
export const db = getDatabase(app);
2224
export const googleProvider = new GoogleAuthProvider();
@@ -50,6 +52,7 @@ export function connectToFirebase(model) {
5052
firebaseToModel(model); // Set up listeners for user-specific data
5153
syncModelToFirebase(model); // Start syncing changes to Firebase
5254
syncScrollPositionToFirebase(model);
55+
startAverageRatingListener(model);
5356
} else {
5457
model.setUser(null); // If no user, clear user-specific data
5558
}
@@ -123,6 +126,52 @@ export function syncScrollPositionToFirebase(model, containerRef) {
123126
containerRef.current?.removeEventListener("scroll", handleScroll);
124127
}
125128

129+
function startAverageRatingListener(model) {
130+
const coursesRef = ref(db, "reviews");
131+
132+
// Step 1: One-time fetch if model.avgRating is not initialized
133+
if (!model.avgRating || Object.keys(model.avgRating).length === 0) {
134+
get(coursesRef).then((snapshot) => {
135+
if (!snapshot.exists()) return;
136+
137+
const initialRatings = {};
138+
139+
snapshot.forEach((courseSnapshot) => {
140+
const courseCode = courseSnapshot.key;
141+
const avgRating = courseSnapshot.child("avgRating").val();
142+
143+
if (typeof avgRating === "number") {
144+
initialRatings[courseCode] = avgRating;
145+
}
146+
});
147+
148+
model.setAverageRatings(initialRatings);
149+
});
150+
}
151+
152+
// Step 2: listener for each courses avgRating
153+
onChildAdded(coursesRef, (courseSnapshot) => {
154+
const courseCode = courseSnapshot.key;
155+
const avgRatingRef = ref(db, `reviews/${courseCode}/avgRating`);
156+
157+
onValue(avgRatingRef, (ratingSnapshot) => {
158+
if (!ratingSnapshot.exists()) return;
159+
160+
const rating = ratingSnapshot.val();
161+
162+
if (typeof rating === "number") {
163+
model.updateAverageRating(courseCode, rating);
164+
}
165+
});
166+
});
167+
168+
onChildRemoved(coursesRef, (courseSnapshot) => {
169+
const courseCode = courseSnapshot.key;
170+
model.updateAverageRating(courseCode, null);
171+
});
172+
}
173+
174+
126175
function saveCoursesToCache(courses, timestamp) {
127176
const request = indexedDB.open("CourseDB", 1);
128177

@@ -288,11 +337,14 @@ async function loadCoursesFromCacheOrFirebase(model) {
288337

289338
export async function addReviewForCourse(courseCode, review) {
290339
try {
291-
const reviewsRef = ref(db, `reviews/${courseCode}`);
292-
const newReviewRef = push(reviewsRef);
293-
await set(newReviewRef, review);
340+
const reviewsRef = ref(db, `reviews/${courseCode}/${review.uid}`);
341+
await set(reviewsRef, review);
342+
const updateCourseAvgRating = httpsCallable(functions, 'updateCourseAvgRating');
343+
const result = await updateCourseAvgRating({ courseCode });
344+
345+
console.log('Average rating updated:', result.data.avgRating);
294346
} catch (error) {
295-
console.error("Error when adding a course to firebase:", error);
347+
console.error("Error when adding a course to firebase or updating the average:", error);
296348
}
297349
}
298350

@@ -302,6 +354,7 @@ export async function getReviewsForCourse(courseCode) {
302354
if (!snapshot.exists()) return [];
303355
const reviews = [];
304356
snapshot.forEach((childSnapshot) => {
357+
if(childSnapshot.key!="avgRating")
305358
reviews.push({
306359
id: childSnapshot.key,
307360
...childSnapshot.val(),

my-app/firebase.json

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,21 @@
77
"**/.*",
88
"**/node_modules/**"
99
]
10-
}
10+
},
11+
"functions": [
12+
{
13+
"source": "functions",
14+
"codebase": "default",
15+
"ignore": [
16+
"node_modules",
17+
".git",
18+
"firebase-debug.log",
19+
"firebase-debug.*.log",
20+
"*.local"
21+
],
22+
"predeploy": [
23+
"npm --prefix \"$RESOURCE_DIR\" run lint"
24+
]
25+
}
26+
]
1127
}

my-app/functions/.eslintrc.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
module.exports = {
2+
env: {
3+
es6: true,
4+
node: true,
5+
},
6+
parserOptions: {
7+
"ecmaVersion": 2018,
8+
},
9+
extends: [
10+
"eslint:recommended",
11+
"google",
12+
],
13+
rules: {
14+
"no-restricted-globals": ["error", "name", "length"],
15+
"prefer-arrow-callback": "error",
16+
"quotes": ["error", "double", {"allowTemplateLiterals": true}],
17+
},
18+
overrides: [
19+
{
20+
files: ["**/*.spec.*"],
21+
env: {
22+
mocha: true,
23+
},
24+
rules: {},
25+
},
26+
],
27+
globals: {},
28+
};

my-app/functions/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
*.local

my-app/functions/index.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const functions = require('firebase-functions');
2+
const admin = require('firebase-admin');
3+
admin.initializeApp();
4+
const db = admin.database();
5+
6+
// eslint-disable-next-line no-unused-vars
7+
exports.updateCourseAvgRating = functions.https.onCall(async (data, context) => {
8+
const courseCode = data.courseCode || data?.data?.courseCode;
9+
if (!courseCode) {
10+
throw new functions.https.HttpsError('invalid-argument', 'Missing courseCode');
11+
}
12+
13+
try {
14+
// Get all reviews for the course
15+
const reviewsSnap = await db.ref(`reviews/${courseCode}`).once('value');
16+
const reviews = reviewsSnap.val();
17+
18+
if (!reviews) {
19+
throw new functions.https.HttpsError('not-found', 'No reviews found for this course');
20+
}
21+
22+
// Compute average from overallRating
23+
let total = 0;
24+
let count = 0;
25+
26+
Object.values(reviews).forEach(review => {
27+
if (typeof review.overallRating === 'number') {
28+
total += review.overallRating;
29+
count++;
30+
}
31+
});
32+
33+
if (count === 0) {
34+
throw new functions.https.HttpsError('failed-precondition', 'No valid ratings');
35+
}
36+
37+
const avgRating = parseFloat((total / count).toFixed(2));
38+
39+
// Update the avgRating in reviews
40+
await db.ref(`reviews/${courseCode}`).update({ avgRating });
41+
42+
return { success: true, avgRating };
43+
} catch (error) {
44+
throw new functions.https.HttpsError('internal', error.message);
45+
}
46+
});

0 commit comments

Comments
 (0)