Skip to content

Commit

Permalink
Merge branch 'master' into tim/coursebag
Browse files Browse the repository at this point in the history
  • Loading branch information
timobraz committed Oct 16, 2024
2 parents 36ab8cd + f5fda8a commit 57e7455
Show file tree
Hide file tree
Showing 14 changed files with 291 additions and 187 deletions.
52 changes: 36 additions & 16 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 @@ -68,7 +76,7 @@ router.get('/featured', async function (req: Request<never, unknown, never, Feat

// find first review with the highest score
Review.find({ [field]: req.query.id })
.sort({ score: -1 })
.sort({ reviewContent: -1, score: -1, verified: -1 })
.limit(1)
.then((reviewsCollection) => {
if (reviewsCollection) {
Expand Down Expand Up @@ -200,12 +208,6 @@ router.post('/', async function (req, res) {
// Set on server so the client can't automatically verify their own review.
req.body.verified = verifiedCount >= 3; // auto-verify if use has posted 3+ reviews

// Verify the captcha
const verifyResponse = await verifyCaptcha(req.body);
if (!verifyResponse?.success)
return res.status(400).json({ error: 'ReCAPTCHA token is invalid', data: verifyResponse });
delete req.body.captchaToken; // so it doesn't get stored in DB

//check if review already exists for same professor, course, and user
const query: ReviewFilter = {
courseID: req.body.courseID,
Expand All @@ -216,14 +218,18 @@ router.post('/', async function (req, res) {
const reviews = await Review.find(query);
if (reviews?.length > 0)
return res.status(400).json({ error: 'Review already exists for this professor and course!' });

// Verify the captcha
const verifyResponse = await verifyCaptcha(req.body);
if (!verifyResponse?.success)
return res.status(400).json({ error: 'ReCAPTCHA token is invalid', data: verifyResponse });
delete req.body.captchaToken; // so it doesn't get stored in DB

// add review to mongo
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 +243,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 +334,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;
12 changes: 12 additions & 0 deletions site/src/component/Review/Review.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
border-radius: var(--border-radius);
padding: 2rem;
margin-bottom: 3vh;
word-wrap: break-word;
overflow-wrap: break-word;
}
// name of the class/professor
.subreview-identifier {
Expand All @@ -12,6 +14,7 @@
a {
color: var(--text);
}
word-break: break-word;
}
// content of review
.subreview-content {
Expand Down Expand Up @@ -59,6 +62,8 @@
}
.subreview-info {
width: 100%;
flex: 1;
min-width: 0;
}
.subreview-details {
margin: 1vh 0;
Expand Down Expand Up @@ -212,3 +217,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

0 comments on commit 57e7455

Please sign in to comment.