Skip to content

Commit ca40c26

Browse files
committed
feat: add pagination to the products page
1 parent 845d76b commit ca40c26

File tree

8 files changed

+137
-38
lines changed

8 files changed

+137
-38
lines changed

.env.sample

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
NEXT_PUBLIC_BACKEND_URL=https://api.nextwoo.ir/graphql
1+
NEXT_PUBLIC_BACKEND_URL=https://nextwoo.ir/graphql
22
NEXTAUTH_SECRET=jg3I65KxxWvmLWwbI8Zp9DbJsgyVJ+vHRAARmhF68+A=

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"stylis": "^4.3.1",
4444
"stylis-plugin-rtl": "^2.1.1",
4545
"swiper": "^11.1.1",
46+
"usehooks-ts": "^3.1.0",
4647
"yarn": "^1.22.22",
4748
"yup": "^1.4.0"
4849
},

src/app/[locale]/(main)/(container)/search/page.tsx

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,18 @@
11
import { ColumnFilters } from '@/components/ColumnFilters';
22
import { InlineFilters } from '@/components/InlineFilters';
3-
import ProductsCount from '@/components/ProductsCount/ProductsCount';
43
import ProductsList from '@/components/ProductsList/ProductsList';
54
import DesktopView from '@/components/ResponsiveDesign/components/DesktopView';
65
import MobileView from '@/components/ResponsiveDesign/components/MobileView';
7-
import { getClient } from '@/graphql/clients/serverSideClient';
8-
import { GET_VARIABLE_PRODUCTS_QUERY } from '@/graphql/queries/products';
9-
import { GetAllProductsQuery, StockStatusEnum } from '@/graphql/types/graphql';
10-
import { sortOptions } from '@/static/sortOptions';
11-
import { getSearchPageParams } from '@/utils/params';
126
import { Box } from '@mui/material';
13-
import SortWrapper from './components/SortWrapper';
147
import SortRow from './components/SortRow';
8+
import SortWrapper from './components/SortWrapper';
159

1610
const Page = async (props: { searchParams: Record<string, string> }) => {
17-
const { inStock, categoryId, sort, q } = getSearchPageParams(
18-
new URLSearchParams(props.searchParams),
19-
);
20-
21-
const { data } = await getClient().query<GetAllProductsQuery>({
22-
query: GET_VARIABLE_PRODUCTS_QUERY,
23-
variables: {
24-
stockStatus: inStock ? StockStatusEnum.InStock : null,
25-
categoryIdIn: categoryId ? [+categoryId] : null,
26-
q,
27-
orderBy: [sortOptions.find((item) => item.key === sort)?.props],
28-
first: 10,
29-
},
30-
});
31-
3211
return (
3312
<>
3413
<MobileView>
3514
<InlineFilters />
36-
<ProductsList items={data.products?.nodes} />
15+
<ProductsList />
3716
</MobileView>
3817

3918
<DesktopView>
@@ -55,10 +34,10 @@ const Page = async (props: { searchParams: Record<string, string> }) => {
5534
<Box sx={{ flexGrow: 1 }}>
5635
<SortWrapper>
5736
<SortRow />
58-
<ProductsCount value={data.products?.pageInfo.total} />
37+
{/* <ProductsCount value={data.products?.pageInfo.total} /> */}
5938
</SortWrapper>
6039

61-
<ProductsList items={data.products?.nodes} />
40+
<ProductsList />
6241
</Box>
6342
</Box>
6443
</DesktopView>

src/components/ProductsList/ProductsList.tsx

Lines changed: 105 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,93 @@
1-
import { GetAllProductsQuery } from '@/graphql/types/graphql';
1+
'use client';
2+
3+
import { GET_VARIABLE_PRODUCTS_QUERY } from '@/graphql/queries/products';
4+
import { GetAllProductsQuery, StockStatusEnum } from '@/graphql/types/graphql';
5+
import useCustomSearchParams from '@/hooks/useCustomSearchParams';
6+
import { sortOptions } from '@/static/sortOptions';
7+
import { useQuery, useSuspenseQuery } from '@apollo/client';
28
import { Grid } from '@mui/material';
3-
import { FC } from 'react';
4-
import { VariableProductItem } from '../VariableProductItem';
9+
import { FC, useEffect } from 'react';
10+
import { useIntersectionObserver } from 'usehooks-ts';
11+
import {
12+
VariableProductItem,
13+
VariableProductItemSkeleton,
14+
} from '../VariableProductItem';
515
import NotFoundItem from '../common/NotFoundItem';
616

7-
export interface ProductsListProps {
8-
items?: NonNullable<GetAllProductsQuery['products']>['nodes'];
9-
}
10-
const ProductsList: FC<ProductsListProps> = ({ items }) => {
11-
if (!items?.length) {
17+
export interface ProductsListProps {}
18+
const ProductsList: FC<ProductsListProps> = () => {
19+
const { inStock, categoryId, sort, q } = useCustomSearchParams();
20+
21+
const variables = {
22+
stockStatus: inStock ? StockStatusEnum.InStock : null,
23+
categoryIdIn: categoryId ? [+categoryId] : null,
24+
q,
25+
orderBy: [sortOptions.find((item) => item.key === sort)?.props],
26+
first: 15,
27+
};
28+
29+
const initQuery = useSuspenseQuery<GetAllProductsQuery>(
30+
GET_VARIABLE_PRODUCTS_QUERY,
31+
{
32+
variables,
33+
},
34+
);
35+
36+
const { isIntersecting, ref } = useIntersectionObserver({
37+
threshold: 0.1,
38+
});
39+
40+
const paginateQuery = useQuery<GetAllProductsQuery>(
41+
GET_VARIABLE_PRODUCTS_QUERY,
42+
{
43+
variables,
44+
skip: true,
45+
},
46+
);
47+
48+
useEffect(() => {
49+
if (isIntersecting) {
50+
paginateQuery.fetchMore({
51+
variables: {
52+
...variables,
53+
after:
54+
paginateQuery.data?.products?.pageInfo.endCursor ||
55+
initQuery.data?.products?.pageInfo.endCursor,
56+
},
57+
updateQuery: (previousQueryResult, { fetchMoreResult }) => {
58+
const newNodes = fetchMoreResult.products?.nodes || [];
59+
const pageInfo = fetchMoreResult.products?.pageInfo!;
60+
61+
return newNodes?.length!
62+
? {
63+
...previousQueryResult,
64+
65+
products: {
66+
...previousQueryResult.products,
67+
68+
nodes: [
69+
...(previousQueryResult.products?.nodes || []),
70+
...newNodes,
71+
],
72+
73+
pageInfo,
74+
},
75+
}
76+
: previousQueryResult;
77+
},
78+
});
79+
}
80+
}, [isIntersecting]);
81+
82+
if (!initQuery.data?.products?.nodes?.length) {
1283
return <NotFoundItem />;
1384
}
1485

86+
const items = [
87+
...initQuery.data.products.nodes,
88+
...(paginateQuery.data?.products?.nodes || []),
89+
];
90+
1591
return (
1692
<Grid container spacing={1}>
1793
{items?.map((item) => {
@@ -23,6 +99,27 @@ const ProductsList: FC<ProductsListProps> = ({ items }) => {
2399
);
24100
}
25101
})}
102+
103+
{(initQuery.data.products.pageInfo.hasNextPage ||
104+
paginateQuery.data?.products?.pageInfo.hasNextPage) && (
105+
<>
106+
{new Array(4 - (items.length % 4)).fill(1).map((_, index) => {
107+
return (
108+
<Grid
109+
ref={index === 0 ? ref : null}
110+
key={index.toString()}
111+
item
112+
xs={12}
113+
md={6}
114+
lg={4}
115+
xl={3}
116+
>
117+
<VariableProductItemSkeleton />
118+
</Grid>
119+
);
120+
})}
121+
</>
122+
)}
26123
</Grid>
27124
);
28125
};

src/components/VariableProductItem/VariableProductItem.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ const VariableProductItem: FC<ProductItemProps> = ({ data }) => {
8181
WebkitBoxOrient: 'vertical',
8282
}}
8383
>
84-
{data.name}as
84+
{data.name}
8585
</Typography>
8686
<Box
8787
sx={{

src/graphql/queries/products.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ export const GET_VARIABLE_PRODUCTS_QUERY = gql`
1111
$categoryIdIn: [Int]
1212
$q: String
1313
$first: Int
14+
$after: String
1415
) {
1516
products(
1617
first: $first
18+
after: $after
1719
where: {
1820
stockStatus: $stockStatus
1921
orderby: $orderBy
@@ -23,6 +25,8 @@ export const GET_VARIABLE_PRODUCTS_QUERY = gql`
2325
) {
2426
pageInfo {
2527
total
28+
endCursor
29+
startCursor
2630
hasNextPage
2731
hasPreviousPage
2832
}

src/hooks/useCustomSearchParams.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use client';
22

3-
import { useRouter } from '@/navigation';
43
import {
54
SearchPageParamsKeys,
65
SearchPagesParams,
@@ -19,7 +18,6 @@ export interface IUseCustomSearchParams {
1918

2019
const useCustomSearchParams: IUseCustomSearchParams = () => {
2120
const params = useSearchParams();
22-
const router = useRouter();
2321

2422
const navigate: ReturnType<IUseCustomSearchParams>['navigate'] = (
2523
key,
@@ -31,7 +29,8 @@ const useCustomSearchParams: IUseCustomSearchParams = () => {
3129
} else {
3230
newParams.set(key, value.toString());
3331
}
34-
router.push(`/search?${newParams}`);
32+
33+
window.history.pushState(null, '', `/search?${newParams}`);
3534
};
3635

3736
return {

yarn.lock

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7820,6 +7820,13 @@ __metadata:
78207820
languageName: node
78217821
linkType: hard
78227822

7823+
"lodash.debounce@npm:^4.0.8":
7824+
version: 4.0.8
7825+
resolution: "lodash.debounce@npm:4.0.8"
7826+
checksum: 10c0/762998a63e095412b6099b8290903e0a8ddcb353ac6e2e0f2d7e7d03abd4275fe3c689d88960eb90b0dde4f177554d51a690f22a343932ecbc50a5d111849987
7827+
languageName: node
7828+
linkType: hard
7829+
78237830
"lodash.isplainobject@npm:^4.0.6":
78247831
version: 4.0.6
78257832
resolution: "lodash.isplainobject@npm:4.0.6"
@@ -8457,6 +8464,7 @@ __metadata:
84578464
swiper: "npm:^11.1.1"
84588465
ts-jest: "npm:^29.1.2"
84598466
typescript: "npm:5.4"
8467+
usehooks-ts: "npm:^3.1.0"
84608468
yarn: "npm:^1.22.22"
84618469
yup: "npm:^1.4.0"
84628470
languageName: unknown
@@ -11027,6 +11035,17 @@ __metadata:
1102711035
languageName: node
1102811036
linkType: hard
1102911037

11038+
"usehooks-ts@npm:^3.1.0":
11039+
version: 3.1.0
11040+
resolution: "usehooks-ts@npm:3.1.0"
11041+
dependencies:
11042+
lodash.debounce: "npm:^4.0.8"
11043+
peerDependencies:
11044+
react: ^16.8.0 || ^17 || ^18
11045+
checksum: 10c0/2204d8c95109302bdaaa51a66bf216f3dba750f1d2795c20ecba75ba1c44a070a253935d537ef536514ab6e363bcc02ccc78b5ad63576ff8d880d577cf3fc48f
11046+
languageName: node
11047+
linkType: hard
11048+
1103011049
"util-deprecate@npm:^1.0.1":
1103111050
version: 1.0.2
1103211051
resolution: "util-deprecate@npm:1.0.2"

0 commit comments

Comments
 (0)