Skip to content

Commit

Permalink
Merge branch 'main' into search-suggestion-redesign
Browse files Browse the repository at this point in the history
  • Loading branch information
CasperL1218 authored Dec 4, 2024
2 parents 94c0e44 + c5e088d commit 99ac974
Show file tree
Hide file tree
Showing 12 changed files with 474 additions and 132 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ on its own, run `yarn frontend-dev` or `yarn backend-dev`.
- **Vicky Wang** - Designer
- **Kea-Roy Ong** - Developer
- **Casper Liao** - Developer
- **Parsa Tehranipoor** - Developer

### 2023-2024

Expand Down
50 changes: 35 additions & 15 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,28 +152,48 @@ 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();
querySnapshot.forEach((doc) => {
const data = doc.data();
const reviewData = { ...data, date: data.date.toDate() };
matchingReviews.push({ ...reviewData, id: doc.id } as ReviewWithId);
});
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);
});
}
res.status(200).send(JSON.stringify(matchingReviews));
return;
}
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.
23 changes: 21 additions & 2 deletions frontend/src/components/Admin/AdminReview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ type Props = {
readonly setToggle: React.Dispatch<React.SetStateAction<boolean>>;
/** Indicates if the review is in the declined section. */
readonly declinedSection: boolean;
/** Function to trigger the photo carousel. */
readonly triggerPhotoCarousel: (photos: readonly string[], startIndex: number) => void;
};

/**
Expand Down Expand Up @@ -70,14 +72,24 @@ const useStyles = makeStyles(() => ({
borderRadius: '4px',
height: '15em',
width: '15em',
cursor: 'pointer',
transition: '0.3s ease-in-out',
'&:hover': {
filter: 'brightness(0.85)',
boxShadow: '0 4px 4px rgba(0, 0, 0, 0.1)',
transform: 'scale(1.02)',
},
},
photoRowStyle: {
overflowX: 'auto',
overflowY: 'hidden',
display: 'flex',
flexDirection: 'row',
gap: '1vw',
paddingTop: '2%',
paddingLeft: '0.6%',
paddingRight: '0.6%',
paddingBottom: '2%',
},
}));

Expand All @@ -87,7 +99,12 @@ const useStyles = makeStyles(() => ({
* @param review review - The review to approve
* @returns The rendered component.
*/
const AdminReviewComponent = ({ review, setToggle, declinedSection }: Props): ReactElement => {
const AdminReviewComponent = ({
review,
setToggle,
declinedSection,
triggerPhotoCarousel,
}: Props): ReactElement => {
const { detailedRatings, overallRating, bedrooms, price, date, reviewText, photos } = review;
const formattedDate = format(new Date(date), 'MMM dd, yyyy').toUpperCase();
const { root, dateText, bedroomsPriceText, ratingInfo, photoStyle, photoRowStyle } = useStyles();
Expand Down Expand Up @@ -202,14 +219,16 @@ const AdminReviewComponent = ({ review, setToggle, declinedSection }: Props): Re
{photos.length > 0 && (
<Grid container>
<Grid item className={photoRowStyle}>
{photos.map((photo) => {
{photos.map((photo, i) => {
return (
<CardMedia
component="img"
alt="Apt image"
image={photo}
title="Apt image"
className={photoStyle}
onClick={() => triggerPhotoCarousel(photos, i)}
loading="lazy"
/>
);
})}
Expand Down
75 changes: 67 additions & 8 deletions frontend/src/components/PhotoCarousel/PhotoCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ interface Props {
photos: readonly string[];
open: boolean;
onClose?: () => void;
startIndex: number;
}

const useStyles = makeStyles((theme) => ({
Expand All @@ -18,33 +19,91 @@ const useStyles = makeStyles((theme) => ({
backgroundColor: 'rgba(0, 0, 0, 0.5)',
opacity: 1,
},
indicatorContainer: {
position: 'absolute',
bottom: '-10px',
width: '100%',
textAlign: 'center',
},
carouselContainer: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
overflow: 'visible',
height: '80vh',
[theme.breakpoints.down('md')]: {
height: '60dvw',
},
cursor: 'pointer',
},
}));
const ImageBox = styled(Box)({
width: 'fit-content',
margin: 'auto',
borderRadius: '10px',
overflow: 'hidden',
maxHeight: '80dvh',
backgroundColor: 'transparent',
'& img': {
borderRadius: '10px',
maxHeight: '80dvh',
objectFit: 'contain',
width: 'calc(69dvw - 96px)',
margin: 'auto',
cursor: 'default',
},
});

const PhotoCarousel = ({ photos, open, onClose }: Props) => {
const { modalBackground, navButton } = useStyles();
/**
* PhotoCarousel - this component displays a modal with a carousel of photos.
*
* @remarks
* This component is used to display a modal with a carousel of photos.
* It dynamically adjusts to different screen sizes and can be navigated using the arrow buttons.
*
* @component
* @param {readonly string[]} props.photos - An array of photo URLs to display in the carousel.
* @param {boolean} props.open - A boolean indicating whether the modal is open.
* @param {() => void} [props.onClose] - An optional callback function to handle closing the modal.
* @param {number} props.startIndex - The starting index of the carousel.
*
* @returns {JSX.Element} The rendered PhotoCarousel component.
*/
const PhotoCarousel = ({ photos, open, onClose, startIndex }: Props) => {
const { modalBackground, navButton, indicatorContainer, carouselContainer } = useStyles();
return (
<Dialog
open={open}
onClose={onClose}
fullWidth
maxWidth="md"
maxWidth={false}
style={{ cursor: 'pointer' }}
PaperProps={{ className: modalBackground }}
>
<Container>
<Container
onClick={(e) => {
const target = e.target as HTMLElement;
if (
target.tagName !== 'IMG' &&
target.tagName !== 'BUTTON' &&
target.tagName !== 'svg' &&
target.tagName !== 'circle'
) {
console.log(target.tagName);
onClose?.();
}
}}
>
<Carousel
autoPlay={false}
className={carouselContainer}
navButtonsAlwaysVisible={true}
navButtonsProps={{ className: navButton }}
indicatorContainerProps={{ className: indicatorContainer }}
index={startIndex}
animation="fade"
>
{photos.map((src, index) => {
return (
<ImageBox key={index}>
<ImageBox key={index} style={{ backgroundColor: 'rgba(0, 0, 0, 0.05)' }}>
<CardMedia component="img" src={src} />
</ImageBox>
);
Expand Down
39 changes: 39 additions & 0 deletions frontend/src/components/PhotoCarousel/usePhotoCarousel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// src/hooks/usePhotoCarousel.ts
import { useState } from 'react';

const usePhotoCarousel = (defaultPhotos: readonly string[] = []) => {
const [carouselPhotos, setCarouselPhotos] = useState<readonly string[]>(defaultPhotos);
const [carouselStartIndex, setCarouselStartIndex] = useState<number>(0);
const [carouselOpen, setCarouselOpen] = useState<boolean>(false);

/**
* showPhotoCarousel – Opens the photo carousel modal with the provided photos and start index.
*
* @remarks
* This function sets the photos and start index for the photo carousel and then opens the carousel modal.
* If no photos are provided, it defaults to [defaultPhotos].
*
* @param {readonly string[]} [photos] – The array of photo URLs to display in the carousel.
* @param {number} [startIndex] – The index of the photo to start the carousel from.
* @return {void} – This function does not return anything.
*/
const showPhotoCarousel = (photos: readonly string[] = defaultPhotos, startIndex: number = 0) => {
setCarouselPhotos(photos);
setCarouselStartIndex(startIndex);
setCarouselOpen(true);
};

const closePhotoCarousel = () => {
setCarouselOpen(false);
};

return {
carouselPhotos,
carouselStartIndex,
carouselOpen,
showPhotoCarousel,
closePhotoCarousel,
};
};

export default usePhotoCarousel;
Loading

0 comments on commit 99ac974

Please sign in to comment.