diff --git a/frontend/src/assets/dropdown-arrow-down.svg b/frontend/src/assets/dropdown-arrow-down.svg new file mode 100644 index 00000000..5e8cb888 --- /dev/null +++ b/frontend/src/assets/dropdown-arrow-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/ApartmentCard/ApartmentCards.tsx b/frontend/src/components/ApartmentCard/ApartmentCards.tsx index b2aa9061..5d0f0279 100644 --- a/frontend/src/components/ApartmentCard/ApartmentCards.tsx +++ b/frontend/src/components/ApartmentCard/ApartmentCards.tsx @@ -1,9 +1,12 @@ -import React, { ReactElement, useState } from 'react'; +import React, { ReactElement, useEffect, useState } from 'react'; import ApartmentCard from './ApartmentCard'; import { Grid, Link, makeStyles, Button } from '@material-ui/core'; import { Link as RouterLink } from 'react-router-dom'; import { CardData } from '../../App'; import { loadingLength } from '../../constants/HomeConsts'; +import { ApartmentWithId } from '../../../../common/types/db-types'; +import { sortApartments } from '../../utils/sortApartments'; +import DropDownWithLabel from '../utils/DropDownWithLabel'; type Props = { data: CardData[]; @@ -36,8 +39,10 @@ const useStyles = makeStyles({ /** * ApartmentCards Component * - * This component displays ApartmentCard components of the data. It also shows - * a 'Show more' button if there is more data than the loadingLength constant. + * @remarks + * This component displays ApartmentCard components of the data. + * It also shows a 'Show more' button if there is more data than the loadingLength constant. + * It also has a dropdown to sort the apartments based on different properties, such as price and rating. * The component is responsive and adjusts its layout based on the screen size. * * @component @@ -47,40 +52,99 @@ const useStyles = makeStyles({ */ const ApartmentCards = ({ data, user, setUser }: Props): ReactElement => { const { boundingBox, showMoreButton, horizontalLine } = useStyles(); - + const [isMobile, setIsMobile] = useState(false); const [resultsToShow, setResultsToShow] = useState(loadingLength); const handleShowMore = () => { setResultsToShow(resultsToShow + loadingLength); }; + // Handle resizing of the window depending on mobile and if it is clicked. + useEffect(() => { + const handleResize = () => { + setIsMobile(window.innerWidth <= 600); + }; + handleResize(); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + type Fields = keyof CardData | keyof ApartmentWithId | 'originalOrder'; + const [sortBy, setSortBy] = useState('originalOrder'); + const [orderLowToHigh, setOrderLowToHigh] = useState(false); + return ( <> + + { + setSortBy('originalOrder'); + setOrderLowToHigh(false); + }, + }, + { + item: 'Lowest Price', + callback: () => { + setSortBy('avgPrice'); + setOrderLowToHigh(true); + }, + }, + { + item: 'Highest Price', + callback: () => { + setSortBy('avgPrice'); + setOrderLowToHigh(false); + }, + }, + { + item: 'Lowest Rating', + callback: () => { + setSortBy('avgRating'); + setOrderLowToHigh(true); + }, + }, + { + item: 'Highest Rating', + callback: () => { + setSortBy('avgRating'); + setOrderLowToHigh(false); + }, + }, + ]} + isMobile={isMobile} + /> + {data && - data.slice(0, resultsToShow).map(({ buildingData, numReviews, company }, index) => { - const { id } = buildingData; - return ( - - - - - - ); - })} + sortApartments(data, sortBy, orderLowToHigh) + .slice(0, resultsToShow) + .map(({ buildingData, numReviews, company }, index) => { + const { id } = buildingData; + return ( + + + + + + ); + })} {data && data.length > resultsToShow && ( <> diff --git a/frontend/src/components/LeaveReview/ReviewModal.tsx b/frontend/src/components/LeaveReview/ReviewModal.tsx index 8de8ced9..678aa2ed 100644 --- a/frontend/src/components/LeaveReview/ReviewModal.tsx +++ b/frontend/src/components/LeaveReview/ReviewModal.tsx @@ -12,7 +12,6 @@ import { makeStyles, IconButton, CardMedia, - Icon, useMediaQuery, } from '@material-ui/core'; import axios from 'axios'; @@ -69,18 +68,14 @@ const useStyle = makeStyles({ fontWeight: 600, width: '100%', height: '100%', - borderRadius: '50px !important', - backgroundColor: 'transparent', top: '0', left: '0', textTransform: 'none', fontSize: '18px', - textAlign: 'left', position: 'absolute', - zIndex: 1, - display: 'flex', - justifyContent: 'left', - paddingLeft: '25px', + borderRadius: '50px', + justifyContent: 'flex-start', + paddingLeft: '20px', }, expandMoreIcon: { right: '10px', @@ -515,7 +510,7 @@ const ReviewModal = ({ spacing={1} direction="row" alignItems="center" - justifyContent="flex-start" + justifyContent={isMobile ? 'flex-start' : 'flex-end'} > setSortBy('avgPrice')}, + * { item: 'Rating', callback: () => setSortBy('avgRating')}, + * { item: 'Date Added', callback: () => setSortBy('id')}, + * ]; + * + * function App() { + * return ( + * + * ); + * } + *``` + * @param {Object} props - The props of the component. + * @param {MenuElement[]} props.menuItems - An array of menu items, each containing an item name and a callback function. + * @returns {JSX.Element} The rendered dropdown component. + */ +import React, { useState } from 'react'; +import { Button, Menu, MenuItem, SvgIcon } from '@material-ui/core'; import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; -import SvgIcon from '@material-ui/core/SvgIcon'; +import { makeStyles } from '@material-ui/styles'; +import ArrowDownSrc from '../../assets/dropdown-arrow-down.svg'; + +const expandArrow = (direction: boolean) => { + return ( +
+ ⬇ +
+ ); +}; type MenuElement = { item: string; @@ -11,6 +51,7 @@ type MenuElement = { type Props = { menuItems: MenuElement[]; + isMobile?: boolean; defaultValue?: string; className?: string; icon?: boolean; @@ -18,17 +59,23 @@ type Props = { const useStyles = makeStyles({ button: { - minWidth: '64px', - backgroundColor: '#e8e8e8', - borderColor: '#e8e8e8', + borderColor: '#E8E8E8', + textTransform: 'none', + fontSize: '18px', + lineHeight: 'normal', + fontWeight: 'normal', + height: '44px', + borderRadius: '10px', + backgroundColor: '#E8E8E8', + scale: '1', + whiteSpace: 'nowrap', }, }); -export default function BasicMenu({ menuItems, defaultValue, className, icon }: Props) { +export default function DropDown({ menuItems, isMobile, defaultValue, className, icon }: Props) { const [anchorEl, setAnchorEl] = useState(null); - const [selected, setSelected] = useState(defaultValue ? defaultValue : 'Recent'); + const [selected, setSelected] = useState(defaultValue ? defaultValue : menuItems[0].item); const { button } = useStyles(); - const [isMobile, setIsMobile] = useState(false); const open = Boolean(anchorEl); const handleClick = (event: React.MouseEvent) => { @@ -37,12 +84,6 @@ export default function BasicMenu({ menuItems, defaultValue, className, icon }: const handleClose = () => { setAnchorEl(null); }; - useEffect(() => { - const handleResize = () => setIsMobile(window.innerWidth <= 600); - handleResize(); - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, []); return (
@@ -55,7 +96,9 @@ export default function BasicMenu({ menuItems, defaultValue, className, icon }: className={className || button} > {selected} - {icon != false && } + {icon === undefined + ? expandArrow(open) + : icon === true && } console.log('Item 1 selected') }, + * { item: 'Item 2', callback: () => console.log('Item 2 selected') }, + * { item: 'Item 3', callback: () => console.log('Item 3 selected') }, + * ]; + * + * + */ +import React from 'react'; +import { Typography, Grid } from '@material-ui/core'; +import DropDown from './DropDown'; + +type MenuElement = { + item: string; + callback: () => void; +}; + +interface DropDownWithLabelProps { + label: string; + menuItems: MenuElement[]; + labelStyle?: React.CSSProperties; + isMobile: boolean; +} + +const DropDownWithLabel: React.FC = ({ + label, + menuItems, + labelStyle, + isMobile, +}) => { + return ( + + + + {label} + + + + + + + ); +}; + +export default DropDownWithLabel; diff --git a/frontend/src/pages/ApartmentPage.tsx b/frontend/src/pages/ApartmentPage.tsx index 29d7a6bd..a9d60aab 100644 --- a/frontend/src/pages/ApartmentPage.tsx +++ b/frontend/src/pages/ApartmentPage.tsx @@ -30,7 +30,6 @@ import LinearProgress from '../components/utils/LinearProgress'; import { Likes, ReviewWithId } from '../../../common/types/db-types'; import axios from 'axios'; import { createAuthHeaders, subscribeLikes, getUser } from '../utils/firebase'; -import DropDown from '../components/utils/DropDown'; import { useParams } from 'react-router-dom'; import NotFoundPage from './NotFoundPage'; import HeartRating from '../components/utils/HeartRating'; @@ -41,6 +40,7 @@ import clsx from 'clsx'; import { sortReviews } from '../utils/sortReviews'; import savedIcon from '../assets/filled-large-saved-icon.png'; import unsavedIcon from '../assets/unfilled-large-saved-icon.png'; +import DropDownWithLabel from '../components/utils/DropDownWithLabel'; type Props = { user: firebase.User | null; @@ -53,13 +53,6 @@ export type RatingInfo = { }; const useStyles = makeStyles((theme) => ({ - sortByButton: { - background: '#E8E8E8', - border: 'none', - borderRadius: '10px', - paddingRight: '5px', - paddingLeft: '5px', - }, reviewButton: { borderRadius: '30px', marginTop: '10px', @@ -168,7 +161,6 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { }; const { - sortByButton, reviewButton, aptRating, heartRating, @@ -543,21 +535,7 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { )} - - - {isSaved - + - - - - Sort by: - - - { - setSortBy('date'); - }, - }, - { - item: 'Helpful', - callback: () => { - setSortBy('likes'); - }, - }, - ]} - /> - - + + { + setSortBy('date'); + }, + }, + { + item: 'Helpful', + callback: () => { + setSortBy('likes'); + }, + }, + ]} + isMobile={isMobile} + /> @@ -653,39 +626,36 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { )} - - {!isMobile && ( - - - Sort by: - - - { - setSortBy('date'); - }, + + + {!isMobile && ( + { + setSortBy('date'); }, - { - item: 'Helpful', - callback: () => { - setSortBy('likes'); - }, + }, + { + item: 'Helpful', + callback: () => { + setSortBy('likes'); }, - ]} - /> - - - )} + }, + ]} + isMobile={isMobile} + /> + )} + {sortReviews(reviewData, sortBy) diff --git a/frontend/src/pages/BookmarksPage.tsx b/frontend/src/pages/BookmarksPage.tsx index 644dce94..7adb75c1 100644 --- a/frontend/src/pages/BookmarksPage.tsx +++ b/frontend/src/pages/BookmarksPage.tsx @@ -13,8 +13,8 @@ import axios from 'axios'; import { createAuthHeaders, getUser } from '../utils/firebase'; import ReviewComponent from '../components/Review/Review'; import { sortReviews } from '../utils/sortReviews'; -import SortByButton from '../components/Bookmarks/SortByButton'; -import { AptSortField, sortApartments } from '../utils/sortApartments'; +import DropDownWithLabel from '../components/utils/DropDownWithLabel'; +import { AptSortFields, sortApartments } from '../utils/sortApartments'; type Props = { user: firebase.User | null; @@ -81,16 +81,13 @@ const ToggleButton = ({ */ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { const { background, headerStyle, headerContainer, gridContainer } = useStyles(); - useTitle('Bookmarks'); - - /**** Saved apartments ****/ const defaultShow = 6; const savedAPI = '/api/saved-apartments'; const [aptsToShow, setAptsToShow] = useState(defaultShow); const [savedAptsData, setSavedAptsData] = useState([]); // handle sort (either number of reviews or average rate) - const [sortAptsBy, setSortAptsBy] = useState('numReviews'); + const [sortAptsBy, setSortAptsBy] = useState('numReviews'); // handle toggle const handleViewAll = () => { @@ -107,6 +104,16 @@ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { const [likeStatuses, setLikeStatuses] = useState({}); const [toggle, setToggle] = useState(false); const [showMoreLessState, setShowMoreLessState] = useState('Show More'); + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + const handleResize = () => setIsMobile(window.innerWidth <= 600); + handleResize(); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + useTitle('Bookmarks'); // Fetch helpful reviews data when the component mounts or when user changes or when toggle changes useEffect(() => { @@ -126,9 +133,7 @@ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { savedAPI, { callback: (data) => { - sortApartments(data, sortAptsBy).then((res) => { - setSavedAptsData(res); - }); + setSavedAptsData(data); }, }, createAuthHeaders(token) @@ -190,6 +195,7 @@ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { { - { setSortAptsBy('numReviews'); - sortApartments(savedAptsData, 'numReviews').then((res) => { - setSavedAptsData(res); - }); }, }, { item: 'Rating', callback: () => { - setSortAptsBy('overallRating'); - sortApartments(savedAptsData, 'overallRating').then((res) => { - setSavedAptsData(res); - }); + setSortAptsBy('avgRating'); }, }, ]} + isMobile={isMobile} /> @@ -229,7 +231,7 @@ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { {savedAptsData.length > 0 ? ( {savedAptsData && - savedAptsData + sortApartments(savedAptsData, sortAptsBy, false) .slice(0, aptsToShow) .map(({ buildingData, numReviews, company }, index) => { const { id } = buildingData; @@ -252,7 +254,7 @@ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { ); })} - + {savedAptsData.length > defaultShow && (savedAptsData.length > aptsToShow ? ( { { - { }, }, ]} + isMobile={isMobile} /> diff --git a/frontend/src/pages/LandlordPage.tsx b/frontend/src/pages/LandlordPage.tsx index 1aaa009e..acd47b59 100644 --- a/frontend/src/pages/LandlordPage.tsx +++ b/frontend/src/pages/LandlordPage.tsx @@ -24,7 +24,6 @@ import LinearProgress from '../components/utils/LinearProgress'; import { Likes, ReviewWithId } from '../../../common/types/db-types'; import axios from 'axios'; import { createAuthHeaders, subscribeLikes, getUser } from '../utils/firebase'; -import DropDown from '../components/utils/DropDown'; import NotFoundPage from './NotFoundPage'; import { CardData } from '../App'; import { getAverageRating } from '../utils/average'; @@ -33,6 +32,7 @@ import { colors } from '../colors'; import { sortReviews } from '../utils/sortReviews'; import savedIcon from '../assets/filled-large-saved-icon.png'; import unsavedIcon from '../assets/unfilled-large-saved-icon.png'; +import DropDownWithLabel from '../components/utils/DropDownWithLabel'; export type RatingInfo = { feature: string; @@ -50,7 +50,6 @@ const useStyles = makeStyles((theme) => ({ }, leaveReviewContainer: { marginTop: '16px', - marginBottom: '24px', }, horizontalLine: { borderTop: '1px solid #C4C4C4', @@ -72,13 +71,6 @@ const useStyles = makeStyles((theme) => ({ marginTop: '10px', marginBottom: '10px', }, - sortByButton: { - background: '#E8E8E8', - border: 'none', - borderRadius: '10px', - paddingRight: '5px', - paddingLeft: '5px', - }, })); /** @@ -114,15 +106,8 @@ const LandlordPage = ({ user, setUser }: Props): ReactElement => { const saved = savedIcon; const unsaved = unsavedIcon; const [isSaved, setIsSaved] = useState(false); - const { - container, - leaveReviewContainer, - horizontalLine, - heartRating, - aptRating, - reviewButton, - sortByButton, - } = useStyles(); + const { container, leaveReviewContainer, horizontalLine, heartRating, aptRating, reviewButton } = + useStyles(); // useEffect hook to control the number of results to show based on screen size and reviewData length useEffect(() => { @@ -287,6 +272,16 @@ const LandlordPage = ({ user, setUser }: Props): ReactElement => { } }; + const openReviewModal = async () => { + let user = await getUser(true); + setUser(user); + if (!user) { + showSignInErrorToast(); + return; + } + setReviewOpen(true); + }; + // Define a component 'Modals' conditionally based on landlordData existence const Modals = landlordData && ( <> @@ -365,7 +360,7 @@ const LandlordPage = ({ user, setUser }: Props): ReactElement => { style={{ width: '107px', height: '43px' }} /> - {/* */} + - - - - Sort by: - - - { - setSortBy('date'); - }, - }, - { - item: 'Helpful', - callback: () => { - setSortBy('likes'); - }, - }, - ]} - /> - - + + { + setSortBy('date'); + }, + }, + { + item: 'Helpful', + callback: () => { + setSortBy('likes'); + }, + }, + ]} + isMobile={isMobile} + /> @@ -453,46 +443,36 @@ const LandlordPage = ({ user, setUser }: Props): ReactElement => { - - + - - - - Sort by: - - - { - setSortBy('date'); - }, - }, - { - item: 'Helpful', - callback: () => { - setSortBy('likes'); - }, - }, - ]} - /> - - + + { + setSortBy('date'); + }, + }, + { + item: 'Helpful', + callback: () => { + setSortBy('likes'); + }, + }, + ]} + isMobile={isMobile} + /> diff --git a/frontend/src/utils/sortApartments.ts b/frontend/src/utils/sortApartments.ts index a73a3b46..6774cd90 100644 --- a/frontend/src/utils/sortApartments.ts +++ b/frontend/src/utils/sortApartments.ts @@ -1,44 +1,51 @@ import { CardData } from '../App'; -import axios from 'axios'; -import { getAverageRating } from './average'; +import { ApartmentWithId } from '../../../common/types/db-types'; +export type AptSortFields = keyof CardData | keyof ApartmentWithId | 'originalOrder'; -export type AptSortField = 'overallRating' | 'numReviews'; -export type CardDataWithRating = CardData & { - avgRating: number; -}; +/** + * Sort apartments based on a specific property. + * @param arr CardData[] – array of CardData objects. + * @param property Fields – the property to sort the reviews with + * @param orderLowToHigh boolean – if true, sort from low to high, otherwise sort from high to low + * @returns CardData[] – a sorted shallow copy of the array of CardData objects + */ -const sortApartments = async (arr: CardData[], property: AptSortField) => { - const withRatings: CardDataWithRating[] = await Promise.all( - arr.map(async (elem) => { - return { - avgRating: getAverageRating( - (await axios.get(`/api/review/aptId/${elem.buildingData.id}/APPROVED`)).data - ), - ...elem, - }; - }) - ); +const sortApartments = (arr: CardData[], property: AptSortFields, orderLowToHigh: boolean) => { + // clone array to ensure we can keep the original indexes. + let clonedArr: CardData[] = arr.slice(); + if (property === 'originalOrder') { + return orderLowToHigh ? clonedArr.reverse() : clonedArr; + } + return clonedArr.sort((r1, r2) => { + let first, second; - withRatings.sort((apt1, apt2) => { - switch (property) { - case 'numReviews': - // sorts by decreasing number of reviews - return apt2.numReviews - apt1.numReviews; - case 'overallRating': - // sorts by decreasing average rating - return apt2.avgRating - apt1.avgRating; + //if property is a key of ApartmentWithId, then sort by that property using r1?.buildingData[property] + if (property in r1.buildingData) { + const prop = property as keyof ApartmentWithId; + first = r1.buildingData?.[prop] ?? 0; + second = r2.buildingData?.[prop] ?? 0; + // @ts-ignore: Object possibly null or undefined + } else { + const prop = property as keyof CardData; + first = r1?.[prop] ?? 0; + second = r2?.[prop] ?? 0; + // @ts-ignore: Object possibly null or undefined + } + //if first === second, then compare IDs + if (first === second) { + const firstID = r1?.buildingData.id ?? ''; + const secondID = r2?.buildingData.id ?? ''; + //always order bigger ids first + return firstID < secondID ? 1 : -1; + } else { + //if we are trying to order it from high to low + if (typeof orderLowToHigh == 'undefined' || !orderLowToHigh) { + return first < second ? 1 : -1; + } + //otherwise we order it from low to high + return first < second ? -1 : 1; } - return -1; }); - - return withRatings.map( - (elem) => - ({ - buildingData: elem.buildingData, - numReviews: elem.numReviews, - company: elem.company, - } as CardData) - ); }; export { sortApartments };