Skip to content

Commit a7bc500

Browse files
authored
Merge pull request #1165 from data-for-change/1051-bug-coordinate-the-infographics-main-screen-with-the-newsflash-list
Apply pagination to news list + scroll observer
2 parents fcb99b4 + c12247f commit a7bc500

18 files changed

+283
-128
lines changed

package-lock.json

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"react-async-loader": "^0.1.2",
3131
"react-dom": "^17.0.2",
3232
"react-i18next": "^14.1.1",
33+
"react-intersection-observer": "^9.13.1",
3334
"react-leaflet": "3.2.5",
3435
"react-leaflet-google-layer": "^2.2.0",
3536
"react-router-dom": "^6.23.0",
@@ -134,7 +135,7 @@
134135
"prettier --write"
135136
]
136137
},
137-
"overrides": {
138-
"react-refresh": "0.11.0"
139-
}
138+
"overrides": {
139+
"react-refresh": "0.11.0"
140+
}
140141
}

src/App.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next';
1111
import { useTheme } from '@material-ui/core/styles';
1212
import PopUpRedirect from './components/atoms/PopUpRedirect';
1313
import WidgetsTemplate from './components/organisms/WidgetsTemplate';
14-
import {observer} from "mobx-react-lite";
14+
import { observer } from 'mobx-react-lite';
1515
// main components height - must add up to 100
1616

1717
const headerHeight = '5vh';
@@ -31,7 +31,6 @@ const App: FC = () => {
3131
const classes = useStyles();
3232
const store = useStore();
3333
const theme = useTheme();
34-
3534
const appDir = i18n.dir();
3635

3736
useEffect(() => {

src/components/atoms/InfiniteScroll.tsx

Lines changed: 0 additions & 49 deletions
This file was deleted.

src/components/atoms/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ export { default as Textbox } from './Textbox';
77
export { default as ErrorBoundary } from './ErrorBoundary';
88
export { default as MetaTag } from './MetaTag';
99
export { default as Loader } from './Loader';
10-
export { default as InfiniteScroll } from './InfiniteScroll';
1110
export { default as Dialog } from './Dialog';
1211
export { default as AppBar } from './AppBar';
1312
export { default as Logo } from './Logo';

src/components/molecules/NewsFlashComp.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ const NewsFlashComp: FC<IProps> = ({ news }) => {
5757
const verificationIcon = getVerificationIcon(news.newsflash_location_qualification);
5858
const criticalIcon = news.critical && <CriticalIcon className={classes.icon} />;
5959
const {newsId} = useParams()
60-
const newsID = newsId ? parseInt(newsId) : ''
60+
const newsID = newsId ? parseInt(newsId) : '';
6161
const className = news.id === newsID ? classes.activeNewsFlash : '';
6262
const date = news.date == null ? '' : dateFormat(new Date(news.date.replace(/-/g, '/')), locale);
6363
const handleLocationEditorOpen = () => setOpen(true);

src/components/organisms/News.tsx

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { FC } from 'react';
1+
import { FC, useRef } from 'react';
22
import { Typography } from 'components/atoms';
33
import { Box, makeStyles } from '@material-ui/core';
44
import { useStore } from 'store/storeConfig';
@@ -7,41 +7,65 @@ import RootStore from 'store/root.store';
77
import { observer } from 'mobx-react-lite';
88
import LocationSearchIndicator from 'components/molecules/LocationSearchIndicator';
99
import { IRouteProps } from 'models/Route';
10-
import NewsFlashComp from "components/molecules/NewsFlashComp";
11-
10+
import NewsFlashComp from 'components/molecules/NewsFlashComp';
11+
import { useScrollObserver } from 'hooks/useScrollObserver.hooks';
12+
import { Direction } from 'models/ScrollObserver.model';
13+
import { combineRefs } from 'utils/element.util';
1214

1315
const useStyles = makeStyles({
1416
container: {},
1517
newsFeed: {
18+
display: 'flex',
19+
flexDirection: 'column',
20+
flexGrow: 1,
1621
overflow: 'auto',
1722
},
1823
});
1924

20-
<img src="" alt="" />
21-
const News: FC = () => {
25+
interface InfiniteScrollProps {
26+
onScroll: (direction: Direction) => void;
27+
}
28+
29+
const News: FC<InfiniteScrollProps> = ({ onScroll }) => {
2230
const store: RootStore = useStore();
2331
const classes = useStyles();
24-
const { gpsId, street, city } = useParams<IRouteProps>();
32+
const { gpsId, street, city, newsId = '' } = useParams<IRouteProps>();
2533
const { newsFlashStore } = store;
34+
const containerRef = useRef<HTMLDivElement>(null);
35+
36+
const { firstItemRef, lastItemRef, selectedItemRef } = useScrollObserver({
37+
newsId,
38+
onScroll,
39+
containerRef,
40+
newsData: newsFlashStore.newsFlashCollection.data,
41+
newsLoading: newsFlashStore.newsFlashLoading,
42+
});
2643

2744
return (
28-
<Box flexGrow={1} display="flex" flexDirection="column" className={classes.newsFeed}>
29-
<Box flexGrow={1}>
30-
<Box className={classes.container} flexDirection={'column'}>
31-
{gpsId && <LocationSearchIndicator searchType={'gps'} />}
32-
{street && city && <LocationSearchIndicator searchType={'cityAndStreet'} />}
33-
{newsFlashStore.newsFlashCollection.length > 0 ? (
34-
newsFlashStore.newsFlashCollection.map((news) =>
35-
<NewsFlashComp news={news} />
36-
)
37-
) : (
38-
<Box p={1}>
39-
<Typography.Body4>לא נמצאו תוצאות מהמקור המבוקש</Typography.Body4>
40-
</Box>
41-
)}
45+
<div ref={containerRef} className={classes.newsFeed}>
46+
{gpsId && <LocationSearchIndicator searchType={'gps'} />}
47+
{street && city && <LocationSearchIndicator searchType={'cityAndStreet'} />}
48+
{newsFlashStore.newsFlashCollection.data.length > 0 ? (
49+
newsFlashStore.newsFlashCollection.data.map((news, index) => {
50+
const isFirst = index === 0;
51+
const isLast = index === newsFlashStore.newsFlashCollection.data.length - 1;
52+
const selectedItem = news.id === +newsId ? selectedItemRef : undefined;
53+
54+
return (
55+
<div
56+
key={news.id}
57+
ref={combineRefs(isFirst ? firstItemRef : undefined, isLast ? lastItemRef : undefined, selectedItem)}
58+
>
59+
<NewsFlashComp news={news} />
60+
</div>
61+
);
62+
})
63+
) : (
64+
<Box p={1}>
65+
<Typography.Body4>לא נמצאו תוצאות מהמקור המבוקש</Typography.Body4>
4266
</Box>
43-
</Box>
44-
</Box>
67+
)}
68+
</div>
4569
);
4670
};
4771

src/components/organisms/SideBar.tsx

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@ import { Typography, ErrorBoundary } from 'components/atoms';
88
import { observer } from 'mobx-react-lite';
99
import { useStore } from 'store/storeConfig';
1010
import RootStore from 'store/root.store';
11-
import { InfiniteScroll } from 'components/atoms';
1211
import SideBarMap from 'components/molecules/SideBarMap';
1312
import { useTranslation } from 'react-i18next';
14-
15-
const INFINITE_SCROLL_FETCH_SIZE = 100;
13+
import { Direction } from 'models/ScrollObserver.model';
1614

1715
interface IProps {}
1816

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

41-
const fetchMoreNewsItems = useCallback(() => {
42-
newsFlashStore.infiniteFetchLimit(INFINITE_SCROLL_FETCH_SIZE);
43-
}, [newsFlashStore]);
42+
const fetchMoreNewsItems = useCallback(
43+
(direction: Direction) => {
44+
if (loading) return;
45+
if (direction === Direction.PREV && currentPageNumber > 1 && lastPrevPage > 1) {
46+
newsFlashStore.filterNewsFlashCollection(direction);
47+
return;
48+
}
49+
if (direction === Direction.NEXT && totalPages > currentPageNumber) {
50+
newsFlashStore.filterNewsFlashCollection(direction);
51+
}
52+
},
53+
[currentPageNumber, lastPrevPage, loading, newsFlashStore, totalPages],
54+
);
4455

4556
return (
4657
<Box display="flex" flexDirection="column" justifyContent="center" alignItems="stretch">
@@ -51,9 +62,7 @@ const SideBar: FC<IProps> = () => {
5162
<NewsFlashFilterPanel />
5263
</ErrorBoundary>
5364
</Box>
54-
<InfiniteScroll onScrollEnd={fetchMoreNewsItems}>
55-
<News />
56-
</InfiniteScroll>
65+
<News onScroll={fetchMoreNewsItems} />
5766
</Box>
5867
<Box borderTop={`1px solid ${silverSmokeColor}`} flexShrink={0} flexGrow={0} p={1}>
5968
<Typography.Body4 children={mapTitle} />
@@ -62,7 +71,7 @@ const SideBar: FC<IProps> = () => {
6271
{location && (
6372
<ErrorBoundary>
6473
<SideBarMap items={[location]} />
65-
</ErrorBoundary>
74+
</ErrorBoundary>
6675
)}
6776
</Box>
6877
</Box>

src/hooks/useScrollObserver.hooks.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { Direction } from 'models/ScrollObserver.model';
2+
import { useEffect, useRef } from 'react';
3+
import { useInView } from 'react-intersection-observer';
4+
5+
interface IProps {
6+
newsId: string;
7+
containerRef: React.RefObject<HTMLDivElement>;
8+
newsData: any[];
9+
newsLoading: boolean;
10+
onScroll: (direction: Direction) => void;
11+
}
12+
13+
export const useScrollObserver = ({ newsId, onScroll, containerRef, newsData, newsLoading }: IProps) => {
14+
const selectedItemRef = useRef<HTMLDivElement>(null);
15+
const isFirstRender = useRef(true);
16+
const isInViewFirstRender = useRef(true);
17+
18+
useEffect(() => {
19+
if (isFirstRender.current && newsId && newsData.length > 0 && !newsLoading) {
20+
const itemIndex = newsData.findIndex((item) => item.id.toString() === newsId);
21+
22+
if (itemIndex !== -1) {
23+
requestAnimationFrame(() => {
24+
selectedItemRef.current?.scrollIntoView({
25+
behavior: 'smooth',
26+
block: 'center',
27+
});
28+
});
29+
}
30+
isFirstRender.current = false;
31+
}
32+
}, [newsId, newsData, newsLoading]);
33+
34+
const [firstItemRef] = useInView({
35+
threshold: 0.1,
36+
delay: 100,
37+
onChange: (inView) => {
38+
if (isInViewFirstRender.current) {
39+
isInViewFirstRender.current = false;
40+
return;
41+
}
42+
if (inView && !newsLoading) {
43+
const container = containerRef.current;
44+
if (!container) return;
45+
46+
const oldScrollHeight = container.scrollHeight;
47+
const oldScrollTop = container.scrollTop;
48+
49+
// Create mutation observer before fetching
50+
const observer = new MutationObserver(() => {
51+
const newScrollHeight = container.scrollHeight;
52+
const heightDiff = newScrollHeight - oldScrollHeight;
53+
54+
// Adjust scroll position to prevent jump
55+
container.scrollTop = oldScrollTop + heightDiff;
56+
observer.disconnect();
57+
});
58+
// Start observing before fetch
59+
observer.observe(container, { childList: true, subtree: true });
60+
61+
onScroll(Direction.PREV);
62+
isInViewFirstRender.current = true;
63+
}
64+
},
65+
});
66+
67+
const [lastItemRef] = useInView({
68+
threshold: 0.1,
69+
delay: 100,
70+
onChange: (inView) => {
71+
if (isInViewFirstRender.current) {
72+
isInViewFirstRender.current = false;
73+
return;
74+
}
75+
if (inView && !newsLoading) {
76+
onScroll(Direction.NEXT);
77+
isInViewFirstRender.current = true;
78+
}
79+
},
80+
});
81+
82+
return {
83+
firstItemRef,
84+
lastItemRef,
85+
selectedItemRef,
86+
};
87+
};

src/models/NewFlash.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,15 @@ export interface INewsFlash {
1717
source: string;
1818
critical?: boolean;
1919
}
20+
21+
export interface IPagination {
22+
pageNumber: number;
23+
pageSize: number;
24+
totalRecords: number;
25+
totalPages: number;
26+
}
27+
28+
export interface INewsFlashCollection {
29+
data: INewsFlash[];
30+
pagination: IPagination;
31+
}

src/models/ScrollObserver.model.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export enum Direction {
2+
PREV = 'PREV',
3+
NEXT = 'NEXT',
4+
}

0 commit comments

Comments
 (0)