diff --git a/my-app/src/model.js b/my-app/src/model.js index 2d3996b..34cbea0 100644 --- a/my-app/src/model.js +++ b/my-app/src/model.js @@ -15,22 +15,22 @@ export const model = { scrollPosition: 0, /* list of all course objects downloaded from the Firebase realtime database and stored locally as JSON object in this array */ courses: [], - departments : [], + departments: [], locations: [], // indexes: 0 -> overall rating; 1 -> difficulty; 2->teacher rating avgRatings: [], // model.avgRatings["IK1203"][0] /* courses the user selected as their favourite */ favourites: [], - searchHistory:[], + searchHistory: [], isReady: false, /* this is a boolean flag showing that filtering options in the UI have changed, triggering the FilterPresenter to recalculate the filteredCourses[] */ - filtersChange: false, + filtersChange: false, /* this is a flag showing if the filteredCourses[] has changed (since FilterPresenter recalculated it), so now SearchBarPresenter needs to recalculate currentSearch[] depending this updated list of courses */ filtersCalculated: false, /* this is the array that FilterPresenter fills up with course objects, filtered from the model.courses[] */ - filteredCourses: [], + filteredCourses: [], /* JSON object containing all important parameters the FilterPresenter needs to calculate the filtered list of courses */ filterOptions: { //apply-X-Filter boolean triggering flag wether corresponding filtering functions should run or not @@ -41,9 +41,9 @@ export const model = { level: ["PREPARATORY", "BASIC", "ADVANCED", "RESEARCH"], //the possible values for the array are: "PREPARATORY", "BASIC", "ADVANCED", "RESEARCH" applyLanguageFilter: false, language: "none", //the possible values for the string are: "none"/"english"/"swedish"/"both" - applyLocationFilter:false, + applyLocationFilter: false, location: [], //the possible values for the array are: 'KTH Campus', 'KTH Kista', 'AlbaNova', 'KTH Flemingsberg', 'KTH Solna', 'KTH Södertälje', 'Handelshögskolan', 'KI Solna', 'Stockholms universitet', 'KONSTFACK' - applyCreditsFilter:true, + applyCreditsFilter: true, creditMin: 0, creditMax: 45, applyDepartmentFilter: false, @@ -59,14 +59,14 @@ export const model = { _coursesListeners: [], // internal list of listeners onCoursesSet(callback) { - this._coursesListeners.push(callback); + this._coursesListeners.push(callback); }, _coursesListeners: [], // internal list of listeners urlStackPointer: 0, onCoursesSet(callback) { - this._coursesListeners.push(callback); + this._coursesListeners.push(callback); }, setUser(user) { @@ -74,19 +74,19 @@ export const model = { this.user = user; }, - setCurrentSearch(searchResults){ + setCurrentSearch(searchResults) { this.currentSearch = searchResults; }, - setCurrentSearchText(text){ + setCurrentSearchText(text) { this.currentSearchText = text; }, - + setScrollPosition(position) { this.scrollPosition = position; }, - setCourses(courses){ + setCourses(courses) { this.courses = courses; this._coursesListeners.forEach(cb => cb(courses)); }, @@ -106,20 +106,20 @@ export const model = { console.error("Error adding course code to the history:", error); } }, - setDepartments(departments){ + setDepartments(departments) { this.departments = departments; }, - setLocations(locations){ + setLocations(locations) { this.locations = locations; }, setAverageRatings(ratings) { this.avgRatings = ratings; }, - updateAverageRating(courseCode, rating){ - if(this.avgRatings!= null) + updateAverageRating(courseCode, rating) { + if (this.avgRatings != null) this.avgRatings[courseCode] = rating; }, - setFavourite(favorites){ + setFavourite(favorites) { this.favourites = favorites; }, @@ -175,7 +175,7 @@ export const model = { this.departments = Array.from(dep); this.locations = Array.from(loc); uploadDepartmentsAndLocations(this.departments, this.locations); - + }, //for reviews async addReview(courseCode, review) { @@ -187,7 +187,7 @@ export const model = { return false; } }, - + async getReviews(courseCode) { try { return await getReviewsForCourse(courseCode); @@ -206,7 +206,7 @@ export const model = { this.filtersCalculated = true; }, - setFilterOptions(options){ + setFilterOptions(options) { this.filterOptions = options; // do we want to set the flags? What about useEffect? }, @@ -214,7 +214,7 @@ export const model = { this.filterOptions.applyRemoveNullCourses = !this.filterOptions.applyRemoveNullCourses; this.setFiltersChange(); }, - + setApplyRemoveNullCourses() { this.filterOptions.applyRemoveNullCourses = !this.filterOptions.applyRemoveNullCourses; this.setFiltersChange(); @@ -285,8 +285,8 @@ export const model = { const sortedGrouped = Object.keys(grouped) .sort() .reduce((acc, key) => { - acc[key] = grouped[key].sort(); - return acc; + acc[key] = grouped[key].sort(); + return acc; }, {}); const fields = Object.entries(sortedGrouped).map(([school, departments], index) => ({ id: index + 1, @@ -295,12 +295,44 @@ export const model = { })); return fields; }, - async getAverageRating(courseCode) { + async getAverageRating(courseCode, option) { const reviews = await getReviewsForCourse(courseCode); if (!reviews || reviews.length === 0) return null; - const total = reviews.reduce((sum, review) => sum + (review.overallRating || 0), 0); - const avgRtg = (total / reviews.length).toFixed(1); - return avgRtg; + + let validReviews = 0; + let total = 0; + + switch (option) { + case "avg": + reviews.forEach(review => { + if (typeof review.overallRating === 'number') { + total += review.overallRating; + validReviews++; + } + }); + break; + case "diff": + reviews.forEach(review => { + if (typeof review.difficultyRating === 'number') { + total += review.difficultyRating; + validReviews++; + } + }); + break; + case "prof": + reviews.forEach(review => { + if (typeof review.professorRating === 'number') { + total += review.professorRating; + validReviews++; + } + }); + break; + default: + return null; + } + + if (validReviews === 0) return null; + return (total / validReviews).toFixed(1); }, setPopupOpen(isOpen) { @@ -326,9 +358,11 @@ export const model = { handleUrlChange() { let current_url = window.location.href; + let start_idx = indexOfNth(current_url, '/', 3) + 2; if (start_idx > 0 && start_idx < current_url.length) { + let course_code = current_url.slice(start_idx); let course = this.getCourse(course_code); if (course) { @@ -336,7 +370,7 @@ export const model = { this.setPopupOpen(true); } this.urlStackPointer++; - } else if (start_idx > 0){ + } else if (start_idx > 0) { this.setPopupOpen(false); } } diff --git a/my-app/src/presenters/SearchbarPresenter.jsx b/my-app/src/presenters/SearchbarPresenter.jsx index 08ed694..033d6d1 100644 --- a/my-app/src/presenters/SearchbarPresenter.jsx +++ b/my-app/src/presenters/SearchbarPresenter.jsx @@ -13,8 +13,8 @@ const SearchbarPresenter = observer(({ model }) => { const fuseOptions = { keys: [ - { name: 'code', weight: 0.6 }, - { name: 'name', weight: 0.3 }, + { name: 'code', weight: 0.5 }, + { name: 'name', weight: 0.4 }, { name: 'description', weight: 0.1 }, ], threshold: 0.3141592653589793238, // adjust this for sensitivity diff --git a/my-app/src/views/Components/CoursePagePopup.jsx b/my-app/src/views/Components/CoursePagePopup.jsx index e06bae8..457697e 100644 --- a/my-app/src/views/Components/CoursePagePopup.jsx +++ b/my-app/src/views/Components/CoursePagePopup.jsx @@ -4,32 +4,79 @@ import { model } from "../../model.js"; function CoursePagePopup({ - favouriteCourses, - handleFavouriteClick, - isOpen, - onClose, - course, - prerequisiteTree, - reviewPresenter, - sidebarIsOpen + favouriteCourses, + handleFavouriteClick, + isOpen, + onClose, + course, + prerequisiteTree, + reviewPresenter, + sidebarIsOpen }) { const treeRef = useRef(null); - const [showOverlay, setShowOverlay] = useState(false); + const [showOverlay, setShowOverlay] = useState(true); const [averageRating, setAverageRating] = useState(null); + const [professorRating, setProfessorRating] = useState(null); + const [difficultyRating, setDifficultyRating] = useState(null); + const handlePeriodsAndLanguages = (periods) => { + let ret_string = ""; + if (periods) { + let keys = Object.keys(periods); + for (let key of keys) { + if (periods[key]) { + ret_string += key + ", "; + } + } + return ret_string.slice(0, -2); + } else { + return; + } + }; + + const formatText = (text) => { + return text + .replace(/\n\n/g, '

') // Double line breaks + .replace(/•\s*/g, '
• ') // Bullet points with proper spacing + .replace(/(\d+\.)\s*/g, '
$1 ') // Numbered lists with proper spacing + .replace(/([.!?])\s+(?=[A-Z])/g, '$1

') // New line after sentences that end with capital letter + .replace(/[:]\s*(?=[A-Z])/g, ':
') // New line after colons followed by capital letter + .split(/\n/).map(line => { + // Check if line starts with bullet or number + if (!/^[•\d]/.test(line.trim())) { + return `
${line}
`; + } + return line; + }).join(''); + }; useEffect(() => { - const fetchAverageRating = async () => { + const fetchRatings = async () => { try { - const avg = await model.getAverageRating(course.code); + const avg = await model.getAverageRating(course.code, "avg"); setAverageRating(avg); + } catch (error) { setAverageRating(null); } + try { + const avg = await model.getAverageRating(course.code, "diff"); + setDifficultyRating(avg); + + } catch (error) { + setDifficultyRating(null); + } + try { + const avg = await model.getAverageRating(course.code, "prof"); + setProfessorRating(avg); + + } catch (error) { + setProfessorRating(null); + } }; - if (isOpen && course) fetchAverageRating(); + if (isOpen && course) fetchRatings(); }, [isOpen, course]); @@ -37,6 +84,7 @@ function CoursePagePopup({ useEffect(() => { const handleKeyDown = (event) => { if (event.key === 'Escape' || event.key === 'ArrowLeft') { + setShowOverlay(true); onClose(); } }; @@ -55,12 +103,10 @@ function CoursePagePopup({ }; if (!isOpen || !course) return null; - return (
-
+
{/* Course Title Section */}

@@ -89,155 +135,282 @@ function CoursePagePopup({ - ({course.credits} Credits) - + ({course.credits} Credits) +

-
- - -
- {averageRating !== null ? ( -

- Average Rating: {averageRating} / 5 -

- ) : ( -

- No Reviews Yet -

- )} - + + {/* Course Info Section */} +
+ {/* Top Row - Basic Info */} +
+
+
+ Academic Level: + + {course.academicLevel || 'Not specified'} + +
+
+ Department: + + {course.department || 'Not specified'} + +
+
+ Language: + + {handlePeriodsAndLanguages(course?.language) || 'Languages not specified'} + +
+
+ Course Periods: + + {handlePeriodsAndLanguages(course?.periods) || 'Periods not specified'} + +
+ +
+ +
+
+
+
+ Overall Rating: +
+ + + {averageRating !== null + ? `${Number(averageRating).toFixed(1)}/5.0` + : '(No ratings yet)'} + +
+
+ +
+ Professor Rating: +
+ + + {professorRating !== null + ? `${Number(professorRating).toFixed(1)}/5.0` + : '(No ratings yet)'} + +
+
+ +
+ Difficulty: +
+ + + {difficultyRating !== null + ? `${Number(difficultyRating).toFixed(1)}/5.0` + : '(No ratings yet)'} + +
+
+
+
+
+
+ + {/* Bottom Row - Favorite Button */} +
+
{/* Description Section */} - {course.description && - course.description.trim() && - course.description.trim() !== "null" && ( -
-

Course Description

-
+ {course.description && course.description.trim() && course.description.trim() !== "null" && ( +
+

+ + + + Course Description +

+
- )} +
+ )} - {/* Learning outcomes */} -
-

Learning Outcomes:

-
- {course.learning_outcomes && course.learning_outcomes.trim() && - course.learning_outcomes.trim() !== "null" ? ( -
+ {/* Learning Outcomes Section */} +
+

+ + + + Learning Outcomes +

+ {course.learning_outcomes && course.learning_outcomes.trim() && course.learning_outcomes.trim() !== "null" ? ( +
+
+
) : ( -

+

No learning outcomes information available

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

Prerequisite Graph Tree

-
-
- {showOverlay && ( + + {/* Prerequisite Tree Section - Updated icon */} +
+

+ + + + Prerequisite Graph Tree +

+ +
+
+
+
+ {showOverlay && ( +
{ + e.stopPropagation(); + setShowOverlay(false); + }} + > + + Click to interact with the graph + +
+ )}
{ - e.stopPropagation(); - setShowOverlay(false); - }} + className="bg-indigo-300/20 outline-none focus:outline-none focus:ring-2 focus:ring-violet-600 + rounded-lg transition-shadow min-h-[300px] p-4" + ref={treeRef} + onClick={handleTreeClick} + tabIndex={0} > + {prerequisiteTree}
- )} -
- {prerequisiteTree}
- {/* Prereq Section */} -
-

Prerequisites:

-
- {(course.prerequisites_text && course.prerequisites_text.trim() && - course.prerequisites_text.trim() !== "null" )? ( -
+ + {/* Prerequisites Section */} +
+

+ + + + Prerequisites +

+ {(course.prerequisites_text && course.prerequisites_text.trim() && course.prerequisites_text.trim() !== "null") ? ( +
+
+
) : ( -

+

Prerequisites information not available

)}
- {/* Reviews Section (optional) */} +
+ {/* Reviews Section */} {reviewPresenter && ( -
-

Reviews

-
+
+

+ + + + Reviews +

{reviewPresenter}
)} @@ -251,4 +424,4 @@ function CoursePagePopup({ ); } -export default CoursePagePopup; \ No newline at end of file +export default CoursePagePopup; diff --git a/my-app/src/views/SearchbarView.jsx b/my-app/src/views/SearchbarView.jsx index 985f948..50d1076 100644 --- a/my-app/src/views/SearchbarView.jsx +++ b/my-app/src/views/SearchbarView.jsx @@ -45,7 +45,7 @@ function SearchbarView(props) { return (
- KTH Logo