Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a filter by course or professor to reviews #429

Merged
merged 14 commits into from
Feb 22, 2024
78 changes: 78 additions & 0 deletions site/src/component/Review/Review.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const Review: FC<ReviewProps> = (props) => {
const reviewData = useAppSelector(selectReviews);
const [voteColors, setVoteColors] = useState([]);
const [sortingOption, setSortingOption] = useState<SortingOption>(SortingOption.MOST_RECENT);
const [filterOption, setFilterOption] = useState('');
const [showOnlyVerifiedReviews, setShowOnlyVerifiedReviews] = useState(false);

const getColors = async (vote: VoteColorsRequest) => {
Expand Down Expand Up @@ -102,6 +103,16 @@ const Review: FC<ReviewProps> = (props) => {
sortedReviews = reviewData.slice(0);
}

if (filterOption.length > 0) {
if (props.course) {
// filter course reviews by specific professor
sortedReviews = sortedReviews.filter((review) => review.professorID === filterOption);
} else if (props.professor) {
// filter professor reviews by specific course
sortedReviews = sortedReviews.filter((review) => review.courseID === filterOption);
}
}

switch (sortingOption) {
case SortingOption.MOST_RECENT:
sortedReviews.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
Expand All @@ -118,6 +129,20 @@ const Review: FC<ReviewProps> = (props) => {
break;
}

// calculate frequencies of professors or courses in list of reviews
let reviewFreq = new Map();
ptruong0 marked this conversation as resolved.
Show resolved Hide resolved
if (props.course) {
reviewFreq = sortedReviews.reduce(
(acc, review) => acc.set(review.professorID, (acc.get(review.professorID) || 0) + 1),
reviewFreq,
);
} else if (props.professor) {
reviewFreq = sortedReviews.reduce(
(acc, review) => acc.set(review.courseID, (acc.get(review.courseID) || 0) + 1),
reviewFreq,
);
}

const openReviewForm = () => {
dispatch(setFormStatus(true));
document.body.style.overflow = 'hidden';
Expand Down Expand Up @@ -146,6 +171,59 @@ const Review: FC<ReviewProps> = (props) => {
value={sortingOption}
onChange={(_, s) => setSortingOption(s.value as SortingOption)}
/>
{props.course && (
<Dropdown
placeholder="Professor"
scrolling
selection
options={
// include option for filter to be empty
[{ text: 'All Professors', value: '' }].concat(
// map course's instructors to dropdown options
Object.keys(props.course?.instructors)
.map((profID) => {
const name =
`${props.course?.instructors[profID].name} (${reviewFreq.get(profID) || 0})` as string;
ptruong0 marked this conversation as resolved.
Show resolved Hide resolved
return {
text: name,
value: profID,
};
})
.sort((a, b) => a.text.localeCompare(b.text)),
)
}
value={filterOption}
onChange={(_, s) => setFilterOption(s.value as string)}
/>
)}
{props.professor && (
<Dropdown
placeholder="Course"
scrolling
selection
options={
// include option for filter to be empty
[{ text: 'All Courses', value: '' }].concat(
// map professor's courses to dropdown options
Object.keys(props.professor?.courses)
.map((courseID) => {
const name =
props.professor?.courses[courseID].department +
' ' +
props.professor?.courses[courseID].courseNumber +
` (${reviewFreq.get(courseID) || 0})`;
return {
text: name,
value: courseID,
};
})
.sort((a, b) => a.text.localeCompare(b.text)),
)
}
value={filterOption}
onChange={(_, s) => setFilterOption(s.value as string)}
/>
)}
<div id="checkbox">
<Checkbox
label="Show verified reviews only"
Expand Down
2 changes: 1 addition & 1 deletion site/src/pages/CoursePage/CoursePage.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
}

.course-page-section {
max-width: 42vw;
max-width: 45vw;
background-color: var(--overlay1);
border-radius: var(--border-radius);
padding: 3rem;
Expand Down
22 changes: 20 additions & 2 deletions site/src/pages/RoadmapPage/Planner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
setYearPlans,
setInvalidCourses,
setTransfers,
setUnsavedChanges,
addYear,
} from '../../store/slices/roadmapSlice';
import { useFirstRender } from '../../hooks/firstRenderer';
Expand Down Expand Up @@ -39,13 +40,27 @@ const Planner: FC = () => {
const [missingPrerequisites, setMissingPrerequisites] = useState(new Set<string>());

useEffect(() => {
// if is first render, load from local storage
if (isFirstRenderer) {
// stringify current roadmap
const roadmapStr = JSON.stringify({
planner: collapsePlanner(data),
transfers: transfers,
});

// stringified value of an empty roadmap
const emptyRoadmap =
'{"planner":[{"startYear":2024,"name":"Year 1","quarters":[{"name":"fall","courses":[]},{"name":"winter","courses":[]},{"name":"spring","courses":[]}]}],"transfers":[]}';

// if first render and current roadmap is empty, load from local storage
if (isFirstRenderer && roadmapStr === emptyRoadmap) {
loadRoadmap();
}
// validate planner every time something changes
else {
validatePlanner();

// check current roadmap against last-saved roadmap in local storage
// if they are different, mark changes as unsaved to enable alert on page leave
dispatch(setUnsavedChanges(localStorage.getItem('roadmap') !== roadmapStr));
}
}, [data, transfers]);

Expand Down Expand Up @@ -142,6 +157,9 @@ const Planner: FC = () => {
// save to local storage as well
localStorage.setItem('roadmap', JSON.stringify(roadmap));

// mark changes as saved to bypass alert on page leave
dispatch(setUnsavedChanges(false));

if (savedAccount) {
alert(`Roadmap saved under ${cookies.user.email}`);
} else {
Expand Down
18 changes: 18 additions & 0 deletions site/src/store/slices/roadmapSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ interface RoadmapState {
showSearch: boolean;
// Whether or not to show the add course modal on mobile
showAddCourse: boolean;
// Whether or not to alert the user of unsaved changes before leaving
unsavedChanges: boolean;
}

// Define the initial state using that type
Expand All @@ -39,6 +41,7 @@ const initialState: RoadmapState = {
transfers: [],
showSearch: false,
showAddCourse: false,
unsavedChanges: false,
};

// Payload to pass in to move a course
Expand Down Expand Up @@ -76,6 +79,9 @@ interface SetTransferPayload {
transfer: TransferData;
}

// onbeforeunload event listener
const alertUnsaved = (event: BeforeUnloadEvent) => event.preventDefault();

export const roadmapSlice = createSlice({
name: 'roadmap',
// `createSlice` will infer the state type from the `initialState` argument
Expand Down Expand Up @@ -262,6 +268,17 @@ export const roadmapSlice = createSlice({
setShowAddCourse: (state, action: PayloadAction<boolean>) => {
state.showAddCourse = action.payload;
},
setUnsavedChanges: (state, action: PayloadAction<boolean>) => {
state.unsavedChanges = action.payload;

// when there are unsaved changes, add event listener for alert on page leave
if (state.unsavedChanges) {
window.addEventListener('beforeunload', alertUnsaved);
} else {
// remove listener after saving changes
window.removeEventListener('beforeunload', alertUnsaved);
}
},
},
});

Expand All @@ -287,6 +304,7 @@ export const {
deleteTransfer,
setShowSearch,
setShowAddCourse,
setUnsavedChanges,
} = roadmapSlice.actions;

// Other code such as selectors can use the imported `RootState` type
Expand Down