diff --git a/client/.eslintcache b/client/.eslintcache
index 310575d3..57e2043d 100644
--- a/client/.eslintcache
+++ b/client/.eslintcache
@@ -18,7 +18,7 @@
"/Users/willzhang/Developments/dti/cureviews-stable/client/src/modules/NotFound/Components/NotFound.tsx": "16",
"/Users/willzhang/Developments/dti/cureviews-stable/client/src/modules/Admin/Components/Admin.tsx": "17",
"/Users/willzhang/Developments/dti/cureviews-stable/client/src/modules/Results/Components/Results.tsx": "18",
- "/Users/willzhang/Developments/dti/cureviews-stable/client/src/modules/Results/Components/ResultsDisplay.jsx": "19",
+ "/Users/willzhang/Developments/dti/cureviews-stable/client/src/modules/Results/Components/ResultsDisplay.tsx": "19",
"/Users/willzhang/Developments/dti/cureviews-stable/client/src/modules/Course/Components/Feedback.js": "20",
"/Users/willzhang/Developments/dti/cureviews-stable/client/src/modules/Globals/Navbar.tsx": "21",
"/Users/willzhang/Developments/dti/cureviews-stable/client/src/session-store.ts": "22",
@@ -36,7 +36,7 @@
"/Users/willzhang/Developments/dti/cureviews-stable/client/src/modules/Admin/Components/AdminReview.tsx": "34",
"/Users/willzhang/Developments/dti/cureviews-stable/client/src/modules/Admin/Components/Stats.tsx": "35",
"/Users/willzhang/Developments/dti/cureviews-stable/client/src/modules/Admin/Components/RaffleWinner.tsx": "36",
- "/Users/willzhang/Developments/dti/cureviews-stable/client/src/modules/Results/Components/PreviewCard.jsx": "37",
+ "/Users/willzhang/Developments/dti/cureviews-stable/client/src/modules/Results/Components/PreviewCard.tsx": "37",
"/Users/willzhang/Developments/dti/cureviews-stable/client/src/modules/Results/Components/FilteredResult.tsx": "38",
"/Users/willzhang/Developments/dti/cureviews-stable/client/src/modules/Results/Components/FilterPopup.tsx": "39",
"/Users/willzhang/Developments/dti/cureviews-stable/client/src/modules/Globals/majors.ts": "40",
@@ -884,7 +884,7 @@
],
"/Users/willzhang/Developments/dti/cureviews-stable/client/src/modules/Results/Components/Results.tsx",
[],
- "/Users/willzhang/Developments/dti/cureviews-stable/client/src/modules/Results/Components/ResultsDisplay.jsx",
+ "/Users/willzhang/Developments/dti/cureviews-stable/client/src/modules/Results/Components/ResultsDisplay.tsx",
[],
"/Users/willzhang/Developments/dti/cureviews-stable/client/src/modules/Course/Components/Feedback.js",
[],
@@ -931,7 +931,7 @@
[
"220"
],
- "/Users/willzhang/Developments/dti/cureviews-stable/client/src/modules/Results/Components/PreviewCard.jsx",
+ "/Users/willzhang/Developments/dti/cureviews-stable/client/src/modules/Results/Components/PreviewCard.tsx",
[],
"/Users/willzhang/Developments/dti/cureviews-stable/client/src/modules/Results/Components/FilteredResult.tsx",
[
diff --git a/client/public/surprised_bear.svg b/client/public/surprised_bear.svg
new file mode 100644
index 00000000..ee1c59a3
--- /dev/null
+++ b/client/public/surprised_bear.svg
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/modules/Globals/Styles/Loading.module.css b/client/src/modules/Globals/Styles/Loading.module.css
index d6ec7dd6..e9a292d6 100644
--- a/client/src/modules/Globals/Styles/Loading.module.css
+++ b/client/src/modules/Globals/Styles/Loading.module.css
@@ -15,8 +15,14 @@
width: 80px;
height: 80px;
animation: spin 1s linear infinite;
- position: absolute;
- top: 30%;
- left: 50%;
transform: translate(-50%, 0);
+ position: fixed;
+ margin: auto;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ /*position: absolute;*/
+ /*top: 30%;*/
+ /*left: 50%;*/
}
diff --git a/client/src/modules/Results/Components/PreviewCard.jsx b/client/src/modules/Results/Components/PreviewCard.jsx
deleted file mode 100644
index ddc5fc1b..00000000
--- a/client/src/modules/Results/Components/PreviewCard.jsx
+++ /dev/null
@@ -1,178 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import axios from 'axios';
-import { lastOfferedSems } from 'common/CourseCard';
-
-import Gauges from '../../Course/Components/Gauges';
-import ReviewCard from '../../Course/Components/ReviewCard';
-
-import styles from '../Styles/CoursePreview.module.css';
-const Review = ReviewCard;
-
-/*
- Preview Card component.
-
- Props: course - course object used to render a preview card for ResultsDisplay
- to use.
-*/
-
-export default class PreviewCard extends Component {
- constructor(props) {
- super(props);
- // Set gauge values
- this.state = {
- id: this.props.course._id,
- rating: this.props.course.classRating,
- ratingColor: 'E64458',
- diff: this.props.course.classDifficulty,
- diffColor: 'E64458',
- workload: this.props.course.classWorkload,
- workloadColor: 'E64458',
- topReview: {},
- numReviews: 0,
- topReviewLikes: 0
- };
-
- this.updateTopReview = this.updateTopReview.bind(this);
- this.updateGauges = this.updateGauges.bind(this);
- }
-
- componentDidMount() {
- this.updateGauges();
- }
-
- componentDidUpdate(prevProps) {
- if (prevProps !== this.props) {
- this.updateGauges();
- }
- }
-
- // If the value of the metric is null, set the Gauge value to "-"
- updateGauges() {
- this.setState(
- {
- id: this.props.course._id,
- rating: Number(this.props.course.classRating)
- ? this.props.course.classRating
- : '-',
- diff: Number(this.props.course.classDifficulty)
- ? this.props.course.classDifficulty
- : '-',
- workload: Number(this.props.course.classWorkload)
- ? this.props.course.classWorkload
- : '-'
- },
- );
- this.updateTopReview();
- }
-
- // Updates the top review to be the one with the most likes
- updateTopReview() {
- axios
- .post(`/api/courses/get-reviews`, {
- courseId: this.props.course._id
- })
- .then((response) => {
- const reviews = response.data.result;
- if (reviews) {
- if (reviews.length > 0) {
- reviews.sort((a, b) =>
- (a.likes ? a.likes : 0) < (b.likes ? b.likes : 0) ? 1 : -1
- );
- this.setState({
- topReview: reviews[0],
- topReviewLikes: reviews[0].likes ? reviews[0].likes : 0, //Account for undefined likes in review obj
- numReviews: reviews.length
- });
- } else {
- this.setState({
- topReview: {},
- numReviews: 0
- });
- }
- }
- });
- }
-
- render() {
- let theClass = this.props.course;
- const offered = lastOfferedSems(theClass);
- return (
-
-
-
-
- {theClass.classSub.toUpperCase() +
- ' ' +
- theClass.classNum +
- ', ' +
- offered}
-
-
-
-
-
- {this.state.numReviews !== 0 && (
-
Top Review
- )}
-
-
-
- );
- }
-}
-
-// takes in the database object representing this review
-PreviewCard.propTypes = {
- course: PropTypes.object.isRequired
-};
diff --git a/client/src/modules/Results/Components/PreviewCard.tsx b/client/src/modules/Results/Components/PreviewCard.tsx
new file mode 100644
index 00000000..f89d96e3
--- /dev/null
+++ b/client/src/modules/Results/Components/PreviewCard.tsx
@@ -0,0 +1,136 @@
+import React, { useEffect, useState } from 'react';
+import axios from 'axios';
+import { lastOfferedSems } from 'common/CourseCard';
+
+import Gauges from '../../Course/Components/Gauges';
+import { Review as ReviewType } from 'common';
+import ReviewCard from '../../Course/Components/ReviewCard';
+
+import styles from '../Styles/CoursePreview.module.css';
+import { Class } from 'common';
+
+import Bear from '/surprised_bear.svg';
+
+const Review = ReviewCard;
+
+export const PreviewCard = ({ course }: PreviewCardProps) => {
+ const [id, setId] = useState('');
+ const [rating, setRating] = useState('-');
+ const [diff, setDiff] = useState('-');
+ const [workload, setWorkload] = useState('-');
+ const [topReview, setTopReview] = useState(null);
+ const [numReviews, setNumReviews] = useState(0);
+ const [topReviewLikes, setTopReviewLikes] = useState(0);
+ const [offered, setOffered] = useState('');
+ const [loading, setLoading] = useState(true);
+
+ const updateCourseInfo = () => {
+ if (course && course._id !== id) {
+ setId(course._id);
+ setRating(course.classRating ? String(course.classRating) : '-');
+ setDiff(course.classDifficulty ? String(course.classDifficulty) : '-');
+ setWorkload(course.classWorkload ? String(course.classWorkload) : '-');
+
+ axios.post(`/api/courses/get-reviews`, { courseId: course ? course._id : id })
+ .then((response) => {
+ const reviews = response.data.result;
+ if (reviews && reviews.length > 0) {
+ reviews.sort((a: ReviewType, b: ReviewType) =>
+ (a.likes || 0) < (b.likes || 0) ? 1 : -1
+ );
+ setTopReview(reviews[0]);
+ setTopReviewLikes(reviews[0].likes || 0);
+ setNumReviews(reviews.length);
+ setOffered(lastOfferedSems(course));
+ } else {
+ setTopReview({});
+ setNumReviews(0);
+ }
+ setLoading(false);
+ });
+ }
+ }
+
+ useEffect(() => {
+ setLoading(true);
+ }, []);
+
+ useEffect(() => {
+ updateCourseInfo();
+ }, [course, updateCourseInfo]);
+
+ if (!course) return (<>>);
+
+ return !loading && (
+
+
+
+
+ {course.classSub.toUpperCase() +
+ ' ' +
+ course.classNum +
+ ', ' +
+ offered}
+
+
+
+
+
+ {numReviews !== 0 &&
Top Review
}
+
+
+
+ );
+};
+
+interface PreviewCardProps {
+ course: Class;
+}
+
+export default PreviewCard;
\ No newline at end of file
diff --git a/client/src/modules/Results/Components/Results.tsx b/client/src/modules/Results/Components/Results.tsx
index 11ca38db..b1e70fb9 100644
--- a/client/src/modules/Results/Components/Results.tsx
+++ b/client/src/modules/Results/Components/Results.tsx
@@ -1,8 +1,8 @@
-import React, { Component } from 'react';
+import React, { Component, useEffect, useState } from 'react';
import axios from 'axios';
import Navbar from '../../Globals/Navbar';
-import ResultsDisplay from './ResultsDisplay.jsx';
+import ResultsDisplay from './ResultsDisplay.js';
import styles from '../Styles/Results.module.css';
@@ -25,57 +25,39 @@ type ResultsLists = {
* Results Component
* Used to render the results page. Uses Navbar and ResultsDisplay components directly.
*/
-export class Results extends Component {
- constructor(props: ResultsProps) {
- super(props);
- this.state = {
- courseList: [],
- loading: true
- };
-
- this.updateResults = this.updateResults.bind(this);
- }
+export const Results = ({match, history}: ResultsProps) => {
+ const [courseList, setCourseList] = useState([]);
+ const [loading, setLoading] = useState(true);
- async updateResults() {
+ const updateResults = async () => {
const response = await axios.post(`/api/search/get-courses`, {
- query: this.props.match.params.input.toLowerCase()
+ query: match.params.input.toLowerCase()
});
-
- const courseList = response.data.result.courses;
- this.setState({
- courseList: !courseList.error && courseList.length > 0 ? courseList : [],
- loading: false
- });
- }
-
- componentDidUpdate(prevProps: ResultsProps) {
- if (prevProps !== this.props) {
- this.setState({
- courseList: [],
- loading: true
- });
- this.updateResults();
- }
- }
-
- componentDidMount() {
- this.updateResults();
+ const list = response.data.result.courses;
+ setCourseList(!list.error && list.length > 0 ? list : []);
+ setLoading(false);
}
- render() {
- const userInput = this.props.match.params.input.split('+').join(' ');
- return (
-
-
-
-
-
- );
- }
+ useEffect(() => {
+ setCourseList([]);
+ setLoading(true);
+ updateResults();
+ }, [match, history])
+
+ const userInput = match.params.input.split('+').join(' ');
+ return (
+
+
+
+
+
+ );
}
+
+export default Results;
\ No newline at end of file
diff --git a/client/src/modules/Results/Components/ResultsDisplay.jsx b/client/src/modules/Results/Components/ResultsDisplay.jsx
deleted file mode 100644
index f4b7b384..00000000
--- a/client/src/modules/Results/Components/ResultsDisplay.jsx
+++ /dev/null
@@ -1,410 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-
-import FilteredResult from './FilteredResult.tsx';
-import PreviewCard from './PreviewCard.jsx';
-import FilterPopup from './FilterPopup';
-import Loading from '../../Globals/Loading';
-
-import FilterIcon from '../../../assets/icons/filtericon.svg';
-
-import styles from '../Styles/Results.module.css';
-
-/*
- ResultsDisplay Component.a
-
- Used by Results component, renders filters,
- list of class objects (results), and PreviewCard.
-
- Props: courses - is a list of class objects
- loading - bool, true if back-end has no returned from search yet
-
-*/
-
-export default class ResultsDisplay extends Component {
- constructor(props) {
- super(props);
- this.state = {
- courseList: this.props.courses,
- cardCourse: this.props.courses[0],
- activeCard: 0,
- selected: props.type === 'major' ? 'rating' : 'relevance',
- filters: {
- Fall: true,
- Spring: true,
- 1000: true,
- 2000: true,
- 3000: true,
- 4000: true,
- '5000+': true
- },
- filterMap: this.getInitialFilterMap(), // key value pair name:checked
- filteredItems: this.props.courses,
- fullscreen: false,
- transformGauges: false,
- showFilterPopup: false
- };
- this.previewHandler = this.previewHandler.bind(this);
- this.sortBy = this.sortBy.bind(this);
- this.getSubjectOptions = this.getSubjectOptions.bind(this);
- this.renderCheckboxes = this.renderCheckboxes.bind(this);
- this.filterClasses = this.filterClasses.bind(this);
- this.getInitialFilterMap = this.getInitialFilterMap.bind(this);
- this.sort = this.sort.bind(this);
- this.setShowFilterPopup = this.setShowFilterPopup.bind(this);
- this.scrollReviews = this.scrollReviews.bind(this);
- this.toggleFullscreen = this.toggleFullscreen.bind(this);
- }
-
- componentDidUpdate(prevProps, prevState) {
- if (
- prevProps !== this.props ||
- prevState.courseList.length !== this.state.courseList.length
- ) {
- this.setState(
- {
- courseList: this.props.courses,
- relevantCourseList: this.props.courses,
- cardCourse: this.props.courses[0]
- },
- () => this.filterClasses()
- );
- }
- if (prevProps.userInput !== this.props.userInput) {
- this.setState({ filterMap: this.getInitialFilterMap() }, () =>
- this.filterClasses()
- );
- }
- }
-
- getInitialFilterMap() {
- return new Map([
- [
- 'levels',
- new Map([
- ['1000', true],
- ['2000', true],
- ['3000', true],
- ['4000', true],
- ['5000+', true]
- ])
- ],
- [
- 'semesters',
- new Map([
- ['Fall', true],
- ['Spring', true]
- ])
- ],
- ['subjects', []]
- ]);
- }
-
- /**
- * Handles selecting different sort filters
- */
- handleSelect = (event) => {
- let opt = event.target.value;
- this.setState({ selected: opt }, () => this.sort());
- };
-
- /**
- * Helper function to sort()
- */
- sortBy(courseList, sortByField, fieldDefault, increasing) {
- courseList = courseList.sort((a, b) => {
- let first = Number(b[sortByField]) || fieldDefault;
- let second = Number(a[sortByField]) || fieldDefault;
-
- if (first === second) {
- return a.classNum - b.classNum;
- } else {
- if (increasing) {
- return first - second;
- } else {
- return second - first;
- }
- }
- });
- this.setState({
- filteredItems: courseList,
- cardCourse: courseList[0],
- activeCard: 0
- });
- }
-
- /**
- * Sorts list of class results by category selected in this.state.selected
- */
- sort() {
- let availableClasses;
- if (this.state.filteredItems.length === 0) {
- availableClasses = this.state.courseList;
- } else {
- availableClasses = this.state.filteredItems;
- }
-
- if (this.state.selected === 'relevance') {
- this.sortBy(availableClasses, 'score', 0, true);
- } else if (this.state.selected === 'rating') {
- this.sortBy(availableClasses, 'classRating', 0, true);
- } else if (this.state.selected === 'diff') {
- this.sortBy(
- availableClasses,
- 'classDifficulty',
- Number.MAX_SAFE_INTEGER,
- false
- );
- } else if (this.state.selected === 'work') {
- this.sortBy(
- availableClasses,
- 'classWorkload',
- Number.MAX_SAFE_INTEGER,
- false
- );
- }
- }
-
- filterClasses() {
- let semesters = Array.from(
- this.state.filterMap.get('semesters').keys()
- ).filter((semester) => this.state.filterMap.get('semesters').get(semester));
-
- let filteredItems = this.state.courseList.filter((course) =>
- semesters.some((semester) =>
- course.classSems.some((element) =>
- element.includes(semester.slice(0, 2).toUpperCase())
- )
- )
- );
-
- let levels = Array.from(this.state.filterMap.get('levels').keys()).filter(
- (level) => this.state.filterMap.get('levels').get(level)
- );
- filteredItems = filteredItems.filter((course) =>
- levels.some((level) =>
- level === '5000+'
- ? course.classNum.slice(0, 1) >= '5'
- : course.classNum.slice(0, 1) === level.slice(0, 1)
- )
- );
-
- let subjectsObjects = this.state.filterMap.get('subjects');
- if (subjectsObjects && subjectsObjects.length > 0) {
- filteredItems = filteredItems.filter((course) =>
- subjectsObjects.some(
- (subjectObject) =>
- course.classSub.toUpperCase() === subjectObject.value
- )
- );
- }
-
- this.setState({ filteredItems: filteredItems }, () => this.sort());
- }
-
- /**
- * Updates the list of filtered items when filters are checked/unchecked
- */
- checkboxOnChange = (e) => {
- const group = e.target.getAttribute('group');
- const name = e.target.name;
- const checked = e.target.checked;
-
- let newFilterMap = this.state.filterMap;
-
- newFilterMap.get(group).set(name, checked);
-
- this.setState({ filterMap: newFilterMap }, () => this.filterClasses());
- };
-
- /**
- * Updates the displayed PreviewCard to the correct [course]
- * if the course's [index] in the list of FilteredResult components is clicked
- */
- previewHandler(course, index) {
- this.setState({
- cardCourse: course,
- activeCard: index
- });
- this.setState({ transformGauges: false });
- }
-
- computeHeight() {
- return (
- window.innerWidth ||
- document.documentElement.clientWidth ||
- document.body.clientWidth
- );
- }
-
- /**
- * Displays the filtered items as FilteredResult components if there are any
- * The original list as FilteredResult components otherwise
- */
- renderResults() {
- const items = this.state.filteredItems.length
- ? this.state.filteredItems
- : this.state.courseList;
-
- return items.map((result, index) => (
-
-
-
- ));
- }
-
- renderCheckboxes(group) {
- let groupList = Array.from(this.state.filterMap.get(group).keys());
- return groupList.map((name, index) => (
-
-
- this.checkboxOnChange(e)}
- type="checkbox"
- checked={this.state.filterMap.get(group).get(name)}
- group={group}
- name={name}
- />
- {name}
-
-
- ));
- }
-
- getSubjectOptions(inputValue, callback) {
- console.log();
- }
-
- setShowFilterPopup() {
- this.setState({ showFilterPopup: !this.state.showFilterPopup });
- }
-
- scrollReviews(e) {
- const currentScrollY = e.target.scrollTop;
- if (currentScrollY > 80) {
- this.setState({ transformGauges: true });
- } else {
- this.setState({ transformGauges: false });
- }
- }
-
- toggleFullscreen() {
- this.setState({ fullscreen: false });
- }
-
- render() {
- return (
-
-
Search Results
- {/* Case where results are still being loaded */}
- {this.props.loading === true &&
}
- {/* Case where no results returned */}
- {this.state.courseList.length === 0 && this.props.loading === false && (
-
-
-
Sorry! No classes match your search.
-
- )}
- {/* Case where results are returned (non-empty) */}
- {this.state.courseList.length !== 0 && this.props.loading !== true && (
-
-
-
Filter
-
-
Semester
- {this.renderCheckboxes('semesters')}
-
-
-
Level
- {this.renderCheckboxes('levels')}
-
-
-
-
-
- We found{' '}
-
- {this.state.filteredItems.length === 0
- ? this.state.courseList.length
- : this.state.filteredItems.length}
- {' '}
- courses for "{this.props.userInput}
- "
-
-
-
-
- Sort By:
- this.handleSelect(e)}
- >
- Relevance
- Overall Rating
- Difficulty
- Workload
-
-
-
-
- Filter
-
-
- {this.state.showFilterPopup && (
-
- )}
-
-
-
-
- )}
-
- );
- }
-}
-
-ResultsDisplay.propTypes = {
- courses: PropTypes.array.isRequired,
- loading: PropTypes.bool.isRequired,
- type: PropTypes.string.isRequired,
- userInput: PropTypes.string.isRequired
-};
diff --git a/client/src/modules/Results/Components/ResultsDisplay.tsx b/client/src/modules/Results/Components/ResultsDisplay.tsx
new file mode 100644
index 00000000..f94564c3
--- /dev/null
+++ b/client/src/modules/Results/Components/ResultsDisplay.tsx
@@ -0,0 +1,411 @@
+import React, { useEffect, useState } from 'react';
+
+import FilteredResult from './FilteredResult';
+import PreviewCard from './PreviewCard';
+import FilterPopup from './FilterPopup';
+import Loading from '../../Globals/Loading';
+
+import FilterIcon from '../../../assets/icons/filtericon.svg';
+
+import styles from '../Styles/Results.module.css';
+import { Class } from 'common';
+import Bear from '/surprised_bear.svg';
+
+/*
+ ResultsDisplay Component.a
+
+ Used by Results component, renders filters,
+ list of class objects (results), and PreviewCard.
+
+ Props: courses - is a list of class objects
+ loading - bool, true if back-end has no returned from search yet
+
+*/
+
+export const ResultsDisplay = ({
+ courses,
+ loading,
+ type,
+ userInput
+}: ResultsDisplayProps) => {
+ const [courseList, setCourseList] = useState(courses);
+ const [filteredItems, setFilteredItems] = useState(courses);
+ const [cardCourse, setCardCourse] = useState(courses[0]);
+ const [activeCard, setActiveCard] = useState(0);
+
+ type SortBy = 'relevance' | 'rating' | 'diff' | 'work';
+ const [selected, setSelected] = useState(
+ type === 'major' ? 'rating' : 'relevance'
+ );
+
+ const [transformGauges, setTransformGauges] = useState(false);
+ const [showFilterPopup, setShowFilterPopup] = useState(false);
+ const [searchListViewEnabled, setSearchListViewEnabled] = useState(true);
+
+ type FilterValue = Map | string[];
+ type FilterMap = Map;
+ const getInitialFilterMap = (): FilterMap =>
+ new Map([
+ [
+ 'levels',
+ new Map([
+ ['1000', true],
+ ['2000', true],
+ ['3000', true],
+ ['4000', true],
+ ['5000+', true]
+ ])
+ ],
+ [
+ 'semesters',
+ new Map([
+ ['Fall', true],
+ ['Spring', true]
+ ])
+ ],
+ [
+ 'subjects', []
+ ]
+ ]);
+ const [filterMap, setFilterMap] = useState(getInitialFilterMap());
+
+ useEffect(() => {
+ setCourseList(courses);
+ setCardCourse(courses[0] || {});
+ setFilteredItems(courses);
+ setFilterMap(getInitialFilterMap());
+ filterClasses();
+ }, [courses, loading, type, userInput, courseList]);
+
+ useEffect(() => {
+ filterClasses();
+ }, [filterMap]);
+
+ useEffect(() => {
+ sort(filteredItems);
+ }, [selected]);
+
+ /**
+ * Handles selecting different sort filters
+ */
+ const handleSelect = (event: React.ChangeEvent) => {
+ let opt = event.target.value as SortBy;
+ setSelected(opt);
+ };
+
+ /**
+ * Helper function to sort()
+ */
+ const sortBy = (
+ courseList: Array,
+ sortByField: string,
+ fieldDefault: number,
+ increasing: boolean
+ ) => {
+ if (courseList.length === 0) return;
+ const sorted = [...courseList].sort((a, b) => {
+ const first = Number(b[sortByField]) || fieldDefault;
+ const second = Number(a[sortByField]) || fieldDefault;
+
+ if (first === second) {
+ return a.classNum - b.classNum;
+ } else {
+ return increasing ? first - second : second - first;
+ }
+ });
+
+ setFilteredItems(sorted);
+ setCardCourse(sorted[0]);
+ setActiveCard(0);
+ };
+
+ /**
+ * Sorts list of class results by category selected in state
+ */
+ const sort = (items: Array) => {
+ switch (selected) {
+ case 'relevance':
+ sortBy(items, 'score', 0, true);
+ break;
+ case 'rating':
+ sortBy(items, 'classRating', 0, true);
+ break;
+ case 'diff':
+ sortBy(items, 'classDifficulty', Number.MAX_SAFE_INTEGER, false);
+ break;
+ case 'work':
+ sortBy(items, 'classDifficulty', 0, true);
+ break;
+ }
+ };
+
+ const filterClasses = () => {
+ const semesters = Array.from(
+ (filterMap.get('semesters') as Map).entries()
+ )
+ .filter(([_, value]) => value)
+ .map(([key]) => key);
+
+ let filtered = courseList.filter((course) =>
+ semesters.some((semester) =>
+ course.classSems?.some((element: string) =>
+ element.includes(semester.slice(0, 2).toUpperCase())
+ )
+ )
+ );
+
+ const levels = Array.from(
+ (filterMap.get('levels') as Map).entries()
+ )
+ .filter(([_, value]) => value)
+ .map(([key]) => key);
+
+ filtered = filtered.filter((course) =>
+ levels.some((level) =>
+ level === '5000+'
+ ? course.classNum[0] >= '5'
+ : course.classNum[0] === level[0]
+ )
+ );
+
+ const subjects = filterMap.get('subjects') as string[];
+ if (subjects && subjects.length > 0) {
+ filtered = filtered.filter((course) =>
+ subjects.some(
+ (subject) => course.classSub.toUpperCase() === subject.toUpperCase()
+ )
+ );
+ }
+
+ setFilteredItems(filtered);
+ sort(filtered);
+ };
+
+ /**
+ * Updates the list of filtered items when filters are checked/unchecked
+ */
+ const checkboxOnChange = (e: React.ChangeEvent) => {
+ const group = e.currentTarget.dataset.group!;
+ const name = e.currentTarget.name;
+ const checked = e.currentTarget.checked;
+
+ setFilterMap((prev) => {
+ const updatedMap = new Map(prev);
+ const groupValue = updatedMap.get(group);
+
+ if (groupValue instanceof Map) {
+ groupValue.set(name, checked); // Update boolean map
+ updatedMap.set(group, groupValue);
+ }
+ return updatedMap;
+ });
+ filterClasses();
+ };
+
+ /**
+ * Updates the displayed PreviewCard to the correct [course]
+ * if the course's [index] in the list of FilteredResult components is clicked
+ */
+ const previewHandler = (course: any, index: number) => {
+ setCardCourse(course);
+ setActiveCard(index);
+ setTransformGauges(false);
+ };
+
+ /**
+ * Displays the filtered items as FilteredResult components if there are any
+ * The original list as FilteredResult components otherwise
+ */
+ const renderResults = () => {
+ if (filteredItems.length === 0) {
+ return <>>
+ } else {
+ return filteredItems.map((result, index) => (
+
+
+
+ ));
+ }
+ };
+
+ const renderCheckboxes = (group: string) => {
+ let groupList = Array.from(
+ (filterMap.get(group) as Map).keys()
+ );
+ return groupList.map((name) => (
+
+
+ checkboxOnChange(e)}
+ type="checkbox"
+ checked={(filterMap.get(group) as Map).get(name)}
+ data-group={group}
+ name={name}
+ />
+ {name}
+
+
+ ));
+ };
+
+ return (
+ <>
+ {loading && }
+
+ {/* Case where results are returned, even if zero */}
+ {!loading && (
+
+ {/* Case where no results returned */}
+
+ {courseList.length !== 0 &&
Search Results }
+ {/*
setSearchListViewEnabled(!searchListViewEnabled)}*/}
+ {/*>*/}
+ {/* {searchListViewEnabled ? 'Hide' : 'View'} Search Results*/}
+ {/* */}
+
+ {courseList.length !== 0 && (
+
+
Filter
+
+
Semester
+ {renderCheckboxes('semesters')}
+
+
+
Level
+ {renderCheckboxes('levels')}
+
+
+ )}
+
+ {filteredItems.length !== 0 && (
+
+ {searchListViewEnabled && (
+ <>
+
+ We found {filteredItems.length} courses for
+ "
+ {userInput}
+ "
+
+
+
+
+ Sort By:
+ handleSelect(e)}
+ >
+ Relevance
+ Overall Rating
+ Difficulty
+ Workload
+
+
+
+
setShowFilterPopup(!showFilterPopup)}
+ >
+ Filter
+
+
+ {showFilterPopup && (
+
+ setShowFilterPopup(!showFilterPopup)
+ }
+ />
+ )}
+
+
+ >
+ )}
+
+ )}
+ {filteredItems.length === 0 && courseList.length !== 0 && (
+
+ {searchListViewEnabled && (
+ <>
+
+
+
setShowFilterPopup(!showFilterPopup)}
+ >
+ Filter
+
+
+ {showFilterPopup && (
+
+ setShowFilterPopup(!showFilterPopup)
+ }
+ />
+ )}
+
+ >
+ )}
+
+ )}
+
+
+
+ {filteredItems.length === 0 && (
+
+
+
+ No classes found. Try another search
+ {courseList.length !== 0 ? " or switch up the filters!" : "!"}
+
+
+ )}
+
+ {filteredItems.length !== 0 && (
+
+ )}
+
+ )}
+
+ >
+ );
+};
+
+interface ResultsDisplayProps {
+ courses: Array;
+ loading: boolean;
+ type: string;
+ userInput: string;
+}
+
+export default ResultsDisplay;
\ No newline at end of file
diff --git a/client/src/modules/Results/Styles/CoursePreview.module.css b/client/src/modules/Results/Styles/CoursePreview.module.css
index 5effb9b3..7bea978c 100644
--- a/client/src/modules/Results/Styles/CoursePreview.module.css
+++ b/client/src/modules/Results/Styles/CoursePreview.module.css
@@ -1,8 +1,15 @@
+.container {
+ padding: 16px;
+ border: 1px solid var(--clr-gray-300);
+ background-color: color-mix(in srgb, var(--clr-blue-100) 90%, var(--clr-gray-300));
+ border-radius: 8px;
+}
+
.columns {
display: flex;
flex-flow: column;
- gap: 24px;
+ gap: 10px;
width: 100%;
}
@@ -63,8 +70,33 @@
.noreviews {
display: flex;
+ font-size: 1.25em;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 50px;
+}
+
+.nocourse {
+ display: flex;
+ font-size: 1.75em;
justify-content: center;
align-items: center;
width: 100%;
- height: 150px;
+ height: 50px;
+ padding: 30px;
}
+
+.bearicon {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ width: 100%;
+ max-height: 300px;
+ padding-top: 40px;
+
+ @media only screen and (max-width: 843px) {
+ max-height: 200px;
+ }
+}
\ No newline at end of file
diff --git a/client/src/modules/Results/Styles/Results.module.css b/client/src/modules/Results/Styles/Results.module.css
index 52ee1a8c..3f9c0d3b 100644
--- a/client/src/modules/Results/Styles/Results.module.css
+++ b/client/src/modules/Results/Styles/Results.module.css
@@ -1,6 +1,10 @@
-.page {
+html {
background-color: var(--clr-blue-100);
- min-height: 100vh;
+}
+
+.page {
+ height: 100%;
+ max-height: 100vh;
padding-bottom: 63px;
}
@@ -9,7 +13,7 @@
width: 100%;
max-width: 1440px;
margin: 0 auto;
- padding: 36px 25px;
+ padding: 36px 25px 0 36px;
justify-content: center;
align-items: center;
@@ -18,19 +22,23 @@
flex-flow: column nowrap;
}
-.container > h1 {
+.header {
width: 100%;
- margin-bottom: 50px;
+ white-space: nowrap;
+ margin-bottom: 30px;
}
.layout {
width: 100%;
+ max-height: 100%;
+ box-sizing: border-box;
+ overflow: hidden;
display: flex;
flex-flow: row nowrap;
gap: 24px;
justify-content: space-between;
- align-items: flex-start;
+ align-items: stretch;
}
.columns {
@@ -46,11 +54,39 @@
gap: 16px;
width: 100%;
max-width: 426px;
+ height: 100%;
+ overflow: hidden;
+}
+
+.resultslist {
+ max-height: 100%;
+ height: 100%;
+ overflow-y: auto;
+ padding-right: 8px;
}
.preview {
- width: 100%;
- max-width: 620px;
+ width: 60vw;
+ max-width: 900px;
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ max-height: 100%;
+ height: 100%;
+ overflow: hidden;
+ padding-bottom: 10px;
+}
+
+.previewcard {
+ min-width: 100%;
+ min-height: 100%;
+}
+
+.filtersearch {
+ display: flex;
+ flex-flow: row nowrap;
+ max-width: 40vw;
}
.noresultimg {
@@ -83,6 +119,7 @@
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
+ align-items: center;
width: 100%;
position: relative;
@@ -120,7 +157,55 @@
overflow-y: auto;
}
+.noitems {
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ gap: 24px;
+ font-size: 1.5em;
+ text-align: center;
+}
+
+.bearicon {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ width: 100%;
+ max-height: 500px;
+ padding-top: 40px;
+
+ @media only screen and (max-width: 843px) {
+ max-height: 200px;
+ }
+}
+
@media only screen and (max-width: 843px) {
+ .page {
+ max-height: 100%;
+ }
+
+ .container {
+ padding: 36px 25px;
+ }
+
+ .header {
+ margin-bottom: 10px;
+ }
+
+ .filtersearch {
+ max-width: 100vw;
+ }
+
+ .filtersortbuttons {
+ display: flex;
+ justify-content: center;
+ flex-flow: column nowrap;
+ }
+
.resultslist {
max-height: 40vh;
overflow-y: auto;
@@ -141,7 +226,7 @@
}
.preview {
- max-width: none;
+ width: 100%;
}
}