From c812f809f071aa79c603428e6a39e7b107c2d616 Mon Sep 17 00:00:00 2001 From: Senghoung Date: Tue, 23 Jan 2024 10:02:29 -0800 Subject: [PATCH 01/30] Implement the edit review feature for user reviews --- api/src/controllers/reviews.ts | 38 +++++ site/src/component/Review/SubReview.tsx | 19 ++- site/src/component/ReviewForm/ReviewForm.tsx | 157 +++++++++++++----- .../src/component/UserReviews/UserReviews.tsx | 97 ++++++++++- 4 files changed, 264 insertions(+), 47 deletions(-) diff --git a/api/src/controllers/reviews.ts b/api/src/controllers/reviews.ts index 6fa04f00..bac1facd 100644 --- a/api/src/controllers/reviews.ts +++ b/api/src/controllers/reviews.ts @@ -336,5 +336,43 @@ router.delete('/clear', async function (req, res) { res.json({ error: 'Can only clear on development environment' }); } }); +/** + * Updating the review + */ +router.patch("/updateReview", async function (req, res) { + if (req.session.passport) { + const updatedReviewBody = req.body; + console.log("updatedReview before query: ", req.body); + // const reviewId = req.body._id; + // console.log("patch reviewId: ", reviewId); + + const query = { + _id: new ObjectID(req.body._id) + }; + + console.log(`Update Review: ${JSON.stringify(updatedReviewBody)}`); + console.log("HELLO1"); + const { _id, ...updateWithoutId } = updatedReviewBody; + console.log("HELLO2"); + console.log(`Update without _id: ${JSON.stringify(updateWithoutId)}`); + + await updateDocument( + COLLECTION_NAMES.REVIEWS, + query, + { $set: updateWithoutId } + ); + console.log("HELLO3"); + const responseWithId = { + _id: query._id, + ...updateWithoutId + }; + console.log(`response with _id: ${JSON.stringify(updateWithoutId)}`); + console.log("HELLO4"); + res.json(responseWithId); + console.log("HELLO5"); + } else { + res.status(401).json({ error: 'Must be logged in to update a review.' }); + } +}); export default router; diff --git a/site/src/component/Review/SubReview.tsx b/site/src/component/Review/SubReview.tsx index 7ec0506d..15ffc756 100644 --- a/site/src/component/Review/SubReview.tsx +++ b/site/src/component/Review/SubReview.tsx @@ -9,6 +9,7 @@ import { Link } from 'react-router-dom'; import { ReviewData, VoteRequest, CourseGQLData, ProfessorGQLData, VoteColor } from '../../types/types'; import ReportForm from '../ReportForm/ReportForm'; +import * as Icon from 'react-bootstrap-icons'; interface SubReviewProps { review: ReviewData; @@ -16,11 +17,21 @@ interface SubReviewProps { professor?: ProfessorGQLData; colors?: VoteColor; colorUpdater?: () => void; + editable?: boolean; + editReview?: ( + review: ReviewData, + course?: CourseGQLData, + professor?: ProfessorGQLData + ) => void; + } -const SubReview: FC = ({ review, course, professor, colors, colorUpdater }) => { +const SubReview: FC = ({ review, course, professor, colors, colorUpdater, editable, editReview }) => { const [score, setScore] = useState(review.score); const [cookies] = useCookies(['user']); + //Edit Review + + let upvoteClass; let downvoteClass; if (colors != undefined && colors.colors != undefined) { @@ -81,8 +92,14 @@ const SubReview: FC = ({ review, course, professor, colors, colo ); + console.log("In Sub Review, coures: " + course + " Professor: " + professor); return (
+ {editable && editReview && ( +
+ editReview(review, course, professor)} /> +
+ )}

{professor && ( diff --git a/site/src/component/ReviewForm/ReviewForm.tsx b/site/src/component/ReviewForm/ReviewForm.tsx index f4145ca8..ee4788ac 100644 --- a/site/src/component/ReviewForm/ReviewForm.tsx +++ b/site/src/component/ReviewForm/ReviewForm.tsx @@ -19,6 +19,8 @@ import { ReviewData } from '../../types/types'; interface ReviewFormProps extends ReviewProps { closeForm: () => void; + editable?: boolean; + review?: ReviewData; } const ReviewForm: FC = (props) => { @@ -42,7 +44,7 @@ const ReviewForm: FC = (props) => { 'Group projects', 'Gives good feedback', ]; - + const [reviewId, setReviewId] = useState(props.review?._id);//edit review const [professor, setProfessor] = useState(props.professor?.ucinetid || ''); const [course, setCourse] = useState(props.course?.id || ''); const [yearTaken, setYearTaken] = useState(''); @@ -81,17 +83,48 @@ const ReviewForm: FC = (props) => { props.closeForm(); } } - }, [showForm, props, cookies]); + //If editable is true + console.log("set props into the form: ", props.review) + if (props.review) { + const [year, quarter] = props.review.quarter.split(' '); + setReviewId(props.review?._id); + setQuarterTaken(quarter); + setYearTaken(year); + setGradeReceived(props.review.gradeReceived); + console.log("Grade in set data in the form: " + props.review.gradeReceived) + setDifficulty(props.review.difficulty); + setQuality(props.review.rating); + setContent(props.review?.reviewContent); + setSelectedTags(props.review?.tags); + setAttendance(props.review?.attendance); + setTakeAgain(props.review?.takeAgain); + setTextbook(props.review?.textbook); + setUserName(props.review?.userDisplay); + setProfessor(props.review?.professorID); + setCourse(props.review?.courseID); + } + }, [showForm]); 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('You must be logged in to add a review!'); + if (props.editable) { + const res = await axios.patch('/api/reviews/updateReview', review); + console.log('Inside postReview res.data: '); + console.log(res.data); + if (res.data.hasOwnProperty('error')) { + alert('You must be logged in to edit the review!'); + }else{ + //setSubmitted(false); + } } 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)); + } } }; @@ -113,35 +146,68 @@ const ReviewForm: FC = (props) => { 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 review = { - professorID: professor, - courseID: course, - userID: userID, - userDisplay: userName, - reviewContent: content, - rating: quality, - difficulty: difficulty, - timestamp: month + '/' + day + '/' + year, - gradeReceived: gradeReceived, - forCredit: true, - quarter: yearTaken + ' ' + quarterTaken, - score: 0, - takeAgain: takeAgain, - textbook: textbook, - attendance: attendance, - tags: selectedTags, - captchaToken: captchaToken, - }; - if (content.length > 500) { - setOverCharLimit(true); + if (props.editable === false) { + const date = new Date(); + const year = date.getFullYear(); + const month = (1 + date.getMonth()).toString(); + const day = date.getDate().toString(); + const review = { + professorID: professor, + courseID: course, + userID: userID, + userDisplay: userName, + reviewContent: content, + rating: quality, + difficulty: difficulty, + timestamp: month + '/' + day + '/' + year, + gradeReceived: gradeReceived, + forCredit: true, + quarter: yearTaken + ' ' + quarterTaken, + score: 0, + takeAgain: takeAgain, + textbook: textbook, + attendance: attendance, + tags: selectedTags, + captchaToken: captchaToken, + }; + if (content.length > 500) { + setOverCharLimit(true); + } else { + setOverCharLimit(false); + postReview(review); + } } else { - setOverCharLimit(false); - postReview(review); + const date = new Date(); + const year = date.getFullYear(); + const month = (1 + date.getMonth()).toString(); + const day = date.getDate().toString(); + const review = { + _id: reviewId, + professorID: professor, + courseID: course, + userID: userID, + userDisplay: userName, + reviewContent: content, + rating: quality, + difficulty: difficulty, + timestamp: month + '/' + day + '/' + year, + gradeReceived: gradeReceived, + forCredit: true, + quarter: yearTaken + ' ' + quarterTaken, + score: 0, + takeAgain: takeAgain, + textbook: textbook, + attendance: attendance, + tags: selectedTags, + verified: false, + }; + if (content.length > 500) { + setOverCharLimit(true); + } else { + setOverCharLimit(false); + postReview(review); + setSubmitted(true); + } } }; @@ -231,10 +297,17 @@ const ReviewForm: FC = (props) => { -

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

+ {props.editable ? ( +

+ Edit your review for{' '} + {props.review?.courseID + ' ' + props.review?.professorID} +

+ ) : ( +

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

+ )}
@@ -487,3 +560,5 @@ const ReviewForm: FC = (props) => { }; export default ReviewForm; + + diff --git a/site/src/component/UserReviews/UserReviews.tsx b/site/src/component/UserReviews/UserReviews.tsx index 16221ffd..f227e838 100644 --- a/site/src/component/UserReviews/UserReviews.tsx +++ b/site/src/component/UserReviews/UserReviews.tsx @@ -3,21 +3,67 @@ import { FC, useEffect, useState } from 'react'; import SubReview from '../../component/Review/SubReview'; import Button from 'react-bootstrap/Button'; import { Divider } from 'semantic-ui-react'; -import { ReviewData } from '../../../src/types/types'; +import { CourseGQLData, ProfessorGQLData, ReviewData, VoteColorsRequest } from '../../../src/types/types'; import './UserReviews.scss'; import { useCookies } from 'react-cookie'; +import { useAppDispatch, useAppSelector } from '../../store/hooks'; +import { selectReviews, setFormStatus, setReviews } from '../../store/slices/reviewSlice'; +import ReviewForm from '../ReviewForm/ReviewForm'; const UserReviews: FC = () => { - const [reviews, setReviews] = useState([]); + //const [reviews, setReviews] = useState([]); const [loaded, setLoaded] = useState(false); const [cookies] = useCookies(['user']); - + //edit review states + const [professorData] = useState > (new Map()); + const [courseData] = useState > (new Map()); + const [voteColors, setVoteColors] = useState([]); + const [courseToEdit, setCourseToEdit] = useState(); + const [professorToEdit, setProfessorToEdit] = useState(); + const [reviewToEdit, setReviewToEdit] = useState(); + const dispatch = useAppDispatch(); + const reviews = useAppSelector(selectReviews); + const getUserReviews = async () => { const response: AxiosResponse = await axios.get(`/api/reviews?userID=${cookies.user.id}`); - setReviews(response.data); + // setReviews(response.data); + dispatch(setReviews(response.data)); setLoaded(true); }; + const getColors = async (vote: VoteColorsRequest) => { + const res = await axios.patch('/api/reviews/getVoteColors', vote); + return res.data; + }; + + const updateVoteColors = async () => { + let reviewIDs = []; + for (let i = 0; i < reviews.length; i++) { + reviewIDs.push(reviews[i]._id); + } + const req = { + ids: reviewIDs as string[], + }; + let colors = await getColors(req); + setVoteColors(colors); + }; + + const getU = (id: string | undefined) => { + let temp = voteColors as Object; + let v = temp[id as keyof typeof temp] as unknown as number; + if (v == 1) { + return { + colors: [true, false], + }; + } else if (v == -1) { + return { + colors: [false, true], + }; + } + return { + colors: [false, false], + }; + }; useEffect(() => { getUserReviews(); }, []); @@ -27,6 +73,32 @@ const UserReviews: FC = () => { setReviews(reviews.filter((review) => review._id !== reviewID)); }; + //Edit Review + + const editReview = ( + review: ReviewData, + course?: CourseGQLData, + professor?: ProfessorGQLData + )=> { + setCourseToEdit(course); + setProfessorToEdit(professor); + setReviewToEdit(review); + console.log("Data received from SubReview:", { + course, + professor, + review, + }); + dispatch(setFormStatus(true)); + document.body.style.overflow = 'hidden'; + console.log('Edit Review clicked!'); + } + + const closeForm = async () => { + dispatch(setFormStatus(false)); + document.body.style.overflow = 'visible'; + await getUserReviews(); + }; + if (!loaded) { return

Loading...

; } else if (reviews.length === 0) { @@ -39,7 +111,15 @@ const UserReviews: FC = () => { {reviews.map((review, i) => (
- +
))} +

); } From 73522af2efd6ad94bafe3fd6542593b08f0fb4b8 Mon Sep 17 00:00:00 2001 From: Senghoung Date: Tue, 23 Jan 2024 10:10:48 -0800 Subject: [PATCH 02/30] Using await getUserReview() to trigger the state after user edit the review --- site/src/component/UserReviews/UserReviews.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/site/src/component/UserReviews/UserReviews.tsx b/site/src/component/UserReviews/UserReviews.tsx index f227e838..e4134434 100644 --- a/site/src/component/UserReviews/UserReviews.tsx +++ b/site/src/component/UserReviews/UserReviews.tsx @@ -11,7 +11,7 @@ import { selectReviews, setFormStatus, setReviews } from '../../store/slices/rev import ReviewForm from '../ReviewForm/ReviewForm'; const UserReviews: FC = () => { - //const [reviews, setReviews] = useState([]); + const [reviews, setReviews] = useState([]); const [loaded, setLoaded] = useState(false); const [cookies] = useCookies(['user']); //edit review states @@ -22,12 +22,12 @@ const UserReviews: FC = () => { const [professorToEdit, setProfessorToEdit] = useState(); const [reviewToEdit, setReviewToEdit] = useState(); const dispatch = useAppDispatch(); - const reviews = useAppSelector(selectReviews); + //const reviews = useAppSelector(selectReviews); const getUserReviews = async () => { const response: AxiosResponse = await axios.get(`/api/reviews?userID=${cookies.user.id}`); - // setReviews(response.data); - dispatch(setReviews(response.data)); + setReviews(response.data); + //dispatch(setReviews(response.data)); setLoaded(true); }; From 68bf590ac5261a36afdbd710361661275dd66841 Mon Sep 17 00:00:00 2001 From: Senghoung Date: Tue, 23 Jan 2024 10:39:02 -0800 Subject: [PATCH 03/30] Adding back reCaptcha in the add review props --- site/src/component/ReviewForm/ReviewForm.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/component/ReviewForm/ReviewForm.tsx b/site/src/component/ReviewForm/ReviewForm.tsx index ee4788ac..33e4110f 100644 --- a/site/src/component/ReviewForm/ReviewForm.tsx +++ b/site/src/component/ReviewForm/ReviewForm.tsx @@ -113,7 +113,7 @@ const ReviewForm: FC = (props) => { if (res.data.hasOwnProperty('error')) { alert('You must be logged in to edit the review!'); }else{ - //setSubmitted(false); + setSubmitted(false); } } else { const res = await axios.post('/api/reviews', review).catch((err) => err.response); @@ -200,6 +200,7 @@ const ReviewForm: FC = (props) => { attendance: attendance, tags: selectedTags, verified: false, + captchaToken: captchaToken, }; if (content.length > 500) { setOverCharLimit(true); @@ -525,8 +526,7 @@ const ReviewForm: FC = (props) => { setCaptchaToken(token ?? '')} - /> + onChange={(token) => setCaptchaToken(token ?? '')}/>