diff --git a/frontend/package.json b/frontend/package.json index e6cb986bd..2444f2db5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,7 @@ }, "dependencies": { "@apollo/client": "^3.2.5", + "@types/object-hash": "^3.0.5", "axios": "^1.2.6", "bootstrap": "^4.5.2", "color": "^3.1.3", diff --git a/frontend/src/Berkeleytime.tsx b/frontend/src/Berkeleytime.tsx index 9d6f45d6a..77c55a815 100644 --- a/frontend/src/Berkeleytime.tsx +++ b/frontend/src/Berkeleytime.tsx @@ -1,11 +1,11 @@ +import { IconoirProvider } from 'iconoir-react'; import { memo, useEffect } from 'react'; -import { useDispatch } from 'react-redux'; -import { openBanner, enterMobile, exitMobile, openLandingModal } from './redux/common/actions'; import useDimensions from 'react-cool-dimensions'; +import { useDispatch } from 'react-redux'; +import { fetchEnrollContext } from 'redux/enrollment/actions'; import easterEgg from 'utils/easterEgg'; import Routes from './Routes'; -import { fetchEnrollContext } from 'redux/actions'; -import { IconoirProvider } from 'iconoir-react'; +import { enterMobile, exitMobile, openBanner, openLandingModal } from './redux/common/actions'; const Berkeleytime = () => { const dispatch = useDispatch(); diff --git a/frontend/src/Routes.tsx b/frontend/src/Routes.tsx index 71a8516f2..160d4ce7c 100644 --- a/frontend/src/Routes.tsx +++ b/frontend/src/Routes.tsx @@ -19,7 +19,6 @@ const RemoteScheduler = () => import('./app/Scheduler/RemoteSchedulerPage'); const ViewSchedule = () => import('./app/Scheduler/ViewSchedule'); const PrivacyPolicy = () => import('./views/PrivacyPolicy'); const TermsOfService = () => import('./views/TermsOfService'); -const RedirectLink = () => import('./views/RedirectLink'); // const Apply = () => import('./views/Apply'); function ScheduleRedirect() { @@ -48,8 +47,7 @@ const router = createBrowserRouter([ { path: '/schedule/:scheduleId', lazy: ViewSchedule }, { path: '/error', Component: Error }, { path: '/legal/privacy', lazy: PrivacyPolicy }, - { path: '/legal/terms', lazy: TermsOfService }, - { path: '/redirect', lazy: RedirectLink }, + { path: '/legal/terms', lazy: TermsOfService } // { path: '/apply', lazy: Apply } ] }, diff --git a/frontend/src/app/Enrollment/index.jsx b/frontend/src/app/Enrollment/index.jsx index 1dcafff05..13dda0d84 100644 --- a/frontend/src/app/Enrollment/index.jsx +++ b/frontend/src/app/Enrollment/index.jsx @@ -1,20 +1,17 @@ -import { useLocation, useNavigate } from 'react-router-dom'; - +import { useCallback, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { + enrollRemoveCourse, + enrollReset, + fetchEnrollClass, + fetchEnrollContext, + fetchEnrollFromUrl +} from 'redux/enrollment/actions'; +import info from '../../assets/img/images/graphs/info.svg'; import ClassCardList from '../../components/ClassCards/ClassCardList'; -import EnrollmentGraphCard from '../../components/GraphCard/EnrollmentGraphCard'; import EnrollmentSearchBar from '../../components/ClassSearchBar/EnrollmentSearchBar'; - -import info from '../../assets/img/images/graphs/info.svg'; - -import { - fetchEnrollContext, - fetchEnrollClass, - enrollRemoveCourse, - enrollReset, - fetchEnrollFromUrl -} from '../../redux/actions'; -import { useCallback, useEffect, useState } from 'react'; +import EnrollmentGraphCard from '../../components/GraphCard/EnrollmentGraphCard'; const toUrlForm = (s) => { return s.toLowerCase().split(' ').join('-'); diff --git a/frontend/src/app/Grades/index.jsx b/frontend/src/app/Grades/index.jsx index 694d0bf2c..fdfadfa56 100644 --- a/frontend/src/app/Grades/index.jsx +++ b/frontend/src/app/Grades/index.jsx @@ -1,20 +1,17 @@ import { useCallback, useEffect, useState } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; - import { useDispatch, useSelector } from 'react-redux'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { + fetchGradeClass, + fetchGradeContext, + fetchGradeFromUrl, + gradeRemoveCourse, + gradeReset +} from 'redux/grades/actions'; +import info from '../../assets/img/images/graphs/info.svg'; import ClassCardList from '../../components/ClassCards/ClassCardList'; -import GradesGraphCard from '../../components/GraphCard/GradesGraphCard'; import GradesSearchBar from '../../components/ClassSearchBar/GradesSearchBar'; - -import info from '../../assets/img/images/graphs/info.svg'; - -import { - fetchGradeContext, - fetchGradeClass, - gradeRemoveCourse, - gradeReset, - fetchGradeFromUrl -} from '../../redux/actions'; +import GradesGraphCard from '../../components/GraphCard/GradesGraphCard'; const toUrlForm = (s) => { s = s.replace('/', '_'); diff --git a/frontend/src/components/ClassCards/ClassCardList.jsx b/frontend/src/components/ClassCards/ClassCardList.jsx index fc85a09ea..20b782e63 100644 --- a/frontend/src/components/ClassCards/ClassCardList.jsx +++ b/frontend/src/components/ClassCards/ClassCardList.jsx @@ -1,7 +1,6 @@ import { Container, Row } from 'react-bootstrap'; - +import colors from 'utils/colors'; import ClassCard from './ClassCard'; -import vars from '../../utils/variables'; export default function ClassCardList({ selectedCourses, @@ -19,7 +18,7 @@ export default function ClassCardList({ id={item.id} course={item.course} title={item.title} - fill={vars.colors[item.colorId]} + fill={colors[item.colorId]} semester={item.semester === 'all' ? 'All Semesters' : item.semester} faculty={item.instructor === 'all' ? 'All Instructors' : item.instructor} removeCourse={removeCourse} diff --git a/frontend/src/components/ClassSearchBar/EnrollmentSearchBar.jsx b/frontend/src/components/ClassSearchBar/EnrollmentSearchBar.jsx index f320e166d..41054c99e 100644 --- a/frontend/src/components/ClassSearchBar/EnrollmentSearchBar.jsx +++ b/frontend/src/components/ClassSearchBar/EnrollmentSearchBar.jsx @@ -1,10 +1,10 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { Container, Row, Col, Button } from 'react-bootstrap'; import hash from 'object-hash'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { Button, Col, Container, Row } from 'react-bootstrap'; -import { fetchEnrollSelected } from '../../redux/actions'; -import { useDispatch, useSelector } from 'react-redux'; import BTSelect from 'components/Custom/Select'; +import { useDispatch, useSelector } from 'react-redux'; +import { fetchEnrollSelected } from 'redux/enrollment/actions'; const buildCoursesOptions = (courses) => { if (!courses) { diff --git a/frontend/src/components/ClassSearchBar/GradesSearchBar.jsx b/frontend/src/components/ClassSearchBar/GradesSearchBar.jsx index 39038bedc..f1bc58706 100644 --- a/frontend/src/components/ClassSearchBar/GradesSearchBar.jsx +++ b/frontend/src/components/ClassSearchBar/GradesSearchBar.jsx @@ -1,11 +1,9 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { Container, Row, Col, Button } from 'react-bootstrap'; +import BTSelect from 'components/Custom/Select'; import hash from 'object-hash'; - +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { Button, Col, Container, Row } from 'react-bootstrap'; import { useDispatch, useSelector } from 'react-redux'; - -import { fetchGradeSelected } from '../../redux/actions'; -import BTSelect from 'components/Custom/Select'; +import { fetchGradeSelected } from 'redux/grades/actions'; const sortOptions = [ { value: 'instructor', label: 'By Instructor' }, diff --git a/frontend/src/components/Common/Banner.tsx b/frontend/src/components/Common/Banner.tsx index 61295a1f4..8d4508f3d 100644 --- a/frontend/src/components/Common/Banner.tsx +++ b/frontend/src/components/Common/Banner.tsx @@ -1,9 +1,8 @@ -import { useSelector, useDispatch } from 'react-redux'; import { Button } from 'bt/custom'; -import { closeBanner } from '../../redux/common/actions'; - -import close from '../../assets/svg/common/close.svg'; +import { useDispatch, useSelector } from 'react-redux'; +import { closeBanner } from 'redux/common/actions'; import { ReduxState } from 'redux/store'; +import close from '../../assets/svg/common/close.svg'; export default function Banner() { const { banner } = useSelector((state: ReduxState) => state.common); diff --git a/frontend/src/components/Common/Footer.tsx b/frontend/src/components/Common/Footer.tsx index 0d48d8ff7..4c166d9a3 100644 --- a/frontend/src/components/Common/Footer.tsx +++ b/frontend/src/components/Common/Footer.tsx @@ -36,9 +36,6 @@ export default function Footer() { Discord - - Facebook - diff --git a/frontend/src/components/Dashboard/CurrentSchedule.jsx b/frontend/src/components/Dashboard/CurrentSchedule.tsx similarity index 81% rename from frontend/src/components/Dashboard/CurrentSchedule.jsx rename to frontend/src/components/Dashboard/CurrentSchedule.tsx index 48df75079..5a33d7d1e 100644 --- a/frontend/src/components/Dashboard/CurrentSchedule.jsx +++ b/frontend/src/components/Dashboard/CurrentSchedule.tsx @@ -1,6 +1,9 @@ -/* eslint-disable */ +import { CurrentScheduleProps } from './type'; -function CurrentSchedule({ current }) { +/** + * @audit this file is not used + */ +function CurrentSchedule({ current }: CurrentScheduleProps) { return (
@@ -10,7 +13,7 @@ function CurrentSchedule({ current }) {
{current.semester}
{current.classes.map((currentClass) => ( -

+

{currentClass.name} {currentClass.units} Units

))} diff --git a/frontend/src/components/Dashboard/PastSchedule.jsx b/frontend/src/components/Dashboard/PastSchedule.tsx similarity index 79% rename from frontend/src/components/Dashboard/PastSchedule.jsx rename to frontend/src/components/Dashboard/PastSchedule.tsx index d2d56e00d..52315a703 100644 --- a/frontend/src/components/Dashboard/PastSchedule.jsx +++ b/frontend/src/components/Dashboard/PastSchedule.tsx @@ -1,6 +1,9 @@ -/* eslint-disable */ +import { PastScheduleProps } from './type'; -function PastSchedule({ past }) { +/** + * @audit this file is not used + */ +function PastSchedule({ past }: PastScheduleProps) { return (
@@ -8,10 +11,10 @@ function PastSchedule({ past }) { Past Semesters {past.map((semester) => ( -
+
{semester.semester}
{semester.classes.map((pastClass) => ( -

+

{pastClass.name} {pastClass.units} Units

))} diff --git a/frontend/src/components/Dashboard/Profile.jsx b/frontend/src/components/Dashboard/Profile.tsx similarity index 86% rename from frontend/src/components/Dashboard/Profile.jsx rename to frontend/src/components/Dashboard/Profile.tsx index a0090b97d..8824e4fc8 100644 --- a/frontend/src/components/Dashboard/Profile.jsx +++ b/frontend/src/components/Dashboard/Profile.tsx @@ -1,8 +1,10 @@ -/* eslint-disable */ - import jemma from 'assets/img/about/2020-21/michael_1.jpg'; +import { ProfileProps } from './type'; -function Profile({ profile }) { +/** + * @audit this file is not used + */ +function Profile({ profile }: ProfileProps) { return (
@@ -16,7 +18,7 @@ function Profile({ profile }) {

Major(s)

{profile.majors.map((major) => ( -

{major}

+

{major}

))}
diff --git a/frontend/src/components/Dashboard/type.ts b/frontend/src/components/Dashboard/type.ts new file mode 100644 index 000000000..1c23a93d3 --- /dev/null +++ b/frontend/src/components/Dashboard/type.ts @@ -0,0 +1,16 @@ +export type CurrentScheduleProps = { + current: ScheduleSemesterType; +}; + +export type PastScheduleProps = { + past: ScheduleSemesterType[]; +}; + +export type ProfileProps = { + profile: { name: string; pic: string; majors: string[]; academic_career: string; level: string }; +}; + +export type ScheduleSemesterType = { + semester: string; + classes: { name: string; units: number }[]; +}; diff --git a/frontend/src/components/GraphCard/EnrollmentGraphCard.jsx b/frontend/src/components/GraphCard/EnrollmentGraphCard.jsx index 6fa4a9563..02b65a46d 100644 --- a/frontend/src/components/GraphCard/EnrollmentGraphCard.jsx +++ b/frontend/src/components/GraphCard/EnrollmentGraphCard.jsx @@ -1,14 +1,11 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; -import { Container, Row, Col } from 'react-bootstrap'; - +import { Col, Container, Row } from 'react-bootstrap'; import { useDispatch, useSelector } from 'react-redux'; -import vars from '../../utils/variables'; - +import { fetchEnrollData } from 'redux/enrollment/actions'; +import colors from 'utils/colors.ts'; +import EnrollmentInfoCard from '../EnrollmentInfoCard/EnrollmentInfoCard.jsx'; import EnrollmentGraph from '../Graphs/EnrollmentGraph.jsx'; import GraphEmpty from '../Graphs/GraphEmpty.jsx'; -import EnrollmentInfoCard from '../EnrollmentInfoCard/EnrollmentInfoCard.jsx'; - -import { fetchEnrollData } from '../../redux/actions'; export default function EnrollmentGraphCard({ isMobile, updateClassCardEnrollment }) { const [hoveredClass, setHoveredClass] = useState(false); @@ -144,7 +141,7 @@ export default function EnrollmentGraphCard({ isMobile, updateClassCardEnrollmen } todayPoint={hoveredClass.data[hoveredClass.data.length - 1]} telebears={telebears} - color={vars.colors[hoveredClass.colorId]} + color={colors[hoveredClass.colorId]} enrolledMax={hoveredClass.enrolled_max} waitlistedMax={hoveredClass.waitlisted_max} /> diff --git a/frontend/src/components/GraphCard/GradesGraphCard.jsx b/frontend/src/components/GraphCard/GradesGraphCard.jsx index 5ce7e5f05..41fc6b2f8 100644 --- a/frontend/src/components/GraphCard/GradesGraphCard.jsx +++ b/frontend/src/components/GraphCard/GradesGraphCard.jsx @@ -1,14 +1,11 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; -import { Container, Row, Col } from 'react-bootstrap'; - +import { Col, Container, Row } from 'react-bootstrap'; import { useDispatch, useSelector } from 'react-redux'; -import vars from '../../utils/variables'; - +import { fetchGradeData } from 'redux/grades/actions'; +import colors from 'utils/colors'; +import GradesInfoCard from '../GradesInfoCard/GradesInfoCard'; import GradesGraph from '../Graphs/GradesGraph'; import GraphEmpty from '../Graphs/GraphEmpty'; -import GradesInfoCard from '../GradesInfoCard/GradesInfoCard'; - -import { fetchGradeData } from '../../redux/actions'; export default function GradesGraphCard({ isMobile, updateClassCardGrade }) { const { gradesData, graphData, selectedCourses } = useSelector((state) => state.grade); @@ -117,7 +114,7 @@ export default function GradesGraphCard({ isMobile, updateClassCardGrade }) { } selectedPercentiles={hoveredClass[hoveredClass.hoverGrade]} denominator={hoveredClass.denominator} - color={vars.colors[hoveredClass.colorId]} + color={colors[hoveredClass.colorId]} isMobile={isMobile} graphEmpty={graphEmpty} /> @@ -153,7 +150,7 @@ export default function GradesGraphCard({ isMobile, updateClassCardGrade }) { denominator={hoveredClass.denominator} selectedPercentiles={hoveredClass[hoveredClass.hoverGrade]} selectedGrade={hoveredClass.hoverGrade} - color={vars.colors[hoveredClass.colorId]} + color={colors[hoveredClass.colorId]} /> )} diff --git a/frontend/src/components/Graphs/EnrollmentGraph.jsx b/frontend/src/components/Graphs/EnrollmentGraph.jsx index 88bf20fed..8494135d4 100644 --- a/frontend/src/components/Graphs/EnrollmentGraph.jsx +++ b/frontend/src/components/Graphs/EnrollmentGraph.jsx @@ -1,16 +1,15 @@ import { - LineChart, - XAxis, - YAxis, - Tooltip, - Line, - Legend, - ReferenceLine, - Label, - ResponsiveContainer + Label, + Legend, + Line, + LineChart, + ReferenceLine, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis } from 'recharts'; - -import vars from '../../utils/variables'; +import colors from 'utils/colors'; import emptyImage from '../../assets/img/images/graphs/empty.svg'; const EmptyLabel = (props) => { @@ -95,7 +94,7 @@ export default function EnrollmentGraph({ name={`${item.title} • ${item.section_name}`} type="monotone" dataKey={item.id} - stroke={vars.colors[item.colorId]} + stroke={colors[item.colorId]} strokeWidth={3} dot={false} activeDot={{ onMouseOver: updateLineHover }} diff --git a/frontend/src/components/Graphs/GradesGraph.jsx b/frontend/src/components/Graphs/GradesGraph.jsx index eb9d6c8db..4f7c25ccd 100644 --- a/frontend/src/components/Graphs/GradesGraph.jsx +++ b/frontend/src/components/Graphs/GradesGraph.jsx @@ -1,8 +1,7 @@ -import { BarChart, Bar, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer } from 'recharts'; -import { percentileToString } from '../../utils/utils'; - -import vars from '../../utils/variables'; +import { Bar, BarChart, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; +import colors from 'utils/colors'; import emptyImage from '../../assets/img/images/graphs/empty.svg'; +import { percentileToString } from '../../utils/utils'; const EmptyLabel = (props) => { return ( @@ -99,7 +98,7 @@ export default function GradesGraph({ key={i} name={`${item.title} • ${item.semester} • ${item.instructor}`} dataKey={item.id} - fill={vars.colors[item.colorId]} + fill={colors[item.colorId]} onMouseEnter={updateBarHover} radius={[4, 4, 0, 0]} /> @@ -143,7 +142,7 @@ export default function GradesGraph({ key={i} name={`${item.title} • ${item.semester} • ${item.instructor}`} dataKey={item.id} - fill={vars.colors[item.colorId]} + fill={colors[item.colorId]} onMouseEnter={updateBarHover} label={} radius={[0, 4, 4, 0]} diff --git a/frontend/src/components/Landing/LandingModal.tsx b/frontend/src/components/Landing/LandingModal.tsx index 5ac92185a..ee1fd57d1 100644 --- a/frontend/src/components/Landing/LandingModal.tsx +++ b/frontend/src/components/Landing/LandingModal.tsx @@ -1,12 +1,11 @@ +import closeIcon from 'assets/svg/common/close.svg'; +import { Button, H3, P } from 'bt/custom'; import { useCallback, useEffect } from 'react'; import { Modal } from 'react-bootstrap'; -import { H3, P, Button } from 'bt/custom'; -import closeIcon from 'assets/svg/common/close.svg'; -import schedulerImg from '../../assets/img/landing/scheduler.png'; - import { useDispatch, useSelector } from 'react-redux'; -import { ReduxState } from '../../redux/store'; -import { closeLandingModal } from '../../redux/common/actions'; +import { closeLandingModal } from 'redux/common/actions'; +import { ReduxState } from 'redux/store'; +import schedulerImg from '../../assets/img/landing/scheduler.png'; const modal_info = { subtitle: 'NEW!', diff --git a/frontend/src/components/Releases/Log.jsx b/frontend/src/components/Releases/Log.tsx similarity index 85% rename from frontend/src/components/Releases/Log.jsx rename to frontend/src/components/Releases/Log.tsx index 940f81112..2d4efa8dc 100644 --- a/frontend/src/components/Releases/Log.jsx +++ b/frontend/src/components/Releases/Log.tsx @@ -1,4 +1,6 @@ -function Log({ date, whatsNew, fixes }) { +import { ReleaseType } from 'lib/releases'; + +export default function Log({ date, whatsNew, fixes }: ReleaseType) { return (
@@ -31,5 +33,3 @@ function Log({ date, whatsNew, fixes }) {
); } - -export default Log; diff --git a/frontend/src/lib/courses/sorting.ts b/frontend/src/lib/courses/sorting.ts index 6b5a75f5f..8a7195455 100644 --- a/frontend/src/lib/courses/sorting.ts +++ b/frontend/src/lib/courses/sorting.ts @@ -7,8 +7,8 @@ type CompareFn = (courseA: CourseOverviewFragment, courseB: CourseOverviewFragme * Comparator for department name. Essentially alphabetical order. */ export const compareDepartmentName: CompareFn = (courseA, courseB) => { - const courseATitle = `${courseA.abbreviation} ${courseA.courseNumber}`; - const courseBTitle = `${courseB.abbreviation} ${courseB.courseNumber}`; + const courseATitle = `${courseA.abbreviation} ${courseA.courseNumber.replace(/^[A-Za-z]/, '')}`; + const courseBTitle = `${courseB.abbreviation} ${courseB.courseNumber.replace(/^[A-Za-z]/, '')}`; return courseATitle.localeCompare(courseBTitle); }; diff --git a/frontend/src/lib/releases.ts b/frontend/src/lib/releases.ts index 6928dd38f..a60581db4 100644 --- a/frontend/src/lib/releases.ts +++ b/frontend/src/lib/releases.ts @@ -1,4 +1,6 @@ -const releases = [ +export type ReleaseType = { date: string; whatsNew: string[]; fixes: string[] }; + +const releases: ReleaseType[] = [ { date: 'Jan 24, 2021', whatsNew: [ diff --git a/frontend/src/redux/actionTypes.js b/frontend/src/redux/actionTypes.js deleted file mode 100644 index 42cbb9155..000000000 --- a/frontend/src/redux/actionTypes.js +++ /dev/null @@ -1,13 +0,0 @@ -export const UPDATE_GRADE_CONTEXT = 'UPDATE_GRADE_CONTEXT'; -export const GRADE_ADD_COURSE = 'GRADE_ADD_COURSE'; -export const GRADE_REMOVE_COURSE = 'GRADE_REMOVE_COURSE'; -export const UPDATE_GRADE_DATA = 'UPDATE_GRADE_DATA'; -export const UPDATE_GRADE_SELECTED = 'UPDATE_GRADE_SELECTED'; -export const GRADE_RESET = 'GRADE_RESET'; - -export const UPDATE_ENROLL_CONTEXT = 'UPDATE_ENROLL_CONTEXT'; -export const ENROLL_ADD_COURSE = 'ENROLL_ADD_COURSE'; -export const ENROLL_REMOVE_COURSE = 'ENROLL_REMOVE_COURSE'; -export const UPDATE_ENROLL_DATA = 'UPDATE_ENROLL_DATA'; -export const UPDATE_ENROLL_SELECTED = 'UPDATE_ENROLL_SELECTED'; -export const ENROLL_RESET = 'ENROLL_RESET'; diff --git a/frontend/src/redux/actions.js b/frontend/src/redux/actions.js deleted file mode 100644 index 574ff99c7..000000000 --- a/frontend/src/redux/actions.js +++ /dev/null @@ -1,464 +0,0 @@ -/* eslint-disable */ -import axios from 'axios'; -import hash from 'object-hash'; -import { - UPDATE_GRADE_CONTEXT, - GRADE_ADD_COURSE, - GRADE_REMOVE_COURSE, - GRADE_RESET, - UPDATE_GRADE_DATA, - UPDATE_GRADE_SELECTED, - UPDATE_ENROLL_CONTEXT, - ENROLL_RESET, - ENROLL_ADD_COURSE, - ENROLL_REMOVE_COURSE, - UPDATE_ENROLL_DATA, - UPDATE_ENROLL_SELECTED -} from './actionTypes'; - -axios.defaults.baseURL = import.meta.env.PROD ? axios.defaults.baseURL : 'https://staging.berkeleytime.com'; - -// update grade list -const updateGradeContext = (data) => ({ - type: UPDATE_GRADE_CONTEXT, - payload: { - data - } -}); - -export const gradeReset = () => ({ - type: GRADE_RESET -}); - -// add displayed course to the grade page -const gradeAddCourse = (formattedCourse) => ({ - type: GRADE_ADD_COURSE, - payload: { - formattedCourse - } -}); - -export const gradeRemoveCourse = (id, color) => ({ - type: GRADE_REMOVE_COURSE, - payload: { - id, - color - } -}); - -const updateGradeData = (gradesData) => ({ - type: UPDATE_GRADE_DATA, - payload: { - gradesData - } -}); - -const updatedGradeSelected = (data) => ({ - type: UPDATE_GRADE_SELECTED, - payload: { - data - } -}); - -// update enroll list -const updateEnrollContext = (data) => ({ - type: UPDATE_ENROLL_CONTEXT, - payload: { - data - } -}); - -export const enrollReset = () => ({ - type: ENROLL_RESET -}); - -// add displayed course to the enroll page -const enrollAddCourse = (formattedCourse) => ({ - type: ENROLL_ADD_COURSE, - payload: { - formattedCourse - } -}); - -export const enrollRemoveCourse = (id, color) => ({ - type: ENROLL_REMOVE_COURSE, - payload: { - id, - color - } -}); - -export const updateEnrollData = (enrollmentData) => ({ - type: UPDATE_ENROLL_DATA, - payload: { - enrollmentData - } -}); - -const updatedEnrollSelected = (sections) => ({ - type: UPDATE_ENROLL_SELECTED, - payload: { - sections - } -}); - -export function fetchGradeContext() { - return (dispatch) => - axios.get('/api/grades/grades_json/').then( - (res) => { - dispatch(updateGradeContext(res.data)); - }, - (error) => console.log('An error occurred.', error) - ); -} - -export function fetchGradeClass(course) { - return (dispatch) => - axios.get(`/api/catalog/catalog_json/course/${course.courseID}/`).then( - (res) => { - const courseData = res.data; - const formattedCourse = { - id: course.id, - course: courseData.course, - title: courseData.title, - semester: course.semester, - instructor: course.instructor, - courseID: course.courseID, - sections: course.sections, - colorId: course.colorId - }; - dispatch(gradeAddCourse(formattedCourse)); - }, - (error) => console.log('An error occurred.', error) - ); -} - -export function fetchGradeData(classData) { - const promises = []; - for (const course of classData) { - const { sections } = course; - const url = `/api/grades/sections/${sections.join('&')}/`; - promises.push(axios.get(url)); - } - return (dispatch) => - axios.all(promises).then( - (data) => { - let gradesData = data.map((res, i) => { - let gradesData = res.data; - gradesData['id'] = classData[i].id; - gradesData['instructor'] = - classData[i].instructor === 'all' ? 'All Instructors' : classData[i].instructor; - gradesData['semester'] = - classData[i].semester === 'all' ? 'All Semesters' : classData[i].semester; - gradesData['colorId'] = classData[i].colorId; - return gradesData; - }); - dispatch(updateGradeData(gradesData)); - }, - (error) => console.log('An error occurred.', error) - ); -} - -export function fetchGradeSelected(updatedClass) { - const url = `/api/grades/course_grades/${updatedClass.value}/`; - return (dispatch) => - axios.get(url).then( - (res) => { - dispatch(updatedGradeSelected(res.data)); - // if (updatedClass.addSelected) { - // this.addSelected(); - // this.handleClassSelect({value: updatedClass.value, addSelected: false}); - // } - }, - (error) => console.log('An error occurred.', error) - ); -} - -export function fetchGradeFromUrl(url, navigate) { - const toUrlForm = (s) => s.replace('/', '_').toLowerCase().split(' ').join('-'); - const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1); - let courseUrls = url.split('/')[2].split('&'); - const urlData = []; - let promises = []; - for (const c of courseUrls) { - let cUrl = c.split('-'); - let semester, instructor; - if (cUrl[2] === 'all') { - semester = cUrl[2]; - instructor = cUrl.slice(3).join('-'); - } else if (cUrl[4] === '_') { - semester = capitalize(cUrl[2]) + ' ' + cUrl[3] + ' / ' + cUrl[5]; - instructor = cUrl.slice(6).join('-').replace('_', '/'); - } else { - semester = capitalize(cUrl[2]) + ' ' + cUrl[3]; - instructor = cUrl.slice(4).join('-').replace('_', '/'); - } - urlData.push({ - colorId: cUrl[0], - courseID: cUrl[1], - semester: semester, - instructor: instructor - }); - let u = `/api/grades/course_grades/${cUrl[1]}/`; - promises.push(axios.get(u)); - } - let courses = []; - let success = true; - return (dispatch) => - axios - .all(promises) - .then( - (data) => { - courses = data.map((res, i) => { - try { - let instructor = urlData[i].instructor; - let semester = urlData[i].semester; - let sections = []; - if (instructor === 'all') { - res.data.map((item, i) => (sections[i] = item.grade_id)); - } else { - let matches = []; - if (instructor.includes('/')) { - matches = res.data.filter( - (item) => - instructor === toUrlForm(item.instructor) + '-/-' + item.section_number - ); - matches.map((item, i) => (sections[i] = item.grade_id)); - instructor = matches[0].instructor + ' / ' + matches[0].section_number; - } else { - matches = res.data.filter((item) => instructor === toUrlForm(item.instructor)); - matches.map((item, i) => (sections[i] = item.grade_id)); - instructor = matches[0].instructor; - } - } - if (semester !== 'all') { - let matches = []; - if (semester.split(' ').length > 2) { - matches = res.data.filter( - (item) => - semester === - capitalize(item.semester) + ' ' + item.year + ' / ' + item.section_number - ); - } else { - matches = res.data.filter( - (item) => semester === capitalize(item.semester) + ' ' + item.year - ); - } - let allSems = matches.map((item) => item.grade_id); - sections = sections.filter((item) => allSems.includes(item)); - } - let formattedCourse = { - courseID: parseInt(urlData[i].courseID), - instructor: instructor, - semester: semester, - sections: sections - }; - formattedCourse.id = hash(formattedCourse); - formattedCourse.colorId = urlData[i].colorId; - return formattedCourse; - } catch (err) { - success = false; - navigate('/error'); - } - }); - }, - (error) => console.log('An error occurred.', error) - ) - .then(() => { - if (success) { - promises = []; - for (const course of courses) { - const u = `/api/catalog/catalog_json/course/${course.courseID}/`; - promises.push(axios.get(u)); - } - axios.all(promises).then( - (data) => { - data.map((res, i) => { - const courseData = res.data; - const course = courses[i]; - const formattedCourse = { - id: course.id, - course: courseData.course, - title: courseData.title, - semester: course.semester, - instructor: course.instructor, - courseID: course.courseID, - sections: course.sections, - colorId: course.colorId - }; - dispatch(gradeAddCourse(formattedCourse)); - }); - }, - (error) => console.log('An error occurred.', error) - ); - } - }); -} - -export function fetchEnrollContext() { - return async (dispatch, getState) => { - // Avoid fetching enrollment data twice. - if (getState().enrollment.context?.courses) { - return; - } - - const res = await axios.get('/api/enrollment/enrollment_json/'); - dispatch(updateEnrollContext(res.data)); - }; -} - -export function fetchEnrollClass(course) { - return (dispatch) => - axios.get(`/api/catalog/catalog_json/course/${course.courseID}/`).then( - (res) => { - const courseData = res.data; - const formattedCourse = { - id: course.id, - course: courseData.course, - title: courseData.title, - semester: course.semester, - instructor: course.instructor, - courseID: course.courseID, - sections: course.sections, - colorId: course.colorId - }; - dispatch(enrollAddCourse(formattedCourse)); - }, - (error) => console.log('An error occurred.', error) - ); -} - -export function fetchEnrollData(classData) { - const promises = []; - for (const course of classData) { - const { instructor, courseID, semester, sections } = course; - let url; - if (instructor === 'all') { - const [sem, year] = semester.split(' '); - url = `/api/enrollment/aggregate/${courseID}/${sem.toLowerCase()}/${year}/`; - } else { - url = `/api/enrollment/data/${sections[0]}/`; - } - promises.push(axios.get(url)); - } - return (dispatch) => - axios.all(promises).then( - (data) => { - let enrollmentData = data.map((res, i) => { - let enrollmentData = res.data; - enrollmentData['id'] = classData[i].id; - enrollmentData['colorId'] = classData[i].colorId; - return enrollmentData; - }); - dispatch(updateEnrollData(enrollmentData)); - }, - (error) => console.log('An error occurred.', error) - ); -} - -export function fetchEnrollSelected(updatedClass) { - const url = `/api/enrollment/sections/${updatedClass.value}/`; - return (dispatch) => - axios.get(url).then( - (res) => { - dispatch(updatedEnrollSelected(res.data)); - // if (updatedClass.addSelected) { - // this.addSelected(); - // this.handleClassSelect({value: updatedClass.value, addSelected: false}); - // } - }, - (error) => console.log('An error occurred.', error) - ); -} - -export function fetchEnrollFromUrl(url, navigate) { - const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1); - let courseUrls = url.split('/')[2].split('&'); - const urlData = []; - let promises = []; - for (const c of courseUrls) { - let cUrl = c.split('-'); - let semester = capitalize(cUrl[2]) + ' ' + cUrl[3]; - urlData.push({ - colorId: cUrl[0], - courseID: cUrl[1], - semester: semester, - section: cUrl[4] - }); - let u = `/api/enrollment/sections/${cUrl[1]}/`; - promises.push(axios.get(u)); - } - let courses = []; - let success = true; - return (dispatch) => - axios - .all(promises) - .then( - (data) => { - courses = data.map((res, i) => { - try { - let semester = urlData[i].semester; - let section = - urlData[i].section === 'all' ? urlData[i].section : parseInt(urlData[i].section); - let sections = [section]; - let instructor = 'all'; - let match = []; - if (section === 'all') { - match = res.data.filter( - (item) => semester === capitalize(item.semester) + ' ' + item.year - )[0]; - sections = match.sections.map((item) => item.section_id); - } else { - match = res.data.map((item) => - item.sections.filter((item) => item.section_id == section) - ); - match = match.filter((item) => item.length !== 0); - instructor = match[0][0].instructor + ' / ' + match[0][0].section_number; - } - let formattedCourse = { - courseID: parseInt(urlData[i].courseID), - instructor: instructor, - semester: semester, - sections: sections - }; - formattedCourse.id = hash(formattedCourse); - formattedCourse.colorId = urlData[i].colorId; - return formattedCourse; - } catch (err) { - success = false; - navigate('/error'); - } - }); - }, - (error) => console.log('An error occurred.', error) - ) - .then(() => { - if (success) { - promises = []; - for (const course of courses) { - const u = `/api/catalog/catalog_json/course/${course.courseID}/`; - promises.push(axios.get(u)); - } - axios.all(promises).then( - (data) => { - data.map((res, i) => { - const courseData = res.data; - const course = courses[i]; - const formattedCourse = { - id: course.id, - course: courseData.course, - title: courseData.title, - semester: course.semester, - instructor: course.instructor, - courseID: course.courseID, - sections: course.sections, - colorId: course.colorId - }; - dispatch(enrollAddCourse(formattedCourse)); - }); - }, - (error) => console.log('An error occurred.', error) - ); - } - }); -} diff --git a/frontend/src/redux/common/reducer.ts b/frontend/src/redux/common/reducer.ts index a9d361132..d00bd16a7 100644 --- a/frontend/src/redux/common/reducer.ts +++ b/frontend/src/redux/common/reducer.ts @@ -23,8 +23,7 @@ export function commonReducer(state = initialState, action: CommonAction): Commo banner: true }; case CLOSE_BANNER: - const bannerType = 'fa23recruitment'; - localStorage.setItem('bt-hide-banner', bannerType); + localStorage.setItem('bt-hide-banner', 'fa23recruitment'); return { ...state, banner: false @@ -35,8 +34,7 @@ export function commonReducer(state = initialState, action: CommonAction): Commo landingModal: true }; case CLOSE_LANDING_MODAL: - const modalType = 'sp22scheduler'; - localStorage.setItem('bt-hide-landing-modal', modalType); + localStorage.setItem('bt-hide-landing-modal', 'sp22scheduler'); return { ...state, landingModal: false diff --git a/frontend/src/redux/enrollment/actions.ts b/frontend/src/redux/enrollment/actions.ts new file mode 100644 index 000000000..41a1d897b --- /dev/null +++ b/frontend/src/redux/enrollment/actions.ts @@ -0,0 +1,245 @@ +import axios from 'axios'; +import hash from 'object-hash'; +import { NavigateFunction } from 'react-router-dom'; +import { Action } from 'redux'; +import { ThunkAction } from 'redux-thunk'; +import { + CourseSnapshotType, + FormattedCourseDataType, + FormattedCourseType, + UnformattedCourseType +} from 'redux/types'; +import { SectionType } from './types'; +import { ReduxState } from '../store'; +import { + ENROLL_ADD_COURSE, + ENROLL_REMOVE_COURSE, + ENROLL_RESET, + EnrollmentDataType, + UPDATE_ENROLL_CONTEXT, + UPDATE_ENROLL_DATA, + UPDATE_ENROLL_SELECTED +} from './types'; +import { UpdatedClassType } from 'redux/grades/types'; + +axios.defaults.baseURL = import.meta.env.PROD + ? axios.defaults.baseURL + : 'https://staging.berkeleytime.com'; + +// update enroll list +const updateEnrollContext = (data: { courses: CourseSnapshotType[] }) => ({ + type: UPDATE_ENROLL_CONTEXT, + payload: { + data + } +}); + +export const enrollReset = () => ({ + type: ENROLL_RESET +}); + +// add displayed course to the enroll page +const enrollAddCourse = (formattedCourse: FormattedCourseType) => ({ + type: ENROLL_ADD_COURSE, + payload: { + formattedCourse + } +}); + +export const enrollRemoveCourse = (id: string, color: string) => ({ + type: ENROLL_REMOVE_COURSE, + payload: { + id, + color + } +}); + +export const updateEnrollData = (enrollmentData: EnrollmentDataType[]) => ({ + type: UPDATE_ENROLL_DATA, + payload: { + enrollmentData + } +}); + +const updatedEnrollSelected = (sections: SectionType[]) => ({ + type: UPDATE_ENROLL_SELECTED, + payload: { + sections + } +}); + +export function fetchEnrollContext(): ThunkAction { + return async (dispatch, getState) => { + // Avoid fetching enrollment data twice. + if (getState().enrollment.context.courses) { + return; + } + + const res = await axios.get('/api/enrollment/enrollment_json/'); + dispatch(updateEnrollContext(res.data)); + }; +} + +export function fetchEnrollClass( + course: UnformattedCourseType +): ThunkAction { + return (dispatch) => + axios.get(`/api/catalog/catalog_json/course/${course.courseID}/`).then( + (res) => { + const courseData = res.data; + const formattedCourse = { + id: course.id, + course: courseData.course, + title: courseData.title, + semester: course.semester, + instructor: course.instructor, + courseID: course.courseID, + sections: course.sections, + colorId: course.colorId + }; + dispatch(enrollAddCourse(formattedCourse)); + }, + (error) => console.log('An error occurred.', error) + ); +} + +export function fetchEnrollData( + classData: FormattedCourseType[] +): ThunkAction { + const promises = classData.map((course) => { + const { instructor, courseID, semester, sections } = course; + let url; + if (instructor === 'all') { + const [sem, year] = semester.split(' '); + url = `/api/enrollment/aggregate/${courseID}/${sem.toLowerCase()}/${year}/`; + } else { + url = `/api/enrollment/data/${sections[0]}/`; + } + return axios.get(url); + }); + + return (dispatch) => + axios.all(promises).then( + (data) => { + const enrollmentData: EnrollmentDataType[] = data.map((res, i) => ({ + ...res.data, + id: classData[i].id, + colorId: classData[i].colorId + })); + dispatch(updateEnrollData(enrollmentData)); + }, + (error) => console.log('An error occurred.', error) + ); +} + +export function fetchEnrollSelected( + updatedClass: UpdatedClassType +): ThunkAction { + const url = `/api/enrollment/sections/${updatedClass.value}/`; + return (dispatch) => + axios.get(url).then( + (res) => { + dispatch(updatedEnrollSelected(res.data)); + }, + (error) => console.log('An error occurred.', error) + ); +} + +export function fetchEnrollFromUrl( + url: string, + navigate: NavigateFunction +): ThunkAction { + const capitalize = (string: string) => string.charAt(0).toUpperCase() + string.slice(1); + + const courseUrls = url.split('/')[2].split('&'); + const urlData = courseUrls.map((course) => { + const courseUrl = course.split('-'); + const semester = capitalize(courseUrl[2]) + ' ' + courseUrl[3]; + return { + colorId: courseUrl[0], + courseID: courseUrl[1], + semester: semester, + section: courseUrl[4] + }; + }); + const promises = urlData.map(({ courseID }) => { + const u = `/api/enrollment/sections/${courseID}/`; + return axios.get(u); + }); + + let success = true; + return (dispatch) => + axios + .all(promises) + .then((data) => { + return data.map((res, i) => { + const semester = urlData[i].semester; + const section = + urlData[i].section === 'all' ? urlData[i].section : parseInt(urlData[i].section); + let sections = [section]; + let instructor = 'all'; + let match: SectionType; + if (section === 'all') { + match = res.data.filter( + (item) => semester === capitalize(item.semester) + ' ' + item.year + )[0]; + sections = match.sections.map((item) => item.section_id); + } else { + match = res.data + .map(({ sections, ...rest }) => ({ + sections: sections.filter((item) => item.section_id === section), + ...rest + })) + .filter((item) => { + item.sections.length > 0; + })[0]; + instructor = match.sections[0].instructor + ' / ' + match.sections[0].section_number; + } + const formattedCourse = { + courseID: parseInt(urlData[i].courseID), + instructor: instructor, + semester: semester, + sections: sections + }; + return { + ...formattedCourse, + id: hash(formattedCourse), + colorId: urlData[i].colorId + }; + }); + }) + .catch((error) => { + success = false; + navigate('/error'); + console.log('An error occurred.', error); + }) + .then((courses) => { + if (success && courses) { + const promises = courses.map((course) => { + const u = `/api/catalog/catalog_json/course/${course.courseID}/`; + return axios.get(u); + }); + + axios.all(promises).then( + (data) => { + data.map((res, i) => { + const courseData = res.data; + const course = courses[i]; + const formattedCourse = { + id: course.id, + course: courseData.course, + title: courseData.title, + semester: course.semester, + instructor: course.instructor, + courseID: course.courseID, + sections: course.sections, + colorId: course.colorId + } satisfies FormattedCourseType; + dispatch(enrollAddCourse(formattedCourse)); + }); + }, + (error) => console.log('An error occurred.', error) + ); + } + }); +} diff --git a/frontend/src/redux/reducers/enrollment.js b/frontend/src/redux/enrollment/reducers.ts similarity index 70% rename from frontend/src/redux/reducers/enrollment.js rename to frontend/src/redux/enrollment/reducers.ts index 51f55b560..8fbdb0749 100644 --- a/frontend/src/redux/reducers/enrollment.js +++ b/frontend/src/redux/enrollment/reducers.ts @@ -4,11 +4,13 @@ import { UPDATE_ENROLL_DATA, UPDATE_ENROLL_SELECTED, ENROLL_REMOVE_COURSE, - ENROLL_RESET -} from '../actionTypes'; + ENROLL_RESET, + EnrollAction, + EnrollmentState +} from './types'; -const initialState = { - context: {}, +const initialState: EnrollmentState = { + context: { courses: [] }, selectedCourses: [], enrollmentData: [], graphData: [], @@ -18,7 +20,7 @@ const initialState = { usedColorIds: [] }; -export default function enrollment(state = initialState, action) { +export default function enrollment(state = initialState, action: EnrollAction) { switch (action.type) { case ENROLL_RESET: { return { @@ -51,23 +53,18 @@ export default function enrollment(state = initialState, action) { case UPDATE_ENROLL_DATA: { const { enrollmentData } = action.payload; const days = [...Array(200).keys()]; - const graphData = days.map((day) => { - const ret = { - name: day - }; - for (const enrollment of enrollmentData) { + const graphData = days.map((day) => ({ + name: day, + ...enrollmentData.reduce((enrollmentTimes, enrollment) => { const validTimes = enrollment.data.filter((time) => time.day >= 0); - const enrollmentTimes = {}; - for (const validTime of validTimes) { - enrollmentTimes[validTime.day] = validTime; - } - - if (day in enrollmentTimes) { - ret[enrollment.id] = (enrollmentTimes[day].enrolled_percent * 100).toFixed(1); - } - } - return ret; - }); + validTimes.forEach((validTime) => { + if (validTime.day === day) { + enrollmentTimes[enrollment.id] = (validTime.enrolled_percent * 100).toFixed(1); + } + }); + return enrollmentTimes; + }, {} as Record) + })); return { ...state, enrollmentData, @@ -96,11 +93,3 @@ export default function enrollment(state = initialState, action) { return state; } } -// -// capitalize(str) { -// return str.charAt(0).toUpperCase() + str.slice(1); -// } -// -// getSectionSemester(section) { -// return `${this.capitalize(section.semester)} ${section.year}`; -// } diff --git a/frontend/src/redux/enrollment/types.ts b/frontend/src/redux/enrollment/types.ts new file mode 100644 index 000000000..33efa9f44 --- /dev/null +++ b/frontend/src/redux/enrollment/types.ts @@ -0,0 +1,88 @@ +import { BaseDataType, BaseState, CourseSnapshotType, FormattedCourseType } from 'redux/types'; + +export const UPDATE_ENROLL_CONTEXT = 'UPDATE_ENROLL_CONTEXT'; +export const ENROLL_ADD_COURSE = 'ENROLL_ADD_COURSE'; +export const ENROLL_REMOVE_COURSE = 'ENROLL_REMOVE_COURSE'; +export const UPDATE_ENROLL_DATA = 'UPDATE_ENROLL_DATA'; +export const UPDATE_ENROLL_SELECTED = 'UPDATE_ENROLL_SELECTED'; +export const ENROLL_RESET = 'ENROLL_RESET'; + +export type EnrollAction = + | { + type: typeof UPDATE_ENROLL_CONTEXT; + payload: { data: { courses: CourseSnapshotType[] } }; + } + | { + type: typeof UPDATE_ENROLL_DATA; + payload: { enrollmentData: EnrollmentDataType[] }; + } + | { + type: typeof UPDATE_ENROLL_SELECTED; + payload: { + sections: SectionType[]; + }; + } + | { + type: typeof ENROLL_RESET; + } + | { + type: typeof ENROLL_ADD_COURSE; + payload: { + formattedCourse: FormattedCourseType; + }; + } + | { + type: typeof ENROLL_REMOVE_COURSE; + payload: { + id: string; + color: string; + }; + }; + +export type EnrollmentState = BaseState & { + enrollmentData: EnrollmentDataType[]; + sections: SectionType[]; + graphData: { name: number }[]; +}; + +export type EnrollmentStatusType = { + date: string; + day: number; + enrolled: number; + enrolled_max: number; + enrolled_percent: number; + waitlisted: number; + waitlisted_max: number; + waitlisted_percent: number; +}; + +export type TelebearsType = { + adj_start_date: string; + adj_start_day: number; + phase1_end_date: number; + phase1_start_date: string; + phase1_start_day: number; + phase2_end_date: number; + phase2_start_date: string; + phase2_start_day: number; +}; + +export type EnrollmentDataType = BaseDataType & { + data: EnrollmentStatusType[]; + enrolled_max: number; + enrolled_percent_max: number; + enrolled_scale_max: number; + section_id: number; + section_name: string; + telebears: TelebearsType; + title: string; + waitlisted_max: number; + waitlisted_percent_max: number; + waitlisted_scale_max: number; +}; + +export type SectionType = { + sections: { instructor: string; section_id: number; section_number: string }[]; + semester: string; + year: string; +}; diff --git a/frontend/src/redux/grades/actions.ts b/frontend/src/redux/grades/actions.ts new file mode 100644 index 000000000..f5f8091bf --- /dev/null +++ b/frontend/src/redux/grades/actions.ts @@ -0,0 +1,263 @@ +import axios from 'axios'; +import hash from 'object-hash'; +import { NavigateFunction } from 'react-router-dom'; +import { Action } from 'redux'; +import { ThunkAction } from 'redux-thunk'; +import { ReduxState } from 'redux/store'; +import { + CourseSnapshotType, + FormattedCourseDataType, + FormattedCourseType, + UnformattedCourseType +} from 'redux/types'; +import { + GRADE_ADD_COURSE, + GRADE_REMOVE_COURSE, + GRADE_RESET, + GradeSelectedType, + GradesDataType, + UPDATE_GRADE_CONTEXT, + UPDATE_GRADE_DATA, + UPDATE_GRADE_SELECTED, + UpdatedClassType +} from './types'; + +axios.defaults.baseURL = import.meta.env.PROD + ? axios.defaults.baseURL + : 'https://staging.berkeleytime.com'; + +const updateGradeContext = (data: { courses: CourseSnapshotType[] }) => ({ + type: UPDATE_GRADE_CONTEXT, + payload: { + data + } +}); + +export const gradeReset = () => ({ + type: GRADE_RESET +}); + +const gradeAddCourse = (formattedCourse: FormattedCourseType) => ({ + type: GRADE_ADD_COURSE, + payload: { + formattedCourse + } +}); + +export const gradeRemoveCourse = (id: string, color: string) => ({ + type: GRADE_REMOVE_COURSE, + payload: { + id, + color + } +}); + +const updateGradeData = (gradesData: GradesDataType[]) => ({ + type: UPDATE_GRADE_DATA, + payload: { + gradesData + } +}); + +const updatedGradeSelected = (data: GradeSelectedType[]) => ({ + type: UPDATE_GRADE_SELECTED, + payload: { + data + } +}); + +export function fetchGradeContext(): ThunkAction { + return (dispatch) => + axios.get('/api/grades/grades_json/').then( + (res) => { + dispatch(updateGradeContext(res.data)); + }, + (error) => console.log('An error occurred.', error) + ); +} + +export function fetchGradeClass( + course: UnformattedCourseType +): ThunkAction { + return (dispatch) => + axios.get(`/api/catalog/catalog_json/course/${course.courseID}/`).then( + (res) => { + const courseData = res.data; + const formattedCourse = { + id: course.id, + course: courseData.course, + title: courseData.title, + semester: course.semester, + instructor: course.instructor, + courseID: course.courseID, + sections: course.sections, + colorId: course.colorId + }; + dispatch(gradeAddCourse(formattedCourse)); + }, + (error) => console.log('An error occurred.', error) + ); +} + +export function fetchGradeData( + classData: FormattedCourseType[] +): ThunkAction { + const promises = classData.map((course) => { + const { sections } = course; + const url = `/api/grades/sections/${sections.join('&')}/`; + return axios.get(url); + }); + return (dispatch) => + axios.all(promises).then( + (data) => { + const gradesData = data.map((res, i) => ({ + ...res.data, + id: classData[i].id, + instructor: + classData[i].instructor === 'all' ? 'All Instructors' : classData[i].instructor, + semester: classData[i].semester === 'all' ? 'All Semesters' : classData[i].semester, + colorId: classData[i].colorId + })); + dispatch(updateGradeData(gradesData)); + }, + (error) => console.log('An error occurred.', error) + ); +} + +export function fetchGradeSelected( + updatedClass: UpdatedClassType +): ThunkAction { + const url = `/api/grades/course_grades/${updatedClass.value}/`; + return (dispatch) => + axios.get(url).then( + (res) => { + dispatch(updatedGradeSelected(res.data)); + }, + (error) => console.log('An error occurred.', error) + ); +} + +export function fetchGradeFromUrl( + url: string, + navigate: NavigateFunction +): ThunkAction { + const toUrlForm = (string: string) => string.replace('/', '_').toLowerCase().split(' ').join('-'); + const capitalize = (string: string) => string.charAt(0).toUpperCase() + string.slice(1); + + const courseUrls = url.split('/')[2].split('&'); + const urlData = courseUrls.map((course) => { + const courseUrl = course.split('-'); + let semester, instructor; + if (courseUrl[2] === 'all') { + semester = courseUrl[2]; + instructor = courseUrl.slice(3).join('-'); + } else if (courseUrl[4] === '_') { + semester = capitalize(courseUrl[2]) + ' ' + courseUrl[3] + ' / ' + courseUrl[5]; + instructor = courseUrl.slice(6).join('-').replace('_', '/'); + } else { + semester = capitalize(courseUrl[2]) + ' ' + courseUrl[3]; + instructor = courseUrl.slice(4).join('-').replace('_', '/'); + } + return { + colorId: courseUrl[0], + courseID: courseUrl[1], + semester: semester, + instructor: instructor + }; + }); + const promises = urlData.map(({ courseID }) => { + const u = `/api/grades/course_grades/${courseID}/`; + return axios.get(u); + }); + + let success = true; + + return (dispatch) => + axios + .all(promises) + .then((data) => { + const courses = data.map((res, i) => { + let instructor = urlData[i].instructor; + const semester = urlData[i].semester; + let sections: number[] = []; + if (instructor === 'all') { + res.data.map((item, i) => (sections[i] = item.grade_id)); + } else { + let matches = []; + if (instructor.includes('/')) { + matches = res.data.filter( + (item) => instructor === toUrlForm(item.instructor) + '-/-' + item.section_number + ); + matches.map((item, i) => (sections[i] = item.grade_id)); + instructor = matches[0].instructor + ' / ' + matches[0].section_number; + } else { + matches = res.data.filter((item) => instructor === toUrlForm(item.instructor)); + matches.map((item, i) => (sections[i] = item.grade_id)); + instructor = matches[0].instructor; + } + } + if (semester !== 'all') { + let matches = []; + if (semester.split(' ').length > 2) { + matches = res.data.filter( + (item) => + semester === + capitalize(item.semester) + ' ' + item.year + ' / ' + item.section_number + ); + } else { + matches = res.data.filter( + (item) => semester === capitalize(item.semester) + ' ' + item.year + ); + } + const allSems = matches.map((item) => item.grade_id); + sections = sections.filter((item) => allSems.includes(item)); + } + const formattedCourse = { + courseID: parseInt(urlData[i].courseID), + instructor: instructor, + semester: semester, + sections: sections + }; + + return { + ...formattedCourse, + id: hash(formattedCourse), + colorId: urlData[i].colorId + }; + }); + return courses; + }) + .catch((error) => { + success = false; + navigate('/error'); + console.log('An error occurred.', error); + }) + .then((courses) => { + if (success && courses) { + const promises = courses.map((course) => { + const u = `/api/catalog/catalog_json/course/${course.courseID}/`; + return axios.get(u); + }); + axios.all(promises).then( + (data) => { + data.map((res, i) => { + const courseData = res.data; + const course = courses[i]; + const formattedCourse = { + id: course.id, + course: courseData.course, + title: courseData.title, + semester: course.semester, + instructor: course.instructor, + courseID: course.courseID, + sections: course.sections, + colorId: course.colorId + }; + dispatch(gradeAddCourse(formattedCourse)); + }); + }, + (error) => console.log('An error occurred.', error) + ); + } + }); +} diff --git a/frontend/src/redux/reducers/grade.js b/frontend/src/redux/grades/reducers.ts similarity index 60% rename from frontend/src/redux/reducers/grade.js rename to frontend/src/redux/grades/reducers.ts index 2085274d7..39618dad7 100644 --- a/frontend/src/redux/reducers/grade.js +++ b/frontend/src/redux/grades/reducers.ts @@ -1,15 +1,17 @@ +import { GRADE } from './types'; import { - UPDATE_GRADE_CONTEXT, GRADE_ADD_COURSE, - UPDATE_GRADE_DATA, - UPDATE_GRADE_SELECTED, GRADE_REMOVE_COURSE, - GRADE_RESET -} from '../actionTypes'; -import vars from '../../utils/variables'; + GRADE_RESET, + GradeAction, + GradeState, + UPDATE_GRADE_CONTEXT, + UPDATE_GRADE_DATA, + UPDATE_GRADE_SELECTED +} from './types'; -const initialState = { - context: {}, +const initialState: GradeState = { + context: { courses: [] }, selectedCourses: [], gradesData: [], graphData: [], @@ -19,10 +21,13 @@ const initialState = { usedColorIds: [] }; -export default function grade(state = initialState, action) { +export default function grade(state = initialState, action: GradeAction): GradeState { switch (action.type) { case GRADE_RESET: { - return initialState; + return { + ...initialState, + context: state.context + }; } case UPDATE_GRADE_CONTEXT: { const { data } = action.payload; @@ -37,8 +42,8 @@ export default function grade(state = initialState, action) { } case GRADE_REMOVE_COURSE: { const { id, color } = action.payload; - let updatedCourses = state.selectedCourses.filter((classInfo) => classInfo.id !== id); - let updatedColors = state.usedColorIds.filter((c) => c !== color); + const updatedCourses = state.selectedCourses.filter((classInfo) => classInfo.id !== id); + const updatedColors = state.usedColorIds.filter((c) => c !== color); return Object.assign({}, state, { selectedCourses: updatedCourses, usedColorIds: updatedColors @@ -46,15 +51,13 @@ export default function grade(state = initialState, action) { } case UPDATE_GRADE_DATA: { const { gradesData } = action.payload; - const graphData = vars.possibleGrades.map((letterGrade) => { - const ret = { - name: letterGrade - }; - for (const grade of gradesData) { - ret[grade.id] = (grade[letterGrade].numerator / grade.denominator) * 100; - } - return ret; - }); + const graphData = Object.values(GRADE).map((letterGrade) => ({ + name: letterGrade, + ...gradesData.reduce((grades, grade) => { + grades[grade.id] = (grade[letterGrade].numerator / grade.denominator) * 100; + return grades; + }, {} as Record) + })); return { ...state, gradesData, diff --git a/frontend/src/redux/grades/types.ts b/frontend/src/redux/grades/types.ts new file mode 100644 index 000000000..e3899ecb3 --- /dev/null +++ b/frontend/src/redux/grades/types.ts @@ -0,0 +1,89 @@ +import { BaseDataType, BaseState, CourseSnapshotType, FormattedCourseType } from 'redux/types'; + +export const UPDATE_GRADE_CONTEXT = 'UPDATE_GRADE_CONTEXT'; +export const GRADE_ADD_COURSE = 'GRADE_ADD_COURSE'; +export const GRADE_REMOVE_COURSE = 'GRADE_REMOVE_COURSE'; +export const UPDATE_GRADE_DATA = 'UPDATE_GRADE_DATA'; +export const UPDATE_GRADE_SELECTED = 'UPDATE_GRADE_SELECTED'; +export const GRADE_RESET = 'GRADE_RESET'; + +export type GradeAction = + | { type: typeof UPDATE_GRADE_CONTEXT; payload: { data: { courses: CourseSnapshotType[] } } } + | { type: typeof GRADE_RESET } + | { + type: typeof GRADE_ADD_COURSE; + payload: { + formattedCourse: FormattedCourseType; + }; + } + | { + type: typeof GRADE_REMOVE_COURSE; + payload: { + id: string; + color: string; + }; + } + | { + type: typeof UPDATE_GRADE_DATA; + payload: { + gradesData: GradesDataType[]; + }; + } + | { + type: typeof UPDATE_GRADE_SELECTED; + payload: { + data: GradeSelectedType[]; + }; + }; + +export type GradeState = BaseState & { + gradesData: GradesDataType[]; + sections: GradeSelectedType[]; + graphData: { name: string }[]; +}; + +export enum GRADE { + 'A+' = 'A+', + 'A' = 'A', + 'A-' = 'A-', + 'B+' = 'B+', + 'B' = 'B', + 'B-' = 'B-', + 'C+' = 'C+', + 'C' = 'C', + 'C-' = 'C-', + 'D' = 'D', + 'F' = 'F', + 'P' = 'P', + 'NP' = 'NP' +} + +export type GradesDataType = BaseDataType & { + course_gpa: number; + course_letter: string; + denominator: number; + section_gpa: number; + section_letter: string; + semester: string; +} & { + [grade in GRADE]: { + percent: number; + numerator: number; + percentile_high: number; + percentile_low: number; + }; +}; + +export type GradeSelectedType = { + grade_id: number; + instructor: string; + section_number: string; + semester: string; + year: string; +}; + +export type UpdatedClassType = { + value: number; + label: string; + course: CourseSnapshotType; +}; diff --git a/frontend/src/redux/store.ts b/frontend/src/redux/store.ts index 70f531ab6..846c68047 100644 --- a/frontend/src/redux/store.ts +++ b/frontend/src/redux/store.ts @@ -1,11 +1,12 @@ import { createStore, combineReducers, applyMiddleware, compose } from 'redux'; import thunkMiddleware from 'redux-thunk'; -import grade from './reducers/grade'; -import enrollment from './reducers/enrollment'; +import grade from './grades/reducers'; +import enrollment from './enrollment/reducers'; import authReducer from './auth/reducer'; import { commonReducer } from './common/reducer'; +import { TypedUseSelectorHook, useSelector } from 'react-redux'; const reducer = combineReducers({ grade, @@ -16,6 +17,8 @@ const reducer = combineReducers({ export type ReduxState = ReturnType; +export const useReduxSelector: TypedUseSelectorHook = useSelector; + const composeEnhancers = (window && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose; export default createStore(reducer, composeEnhancers(applyMiddleware(thunkMiddleware))); diff --git a/frontend/src/redux/types.ts b/frontend/src/redux/types.ts new file mode 100644 index 000000000..18a63d72d --- /dev/null +++ b/frontend/src/redux/types.ts @@ -0,0 +1,35 @@ +export type CourseSnapshotType = { + abbreviation: string; + course_number: string; + id: number; +}; + +export type UnformattedCourseType = { + colorId: string; + courseID: number; + id: string; + instructor: string; + sections: (number | string)[]; + semester: string; +}; + +export type FormattedCourseDataType = { course: string; title: string }; + +export type FormattedCourseType = UnformattedCourseType & FormattedCourseDataType; + +export type BaseState = { + context: { courses: CourseSnapshotType[] }; + selectedCourses: FormattedCourseType[]; + selectPrimary: string | { value: string; label: string }; + selectSecondary: string | { value: string; label: string }; + usedColorIds: string[]; +}; + +export type BaseDataType = { + colorId: string; + course_id: number; + id: string; + instructor: string; + subtitle: string; + title: string; +}; diff --git a/frontend/src/utils/colors.ts b/frontend/src/utils/colors.ts new file mode 100644 index 000000000..fe760068c --- /dev/null +++ b/frontend/src/utils/colors.ts @@ -0,0 +1 @@ +export default ['#4EA6FB', '#6AE086', '#ED5186', '#F9E152']; diff --git a/frontend/src/utils/range.ts b/frontend/src/utils/range.ts index a84fda9f1..1655d2fb2 100644 --- a/frontend/src/utils/range.ts +++ b/frontend/src/utils/range.ts @@ -4,5 +4,5 @@ export function range(a: number, b: number, step = 1): number[] { return Array(b - a) .fill(0) - .map((_, i) => i + a); + .map((_, i) => step * i + a); } diff --git a/frontend/src/utils/utils.jsx b/frontend/src/utils/utils.tsx similarity index 84% rename from frontend/src/utils/utils.jsx rename to frontend/src/utils/utils.tsx index 8c40e07ba..d202d2f56 100644 --- a/frontend/src/utils/utils.jsx +++ b/frontend/src/utils/utils.tsx @@ -7,7 +7,7 @@ * @param {string} text text in the paragraph tag * @param {number} percentage percentage from 0.0 to 1.0 */ -function applyIndicatorPercent(text, percentage) { +function applyIndicatorPercent(text: string, percentage: number) { let theme = 'bt-indicator-red'; if (percentage < 0.34) { theme = 'bt-indicator-green'; @@ -23,7 +23,7 @@ function applyIndicatorPercent(text, percentage) { * @param {string} text text in the paragraph tag * @param {string | null} grade grade, either as a string (ex. "B+") or null */ -function applyIndicatorGrade(grade) { +function applyIndicatorGrade(grade: string | null) { if (grade === null) { return N/A; } @@ -46,7 +46,7 @@ function applyIndicatorGrade(grade) { * ex: "4.0" -> "4 Units" * "2.0 - 12.0" -> "2-12 Units" */ -function formatUnits(units) { +function formatUnits(units: string) { return `${units} Unit${units === '1.0' || units === '1' ? '' : 's'}` .replace(/.0/g, '') .replace(/ - /, '-') @@ -54,7 +54,7 @@ function formatUnits(units) { } /** Accepts a percentile between 0 and 1, converts it to a string. */ -function percentileToString(percentile) { +function percentileToString(percentile: number) { if (percentile === 1) { return '100th'; } @@ -83,7 +83,7 @@ function percentileToString(percentile) { } } -function getGradeColor(grade) { +function getGradeColor(grade: string | undefined) { if (grade === undefined) { return ''; } else if (grade.includes('A') || grade === 'P') { @@ -95,7 +95,11 @@ function getGradeColor(grade) { } } -function getEnrollmentDay(selectedPoint, telebears) { +/** + * TODO: remove the any's + * + */ +function getEnrollmentDay(selectedPoint: any, telebears: any) { let period = ''; let daysAfterPeriodStarts = 0; if (selectedPoint.day < telebears.phase2_start_day) { @@ -111,14 +115,14 @@ function getEnrollmentDay(selectedPoint, telebears) { return { period, daysAfterPeriodStarts }; } -function formatPercentage(num) { - if (num === -1) { +function formatPercentage(number: number) { + if (number === -1) { return 'N/A'; } - return (num * 100).toFixed(1).toString() + '%'; + return (number * 100).toFixed(1).toString() + '%'; } -function applyIndicatorEnrollment(enrolled, enrolledMax, percentage) { +function applyIndicatorEnrollment(enrolled: number, enrolledMax: number, percentage: number) { let theme; if (percentage < 0.34) { theme = 'bt-indicator-green'; diff --git a/frontend/src/utils/variables.js b/frontend/src/utils/variables.js deleted file mode 100644 index 39c7e0f8f..000000000 --- a/frontend/src/utils/variables.js +++ /dev/null @@ -1,640 +0,0 @@ -// -// // -// // // For notifications -// // -// -var defaultWidth = window.screen.width > 768 ? (window.screen.width * 1) / 3 : window.screen.width; - -var style = { - Wrapper: {}, - Containers: { - DefaultStyle: { - position: 'fixed', - width: defaultWidth, - padding: '10px 10px 10px 20px', - zIndex: 9998, - WebkitBoxSizing: '', - MozBoxSizing: '', - boxSizing: '', - height: 'auto', - display: 'inline-block', - border: '0', - fontSize: '14px', - WebkitFontSmoothing: 'antialiased', - fontFamily: '"Roboto","Helvetica Neue",Arial,sans-serif', - fontWeight: '400', - color: '#FFFFFF' - }, - - tl: { - top: '0px', - bottom: 'auto', - left: '0px', - right: 'auto' - }, - - tr: { - top: '0px', - bottom: 'auto', - left: 'auto', - right: '0px' - }, - - tc: { - top: '0px', - bottom: 'auto', - margin: '0 auto', - left: '50%', - marginLeft: -(defaultWidth / 2) - }, - - bl: { - top: 'auto', - bottom: '0px', - left: '0px', - right: 'auto' - }, - - br: { - top: 'auto', - bottom: '0px', - left: 'auto', - right: '0px' - }, - - bc: { - top: 'auto', - bottom: '0px', - margin: '0 auto', - left: '50%', - marginLeft: -(defaultWidth / 2) - } - }, - - NotificationItem: { - DefaultStyle: { - position: 'relative', - width: '100%', - cursor: 'pointer', - borderRadius: '4px', - fontSize: '14px', - margin: '10px 0 0', - padding: '10px', - display: 'block', - WebkitBoxSizing: 'border-box', - MozBoxSizing: 'border-box', - boxSizing: 'border-box', - opacity: 0, - transition: 'all 0.5s ease-in-out', - WebkitTransform: 'translate3d(0, 0, 0)', - transform: 'translate3d(0, 0, 0)', - willChange: 'transform, opacity', - - isHidden: { - opacity: 0 - }, - - isVisible: { - opacity: 1 - } - }, - - success: { - borderTop: 0, - backgroundColor: '#a1e82c', - WebkitBoxShadow: 0, - MozBoxShadow: 0, - boxShadow: 0 - }, - - error: { - borderTop: 0, - backgroundColor: '#fc727a', - WebkitBoxShadow: 0, - MozBoxShadow: 0, - boxShadow: 0 - }, - - warning: { - borderTop: 0, - backgroundColor: '#ffbc67', - WebkitBoxShadow: 0, - MozBoxShadow: 0, - boxShadow: 0 - }, - - info: { - borderTop: 0, - backgroundColor: '#63d8f1', - WebkitBoxShadow: 0, - MozBoxShadow: 0, - boxShadow: 0 - } - }, - - Title: { - DefaultStyle: { - fontSize: '30px', - margin: '0', - padding: 0, - fontWeight: 'bold', - color: '#FFFFFF', - display: 'block', - left: '15px', - position: 'absolute', - top: '50%', - marginTop: '-15px' - } - }, - - MessageWrapper: { - DefaultStyle: { - marginLeft: '55px', - marginRight: '30px', - padding: '0 12px 0 0', - color: '#FFFFFF', - maxWidthwidth: '89%' - } - }, - - Dismiss: { - DefaultStyle: { - fontFamily: 'inherit', - fontSize: '21px', - color: '#000', - float: 'right', - position: 'absolute', - right: '10px', - top: '50%', - marginTop: '-13px', - backgroundColor: '#FFFFFF', - display: 'block', - borderRadius: '50%', - opacity: '.4', - lineHeight: '11px', - width: '25px', - height: '25px', - outline: '0 !important', - textAlign: 'center', - padding: '6px 3px 3px 3px', - fontWeight: '300', - marginLeft: '65px' - }, - - success: { - // color: '#f0f5ea', - // backgroundColor: '#a1e82c' - }, - - error: { - // color: '#f4e9e9', - // backgroundColor: '#fc727a' - }, - - warning: { - // color: '#f9f6f0', - // backgroundColor: '#ffbc67' - }, - - info: { - // color: '#e8f0f4', - // backgroundColor: '#63d8f1' - } - }, - - Action: { - DefaultStyle: { - background: '#ffffff', - borderRadius: '2px', - padding: '6px 20px', - fontWeight: 'bold', - margin: '10px 0 0 0', - border: 0 - }, - - success: { - backgroundColor: '#a1e82c', - color: '#ffffff' - }, - - error: { - backgroundColor: '#fc727a', - color: '#ffffff' - }, - - warning: { - backgroundColor: '#ffbc67', - color: '#ffffff' - }, - - info: { - backgroundColor: '#63d8f1', - color: '#ffffff' - } - }, - - ActionWrapper: { - DefaultStyle: { - margin: 0, - padding: 0 - } - } -}; - -// -// // -// // // For tables -// // -// -const thArray = ['ID', 'Name', 'Salary', 'Country', 'City']; -const tdArray = [ - ['1', 'Dakota Rice', '$36,738', 'Niger', 'Oud-Turnhout'], - ['2', 'Minerva Hooper', '$23,789', 'Curaçao', 'Sinaai-Waas'], - ['3', 'Sage Rodriguez', '$56,142', 'Netherlands', 'Baileux'], - ['4', 'Philip Chaney', '$38,735', 'Korea, South', 'Overland Park'], - ['5', 'Doris Greene', '$63,542', 'Malawi', 'Feldkirchen in Kärnten'], - ['6', 'Mason Porter', '$78,615', 'Chile', 'Gloucester'] -]; - -// -// // -// // // For icons -// // -// -const iconsArray = [ - 'pe-7s-album', - 'pe-7s-arc', - 'pe-7s-back-2', - 'pe-7s-bandaid', - 'pe-7s-car', - 'pe-7s-diamond', - 'pe-7s-door-lock', - 'pe-7s-eyedropper', - 'pe-7s-female', - 'pe-7s-gym', - 'pe-7s-hammer', - 'pe-7s-headphones', - 'pe-7s-helm', - 'pe-7s-hourglass', - 'pe-7s-leaf', - 'pe-7s-magic-wand', - 'pe-7s-male', - 'pe-7s-map-2', - 'pe-7s-next-2', - 'pe-7s-paint-bucket', - 'pe-7s-pendrive', - 'pe-7s-photo', - 'pe-7s-piggy', - 'pe-7s-plugin', - 'pe-7s-refresh-2', - 'pe-7s-rocket', - 'pe-7s-settings', - 'pe-7s-shield', - 'pe-7s-smile', - 'pe-7s-usb', - 'pe-7s-vector', - 'pe-7s-wine', - 'pe-7s-cloud-upload', - 'pe-7s-cash', - 'pe-7s-close', - 'pe-7s-bluetooth', - 'pe-7s-cloud-download', - 'pe-7s-way', - 'pe-7s-close-circle', - 'pe-7s-id', - 'pe-7s-angle-up', - 'pe-7s-wristwatch', - 'pe-7s-angle-up-circle', - 'pe-7s-world', - 'pe-7s-angle-right', - 'pe-7s-volume', - 'pe-7s-angle-right-circle', - 'pe-7s-users', - 'pe-7s-angle-left', - 'pe-7s-user-female', - 'pe-7s-angle-left-circle', - 'pe-7s-up-arrow', - 'pe-7s-angle-down', - 'pe-7s-switch', - 'pe-7s-angle-down-circle', - 'pe-7s-scissors', - 'pe-7s-wallet', - 'pe-7s-safe', - 'pe-7s-volume2', - 'pe-7s-volume1', - 'pe-7s-voicemail', - 'pe-7s-video', - 'pe-7s-user', - 'pe-7s-upload', - 'pe-7s-unlock', - 'pe-7s-umbrella', - 'pe-7s-trash', - 'pe-7s-tools', - 'pe-7s-timer', - 'pe-7s-ticket', - 'pe-7s-target', - 'pe-7s-sun', - 'pe-7s-study', - 'pe-7s-stopwatch', - 'pe-7s-star', - 'pe-7s-speaker', - 'pe-7s-signal', - 'pe-7s-shuffle', - 'pe-7s-shopbag', - 'pe-7s-share', - 'pe-7s-server', - 'pe-7s-search', - 'pe-7s-film', - 'pe-7s-science', - 'pe-7s-disk', - 'pe-7s-ribbon', - 'pe-7s-repeat', - 'pe-7s-refresh', - 'pe-7s-add-user', - 'pe-7s-refresh-cloud', - 'pe-7s-paperclip', - 'pe-7s-radio', - 'pe-7s-note2', - 'pe-7s-print', - 'pe-7s-network', - 'pe-7s-prev', - 'pe-7s-mute', - 'pe-7s-power', - 'pe-7s-medal', - 'pe-7s-portfolio', - 'pe-7s-like2', - 'pe-7s-plus', - 'pe-7s-left-arrow', - 'pe-7s-play', - 'pe-7s-key', - 'pe-7s-plane', - 'pe-7s-joy', - 'pe-7s-photo-gallery', - 'pe-7s-pin', - 'pe-7s-phone', - 'pe-7s-plug', - 'pe-7s-pen', - 'pe-7s-right-arrow', - 'pe-7s-paper-plane', - 'pe-7s-delete-user', - 'pe-7s-paint', - 'pe-7s-bottom-arrow', - 'pe-7s-notebook', - 'pe-7s-note', - 'pe-7s-next', - 'pe-7s-news-paper', - 'pe-7s-musiclist', - 'pe-7s-music', - 'pe-7s-mouse', - 'pe-7s-more', - 'pe-7s-moon', - 'pe-7s-monitor', - 'pe-7s-micro', - 'pe-7s-menu', - 'pe-7s-map', - 'pe-7s-map-marker', - 'pe-7s-mail', - 'pe-7s-mail-open', - 'pe-7s-mail-open-file', - 'pe-7s-magnet', - 'pe-7s-loop', - 'pe-7s-look', - 'pe-7s-lock', - 'pe-7s-lintern', - 'pe-7s-link', - 'pe-7s-like', - 'pe-7s-light', - 'pe-7s-less', - 'pe-7s-keypad', - 'pe-7s-junk', - 'pe-7s-info', - 'pe-7s-home', - 'pe-7s-help2', - 'pe-7s-help1', - 'pe-7s-graph3', - 'pe-7s-graph2', - 'pe-7s-graph1', - 'pe-7s-graph', - 'pe-7s-global', - 'pe-7s-gleam', - 'pe-7s-glasses', - 'pe-7s-gift', - 'pe-7s-folder', - 'pe-7s-flag', - 'pe-7s-filter', - 'pe-7s-file', - 'pe-7s-expand1', - 'pe-7s-exapnd2', - 'pe-7s-edit', - 'pe-7s-drop', - 'pe-7s-drawer', - 'pe-7s-download', - 'pe-7s-display2', - 'pe-7s-display1', - 'pe-7s-diskette', - 'pe-7s-date', - 'pe-7s-cup', - 'pe-7s-culture', - 'pe-7s-crop', - 'pe-7s-credit', - 'pe-7s-copy-file', - 'pe-7s-config', - 'pe-7s-compass', - 'pe-7s-comment', - 'pe-7s-coffee', - 'pe-7s-cloud', - 'pe-7s-clock', - 'pe-7s-check', - 'pe-7s-chat', - 'pe-7s-cart', - 'pe-7s-camera', - 'pe-7s-call', - 'pe-7s-calculator', - 'pe-7s-browser', - 'pe-7s-box2', - 'pe-7s-box1', - 'pe-7s-bookmarks', - 'pe-7s-bicycle', - 'pe-7s-bell', - 'pe-7s-battery', - 'pe-7s-ball', - 'pe-7s-back', - 'pe-7s-attention', - 'pe-7s-anchor', - 'pe-7s-albums', - 'pe-7s-alarm', - 'pe-7s-airplay' -]; - -// -// // -// // // // For dashboard's charts -// // -// -// Data for Pie Chart -var dataPie = { - labels: ['40%', '20%', '40%'], - series: [40, 20, 40] -}; -var legendPie = { - names: ['Open', 'Bounce', 'Unsubscribe'], - types: ['info', 'danger', 'warning'] -}; - -// Data for Line Chart -var dataSales = { - labels: ['9:00AM', '12:00AM', '3:00PM', '6:00PM', '9:00PM', '12:00PM', '3:00AM', '6:00AM'], - series: [ - [287, 385, 490, 492, 554, 586, 698, 695], - [67, 152, 143, 240, 287, 335, 435, 437], - [23, 113, 67, 108, 190, 239, 307, 308] - ] -}; -var optionsSales = { - low: 0, - high: 800, - showArea: false, - height: '245px', - axisX: { - showGrid: false - }, - lineSmooth: true, - showLine: true, - showPoint: true, - fullWidth: true, - chartPadding: { - right: 50 - } -}; -var responsiveSales = [ - [ - 'screen and (max-width: 640px)', - { - axisX: { - labelInterpolationFnc: function (value) { - return value[0]; - } - } - } - ] -]; -var legendSales = { - names: ['Open', 'Click', 'Click Second Time'], - types: ['info', 'danger', 'warning'] -}; -// Data for Bar Chart -var dataBar = { - labels: ['Jan', 'Feb', 'Mar', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], - series: [ - [542, 443, 320, 780, 553, 453, 326, 434, 568, 610, 756, 895], - [412, 243, 280, 580, 453, 353, 300, 364, 368, 410, 636, 695] - ] -}; -var optionsBar = { - seriesBarDistance: 10, - axisX: { - showGrid: false - }, - height: '245px' -}; -var responsiveBar = [ - [ - 'screen and (max-width: 640px)', - { - seriesBarDistance: 5, - axisX: { - labelInterpolationFnc: function (value) { - return value[0]; - } - } - } - ] -]; -var legendBar = { - names: ['Tesla Model S', 'BMW 5 Series'], - types: ['info', 'danger'] -}; - -//Berkeleytime -var enrollment = [ - { name: 'Phase 1', percent: 50 }, - { name: 'Phase 1.5', percent: 56 }, - { name: 'Phase 2', percent: 70 }, - { name: 'Phase 2.5', percent: 90 }, - { name: 'Adjustment', percent: 95 }, - { name: 'Current', percent: 100 } -]; - -var optionsEnrollment = { - low: 0, - high: 100, - showArea: false, - height: '245px', - axisX: { - showGrid: false - }, - lineSmooth: true, - showLine: true, - showPoint: true, - fullWidth: true, - chartPadding: { - right: 50 - } -}; -var responsiveEnrollment = [ - [ - 'screen and (max-width: 640px)', - { - axisX: { - labelInterpolationFnc: function (value) { - return value[0]; - } - } - } - ] -]; - -var grades = [ - { name: 'A+', classA: 20, classB: 2 }, - { name: 'A', classA: 56, classB: 2 }, - { name: 'A-', classA: 1, classB: 2 }, - { name: 'B+', classA: 3, classB: 2 }, - { name: 'B', classA: 20, classB: 2 }, - { name: 'B-', classA: 10, classB: 2 }, - { name: 'C+', classA: 0, classB: 2 }, - { name: 'C', classA: 0, classB: 2 }, - { name: 'C-', classA: 0, classB: 2 }, - { name: 'D+', classA: 0, classB: 2 }, - { name: 'D', classA: 0, classB: 2 }, - { name: 'D-', classA: 0, classB: 2 }, - { name: 'F', classA: 0, classB: 2 } -]; - -var possibleGrades = ['A+', 'A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D', 'F', 'P', 'NP']; - -var colors = ['#4EA6FB', '#6AE086', '#ED5186', '#F9E152']; - -const vars = { - style, // For notifications (App container and Notifications view) - thArray, - tdArray, // For tables (TableList view) - iconsArray, // For icons (Icons view) - dataPie, - legendPie, - dataSales, - optionsSales, - responsiveSales, // For charts (Dashboard view) - legendSales, - dataBar, - optionsBar, - responsiveBar, - legendBar, // For charts (Dashboard view) - colors, - enrollment, - optionsEnrollment, - responsiveEnrollment, - grades, - possibleGrades -}; - -export default vars; diff --git a/frontend/src/views/Error.jsx b/frontend/src/views/Error.tsx similarity index 79% rename from frontend/src/views/Error.jsx rename to frontend/src/views/Error.tsx index 9bdb72c27..44b0fa207 100644 --- a/frontend/src/views/Error.jsx +++ b/frontend/src/views/Error.tsx @@ -1,8 +1,8 @@ -import { Container, Row, Col, ButtonToolbar, ButtonGroup } from 'react-bootstrap'; -import empty_graph from '../assets/img/images/empty-graph.png'; import { Button } from 'bt/custom'; +import { ButtonGroup, ButtonToolbar, Col, Container, Row } from 'react-bootstrap'; +import empty_graph from '../assets/img/images/empty-graph.png'; -function Error() { +export default function Error() { return (
@@ -16,7 +16,7 @@ function Error() {

Here are a couple of things you can do.

- @@ -27,5 +27,3 @@ function Error() {
); } - -export default Error; diff --git a/frontend/src/views/RedirectLink.tsx b/frontend/src/views/RedirectLink.tsx deleted file mode 100644 index ee2eeb2be..000000000 --- a/frontend/src/views/RedirectLink.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { useNavigate, useLocation } from 'react-router-dom'; - -// We must have an allowlist of redirects to prevent an attacker from arbitrarily opening external sites. -const allowedRedirects = new Map([ - ['workshop-facebook', 'https://www.facebook.com/events/314954970047731'], - [ - 'workshop-register', - 'https://docs.google.com/forms/d/e/1FAIpQLSf92FiqIwMc5et1ZSI_Rj1NGi3Y7Rx2kyMl8uQLSX1QzDIsuQ/viewform?usp=sf_link' - ] -]); - -export function Component() { - const params = new URLSearchParams(useLocation().search); - const site = params.get('site'); - if (site != null && allowedRedirects.has(site)) { - window.open(allowedRedirects.get(site), '_blank'); - } - const navigate = useNavigate(); - navigate(-1); - return null; -} diff --git a/frontend/src/views/Releases.jsx b/frontend/src/views/Releases.tsx similarity index 91% rename from frontend/src/views/Releases.jsx rename to frontend/src/views/Releases.tsx index 34c0b5b79..55e81741d 100644 --- a/frontend/src/views/Releases.jsx +++ b/frontend/src/views/Releases.tsx @@ -1,4 +1,4 @@ -import { Container, Row, Col, ButtonToolbar } from 'react-bootstrap'; +import { ButtonToolbar, Col, Container, Row } from 'react-bootstrap'; import releases from '../lib/releases'; import Log from '../components/Releases/Log';