From e7c7d6d46924aa4928ad54acecad7da1c896f03a Mon Sep 17 00:00:00 2001 From: Timofey Obraztsov <35554964+timobraz@users.noreply.github.com> Date: Wed, 16 Oct 2024 12:11:45 -0700 Subject: [PATCH 1/6] Prerequisite unmet popup in search results (#458) * feat: Unmatched prerequisite courses have a popup when searched for * fix: changed icon per ethan's request * style: moved search prereq popover to the bottom * fix: lint issues * multiple plans work now and logic reused * type fixing --- .../SearchHitContainer/SearchHitContainer.tsx | 18 ++++++++++++++++-- site/src/pages/RoadmapPage/Course.tsx | 4 ++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/site/src/component/SearchHitContainer/SearchHitContainer.tsx b/site/src/component/SearchHitContainer/SearchHitContainer.tsx index d45ed1f6..5ff94efa 100644 --- a/site/src/component/SearchHitContainer/SearchHitContainer.tsx +++ b/site/src/component/SearchHitContainer/SearchHitContainer.tsx @@ -7,12 +7,13 @@ import { SearchIndex, CourseGQLData, ProfessorGQLData } from '../../types/types' import SearchPagination from '../SearchPagination/SearchPagination'; import noResultsImg from '../../asset/no-results-crop.webp'; import { useFirstRender } from '../../hooks/firstRenderer'; +import { validateCourse } from '../../helpers/planner'; // TODO: CourseHitItem and ProfessorHitem should not need index // investigate: see if you can refactor respective components to use course id/ucinetid for keys instead then remove index from props interface SearchHitContainerProps { index: SearchIndex; - CourseHitItem: FC; + CourseHitItem: FC; ProfessorHitItem?: FC; } @@ -22,8 +23,21 @@ const SearchResults = ({ CourseHitItem, ProfessorHitItem, }: Required & { results: CourseGQLData[] | ProfessorGQLData[] }) => { + const roadmap = useAppSelector((state) => state.roadmap); + const allExistingCourses = roadmap?.plans[roadmap.currentPlanIndex].content.yearPlans.flatMap((yearPlan) => + yearPlan.quarters.flatMap((quarter) => + quarter.courses.map((course) => course.department + ' ' + course.courseNumber), + ), + ); if (index === 'courses') { - return (results as CourseGQLData[]).map((course, i) => ); + return (results as CourseGQLData[]).map((course, i) => { + const requiredCourses = Array.from( + validateCourse(new Set(allExistingCourses), course.prerequisiteTree, new Set(), course.corequisites), + ); + return ( + 0 && { requiredCourses })} /> + ); + }); } else { return (results as ProfessorGQLData[]).map((professor, i) => ( diff --git a/site/src/pages/RoadmapPage/Course.tsx b/site/src/pages/RoadmapPage/Course.tsx index 8f8178f9..7bc5142a 100644 --- a/site/src/pages/RoadmapPage/Course.tsx +++ b/site/src/pages/RoadmapPage/Course.tsx @@ -11,6 +11,7 @@ import ThemeContext from '../../style/theme-context'; interface CourseProps extends CourseGQLData { requiredCourses?: string[]; + unmatchedPrerequisites?: string[]; onDelete?: () => void; } @@ -29,7 +30,6 @@ const Course: FC = (props) => { terms, onDelete, } = props; - const CoursePopover = ( @@ -81,7 +81,7 @@ const Course: FC = (props) => { , {minUnits === maxUnits ? minUnits : `${minUnits}-${maxUnits}`} units - + {requiredCourses && ( From 7a23351d183b5cf3ba69154f8dad28df03d0cf1e Mon Sep 17 00:00:00 2001 From: Timofey Obraztsov <35554964+timobraz@users.noreply.github.com> Date: Wed, 16 Oct 2024 12:23:04 -0700 Subject: [PATCH 2/6] Featured review criteria changes (#482) * conditonally render transfer missing prereqs * featured algorithm changes --- api/src/controllers/reviews.ts | 2 +- site/src/pages/SearchPage/ProfessorPopup.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/api/src/controllers/reviews.ts b/api/src/controllers/reviews.ts index 467daad6..cda813dd 100644 --- a/api/src/controllers/reviews.ts +++ b/api/src/controllers/reviews.ts @@ -68,7 +68,7 @@ router.get('/featured', async function (req: Request { if (reviewsCollection) { diff --git a/site/src/pages/SearchPage/ProfessorPopup.tsx b/site/src/pages/SearchPage/ProfessorPopup.tsx index 825b3d99..ed83bbc5 100644 --- a/site/src/pages/SearchPage/ProfessorPopup.tsx +++ b/site/src/pages/SearchPage/ProfessorPopup.tsx @@ -54,7 +54,9 @@ const ProfessorPopup: FC = () => { }, { title: `Featured Review`, - content: featured ? `For ${featured.courseID}: ${featured.reviewContent}` : 'No Reviews Yet!', + content: featured + ? `For ${featured.courseID}: ${featured.reviewContent.length > 0 ? featured.reviewContent : 'Rating of ' + featured.rating + '/5'}` + : 'No Reviews Yet!', }, ]; From cc7cd3ed026c644d24023863fcc0c763268a656b Mon Sep 17 00:00:00 2001 From: Jacob Sommer Date: Wed, 16 Oct 2024 12:54:43 -0700 Subject: [PATCH 3/6] Add ability to edit/delete reviews, refactor review components (#487) * Implement the edit review feature for user reviews * Using await getUserReview() to trigger the state after user edit the review * Adding back reCaptcha in the add review props * reset setSubmited to True so it does not show form after submission * Finished the edit review form functionality, styling, and delete review confirmation * Clean up * Fixed close dialog after the first review deleted * Fix Lint issues of using const * Fix lint issues 2 * Npm update fixing dependency * Fixed " Don't use `Object` as a type" * disable eslint because _id is intentionally used * checkout package.json from main * Checkout package-lock.json from master * Update edit button * Omit captcha from edit review form * Fix delete & move icon * Link user reviews to course/professor page * Remove unused states from userreviews * Fixing edit to work from course/professor page and review page. Tweak review form styles * Change authored review badge * Adjust button color for light theme * Move UserReviews component to ReviewsPage folder. Remove unused scss file * Verify user wrote the review they're updating * Reset form validation * Refactoring * simplify timestamp * update comments * remove console log * add hook dependencies * send id back when adding review so edits work immediately. simplify editReview reducer * return updatedReviewBody --------- Co-authored-by: Senghoung --- api/src/controllers/reviews.ts | 37 ++- site/src/component/Review/Review.scss | 7 + site/src/component/Review/Review.tsx | 19 +- site/src/component/Review/SubReview.tsx | 67 ++++- site/src/component/ReviewForm/ReviewForm.scss | 32 +-- site/src/component/ReviewForm/ReviewForm.tsx | 229 ++++++++++-------- site/src/pages/ReviewsPage/ReviewsPage.scss | 0 .../ReviewsPage}/UserReviews.scss | 6 - .../ReviewsPage}/UserReviews.tsx | 26 +- site/src/pages/ReviewsPage/index.tsx | 3 +- site/src/store/slices/reviewSlice.ts | 6 +- 11 files changed, 257 insertions(+), 175 deletions(-) delete mode 100644 site/src/pages/ReviewsPage/ReviewsPage.scss rename site/src/{component/UserReviews => pages/ReviewsPage}/UserReviews.scss (67%) rename site/src/{component/UserReviews => pages/ReviewsPage}/UserReviews.tsx (57%) diff --git a/api/src/controllers/reviews.ts b/api/src/controllers/reviews.ts index cda813dd..1fa0e41f 100644 --- a/api/src/controllers/reviews.ts +++ b/api/src/controllers/reviews.ts @@ -10,6 +10,14 @@ import Vote from '../models/vote'; import Report from '../models/report'; const router = express.Router(); +async function userWroteReview(userID: string | undefined, reviewID: string) { + if (!userID) { + return false; + } + + return await Review.exists({ _id: reviewID, userID: userID }); +} + /** * Get review scores */ @@ -220,10 +228,7 @@ router.post('/', async function (req, res) { req.body.userDisplay = req.body.userDisplay === 'Anonymous Peter' ? 'Anonymous Peter' : req.session.passport.user.name; req.body.userID = req.session.passport.user.id; - await new Review(req.body).save(); - - // echo back body - res.json(req.body); + res.json(await new Review(req.body).save()); } catch { res.json({ error: 'Cannot add review' }); } @@ -237,11 +242,7 @@ router.post('/', async function (req, res) { */ router.delete('/', async (req, res) => { try { - const checkUser = async () => { - return await Review.findOne({ _id: req.body.id as string, userID: req.session.passport?.user.id }).exec(); - }; - - if (req.session.passport?.admin || (await checkUser())) { + if (req.session.passport?.admin || (await userWroteReview(req.session.passport?.user.id, req.body.id))) { await Review.deleteOne({ _id: req.body.id }); await Vote.deleteMany({ reviewID: req.body.id }); await Report.deleteMany({ reviewID: req.body.id }); @@ -332,5 +333,23 @@ router.delete('/clear', async function (req, res) { res.json({ error: 'Can only clear on development environment' }); } }); +/** + * Updating the review + */ +router.patch('/update', async function (req, res) { + if (req.session.passport) { + if (!(await userWroteReview(req.session.passport.user.id, req.body._id))) { + return res.json({ error: 'You are not the author of this review.' }); + } + + const updatedReviewBody = req.body; + + const { _id, ...updateWithoutId } = updatedReviewBody; + await Review.updateOne({ _id }, updateWithoutId); + res.json(updatedReviewBody); + } else { + res.status(401).json({ error: 'Must be logged in to update a review.' }); + } +}); export default router; diff --git a/site/src/component/Review/Review.scss b/site/src/component/Review/Review.scss index 33ab6220..14028e93 100644 --- a/site/src/component/Review/Review.scss +++ b/site/src/component/Review/Review.scss @@ -212,3 +212,10 @@ } } } + +.edit-buttons { + display: flex; + flex-direction: row; + float: right; + gap: 10px; +} diff --git a/site/src/component/Review/Review.tsx b/site/src/component/Review/Review.tsx index 71a10354..bd0a77aa 100644 --- a/site/src/component/Review/Review.tsx +++ b/site/src/component/Review/Review.tsx @@ -1,4 +1,4 @@ -import { FC, useState, useEffect } from 'react'; +import { FC, useState, useEffect, useCallback } from 'react'; import axios, { AxiosResponse } from 'axios'; import SubReview from './SubReview'; import ReviewForm from '../ReviewForm/ReviewForm'; @@ -26,8 +26,9 @@ const Review: FC = (props) => { const [sortingOption, setSortingOption] = useState(SortingOption.MOST_RECENT); const [filterOption, setFilterOption] = useState(''); const [showOnlyVerifiedReviews, setShowOnlyVerifiedReviews] = useState(false); + const showForm = useAppSelector((state) => state.review.formOpen); - const getReviews = async () => { + const getReviews = useCallback(async () => { interface paramsProps { courseID?: string; professorID?: string; @@ -43,13 +44,13 @@ const Review: FC = (props) => { const data = res.data.filter((review) => review !== null); dispatch(setReviews(data)); }); - }; + }, [dispatch, props.course, props.professor]); useEffect(() => { // prevent reviews from carrying over dispatch(setReviews([])); getReviews(); - }, [props.course?.id, props.professor?.ucinetid]); + }, [dispatch, getReviews]); let sortedReviews: ReviewData[]; // filter verified if option is set @@ -189,19 +190,13 @@ const Review: FC = (props) => { {sortedReviews.map((review) => ( - updateScore(review._id!, newUserVote)} - /> + ))} - + ); } diff --git a/site/src/component/Review/SubReview.tsx b/site/src/component/Review/SubReview.tsx index c5b88e8d..aac27ab2 100644 --- a/site/src/component/Review/SubReview.tsx +++ b/site/src/component/Review/SubReview.tsx @@ -1,4 +1,4 @@ -import { FC, useState } from 'react'; +import { FC, useContext, useState } from 'react'; import axios from 'axios'; import './Review.scss'; import Badge from 'react-bootstrap/Badge'; @@ -6,17 +6,19 @@ import Tooltip from 'react-bootstrap/Tooltip'; import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; import { useCookies } from 'react-cookie'; import { Link } from 'react-router-dom'; -import { PersonFill } from 'react-bootstrap-icons'; +import { PencilFill, PersonFill, TrashFill } from 'react-bootstrap-icons'; import { ReviewData, VoteRequest, CourseGQLData, ProfessorGQLData } from '../../types/types'; import ReportForm from '../ReportForm/ReportForm'; import { selectReviews, setReviews } from '../../store/slices/reviewSlice'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; +import { Button, Modal } from 'react-bootstrap'; +import ThemeContext from '../../style/theme-context'; +import ReviewForm from '../ReviewForm/ReviewForm'; interface SubReviewProps { review: ReviewData; course?: CourseGQLData; professor?: ProfessorGQLData; - updateScore?: (newUserVote: number) => void; } const SubReview: FC = ({ review, course, professor }) => { @@ -24,6 +26,10 @@ const SubReview: FC = ({ review, course, professor }) => { const reviewData = useAppSelector(selectReviews); const [cookies] = useCookies(['user']); const [reportFormOpen, setReportFormOpen] = useState(false); + const { darkMode } = useContext(ThemeContext); + const buttonVariant = darkMode ? 'dark' : 'secondary'; + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [showReviewForm, setShowReviewForm] = useState(false); const sendVote = async (voteReq: VoteRequest) => { const res = await axios.patch('/api/reviews/vote', voteReq); @@ -48,6 +54,12 @@ const SubReview: FC = ({ review, course, professor }) => { ); }; + const deleteReview = async (reviewID: string) => { + await axios.delete('/api/reviews', { data: { id: reviewID } }); + dispatch(setReviews(reviewData.filter((review) => review._id !== reviewID))); + setShowDeleteModal(false); + }; + const upvote = async () => { if (cookies.user === undefined) { alert('You must be logged in to vote.'); @@ -103,6 +115,16 @@ const SubReview: FC = ({ review, course, professor }) => { setReportFormOpen(true); }; + const openReviewForm = () => { + setShowReviewForm(true); + document.body.style.overflow = 'hidden'; + }; + + const closeReviewForm = () => { + setShowReviewForm(false); + document.body.style.overflow = 'visible'; + }; + const badgeOverlay = This review was verified by an administrator.; const authorOverlay = You are the author of this review.; @@ -117,12 +139,38 @@ const SubReview: FC = ({ review, course, professor }) => { const authorBadge = ( - + + + ); return (
+ {cookies.user?.id === review.userID && ( +
+ + + setShowDeleteModal(false)} centered> + +

Delete Review

+
+ Deleting a review will remove it permanently. Are you sure you want to proceed? + + + + +
+
+ )}

{professor && ( @@ -137,7 +185,8 @@ const SubReview: FC = ({ review, course, professor }) => { )} {!course && !professor && (
- {review.courseID} {review.professorID} + {review.courseID}{' '} + {review.professorID}
)}

@@ -219,6 +268,14 @@ const SubReview: FC = ({ review, course, professor }) => { reviewContent={review.reviewContent} closeForm={() => setReportFormOpen(false)} /> +
); diff --git a/site/src/component/ReviewForm/ReviewForm.scss b/site/src/component/ReviewForm/ReviewForm.scss index eb7136b7..613d1914 100644 --- a/site/src/component/ReviewForm/ReviewForm.scss +++ b/site/src/component/ReviewForm/ReviewForm.scss @@ -64,9 +64,6 @@ cursor: pointer; } } -.review-form-grade { - margin-left: 1vw; -} .review-form-section { background-color: var(--overlay3); border-radius: var(--border-radius); @@ -74,6 +71,8 @@ } .review-form-row { display: flex; + flex-wrap: wrap; + column-gap: 16px; } .review-form-center { display: flex; @@ -100,7 +99,7 @@ margin: auto auto 25px auto; cursor: pointer; } -.review-form-submit { +.review-form-captcha-submit { display: flex; justify-content: space-between; align-items: center; @@ -124,6 +123,13 @@ top: 2vh; } +.review-form-submit-cancel-buttons { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 14px; +} + @media only screen and (max-width: 800px) { .review-form { height: 80vh; @@ -134,22 +140,4 @@ .review-form-taken { margin-bottom: 3vh; } - - .review-form-submit { - flex-direction: column; - - button { - margin-top: 1vh; - } - } -} - -@media only screen and (max-width: 1700px) { - .review-form-row { - flex-direction: column; - } - - .review-form-grade { - margin-left: 0; - } } diff --git a/site/src/component/ReviewForm/ReviewForm.tsx b/site/src/component/ReviewForm/ReviewForm.tsx index 3c04270f..21c5d832 100644 --- a/site/src/component/ReviewForm/ReviewForm.tsx +++ b/site/src/component/ReviewForm/ReviewForm.tsx @@ -11,9 +11,8 @@ import Button from 'react-bootstrap/Button'; import RangeSlider from 'react-bootstrap-range-slider'; import Modal from 'react-bootstrap/Modal'; import ReCAPTCHA from 'react-google-recaptcha'; - -import { addReview } from '../../store/slices/reviewSlice'; -import { useAppDispatch, useAppSelector } from '../../store/hooks'; +import { addReview, editReview } from '../../store/slices/reviewSlice'; +import { useAppDispatch } from '../../store/hooks'; import { ReviewProps } from '../Review/Review'; import { ReviewData } from '../../types/types'; import ThemeContext from '../../style/theme-context'; @@ -21,9 +20,19 @@ import { quarterNames } from '../../helpers/planner'; interface ReviewFormProps extends ReviewProps { closeForm: () => void; + show: boolean; + editing?: boolean; + reviewToEdit?: ReviewData; } -const ReviewForm: FC = (props) => { +const ReviewForm: FC = ({ + closeForm, + show, + editing, + reviewToEdit, + professor: professorProp, + course: courseProp, +}) => { const dispatch = useAppDispatch(); const grades = ['A+', 'A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'D-', 'F', 'P', 'NP']; const tags = [ @@ -44,57 +53,62 @@ const ReviewForm: FC = (props) => { 'Group projects', 'Gives good feedback', ]; - - const [professor, setProfessor] = useState(props.professor?.ucinetid || ''); - const [course, setCourse] = useState(props.course?.id || ''); - const [yearTaken, setYearTaken] = useState(''); - const [quarterTaken, setQuarterTaken] = useState(''); - const [gradeReceived, setGradeReceived] = useState(''); - const [userID, setUserID] = useState(''); - const [userName, setUserName] = useState('Anonymous Peter'); - const [content, setContent] = useState(''); - const [quality, setQuality] = useState(3); - const [difficulty, setDifficulty] = useState(3); - const [takeAgain, setTakeAgain] = useState(false); - const [textbook, setTextbook] = useState(false); - const [attendance, setAttendance] = useState(false); - const [selectedTags, setSelectedTags] = useState([]); + const [professor, setProfessor] = useState(professorProp?.ucinetid ?? reviewToEdit?.professorID ?? ''); + const [course, setCourse] = useState(courseProp?.id ?? reviewToEdit?.courseID ?? ''); + const [yearTakenDefault, quarterTakenDefault] = reviewToEdit?.quarter.split(' ') ?? ['', '']; + const [yearTaken, setYearTaken] = useState(yearTakenDefault); + const [quarterTaken, setQuarterTaken] = useState(quarterTakenDefault); + const [gradeReceived, setGradeReceived] = useState(reviewToEdit?.gradeReceived ?? ''); + const [content, setContent] = useState(reviewToEdit?.reviewContent ?? ''); + const [quality, setQuality] = useState(reviewToEdit?.rating ?? 3); + const [difficulty, setDifficulty] = useState(reviewToEdit?.difficulty ?? 3); + const [takeAgain, setTakeAgain] = useState(reviewToEdit?.takeAgain ?? false); + const [textbook, setTextbook] = useState(reviewToEdit?.textbook ?? false); + const [attendance, setAttendance] = useState(reviewToEdit?.attendance ?? false); + const [selectedTags, setSelectedTags] = useState(reviewToEdit?.tags ?? []); const [captchaToken, setCaptchaToken] = useState(''); const [submitted, setSubmitted] = useState(false); - const [overCharLimit, setOverCharLimit] = useState(false); const [cookies] = useCookies(['user']); + const userID: string = cookies.user?.id; + const [userName, setUserName] = useState(reviewToEdit?.userDisplay ?? cookies.user?.name); const [validated, setValidated] = useState(false); - const showForm = useAppSelector((state) => state.review.formOpen); const { darkMode } = useContext(ThemeContext); useEffect(() => { - // get user info from cookie - if (cookies.user) { - setUserID(cookies.user.id); - setUserName(cookies.user.name); - } - }, [cookies]); - - useEffect(() => { - // upon opening this form - if (showForm) { + if (show) { + // form opened // if not logged in, close the form if (cookies.user === undefined) { alert('You must be logged in to add a review!'); - props.closeForm(); + closeForm(); } + + setValidated(false); + setSubmitted(false); } - }, [showForm, props, cookies]); + // we do not want closeForm to be a dependency, would cause unexpected behavior since the closeForm function is different on each render + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [show]); const postReview = async (review: ReviewData) => { - const res = await axios.post('/api/reviews', review).catch((err) => err.response); - if (res.status === 400) { - alert(res.data.error ?? 'You have already submitted a review for this course/professor'); - } else if (res.data.error !== undefined) { - alert('Error submitting review'); + if (editing) { + const res = await axios.patch('/api/reviews/update', review); + if (res.data.error !== undefined) { + alert(res.data.error); + } else { + setSubmitted(true); + dispatch(editReview(res.data)); + } } else { - setSubmitted(true); - dispatch(addReview(res.data)); + const res = await axios.post('/api/reviews', review).catch((err) => err.response); + if (res.status === 400) { + alert(res.data.error ?? 'You have already submitted a review for this course/professor'); + } else if (res.data.error !== undefined) { + alert('You must be logged in to add a review!'); + } else { + setSubmitted(true); + dispatch(addReview(res.data)); + } } }; @@ -109,43 +123,38 @@ const ReviewForm: FC = (props) => { setValidated(true); // do not proceed if not valid - if (valid === false) { + if (!valid) { return; } - if (!captchaToken) { + // check if CAPTCHA is completed for new reviews (captcha omitted for editing) + if (!editing && !captchaToken) { alert('Please complete the CAPTCHA'); return; } - - const date = new Date(); - const year = date.getFullYear(); - const month = (1 + date.getMonth()).toString(); - const day = date.getDate().toString(); + const timestamp = new Date().toLocaleDateString('en-US'); const review = { + _id: reviewToEdit?._id, professorID: professor, courseID: course, - userID: userID, + userID, userDisplay: userName, reviewContent: content, rating: quality, - difficulty: difficulty, - timestamp: month + '/' + day + '/' + year, - gradeReceived: gradeReceived, + difficulty, + timestamp, + gradeReceived, forCredit: true, quarter: yearTaken + ' ' + quarterTaken, score: 0, - takeAgain: takeAgain, - textbook: textbook, - attendance: attendance, + takeAgain, + textbook, + attendance, tags: selectedTags, - captchaToken: captchaToken, + verified: false, + captchaToken, }; - if (content.length > 500) { - setOverCharLimit(true); - } else { - setOverCharLimit(false); - postReview(review); - } + + postReview(review); }; const selectTag = (tag: string) => { @@ -168,7 +177,7 @@ const ReviewForm: FC = (props) => { }; // select instructor if in course context - const instructorSelect = props.course && ( + const instructorSelect = courseProp && ( Taken With = (props) => { defaultValue="" required onChange={(e) => setProfessor(e.target.value)} + value={professor} > - {Object.keys(props.course?.instructors).map((ucinetid) => { - const name = props.course?.instructors[ucinetid].shortenedName; + {Object.keys(courseProp?.instructors).map((ucinetid) => { + const name = courseProp?.instructors[ucinetid].shortenedName; return ( ); // select course if in professor context - const courseSelect = props.professor && ( + const courseSelect = professorProp && ( Course Taken = (props) => { defaultValue="" required onChange={(e) => setCourse(e.target.value)} + value={course} > - {Object.keys(props.professor?.courses).map((courseID) => { + {Object.keys(professorProp?.courses).map((courseID) => { const name = - props.professor?.courses[courseID].department + ' ' + props.professor?.courses[courseID].courseNumber; + professorProp?.courses[courseID].department + ' ' + professorProp?.courses[courseID].courseNumber; return ( ); + function editReviewHeading() { + if (!courseProp && !professorProp) { + return `Edit your review for ${reviewToEdit?.courseID} ${reviewToEdit?.professorID}`; + } else if (courseProp) { + return `Edit your review for ${courseProp?.department} ${courseProp?.courseNumber}`; + } else { + return `Edit your review for ${professorProp?.name}`; + } + } + const reviewForm = (
-

- It's your turn to review{' '} - {props.course ? props.course?.department + ' ' + props.course?.courseNumber : props.professor?.name} -

+ {editing ? ( +

{editReviewHeading()}

+ ) : ( +

+ It's your turn to review{' '} + {courseProp ? courseProp?.department + ' ' + courseProp?.courseNumber : professorProp?.name} +

+ )}
@@ -245,7 +270,7 @@ const ReviewForm: FC = (props) => {
{instructorSelect} {courseSelect} - + Grade = (props) => { defaultValue="" required onChange={(e) => setGradeReceived(e.target.value)} + value={gradeReceived} >