Skip to content

Commit 60c500b

Browse files
committed
Fix pagination
1 parent 1f9137f commit 60c500b

File tree

3 files changed

+241
-51
lines changed

3 files changed

+241
-51
lines changed

packages/client/src/pages/CollectionDetail/index.tsx

Lines changed: 86 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useMemo } from 'react';
1+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
22
import { useParams } from 'react-router-dom';
33
import {
44
Box,
@@ -23,7 +23,8 @@ import {
2323
PopoverTrigger,
2424
PopoverArrow,
2525
PopoverBody,
26-
PopoverContent
26+
PopoverContent,
27+
ButtonGroup
2728
} from '@chakra-ui/react';
2829
import { useCollection, useStacSearch } from '@developmentseed/stac-react';
2930
import {
@@ -55,19 +56,33 @@ function CollectionDetail() {
5556
const { collectionId } = useParams();
5657
usePageTitle(`Collection ${collectionId}`);
5758

58-
// const [urlParams, setUrlParams] = useSearchParams({ page: '1' });
59-
// const page = parseInt(urlParams.get('page') || '1', 10);
60-
// const setPage = useCallback(
61-
// (v: number | ((v: number) => number)) => {
62-
// const newVal = typeof v === 'function' ? v(page) : v;
63-
// setUrlParams({ page: newVal.toString() });
64-
// },
65-
// [page]
66-
// );
67-
6859
const { collection, state } = useCollection(collectionId!); // eslint-disable-line @typescript-eslint/no-non-null-assertion
6960

70-
const { results, collections, setCollections, submit } = useStacSearch();
61+
const {
62+
results,
63+
collections,
64+
setCollections,
65+
submit,
66+
nextPage,
67+
previousPage
68+
} = useStacSearch();
69+
70+
// The stac search pagination is token based and has no pages, but we can fake
71+
// it tracking the prev and next clicks.
72+
const [page, setPage] = useState<number>(1);
73+
74+
const onPageNavigate = useCallback(
75+
(actions: 'next' | 'previous') => {
76+
if (actions === 'next') {
77+
setPage((prev) => prev + 1);
78+
nextPage?.();
79+
} else {
80+
setPage((prev) => prev - 1);
81+
previousPage?.();
82+
}
83+
},
84+
[nextPage, previousPage]
85+
);
7186

7287
// Initialize the search with the current collection ID
7388
useEffect(() => {
@@ -128,6 +143,10 @@ function CollectionDetail() {
128143
const { id, title, description, keywords, license } =
129144
collection as StacCollection;
130145

146+
const resultCount = results?.numberMatched || 0;
147+
const shouldPaginate =
148+
results?.links?.length > 1 && resultCount > results?.numberReturned;
149+
131150
return (
132151
<Flex direction='column' gap={8}>
133152
<InnerPageHeader
@@ -248,10 +267,14 @@ function CollectionDetail() {
248267
<Box flexBasis='100%'>
249268
<Heading size='md' as='h2'>
250269
Items{' '}
251-
{results && (
252-
<Badge variant='solid'>{zeroPad(results.numberMatched)}</Badge>
253-
)}
270+
{results && <Badge variant='solid'>{zeroPad(resultCount)}</Badge>}
254271
</Heading>
272+
{!!resultCount && (
273+
<Text size='sm' color='base.400'>
274+
Showing page {page} of{' '}
275+
{Math.ceil(resultCount / results.numberReturned)}
276+
</Text>
277+
)}
255278
</Box>
256279
{/* <Flex direction='row' gap='4'>
257280
<Button
@@ -300,30 +323,35 @@ function CollectionDetail() {
300323
</MenuList>
301324
</Menu>
302325
<Popover placement='top' isLazy>
303-
<PopoverTrigger>
304-
<IconButton
305-
aria-label='Preview'
306-
icon={<CollecticonEye />}
307-
variant='outline'
308-
size='sm'
309-
/>
310-
</PopoverTrigger>
311-
<PopoverContent
312-
boxShadow='sm'
313-
borderColor='base.200'
314-
borderWidth='2px'
315-
>
316-
<PopoverArrow bg='base.200' />
317-
<PopoverBody
318-
p={0}
319-
overflow='hidden'
320-
borderRadius='md'
321-
>
322-
<Box h='15rem'>
323-
<ItemMap item={item} reuseMaps />
324-
</Box>
325-
</PopoverBody>
326-
</PopoverContent>
326+
{({ isOpen }) => (
327+
<>
328+
<PopoverTrigger>
329+
<IconButton
330+
aria-label='Preview'
331+
icon={<CollecticonEye />}
332+
variant='outline'
333+
size='sm'
334+
isActive={isOpen}
335+
/>
336+
</PopoverTrigger>
337+
<PopoverContent
338+
boxShadow='sm'
339+
borderColor='base.200'
340+
borderWidth='2px'
341+
>
342+
<PopoverArrow bg='base.200' />
343+
<PopoverBody
344+
p={0}
345+
overflow='hidden'
346+
borderRadius='md'
347+
>
348+
<Box h='15rem'>
349+
<ItemMap item={item} reuseMaps />
350+
</Box>
351+
</PopoverBody>
352+
</PopoverContent>
353+
</>
354+
)}
327355
</Popover>
328356
</Flex>
329357
);
@@ -338,11 +366,25 @@ function CollectionDetail() {
338366
</>
339367
)}
340368
</SimpleGrid>
369+
{shouldPaginate && (
370+
<Flex direction='column' alignItems='center'>
371+
<ButtonGroup size='sm' variant='outline' isAttached>
372+
<Button
373+
disabled={!previousPage}
374+
onClick={() => onPageNavigate('previous')}
375+
>
376+
Previous
377+
</Button>
378+
<Button
379+
disabled={!nextPage}
380+
onClick={() => onPageNavigate('next')}
381+
>
382+
Next
383+
</Button>
384+
</ButtonGroup>
385+
</Flex>
386+
)}
341387
</Flex>
342-
343-
{/* <Flex direction='column' alignItems='center'>
344-
<Pagination numPages={20} page={page} onPageChange={setPage} />
345-
</Flex> */}
346388
</Flex>
347389
);
348390
}

packages/client/src/pages/CollectionList/index.tsx

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,33 @@ import {
2222
CollecticonPlusSmall,
2323
CollecticonTextBlock
2424
} from '@devseed-ui/collecticons-chakra';
25-
import { useCollections } from '@developmentseed/stac-react';
2625
import type { StacCollection } from 'stac-ts';
2726

2827
import { usePageTitle } from '../../hooks';
2928
import { InnerPageHeader } from '$components/InnerPageHeader';
3029
import SmartLink from '$components/SmartLink';
3130
import { ItemCard, ItemCardLoading } from '$components/ItemCard';
32-
import { zeroPad } from '$utils/format';
3331
import { ButtonWithAuth } from '$components/auth/ButtonWithAuth';
3432
import { MenuItemWithAuth } from '$components/auth/MenuItemWithAuth';
33+
import { zeroPad } from '$utils/format';
34+
import { useCollections } from './useCollections';
3535

3636
function CollectionList() {
3737
usePageTitle('Collections');
38-
const { collections, state } = useCollections();
38+
39+
// const [urlParams, setUrlParams] = useSearchParams({ page: '1' });
40+
// const page = parseInt(urlParams.get('page') || '1', 10);
41+
// const setPage = useCallback(
42+
// (v: number | ((v: number) => number)) => {
43+
// const newVal = typeof v === 'function' ? v(page) : v;
44+
// setUrlParams({ page: newVal.toString() });
45+
// },
46+
// [page]
47+
// );
48+
49+
const { collections, state } = useCollections({
50+
limit: 1000
51+
});
3952

4053
// Quick search system.
4154
const [searchTerm, setSearchTerm] = useState('');
@@ -71,6 +84,11 @@ function CollectionList() {
7184
});
7285
}, [collections, searchTerm, keyword]);
7386

87+
const collectionsCount = collections?.numberMatched || 0;
88+
// const collectionsReturned = collections?.numberReturned || 0;
89+
// const numPages = Math.ceil(collectionsCount / pageSize);
90+
// const shouldPaginate = collectionsCount > collectionsReturned;
91+
7492
return (
7593
<Flex direction='column' gap={8}>
7694
<InnerPageHeader
@@ -91,10 +109,8 @@ function CollectionList() {
91109
<Box flexBasis='100%'>
92110
<Heading size='md' as='h2'>
93111
Collections{' '}
94-
{collections && (
95-
<Badge variant='solid'>
96-
{zeroPad(filteredCollections.length)}
97-
</Badge>
112+
{!!collectionsCount && (
113+
<Badge variant='solid'>{zeroPad(collectionsCount)}</Badge>
98114
)}
99115
</Heading>
100116
</Box>
@@ -186,6 +202,15 @@ function CollectionList() {
186202
))
187203
)}
188204
</SimpleGrid>
205+
{/* {shouldPaginate && (
206+
<Flex direction='column' alignItems='center'>
207+
<Pagination
208+
numPages={numPages}
209+
page={page}
210+
onPageChange={setPage}
211+
/>
212+
</Flex>
213+
)} */}
189214
</Flex>
190215
</Flex>
191216
);
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* Why this file exists:
3+
* @developmentseed/stac-react does not allow to change the limit and offset of
4+
* the collections endpoint. This file is a temporary workaround to allow
5+
* pagination.
6+
*/
7+
8+
import { useStacApi } from '@developmentseed/stac-react';
9+
import { useCallback, useEffect, useState } from 'react';
10+
import { StacCollection, StacLink } from 'stac-ts';
11+
12+
type ApiError = {
13+
detail?: { [key: string]: any } | string;
14+
status: number;
15+
statusText: string;
16+
};
17+
18+
type LoadingState = 'IDLE' | 'LOADING';
19+
20+
const debounce = <F extends (...args: any) => any>(fn: F, ms = 250) => {
21+
let timeoutId: ReturnType<typeof setTimeout>;
22+
23+
return function (this: any, ...args: any[]) {
24+
clearTimeout(timeoutId);
25+
timeoutId = setTimeout(() => fn.apply(this, args), ms);
26+
};
27+
};
28+
29+
type ApiResponse = {
30+
collections: StacCollection[];
31+
links: StacLink[];
32+
numberMatched: number;
33+
numberReturned: number;
34+
};
35+
36+
type StacCollectionsHook = {
37+
collections?: ApiResponse;
38+
reload: () => void;
39+
state: LoadingState;
40+
error?: ApiError;
41+
nextPage?: () => void;
42+
prevPage?: () => void;
43+
setOffset: (newOffset: number) => void;
44+
};
45+
46+
export function useCollections(opts?: {
47+
limit?: number;
48+
initialOffset?: number;
49+
}): StacCollectionsHook {
50+
const { limit = 10, initialOffset = 0 } = opts || {};
51+
52+
const { stacApi } = useStacApi(process.env.REACT_APP_STAC_API!);
53+
54+
const [collections, setCollections] = useState<ApiResponse>();
55+
const [state, setState] = useState<LoadingState>('IDLE');
56+
const [error, setError] = useState<ApiError>();
57+
58+
const [offset, setOffset] = useState(initialOffset);
59+
60+
const [hasNext, setHasNext] = useState(false);
61+
const [hasPrev, setHasPrev] = useState(false);
62+
63+
const _getCollections = useCallback(
64+
async (offset: number, limit: number) => {
65+
if (stacApi) {
66+
setState('LOADING');
67+
68+
try {
69+
const res = await stacApi.fetch(
70+
`${stacApi.baseUrl}/collections?limit=${limit}&offset=${offset}`
71+
);
72+
const data: ApiResponse = await res.json();
73+
74+
setHasNext(!!data.links.find((l) => l.rel === 'next'));
75+
setHasPrev(
76+
!!data.links.find((l) => ['prev', 'previous'].includes(l.rel))
77+
);
78+
79+
setCollections(data);
80+
} catch (err: any) {
81+
setError(err);
82+
setCollections(undefined);
83+
} finally {
84+
setState('IDLE');
85+
}
86+
}
87+
},
88+
[stacApi]
89+
);
90+
91+
const getCollections = useCallback(
92+
(offset: number, limit: number) =>
93+
debounce(() => _getCollections(offset, limit))(),
94+
[_getCollections]
95+
);
96+
97+
const nextPage = useCallback(() => {
98+
setOffset(offset + limit);
99+
}, [offset, limit]);
100+
101+
const prevPage = useCallback(() => {
102+
setOffset(offset - limit);
103+
}, [offset, limit]);
104+
105+
useEffect(() => {
106+
if (stacApi && !error && !collections) {
107+
getCollections(offset, limit);
108+
}
109+
}, [getCollections, stacApi, collections, error, offset, limit]);
110+
111+
return {
112+
collections,
113+
reload: useCallback(
114+
() => getCollections(offset, limit),
115+
[getCollections, offset, limit]
116+
),
117+
nextPage: hasNext ? nextPage : undefined,
118+
prevPage: hasPrev ? prevPage : undefined,
119+
setOffset,
120+
state,
121+
error
122+
};
123+
}

0 commit comments

Comments
 (0)