Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to edit/delete reviews, refactor review components #487

Merged
merged 38 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
c812f80
Implement the edit review feature for user reviews
SenghoungLim Jan 23, 2024
73522af
Using await getUserReview() to trigger the state after user edit the …
SenghoungLim Jan 23, 2024
68bf590
Adding back reCaptcha in the add review props
SenghoungLim Jan 23, 2024
6fabcb3
reset setSubmited to True so it does not show form after submission
SenghoungLim Jan 23, 2024
00d2e05
Merge branch 'master' into Senghoung/edit-review-feature
SenghoungLim Feb 1, 2024
6b485c9
Finished the edit review form functionality, styling, and delete revi…
SenghoungLim Feb 13, 2024
04bf079
Clean up
SenghoungLim Feb 13, 2024
0081f8c
Merge branch 'master' into Senghoung/edit-review-feature
SenghoungLim Feb 13, 2024
026ed8b
Fixed close dialog after the first review deleted
SenghoungLim Feb 13, 2024
7b8c4fc
Fix Lint issues of using const
SenghoungLim Feb 13, 2024
ff0aed5
Fix lint issues 2
SenghoungLim Feb 13, 2024
810001c
Npm update fixing dependency
SenghoungLim Feb 13, 2024
e23a9bc
Fixed " Don't use `Object` as a type"
SenghoungLim Feb 13, 2024
e1fb4cb
disable eslint because _id is intentionally used
SenghoungLim Feb 13, 2024
7b5ad98
checkout package.json from main
SenghoungLim Apr 3, 2024
a007eb8
Checkout package-lock.json from master
SenghoungLim Apr 3, 2024
41af00c
Merge branch 'master' into Senghoung/edit-review-feature
SenghoungLim Apr 3, 2024
325d9fd
Merge branch 'master' into edit-reviews
js0mmer Oct 8, 2024
8dc2221
Update edit button
js0mmer Oct 8, 2024
cd36a82
Omit captcha from edit review form
js0mmer Oct 8, 2024
2e1db02
Fix delete & move icon
js0mmer Oct 9, 2024
f699e2f
Link user reviews to course/professor page
js0mmer Oct 10, 2024
ce7a0bf
Remove unused states from userreviews
js0mmer Oct 10, 2024
a8fe1d7
Fixing edit to work from course/professor page and review page. Tweak…
js0mmer Oct 10, 2024
9c938fc
Change authored review badge
js0mmer Oct 10, 2024
ec6b36f
Adjust button color for light theme
js0mmer Oct 10, 2024
aa71c8c
Move UserReviews component to ReviewsPage folder. Remove unused scss …
js0mmer Oct 10, 2024
824e535
Verify user wrote the review they're updating
js0mmer Oct 10, 2024
ba9c972
Merge branch 'master' into edit-reviews
js0mmer Oct 10, 2024
1cd954e
Reset form validation
js0mmer Oct 11, 2024
9b11c6a
Refactoring
js0mmer Oct 11, 2024
6c9cf87
simplify timestamp
js0mmer Oct 11, 2024
00ff016
update comments
js0mmer Oct 11, 2024
c418819
remove console log
js0mmer Oct 11, 2024
c0473ae
add hook dependencies
js0mmer Oct 11, 2024
364ea2a
send id back when adding review so edits work immediately. simplify e…
js0mmer Oct 16, 2024
0fedd5f
return updatedReviewBody
js0mmer Oct 16, 2024
984ff09
Merge branch 'master' into edit-reviews
js0mmer Oct 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions api/src/controllers/reviews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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' });
}
Expand All @@ -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 });
Expand Down Expand Up @@ -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;
7 changes: 7 additions & 0 deletions site/src/component/Review/Review.scss
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,10 @@
}
}
}

.edit-buttons {
display: flex;
flex-direction: row;
float: right;
gap: 10px;
}
19 changes: 7 additions & 12 deletions site/src/component/Review/Review.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -26,8 +26,9 @@ const Review: FC<ReviewProps> = (props) => {
const [sortingOption, setSortingOption] = useState<SortingOption>(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;
Expand All @@ -43,13 +44,13 @@ const Review: FC<ReviewProps> = (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
Expand Down Expand Up @@ -189,19 +190,13 @@ const Review: FC<ReviewProps> = (props) => {
</div>
</div>
{sortedReviews.map((review) => (
<SubReview
review={review}
key={review._id}
course={props.course}
professor={props.professor}
// updateScore={(newUserVote) => updateScore(review._id!, newUserVote)}
/>
<SubReview review={review} key={review._id} course={props.course} professor={props.professor} />
))}
<button type="button" className="add-review-btn" onClick={openReviewForm}>
+ Add Review
</button>
</div>
<ReviewForm closeForm={closeForm} {...props} />
<ReviewForm closeForm={closeForm} show={showForm} {...props} />
</>
);
}
Expand Down
67 changes: 62 additions & 5 deletions site/src/component/Review/SubReview.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
import { FC, useState } from 'react';
import { FC, useContext, useState } from 'react';
import axios from 'axios';
import './Review.scss';
import Badge from 'react-bootstrap/Badge';
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<SubReviewProps> = ({ review, course, professor }) => {
const dispatch = useAppDispatch();
const reviewData = useAppSelector(selectReviews);
const [cookies] = useCookies(['user']);
const [reportFormOpen, setReportFormOpen] = useState<boolean>(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);
Expand All @@ -48,6 +54,12 @@ const SubReview: FC<SubReviewProps> = ({ 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.');
Expand Down Expand Up @@ -103,6 +115,16 @@ const SubReview: FC<SubReviewProps> = ({ 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 = <Tooltip id="verified-tooltip">This review was verified by an administrator.</Tooltip>;
const authorOverlay = <Tooltip id="authored-tooltip">You are the author of this review.</Tooltip>;

Expand All @@ -117,12 +139,38 @@ const SubReview: FC<SubReviewProps> = ({ review, course, professor }) => {

const authorBadge = (
<OverlayTrigger overlay={authorOverlay}>
<PersonFill size={25} fill="green"></PersonFill>
<Badge variant="success" style={{ padding: '1px' }}>
<PersonFill size={14}></PersonFill>
</Badge>
</OverlayTrigger>
);

return (
<div className="subreview">
{cookies.user?.id === review.userID && (
<div className="edit-buttons">
<Button variant={buttonVariant} className="edit-button" onClick={openReviewForm}>
<PencilFill width="16" height="16" />
</Button>
<Button variant="danger" className="delete-button" onClick={() => setShowDeleteModal(true)}>
<TrashFill width="16" height="16" />
</Button>
<Modal className="ppc-modal" show={showDeleteModal} onHide={() => setShowDeleteModal(false)} centered>
<Modal.Header closeButton>
<h2>Delete Review</h2>
</Modal.Header>
<Modal.Body>Deleting a review will remove it permanently. Are you sure you want to proceed?</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={() => setShowDeleteModal(false)}>
Cancel
</Button>
<Button variant="danger" onClick={() => deleteReview(review._id!)}>
Delete
</Button>
</Modal.Footer>
</Modal>
</div>
)}
<div>
<h3 className="subreview-identifier">
{professor && (
Expand All @@ -137,7 +185,8 @@ const SubReview: FC<SubReviewProps> = ({ review, course, professor }) => {
)}
{!course && !professor && (
<div>
{review.courseID} {review.professorID}
<Link to={{ pathname: `/course/${review.courseID}` }}>{review.courseID}</Link>{' '}
<Link to={{ pathname: `/professor/${review.professorID}` }}>{review.professorID}</Link>
</div>
)}
</h3>
Expand Down Expand Up @@ -219,6 +268,14 @@ const SubReview: FC<SubReviewProps> = ({ review, course, professor }) => {
reviewContent={review.reviewContent}
closeForm={() => setReportFormOpen(false)}
/>
<ReviewForm
course={course}
professor={professor}
reviewToEdit={review}
closeForm={closeReviewForm}
show={showReviewForm}
editing
/>
</div>
</div>
);
Expand Down
32 changes: 10 additions & 22 deletions site/src/component/ReviewForm/ReviewForm.scss
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,15 @@
cursor: pointer;
}
}
.review-form-grade {
margin-left: 1vw;
}
.review-form-section {
background-color: var(--overlay3);
border-radius: var(--border-radius);
padding: 1rem;
}
.review-form-row {
display: flex;
flex-wrap: wrap;
column-gap: 16px;
}
.review-form-center {
display: flex;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
}
}
Loading
Loading