mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-02-23 15:34:21 +01:00
feat: add pagination
This commit is contained in:
@@ -3,7 +3,7 @@ import Skeleton from 'react-loading-skeleton'
|
||||
|
||||
export default function ProjectCardMiniSkeleton() {
|
||||
return (
|
||||
<div className="bg-gray-25 select-none px-16 py-16 flex flex-[0_0_100%] gap-16 border border-gray-200 rounded-10 items-center" >
|
||||
<div className="select-none px-16 py-16 flex flex-[0_0_100%] gap-16 rounded-10 items-center" >
|
||||
<Skeleton circle width={64} height={64} containerClassName='flex-shrink-0' />
|
||||
<div className="justify-around items-start min-w-0">
|
||||
<p className="text-body4 w-full font-bold overflow-ellipsis overflow-hidden whitespace-nowrap"><Skeleton width="15ch" /></p>
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function ProjectCardMini({ project, onClick }: Props) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="py-16 select-none px-16 flex items-center gap-16 rounded-16 hover:bg-gray-50 hover:outline outline-1 outline-gray-200"
|
||||
className="py-16 select-none px-16 flex items-center gap-16 rounded-16 hover:bg-gray-50 hover:outline active:scale-95 transition-transform outline-1 outline-gray-200"
|
||||
onKeyDown={e => {
|
||||
e.key !== 'Enter' || onClick(project?.id!)
|
||||
}}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useExplorePageQuery } from 'src/graphql';
|
||||
import ProjectsGrid from './ProjectsGrid/ProjectsGrid';
|
||||
import { Helmet } from "react-helmet";
|
||||
import Categories, { Category } from '../../Components/Categories/Categories';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import Header from './Header/Header';
|
||||
import Button from 'src/Components/Button/Button';
|
||||
import { useAppDispatch } from 'src/utils/hooks';
|
||||
@@ -13,10 +13,12 @@ import { createAction } from '@reduxjs/toolkit';
|
||||
import { useReduxEffect } from 'src/utils/hooks/useReduxEffect';
|
||||
import { NetworkStatus } from '@apollo/client';
|
||||
import { FiSliders } from 'react-icons/fi';
|
||||
import { HiOutlineChevronDoubleDown } from 'react-icons/hi'
|
||||
import { ProjectsFilters } from './Filters/FiltersModal';
|
||||
|
||||
const UPDATE_FILTERS_ACTION = createAction<Partial<ProjectsFilters>>('PROJECTS_FILTERS_UPDATED')({})
|
||||
|
||||
|
||||
type QueryFilter = Partial<{
|
||||
categoryId: string[] | null
|
||||
tags: string[] | null
|
||||
@@ -25,11 +27,15 @@ type QueryFilter = Partial<{
|
||||
license: string | null
|
||||
}>
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
export default function ExplorePage() {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const [filters, setFilters] = useState<Partial<ProjectsFilters> | null>(null)
|
||||
const [selectedCategory, setSelectedCategory] = useState<Category | null>(null)
|
||||
const projectsLength = useRef<number>(0);
|
||||
const [canFetchMore, setCanFetchMore] = useState(true);
|
||||
|
||||
const { queryFilters, hasSearchFilters } = useMemo(() => {
|
||||
let filter: QueryFilter = {}
|
||||
@@ -68,23 +74,30 @@ export default function ExplorePage() {
|
||||
return { queryFilters: filter, hasSearchFilters };
|
||||
}, [filters, selectedCategory?.id])
|
||||
|
||||
const { data, networkStatus, error } = useExplorePageQuery({
|
||||
const { data, networkStatus, error, fetchMore } = useExplorePageQuery({
|
||||
variables: {
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
pageSize: PAGE_SIZE,
|
||||
filter: queryFilters
|
||||
},
|
||||
notifyOnNetworkStatusChange: true,
|
||||
onCompleted: data => {
|
||||
if ((data.projects?.length ?? 0) < PAGE_SIZE) setCanFetchMore(false);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
projectsLength.current = data?.projects?.length ?? 0;
|
||||
|
||||
|
||||
const onFiltersUpdated = useCallback(({ payload }: typeof UPDATE_FILTERS_ACTION) => {
|
||||
setSelectedCategory(null)
|
||||
setCanFetchMore(true);
|
||||
if (Object.keys(payload).length === 0)
|
||||
setFilters(null);
|
||||
else
|
||||
setFilters(payload);
|
||||
|
||||
}, [])
|
||||
|
||||
useReduxEffect(onFiltersUpdated, UPDATE_FILTERS_ACTION.type)
|
||||
@@ -110,6 +123,14 @@ export default function ExplorePage() {
|
||||
|
||||
const selectCategoryTab = (category: Category | null) => {
|
||||
setSelectedCategory(category);
|
||||
setCanFetchMore(true);
|
||||
}
|
||||
|
||||
const clickFetchMore = () => {
|
||||
fetchMore({ variables: { page: Math.floor((data?.projects?.length ?? 0) / PAGE_SIZE) + 1 } })
|
||||
.then(res => {
|
||||
if (!res.data.projects || res.data.projects.length < PAGE_SIZE) setCanFetchMore(false);
|
||||
})
|
||||
}
|
||||
|
||||
if (error) {
|
||||
@@ -120,6 +141,8 @@ export default function ExplorePage() {
|
||||
|
||||
|
||||
const isLoading = networkStatus === NetworkStatus.loading || networkStatus === NetworkStatus.refetch || networkStatus === NetworkStatus.setVariables;
|
||||
const isLoadingMore = networkStatus === NetworkStatus.fetchMore;
|
||||
const canLoadMore = !isLoading && !isLoadingMore && data?.projects && data.projects.length > 0 && canFetchMore;
|
||||
|
||||
|
||||
return (
|
||||
@@ -146,8 +169,12 @@ export default function ExplorePage() {
|
||||
<div className="mt-40">
|
||||
<ProjectsGrid
|
||||
isLoading={isLoading}
|
||||
isLoadingMore={isLoadingMore}
|
||||
projects={data?.projects?.filter((p) => p !== null) as any[] ?? []}
|
||||
/>
|
||||
{canLoadMore && <div className="flex justify-center mt-36">
|
||||
<Button onClick={clickFetchMore} color="gray"><HiOutlineChevronDoubleDown /> Load more</Button>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -7,10 +7,11 @@ import { ProjectCard } from 'src/utils/interfaces';
|
||||
|
||||
interface Props {
|
||||
isLoading?: boolean;
|
||||
isLoadingMore?: boolean;
|
||||
projects: ProjectCard[]
|
||||
}
|
||||
|
||||
export default function ProjectsGrid({ isLoading, projects }: Props) {
|
||||
export default function ProjectsGrid({ isLoading, isLoadingMore, projects }: Props) {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -33,6 +34,8 @@ export default function ProjectsGrid({ isLoading, projects }: Props) {
|
||||
{isLoading && Array(12).fill(0).map((_, idx) => <ProjectCardMiniSkeleton key={idx} />)}
|
||||
{!isLoading && projects.length === 0 && <p className="text-center text-gray-400 py-48 text-body2 font-medium col-span-full">No results found here...</p>}
|
||||
{!isLoading && projects.map((project) => <ProjectCardMini key={project.id} project={project} onClick={handleClick} />)}
|
||||
|
||||
{isLoadingMore && Array(4).fill(0).map((_, idx) => <ProjectCardMiniSkeleton key={idx} />)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export const apolloClient = new ApolloClient({
|
||||
typePolicies: {
|
||||
Query: {
|
||||
fields: {
|
||||
getFeed: offsetLimitPagination(['sortBy', 'tag'])
|
||||
projects: offsetLimitPagination(['_filter'])
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -72,10 +72,11 @@ function offsetLimitPagination<T = Reference>(
|
||||
const merged = existing ? existing.slice(0) : [];
|
||||
|
||||
if (args) {
|
||||
// Assume an skip of 0 if args.skip omitted.
|
||||
const { skip = 0 } = args;
|
||||
// Assume an _page of 0 if args._page omitted.
|
||||
const { _page = 1, _page_size = 20 } = args;
|
||||
const offset = (_page - 1) * _page_size;
|
||||
for (let i = 0; i < incoming.length; ++i) {
|
||||
merged[skip + i] = incoming[i];
|
||||
merged[offset + i] = incoming[i];
|
||||
}
|
||||
} else {
|
||||
// It's unusual (probably a mistake) for a paginated field not
|
||||
|
||||
Reference in New Issue
Block a user