Skip to content

Commit

Permalink
Merge branch 'master' into jacob/dark-mode
Browse files Browse the repository at this point in the history
  • Loading branch information
js0mmer committed Oct 21, 2023
2 parents f4d8c6e + 4b598ef commit 11d4d2d
Show file tree
Hide file tree
Showing 17 changed files with 295 additions and 169 deletions.
4 changes: 4 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@
(optional)
- [ ] Write tests
- [ ] Write documentation

## Issues
<!-- Link the issue you're closing -->
Closes #
22 changes: 18 additions & 4 deletions site/src/component/GradeDist/Chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export default class Chart extends React.Component<ChartProps> {
* 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;

Expand Down Expand Up @@ -147,18 +147,32 @@ export default class Chart extends React.Component<ChartProps> {
* @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 <>
<ThemeContext.Consumer>
{({ darkMode }) =>
<ResponsiveBar
data={this.getClassData()}
data={data}
keys={['A', 'B', 'C', 'D', 'F', 'P', 'NP']}
indexBy='label'
margin={{
top: 50,
right: 30,
right: marginX,
bottom: 50,
left: 30
left: marginX,
}}
layout='vertical'
axisBottom={{
Expand Down
19 changes: 11 additions & 8 deletions site/src/component/GradeDist/GradeDist.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
width: 100%;
display: flex;
flex-direction: column;
margin-left: 1vw;

.gradedist-filter {
margin: 1vh;
Expand All @@ -16,11 +15,14 @@

#menu {
display: flex;
margin: 0;
}

#chart {
display: flex;
justify-content: space-evenly;
margin-left: 0;
margin-right: 0;
}

.grade_distribution_chart-container {
Expand All @@ -45,17 +47,20 @@

#menu {
padding-top: 2vh;
justify-content: flex-start;
}

.chart {
width: 80%;
}

.pie {
width: 80%;
}
}

@media only screen and (max-width: 600px) {
#chart {
flex-direction: column;
flex-direction: column;
}

.chart {
Expand Down Expand Up @@ -83,15 +88,13 @@
}
}

@media only screen and (min-device-width : 1320px)
and (max-device-width : 1440px) {
@media only screen and (min-device-width: 1320px) and (max-device-width: 1440px) {
.pie-text {
font-size: 1.2em;
}
}
@media only screen and (min-device-width : 1441px)
and (max-device-width : 1600px) {
@media only screen and (min-device-width: 1441px) and (max-device-width: 1600px) {
.pie-text {
font-size: 1.3em;
}
}
}
4 changes: 2 additions & 2 deletions site/src/component/GradeDist/GradeDist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,8 @@ const GradeDist: FC<GradeDistProps> = (props) => {
</Grid.Row>
</div>
);
} 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 (
<>
Expand Down
21 changes: 13 additions & 8 deletions site/src/component/GradeDist/Pie.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export default class Pie extends React.Component<PieProps> {

render() {
return (
<div style={{ width: '100%' }}>
<div style={{ width: '100%', position: 'relative' }}>
<ResponsivePie<Slice>
data={this.getClassData()}
margin={{
Expand Down Expand Up @@ -173,13 +173,18 @@ export default class Pie extends React.Component<PieProps> {
</div>
)}
/>
<div style={{ display: 'flex', textAlign: 'center', margin: '-235px' }}>
<div style={{ margin: 'auto' }}>
{this.totalPNP == this.total ? <h3 className='pie-text'>Average Grade: {this.averagePNP}</h3> : null}
{this.totalPNP != this.total ? <h3 className='pie-text'>Average Grade: {this.averageGrade} ({this.averageGPA})</h3> : null}
<h3 className='pie-text' style={{ marginBottom: '6px' }}>Total Enrolled: <strong>{this.total}</strong></h3>
{this.totalPNP > 0 ? <small>{this.totalPNP} enrolled as P/NP</small> : null}
</div>
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
textAlign: 'center',
width: '100%'
}}>
{this.totalPNP == this.total ? <h3 className='pie-text'>Average Grade: {this.averagePNP}</h3> : null}
{this.totalPNP != this.total ? <h3 className='pie-text'>Average Grade: {this.averageGrade} ({this.averageGPA})</h3> : null}
<h3 className='pie-text' style={{ marginBottom: '6px' }}>Total Enrolled: <strong>{this.total}</strong></h3>
{this.totalPNP > 0 ? <small>{this.totalPNP} enrolled as P/NP</small> : null}
</div>
</div>
)
Expand Down
25 changes: 14 additions & 11 deletions site/src/component/PrereqTree/PrereqTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,17 +154,20 @@ const PrereqTree: FC<PrereqProps> = (props) => {
</div>} */}

</div>
<div className='prereq-text-box'
style={{
padding: '1em',
marginTop: '2em',
}}
>
<p>
{props.prerequisite_text !== '' && <b>Prerequisite: </b>}
{props.prerequisite_text}
</p>
</div>
{props.prerequisite_text !== '' && (
<div
className='prereq-text-box'
style={{
padding: '1em',
marginTop: '2em',
}}
>
<p>
<b>Prerequisite: </b>
{props.prerequisite_text}
</p>
</div>
)}
</Grid.Row>
</div>
);
Expand Down
22 changes: 22 additions & 0 deletions site/src/component/Review/Review.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
53 changes: 45 additions & 8 deletions site/src/component/Review/Review.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReviewProps> = (props) => {
const dispatch = useAppDispatch();
const reviewData = useAppSelector(selectReviews);
const [voteColors, setVoteColors] = useState([]);
const openForm = useAppSelector(state => state.review.formOpen);
const [sortingOption, setSortingOption] = useState<SortingOption>(SortingOption.MOST_RECENT);
const [showOnlyVerifiedReviews, setShowOnlyVerifiedReviews] = useState(false);

const getColors = async (vote: VoteColorsRequest) => {
const res = await axios.patch('/api/reviews/getVoteColors', vote);
Expand All @@ -37,11 +46,6 @@ const Review: FC<ReviewProps> = (props) => {
})
.then(async (res: AxiosResponse<ReviewData[]>) => {
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<data.length;i++){
reviewIDs.push(data[i]._id);
Expand Down Expand Up @@ -89,6 +93,26 @@ const Review: FC<ReviewProps> = (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';
Expand All @@ -104,9 +128,22 @@ const Review: FC<ReviewProps> = (props) => {
return (
<>
<div className='reviews'>
{reviewData.map((review, i) => {
if (review !== null) return (<SubReview review={review} key={i} course={props.course} professor={props.professor} colors={getU(review._id) as VoteColor} colorUpdater={updateVoteColors}/>)
})}
<div className='sorting-menu row'>
<Dropdown
placeholder='Sorting Option'
scrolling
selection
options={[{ text: 'Most Recent', value: SortingOption.MOST_RECENT },
{ text: 'Top Reviews', value: SortingOption.TOP_REVIEWS },
{ text: 'Controversial', value: SortingOption.CONTROVERSIAL }]}
value={sortingOption}
onChange={(e, s) => setSortingOption(s.value as SortingOption)}
/>
<div id="checkbox">
<Checkbox label="Show verified reviews only" checked={showOnlyVerifiedReviews} onChange={(e, props) => setShowOnlyVerifiedReviews(props.checked!)} />
</div>
</div>
{sortedReviews.map(review => <SubReview review={review} key={review._id} course={props.course} professor={props.professor} colors={getU(review._id) as VoteColor} colorUpdater={updateVoteColors}/>)}
<button type='button' className='add-review-btn' onClick={openReviewForm}>+ Add Review</button>
</div>
<ReviewForm closeForm={closeForm} {...props} />
Expand Down
4 changes: 2 additions & 2 deletions site/src/component/Review/SubReview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ const SubReview: FC<SubReviewProps> = ({ review, course, professor, colors, colo
</div>
</div>
<div>
{review.tags?.map((tag, i) =>
<Badge pill className='p-3 mr-2 mt-2' variant='info' key={`review-tag-${review._id}-${i}`}>
{review.tags?.map(tag =>
<Badge pill className='p-3 mr-2 mt-2' variant='info' key={tag}>
{tag}
</Badge>
)}
Expand Down
2 changes: 1 addition & 1 deletion site/src/component/ReviewForm/ReviewForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ const ReviewForm: FC<ReviewFormProps> = (props) => {
<Form.Label>Select up to 3 tags</Form.Label>
<div>
{tags.map((tag, i) =>
<Badge pill className='p-3 mr-2 mt-2' variant={selectedTags.includes(tag) ? 'success' : 'info'} id={`tag-${i}`}
<Badge key={tag} pill className='p-3 mr-2 mt-2' variant={selectedTags.includes(tag) ? 'success' : 'info'} id={`tag-${i}`}
onClick={(e: React.MouseEvent<HTMLInputElement>) => { selectTag(tag) }}>
{tag}
</Badge>
Expand Down
21 changes: 10 additions & 11 deletions site/src/component/SearchModule/SearchModule.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -28,7 +27,7 @@ const SearchModule: FC<SearchModuleProps> = ({ index }) => {
// Search empty string to load some results
useEffect(() => {
searchNames('');
}, [])
}, [index])

// Refresh search results when names and page number changes
useEffect(() => {
Expand All @@ -54,10 +53,10 @@ const SearchModule: FC<SearchModuleProps> = ({ 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)
Expand Down Expand Up @@ -88,7 +87,7 @@ const SearchModule: FC<SearchModuleProps> = ({ 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 <div className='search-module'>
<Form.Group className="mb-3">
Expand Down
Loading

0 comments on commit 11d4d2d

Please sign in to comment.