From a998f1f489a56ad4529c20868133ce507053d134 Mon Sep 17 00:00:00 2001 From: Kea-Roy Ong <146872846+kea-roy@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:06:02 -0500 Subject: [PATCH 01/10] add helpful icon --- frontend/src/assets/helpful-icon.svg | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 frontend/src/assets/helpful-icon.svg diff --git a/frontend/src/assets/helpful-icon.svg b/frontend/src/assets/helpful-icon.svg new file mode 100644 index 00000000..4755ddf9 --- /dev/null +++ b/frontend/src/assets/helpful-icon.svg @@ -0,0 +1,6 @@ + + + + + + From 4dd81352aaece6be49b54884459486c3a77580c4 Mon Sep 17 00:00:00 2001 From: Kea-Roy Ong <146872846+kea-roy@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:08:22 -0500 Subject: [PATCH 02/10] update review/like/:userId - supports optional query parameter to filter by status - by default it will not filter by status - add documentation for review/like/:userId --- backend/src/app.ts | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index 27bca15b..aad2004b 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -152,23 +152,41 @@ app.get('/api/review/:status', async (req, res) => { }); /** - * Return list of reviews that user marked as helpful (like) + * review/like/:userId – Fetches reviews liked by a user. + * + * @remarks + * This endpoint retrieves reviews that a user has marked as helpful (liked). It can also filter by review status by passing in a query parameter. If no parameter is provided, no additional filter is applied. + * + * @route GET /api/review/like/:userId + * + * @status + * - 200: Successfully retrieved the liked reviews. + * - 401: Error due to unauthorized access or authentication issues. */ -// TODO: uid param is unused here but when remove it encounter 304 status and req.user is null -app.get('/api/review/like/:uid', authenticate, async (req, res) => { - if (!req.user) throw new Error('not authenticated'); - const { uid } = req.user; - const likesDoc = await likesCollection.doc(uid).get(); +app.get('/api/review/like/:userId', authenticate, async (req, res) => { + if (!req.user) { + throw new Error('not authenticated'); + } + const realUserId = req.user.uid; + const {userId} = req.params; + const statusType = req.query.status; + if (userId !== realUserId) { + res.status(401).send("Error: user is not authorized to access another user's likes"); + return; + } + const likesDoc = await likesCollection.doc(realUserId).get(); if (likesDoc.exists) { const data = likesDoc.data(); if (data) { const reviewIds = Object.keys(data); const matchingReviews: ReviewWithId[] = []; - const querySnapshot = await reviewCollection - .where(FieldPath.documentId(), 'in', reviewIds) - .where('status', '==', 'APPROVED') - .get(); + let query = reviewCollection.where(FieldPath.documentId(), 'in', reviewIds); + if (statusType) { + // filter by status if provided + query = query.where('status', '==', statusType); + } + const querySnapshot = await query.get(); querySnapshot.forEach((doc) => { const data = doc.data(); const reviewData = { ...data, date: data.date.toDate() }; From 99b3ff2af7dee1b7da6b5d14cfa2fe133a9cb3b5 Mon Sep 17 00:00:00 2001 From: Kea-Roy Ong <146872846+kea-roy@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:33:19 -0500 Subject: [PATCH 03/10] update ReviewComponent - simplified update process - removed useState for reviewData so changes to originally passed prop review will update on the component. - added helpful icon - ensure color of icon updated when liked --- frontend/src/components/Review/Review.tsx | 89 +++++++++++++---------- 1 file changed, 49 insertions(+), 40 deletions(-) diff --git a/frontend/src/components/Review/Review.tsx b/frontend/src/components/Review/Review.tsx index 70fc2184..ebc8f7bd 100644 --- a/frontend/src/components/Review/Review.tsx +++ b/frontend/src/components/Review/Review.tsx @@ -18,6 +18,7 @@ import { format } from 'date-fns'; import { makeStyles } from '@material-ui/styles'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import MoreVertIcon from '@material-ui/icons/MoreVert'; +import HelpfulIcon from '../../assets/helpful-icon.svg'; import clsx from 'clsx'; import { DetailedRating, @@ -112,6 +113,11 @@ const useStyles = makeStyles(() => ({ width: '21px', height: '21px', }, + helpfulIcon: { + width: '21px', + height: '21px', + marginRight: '5px', + }, })); const ReviewComponent = ({ @@ -126,8 +132,7 @@ const ReviewComponent = ({ setUser, showLabel, }: Props): ReactElement => { - const [reviewData, setReviewData] = useState(review); - const formattedDate = format(new Date(reviewData.date), 'MMM dd, yyyy').toUpperCase(); + const formattedDate = format(new Date(review.date), 'MMM dd, yyyy').toUpperCase(); const { root, expand, @@ -142,6 +147,7 @@ const ReviewComponent = ({ bedroomsPrice, bedPriceIcon, bedroomsPriceText, + helpfulIcon, } = useStyles(); const [expanded, setExpanded] = useState(false); const [expandedText, setExpandedText] = useState(false); @@ -151,14 +157,17 @@ const ReviewComponent = ({ const isMobile = useMediaQuery('(max-width:600px)'); const toastTime = 3500; - const onSuccessfulEdit = () => { - get(`/api/review-by-id/${reviewData.id}`, { + const updateReviewData = () => { + get(`/api/review-by-id/${review.id}`, { callback: (updatedReview: ReviewWithId) => { // Update the review state with the new data - setReviewData(updatedReview); + review = updatedReview; setToggle((cur) => !cur); }, }); + }; + const onSuccessfulEdit = () => { + updateReviewData(); if (triggerEditToast) triggerEditToast(); }; @@ -168,13 +177,13 @@ const ReviewComponent = ({ open={reviewOpen} onClose={() => setReviewOpen(false)} setOpen={setReviewOpen} - landlordId={reviewData.landlordId} + landlordId={review.landlordId} onSuccess={onSuccessfulEdit} toastTime={toastTime} - aptId={reviewData.aptId ?? ''} + aptId={review.aptId ?? ''} aptName={apt?.[0]?.name ?? ''} user={user} - initialReview={reviewData} + initialReview={review} /> ); @@ -183,12 +192,12 @@ const ReviewComponent = ({ }; //Retrieving apartment data useEffect(() => { - if (reviewData.aptId !== null) { - get(`/api/apts/${reviewData.aptId}`, { + if (review.aptId !== null) { + get(`/api/apts/${review.aptId}`, { callback: setApt, }); } - }, [reviewData]); + }, [review]); const getRatingInfo = (ratings: DetailedRating): RatingInfo[] => { return [ @@ -210,7 +219,7 @@ const ReviewComponent = ({ }; const reportAbuseHandler = async (reviewId: string) => { - const endpoint = `/api/update-review-status/${reviewData.id}/PENDING`; + const endpoint = `/api/update-review-status/${review.id}/PENDING`; if (user) { const token = await user.getIdToken(true); await axios.put(endpoint, {}, createAuthHeaders(token)); @@ -238,16 +247,16 @@ const ReviewComponent = ({ }; const landlordNotFound = useCallback(() => { - console.error('Landlord with id ' + reviewData.landlordId + ' not found.'); - }, [reviewData.landlordId]); + console.error('Landlord with id ' + review.landlordId + ' not found.'); + }, [review.landlordId]); // Fetch landlord data when the component mounts or when landlordId changes useEffect(() => { - get(`/api/landlord/${reviewData.landlordId}`, { + get(`/api/landlord/${review.landlordId}`, { callback: setLandlordData, errorHandler: landlordNotFound, }); - }, [reviewData.landlordId, landlordNotFound]); + }, [review.landlordId, landlordNotFound]); const propertyLandlordLabel = () => { return ( @@ -258,10 +267,7 @@ const ReviewComponent = ({ 0 - ? `/apartment/${reviewData.aptId}` - : `/landlord/${reviewData.landlordId}`, + to: apt.length > 0 ? `/apartment/${review.aptId}` : `/landlord/${review.landlordId}`, style: { color: 'black', textDecoration: 'underline', @@ -281,18 +287,18 @@ const ReviewComponent = ({ const bedroomsPriceLabel = () => { return ( - {reviewData.bedrooms > 0 && ( + {review.bedrooms > 0 && (
- {reviewData.bedrooms} {reviewData.bedrooms === 1 ? 'Bedroom' : 'Bedrooms'} + {review.bedrooms} {review.bedrooms === 1 ? 'Bedroom' : 'Bedrooms'}
)} - {reviewData.price > 0 && ( + {review.price > 0 && (
{' '} - {getPriceRange(reviewData.price) || 0} + {getPriceRange(review.price) || 0}
)} @@ -317,7 +323,7 @@ const ReviewComponent = ({ - + {formattedDate} - {user && reviewData.userId && user.uid === reviewData.userId && ( + {user && review.userId && user.uid === review.userId && ( openReviewModal()}> @@ -350,7 +356,7 @@ const ReviewComponent = ({ - + @@ -362,19 +368,19 @@ const ReviewComponent = ({ - {expandedText ? reviewData.reviewText : reviewData.reviewText.substring(0, 500)} - {!expandedText && reviewData.reviewText.length > 500 && '...'} - {reviewData.reviewText.length > 500 ? ( + {expandedText ? review.reviewText : review.reviewText.substring(0, 500)} + {!expandedText && review.reviewText.length > 500 && '...'} + {review.reviewText.length > 500 ? ( ) : null} - {reviewData.photos.length > 0 && ( + {review.photos.length > 0 && ( - {reviewData.photos.map((photo) => { + {review.photos.map((photo) => { return ( - From 783b045c817c8920b398473b3991c7691d29940f Mon Sep 17 00:00:00 2001 From: Kea-Roy Ong <146872846+kea-roy@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:34:33 -0500 Subject: [PATCH 04/10] fetch helpful status on open for apartment --- frontend/src/pages/ApartmentPage.tsx | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/frontend/src/pages/ApartmentPage.tsx b/frontend/src/pages/ApartmentPage.tsx index 02eab58a..aeba454f 100644 --- a/frontend/src/pages/ApartmentPage.tsx +++ b/frontend/src/pages/ApartmentPage.tsx @@ -248,6 +248,32 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { return subscribeLikes(setLikedReviews); }, []); + // Fetch the reviews that the user has liked and set the liked reviews and like statuses. + useEffect(() => { + getUser(true).then((user) => { + if (user) { + user.getIdToken(true).then((token) => { + get( + `/api/review/like/${user.uid}`, + { + callback: (reviews) => { + const likedReviewsMap: Likes = {}; + const likeStatusesMap: Likes = {}; + reviews.forEach((review) => { + likedReviewsMap[review.id] = true; + likeStatusesMap[review.id] = false; + }); + setLikedReviews(likedReviewsMap); + setLikeStatuses(likeStatusesMap); + }, + }, + createAuthHeaders(token) + ); + }); + } + }); + }, []); + useEffect(() => { get(`/api/buildings/all/${apt?.landlordId}`, { callback: setOtherproperties, From 8d73914cca22a4fc4a0ea5a8dac8f2bf6e4d5d00 Mon Sep 17 00:00:00 2001 From: Kea-Roy Ong <146872846+kea-roy@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:35:52 -0500 Subject: [PATCH 05/10] update bookmarks page to match new api request. - adds query term for APPROVED - delete from the list locally when it is disliked --- frontend/src/pages/BookmarksPage.tsx | 31 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/frontend/src/pages/BookmarksPage.tsx b/frontend/src/pages/BookmarksPage.tsx index 56407436..86408678 100644 --- a/frontend/src/pages/BookmarksPage.tsx +++ b/frontend/src/pages/BookmarksPage.tsx @@ -120,17 +120,15 @@ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { // Fetch helpful reviews data when the component mounts or when user changes or when toggle changes useEffect(() => { - const fetchLikedReviews = async () => { - if (user) { - const token = await user.getIdToken(true); + if (user) { + user.getIdToken(true).then((token) => { get( - `/api/review/like/${user.uid}`, + `/api/review/like/${user.uid}?status=APPROVED`, { callback: setHelpfulReviewsData, }, createAuthHeaders(token) ); - // this is here so we can get the token when it's fetched and not cause an unauthorized error get( savedAPI, @@ -141,9 +139,8 @@ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { }, createAuthHeaders(token) ); - } - }; - fetchLikedReviews(); + }); + } }, [user, toggle, savedAPI, sortAptsBy]); // Define the type of the properties used for sorting reviews @@ -184,13 +181,17 @@ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { const token = await user.getIdToken(true); const endpoint = dislike ? '/api/remove-like' : '/api/add-like'; await axios.post(endpoint, { reviewId }, createAuthHeaders(token)); - setHelpfulReviewsData((reviews) => - reviews.map((review) => - review.id === reviewId - ? { ...review, likes: (review.likes || defaultLikes) + offsetLikes } - : review - ) - ); + if (dislike) { + setHelpfulReviewsData((reviews) => reviews.filter((review) => review.id !== reviewId)); + } else { + setHelpfulReviewsData((reviews) => + reviews.map((review) => + review.id === reviewId + ? { ...review, likes: (review.likes || defaultLikes) + offsetLikes } + : review + ) + ); + } } catch (err) { throw new Error('Error with liking review'); } From 5b5a5d6502ca7cf1f97c627d194b0316bb1ab86d Mon Sep 17 00:00:00 2001 From: Kea-Roy Ong <146872846+kea-roy@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:36:57 -0500 Subject: [PATCH 06/10] update landlord page to fetch helpful status --- frontend/src/pages/LandlordPage.tsx | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/frontend/src/pages/LandlordPage.tsx b/frontend/src/pages/LandlordPage.tsx index 54716f12..42b82be4 100644 --- a/frontend/src/pages/LandlordPage.tsx +++ b/frontend/src/pages/LandlordPage.tsx @@ -178,6 +178,32 @@ const LandlordPage = ({ user, setUser }: Props): ReactElement => { return subscribeLikes(setLikedReviews); }, []); + // Fetch the reviews that the user has liked and set the liked reviews and like statuses. + useEffect(() => { + getUser(true).then((user) => { + if (user) { + user.getIdToken(true).then((token) => { + get( + `/api/review/like/${user.uid}`, + { + callback: (reviews) => { + const likedReviewsMap: Likes = {}; + const likeStatusesMap: Likes = {}; + reviews.forEach((review) => { + likedReviewsMap[review.id] = true; + likeStatusesMap[review.id] = false; + }); + setLikedReviews(likedReviewsMap); + setLikeStatuses(likeStatusesMap); + }, + }, + createAuthHeaders(token) + ); + }); + } + }); + }, []); + useEffect(() => { const checkIfSaved = async () => { try { From 7849c0605d4413120e677bcdf6319886049d1dba Mon Sep 17 00:00:00 2001 From: Kea-Roy Ong <146872846+kea-roy@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:38:19 -0500 Subject: [PATCH 07/10] update profile page to retreve helpful status - update pending reviews locally when like changes --- frontend/src/pages/ProfilePage.tsx | 40 ++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/ProfilePage.tsx b/frontend/src/pages/ProfilePage.tsx index 7260f48e..09ef591a 100644 --- a/frontend/src/pages/ProfilePage.tsx +++ b/frontend/src/pages/ProfilePage.tsx @@ -181,6 +181,32 @@ const ProfilePage = ({ user, setUser }: Props): ReactElement => { }); }, [user?.uid, toggle]); + // Fetch the reviews that the user has liked and set the liked reviews and like statuses. + useEffect(() => { + getUser(true).then((user) => { + if (user) { + user.getIdToken(true).then((token) => { + get( + `/api/review/like/${user.uid}`, + { + callback: (reviews) => { + const likedReviewsMap: Likes = {}; + const likeStatusesMap: Likes = {}; + reviews.forEach((review) => { + likedReviewsMap[review.id] = true; + likeStatusesMap[review.id] = false; + }); + setLikedReviews(likedReviewsMap); + setLikeStatuses(likeStatusesMap); + }, + }, + createAuthHeaders(token) + ); + }); + } + }); + }, []); + const likeHelper = (dislike = false) => { return async (reviewId: string) => { setLikeStatuses((reviews) => ({ ...reviews, [reviewId]: true })); @@ -198,7 +224,16 @@ const ProfilePage = ({ user, setUser }: Props): ReactElement => { const token = await user.getIdToken(true); const endpoint = dislike ? '/api/remove-like' : '/api/add-like'; await axios.post(endpoint, { reviewId }, createAuthHeaders(token)); - setLikedReviews((reviews) => ({ ...reviews, [reviewId]: !dislike })); + setLikedReviews((reviews) => { + return { ...reviews, [reviewId]: !dislike }; + }); + setPendingReviews((reviews) => + reviews.map((review) => + review.id === reviewId + ? { ...review, likes: (review.likes || defaultLikes) + offsetLikes } + : review + ) + ); setApprovedReviews((reviews) => reviews.map((review) => review.id === reviewId @@ -314,7 +349,7 @@ const ProfilePage = ({ user, setUser }: Props): ReactElement => { {/* Maps list of pending reviews and calls Review Component with fields for each user*/} {sortReviews(pendingReviews, 'date').map((review, index) => ( - + { {sortReviews(approvedReviews, 'date').map((review, index) => ( Date: Thu, 7 Nov 2024 22:45:38 -0500 Subject: [PATCH 08/10] ran prettier --- backend/src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index aad2004b..99036f15 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -168,7 +168,7 @@ app.get('/api/review/like/:userId', authenticate, async (req, res) => { throw new Error('not authenticated'); } const realUserId = req.user.uid; - const {userId} = req.params; + const { userId } = req.params; const statusType = req.query.status; if (userId !== realUserId) { res.status(401).send("Error: user is not authorized to access another user's likes"); From 33769be2bd1cf5106c4d94015de9bd7f350efed5 Mon Sep 17 00:00:00 2001 From: Kea-Roy Ong <146872846+kea-roy@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:55:05 -0500 Subject: [PATCH 09/10] ensure it doesn't prompt on opening of page --- frontend/src/pages/ApartmentPage.tsx | 2 +- frontend/src/pages/LandlordPage.tsx | 2 +- frontend/src/pages/ProfilePage.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/ApartmentPage.tsx b/frontend/src/pages/ApartmentPage.tsx index aeba454f..342c6d10 100644 --- a/frontend/src/pages/ApartmentPage.tsx +++ b/frontend/src/pages/ApartmentPage.tsx @@ -250,7 +250,7 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { // Fetch the reviews that the user has liked and set the liked reviews and like statuses. useEffect(() => { - getUser(true).then((user) => { + getUser(false).then((user) => { if (user) { user.getIdToken(true).then((token) => { get( diff --git a/frontend/src/pages/LandlordPage.tsx b/frontend/src/pages/LandlordPage.tsx index 42b82be4..6e105733 100644 --- a/frontend/src/pages/LandlordPage.tsx +++ b/frontend/src/pages/LandlordPage.tsx @@ -180,7 +180,7 @@ const LandlordPage = ({ user, setUser }: Props): ReactElement => { // Fetch the reviews that the user has liked and set the liked reviews and like statuses. useEffect(() => { - getUser(true).then((user) => { + getUser(false).then((user) => { if (user) { user.getIdToken(true).then((token) => { get( diff --git a/frontend/src/pages/ProfilePage.tsx b/frontend/src/pages/ProfilePage.tsx index 09ef591a..b669adb3 100644 --- a/frontend/src/pages/ProfilePage.tsx +++ b/frontend/src/pages/ProfilePage.tsx @@ -183,7 +183,7 @@ const ProfilePage = ({ user, setUser }: Props): ReactElement => { // Fetch the reviews that the user has liked and set the liked reviews and like statuses. useEffect(() => { - getUser(true).then((user) => { + getUser(false).then((user) => { if (user) { user.getIdToken(true).then((token) => { get( From 5305702619068f18d295f5aa724454981de37b13 Mon Sep 17 00:00:00 2001 From: Kea-Roy Ong <146872846+kea-roy@users.noreply.github.com> Date: Tue, 3 Dec 2024 18:48:52 -0500 Subject: [PATCH 10/10] implemented requested changes --- backend/src/app.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index f26ebc12..8d365cb5 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -181,17 +181,19 @@ app.get('/api/review/like/:userId', authenticate, async (req, res) => { if (data) { const reviewIds = Object.keys(data); const matchingReviews: ReviewWithId[] = []; - let query = reviewCollection.where(FieldPath.documentId(), 'in', reviewIds); - if (statusType) { - // filter by status if provided - query = query.where('status', '==', statusType); + if (reviewIds.length > 0) { + let query = reviewCollection.where(FieldPath.documentId(), 'in', reviewIds); + if (statusType) { + // filter by status if provided + query = query.where('status', '==', statusType); + } + const querySnapshot = await query.get(); + querySnapshot.forEach((doc) => { + const data = doc.data(); + const reviewData = { ...data, date: data.date.toDate() }; + matchingReviews.push({ ...reviewData, id: doc.id } as ReviewWithId); + }); } - const querySnapshot = await query.get(); - querySnapshot.forEach((doc) => { - const data = doc.data(); - const reviewData = { ...data, date: data.date.toDate() }; - matchingReviews.push({ ...reviewData, id: doc.id } as ReviewWithId); - }); res.status(200).send(JSON.stringify(matchingReviews)); return; }