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.classTitle} - -
-
- {theClass.classSub.toUpperCase() + - ' ' + - theClass.classNum + - ', ' + - offered} -
-
- - - - {this.state.numReviews !== 0 && ( -
Top Review
- )} - -
- {/*If class has review show top review and link*/} - {this.state.numReviews !== 0 && ( - - )} - - {this.state.numReviews !== 0 && this.state.numReviews > 1 && ( - - See {this.state.numReviews} more review - {this.state.numReviews > 1 ? 's' : ''} - - )} - - {/*If class has 0 reviews text and button*/} - {this.state.numReviews === 0 && ( -
No reviews yet
- )} - {(this.state.numReviews === 0 || this.state.numReviews === 1) && ( - - Leave a 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.classTitle} + +
+
+ {course.classSub.toUpperCase() + + ' ' + + course.classNum + + ', ' + + offered} +
+
+ + + + {numReviews !== 0 &&
Top Review
} + +
+ {numReviews !== 0 && ( + null} + isPreview={true} + isProfile={false} + /> + )} + + {numReviews !== 0 && numReviews > 1 && ( + + See {numReviews} more review{numReviews > 1 ? 's' : ''} + + )} + + {numReviews === 0 && ( + <> + Bear Icon +
+ No reviews yet! Why not be the first? +
+ + )} + {(numReviews === 0 || numReviews === 1) && ( + + View course page + + )} +
+
+ ); +}; + +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) => ( -
- -
- )); - } - - 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 && ( -
- No class found -
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} - " -
- -
-
- - -
- - -
- {this.state.showFilterPopup && ( - - )} - -
-
-
-
    {this.renderResults()}
-
-
-
- -
-
-
-
- )} -
- ); - } -} - -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) => ( +
+ +
+ )); + }; + + 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} + " +
+
+
+
+ + +
+ + +
+ {showFilterPopup && ( + + setShowFilterPopup(!showFilterPopup) + } + /> + )} +
+
+
+
+
    {renderResults()}
+
+
+
+ + )} +
+ )} + {filteredItems.length === 0 && courseList.length !== 0 && ( +
+ {searchListViewEnabled && ( + <> +
+
+ +
+ {showFilterPopup && ( + + setShowFilterPopup(!showFilterPopup) + } + /> + )} +
+ + )} +
+ )} +
+
+ + {filteredItems.length === 0 && ( +
+ Bear Icon +
+ 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%; } }