diff --git a/src/components/includes/CourseCard.tsx b/src/components/includes/CourseCard.tsx index c24c92990..b1b970b25 100644 --- a/src/components/includes/CourseCard.tsx +++ b/src/components/includes/CourseCard.tsx @@ -1,6 +1,7 @@ -import * as React from 'react'; -import { useHistory } from 'react-router'; -import { Icon } from 'semantic-ui-react'; +import * as React from "react"; +import { useHistory } from "react-router"; +import { Icon } from "semantic-ui-react"; +import { Grid } from "@material-ui/core"; type Props = { course: FireCourse; @@ -11,53 +12,103 @@ type Props = { selected: boolean; inactive?: boolean; }; - +/** + * Renders a course card to display in the course selection page. Displays course code, name, and role if applicable. + * @param course: the course to be displayed + * @param role: the role of the user in the course + * @param onSelectCourse: function to call when the course is selected + * @param editable: whether the course card is editable (ex if you are a ta, you cannot unselect the course) + * @param selected: whether the course is selected + * @param inactive: whether the course is inactive for the current semester + * @returns rendered CourseCard component + */ const CourseCard = ({ course, role, onSelectCourse, editable, selected, inactive = false }: Props) => { const history = useHistory(); const selectCourse = () => { if (!editable) { if (!inactive) { - history.push('/course/' + course.courseId); + history.push("/course/" + course.courseId); } return; } - if (role === undefined || role === 'student') { + if (role === undefined || role === "student") { onSelectCourse(!selected); } }; - let roleString = ''; - if (role === 'ta') { - roleString = 'TA'; - } else if (role === 'professor') { - roleString = 'PROF'; + let roleString = ""; + let roleColor = ""; + let selectedBackgroundColor = "#F5F5F5"; + let selectedBorderColor = "#D8D8D8"; + if (role === "ta") { + roleString = "TA"; + roleColor = "#BF7913"; + } else if (role === "professor") { + roleString = "PROF"; + roleColor = "green"; // a purple thats closer to our brand colors- #726CFF + } else { + selectedBackgroundColor = "rgba(214, 234, 254, 0.4)"; + selectedBorderColor = "#77BBFA"; } + return (
+ + {roleString ? ( + + + {roleString} + {" "} + + ) : ( + + )} + {roleString === "" && !inactive ? ( +
+ {editable ? ( + +
+ {selected ? ( + + ) : ( + + )} +
+
+ ) : ( + + )} +
+ ) : ( + <> + )} +
-
- {course.code} - {roleString && {roleString}} +
{course.code}
+
+ {course.name.length > 30 ? course.name.substring(0, 27) + "..." : course.name}
-
{course.name}
- {!inactive ? ( -
- {editable ? ( - selected ? ( - - ) : ( - - ) - ) : ( -
Go to course
- )} + + {!inactive && !editable ? ( +
+
Go to course
) : ( <> diff --git a/src/components/includes/CourseSelection.tsx b/src/components/includes/CourseSelection.tsx index 0331f4a04..8732d6312 100644 --- a/src/components/includes/CourseSelection.tsx +++ b/src/components/includes/CourseSelection.tsx @@ -1,6 +1,6 @@ -import * as React from 'react'; -import { useEffect, useState } from 'react'; -import { useHistory } from 'react-router'; +import * as React from "react"; +import { useEffect, useState } from "react"; +import { useHistory } from "react-router"; import { connect } from 'react-redux'; import { Icon } from 'semantic-ui-react' @@ -17,12 +17,12 @@ type Props = { readonly isEdit: boolean; }; -export type PageState = 'ready' | 'pending'; +export type PageState = "ready" | "pending"; function CourseSelection({ user, isEdit, allCourses }: Props): React.ReactElement { const history = useHistory(); const [isWritingChanges, setIsWritingChanges] = useState(false); - const [, setPageState] = useState('ready'); + const [, setPageState] = useState("ready"); // Normal editing mode (isNormalEditingMode=true) has all the controls. // On the contrary, onboarding (isNormalEditingMode=false) has only enroll button. @@ -32,14 +32,30 @@ function CourseSelection({ user, isEdit, allCourses }: Props): React.ReactElemen const [formerCourses, setFormerCourses] = useState([]); const [displayWrapped, setDisplayWrapped] = useState(false); + // current searched courses + const [filteredCourses] = useState(currentCourses); useEffect(() => { - setCurrentCourses(allCourses.filter((course) => { - return course.semester === CURRENT_SEMESTER; - })); + setCurrentlyEnrolledCourseIds(new Set(user.courses)); + }, [user.courses]); - setFormerCourses(allCourses.filter((course) => { - return course.semester !== CURRENT_SEMESTER; - })); + const filterOnActiveAndRole = () => { + return allCourses + .filter((course) => course.semester === CURRENT_SEMESTER) + .sort((a, b) => { + const isUserTAorProfA = a.tas?.includes(user.userId) || a.professors?.includes(user.userId) ? 1 : 0; + const isUserTAorProfB = b.tas?.includes(user.userId) || b.professors?.includes(user.userId) ? 1 : 0; + return isUserTAorProfB - isUserTAorProfA; // Sort in descending order (1's before 0's) + }); + }; + + useEffect(() => { + setCurrentCourses(filterOnActiveAndRole); + + setFormerCourses( + allCourses.filter((course) => { + return course.semester !== CURRENT_SEMESTER; + }), + ); }, [allCourses]); const [currentlyEnrolledCourseIds, setCurrentlyEnrolledCourseIds] = useState(new Set()); @@ -48,23 +64,34 @@ function CourseSelection({ user, isEdit, allCourses }: Props): React.ReactElemen setCurrentlyEnrolledCourseIds(new Set(user.courses)); }, [user.courses]); - const [selectedCourses, setSelectedCourses] = useState([]); + const preSelectedCourses: FireCourse[] = + user.courses.length > 0 ? currentCourses.filter((course) => user.roles[course.courseId] !== undefined) : []; + + const [selectedCourses, setSelectedCourses] = useState(preSelectedCourses); + + // courses in which role is defined (TA or Prof) + const [unchangeableCourses, setUnchangableCourses] = useState(preSelectedCourses); useEffect(() => { - setSelectedCourses(currentCourses.filter( - ({ courseId }) => currentlyEnrolledCourseIds.has(courseId) && user.roles[courseId] === undefined - )); - }, [user, currentCourses, currentlyEnrolledCourseIds]); + setSelectedCourses( + currentCourses.filter( + ({ courseId }) => currentlyEnrolledCourseIds.has(courseId) && user.roles[courseId] === undefined, + ), + ); + setUnchangableCourses( + currentCourses.filter( + ({ courseId }) => currentlyEnrolledCourseIds.has(courseId) && user.roles[courseId] !== undefined, + ), + ); + }, [user, filteredCourses, currentlyEnrolledCourseIds]); const [selectedCourseIds, setSelectedCourseIds] = useState([]); - const [collapsed, setCollapsed] = useState(true); - const coursesToEnroll: string[] = []; const coursesToUnenroll: string[] = []; let numCoursesWithRoles = 0; currentCourses.forEach(({ courseId }) => { - if (selectedCourses.some(selected => selected.courseId === courseId)) { + if (selectedCourses.some((selected) => selected.courseId === courseId)) { // The course is selected. if (!currentlyEnrolledCourseIds.has(courseId)) { coursesToEnroll.push(courseId); @@ -76,7 +103,7 @@ function CourseSelection({ user, isEdit, allCourses }: Props): React.ReactElemen // Either // - Previously not enrolled, still not enrolled. // - Is a professor or a TA of the class. Cannot change by themselves. - if (user.roles[courseId] === 'professor' || user.roles[courseId] === 'ta') { + if (user.roles[courseId] === "professor" || user.roles[courseId] === "ta") { numCoursesWithRoles += 1; } // We Do nothing. @@ -91,35 +118,66 @@ function CourseSelection({ user, isEdit, allCourses }: Props): React.ReactElemen useEffect(() => { if (!isNormalEditingMode) { - setIsSaveDisabled(( - coursesToEnroll.length + coursesToUnenroll.length + numCoursesWithRoles === 0) - && !isWritingChanges); + setIsSaveDisabled( + coursesToEnroll.length + coursesToUnenroll.length + numCoursesWithRoles === 0 && !isWritingChanges, + ); } else { - setIsSaveDisabled((coursesToEnroll.length + coursesToUnenroll.length === 0) && !isWritingChanges); + setIsSaveDisabled(coursesToEnroll.length + coursesToUnenroll.length === 0 && !isWritingChanges); } - setPageState(isWritingChanges ? 'pending' : 'ready'); + setPageState(isWritingChanges ? "pending" : "ready"); }, [isWritingChanges, coursesToEnroll, coursesToUnenroll, isNormalEditingMode, numCoursesWithRoles]); useEffect(() => { - setSelectedCourseIds(selectedCourses.map(course => course.courseId)); + setSelectedCourseIds(selectedCourses.map((course) => course.courseId)); }, [selectedCourses]); const onSelectCourse = (course: FireCourse, addCourse: boolean) => { - setSelectedCourses((previousSelectedCourses) => ( + setSelectedCourses((previousSelectedCourses) => addCourse ? [...previousSelectedCourses, course] - : previousSelectedCourses.filter(c => c.courseId !== course.courseId) - )); + : previousSelectedCourses.filter((c) => c.courseId !== course.courseId), + ); + }; + + /** + * Handles the search functionality of the search bar in 'edit courses'. Is called + * onChange of the input element of 'searchbar'. + * Filters the current view of courses based on the search term, while preserving + * the selected courses (shows preservation through list of courses selected under search bar). + * + * @param e = event to pass in search term + */ + const searchInput = (e: React.ChangeEvent) => { + const search = e.target.value.toLowerCase(); + + // Filter courses based on the current semester, reset the search results (when search term is deleted, etc) + const availableCourses = filterOnActiveAndRole(); + + // Filter courses based on the search term + const filteredResults = availableCourses.filter((course) => { + return course.code.toLowerCase().includes(search) || course.name.toLowerCase().includes(search); + }); + + // Preserve the selected courses + const updatedCourses = filteredResults.map((course) => { + const isSelected = selectedCourseIds.includes(course.courseId); + return { + ...course, + isSelected, + }; + }); + + setCurrentCourses(updatedCourses); }; const onSubmit = () => { const newCourseSet = new Set(currentlyEnrolledCourseIds); - coursesToEnroll.forEach(courseId => newCourseSet.add(courseId)); - coursesToUnenroll.forEach(courseId => newCourseSet.delete(courseId)); + coursesToEnroll.forEach((courseId) => newCourseSet.add(courseId)); + coursesToUnenroll.forEach((courseId) => newCourseSet.delete(courseId)); const userUpdate: Partial = { courses: Array.from(newCourseSet.values()) }; setIsWritingChanges(true); updateCourses(user.userId, userUpdate).then(() => { - history.push('/home'); + history.push("/home"); setIsWritingChanges(false); setEditingMode(user.courses.length > 0); }); @@ -128,16 +186,16 @@ function CourseSelection({ user, isEdit, allCourses }: Props): React.ReactElemen const onCancel = () => { // don't add newly-selected courses... add back the newly-deselected courses setSelectedCourses([ - ...(selectedCourses.filter(course => !coursesToEnroll.includes(course.courseId))), - ...currentCourses.filter(course => coursesToUnenroll.includes(course.courseId)) + ...selectedCourses.filter((course) => !coursesToEnroll.includes(course.courseId)), + ...currentCourses.filter((course) => coursesToUnenroll.includes(course.courseId)), ]); - history.push('/home'); + history.push("/home"); }; - const selectedCoursesString = (selectedCourses.length + numCoursesWithRoles === 0 - ? 'No Classes Chosen' - : selectedCourses.map(c => c.code).join(', ')); + // changed guard from selectedCourses.length + numCoursesWithRoles === 0 to selectedCourses.length === 0 + // so that when you cannot unenroll from a course, it says No Classes Chosen instead of an empty box + const selectedCoursesString = selectedCourses.length === 0 ? "" : selectedCourses.map((c) => c.code).join(", "); return (
@@ -149,117 +207,175 @@ function CourseSelection({ user, isEdit, allCourses }: Props): React.ReactElemen // This field is only necessary for professors, but we are always student/TA here. courseId="DUMMY_COURSE_ID" /> -
- {currentCourses.length > 0 || isEdit ? - <> -
-
- {isEdit ? 'Edit Your Classes' : 'My Classes'} -
-
- {isEdit ? 'Add or remove classes.' : 'Select the office hours you want to view.'} -
{selectedCoursesString}
-
-
-
- {currentCourses.filter(course => selectedCourseIds.includes(course.courseId) || - currentlyEnrolledCourseIds.has(course.courseId) - || isEdit) - .map((course) => { - const role = currentlyEnrolledCourseIds.has(course.courseId) - ? (user.roles[course.courseId] || 'student') - : undefined; - const selected = selectedCourseIds.includes(course.courseId) - || (role !== undefined && role !== 'student'); - return ( -
- onSelectCourse(course, addCourse)} - editable={isEdit} - selected={selected} - /> +
+
+
+ {currentCourses.length > 0 || isEdit ? ( + <> +
+
+
+ {isEdit ?
Edit Your Classes
: "My Classes"}
- ); - })} -
- - : <>
-
- {'My Classes'} -
-
- {"You are not enrolled in any courses. Click 'Edit' to enroll in courses."} -
-
} - {!isEdit && formerCourses.length > 0 ? <> -
-
- Former Classes - {collapsed ? ( - { setCollapsed(false) }} - /> - ) : ( - setCollapsed(true)} - /> - )} -
-
-
- {!collapsed && formerCourses.filter(course => selectedCourseIds.includes(course.courseId) || - currentlyEnrolledCourseIds.has(course.courseId)) - .map((course, index) => { - const role = currentlyEnrolledCourseIds.has(course.courseId) - ? (user.roles[course.courseId] || 'student') - : undefined; - return ( -
- { }} - editable={false} - selected={false} +
+ {isEdit + ? "Add or remove classes of your selection." + : "Select the office hours you want to view."} +
{selectedCoursesString}
+
+
+
+ +
+ +
+
+
+
+ {isEdit && (selectedCoursesString.length > 0 || unchangeableCourses.length > 0) ? ( +
+ {unchangeableCourses.map((course) => course.code).join(", ")} + {selectedCoursesString.length > 0 && unchangeableCourses.length > 0 + ? ", " + : ""} + {selectedCoursesString} +
+ ) : ( +
+ )} +
+ {currentCourses + .filter( + (course) => + selectedCourseIds.includes(course.courseId) || + currentlyEnrolledCourseIds.has(course.courseId) || + isEdit, + ) + .map((course) => { + const role = currentlyEnrolledCourseIds.has(course.courseId) + ? user.roles[course.courseId] || "student" + : undefined; + const selected = + selectedCourseIds.includes(course.courseId) || + (role !== undefined && role !== "student"); + return ( +
+ + onSelectCourse(course, addCourse) + } + editable={isEdit} + selected={selected} + /> +
+ ); + })} +
+ + ) : ( + <> +
+
{"My Classes"}
+
+ {"You are not enrolled in any courses. Click 'Edit' to enroll in courses."} +
+
+ +
+ +
+
+
+ + )} + {!isEdit && + formerCourses.filter( + (course) => + selectedCourseIds.includes(course.courseId) || + currentlyEnrolledCourseIds.has(course.courseId), + ).length > 0 ? ( + <> +
+
+ Former Classes +
- ); - })} +
+
+ {formerCourses + .filter( + (course) => + selectedCourseIds.includes(course.courseId) || + currentlyEnrolledCourseIds.has(course.courseId), + ) + .map((course, index) => { + const role = currentlyEnrolledCourseIds.has(course.courseId) + ? user.roles[course.courseId] || "student" + : undefined; + return ( +
+ {}} + editable={false} + selected={false} + /> +
+ ); + })} +
+ + ) : ( + <> + )}
- : <>} -
-
-
-
- {isEdit && selectedCoursesString} +
-
- {!isEdit && ( - - )} - {isEdit && ( - - )} - {isEdit && isNormalEditingMode && ( - - )} + +
+
+ {!isEdit && ( + + )} + {isEdit && ( + + )} + {isEdit && isNormalEditingMode && ( + + )} +
{displayWrapped ? ( @@ -269,8 +385,7 @@ function CourseSelection({ user, isEdit, allCourses }: Props): React.ReactElemen ); } const mapStateToProps = (state: RootState) => ({ - user: state.auth.user -}) - + user: state.auth.user, +}); export default connect(mapStateToProps, {})(CourseSelection); diff --git a/src/styles/courses/CourseCard.scss b/src/styles/courses/CourseCard.scss index e8f6f7bea..577626c25 100644 --- a/src/styles/courses/CourseCard.scss +++ b/src/styles/courses/CourseCard.scss @@ -1,65 +1,144 @@ .CourseCard { - margin: 25px 15px; - border-radius: 4px; - box-shadow: 5px 10px 20px gainsboro; + margin: 18px 30px; + border-radius: 15px; + border-color: black; background-color: white; - text-align: left; + text-align: center; flex: 1 1 400px; - width: 300px; + width: 250px; + font-family: 'Roboto'; + height: 200px; + font-weight: 400; + border: 2px solid #D8D8D8; + overflow-wrap: anywhere; &.selected { - opacity: 0.6; + background-color: rgba(214, 234, 254, 0.4); + border: 2px solid #77BBFA; } - - &.active:hover { + + &.editable:hover, &.active.ineditable:not(.selected):hover { cursor: pointer; - box-shadow: 5px 10px 20px gray; + box-shadow: 5px 10px 20px rgb(219, 219, 219); + } + + .grid-container { + height: 58px; + } + + .courseColor { + // width: 50%; + // align-self: right; + position: relative; + top: 0; + display: flex; + align-items: center; + // justify-content: space-between; + padding: 19px; + width: 100%; + + .icon, + div { + font-size: 18px; + flex-shrink: 0; + margin-left: auto; + text-align: right; + } + + .courseRole { + display: flex; + justify-content: center; + align-items: center; + height: 32px; + padding: 10px 17px; + text-align: center; + gap: 10px; + border-radius: 100px; + + //check coursecard for hex color of prof + border: 2px solid #BF7913; + color: #BF7913; + font-size: 15px; + position: absolute; + } + } + + .myClasses { + display: flex; + align-items: center; + justify-content: center; + font-size: 15px; + position: relative; + + .myClassesText { + background: #F5F5F5; + border-radius: 15px; + align-items: center; + // max-width: 50%; + padding: 10px 15px; + // margin-bottom: 20px; + position: static; + bottom: 5px; + } } .courseText { - padding: 30px; - min-height: 150px; + color: #484848; + font-size: 36px; + font-style: normal; + line-height: normal; + + padding: 0px 30px 0px 30px; + max-height: 150px; .courseCode { display: inline-flex; align-items: center; - font-size: 25px; + font-size: 28px; font-weight: bold; - margin-bottom: 30px; - - .role { - margin-left: 10px; - padding: 2px 10px; - border-radius: 4px; - border: solid 1px black; - font-size: 18px; - } + margin-bottom: 10px; } .courseName { - font-size: 20px; + font-size: 18px; + min-height: 42px; } } - .courseColor { - position: relative; - bottom: 0; - height: 50px; - background-color: #ffc5a1; - .icon, div { - font-size: 20px; - text-align: center; - display: block; - margin: auto; - padding-top: 15px; - } - } } -@media only screen and (max-width: $mobile-breakpoint) { +@media only screen and (max-width: 1062px) { .CourseCard { // min-width: 200px; - // max-width: 200px; + max-width: 300px; + // padding: + } +} + +@media only screen and (max-width: 550px) { + .CourseCard { + min-width: 200px; + // max-width: 300px; + // padding: } } + +@media only screen and (max-width: 430px) { + .CourseCard { + max-width: 200px; + + .courseText { + padding: 0px 20px 10px 20px; + .courseName { + min-height: 2px; + } + } + + .courseColor { + .courseRole { + font-size: 16px; + } + } + } +} \ No newline at end of file diff --git a/src/styles/courses/CourseSelection.scss b/src/styles/courses/CourseSelection.scss index 0d6674167..3aedf1d5d 100644 --- a/src/styles/courses/CourseSelection.scss +++ b/src/styles/courses/CourseSelection.scss @@ -1,4 +1,21 @@ .CourseSelection { + .sectionDivide { + margin-top: 25px; + margin-bottom: 25px; + } + scroll-behavior: "smooth"; + .GreyBackground { + background-color: #F5F5F5; + padding: 30px 47px 150px 47px; + } + + .WhiteBackground { + background-color: #FFFFFF; + box-shadow: 0px 2.362499952316284px 4.724999904632568px 0px #00000040; + border-radius: 30px; + padding: 30px 0px 0 0px; + } + .QMeLogo.course { position: absolute; top: 9px; @@ -10,42 +27,114 @@ } .selectionContent { - margin-top: 46px; - padding: 20px 0 100px; + margin-top: 30px; box-sizing: border-box; + font-family: 'Roboto'; .description { + + padding-left: 72px; + text-align: left; + color: #484848; + display: inline-block; + width: 100%; + + .sideblock { + display: inline-block; + width: 50%; + } + .title { - font-size: 50px; - line-height: 50px; - padding-bottom: 40px; + padding-bottom: 8px; + font-size: 28px; + font-weight: 600; + line-height: normal; } .subtitle { - font-size: 20px; + font-size: 18px; + font-weight: 400; + // padding-bottom: 30px; .EnrolledCourses.mobile { display: none; } - .icon { padding-left: 10px; cursor: pointer; } } - } - .CourseCards { - display: flex; - padding: 10px; - flex-shrink: 1; - flex-wrap: wrap; - justify-content: center; + .searchbar { + float: right; + display: flex; + align-items: center; + width: 36%; + // margin-bottom: 34px; + background-color: white; + border: 2px solid #D8D8D8; + border-radius: 15px; + margin-top: 8px; + margin-right: 72px; + + input { + flex: 1; + border-radius: 15px; + border: none; + font-size: 18px; + font-weight: 400; + color: #7E7E7E; + padding: 12px 28px; + + } + + .searchIcon { + // margin-top: 8px; + font-size: 23px; + color: #484848; + margin-right: 28px; + } + + + } } } + + hr { + border: 2px solid #F0F0F0; + } + + .EnrolledClasses { + margin: 36px; + display: inline-flex; + padding: 12px 64px; + justify-content: center; + align-items: center; + gap: 12px; + border-radius: 15px; + background: #F5F5F5; + color: #5599DB; + font-family: Roboto; + font-size: 18px; + font-weight: 500; + line-height: normal; + } + + .CourseCards { + display: flex; + padding: 0px 35px 0 35px; + flex-shrink: 1; + flex-wrap: wrap; + justify-content: center; + border-radius: 15px; + border-color: black; + } } + .EnrollBar { + font-family: 'Roboto'; + float: right; position: fixed; padding: 0px 30px; left: 0; @@ -56,9 +145,9 @@ height: 100px; font-size: 18px; display: flex; - justify-content: space-between; + justify-content: flex-end; align-items: center; - // flex-wrap: wrap; + flex-wrap: wrap; .EnrolledCourses.web { display: block; @@ -99,6 +188,7 @@ display: inline-block; background-color: white; border: solid 1px #484848; + color: #484848; margin: 5px; } @@ -109,11 +199,13 @@ } } -@media only screen and (max-width: $mobile-breakpoint) { +@media only screen and (max-width: 1062px) { .App { background-color: white; } + + .CourseSelection { background-color: white; @@ -125,6 +217,11 @@ display: none; } + .GreyBackground { + padding: 20px; + padding-bottom: 120px; + } + .selectionContent { margin-top: 0px; @@ -133,22 +230,36 @@ } .description { - padding-bottom: 25px; - border-bottom: solid 2px #e2e2e2; + padding-left: 50px; + // padding-bottom: 25px; + // border-bottom: solid 2px #e2e2e2; .title { - font-size: 24px; padding-bottom: 20px; font-weight: bold; } .subtitle { - font-size: 18px; .EnrolledCourses.mobile { display: none; // Change to block } } + + .searchbar { + font-size: 15px; + // display: flex; + width: 70%; + float: left; + margin-right: 30px; + margin-bottom: 30px; + + .input { + // font-size: 1px; + min-width: 30; + } + + } } } }