From e80030894b1453c943ca6e65536b7249ffc1bdb6 Mon Sep 17 00:00:00 2001 From: Advay Anand Date: Wed, 29 May 2024 11:32:55 -0700 Subject: [PATCH] Optimize upvote and downvote colors (#432) * optimize votes to avoid re-fetching votes for all reviews * implement aggregateDocuments function to get reviews from mongo, and store user vote in review object * remove unused getVoteColors function, voteColors state, and getColors function * remove unused variables deltaScore and MouseEvent e in upvote and downvote functions * move score update function to Review.tsx, create new FeaturedReviewData type, remove unused types and api routes for vote colors * move updateScore function into SubReview * change reviewID type to String in mongoose Vote schema * make userVote property optional in ReviewData type * fix type errors --- api/src/controllers/reviews.ts | 107 +++++++++++-------- api/src/models/vote.ts | 3 +- site/src/component/Review/Review.scss | 4 +- site/src/component/Review/Review.tsx | 48 +-------- site/src/component/Review/SubReview.tsx | 100 +++++++++++------ site/src/pages/SearchPage/ProfessorPopup.tsx | 6 +- site/src/types/types.ts | 15 +-- 7 files changed, 137 insertions(+), 146 deletions(-) diff --git a/api/src/controllers/reviews.ts b/api/src/controllers/reviews.ts index 8d88942f..41a1f343 100644 --- a/api/src/controllers/reviews.ts +++ b/api/src/controllers/reviews.ts @@ -105,7 +105,66 @@ router.get('/', async function (req, res) { } } - const reviews = await Review.find(query); + const pipeline = [ + { + $match: query, + }, + { + $addFields: { + _id: { + $toString: '$_id', + }, + }, + }, + { + $lookup: { + from: 'votes', + let: { + cmpUserID: req.session.passport?.user.id, + cmpReviewID: '$_id', + }, + pipeline: [ + { + $match: { + $expr: { + $and: [ + { + $eq: ['$$cmpUserID', '$userID'], + }, + { + $eq: ['$$cmpReviewID', '$reviewID'], + }, + ], + }, + }, + }, + ], + as: 'userVote', + }, + }, + { + $addFields: { + userVote: { + $cond: { + if: { + $ne: ['$userVote', []], + }, + then: { + $getField: { + field: 'score', + input: { + $first: '$userVote', + }, + }, + }, + else: 0, + }, + }, + }, + }, + ]; + + const reviews = await Review.aggregate(pipeline); if (reviews) { res.json(reviews); } else { @@ -213,7 +272,6 @@ router.patch('/vote', async function (req, res) { res.json({ deltaScore: deltaScore }); } else { //no old vote, just add in new vote data - console.log(`Voting Review ${id} with delta ${deltaScore}`); await Review.updateOne({ _id: id }, { $inc: { score: deltaScore } }); //sends in vote await new Vote({ userID: req.session.passport.user.id, reviewID: id, score: deltaScore }).save(); @@ -223,51 +281,6 @@ router.patch('/vote', async function (req, res) { // }); -/** - * Get whether or not the color of a button should be colored - */ -router.patch('/getVoteColor', async function (req, res) { - //make sure user is logged in - if (req.session?.passport != null) { - //query of the user's email and the review id - const query = { - userID: req.session.passport.user.email, - reviewID: req.body['id'], - }; - //get any existing vote in the db - const existingVote = (await Vote.find(query)) as VoteData[]; - //result an array of either length 1 or empty - if (existingVote.length == 0) { - //if empty, both should be uncolored - res.json([false, false]); - } else { - //if not empty, there is a vote, so color it accordingly - if (existingVote[0].score == 1) { - res.json([true, false]); - } else { - res.json([false, true]); - } - } - } -}); - -/** - * Get multiple review colors - */ -router.patch('/getVoteColors', async function (req, res) { - if (req.session?.passport != null) { - //query of the user's email and the review id - const ids: string[] = req.body.ids; - const votes = await Vote.find({ userID: req.session.passport.user.id, reviewID: { $in: ids } }); - const r: { [key: string]: number } = votes.reduce((acc: { [key: string]: number }, v) => { - acc[v.reviewID.toString()] = v.score; - return acc; - }, {}); - res.json(r); - } else { - res.json({}); - } -}); /* * Verify a review */ diff --git a/api/src/models/vote.ts b/api/src/models/vote.ts index 0d7f5fab..c44be808 100644 --- a/api/src/models/vote.ts +++ b/api/src/models/vote.ts @@ -6,8 +6,7 @@ const voteSchema = new mongoose.Schema({ required: true, }, reviewID: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Review', + type: String, required: true, }, timestamp: { diff --git a/site/src/component/Review/Review.scss b/site/src/component/Review/Review.scss index 16ff812c..33ab6220 100644 --- a/site/src/component/Review/Review.scss +++ b/site/src/component/Review/Review.scss @@ -109,10 +109,10 @@ color: var(--peterportal-mid-gray); } .coloredUpvote { - color: var(--peterportal-primary-color-1); + color: var(--peterportal-secondary-red); } .coloredDownvote { - color: var(--peterportal-secondary-red); + color: var(--peterportal-primary-color-1); } .add-review-btn { border: none; diff --git a/site/src/component/Review/Review.tsx b/site/src/component/Review/Review.tsx index 8b2189b7..71a10354 100644 --- a/site/src/component/Review/Review.tsx +++ b/site/src/component/Review/Review.tsx @@ -6,7 +6,7 @@ import './Review.scss'; import { selectReviews, setReviews, setFormStatus } from '../../store/slices/reviewSlice'; import { useAppSelector, useAppDispatch } from '../../store/hooks'; -import { CourseGQLData, ProfessorGQLData, ReviewData, VoteColorsRequest, VoteColor } from '../../types/types'; +import { CourseGQLData, ProfessorGQLData, ReviewData } from '../../types/types'; import { Checkbox, Dropdown } from 'semantic-ui-react'; export interface ReviewProps { @@ -23,16 +23,10 @@ enum SortingOption { const Review: FC = (props) => { const dispatch = useAppDispatch(); const reviewData = useAppSelector(selectReviews); - const [voteColors, setVoteColors] = useState([]); const [sortingOption, setSortingOption] = useState(SortingOption.MOST_RECENT); const [filterOption, setFilterOption] = useState(''); const [showOnlyVerifiedReviews, setShowOnlyVerifiedReviews] = useState(false); - const getColors = async (vote: VoteColorsRequest) => { - const res = await axios.patch('/api/reviews/getVoteColors', vote); - return res.data; - }; - const getReviews = async () => { interface paramsProps { courseID?: string; @@ -47,47 +41,10 @@ const Review: FC = (props) => { }) .then(async (res: AxiosResponse) => { const data = res.data.filter((review) => review !== null); - const reviewIDs = []; - for (let i = 0; i < data.length; i++) { - reviewIDs.push(data[i]._id); - } - const req = { - ids: reviewIDs as string[], - }; - const colors = await getColors(req); - setVoteColors(colors); dispatch(setReviews(data)); }); }; - const updateVoteColors = async () => { - const reviewIDs = []; - for (let i = 0; i < reviewData.length; i++) { - reviewIDs.push(reviewData[i]._id); - } - const req = { - ids: reviewIDs as string[], - }; - const colors = await getColors(req); - setVoteColors(colors); - }; - - const getU = (id: string | undefined) => { - const temp = voteColors as object; - const 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(() => { // prevent reviews from carrying over dispatch(setReviews([])); @@ -237,8 +194,7 @@ const Review: FC = (props) => { key={review._id} course={props.course} professor={props.professor} - colors={getU(review._id) as VoteColor} - colorUpdater={updateVoteColors} + // updateScore={(newUserVote) => updateScore(review._id!, newUserVote)} /> ))} -

{score}

-