Skip to content

Commit

Permalink
Merge pull request #1165 from data-for-change/1051-bug-coordinate-the…
Browse files Browse the repository at this point in the history
…-infographics-main-screen-with-the-newsflash-list

Apply pagination to news list + scroll observer
  • Loading branch information
atalyaalon authored Nov 27, 2024
2 parents fcb99b4 + c12247f commit a7bc500
Show file tree
Hide file tree
Showing 18 changed files with 283 additions and 128 deletions.
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"react-async-loader": "^0.1.2",
"react-dom": "^17.0.2",
"react-i18next": "^14.1.1",
"react-intersection-observer": "^9.13.1",
"react-leaflet": "3.2.5",
"react-leaflet-google-layer": "^2.2.0",
"react-router-dom": "^6.23.0",
Expand Down Expand Up @@ -134,7 +135,7 @@
"prettier --write"
]
},
"overrides": {
"react-refresh": "0.11.0"
}
"overrides": {
"react-refresh": "0.11.0"
}
}
3 changes: 1 addition & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next';
import { useTheme } from '@material-ui/core/styles';
import PopUpRedirect from './components/atoms/PopUpRedirect';
import WidgetsTemplate from './components/organisms/WidgetsTemplate';
import {observer} from "mobx-react-lite";
import { observer } from 'mobx-react-lite';
// main components height - must add up to 100

const headerHeight = '5vh';
Expand All @@ -31,7 +31,6 @@ const App: FC = () => {
const classes = useStyles();
const store = useStore();
const theme = useTheme();

const appDir = i18n.dir();

useEffect(() => {
Expand Down
49 changes: 0 additions & 49 deletions src/components/atoms/InfiniteScroll.tsx

This file was deleted.

1 change: 0 additions & 1 deletion src/components/atoms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export { default as Textbox } from './Textbox';
export { default as ErrorBoundary } from './ErrorBoundary';
export { default as MetaTag } from './MetaTag';
export { default as Loader } from './Loader';
export { default as InfiniteScroll } from './InfiniteScroll';
export { default as Dialog } from './Dialog';
export { default as AppBar } from './AppBar';
export { default as Logo } from './Logo';
Expand Down
2 changes: 1 addition & 1 deletion src/components/molecules/NewsFlashComp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const NewsFlashComp: FC<IProps> = ({ news }) => {
const verificationIcon = getVerificationIcon(news.newsflash_location_qualification);
const criticalIcon = news.critical && <CriticalIcon className={classes.icon} />;
const {newsId} = useParams()
const newsID = newsId ? parseInt(newsId) : ''
const newsID = newsId ? parseInt(newsId) : '';
const className = news.id === newsID ? classes.activeNewsFlash : '';
const date = news.date == null ? '' : dateFormat(new Date(news.date.replace(/-/g, '/')), locale);
const handleLocationEditorOpen = () => setOpen(true);
Expand Down
68 changes: 46 additions & 22 deletions src/components/organisms/News.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FC } from 'react';
import { FC, useRef } from 'react';
import { Typography } from 'components/atoms';
import { Box, makeStyles } from '@material-ui/core';
import { useStore } from 'store/storeConfig';
Expand All @@ -7,41 +7,65 @@ import RootStore from 'store/root.store';
import { observer } from 'mobx-react-lite';
import LocationSearchIndicator from 'components/molecules/LocationSearchIndicator';
import { IRouteProps } from 'models/Route';
import NewsFlashComp from "components/molecules/NewsFlashComp";

import NewsFlashComp from 'components/molecules/NewsFlashComp';
import { useScrollObserver } from 'hooks/useScrollObserver.hooks';
import { Direction } from 'models/ScrollObserver.model';
import { combineRefs } from 'utils/element.util';

const useStyles = makeStyles({
container: {},
newsFeed: {
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
overflow: 'auto',
},
});

<img src="" alt="" />
const News: FC = () => {
interface InfiniteScrollProps {
onScroll: (direction: Direction) => void;
}

const News: FC<InfiniteScrollProps> = ({ onScroll }) => {
const store: RootStore = useStore();
const classes = useStyles();
const { gpsId, street, city } = useParams<IRouteProps>();
const { gpsId, street, city, newsId = '' } = useParams<IRouteProps>();
const { newsFlashStore } = store;
const containerRef = useRef<HTMLDivElement>(null);

const { firstItemRef, lastItemRef, selectedItemRef } = useScrollObserver({
newsId,
onScroll,
containerRef,
newsData: newsFlashStore.newsFlashCollection.data,
newsLoading: newsFlashStore.newsFlashLoading,
});

return (
<Box flexGrow={1} display="flex" flexDirection="column" className={classes.newsFeed}>
<Box flexGrow={1}>
<Box className={classes.container} flexDirection={'column'}>
{gpsId && <LocationSearchIndicator searchType={'gps'} />}
{street && city && <LocationSearchIndicator searchType={'cityAndStreet'} />}
{newsFlashStore.newsFlashCollection.length > 0 ? (
newsFlashStore.newsFlashCollection.map((news) =>
<NewsFlashComp news={news} />
)
) : (
<Box p={1}>
<Typography.Body4>לא נמצאו תוצאות מהמקור המבוקש</Typography.Body4>
</Box>
)}
<div ref={containerRef} className={classes.newsFeed}>
{gpsId && <LocationSearchIndicator searchType={'gps'} />}
{street && city && <LocationSearchIndicator searchType={'cityAndStreet'} />}
{newsFlashStore.newsFlashCollection.data.length > 0 ? (
newsFlashStore.newsFlashCollection.data.map((news, index) => {
const isFirst = index === 0;
const isLast = index === newsFlashStore.newsFlashCollection.data.length - 1;
const selectedItem = news.id === +newsId ? selectedItemRef : undefined;

return (
<div
key={news.id}
ref={combineRefs(isFirst ? firstItemRef : undefined, isLast ? lastItemRef : undefined, selectedItem)}
>
<NewsFlashComp news={news} />
</div>
);
})
) : (
<Box p={1}>
<Typography.Body4>לא נמצאו תוצאות מהמקור המבוקש</Typography.Body4>
</Box>
</Box>
</Box>
)}
</div>
);
};

Expand Down
29 changes: 19 additions & 10 deletions src/components/organisms/SideBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ import { Typography, ErrorBoundary } from 'components/atoms';
import { observer } from 'mobx-react-lite';
import { useStore } from 'store/storeConfig';
import RootStore from 'store/root.store';
import { InfiniteScroll } from 'components/atoms';
import SideBarMap from 'components/molecules/SideBarMap';
import { useTranslation } from 'react-i18next';

const INFINITE_SCROLL_FETCH_SIZE = 100;
import { Direction } from 'models/ScrollObserver.model';

interface IProps {}

Expand All @@ -37,10 +35,23 @@ const SideBar: FC<IProps> = () => {
const mapTitle = `${t('sideBar')}`;
const location = newsFlashStore.activeNewsFlashLocation;
const loading = newsFlashStore.newsFlashLoading;
const currentPageNumber = newsFlashStore.newsFlashPageNumber;
const lastPrevPage = newsFlashStore.newsFlashLastPrevPage;
const totalPages = newsFlashStore.newsFlashCollection.pagination.totalPages;

const fetchMoreNewsItems = useCallback(() => {
newsFlashStore.infiniteFetchLimit(INFINITE_SCROLL_FETCH_SIZE);
}, [newsFlashStore]);
const fetchMoreNewsItems = useCallback(
(direction: Direction) => {
if (loading) return;
if (direction === Direction.PREV && currentPageNumber > 1 && lastPrevPage > 1) {
newsFlashStore.filterNewsFlashCollection(direction);
return;
}
if (direction === Direction.NEXT && totalPages > currentPageNumber) {
newsFlashStore.filterNewsFlashCollection(direction);
}
},
[currentPageNumber, lastPrevPage, loading, newsFlashStore, totalPages],
);

return (
<Box display="flex" flexDirection="column" justifyContent="center" alignItems="stretch">
Expand All @@ -51,9 +62,7 @@ const SideBar: FC<IProps> = () => {
<NewsFlashFilterPanel />
</ErrorBoundary>
</Box>
<InfiniteScroll onScrollEnd={fetchMoreNewsItems}>
<News />
</InfiniteScroll>
<News onScroll={fetchMoreNewsItems} />
</Box>
<Box borderTop={`1px solid ${silverSmokeColor}`} flexShrink={0} flexGrow={0} p={1}>
<Typography.Body4 children={mapTitle} />
Expand All @@ -62,7 +71,7 @@ const SideBar: FC<IProps> = () => {
{location && (
<ErrorBoundary>
<SideBarMap items={[location]} />
</ErrorBoundary>
</ErrorBoundary>
)}
</Box>
</Box>
Expand Down
87 changes: 87 additions & 0 deletions src/hooks/useScrollObserver.hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Direction } from 'models/ScrollObserver.model';
import { useEffect, useRef } from 'react';
import { useInView } from 'react-intersection-observer';

interface IProps {
newsId: string;
containerRef: React.RefObject<HTMLDivElement>;
newsData: any[];
newsLoading: boolean;
onScroll: (direction: Direction) => void;
}

export const useScrollObserver = ({ newsId, onScroll, containerRef, newsData, newsLoading }: IProps) => {
const selectedItemRef = useRef<HTMLDivElement>(null);
const isFirstRender = useRef(true);
const isInViewFirstRender = useRef(true);

useEffect(() => {
if (isFirstRender.current && newsId && newsData.length > 0 && !newsLoading) {
const itemIndex = newsData.findIndex((item) => item.id.toString() === newsId);

if (itemIndex !== -1) {
requestAnimationFrame(() => {
selectedItemRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
});
}
isFirstRender.current = false;
}
}, [newsId, newsData, newsLoading]);

const [firstItemRef] = useInView({
threshold: 0.1,
delay: 100,
onChange: (inView) => {
if (isInViewFirstRender.current) {
isInViewFirstRender.current = false;
return;
}
if (inView && !newsLoading) {
const container = containerRef.current;
if (!container) return;

const oldScrollHeight = container.scrollHeight;
const oldScrollTop = container.scrollTop;

// Create mutation observer before fetching
const observer = new MutationObserver(() => {
const newScrollHeight = container.scrollHeight;
const heightDiff = newScrollHeight - oldScrollHeight;

// Adjust scroll position to prevent jump
container.scrollTop = oldScrollTop + heightDiff;
observer.disconnect();
});
// Start observing before fetch
observer.observe(container, { childList: true, subtree: true });

onScroll(Direction.PREV);
isInViewFirstRender.current = true;
}
},
});

const [lastItemRef] = useInView({
threshold: 0.1,
delay: 100,
onChange: (inView) => {
if (isInViewFirstRender.current) {
isInViewFirstRender.current = false;
return;
}
if (inView && !newsLoading) {
onScroll(Direction.NEXT);
isInViewFirstRender.current = true;
}
},
});

return {
firstItemRef,
lastItemRef,
selectedItemRef,
};
};
12 changes: 12 additions & 0 deletions src/models/NewFlash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,15 @@ export interface INewsFlash {
source: string;
critical?: boolean;
}

export interface IPagination {
pageNumber: number;
pageSize: number;
totalRecords: number;
totalPages: number;
}

export interface INewsFlashCollection {
data: INewsFlash[];
pagination: IPagination;
}
4 changes: 4 additions & 0 deletions src/models/ScrollObserver.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum Direction {
PREV = 'PREV',
NEXT = 'NEXT',
}
Loading

0 comments on commit a7bc500

Please sign in to comment.