diff --git a/.eslintrc.json b/.eslintrc.json index 832ab93b7..a19121cbe 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -25,6 +25,7 @@ "@typescript-eslint/class-name-casing": "warn", "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/no-empty-function": "warn", + "import/prefer-default-export": "off", "consistent-return": "warn", "guard-for-in": "warn", "import/extensions": "off", diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6b22a55c3..3373509a5 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -24,8 +24,9 @@ Before this PR (i.e. on the main branch), there are X linter warnings. After my -### Breaking Changes +### Breaking Changes + - Database schema change (anything that changes Firestore collection structure) - Other change that could cause problems (Detailed in notes) diff --git a/.github/workflows/ci-policies.yml b/.github/workflows/ci-policies.yml index b49fcdf9a..32844a8f6 100644 --- a/.github/workflows/ci-policies.yml +++ b/.github/workflows/ci-policies.yml @@ -8,4 +8,4 @@ jobs: - uses: actions/checkout@master - uses: cornell-dti/big-diff-warning@master env: - BOT_TOKEN: '${{ secrets.BOT_TOKEN }}' + BOT_TOKEN: "${{ secrets.BOT_TOKEN }}" diff --git a/.gitignore b/.gitignore index 041ec64c6..d3d3eb397 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ mongodb-binaries/ .git .vscode .env -db .meteor mongo/ *error.log diff --git a/app.json b/app.json index aa14cf88e..39067e114 100644 --- a/app.json +++ b/app.json @@ -1,7 +1,6 @@ { "name": "course-reviews-react-2.0", - "scripts": { - }, + "scripts": {}, "env": { "MONGO_URL": { "required": true @@ -16,9 +15,7 @@ "quantity": 1 } }, - "addons": [ - "papertrail" - ], + "addons": ["papertrail"], "buildpacks": [ { "url": "heroku/nodejs" diff --git a/client/.prettierrc.json b/client/.prettierrc.json index 66e7e941c..fa51da29e 100644 --- a/client/.prettierrc.json +++ b/client/.prettierrc.json @@ -3,4 +3,4 @@ "tabWidth": 2, "semi": false, "singleQuote": true -} \ No newline at end of file +} diff --git a/client/public/index.html b/client/public/index.html index 3afa701f4..02f3478b6 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -1,56 +1,68 @@ + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - CU Reviews - - - -
- + function gtag() { + dataLayer.push(arguments) + } + gtag('js', new Date()) + gtag('config', 'UA-111004890-1') + + + + + + + + + + + + + + + + + + + + + + + + + CU Reviews + + + +
+ diff --git a/client/public/site.webmanifest b/client/public/site.webmanifest index b20abb7cb..fa99de77d 100644 --- a/client/public/site.webmanifest +++ b/client/public/site.webmanifest @@ -1,19 +1,19 @@ { - "name": "", - "short_name": "", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" } diff --git a/client/src/App.tsx b/client/src/App.tsx index 5445c2c40..02fb4913e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,7 +1,6 @@ import { BrowserRouter as Router, Route, Switch } from 'react-router-dom' import React from 'react' - /* Importing Pages */ import { Home } from './modules/Home' import { Course } from './modules/Course' @@ -18,16 +17,16 @@ import { AuthRedirect } from './modules/AuthRedirect' import 'bootstrap/dist/css/bootstrap.min.css' import './index.css' -/** - A router is generated using the react-router-dom library. - This determines which component +/** + A router is generated using the react-router-dom library. + This determines which component the user should see based on the URL they enter. */ const App = () => { return ( -
+
diff --git a/client/src/PrivateRoute.tsx b/client/src/PrivateRoute.tsx index 33b886562..60ea22b01 100644 --- a/client/src/PrivateRoute.tsx +++ b/client/src/PrivateRoute.tsx @@ -1,10 +1,10 @@ -import * as React from "react"; -import { Redirect, Route, RouteProps } from "react-router"; +import * as React from 'react' +import { Redirect, Route, RouteProps } from 'react-router' export type ProtectedRouteProps = { - isAuthenticated: boolean; - authenticationPath: string; -} & RouteProps; + isAuthenticated: boolean + authenticationPath: string +} & RouteProps export default function ProtectedRoute({ isAuthenticated, @@ -12,8 +12,8 @@ export default function ProtectedRoute({ ...routeProps }: ProtectedRouteProps) { if (isAuthenticated) { - return ; + return } else { - return ; + return } } diff --git a/client/src/auth/auth_utils.ts b/client/src/auth/auth_utils.ts index 3d2299d05..44ab9f002 100644 --- a/client/src/auth/auth_utils.ts +++ b/client/src/auth/auth_utils.ts @@ -48,25 +48,24 @@ export function useAuthMandatoryLogin( if (!token || token === '') { signIn(redirectFrom) + } else { + axios + .post('/api/getStudentEmailByToken', { + token: token, + }) + .then((response) => { + const res = response.data + let verifiedEmail = '' + + if (response.status === 200) { + verifiedEmail = res.result + } + + setNetId(verifiedEmail.substring(0, verifiedEmail.lastIndexOf('@'))) + }) + .catch((e) => console.log(e.response)) } - axios - .post('/v2/getStudentEmailByToken', { - token: token, - }) - .then((response) => { - const res = response.data.result - var verifiedEmail = '' - - if (res.code === 200) { - console.log(res.message) - verifiedEmail = res.message - } - - setNetId(verifiedEmail.substring(0, verifiedEmail.lastIndexOf('@'))) - }) - .catch((e) => console.log(e.response)) - setToken(token) setIsAuthenticating(false) setIsLoggedIn(true) @@ -92,26 +91,29 @@ export function useAuthOptionalLogin(): [ const token = getAuthToken() if (token && token !== '') { + axios + .post('/api/getStudentEmailByToken', { + token: token, + }) + .then((response) => { + const data = response.data + var verifiedEmail = '' + + if (response.status === 200) { + verifiedEmail = data.result + } + + const netId = verifiedEmail.substring( + 0, + verifiedEmail.lastIndexOf('@') + ) + setNetId(netId) + }) + .catch((e) => console.log(e.response)) + setToken(token) setIsLoggedIn(true) } - - axios - .post('/v2/getStudentEmailByToken', { - token: token, - }) - .then((response) => { - const res = response.data.result - var verifiedEmail = '' - - if (res.code === 200) { - console.log(res.message) - verifiedEmail = res.message - } - - setNetId(verifiedEmail.substring(0, verifiedEmail.lastIndexOf('@'))) - }) - .catch((e) => console.log(e.response)) }, []) const signIn = (redirectFrom: string) => { diff --git a/client/src/index.css b/client/src/index.css index 3735fe218..4ea7a9f88 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,6 +1,35 @@ :root { --regular-weight: 400; --semi-bold-weight: 600; + --bold-weight: 700; + + --font-small-size: 0.8rem; + --font-p-size: 0.875rem; + --font-sub-heading-size: 1.5rem; + --font-heading-size: 2rem; + + --clr-black: #000000; + --clr-white: #ffffff; + --clr-gray-100: #f9f9f9; + --clr-gray-200: #ececec; + --clr-gray-300: #9fa0a1; + --clr-gray-500: #3a3a3a; + + --clr-blue-100: #f4f9fe; + --clr-blue-200: #b7d8ff; + --clr-blue-300: #65acff; + --clr-blue-400: #0076ff; + --clr-blue-500: #004ca3; + + --clr-yellow-100: #ffec5e; + --clr-yellow-200: #f8cc30; + --clr-yellow-300: #f3a953; + + --clr-green-100: #92cf48; + --clr-green-200: #5eb734; + + --clr-red-100: #ff756c; + --clr-red-200: red; } * { @@ -11,6 +40,7 @@ font-size: 100%; font: inherit; vertical-align: baseline; + max-width: 100%; } *, @@ -22,13 +52,15 @@ html, body { height: 100%; + margin: 0; + padding: 0; /* overflow: hidden; */ } body { - padding: 0; - margin: 0; - font-size: 14px; + font-size: var(--font-p-size); + font-weight: var(--regular-weight); + /* line-height: 1.2; */ } @@ -82,6 +114,7 @@ button, textarea, select { font: inherit; + background-color: transparent; } /* Avoid text overflows */ @@ -103,42 +136,25 @@ ul { .container-width { width: 100%; } -.secondary-text { - color: #4a4a4a; - font-size: 14px; - font-weight: normal; - opacity: 0.75; -} h1 { margin: 0; + font-size: var(--font-heading-size); + font-weight: var(--semi-bold-weight); } -.divider { - border-width: 1px; - color: #ddd; - margin: 5px 0 10px 0; -} - -div.sm-spacing { - padding: 10px; +h2 { + font-size: var(--font-sub-heading-size); + font-weight: var(--semi-bold-weight); } -div.med-spacing { - padding: 20px; -} - -div.lrg-spacing { - padding: 40px; +h3 { + font-size: var(--font-heading-size); + font-weight: var(--regular-weight); } ul { - background: white; + background: var(--clr-white); margin: 0; padding: 0; } - -.noLeftRightPadding { - padding-left: 0px; - padding-right: 0px; -} diff --git a/client/src/modules/Admin/Components/Admin.tsx b/client/src/modules/Admin/Components/Admin.tsx index c6b2fc6f6..afe0d3b7f 100644 --- a/client/src/modules/Admin/Components/Admin.tsx +++ b/client/src/modules/Admin/Components/Admin.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react' import { Redirect } from 'react-router-dom' +import Select from 'react-select' import axios from 'axios' @@ -26,6 +27,7 @@ export const Admin = () => { const [loadingSemester, setLoadingSemester] = useState(0) const [loadingProfs, setLoadingProfs] = useState(0) const [resettingProfs, setResettingProfs] = useState(0) + const [addSemester, setAddSemester] = useState('') const [isLoggedIn, token, isAuthenticating] = useAuthMandatoryLogin('admin') const [loading, setLoading] = useState(true) @@ -33,7 +35,7 @@ export const Admin = () => { useEffect(() => { async function confirmAdmin() { - const res = await axios.post(`/v2/tokenIsAdmin`, { + const res = await axios.post(`/api/tokenIsAdmin`, { token: token, }) @@ -52,13 +54,18 @@ export const Admin = () => { useEffect(() => { axios - .post('/v2/fetchReviewableClasses', { token: token }) + .post('/api/fetchPendingReviews', { token: token }) .then((response) => { const result = response.data.result - if (result.resCode !== 0) { - setUnapprovedReviews(result) + if (response.status === 200) { + setUnapprovedReviews( + result.filter((review: Review) => review.reported === 0) + ) + setReportedReviews( + result.filter((review: Review) => review.reported === 1) + ) } else { - console.log('Error at fetchReviewableClasses') + console.log('Error at fetchPendingReviews') } }) }, [token, isAuthenticating]) @@ -76,13 +83,12 @@ export const Admin = () => { // and changes the review with this id to visible. function approveReview(review: Review) { axios - .post('/v2/makeReviewVisible', { + .post('/api/makeReviewVisible', { review: review, token: token, }) .then((response) => { - const result = response.data.result - if (result.resCode === 1) { + if (response.status === 200) { const updatedUnapprovedReviews = removeReviewFromList( review, unapprovedReviews @@ -96,13 +102,12 @@ export const Admin = () => { // and deletes the review with this id. function removeReview(review: Review, isUnapproved: boolean) { axios - .post('/v2/removeReview', { + .post('/api/removeReview', { review: review, token: token, }) .then((response) => { - const result = response.data.result.resCode - if (result === 1) { + if (response.status === 200) { console.log('Review removed') if (isUnapproved) { const updatedUnapprovedReviews = removeReviewFromList( @@ -111,30 +116,28 @@ export const Admin = () => { ) setUnapprovedReviews(updatedUnapprovedReviews) } else { - console.log(reportedReviews) const updatedReportedReviews = removeReviewFromList( review, reportedReviews ) + setReportedReviews(updatedReportedReviews) } - } else { - console.log('Unable to remove review') } }) + .catch((e) => console.log(`Unable to remove review ${e}`)) } // Call when user asks to un-report a reported review. Accesses the Reviews database // and changes the reported flag for this review to false. function unReportReview(review: Review) { axios - .post('/v2/undoReportReview', { + .post('/api/undoReportReview', { review: review, token: token, }) .then((response) => { - const result = response.data.result.resCode - if (result === 1) { + if (response.status === 200) { console.log('Review unreported') const updatedReportedReviews = removeReviewFromList( review, @@ -150,8 +153,28 @@ export const Admin = () => { // Call when user selects "Add New Semester" button. Runs code to check the // course API for new classes and updates classes existing in the database. // sShould run once a semester, when new classes are added to the roster. - function addNewSem() { - console.log('Deprecated functionality') + function addNewSem(semester: string) { + console.log('Adding new semester...') + setDisableNewSem(true) + setDisableInit(true) + setLoadingSemester(1) + + axios + .post('/api/addNewSemester', { + semester, + token: token, + }) + .then((response) => { + const result = response.data.result + if (result === true) { + console.log('New Semester Added') + setDisableNewSem(false) + setDisableInit(false) + setLoadingSemester(2) + } else { + console.log('Unable to add new semester!') + } + }) } // Call when user selects "Initialize Database" button. Scrapes the Cornell @@ -162,7 +185,19 @@ export const Admin = () => { // NOTE: requries an initialize flag to ensure the function is only run on // a button click without this, it will run every time this component is created. function addAllCourses() { - console.log('Deprecated functionality') + console.log('Initializing database') + + setDisableInit(true) + setLoadingInit(1) + + axios.post('/api/dbInit', { token: token }).then((response) => { + if (response.status === 200) { + setDisableInit(false) + setLoadingInit(2) + } else { + console.log('Error at dbInit') + } + }) } function updateProfessors() { @@ -170,9 +205,8 @@ export const Admin = () => { setDisableInit(true) setLoadingProfs(1) - axios.post('/v2/setProfessors', { token: token }).then((response) => { - const result = response.data.result.resCode - if (result === 0) { + axios.post('/api/setProfessors', { token: token }).then((response) => { + if (response.status === 200) { console.log('Updated the professors') setDisableInit(false) setLoadingProfs(2) @@ -187,9 +221,8 @@ export const Admin = () => { setDisableInit(true) setResettingProfs(1) - axios.post('/v2/resetProfessors', { token: token }).then((response) => { - const result = response.data.result.resCode - if (result === 1) { + axios.post('/api/resetProfessors', { token: token }).then((response) => { + if (response.status === 200) { console.log('Reset all the professors to empty arrays') setDisableInit(false) setResettingProfs(2) @@ -243,6 +276,10 @@ export const Admin = () => { } } + function toSelectOptions(options: string[] | undefined) { + return options?.map((option) => ({ value: option, label: option })) || [] + } + function renderAdmin(token: string) { return (
@@ -259,13 +296,39 @@ export const Admin = () => { disabled={disableNewSem} type="button" className="btn btn-warning" - onClick={() => addNewSem()} + onClick={() => addNewSem(addSemester)} > Add New Semester + - - - +
+
+

+ Past Reviews ({courseReviews?.length}) +

+
+
+ + +
+
+
{
- - {/*
- -
*/}
diff --git a/client/src/modules/Course/Components/CourseReviews.tsx b/client/src/modules/Course/Components/CourseReviews.tsx index 7a5366814..bd38d8ee2 100644 --- a/client/src/modules/Course/Components/CourseReviews.tsx +++ b/client/src/modules/Course/Components/CourseReviews.tsx @@ -20,13 +20,14 @@ const CourseReviews = ({ } return ( -
+
{reviews.map((review) => ( ))}
diff --git a/client/src/modules/Course/Components/ReviewCard.tsx b/client/src/modules/Course/Components/ReviewCard.tsx index 2a518b388..a9cba31ed 100644 --- a/client/src/modules/Course/Components/ReviewCard.tsx +++ b/client/src/modules/Course/Components/ReviewCard.tsx @@ -4,6 +4,7 @@ import axios from 'axios' import ShowMoreText from 'react-show-more-text' import { Review as ReviewType } from 'common' + import styles from '../Styles/Review.module.css' import { getAuthToken, useAuthOptionalLogin } from '../../../auth/auth_utils' @@ -12,6 +13,7 @@ import { getAuthToken, useAuthOptionalLogin } from '../../../auth/auth_utils' type ReviewProps = { review: ReviewType + reportHandler: (review: ReviewType) => void isPreview: boolean isProfile: boolean } @@ -28,6 +30,7 @@ type ReviewProps = { */ export default function ReviewCard({ review, + reportHandler, isPreview, isProfile, }: ReviewProps): JSX.Element { @@ -48,12 +51,7 @@ export default function ReviewCard({ : styles.reviewContainerStylePending const ratings_container_color = _review.visible ? styles.ratingsContainerColor - : styles.pendingRatingsContainerColor - const rating_elem_style = _review.visible - ? styles.ratingElem + ' ' + styles.ratingElemColor - : styles.ratingElem - - const windowWidth: number = window.innerWidth + : '' function getDateString() { if (!_review.date) return '' @@ -87,12 +85,12 @@ export default function ReviewCard({ } axios - .post('/v2/updateLiked', { + .post('/api/updateLiked', { id: _review._id, token: getAuthToken(), }) .then((response) => { - setReview(response.data.result.review) + setReview(response.data.review) }) } @@ -101,7 +99,7 @@ export default function ReviewCard({ */ useEffect(() => { async function updateCourse() { - const response = await axios.post(`/v2/getCourseById`, { + const response = await axios.post(`/api/getCourseById`, { courseId: _review.class, }) const course = response.data.result @@ -116,12 +114,12 @@ export default function ReviewCard({ useEffect(() => { async function updateLiked() { - const response = await axios.post('/v2/userHasLiked', { + const response = await axios.post('/api/userHasLiked', { id: _review._id, token: getAuthToken(), }) - setLiked(response.data.result.hasLiked) + setLiked(response.data.hasLiked) } if (isLoggedIn) updateLiked() @@ -136,13 +134,7 @@ export default function ReviewCard({ if (isProfile) { return ( <> -
- {courseTitle} -
+
{courseTitle}

{courseSub?.toUpperCase() + ' ' + @@ -161,56 +153,50 @@ export default function ReviewCard({

{/* Flag */} {!isPreview && ( -
- +
+
+ +
+
+ +
)} {/* Main Section */} -
+
{/* Ratings section. */} -
+
-
- Overall{windowWidth <= 480 ? ':' : ''} +
+ Overall {_review.rating ? _review.rating : '-'} - {windowWidth <= 480 ? ( -
- ) : null}
-
- Difficulty{windowWidth <= 480 ? ':' : ''} +
+ Difficulty {_review.difficulty ? _review.difficulty : '-'} - {windowWidth <= 480 ? ( -
- ) : null}
-
- Workload{windowWidth <= 480 ? ':' : ''} +
+ Workload {_review.workload ? _review.workload : '-'} @@ -226,26 +212,26 @@ export default function ReviewCard({
- Grade: + Grade: {_review.grade && _review.grade.length !== 0 && /^([^0-9]*)$/.test(_review.grade) ? ( - {_review.grade} + {_review.grade} ) : ( - N/A + N/A )}
- Major(s): + Major(s): {_review.major && _review.major.length !== 0 ? ( _review.major.map((major, index) => ( - + {index > 0 ? ', ' : ''} {major} )) ) : ( - N/A + N/A )}
@@ -272,7 +258,7 @@ export default function ReviewCard({ {/* Like Button */} {!isPreview && ( -
+