diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index be35d70b..99970180 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -30,3 +30,7 @@ (optional) - [ ] Write tests - [ ] Write documentation + +## Issues + +Closes # \ No newline at end of file diff --git a/site/src/component/GradeDist/Chart.tsx b/site/src/component/GradeDist/Chart.tsx index 66252203..49045a74 100644 --- a/site/src/component/GradeDist/Chart.tsx +++ b/site/src/component/GradeDist/Chart.tsx @@ -62,7 +62,7 @@ export default class Chart extends React.Component { * Create an array of objects to feed into the chart. * @return an array of JSON objects detailing the grades for each class */ - getClassData = () => { + getClassData = (): Bar[] => { let gradeACount = 0, gradeBCount = 0, gradeCCount = 0, gradeDCount = 0, gradeFCount = 0, gradePCount = 0, gradeNPCount = 0; @@ -147,18 +147,32 @@ export default class Chart extends React.Component { * @return a JSX block rendering the chart */ render() { + const data = this.getClassData() + + // greatestCount calculates the upper bound of the graph (i.e. the greatest number of students in a single grade) + const greatestCount = data.reduce((max, grade) => ( + grade[grade.id] as number > max + ? grade[grade.id] as number + : max + ), 0); + + // The base marginX is 30, with increments of 5 added on for every order of magnitude greater than 100 to accomadate for larger axis labels (1,000, 10,000, etc) + // For example, if greatestCount is 5173 it is (when rounding down (i.e. floor)), one magnitude (calculated with log_10) greater than 100, therefore we add one increment of 5px to our base marginX of 30px + // Math.max() ensures that we're not finding the log of a non-positive number + const marginX = 30 + (5 * Math.floor(Math.log10(Math.max(100, greatestCount) / 100))) + return <> {({ darkMode }) => = (props) => { ); - } else if (gradeDistData == null) { // null if still fetching, don't display anything while it still loads - return null; + } else if (gradeDistData == null) { // null if still fetching, display loading message + return <>Loading Distribution..; } else { // gradeDistData is empty, did not receive any data from API call or received an error, display an error message return ( <> diff --git a/site/src/component/GradeDist/Pie.tsx b/site/src/component/GradeDist/Pie.tsx index 3f60da4d..edf7f46d 100644 --- a/site/src/component/GradeDist/Pie.tsx +++ b/site/src/component/GradeDist/Pie.tsx @@ -143,7 +143,7 @@ export default class Pie extends React.Component { render() { return ( -
+
data={this.getClassData()} margin={{ @@ -173,13 +173,18 @@ export default class Pie extends React.Component {
)} /> -
-
- {this.totalPNP == this.total ?

Average Grade: {this.averagePNP}

: null} - {this.totalPNP != this.total ?

Average Grade: {this.averageGrade} ({this.averageGPA})

: null} -

Total Enrolled: {this.total}

- {this.totalPNP > 0 ? {this.totalPNP} enrolled as P/NP : null} -
+
+ {this.totalPNP == this.total ?

Average Grade: {this.averagePNP}

: null} + {this.totalPNP != this.total ?

Average Grade: {this.averageGrade} ({this.averageGPA})

: null} +

Total Enrolled: {this.total}

+ {this.totalPNP > 0 ? {this.totalPNP} enrolled as P/NP : null}
) diff --git a/site/src/component/PrereqTree/PrereqTree.tsx b/site/src/component/PrereqTree/PrereqTree.tsx index 423873cb..807850f5 100644 --- a/site/src/component/PrereqTree/PrereqTree.tsx +++ b/site/src/component/PrereqTree/PrereqTree.tsx @@ -154,17 +154,20 @@ const PrereqTree: FC = (props) => {
} */} -
-

- {props.prerequisite_text !== '' && Prerequisite: } - {props.prerequisite_text} -

-
+ {props.prerequisite_text !== '' && ( +
+

+ Prerequisite: + {props.prerequisite_text} +

+
+ )} ); diff --git a/site/src/component/Review/Review.scss b/site/src/component/Review/Review.scss index 919ac463..a7205352 100644 --- a/site/src/component/Review/Review.scss +++ b/site/src/component/Review/Review.scss @@ -181,4 +181,26 @@ $avatarWidth: 65px; width: 8vh; height: 8vh; } +} + +.sorting-menu { + margin-left: 0; + margin-right: 0; + + > * { + margin-bottom: 15px; + } + + .dropdown { + margin-right: 1vh; + } + + #checkbox { + display: flex; + align-items: center; + + label { + margin-bottom: 0.25rem; + } + } } \ No newline at end of file diff --git a/site/src/component/Review/Review.tsx b/site/src/component/Review/Review.tsx index 1c39e5e0..b18be1a8 100644 --- a/site/src/component/Review/Review.tsx +++ b/site/src/component/Review/Review.tsx @@ -7,17 +7,26 @@ import './Review.scss'; import { selectReviews, setReviews, setFormStatus } from '../../store/slices/reviewSlice'; import { useAppSelector, useAppDispatch } from '../../store/hooks'; import { CourseGQLData, ProfessorGQLData, ReviewData, VoteColorsRequest, VoteColor } from '../../types/types'; +import { Checkbox, Dropdown } from 'semantic-ui-react'; export interface ReviewProps { course?: CourseGQLData; professor?: ProfessorGQLData; } +enum SortingOption { + MOST_RECENT, + TOP_REVIEWS, + CONTROVERSIAL +} + const Review: FC = (props) => { const dispatch = useAppDispatch(); const reviewData = useAppSelector(selectReviews); const [voteColors, setVoteColors] = useState([]); const openForm = useAppSelector(state => state.review.formOpen); + const [sortingOption, setSortingOption] = useState(SortingOption.MOST_RECENT); + const [showOnlyVerifiedReviews, setShowOnlyVerifiedReviews] = useState(false); const getColors = async (vote: VoteColorsRequest) => { const res = await axios.patch('/api/reviews/getVoteColors', vote); @@ -37,11 +46,6 @@ const Review: FC = (props) => { }) .then(async (res: AxiosResponse) => { const data = res.data.filter((review) => review !== null); - data.sort((a, b) => { - let aScore = a.score + (a.verified ? 10000 : 0); - let bScore = b.score + (b.verified ? 10000 : 0); - return bScore - aScore; - }) let reviewIDs = []; for(let i = 0;i = (props) => { getReviews(); }, [props.course?.id, props.professor?.ucinetid]); + let sortedReviews: ReviewData[]; + // filter verified if option is set + if (showOnlyVerifiedReviews) { + sortedReviews = reviewData.filter(review => review.verified); + } else { // if not, clone reviewData since its const + sortedReviews = reviewData.slice(0); + } + + switch (sortingOption) { + case SortingOption.MOST_RECENT: + sortedReviews.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); + break; + case SortingOption.TOP_REVIEWS: // the right side of || will fall back to most recent when score is equal + sortedReviews.sort((a, b) => b.score - a.score || new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); + break; + case SortingOption.CONTROVERSIAL: + sortedReviews.sort((a, b) => a.score - b.score || new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); + break; + } + const openReviewForm = () => { dispatch(setFormStatus(true)); document.body.style.overflow = 'hidden'; @@ -104,9 +128,22 @@ const Review: FC = (props) => { return ( <>
- {reviewData.map((review, i) => { - if (review !== null) return () - })} +
+ setSortingOption(s.value as SortingOption)} + /> +
+ setShowOnlyVerifiedReviews(props.checked!)} /> +
+
+ {sortedReviews.map(review => )}
diff --git a/site/src/component/Review/SubReview.tsx b/site/src/component/Review/SubReview.tsx index 82e1225c..9c06460b 100644 --- a/site/src/component/Review/SubReview.tsx +++ b/site/src/component/Review/SubReview.tsx @@ -131,8 +131,8 @@ const SubReview: FC = ({ review, course, professor, colors, colo
- {review.tags?.map((tag, i) => - + {review.tags?.map(tag => + {tag} )} diff --git a/site/src/component/ReviewForm/ReviewForm.tsx b/site/src/component/ReviewForm/ReviewForm.tsx index 5d14efbe..e202fde7 100644 --- a/site/src/component/ReviewForm/ReviewForm.tsx +++ b/site/src/component/ReviewForm/ReviewForm.tsx @@ -328,7 +328,7 @@ const ReviewForm: FC = (props) => { Select up to 3 tags
{tags.map((tag, i) => - ) => { selectTag(tag) }}> {tag} diff --git a/site/src/component/SearchModule/SearchModule.tsx b/site/src/component/SearchModule/SearchModule.tsx index 2fb7e3f0..08ea2837 100644 --- a/site/src/component/SearchModule/SearchModule.tsx +++ b/site/src/component/SearchModule/SearchModule.tsx @@ -1,16 +1,15 @@ -import React, { useState, useEffect, Component, FC } from 'react'; -import './SearchModule.scss'; -import wfs from 'websoc-fuzzy-search'; -import axios from 'axios'; +import { FC, useEffect } from 'react'; +import { Search } from 'react-bootstrap-icons'; import Form from 'react-bootstrap/Form'; import InputGroup from 'react-bootstrap/InputGroup'; -import { Search } from 'react-bootstrap-icons'; +import wfs from 'websoc-fuzzy-search'; +import './SearchModule.scss'; +import { searchAPIResults } from '../../helpers/util'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; import { setNames, setResults } from '../../store/slices/searchSlice'; -import { searchAPIResults } from '../../helpers/util'; -import { SearchIndex, BatchCourseData, CourseGQLResponse, ProfessorGQLResponse, BatchProfessorData } from '../../types/types'; +import { SearchIndex } from '../../types/types'; const PAGE_SIZE = 10; const SEARCH_TIMEOUT_MS = 500; @@ -28,7 +27,7 @@ const SearchModule: FC = ({ index }) => { // Search empty string to load some results useEffect(() => { searchNames(''); - }, []) + }, [index]) // Refresh search results when names and page number changes useEffect(() => { @@ -54,10 +53,10 @@ const SearchModule: FC = ({ index }) => { } }) let names: string[] = []; - if (index == 'courses') { + if (index === 'courses') { names = Object.keys(nameResults); } - else if (index == 'professors') { + else if (index === 'professors') { names = Object.keys(nameResults).map(n => nameResults[n].metadata.ucinetid) as string[]; } console.log('From frontend search', names) @@ -88,7 +87,7 @@ const SearchModule: FC = ({ index }) => { let coursePlaceholder = 'Search a course number or department'; let professorPlaceholder = 'Search a professor'; - let placeholder = index == 'courses' ? coursePlaceholder : professorPlaceholder; + let placeholder = index === 'courses' ? coursePlaceholder : professorPlaceholder; return
diff --git a/site/src/component/SearchPopup/SearchPopup.tsx b/site/src/component/SearchPopup/SearchPopup.tsx index dc609b9c..7d4a013b 100644 --- a/site/src/component/SearchPopup/SearchPopup.tsx +++ b/site/src/component/SearchPopup/SearchPopup.tsx @@ -1,128 +1,157 @@ -import React, { FC } from 'react'; -import './SearchPopup.scss' -import GradeDist from '../GradeDist/GradeDist'; -import Button from 'react-bootstrap/Button'; +import React, { FC } from "react"; +import "./SearchPopup.scss"; +import GradeDist from "../GradeDist/GradeDist"; +import Button from "react-bootstrap/Button"; import Carousel from "react-multi-carousel"; import searching from '../../asset/searching.png'; -import { useAppSelector } from '../../store/hooks'; -import { selectCourse, selectProfessor } from '../../store/slices/popupSlice'; -import { CourseGQLData, ProfessorGQLData, SearchType, ScoreData } from '../../types/types'; +import { useAppSelector } from "../../store/hooks"; +import { selectCourse, selectProfessor } from "../../store/slices/popupSlice"; +import { + CourseGQLData, + ProfessorGQLData, + SearchType, + ScoreData, +} from "../../types/types"; interface InfoData { - title: string; - content: string | React.FunctionComponent; + title: string; + content: string | React.FunctionComponent; } interface SearchPopupProps { - searchType: SearchType; - name: string; - id: string; - title: string; - infos: InfoData[]; - scores: ScoreData[]; - course?: CourseGQLData; - professor?: ProfessorGQLData; + searchType: SearchType; + name: string; + id: string; + title: string; + infos: InfoData[]; + scores: ScoreData[]; + course?: CourseGQLData; + professor?: ProfessorGQLData; } const SearchPopup: FC = (props) => { - const course = useAppSelector(selectCourse); - const professor = useAppSelector(selectProfessor); + const course = useAppSelector(selectCourse); + const professor = useAppSelector(selectProfessor); - let selected = false; - if (props.searchType == 'course') { - selected = course != null; - } - else if (props.searchType == 'professor') { - selected = professor != null; - } + let selected = false; + if (props.searchType == "course") { + selected = course != null; + } else if (props.searchType == "professor") { + selected = professor != null; + } - if (!selected) { - return
-
- searching -

- Click on a {props.searchType} card to view more information! -

-
+ if (!selected) { + return ( +
+
+ searching +

Click on a {props.searchType} card to view more information!

- } - else { - return - } -} +
+ ); + } else { + return ; + } +}; const responsive = { - desktop: { - breakpoint: { max: 3000, min: 1024 }, - items: 3, - paritialVisibilityGutter: 60 - }, - tablet: { - breakpoint: { max: 1024, min: 464 }, - items: 2, - paritialVisibilityGutter: 50 - }, - mobile: { - breakpoint: { max: 464, min: 0 }, - items: 1, - paritialVisibilityGutter: 30 - } + desktop: { + breakpoint: { max: 3000, min: 1024 }, + items: 3, + paritialVisibilityGutter: 60, + }, + tablet: { + breakpoint: { max: 1024, min: 464 }, + items: 2, + paritialVisibilityGutter: 50, + }, + mobile: { + breakpoint: { max: 464, min: 0 }, + items: 1, + paritialVisibilityGutter: 30, + }, }; const SearchPopupContent: FC = (props) => { - return
-
-
-

- {props.name} - - - -

-
{props.title}
-
-
-
- { - props.infos.map((info, i) =>
-

- {info.title} -

-

- {info.content || `No ${info.title}`} -

-
) - } -
+ return ( +
+
+
+

+ {props.name} + + + +

+
{props.title}
+
+
+
+ {props.infos.map((info, i) => ( +
+

{info.title}

+

{info.content || `No ${info.title}`}

+
+ ))} +
-

- Grade Distribution -

-
- -
+

Grade Distribution

+
+ +
-

- {props.searchType == 'course' ? 'Current Instructors' : 'Previously Taught'} -

-
- - {props.scores.map((score, i) =>
-
- {score.score == -1 ? '?' : score.score} - / 5.0 -
- {score.name} -
- )} -
-
-
+

+ {props.searchType == "course" + ? "Current Instructors" + : "Previously Taught"} +

+
+ {props.scores.length > 0 ? ( + + {props.scores.map((score, i) => ( +
+
+ + {score.score == -1 ? "?" : score.score} + + + / 5.0 + +
+ + {score.name} + +
+ ))} +
+ ) : ( + "No Instructors Found" + )} +
+
-} + ); +}; -export default SearchPopup; \ No newline at end of file +export default SearchPopup; diff --git a/site/src/pages/RoadmapPage/Course.tsx b/site/src/pages/RoadmapPage/Course.tsx index b71f232d..6168536d 100644 --- a/site/src/pages/RoadmapPage/Course.tsx +++ b/site/src/pages/RoadmapPage/Course.tsx @@ -36,7 +36,9 @@ const Course: FC = (props) => { const WarningPopover = - Prerequisite not met! Missing: {requiredCourses?.join(', ')} + Prerequisite(s) not met! Missing: {requiredCourses?.join(', ')} +
+ Already completed prerequisite(s) at another institution? Click 'Transfer Credits' at the top of the planner to clear the prerequisite(s).
diff --git a/site/src/pages/RoadmapPage/Header.tsx b/site/src/pages/RoadmapPage/Header.tsx index d7e33437..486a750b 100644 --- a/site/src/pages/RoadmapPage/Header.tsx +++ b/site/src/pages/RoadmapPage/Header.tsx @@ -21,7 +21,14 @@ const Header: FC = ({ courseCount, unitCount, saveRoadmap, missingP const [showMenu, setShowMenu] = useState(false); const buttons = <> - diff --git a/site/src/pages/RoadmapPage/index.tsx b/site/src/pages/RoadmapPage/index.tsx index 263fa276..7a205951 100644 --- a/site/src/pages/RoadmapPage/index.tsx +++ b/site/src/pages/RoadmapPage/index.tsx @@ -14,8 +14,11 @@ const RoadmapPage: FC = () => { const onDragEnd = useCallback((result: DropResult) => { if (result.reason === 'DROP') { - // no destination or dragging to search bar - if (!result.destination || result.destination.droppableId === 'search') { + // no destination + if(!result.destination) { return } + + // dragging to search bar + if (result.destination.droppableId === 'search') { // removing from quarter if (result.source.droppableId != 'search') { let [yearIndex, quarterIndex] = result.source.droppableId.split('-'); diff --git a/site/src/pages/SearchPage/CourseHitItem.tsx b/site/src/pages/SearchPage/CourseHitItem.tsx index 58d7f5c8..2140cd51 100644 --- a/site/src/pages/SearchPage/CourseHitItem.tsx +++ b/site/src/pages/SearchPage/CourseHitItem.tsx @@ -37,11 +37,7 @@ const CourseHitItem: FC = (props) => {

- {props.department} -   - {props.number} -   - {props.title} + {props.department} {props.number} {props.title}

diff --git a/site/src/store/slices/roadmapSlice.ts b/site/src/store/slices/roadmapSlice.ts index c1fefa50..d6014fbc 100644 --- a/site/src/store/slices/roadmapSlice.ts +++ b/site/src/store/slices/roadmapSlice.ts @@ -180,7 +180,9 @@ export const roadmapSlice = createSlice({ } }, clearPlanner: (state) => { - state.yearPlans = []; + if(window.confirm("Are you sure you want to clear your Roadmap?")) { + state.yearPlans = []; + } }, setActiveCourse: (state, action: PayloadAction) => { state.activeCourse = action.payload;