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

[Ready for Review] Fix Helpful Reviews #377

Merged
merged 13 commits into from
Dec 4, 2024
38 changes: 28 additions & 10 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
kea-roy marked this conversation as resolved.
Show resolved Hide resolved
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() };
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/assets/helpful-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
112 changes: 55 additions & 57 deletions frontend/src/components/Review/Review.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import HeartRating from '../utils/HeartRating';
import { format } from 'date-fns';
import { makeStyles } from '@material-ui/styles';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import HelpfulIcon from '../../assets/helpful-icon.svg';
import EditIcon from '@material-ui/icons/Edit';
import DeleteIcon from '@material-ui/icons/Delete';
import {
Expand Down Expand Up @@ -132,6 +133,11 @@ const useStyles = makeStyles(() => ({
width: '21px',
height: '21px',
},
helpfulIcon: {
width: '21px',
height: '21px',
marginRight: '5px',
},
submitButton: {
borderRadius: '30px',
minWidth: '80px',
Expand Down Expand Up @@ -206,9 +212,8 @@ const ReviewComponent = ({
setUser,
showLabel,
}: Props): ReactElement => {
const [reviewData, setReviewData] = useState<ReviewWithId>(review);
const formattedDate = format(new Date(reviewData.date), 'MMM dd, yyyy').toUpperCase();
const shortenedDate = format(new Date(reviewData.date), 'MMM yyyy').toUpperCase();
const formattedDate = format(new Date(review.date), 'MMM dd, yyyy').toUpperCase();
const shortenedDate = format(new Date(review.date), 'MMM yyyy').toUpperCase();
const {
root,
expand,
Expand All @@ -225,6 +230,7 @@ const ReviewComponent = ({
priceWithIcon,
bedPriceIcon,
bedroomsPriceText,
helpfulIcon,
deleteDialogTitle,
deleteDialogDesc,
deleteDialogActions,
Expand All @@ -241,14 +247,17 @@ const ReviewComponent = ({
const isSmallScreen = useMediaQuery('(max-width:391px)');
const toastTime = 3500;

const onSuccessfulEdit = () => {
get<ReviewWithId>(`/api/review-by-id/${reviewData.id}`, {
const updateReviewData = () => {
get<ReviewWithId>(`/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();
};
const deleteModal = () => {
Expand Down Expand Up @@ -299,13 +308,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}
/>
{deleteModal()}
</>
Expand All @@ -315,12 +324,12 @@ const ReviewComponent = ({
};
//Retrieving apartment data
useEffect(() => {
if (reviewData.aptId !== null) {
get<ApartmentWithId[]>(`/api/apts/${reviewData.aptId}`, {
if (review.aptId !== null) {
get<ApartmentWithId[]>(`/api/apts/${review.aptId}`, {
callback: setApt,
});
}
}, [reviewData]);
}, [review]);

const getRatingInfo = (ratings: DetailedRating): RatingInfo[] => {
return [
Expand Down Expand Up @@ -349,7 +358,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));
Expand All @@ -371,7 +380,7 @@ const ReviewComponent = ({

const handleDeleteModalClose = async (deleteit: Boolean) => {
if (deleteit) {
const endpoint = `/api/update-review-status/${reviewData.id}/DELETED`;
const endpoint = `/api/update-review-status/${review.id}/DELETED`;
if (user) {
const token = await user.getIdToken(true);
await axios.put(endpoint, {}, createAuthHeaders(token));
Expand All @@ -393,16 +402,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<Landlord>(`/api/landlord/${reviewData.landlordId}`, {
get<Landlord>(`/api/landlord/${review.landlordId}`, {
callback: setLandlordData,
errorHandler: landlordNotFound,
});
}, [reviewData.landlordId, landlordNotFound]);
}, [review.landlordId, landlordNotFound]);

const propertyLandlordLabel = () => {
return (
Expand All @@ -413,10 +422,7 @@ const ReviewComponent = ({
</Grid>
<Link
{...{
to:
apt.length > 0
? `/apartment/${reviewData.aptId}`
: `/landlord/${reviewData.landlordId}`,
to: apt.length > 0 ? `/apartment/${review.aptId}` : `/landlord/${review.landlordId}`,
style: {
color: 'black',
textDecoration: 'underline',
Expand Down Expand Up @@ -454,24 +460,16 @@ const ReviewComponent = ({
};
const bedroomsPriceLabel = (rowNum: number) => {
return (
<Grid
item
className={bedroomsPrice}
style={
rowNum === 2 || isMobile
? { width: '100%' }
: { gap: 'unset', justifyContent: 'space-evenly' }
}
>
{reviewData.bedrooms > 0 && (
<Grid item className={bedroomsPrice} style={isMobile ? { width: '100%' } : {}}>
{review.bedrooms > 0 && (
<div
className={bedroomsWithIcon}
style={rowNum === 2 || isMobile ? {} : { marginLeft: '30px' }}
>
<BedIcon className={bedPriceIcon} />
<Typography className={bedroomsPriceText}>
{reviewData.bedrooms}{' '}
{reviewData.bedrooms === 1
{review.bedrooms}{' '}
{review.bedrooms === 1
? isSmallScreen
? 'Bed'
: 'Bedroom'
Expand All @@ -481,15 +479,15 @@ const ReviewComponent = ({
</Typography>
</div>
)}
{reviewData.price > 0 && (
{review.price > 0 && (
<div
className={priceWithIcon}
style={rowNum === 2 || isMobile ? {} : { marginLeft: '30px' }}
>
<MoneyIcon className={bedPriceIcon} />
<Typography className={bedroomsPriceText}>
{' '}
{getPriceRange(reviewData.price) || 0}
{getPriceRange(review.price) || 0}
</Typography>
</div>
)}
Expand All @@ -505,7 +503,7 @@ const ReviewComponent = ({
<Grid item container justifyContent="space-between" className={bottomborder}>
<Grid container item spacing={1} className={reviewHeader}>
<Grid item>
<HeartRating value={reviewData.overallRating} readOnly />
<HeartRating value={review.overallRating} readOnly />
</Grid>
<Grid item>
<IconButton
Expand All @@ -521,7 +519,7 @@ const ReviewComponent = ({
</IconButton>
</Grid>
{useMediaQuery(
user && reviewData.userId && user.uid === reviewData.userId
user && review.userId && user.uid === review.userId
? '(min-width:1410px)'
: '(min-width:1075px)'
) && bedroomsPriceLabel(1)}
Expand All @@ -530,20 +528,17 @@ const ReviewComponent = ({
{isSmallScreen ? shortenedDate : formattedDate}
</Typography>
</Grid>
{user &&
reviewData.userId &&
user.uid === reviewData.userId &&
editDeleteButtons()}
{user && review.userId && user.uid === review.userId && editDeleteButtons()}
</Grid>
{useMediaQuery(
user && reviewData.userId && user.uid === reviewData.userId
user && review.userId && user.uid === review.userId
? '(max-width:1409px)'
: '(max-width:1074px)'
) && bedroomsPriceLabel(2)}
<Grid item>
<Collapse in={expanded} timeout="auto" unmountOnExit>
<CardContent>
<ReviewHeader aveRatingInfo={getRatingInfo(reviewData.detailedRatings)} />
<ReviewHeader aveRatingInfo={getRatingInfo(review.detailedRatings)} />
</CardContent>
</Collapse>
</Grid>
Expand All @@ -555,19 +550,19 @@ const ReviewComponent = ({

<Grid item container alignContent="center">
<Typography>
{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 ? (
<Button className={button} onClick={() => setExpandedText(!expandedText)}>
{expandedText ? 'Read Less' : 'Read More'}
</Button>
) : null}
</Typography>
</Grid>
{reviewData.photos.length > 0 && (
{review.photos.length > 0 && (
<Grid container>
<Grid item className={photoRowStyle}>
{reviewData.photos.map((photo) => {
{review.photos.map((photo) => {
return (
<CardMedia
component="img"
Expand All @@ -589,21 +584,24 @@ const ReviewComponent = ({
<Grid item container justifyContent="space-between">
<Grid item>
<Button
color={liked ? 'primary' : 'default'}
onClick={() => likeHandler(reviewData.id)}
onClick={() => likeHandler(review.id)}
className={button}
size="small"
disabled={likeLoading}
style={liked ? { color: colors.red1 } : { color: colors.gray1 }}
>
Helpful {`(${reviewData.likes || 0})`}
<img
src={HelpfulIcon}
alt="Helpful icon"
className={helpfulIcon}
loading="lazy"
style={liked ? {} : { filter: 'grayscale(100%)' }}
/>
{`Helpful (${review.likes})`}
</Button>
</Grid>
<Grid item>
<Button
onClick={() => reportAbuseHandler(reviewData.id)}
className={button}
size="small"
>
<Button onClick={() => reportAbuseHandler(review.id)} className={button} size="small">
Report Abuse
</Button>
</Grid>
Expand Down
26 changes: 26 additions & 0 deletions frontend/src/pages/ApartmentPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,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(false).then((user) => {
if (user) {
user.getIdToken(true).then((token) => {
get<ReviewWithId[]>(
`/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<CardData[]>(`/api/buildings/all/${apt?.landlordId}`, {
callback: setOtherproperties,
Expand Down
Loading
Loading