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] Save scroll positions and results to show #381

Merged
merged 11 commits into from
Dec 7, 2024
22 changes: 20 additions & 2 deletions frontend/src/components/ApartmentCard/ApartmentCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import DropDownWithLabel from '../utils/DropDownWithLabel';

type Props = {
data: CardData[];
initialResultsToShow?: number;
user: firebase.User | null;
setUser: React.Dispatch<React.SetStateAction<firebase.User | null>>;
onMoreResultsLoaded?: (results: number) => void;
};

const useStyles = makeStyles({
Expand Down Expand Up @@ -48,12 +50,22 @@ const useStyles = makeStyles({
* @component
* @param {Object} props - Component properties.
* @param {CardData[]} props.data - The data of apartments.
* @param {number} [props.initialResultsToShow] - Initial number of results to show.
* @param {firebase.User | null} props.user - The current user.
* @param {React.Dispatch<React.SetStateAction<firebase.User | null>>} props.setUser - Function to set the current user.
* @param {(results: number) => void} [props.onMoreResultsLoaded] - Callback function when more results are loaded.
* @returns {ReactElement} ApartmentCards component.
*/
const ApartmentCards = ({ data, user, setUser }: Props): ReactElement => {
const ApartmentCards = ({
data,
initialResultsToShow,
user,
onMoreResultsLoaded,
setUser,
}: Props): ReactElement => {
const { boundingBox, showMoreButton, horizontalLine } = useStyles();
const [isMobile, setIsMobile] = useState<boolean>(false);
const [resultsToShow, setResultsToShow] = useState<number>(loadingLength);
const [resultsToShow, setResultsToShow] = useState<number>(initialResultsToShow ?? loadingLength);

const handleShowMore = () => {
setResultsToShow(resultsToShow + loadingLength);
Expand All @@ -69,6 +81,12 @@ const ApartmentCards = ({ data, user, setUser }: Props): ReactElement => {
return () => window.removeEventListener('resize', handleResize);
}, []);

useEffect(() => {
if (onMoreResultsLoaded) {
onMoreResultsLoaded(resultsToShow);
}
}, [resultsToShow, onMoreResultsLoaded]);

type Fields = keyof CardData | keyof ApartmentWithId | 'originalOrder';
const [sortBy, setSortBy] = useState<Fields>('originalOrder');
const [orderLowToHigh, setOrderLowToHigh] = useState<boolean>(false);
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/pages/ApartmentPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ReactElement, useState, useEffect, useRef } from 'react';

Check warning on line 1 in frontend/src/pages/ApartmentPage.tsx

View workflow job for this annotation

GitHub Actions / lint

'useRef' is defined but never used
import {
IconButton,
Button,
Expand Down Expand Up @@ -150,7 +150,7 @@
const toastTime = 3500;
const [notFound, setNotFound] = useState(false);
const [otherProperties, setOtherproperties] = useState<CardData[]>([]);
const [toggle, setToggle] = useState(false);

Check warning on line 153 in frontend/src/pages/ApartmentPage.tsx

View workflow job for this annotation

GitHub Actions / lint

'toggle' is assigned a value but never used
const [isMobile, setIsMobile] = useState<boolean>(false);
const [isClicked, setIsClicked] = useState<boolean>(true);
const [resultsToShow, setResultsToShow] = useState<number>(reviewData.length);
Expand All @@ -159,7 +159,7 @@
const [isSaved, setIsSaved] = useState(false);
const [mapToggle, setMapToggle] = useState(false);

const dummyTravelTimes: LocationTravelTimes = {

Check warning on line 162 in frontend/src/pages/ApartmentPage.tsx

View workflow job for this annotation

GitHub Actions / lint

'dummyTravelTimes' is assigned a value but never used
agQuadDriving: -1,
agQuadWalking: -1,
engQuadDriving: -1,
Expand Down Expand Up @@ -338,6 +338,13 @@
checkIfSaved();
}, [user, setUser, aptId]);

useEffect(() => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
}, []);

const calculateAveRating = (reviews: ReviewWithId[]): RatingInfo[] => {
const features = ['location', 'safety', 'value', 'maintenance', 'communication', 'conditions'];
return features.map((feature) => {
Expand Down Expand Up @@ -531,7 +538,7 @@
fullWidth
disableElevation
>
<img src={isSaved ? saved : unsaved} className={bookmarkRibbon} />

Check warning on line 541 in frontend/src/pages/ApartmentPage.tsx

View workflow job for this annotation

GitHub Actions / lint

img elements must have an alt prop, either with meaningful text, or an empty string for decorative images
{isSaved ? 'Saved' : 'Save'}
</Button>
<Button
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { CardData } from '../App';
import { get } from '../utils/call';
import { loadingLength } from '../constants/HomeConsts';
import { useTitle } from '../utils';
import { useSaveScrollPosition } from '../utils/saveScrollPosition';
import { useLocation } from 'react-router-dom';

const useStyles = makeStyles({
jumboText: {
Expand Down Expand Up @@ -60,6 +62,8 @@ const HomePage = ({ user, setUser }: Props): ReactElement => {
const [data, setData] = useState<returnData>({ buildingData: [], isEnded: false });
const [isMobile, setIsMobile] = useState<boolean>(false);
const [drawerOpen] = useState<boolean>(false);
const path = useLocation();
const [pathName] = useState(path.pathname);

useTitle('Home');

Expand All @@ -76,6 +80,8 @@ const HomePage = ({ user, setUser }: Props): ReactElement => {
return () => window.removeEventListener('resize', handleResize);
}, []);

useSaveScrollPosition(`scrollPosition_${pathName}`, pathName);

return (
<>
<Box className={styles.JumboTron} mt={isMobile ? -2 : 0}>
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/pages/LandlordPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,14 @@ const LandlordPage = ({ user, setUser }: Props): ReactElement => {
return subscribeLikes(setLikedReviews);
}, []);

// Scroll to the top of the page when the component mounts
useEffect(() => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
}, []);

// Fetch the reviews that the user has liked and set the liked reviews and like statuses.
useEffect(() => {
getUser(false).then((user) => {
Expand Down
17 changes: 16 additions & 1 deletion frontend/src/pages/LocationPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { colors } from '../colors';
import { CardData } from '../App';
import { get } from '../utils/call';
import ApartmentCards from '../components/ApartmentCard/ApartmentCards';
import { useSaveScrollPosition } from '../utils/saveScrollPosition';

interface Images {
[location: string]: string;
Expand Down Expand Up @@ -62,6 +63,7 @@ const LocationPage = ({ user, setUser }: Props): ReactElement => {
});

const path = useLocation();
const [pathName] = useState(path.pathname);
const location = path.pathname.substring(path.pathname.lastIndexOf('/') + 1);
const locAPI = `/api/location/${location}/`;
const locToImg: Images = {
Expand All @@ -84,6 +86,10 @@ const LocationPage = ({ user, setUser }: Props): ReactElement => {
return () => window.removeEventListener('resize', handleResize);
}, []);

useSaveScrollPosition(`scrollPosition_${pathName}`, pathName);
const saveResultsCount = (count: number) => {
sessionStorage.setItem(`resultsCount_${location}`, count.toString());
};
const locDescText: Images = {
Collegetown: isMobile
? 'Collegetown is an off-campus gathering place for Cornellians located within walking distance of campus.'
Expand Down Expand Up @@ -112,7 +118,16 @@ const LocationPage = ({ user, setUser }: Props): ReactElement => {
<Typography variant="body1" className={bodyStyle}>
{desc}
</Typography>
<ApartmentCards user={user} setUser={setUser} data={data} />
<ApartmentCards
user={user}
initialResultsToShow={parseInt(
sessionStorage.getItem(`resultsCount_${location}`) || '5',
10
)}
setUser={setUser}
data={data}
onMoreResultsLoaded={saveResultsCount}
/>
</Container>
</Box>
</>
Expand Down
22 changes: 19 additions & 3 deletions frontend/src/pages/SearchResultsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { colors } from '../colors';
import { CardData } from '../App';
import ApartmentCards from '../components/ApartmentCard/ApartmentCards';
import { useTitle } from '../utils';
import { useSaveScrollPosition } from '../utils/saveScrollPosition';

const useStyles = makeStyles({
searchText: {
Expand Down Expand Up @@ -36,9 +37,10 @@ type Props = {

const SearchResultsPage = ({ user, setUser }: Props): ReactElement => {
const { searchText } = useStyles();
const location = useLocation();
const path = useLocation();
const [pathName] = useState(path.pathname);
const [searchResults, setSearchResults] = useState<CardData[]>([]);
const query = location.search.substring(3);
const query = path.search.substring(3);
const isMobile = useMediaQuery('(max-width:600px)');

useTitle('Search Result');
Expand All @@ -49,12 +51,26 @@ const SearchResultsPage = ({ user, setUser }: Props): ReactElement => {
});
}, [query]);

useSaveScrollPosition(`scrollPosition_${pathName}`, pathName);
const saveResultsCount = (count: number) => {
sessionStorage.setItem(`resultsCount_search_${query}`, count.toString());
};

return (
<Container>
<Typography className={searchText} style={{ fontSize: isMobile ? '20px' : '30px' }}>
Search results for "{query}"
</Typography>
<ApartmentCards user={user} setUser={setUser} data={searchResults} />
<ApartmentCards
user={user}
initialResultsToShow={parseInt(
sessionStorage.getItem(`resultsCount_search_${query}`) || '5',
10
)}
setUser={setUser}
data={searchResults}
onMoreResultsLoaded={saveResultsCount}
/>
</Container>
);
};
Expand Down
67 changes: 67 additions & 0 deletions frontend/src/utils/saveScrollPosition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useEffect } from 'react';

/**
* useSaveScrollPosition – Custom hook to save and restore the scroll position of a page using sessionStorage.
*
* @remarks
* This hook listens for scroll events and saves the scroll position in sessionStorage when the document is visible.
* It also restores the scroll position when the component mounts, ensuring a smooth scroll to the saved position.
* The scroll position is only saved if the difference between the current and previous scroll positions is less than 300 pixels.
*
* @param {string} storageKey – The key used to store the scroll position in sessionStorage.
* @param {string} pathName – The pathname of the current route to ensure scroll restoration only happens on the correct page.
* @return {void} – This hook does not return a value.
*/
export const useSaveScrollPosition = (storageKey: string, pathName: string): void => {
useEffect(() => {
const handleScroll = () => {
if (document.visibilityState === 'visible') {
const previousScrollPosition = parseInt(sessionStorage.getItem(storageKey) || '0', 10);
const currentScrollPosition = window.scrollY;
if (Math.abs(currentScrollPosition - previousScrollPosition) < 300) {
sessionStorage.setItem(storageKey, currentScrollPosition.toString());
}
}
};
const scrollDown = () => {
const scrollPosition = sessionStorage.getItem(storageKey);
if (scrollPosition) {
const targetPosition = parseInt(scrollPosition, 10);
if (targetPosition < 200) {
window.addEventListener('scroll', handleScroll);
return;
}
const scrollInterval = setInterval(() => {
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
if (window.scrollY < targetPosition - 50 && window.location.pathname === pathName) {
if (targetPosition <= maxScroll) {
window.scrollTo({
top: targetPosition,
behavior: 'smooth',
});
} else {
window.scrollTo({
top: maxScroll,
behavior: 'smooth',
});
}
} else {
clearInterval(scrollInterval);
window.addEventListener('scroll', handleScroll);
}
}, 16);
} else {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
window.addEventListener('scroll', handleScroll);
}
};
scrollDown();

return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [storageKey, pathName]);
};
Loading