diff --git a/my-app/firebase.js b/my-app/firebase.js index 642f3f44..99cceddd 100644 --- a/my-app/firebase.js +++ b/my-app/firebase.js @@ -1,8 +1,7 @@ import { initializeApp } from "firebase/app"; import { getAuth, GoogleAuthProvider, onAuthStateChanged } from "firebase/auth"; -import { get, getDatabase, ref, set, onValue } from "firebase/database"; +import { get, getDatabase, ref, set, onValue, push } from "firebase/database"; import { reaction, toJS } from "mobx"; -// foo // Your web app's Firebase configuration const firebaseConfig = { apiKey: "AIzaSyCBckVI9nhAP62u5jZJW3F4SLulUv7znis", @@ -49,8 +48,8 @@ async function firebaseToModel(model) { noUpload = true; if (data.favourites) model.setFavourite(data.favourites); - if (data.currentSearch) - model.setCurrentSearch(data.currentSearch); + // if (data.currentSearch) + // model.setCurrentSearch(data.currentSearch); noUpload = false; }); } @@ -61,16 +60,17 @@ export function syncModelToFirebase(model) { () => ({ userId: model?.user, favourites: toJS(model.favourites), - currentSearch: toJS(model.currentSearch), + // currentSearch: toJS(model.currentSearch), // Add more per-user attributes here }), + // eslint-disable-next-line no-unused-vars ({ userId, favourites, currentSearch }) => { if (noUpload || !userId) return; const userRef = ref(db, `users/${userId}`); const dataToSync = { favourites, - currentSearch, + //currentSearch, }; set(userRef, dataToSync) @@ -169,3 +169,35 @@ export async function saveJSONCoursesToFirebase(model, data) { model.addCourse(course); }); } + + +export async function addReviewForCourse(courseCode, review) { + try { + const reviewsRef = ref(db, `reviews/${courseCode}`); + const newReviewRef = push(reviewsRef); + await set(newReviewRef, review); + } catch (error) { + console.error("Error when adding a course to firebase:", error); + } +} + + +export async function getReviewsForCourse(courseCode) { + const reviewsRef = ref(db, `reviews/${courseCode}`); + const snapshot = await get(reviewsRef); + if (!snapshot.exists()) return []; + + const reviews = []; + snapshot.forEach(childSnapshot => { + reviews.push({ + id: childSnapshot.key, // Firebase-generated unique key + userName: childSnapshot.val().userName, + text: childSnapshot.val().text + }); + }); + return reviews; +} + + + + diff --git a/my-app/src/model.js b/my-app/src/model.js index 513b20d5..3da7c114 100644 --- a/my-app/src/model.js +++ b/my-app/src/model.js @@ -1,4 +1,5 @@ -import { addCourse } from "../firebase"; +import { addCourse, addReviewForCourse, getReviewsForCourse } from "../firebase"; // for reviews + export const model = { user: undefined, @@ -65,4 +66,23 @@ export const model = { this.addCourse(course); }); }, + + //for reviews + async addReview(courseCode, review) { + try { + await addReviewForCourse(courseCode, review); + + } catch (error) { + console.error("Error adding review:", error); + } + }, + + async getReviews(courseCode) { + try { + return await getReviewsForCourse(courseCode); + } catch (error) { + console.error("Error fetching reviews:", error); + return []; + } + }, }; diff --git a/my-app/src/presenters/CourseViewPresenter.jsx b/my-app/src/presenters/CourseViewPresenter.jsx deleted file mode 100644 index 789d0fcf..00000000 --- a/my-app/src/presenters/CourseViewPresenter.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { observer } from "mobx-react-lite"; -import CourseView from "../views/CourseView.jsx"; - -const CourseViewPresenter = observer(() => { - return ; -}); - -export { CourseViewPresenter }; \ No newline at end of file diff --git a/my-app/src/presenters/ListViewPresenter.jsx b/my-app/src/presenters/ListViewPresenter.jsx index b3491d14..f90fb3d1 100644 --- a/my-app/src/presenters/ListViewPresenter.jsx +++ b/my-app/src/presenters/ListViewPresenter.jsx @@ -4,6 +4,7 @@ import { useState } from 'react'; import ListView from "../views/ListView.jsx"; import CoursePagePopup from '../views/Components/CoursePagePopup.jsx'; import PrerequisitePresenter from './PrerequisitePresenter.jsx'; +import {ReviewPresenter} from "../presenters/ReviewPresenter.jsx" const ListViewPresenter = observer(({ model }) => { const addFavourite = (course) => { @@ -22,7 +23,8 @@ const ListViewPresenter = observer(({ model }) => { const [isPopupOpen, setIsPopupOpen] = useState(false); const [selectedCourse, setSelectedCourse] = useState(null); - const preP = + const preP = ; + const reviewPresenter = ; const popup = { removeFavourite={removeFavourite} isOpen={isPopupOpen} onClose={() => setIsPopupOpen(false)} course={selectedCourse} - prerequisiteTree={preP} /> + prerequisiteTree={preP} + reviewPresenter={reviewPresenter}/> + return { + const [reviews, setReviews] = useState([]); + const [newReview, setNewReview] = useState(""); + + //fetch reviews when the course code changes + useEffect(() => { + async function fetchReviews() { + const data = await model.getReviews(course.code); + setReviews(data); + } + fetchReviews(); + }, [course.code, model]); + + + const handleReviewSubmit = async () => { + if (newReview.trim()) { + const review = { + userName: model.user?.name || "Anonymous", + text: newReview, + timestamp: Date.now(), //timestamp + }; + await model.addReview(course.code, review); + //fetch and update the reviews after submitting + const updatedReviews = await model.getReviews(course.code); + setReviews(updatedReviews); + setNewReview(""); //clear text bar + } + }; + + + return ( + + ); +}); + diff --git a/my-app/src/views/Components/CoursePagePopup.jsx b/my-app/src/views/Components/CoursePagePopup.jsx index e03a6912..85c88e0b 100644 --- a/my-app/src/views/Components/CoursePagePopup.jsx +++ b/my-app/src/views/Components/CoursePagePopup.jsx @@ -1,120 +1,122 @@ -import React, { useEffect, useRef } from 'react' +import React, { useEffect, useRef } from "react"; -function CoursePagePopup({ favouriteCourses, addFavourite, - removeFavourite, isOpen, onClose, course, prerequisiteTree }) { - const treeRef = useRef(null); - useEffect(() => { - const handleKeyDown = (event) => { - if (event.key === 'Escape') { - onClose(); - } - }; +function CoursePagePopup({ + favouriteCourses, + addFavourite, + removeFavourite, + isOpen, + onClose, + course, + prerequisiteTree, + reviewPresenter, +}) { + const treeRef = useRef(null); + useEffect(() => { + const handleKeyDown = (event) => { + if (event.key === "Escape") { + onClose(); + } + }; - if (isOpen) { - window.addEventListener('keydown', handleKeyDown); - } + if (isOpen) { + window.addEventListener("keydown", handleKeyDown); + } - return () => { - window.removeEventListener('keydown', handleKeyDown); - }; - }, [isOpen, onClose]); + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [isOpen, onClose]); - - const handleFavouriteClick = (course) => { - if (favouriteCourses.some(fav => fav.code === course.code)) { - removeFavourite(course); - } else { - addFavourite(course); - } -}; + const handleFavouriteClick = (course) => { + if (favouriteCourses.some((fav) => fav.code === course.code)) { + removeFavourite(course); + } else { + addFavourite(course); + } + }; - const handleTreeClick = () => { - if (treeRef.current) { - treeRef.current.focus(); // gives it focus - } - }; + const handleTreeClick = () => { + if (treeRef.current) { + treeRef.current.focus(); // gives it focus + } + }; - if (!isOpen || !course) return null; // Don't render if not open or course not selected - return ( -
-
e.stopPropagation()} - > -
-
- {/* Course Title Section */} -
-

- {course.code} - {course.name} - - ({course.credits} Credits) - -

-
-
-
- -
+ if (!isOpen || !course) return null; // Don't render if not open or course not selected + return ( +
+
e.stopPropagation()} + > +
+
+ {/* Course Title Section */} +
+

+ {course.code} -{" "} + {course.name} + + ({course.credits} Credits) + +

+
+
+
+ +
- {/* Description Section */} -
-

Course Description

-
-
+ {/* Description Section */} +
+

+ Course Description +

+
+
+
-
- - {/* Prerequisite Graph Tree Section */} -
-

- Prerequisite Graph Tree -

-
-
- {prerequisiteTree} -
- -
- {/* Reviews Section */} -
-

Reviews

-
-

Here would be some reviews of the course...

-
- -
-
- -
-
- ); + {/* Prerequisite Graph Tree Section */} +
+

+ Prerequisite Graph Tree +

+
+
+ {prerequisiteTree} +
+
+ {reviewPresenter} +
+
+ +
+
+ ); } export default CoursePagePopup; diff --git a/my-app/src/views/CourseView.jsx b/my-app/src/views/CourseView.jsx deleted file mode 100644 index 08de53e6..00000000 --- a/my-app/src/views/CourseView.jsx +++ /dev/null @@ -1,58 +0,0 @@ -// import React from 'react'; -// import PrerequisiteTree from "./PrerequisiteTree.jsx"; -// // import {model} from '/src/model.js'; -// export default function CourseView({ course }) { -// if (!course) return null; - - -// return ( -//
-// {/* Course Title Section */} -//
-//

-// {course.code} - {course.name} -// ({course.credits} Credits) {/* Display Credits */} -//

-//
-//
-// -//
- -// {/* Description Section */} -//
-//

Course Description

-//
-//
- -// {/* Prerequisite Graph Tree Section */} -//
-//

Prerequisite Graph Tree

-//

Graph tree or prerequisite info will go here...

-//
-// {/* Reviews Section */} -//
-//

Reviews

-//

Here would be some reviews of the course...

-//
- -// {/* Prerequisite Graph Tree Section */} -//
-//

Prerequisite Graph Tree

-// {/* Placeholder for graph tree */} -//
-//
-// ); -// } diff --git a/my-app/src/views/ReviewView.jsx b/my-app/src/views/ReviewView.jsx new file mode 100644 index 00000000..124bc638 --- /dev/null +++ b/my-app/src/views/ReviewView.jsx @@ -0,0 +1,42 @@ +import React from "react"; + +export function ReviewView({ + course, + reviews, + newReview, + setNewReview, + handleReviewSubmit, +}) { + if (!course) return null; + + return ( +
+

Reviews

+ {/* Reviews Section */} +
+ setNewReview(e.target.value)} + /> + +
+
+
    + {(reviews || []).map((rev, i) => ( +
  • + {rev.userName}: {rev.text} +
  • + ))} +
+
+
+ ); +}