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

Saved property landlord UI #333

Merged
merged 11 commits into from
Dec 1, 2023
18 changes: 18 additions & 0 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,24 @@ app.post('/api/add-saved-apartment', authenticate, saveApartmentHandler(true));

app.post('/api/remove-saved-apartment', authenticate, saveApartmentHandler(false));

app.get('/api/saved-apartments', authenticate, async (req, res) => {
if (!req.user) throw new Error('Not authenticated');
const { uid } = req.user;
const userRef = usersCollection.doc(uid);
const userDoc = await userRef.get();
const userApartmentsAsStrings: string[] = userDoc.data()?.apartments || [];
const userApartments = await Promise.all(
userApartmentsAsStrings.map((bid) => buildingsCollection.doc(bid).get())
);

const buildings: ApartmentWithId[] = userApartments.map(
(doc) => ({ id: doc.id, ...doc.data() } as ApartmentWithId)
);

const data = JSON.stringify(await pageData(buildings));
res.status(200).send(data);
});

app.post('/api/add-saved-landlord', authenticate, saveLandlordHandler(true));

app.post('/api/remove-saved-landlord', authenticate, saveLandlordHandler(false));
Expand Down
96 changes: 96 additions & 0 deletions frontend/src/components/Bookmarks/BookmarkAptCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, { useEffect, useState } from 'react';
import {
Box,
Card,
CardActionArea,
CardContent,
CardMedia,
Grid,
makeStyles,
Typography,
} from '@material-ui/core';
import { ApartmentWithId, ReviewWithId } from '../../../../common/types/db-types';
import ApartmentImg from '../../assets/apartment-placeholder.svg';
import BookmarkIcon from '@material-ui/icons/Bookmark';
import HeartRating from '../utils/HeartRating';
import { getAverageRating } from '../../utils/average';
import { get } from '../../utils/call';
import { colors } from '../../colors';

type Props = {
buildingData: ApartmentWithId;
numReviews: number;
company?: string;
};

const useStyles = makeStyles({
root: {
borderRadius: '0.8em',
},
media: {
maxHeight: '210px',
borderRadius: '0.8em',
marginBottom: '1em',
},
aptNameTxt: {
fontWeight: 700,
},
reviewNum: {
fontWeight: 600,
marginLeft: '10px',
fontSize: '16px',
},
bookmark: {
color: colors.red1,
},
});

const BookmarkAptCard = ({ buildingData, numReviews, company }: Props) => {
const { id, name, photos } = buildingData;
const img = photos.length > 0 ? photos[0] : ApartmentImg;
const [reviewList, setReviewList] = useState<ReviewWithId[]>([]);

const { root, media, aptNameTxt, reviewNum, bookmark } = useStyles();

useEffect(() => {
// Fetches approved reviews for the current apartment.
get<ReviewWithId[]>(`/api/review/aptId/${id}/APPROVED`, {
callback: setReviewList,
});
}, [id]);

return (
<Card className={root}>
<CardActionArea>
<CardContent>
<CardMedia image={img} component="img" title={name} className={media} />
<Box display="flex" justifyContent="space-between" alignItems="center">
<Box>
<Typography variant="h6" className={aptNameTxt}>
{name}
</Typography>
</Box>
<Box>
<BookmarkIcon fontSize="large" className={bookmark} />
</Box>
</Box>
{company && (
<Grid container item justifyContent="space-between">
<Grid>
<Typography variant="subtitle1">{buildingData.address}</Typography>
</Grid>
</Grid>
)}
<Grid container direction="row" alignItems="center">
<HeartRating value={getAverageRating(reviewList)} precision={0.5} readOnly />
<Typography variant="h6" className={reviewNum}>
{numReviews + (numReviews !== 1 ? ' Reviews' : ' Review')}
</Typography>
</Grid>
</CardContent>
</CardActionArea>
</Card>
);
};

export default BookmarkAptCard;
171 changes: 131 additions & 40 deletions frontend/src/pages/BookmarksPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import React, { ReactElement, useEffect, useState } from 'react';
import { Grid, makeStyles, Typography, Box, Button } from '@material-ui/core';
import { Button, Grid, Link, makeStyles, Typography, Box } from '@material-ui/core';
import { colors } from '../colors';
import { useTitle } from '../utils';
import { CardData } from '../App';
import { get } from '../utils/call';
import BookmarkAptCard from '../components/Bookmarks/BookmarkAptCard';
import { Link as RouterLink } from 'react-router-dom';
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
import { Likes, ReviewWithId } from '../../../common/types/db-types';
import axios from 'axios';
import { createAuthHeaders, getUser } from '../utils/firebase';
Expand Down Expand Up @@ -31,11 +37,11 @@
gridContainer: {
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'center',
marginTop: '10px',
marginBottom: '3em',
},
headerStyle: {
fontFamily: 'Work Sans',
fontWeight: 800,
},
sortByButton: {
background: '#E8E8E8',
Expand All @@ -44,9 +50,9 @@
paddingRight: '5px',
paddingLeft: '5px',
},
reviewsHeaderContainer: {
marginTop: '16px',
marginBottom: '24px',
headerContainer: {
marginTop: '2em',
marginBottom: '2em',
},
}));

Expand All @@ -57,9 +63,24 @@
* @param user props.user - The current user, null if not logged in.
* @returns BookmarksPage The BookmarksPage component.
*/

const BookmarksPage = ({ user, setUser }: Props): ReactElement => {
const { background, root, headerStyle, sortByButton, reviewsHeaderContainer } = useStyles();
const { background, root, headerStyle, sortByButton, headerContainer, gridContainer } =
useStyles();
const defaultShow = 6;

const [toShow, setToShow] = useState<number>(defaultShow);

// used for debugging, may be useful in the future
const [token, setToken] = useState<string>('');

Check warning on line 74 in frontend/src/pages/BookmarksPage.tsx

View workflow job for this annotation

GitHub Actions / lint

'token' is assigned a value but never used

const handleViewAll = () => {
setToShow(toShow + (savedAptsData.length - defaultShow));
};

const handleCollapse = () => {
setToShow(defaultShow);
};

const [helpfulReviewsData, setHelpfulReviewsData] = useState<ReviewWithId[]>([]);
const [sortBy, setSortBy] = useState<Fields>('date');
const [resultsToShow, setResultsToShow] = useState<number>(2);
Expand All @@ -69,17 +90,29 @@

useTitle('Bookmarks');

const [savedAptsData, setsavedAptsData] = useState<CardData[]>([]);
const savedAPI = '/api/saved-apartments';

// 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);
setToken(token);
const response = await axios.get(`/api/review/like/${user.uid}`, createAuthHeaders(token));
setHelpfulReviewsData(response.data);

get<CardData[]>(
savedAPI,
{
callback: setsavedAptsData,
},
createAuthHeaders(token)
);
}
};
fetchLikedReviews();
}, [user, toggle]);
}, [user, toggle, savedAPI]);

// Define the type of the properties used for sorting reviews
type Fields = keyof typeof helpfulReviewsData[0];
Expand Down Expand Up @@ -131,46 +164,104 @@
return (
<div className={background}>
<div className={root}>
<h2 className={headerStyle}>Saved Properties and Landlords</h2>
<Box className={headerContainer}>
<Typography variant="h3" className={headerStyle}>
Saved Properties and Landlords ({savedAptsData.length})
</Typography>
</Box>

<Grid item className={reviewsHeaderContainer} xs={12}>
<Grid container spacing={1} alignItems="center" justifyContent="space-between">
<Grid item>
<h2 className={headerStyle}>Reviews Marked Helpful ({helpfulReviewsData.length})</h2>
{savedAptsData.length > 0 ? (
<Grid container spacing={4} className={gridContainer}>
{savedAptsData &&
savedAptsData.slice(0, toShow).map(({ buildingData, numReviews, company }, index) => {
const { id } = buildingData;
return (
<Grid item xs={12} md={4} key={index}>
<Link
{...{
to: `/apartment/${id}`,
style: { textDecoration: 'none' },
component: RouterLink,
}}
>
<BookmarkAptCard
key={index}
numReviews={numReviews}
buildingData={buildingData}
company={company}
/>
</Link>
</Grid>
);
})}
<Grid item xs={12} container justifyContent="center">
{savedAptsData.length >= toShow &&
(savedAptsData.length > toShow ? (
<Button
variant="outlined"
color="secondary"
onClick={handleViewAll}
endIcon={<KeyboardArrowDownIcon />}
>
View All
</Button>
) : (
<Button
variant="outlined"
color="secondary"
onClick={handleCollapse}
endIcon={<KeyboardArrowUpIcon />}
>
Collapse
</Button>
))}
</Grid>
</Grid>
) : (
<Typography paragraph>You have not saved any apartments.</Typography>
)}

<Grid item>
<Grid container spacing={1} direction="row" alignItems="center">
<Grid item>
<Typography>Sort by:</Typography>
</Grid>
<Grid item className={sortByButton}>
<DropDown
menuItems={[
{
item: 'Recent',
callback: () => {
setSortBy('date');
},
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
className={headerContainer}
>
<Box>
<Typography variant="h3" className={headerStyle}>
Reviews Marked Helpful ({helpfulReviewsData.length})
</Typography>
</Box>

<Box>
<Grid container spacing={1} direction="row" alignItems="center">
<Grid item>
<Typography>Sort by:</Typography>
</Grid>
<Grid item className={sortByButton}>
<DropDown
menuItems={[
{
item: 'Recent',
callback: () => {
setSortBy('date');
},
{
item: 'Helpful',
callback: () => {
setSortBy('likes');
},
},
{
item: 'Helpful',
callback: () => {
setSortBy('likes');
},
]}
/>
</Grid>
},
]}
/>
</Grid>
</Grid>
</Grid>
</Grid>
</Box>
</Box>

{helpfulReviewsData.length === 0 && (
<Grid item style={{ marginTop: '4px' }}>
<Typography>You have not marked any reviews helpful.</Typography>
</Grid>
<Typography paragraph>You have not marked any reviews helpful.</Typography>
)}

{helpfulReviewsData.length > 0 && (
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/utils/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ export type GetOptions<T> = {
body?: any;
};

const get = <T>(route: string, options: GetOptions<T> = {}) => {
const get = <T>(route: string, options: GetOptions<T> = {}, inputConfig?: AxiosRequestConfig) => {
const { callback, errorHandler, body } = options;
const config: AxiosRequestConfig = body && {
data: body,
};
axios
.get<T>(route, config)
.get<T>(route, { ...config, ...inputConfig })
.then((response) => {
callback && callback(response.data);
})
Expand Down
Loading