From 8121c5e72e23550f3583e54285970e492d7225f3 Mon Sep 17 00:00:00 2001 From: Jaden Date: Sun, 26 Nov 2023 15:14:19 -0800 Subject: [PATCH 1/5] saving courses --- .../Catalog/CatalogList/CatalogListItem.tsx | 31 ++-- .../CatalogView/CatalogView.module.scss | 59 +++++++ .../app/Catalog/CatalogView/CatalogView.tsx | 153 ++++++++++++++---- frontend/src/app/Catalog/types.ts | 5 +- frontend/src/app/Catalog/useCatalog.tsx | 42 ++++- 5 files changed, 245 insertions(+), 45 deletions(-) diff --git a/frontend/src/app/Catalog/CatalogList/CatalogListItem.tsx b/frontend/src/app/Catalog/CatalogList/CatalogListItem.tsx index 522babdda..cdedb0c48 100644 --- a/frontend/src/app/Catalog/CatalogList/CatalogListItem.tsx +++ b/frontend/src/app/Catalog/CatalogList/CatalogListItem.tsx @@ -23,10 +23,11 @@ type CatalogListItemProps = { handleCourseSelect: (course: CourseFragment) => void; isSelected: boolean; }; - style: CSSProperties; + style?: CSSProperties; + simple?: boolean; }; -const CatalogListItem = ({ style, data }: CatalogListItemProps) => { +const CatalogListItem = ({ style, data, simple }: CatalogListItemProps) => { const { course, handleCourseSelect, isSelected } = data; const [{ course: currentCourse }] = useCatalog(); @@ -63,20 +64,28 @@ const CatalogListItem = ({ style, data }: CatalogListItemProps) => { {isSaved ? : } )} - - {course.letterAverage !== '' ? course.letterAverage : ''} - + {!simple && ( + + {course.letterAverage !== '' ? course.letterAverage : ''} + + )} -
- - {formatEnrollment(course.enrolledPercentage)} enrolled - - • {course.units ? formatUnits(course.units) : 'N/A'} -
+ {!simple && ( +
+ + {formatEnrollment(course.enrolledPercentage)} enrolled + + • {course.units ? formatUnits(course.units) : 'N/A'} +
+ )} ); }; +CatalogListItem.defaultProps = { + simple: false +}; + export default memo(CatalogListItem, areEqual); diff --git a/frontend/src/app/Catalog/CatalogView/CatalogView.module.scss b/frontend/src/app/Catalog/CatalogView/CatalogView.module.scss index 25178113e..96fbf41b1 100644 --- a/frontend/src/app/Catalog/CatalogView/CatalogView.module.scss +++ b/frontend/src/app/Catalog/CatalogView/CatalogView.module.scss @@ -343,3 +343,62 @@ color: $bt-base-text; background: #f8f8f8; } + +.saveRoot { + display: flex; + flex-direction: row; + padding: 0 20px; + gap: 20px; +} + +.saveContainer { + display: flex; + flex-direction: column; + flex: 1; +} + +.header { + // margin-bottom: 20px; + display: flex; + justify-content: space-between; + flex-direction: row; + align-items: baseline; + gap: 30px; + padding: 6px 23px; + + span { + font-size: 20px; + font-weight: 600; + } + + border-bottom: 1.5px solid #eaeaea; + + button { + color: #2f80ed; + transition: 0.2s; + border: none; + background: none; + padding: 0 8px; + border-radius: 4px; + font-size: 14px; + &:hover { + background: hsla(0, 0%, 92.2%, 0.6196078431); + } + } +} + +.seperator { + display: flex; + height: 100%; + width: 1px; + background: #eaeaea; +} + +.emptyView { + display: flex; + align-items: center; + justify-content: center; + color: $bt-light-text; + padding: 20px 0; + text-align: center; +} diff --git a/frontend/src/app/Catalog/CatalogView/CatalogView.tsx b/frontend/src/app/Catalog/CatalogView/CatalogView.tsx index 7248e5ae2..3d7debb21 100644 --- a/frontend/src/app/Catalog/CatalogView/CatalogView.tsx +++ b/frontend/src/app/Catalog/CatalogView/CatalogView.tsx @@ -5,7 +5,7 @@ import book from 'assets/svg/catalog/book.svg'; import launch from 'assets/svg/catalog/launch.svg'; import { ReactComponent as BackArrow } from 'assets/img/images/catalog/backarrow.svg'; import { applyIndicatorPercent, applyIndicatorGrade, formatUnits } from 'utils/utils'; -import { PlaylistType, useGetCourseForNameLazyQuery } from 'graphql'; +import { CourseFragment, PlaylistType, useGetCourseForNameLazyQuery } from 'graphql'; import { useNavigate, useParams } from 'react-router'; import Skeleton from 'react-loading-skeleton'; import ReadMore from './ReadMore'; @@ -15,14 +15,36 @@ import { sortPills } from '../service'; import CourseTabs from './Tabs'; import styles from './CatalogView.module.scss'; -import { CatalogSlug } from '../types'; +import { CatalogSlug, FilterOption } from '../types'; import Meta from 'components/Common/Meta'; import { courseToName } from 'lib/courses/course'; +import { useUser } from 'graphql/hooks/user'; +import CatalogListItem from '../CatalogList/CatalogListItem'; const skeleton = [...Array(8).keys()]; +// Debug saved courses with dummy array since logging in doesn't work in DEV. +// const dummy = [ +// { +// id: 'Q291cnNlVHlwZToyMTcxOQ==', +// abbreviation: 'MATH', +// courseNumber: '56', +// description: +// 'This is a first course in Linear Algebra. Core topics include: algebra and geometry of vectors and matrices; systems of linear equations and Gaussian elimination; eigenvalues and eigenvectors; Gram-Schmidt and least squares; symmetric matrices and quadratic forms; singular value decomposition and other factorizations. Time permitting, additional topics may include: Markov chains and Perron-Frobenius, dimensionality reduction, or linear programming. This course differs from Math 54 in that it does not cover Differential Equations, but focuses on Linear Algebra motivated by first applications in Data Science and Statistics.', +// title: 'Linear Algebra ', +// gradeAverage: -1, +// letterAverage: 'A', +// openSeats: 0, +// enrolledPercentage: 1, +// enrolled: 292, +// enrolledMax: 292, +// units: '4.0', +// __typename: 'CourseType' +// } +// ]; + const CatalogView = () => { - const [{ course }, dispatch] = useCatalog(); + const [{ course, filters, recentCourses }, dispatch] = useCatalog(); const navigate = useNavigate(); const { abbreviation, courseNumber, semester } = useParams(); @@ -44,26 +66,29 @@ const CatalogView = () => { } }); + const { user } = useUser(); + const savedClasses = useMemo(() => user?.savedClasses ?? [], [user?.savedClasses]); + useEffect(() => { - const [sem, year] = semester?.split(' ') ?? [null, null]; + const [sem, year] = semester?.split(' ') ?? [undefined, undefined]; + + type coursePayload = { + abbreviation: string; + courseNumber: string; + semester: string; + year: string; + }; const variables = { - abbreviation: abbreviation ?? null, - courseNumber: courseNumber ?? null, - semester: sem?.toLowerCase() ?? null, + abbreviation: abbreviation, + courseNumber: courseNumber, + semester: sem?.toLowerCase(), year: year }; // Only fetch the course if every parameter has a value. - if (Object.values(variables).every((value) => value !== null)) - getCourse({ - variables: variables as { - abbreviation: string; - courseNumber: string; - semester: string; - year: string; - } - }); + if (Object.values(variables).every((value) => value !== undefined)) + getCourse({ variables: variables as coursePayload }); }, [getCourse, abbreviation, courseNumber, semester]); useEffect(() => { @@ -92,22 +117,23 @@ const CatalogView = () => { const gradePath = legacyId ? `/grades/0-${legacyId}-all-all` : `/grades`; return ( -
+ <> - - {course && ( - <> + {course ? ( +
+

{course.abbreviation} {course.courseNumber}

@@ -178,9 +204,76 @@ const CatalogView = () => { - +
+ ) : ( +
+
+
+ Recents + +
+ {recentCourses.length > 0 ? ( +
+ {recentCourses.map((course) => ( + { + dispatch({ type: 'setCourse', course }); + navigate({ + pathname: `/catalog/${(filters.semester as FilterOption)?.value?.name}/${ + course.abbreviation + }/${course.courseNumber}`, + search: location.search + }); + }, + isSelected: false + }} + /> + ))} +
+ ) : ( +
Recently viewed courses will appear here!
+ )} +
+
+
+
+ Saved +
+ {savedClasses.length > 0 ? ( +
+ {savedClasses.map((course) => ( + { + dispatch({ type: 'setCourse', course }); + navigate({ + pathname: `/catalog/${(filters.semester as FilterOption)?.value?.name}/${ + course.abbreviation + }/${course.courseNumber}`, + search: location.search + }); + }, + isSelected: false + }} + /> + ))} +
+ ) : ( +
+ Click on the bookmarks in the course list while signed-in to save courses! +
+ )} +
+
)} -
+ ); }; diff --git a/frontend/src/app/Catalog/types.ts b/frontend/src/app/Catalog/types.ts index 15827083d..bcd257dd1 100644 --- a/frontend/src/app/Catalog/types.ts +++ b/frontend/src/app/Catalog/types.ts @@ -78,6 +78,7 @@ export type CatalogContext = { courses: CourseOverviewFragment[]; course: CourseFragment | null; courseIndex: Fuse | null; + recentCourses: CourseFragment[]; }; export type CatalogAction = @@ -88,6 +89,8 @@ export type CatalogAction = | { type: 'filter'; filters: Partial } | { type: 'setCourseList'; allCourses: CatalogContext['courses'] } | { type: 'reset'; filters?: Partial } - | { type: 'setPill'; pillItem: PlaylistType }; + | { type: 'setPill'; pillItem: PlaylistType } + | { type: 'clearRecents' }; + export type CatalogActions = CatalogAction[keyof CatalogAction]; diff --git a/frontend/src/app/Catalog/useCatalog.tsx b/frontend/src/app/Catalog/useCatalog.tsx index 0f49c1700..85226966a 100644 --- a/frontend/src/app/Catalog/useCatalog.tsx +++ b/frontend/src/app/Catalog/useCatalog.tsx @@ -3,6 +3,12 @@ import { DEFAULT_FILTERS, SORT_OPTIONS, buildCourseIndex } from './service'; import { CatalogFilters, CatalogContext, CatalogAction } from './types'; import { searchCatalog, flipCourseList } from './service'; import { byAttribute } from 'lib/courses/sorting'; +import { CourseFragment } from 'graphql'; + +const getRecents = (): CourseFragment[] => { + const recents = localStorage.getItem('recentlyViewedCourses'); + return recents ? JSON.parse(recents) : []; +}; const initialCatalog: CatalogContext = { filters: DEFAULT_FILTERS, @@ -12,7 +18,8 @@ const initialCatalog: CatalogContext = { courses: [], allCourses: [], courseIndex: null, - course: null + course: null, + recentCourses: getRecents() }; const Context = createContext(initialCatalog); @@ -41,11 +48,15 @@ function catalogReducer(catalog: CatalogContext, action: CatalogAction): Catalog sortDir: sortDir === 'ASC' ? 'DESC' : 'ASC', courses: flipCourseList(courses, sortQuery) }; - case 'setCourse': + case 'setCourse': { + const recentCourses = setRecentlyViewed(action.course); + return { ...catalog, - course: action.course + course: action.course, + recentCourses }; + } case 'search': { const newQuery = action.query ?? searchQuery; return { @@ -114,6 +125,13 @@ function catalogReducer(catalog: CatalogContext, action: CatalogAction): Catalog } }; } + case 'clearRecents': { + localStorage.removeItem('recentlyViewedCourses'); + return { + ...catalog, + recentCourses: [] + }; + } default: return catalog; } @@ -133,4 +151,22 @@ const setSearch = (catalog: CatalogContext, query: string | null = null) => { ); }; +const setRecentlyViewed = (course: CourseFragment | null): CourseFragment[] => { + let recents = getRecents(); + + if (!course) return recents; + + // If the course was already viewed, don't add it. + if (recents.find((c) => c.id === course.id)) return recents; + recents = [course, ...recents]; + + // Limit the size; + const maxSize = 25; + recents = recents.slice(-maxSize); + + localStorage.setItem('recentlyViewedCourses', JSON.stringify(recents)); + + return recents; +}; + export default useCatalog; From 67a28b36e03eea583e441260a56cb5689907e780 Mon Sep 17 00:00:00 2001 From: Jaden Date: Sun, 26 Nov 2023 15:24:15 -0800 Subject: [PATCH 2/5] no panel on mobile --- .../src/app/Catalog/CatalogView/CatalogView.module.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/src/app/Catalog/CatalogView/CatalogView.module.scss b/frontend/src/app/Catalog/CatalogView/CatalogView.module.scss index 96fbf41b1..148860eb8 100644 --- a/frontend/src/app/Catalog/CatalogView/CatalogView.module.scss +++ b/frontend/src/app/Catalog/CatalogView/CatalogView.module.scss @@ -351,6 +351,12 @@ gap: 20px; } +@include media(mobile) { + .saveRoot { + display: none; + } +} + .saveContainer { display: flex; flex-direction: column; From b5f94fd340ad8672f386d230787c506828da3229 Mon Sep 17 00:00:00 2001 From: Jaden Date: Sun, 26 Nov 2023 16:22:39 -0800 Subject: [PATCH 3/5] fix container scrolling and tablet view --- .../CatalogView/CatalogView.module.scss | 22 ++++++++++++++----- .../app/Catalog/CatalogView/CatalogView.tsx | 3 +-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/Catalog/CatalogView/CatalogView.module.scss b/frontend/src/app/Catalog/CatalogView/CatalogView.module.scss index 148860eb8..7a89d2b7f 100644 --- a/frontend/src/app/Catalog/CatalogView/CatalogView.module.scss +++ b/frontend/src/app/Catalog/CatalogView/CatalogView.module.scss @@ -57,7 +57,6 @@ padding-right: 10px; // Add size of fixed header. padding-top: 30px; - } &[data-modal='false'] { @@ -348,7 +347,8 @@ display: flex; flex-direction: row; padding: 0 20px; - gap: 20px; + grid-area: view; + overflow: hidden; } @include media(mobile) { @@ -361,6 +361,18 @@ display: flex; flex-direction: column; flex: 1; + overflow-y: scroll; + overflow-x: hidden; + margin: 20px 0px; + + &:first-child { + padding-right: 10px; + // border-right: 1.5px solid #eaeaea; + } + + &:last-child { + padding-left: 10px; + } } .header { @@ -386,7 +398,7 @@ background: none; padding: 0 8px; border-radius: 4px; - font-size: 14px; + font-size: 16px; &:hover { background: hsla(0, 0%, 92.2%, 0.6196078431); } @@ -395,8 +407,8 @@ .seperator { display: flex; - height: 100%; - width: 1px; + width: 1.5px; + margin: 20px 0px; background: #eaeaea; } diff --git a/frontend/src/app/Catalog/CatalogView/CatalogView.tsx b/frontend/src/app/Catalog/CatalogView/CatalogView.tsx index 3d7debb21..2b5ffcd46 100644 --- a/frontend/src/app/Catalog/CatalogView/CatalogView.tsx +++ b/frontend/src/app/Catalog/CatalogView/CatalogView.tsx @@ -209,7 +209,7 @@ const CatalogView = () => {
- Recents + Recent
{recentCourses.length > 0 ? ( @@ -238,7 +238,6 @@ const CatalogView = () => {
Recently viewed courses will appear here!
)}
-
Saved From 45519776fcb45034f90c21de3e775dd78ed6ee57 Mon Sep 17 00:00:00 2001 From: Jaden Date: Sun, 26 Nov 2023 19:49:20 -0800 Subject: [PATCH 4/5] remove used import --- frontend/src/Berkeleytime.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/Berkeleytime.tsx b/frontend/src/Berkeleytime.tsx index b0b72c73e..dc460a79c 100644 --- a/frontend/src/Berkeleytime.tsx +++ b/frontend/src/Berkeleytime.tsx @@ -5,7 +5,6 @@ import useDimensions from 'react-cool-dimensions'; import easterEgg from 'utils/easterEgg'; import Routes from './Routes'; import { fetchEnrollContext, fetchGradeContext } from 'redux/actions'; -import { IconoirProvider } from 'iconoir-react'; const Berkeleytime = () => { const dispatch = useDispatch(); From c05832ece7efd69cbcd35e1c34db9b78bc4a89ad Mon Sep 17 00:00:00 2001 From: Jaden Date: Sun, 26 Nov 2023 20:06:09 -0800 Subject: [PATCH 5/5] capital Clear --- frontend/src/app/Catalog/CatalogView/CatalogView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/Catalog/CatalogView/CatalogView.tsx b/frontend/src/app/Catalog/CatalogView/CatalogView.tsx index 2b5ffcd46..ecf1623d6 100644 --- a/frontend/src/app/Catalog/CatalogView/CatalogView.tsx +++ b/frontend/src/app/Catalog/CatalogView/CatalogView.tsx @@ -210,7 +210,7 @@ const CatalogView = () => {
Recent - +
{recentCourses.length > 0 ? (