Skip to content

Commit

Permalink
feat: Sorting
Browse files Browse the repository at this point in the history
  • Loading branch information
mathhulk committed Apr 28, 2024
1 parent 3f63ced commit 8339052
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 42 deletions.
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-tooltip": "^1.0.7",
Expand Down
35 changes: 34 additions & 1 deletion frontend/src/components/Browser/Filters/Filters.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
color: var(--heading-color);
}

&:hover .checkbox:not([data-state="checked"]) {
&:hover .checkbox:not([data-state="checked"]), &:hover .radio:not([data-state="checked"]) {
border-color: var(--heading-color);
}

Expand All @@ -88,6 +88,39 @@
}
}

.radio {
width: 16px;
height: 16px;
border-radius: 50%;
border: 2px solid var(--paragraph-color);
position: relative;

&::after {
content: "";
width: 10px;
height: 10px;
border-radius: 50%;
background-color: var(--blue-500);
opacity: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

&[data-state="checked"] {
border-color: var(--blue-500);

&::after {
opacity: 1;
}

& + .text .value {
color: var(--heading-color);
}
}
}

.checkbox {
width: 16px;
height: 16px;
Expand Down
66 changes: 32 additions & 34 deletions frontend/src/components/Browser/Filters/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useMemo, useState } from "react";

import * as Checkbox from "@radix-ui/react-checkbox";
import * as RadioGroup from "@radix-ui/react-radio-group";
import classNames from "classnames";
import { Check, NavArrowDown, NavArrowUp } from "iconoir-react";
import { useSearchParams } from "react-router-dom";
Expand All @@ -9,7 +10,7 @@ import { ICatalogCourse, Semester } from "@/lib/api";
import { kindAbbreviations } from "@/lib/section";

import Header from "../Header";
import { getFilteredCourses, getLevel } from "../browser";
import { SortBy, getFilteredCourses, getLevel } from "../browser";
import styles from "./Filters.module.scss";

interface FiltersProps {
Expand All @@ -25,6 +26,7 @@ interface FiltersProps {
currentSemester: Semester;
currentYear: number;
currentQuery: string;
currentSortBy?: SortBy;
}

export default function Filters({
Expand All @@ -40,6 +42,7 @@ export default function Filters({
currentSemester,
currentYear,
currentQuery,
currentSortBy,
}: FiltersProps) {
const [expanded, setExpanded] = useState(false);
const [searchParams, setSearchParams] = useSearchParams();
Expand Down Expand Up @@ -124,8 +127,8 @@ export default function Filters({
for (const course of courses) {
const { unitsMin, unitsMax } = course.classes.reduce(
(acc, { unitsMax, unitsMin }) => ({
unitsMin: Math.floor(Math.min(5, Math.min(acc.unitsMin, unitsMin))),
unitsMax: Math.floor(Math.min(5, Math.max(acc.unitsMax, unitsMax))),
unitsMin: Math.min(5, Math.floor(Math.min(acc.unitsMin, unitsMin))),
unitsMax: Math.min(Math.floor(Math.max(acc.unitsMax, unitsMax))),
}),
{ unitsMax: 0, unitsMin: Infinity }
);
Expand Down Expand Up @@ -167,6 +170,12 @@ export default function Filters({
setSearchParams(searchParams);
};

const handleValueChange = (value: string) => {
if (value === SortBy.Relevance) searchParams.delete("sortBy");
else searchParams.set("sortBy", value);
setSearchParams(searchParams);
};

return (
<div
className={classNames(styles.root, {
Expand All @@ -187,37 +196,26 @@ export default function Filters({
/>
)}
<div className={styles.body}>
<p className={styles.label}>Quick</p>
<div className={styles.filter}>
<Checkbox.Root className={styles.checkbox} id="0">
<Checkbox.Indicator asChild>
<Check width={12} height={12} />
</Checkbox.Indicator>
</Checkbox.Root>
<label className={styles.text} htmlFor="0">
<span className={styles.value}>Open</span> (2,000)
</label>
</div>
<div className={styles.filter}>
<Checkbox.Root className={styles.checkbox} id="0">
<Checkbox.Indicator asChild>
<Check width={12} height={12} />
</Checkbox.Indicator>
</Checkbox.Root>
<label className={styles.text} htmlFor="0">
<span className={styles.value}>Bookmarked</span> (10)
</label>
</div>
<div className={styles.filter}>
<Checkbox.Root className={styles.checkbox} id="0">
<Checkbox.Indicator asChild>
<Check width={12} height={12} />
</Checkbox.Indicator>
</Checkbox.Root>
<label className={styles.text} htmlFor="0">
<span className={styles.value}>Satisfies requirements</span> (10)
</label>
</div>
<p className={styles.label}>Sort by</p>
<RadioGroup.Root
onValueChange={handleValueChange}
value={currentSortBy ?? SortBy.Relevance}
>
{Object.values(SortBy).map((sortBy) => (
<div className={styles.filter}>
<RadioGroup.Item
className={styles.radio}
id={`sortBy-${sortBy}`}
value={sortBy}
>
<RadioGroup.Indicator />
</RadioGroup.Item>
<label className={styles.text} htmlFor={`sortBy-${sortBy}`}>
<span className={styles.value}>{sortBy}</span>
</label>
</div>
))}
</RadioGroup.Root>
<p className={styles.label}>Level</p>
{Object.keys(filteredLevels).map((level) => {
const active = currentLevels.includes(level);
Expand Down
14 changes: 11 additions & 3 deletions frontend/src/components/Browser/browser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { ICatalogCourse } from "@/lib/api";

export enum SortBy {
Relevance = "Relevance",
Units = "Units",
AverageGrade = "Average grade",
OpenSeats = "Open seats",
PercentOpenSeats = "Percent open seats",
}

export const getLevel = (level: string, number: string) => {
if (level !== "Undergraduate") return level;

Expand Down Expand Up @@ -39,8 +47,8 @@ export const getFilteredCourses = (
// Filter by units
const { unitsMin, unitsMax } = course.classes.reduce(
(acc, { unitsMax, unitsMin }) => ({
unitsMin: Math.floor(Math.min(acc.unitsMin, unitsMin)),
unitsMax: Math.floor(Math.max(acc.unitsMax, unitsMax)),
unitsMin: Math.min(5, Math.floor(Math.min(acc.unitsMin, unitsMin))),
unitsMax: Math.min(Math.floor(Math.max(acc.unitsMax, unitsMax))),
}),
{ unitsMax: 0, unitsMin: Infinity }
);
Expand All @@ -50,7 +58,7 @@ export const getFilteredCourses = (
const units = unitsMin + index;

return currentUnits.includes(
unitsMin + index >= 5 ? "5+" : `${units}`
unitsMin + index === 5 ? "5+" : `${units}`
);
}
);
Expand Down
73 changes: 70 additions & 3 deletions frontend/src/components/Browser/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { kindAbbreviations } from "@/lib/section";
import styles from "./Browser.module.scss";
import Filters from "./Filters";
import List from "./List";
import { getFilteredCourses } from "./browser";
import { SortBy, getFilteredCourses } from "./browser";

const initializeFuse = (courses: ICatalogCourse[]) => {
const list = courses.map((course) => {
Expand Down Expand Up @@ -157,6 +157,18 @@ export default function Browser({
);
}, [searchParams]);

const currentSortBy = useMemo(() => {
const parameter = searchParams.get("sortBy");

if (
!Object.values(SortBy).includes(parameter as SortBy) ||
parameter === SortBy.Relevance
)
return;

return parameter as SortBy;
}, [searchParams]);

const { includedCourses, excludedCourses } = useMemo(
() =>
getFilteredCourses(courses, currentKinds, currentUnits, currentLevels),
Expand All @@ -177,17 +189,71 @@ export default function Browser({
};

useEffect(() => {
const _currentCourses = currentQuery
let _currentCourses = currentQuery
? fuse
.search(currentQuery)
.map(({ refIndex }) => includedCourses[refIndex])
: includedCourses;

// Courses are by default sorted by relevance and number
if (currentSortBy) {
_currentCourses = _currentCourses.sort((a, b) => {
if (currentSortBy === SortBy.AverageGrade) {
return b.gradeAverage === a.gradeAverage
? 0
: b.gradeAverage === null
? -1
: a.gradeAverage === null
? 1
: b.gradeAverage - a.gradeAverage;
}

if (currentSortBy === SortBy.Units) {
const getUnits = (course: ICatalogCourse) =>
course.classes.reduce(
(acc, { unitsMax }) => Math.max(acc, unitsMax),
0
);

return getUnits(b) - getUnits(a);
}

if (currentSortBy === SortBy.OpenSeats) {
const getOpenSeats = (course: ICatalogCourse) =>
course.classes.reduce(
(acc, { enrollCount, enrollMax }) =>
acc + (enrollMax - enrollCount),
0
);

return getOpenSeats(b) - getOpenSeats(a);
}

if (currentSortBy === SortBy.PercentOpenSeats) {
const getPercentOpenSeats = (course: ICatalogCourse) => {
const { enrollCount, enrollMax } = course.classes.reduce(
(acc, { enrollCount, enrollMax }) => ({
enrollCount: acc.enrollCount + enrollCount,
enrollMax: acc.enrollMax + enrollMax,
}),
{ enrollCount: 0, enrollMax: 0 }
);

return enrollMax === 0 ? 0 : (enrollMax - enrollCount) / enrollMax;
};

return getPercentOpenSeats(b) - getPercentOpenSeats(a);
}

return 0;
});
}

setCurrentCourses(_currentCourses);

// Collapse courses when filtered courses change
setExpandedCourses(new Array(_currentCourses.length).fill(false));
}, [currentQuery, fuse, includedCourses]);
}, [currentQuery, fuse, includedCourses, currentSortBy]);

console.log(
courses.reduce(
Expand All @@ -212,6 +278,7 @@ export default function Browser({
currentSemester={currentSemester}
currentYear={currentYear}
currentQuery={currentQuery}
currentSortBy={currentSortBy}
/>
)}
<List
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import App from "./App";
import "./main.scss";

const client = new ApolloClient({
uri: "http://192.168.86.22:8080/api/graphql",
uri: "/api/graphql",
cache: new InMemoryCache(),
});

Expand Down

0 comments on commit 8339052

Please sign in to comment.