main page - add infinite page scroll
This commit is contained in:
parent
82998eb2ea
commit
e61607b5d4
@ -1,9 +1,10 @@
|
|||||||
import { Flex, Grid } from '@mantine/core';
|
import { Flex, Grid } from '@mantine/core';
|
||||||
import { IconSearch } from '@tabler/icons-react';
|
import { IconSearch } from '@tabler/icons-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { ChangeEvent, useContext, useEffect, useMemo, useState } from 'react';
|
import { ChangeEvent, useContext, useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useInView } from 'react-intersection-observer';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { Context } from '..';
|
import { Context } from '..';
|
||||||
import { useDebounce } from '../hooks/useDebounce';
|
import { useDebounce } from '../hooks/useDebounce';
|
||||||
@ -11,13 +12,13 @@ import { useRealmUser } from '../hooks/useRealmUser';
|
|||||||
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||||
import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema';
|
import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema';
|
||||||
import ClearableTextInput from '../shared/components/inputs/ClearableTextInput';
|
import ClearableTextInput from '../shared/components/inputs/ClearableTextInput';
|
||||||
import CenterLoader from '../shared/components/loaders/CenterLoader';
|
import CogwheelLoader from '../shared/components/loaders/CogwheelLoader';
|
||||||
import { isProduction } from '../shared/env.const';
|
import { isProduction } from '../shared/env.const';
|
||||||
|
import CameraCard from '../widgets/card/CameraCard';
|
||||||
import MainFiltersRightSide from '../widgets/sidebars/MainFiltersRightSide';
|
import MainFiltersRightSide from '../widgets/sidebars/MainFiltersRightSide';
|
||||||
import { SideBarContext } from '../widgets/sidebars/SideBarContext';
|
import { SideBarContext } from '../widgets/sidebars/SideBarContext';
|
||||||
import RetryErrorPage from './RetryErrorPage';
|
import RetryErrorPage from './RetryErrorPage';
|
||||||
import CameraCard from '../widgets/card/CameraCard';
|
import { useIntersection } from '@mantine/hooks';
|
||||||
import { useInView } from 'react-intersection-observer';
|
|
||||||
|
|
||||||
export const mainPageParams = {
|
export const mainPageParams = {
|
||||||
hostId: 'hostId',
|
hostId: 'hostId',
|
||||||
@ -33,37 +34,64 @@ const MainPage = () => {
|
|||||||
|
|
||||||
const { setRightChildren } = useContext(SideBarContext)
|
const { setRightChildren } = useContext(SideBarContext)
|
||||||
const { hostId: selectedHostId, selectedTags, searchQuery } = mainStore.filters
|
const { hostId: selectedHostId, selectedTags, searchQuery } = mainStore.filters
|
||||||
const [filteredCameras, setFilteredCameras] = useState<GetCameraWHostWConfig[]>([])
|
|
||||||
|
|
||||||
const { ref, inView } = useInView({ threshold: 0.5 })
|
const { ref, inView } = useInView({ threshold: 0.5 })
|
||||||
const [visibleCameras, setVisibleCameras] = useState<GetCameraWHostWConfig[]>([])
|
|
||||||
|
|
||||||
const realmUser = useRealmUser()
|
const realmUser = useRealmUser()
|
||||||
if (!isProduction) console.log('Realmuser:', realmUser)
|
if (!isProduction) console.log('Realmuser:', realmUser)
|
||||||
|
const loadTriggered = useRef(false);
|
||||||
|
|
||||||
const { data: cameras, isPending, isError, refetch } = useQuery({
|
const pageSize = 20;
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
isFetching,
|
||||||
|
isFetchingNextPage,
|
||||||
|
refetch,
|
||||||
|
} = useInfiniteQuery<GetCameraWHostWConfig[]>({
|
||||||
queryKey: [frigateQueryKeys.getCamerasWHost, selectedHostId, searchQuery, selectedTags],
|
queryKey: [frigateQueryKeys.getCamerasWHost, selectedHostId, searchQuery, selectedTags],
|
||||||
queryFn: () =>
|
queryFn: ({ pageParam = 0 }) =>
|
||||||
|
// Pass pagination parameters to the backend
|
||||||
frigateApi.getCamerasWHost({
|
frigateApi.getCamerasWHost({
|
||||||
name: searchQuery, // filter by camera name
|
name: searchQuery,
|
||||||
frigateHostId: selectedHostId, // filter by host id
|
frigateHostId: selectedHostId,
|
||||||
tagIds: selectedTags, // filter by tag id(s)
|
tagIds: selectedTags,
|
||||||
// offset and limit can be added later for pagination
|
offset: pageParam,
|
||||||
|
limit: pageSize,
|
||||||
}),
|
}),
|
||||||
})
|
getNextPageParam: (lastPage, pages) => {
|
||||||
|
// If last page size is less than pageSize, no more pages
|
||||||
|
if (lastPage.length < pageSize) return undefined;
|
||||||
|
// Next page offset is pages.length * pageSize
|
||||||
|
return pages.length * pageSize;
|
||||||
|
},
|
||||||
|
initialPageParam: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const cameras: GetCameraWHostWConfig[] = data?.pages.flat() || [];
|
||||||
|
// const cameras: GetCameraWHostWConfig[] = [];
|
||||||
|
|
||||||
|
const [visibleCount, setVisibleCount] = useState(pageSize)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFilteredCameras(cameras || []);
|
if (inView && !isFetching) {
|
||||||
setVisibleCameras([]); // reset visible cameras for pagination
|
if (visibleCount < cameras.length) {
|
||||||
}, [cameras]);
|
setVisibleCount(prev => Math.min(prev + pageSize, cameras.length));
|
||||||
|
} else if (hasNextPage && !isFetchingNextPage) {
|
||||||
useEffect(() => {
|
loadTriggered.current = true;
|
||||||
const pageSize = 20;
|
fetchNextPage().then(() => {
|
||||||
if (inView && filteredCameras.length > visibleCameras.length) {
|
// Add a small delay before resetting the flag
|
||||||
const nextBatch = filteredCameras.slice(visibleCameras.length, visibleCameras.length + pageSize);
|
setTimeout(() => {
|
||||||
setVisibleCameras(prev => [...prev, ...nextBatch]);
|
loadTriggered.current = false;
|
||||||
|
}, 300); // delay in milliseconds; adjust as needed
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [inView, filteredCameras, visibleCameras]);
|
}, [inView, cameras, visibleCount, hasNextPage, isFetchingNextPage, isFetching, fetchNextPage])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const hostId = searchParams.get(mainPageParams.hostId) || ''
|
const hostId = searchParams.get(mainPageParams.hostId) || ''
|
||||||
@ -95,10 +123,8 @@ const MainPage = () => {
|
|||||||
debouncedHandleSearchQuery(event.currentTarget.value)
|
debouncedHandleSearchQuery(event.currentTarget.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPending) return <CenterLoader />
|
if (isLoading) return <CogwheelLoader />;
|
||||||
|
|
||||||
if (isError) return <RetryErrorPage onRetry={refetch} />
|
if (isError) return <RetryErrorPage onRetry={refetch} />
|
||||||
|
|
||||||
if (!isProduction) console.log('MainPage rendered')
|
if (!isProduction) console.log('MainPage rendered')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -113,16 +139,18 @@ const MainPage = () => {
|
|||||||
placeholder={t('search')}
|
placeholder={t('search')}
|
||||||
icon={<IconSearch size="0.9rem" stroke={1.5} />}
|
icon={<IconSearch size="0.9rem" stroke={1.5} />}
|
||||||
value={searchQuery || undefined}
|
value={searchQuery || undefined}
|
||||||
onChange={onInputChange}
|
// onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex justify='center' h='100%' direction='column' w='100%' >
|
<Flex justify='center' h='100%' direction='column' w='100%' >
|
||||||
<Grid mt='sm' justify="center" mb='sm' align='stretch'>
|
<Grid mt='sm' justify="center" mb='sm' align='stretch'>
|
||||||
{visibleCameras.map(camera => (
|
{cameras.slice(0, visibleCount).map(camera => (
|
||||||
<CameraCard key={camera.id} camera={camera} />
|
<CameraCard key={camera.id} camera={camera} />
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
<div ref={ref} style={{ height: '50px' }} /> {/* trigger point */}
|
{ isFetching && !isFetchingNextPage ? <CogwheelLoader /> : null}
|
||||||
|
{/* trigger point. Rerender twice when enabled */}
|
||||||
|
<div ref={ref} style={{ height: '50px' }} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -50,7 +50,7 @@ export const frigateApi = {
|
|||||||
name?: string | null | undefined;
|
name?: string | null | undefined;
|
||||||
frigateHostId?: string | null | undefined;
|
frigateHostId?: string | null | undefined;
|
||||||
tagIds?: string | string[];
|
tagIds?: string | string[];
|
||||||
offset?: number;
|
offset?: any;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
} = {}) =>
|
} = {}) =>
|
||||||
instanceApi
|
instanceApi
|
||||||
|
|||||||
@ -7,11 +7,11 @@ import { isProduction } from "../env.const";
|
|||||||
interface Filters {
|
interface Filters {
|
||||||
hostId?: string | null
|
hostId?: string | null
|
||||||
searchQuery?: string | null
|
searchQuery?: string | null
|
||||||
selectedTags?: string[]
|
selectedTags: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MainStore {
|
export class MainStore {
|
||||||
filters: Filters = {}
|
filters: Filters = { selectedTags: [] }
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this)
|
makeAutoObservable(this)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user