diff --git a/template/apps/api/src/resources/user/actions/list.ts b/template/apps/api/src/resources/user/actions/list.ts index 15a90711..540a7f05 100644 --- a/template/apps/api/src/resources/user/actions/list.ts +++ b/template/apps/api/src/resources/user/actions/list.ts @@ -67,7 +67,7 @@ async function handler(ctx: AppKoaContext) { { sort }, ); - ctx.body = { ...result, results: result.results.map(userService.getPublic) }; + ctx.body = { ...result, results: result.results.map(userService.getPublic), page }; } export default (router: AppRouter) => { diff --git a/template/apps/web/src/components/InfiniteScrollContainer/index.tsx b/template/apps/web/src/components/InfiniteScrollContainer/index.tsx new file mode 100644 index 00000000..f8ca2b9a --- /dev/null +++ b/template/apps/web/src/components/InfiniteScrollContainer/index.tsx @@ -0,0 +1,60 @@ +import { FC, ReactNode, UIEventHandler, useEffect } from 'react'; +import { Center, Loader, ScrollAreaAutosize, ScrollAreaProps, Stack } from '@mantine/core'; +import { useInViewport } from '@mantine/hooks'; + +export interface InfiniteScrollProps extends ScrollAreaProps { + children: ReactNode; + fetchNextData: () => void; + hasMore: boolean; + maxContainerHeight?: number | string; + isFetchingNextPage: boolean; + isLoading: boolean; +} + +const InfiniteScrollContainer: FC = ({ + children, + maxContainerHeight, + hasMore, + fetchNextData, + isFetchingNextPage, + isLoading, + ...props +}) => { + const { ref: loadMoreBlockRef, inViewport } = useInViewport(); + + useEffect(() => { + if (inViewport && hasMore && !isFetchingNextPage && !isLoading) { + fetchNextData(); + } + }, [inViewport, hasMore, isLoading, isFetchingNextPage]); + + const handleViewportScroll: UIEventHandler = (e) => { + const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; + + if (Math.abs(scrollHeight - clientHeight - scrollTop) < 1 && hasMore && !isLoading) { + fetchNextData(); + } + }; + + return ( + + {children} + + + + {isFetchingNextPage && ( +
+ +
+ )} +
+ ); +}; + +export default InfiniteScrollContainer; diff --git a/template/apps/web/src/components/index.ts b/template/apps/web/src/components/index.ts index 56abcd12..cfd1bffc 100644 --- a/template/apps/web/src/components/index.ts +++ b/template/apps/web/src/components/index.ts @@ -1 +1,2 @@ +export { default as InfiniteScrollContainer } from './InfiniteScrollContainer'; export { default as Table } from './Table'; diff --git a/template/apps/web/src/hooks/index.ts b/template/apps/web/src/hooks/index.ts new file mode 100644 index 00000000..087a9192 --- /dev/null +++ b/template/apps/web/src/hooks/index.ts @@ -0,0 +1,3 @@ +import { useMobile } from './use-mobile'; + +export { useMobile }; diff --git a/template/apps/web/src/hooks/use-mobile.tsx b/template/apps/web/src/hooks/use-mobile.tsx new file mode 100644 index 00000000..48c883d5 --- /dev/null +++ b/template/apps/web/src/hooks/use-mobile.tsx @@ -0,0 +1,8 @@ +import { em } from '@mantine/core'; +import { useMediaQuery } from '@mantine/hooks'; + +export const useMobile = () => { + const isMobile = useMediaQuery(`(max-width: ${em(750)})`); + + return isMobile; +}; diff --git a/template/apps/web/src/pages/home/components/Filters/helpers.tsx b/template/apps/web/src/pages/home/components/Filters/helpers.tsx new file mode 100644 index 00000000..081bf409 --- /dev/null +++ b/template/apps/web/src/pages/home/components/Filters/helpers.tsx @@ -0,0 +1,30 @@ +import { ComboboxItem } from '@mantine/core'; + +export const getSortByOptions = (isMobile: boolean): ComboboxItem[] => { + return [ + { + value: 'newest', + label: 'Newest', + }, + { + value: 'oldest', + label: 'Oldest', + }, + ...(isMobile + ? [ + { + value: 'firstName', + label: 'First Name', + }, + { + value: 'lastName', + label: 'Last Name', + }, + { + value: 'email', + label: 'Email', + }, + ] + : []), + ]; +}; diff --git a/template/apps/web/src/pages/home/components/Filters/index.tsx b/template/apps/web/src/pages/home/components/Filters/index.tsx index 47b00058..caceaf78 100644 --- a/template/apps/web/src/pages/home/components/Filters/index.tsx +++ b/template/apps/web/src/pages/home/components/Filters/index.tsx @@ -1,30 +1,26 @@ import { FC, useLayoutEffect, useState } from 'react'; -import { ActionIcon, ComboboxItem, Group, Select, TextInput } from '@mantine/core'; +import { ActionIcon, Group, Select, Stack, TextInput } from '@mantine/core'; import { DatePickerInput, DatesRangeValue } from '@mantine/dates'; import { useDebouncedValue, useInputState, useSetState } from '@mantine/hooks'; import { IconSearch, IconSelector, IconX } from '@tabler/icons-react'; +import { useMobile } from 'hooks'; import { set } from 'lodash'; import { UserListParams } from 'resources/user'; -const selectOptions: ComboboxItem[] = [ - { - value: 'newest', - label: 'Newest', - }, - { - value: 'oldest', - label: 'Oldest', - }, -]; +import { getSortByOptions } from './helpers'; interface FiltersProps { setParams: ReturnType>[1]; } const Filters: FC = ({ setParams }) => { + const isMobile = useMobile(); + + const sortByOptions = getSortByOptions(isMobile); + const [search, setSearch] = useInputState(''); - const [sortBy, setSortBy] = useState(selectOptions[0].value); + const [sortBy, setSortBy] = useState(sortByOptions[0].value); const [filterDate, setFilterDate] = useState(); const [debouncedSearch] = useDebouncedValue(search, 500); @@ -57,9 +53,9 @@ const Filters: FC = ({ setParams }) => { return ( - + = ({ setParams }) => { />