diff --git a/app/pages/catalog/NoProductsFound.tsx b/app/pages/catalog/NoProductsFound.tsx
new file mode 100644
index 0000000..6b6eade
--- /dev/null
+++ b/app/pages/catalog/NoProductsFound.tsx
@@ -0,0 +1,13 @@
+import { Sparkles } from 'lucide-react'
+import type { ReactElement } from 'react'
+
+export function NoProductsFound(): ReactElement {
+ return (
+
+
+
+
We couldn’t find any products matching your search.
+
+
+ )
+}
diff --git a/app/pages/catalog/PaginationControls.tsx b/app/pages/catalog/PaginationControls.tsx
new file mode 100644
index 0000000..f51c121
--- /dev/null
+++ b/app/pages/catalog/PaginationControls.tsx
@@ -0,0 +1,56 @@
+import type { ReactElement } from 'react'
+import {
+ Pagination,
+ PaginationContent,
+ PaginationEnd,
+ PaginationItem,
+ PaginationNext,
+ PaginationPrevious,
+ PaginationStart
+} from '~/components/ui/Pagination'
+
+type PaginationControlsProperties = {
+ page: number
+ totalPage: number
+ onPageChange: (page: number) => void
+}
+
+export function PaginationControls({ page, totalPage, onPageChange }: PaginationControlsProperties): ReactElement {
+ const isFirstPage = page === 1
+ const isLastPage = page === totalPage
+ const classNamesDisabled = 'pointer-events-none opacity-50'
+
+ return (
+
+
+
+ !isFirstPage && onPageChange(1)}
+ className={isFirstPage ? classNamesDisabled : ''}
+ />
+
+
+ !isFirstPage && onPageChange(page - 1)}
+ className={isFirstPage ? classNamesDisabled : ''}
+ />
+
+
+ {page} / {totalPage}
+
+
+ !isLastPage && onPageChange(page + 1)}
+ className={isLastPage ? classNamesDisabled : ''}
+ />
+
+
+ !isLastPage && onPageChange(totalPage)}
+ className={isLastPage ? classNamesDisabled : ''}
+ />
+
+
+
+ )
+}
diff --git a/app/pages/catalog/ProductItem.tsx b/app/pages/catalog/ProductItem.tsx
index 06d29cb..27b9eb3 100644
--- a/app/pages/catalog/ProductItem.tsx
+++ b/app/pages/catalog/ProductItem.tsx
@@ -2,12 +2,11 @@ import { type ProductProjection } from '@commercetools/platform-sdk'
import { type ReactElement } from 'react'
import { generatePath, useNavigate } from 'react-router'
import { Card, CardContent, CardDescription, CardTitle } from '~/components/ui/Card'
-import { ShoppingCart } from 'lucide-react'
-import { Button } from '~/components/ui/Button'
import { SaleBadge } from '~/components/product/SaleBadge'
import { AspectRatio } from '~/components/ui/AspectRatio'
import { ProductPrice } from '~/components/product/ProductPrice'
import { ROUTES } from '~/routes'
+import { AddToCartButton } from './AddToCartButton'
type ProductItemProperties = { product: ProductProjection }
@@ -26,7 +25,7 @@ export function ProductItem({ product }: ProductItemProperties): ReactElement {
return (
void navigateTo(product.id)}
>
@@ -39,9 +38,7 @@ export function ProductItem({ product }: ProductItemProperties): ReactElement {
{description}
diff --git a/app/pages/catalog/ProductList.tsx b/app/pages/catalog/ProductList.tsx
index a221454..50f5bb4 100644
--- a/app/pages/catalog/ProductList.tsx
+++ b/app/pages/catalog/ProductList.tsx
@@ -3,10 +3,7 @@ import { type ProductProjection } from '@commercetools/platform-sdk'
import { Skeleton } from '~/components/ui/Skeleton'
import { ProductItem } from './ProductItem'
import { CATALOG_STATUS } from './hooks/useCatalogData'
-import { Sparkles } from 'lucide-react'
-
-// TODO: items per page
-const SKELETON_COUNT = 8
+import { ITEMS_PER_PAGE } from '~/api/namespaces/product'
type ProductListProperties = {
products: ProductProjection[]
@@ -15,25 +12,12 @@ type ProductListProperties = {
export function ProductList({ products, status }: ProductListProperties): ReactElement {
const isNotReady = status !== CATALOG_STATUS.READY
- const isEmpty = !isNotReady && products.length === 0
-
- if (isEmpty) {
- // TODO: move to separate component
- return (
-
-
-
-
We couldn’t find any products matching your search.
-
-
- )
- }
return (
{isNotReady
? // TODO: move to separate component
- Array.from({ length: SKELETON_COUNT }).map((_, index) => (
+ Array.from({ length: ITEMS_PER_PAGE }).map((_, index) => (
))
: products.map((product) =>
)}
diff --git a/app/pages/catalog/SearchForm/SearchFomBody.tsx b/app/pages/catalog/SearchForm/SearchFomBody.tsx
index 7a86ec1..afe0dbd 100644
--- a/app/pages/catalog/SearchForm/SearchFomBody.tsx
+++ b/app/pages/catalog/SearchForm/SearchFomBody.tsx
@@ -3,19 +3,23 @@ import { useForm } from 'react-hook-form'
import { Search } from 'lucide-react'
import { Input } from '~/components/ui/Input'
import { Button } from '~/components/ui/Button'
-import { PRODUCTS_LIMIT } from '../FilterForm/FilterFormBody'
import { type UseCatalogDataResult } from '../hooks/useCatalogData'
+import { ITEMS_PER_PAGE } from '~/api/namespaces/product'
type SearchFormBodyProperties = {
fetch: UseCatalogDataResult['fetchProducts']
+ setSearch: (search: string) => void
+ onSearch: () => void
}
-export function SearchFormBody({ fetch }: SearchFormBodyProperties): ReactElement {
+export function SearchFormBody({ fetch, setSearch, onSearch }: SearchFormBodyProperties): ReactElement {
const { register, handleSubmit, getValues } = useForm<{ search: string }>()
- function onSubmit(): void {
+ const onSubmit = (): Promise
=> {
+ onSearch()
const { search } = getValues()
- void fetch({ limit: PRODUCTS_LIMIT }, [], [], search)
+ setSearch(search)
+ return fetch({ limit: ITEMS_PER_PAGE }, [], [], search)
}
return (
diff --git a/app/pages/catalog/hooks/useCatalogData.ts b/app/pages/catalog/hooks/useCatalogData.ts
index 1a16112..b344dec 100644
--- a/app/pages/catalog/hooks/useCatalogData.ts
+++ b/app/pages/catalog/hooks/useCatalogData.ts
@@ -16,6 +16,7 @@ export type UseCatalogDataResult = {
products: UseCatalogProductsDataResult['products']
categories: UseCatalogCategoriesDataResult['categories']
status: CATALOG_STATUS
+ total: number
}
export function useCatalogData(): UseCatalogDataResult {
@@ -24,12 +25,12 @@ export function useCatalogData(): UseCatalogDataResult {
const { categoryId = '' } = useParams()
const { categories, fetchCategories } = useCatalogCategoriesData({ setStatus })
- const { products, fetchProducts } = useCatalogProductsData({ setStatus })
+ const { products, fetchProducts, total } = useCatalogProductsData({ setStatus })
const { filters, fetchFilters } = useCatalogFiltersData({ setStatus })
useEffect(() => void Promise.all([fetchFilters(), fetchProducts(), fetchCategories()]), [])
useEffect(() => void fetchProducts(), [categoryId])
- return { products, filters, categories, status, fetchProducts }
+ return { products, filters, categories, status, total, fetchProducts }
}
diff --git a/app/pages/catalog/hooks/useCatalogProductsData.ts b/app/pages/catalog/hooks/useCatalogProductsData.ts
index e10aa84..bb34cdb 100644
--- a/app/pages/catalog/hooks/useCatalogProductsData.ts
+++ b/app/pages/catalog/hooks/useCatalogProductsData.ts
@@ -3,6 +3,7 @@ import { useParams } from 'react-router'
import { type ProductProjection } from '@commercetools/platform-sdk'
import { toast } from 'sonner'
import {
+ ITEMS_PER_PAGE,
PRODUCT_LIST_DEFAULT_APPLIED_FILTERS,
PRODUCT_LIST_DEFAULT_APPLIED_SORT,
productApi,
@@ -20,6 +21,7 @@ export type UseCatalogProductsDataResult = {
sort?: ProductListAppliedSort,
searchText?: string
) => Promise
+ total: number
}
export type ProductListAppliedPayload = {
@@ -46,6 +48,7 @@ export function useCatalogProductsData({
setStatus: Dispatch>
}): UseCatalogProductsDataResult {
const [products, setProducts] = useState([])
+ const [total, setTotal] = useState(0)
const { categoryId = '' } = useParams()
const fetchProducts = async (
@@ -60,14 +63,14 @@ export function useCatalogProductsData({
try {
const response = await productApi.getProducts(
- { ...parameters, limit: 100 },
+ { ...parameters, limit: ITEMS_PER_PAGE },
cache.filters,
cache.sort,
searchText,
categoryId
)
-
setProducts(response.body.results)
+ setTotal(response.body.total ?? 0)
setStatus(CATALOG_STATUS.READY)
} catch (error) {
setStatus(CATALOG_STATUS.ERROR)
@@ -76,5 +79,5 @@ export function useCatalogProductsData({
}
}
- return { products, fetchProducts }
+ return { products, fetchProducts, total }
}
diff --git a/app/pages/catalog/index.tsx b/app/pages/catalog/index.tsx
index 14573e0..c65bb76 100644
--- a/app/pages/catalog/index.tsx
+++ b/app/pages/catalog/index.tsx
@@ -1,26 +1,50 @@
-import { type ReactElement } from 'react'
+import { useState, type ReactElement } from 'react'
import { useTitle } from '~/hooks/useTitle'
import { Loading } from '~/components/Loading'
import { ProductList } from './ProductList'
import { FilterFormBody } from './FilterForm/FilterFormBody'
-import { useCatalogData } from './hooks/useCatalogData'
+import { CATALOG_STATUS, useCatalogData } from './hooks/useCatalogData'
import { SearchFormBody } from './SearchForm/SearchFomBody'
+import { NoProductsFound } from './NoProductsFound'
+import { PaginationControls } from './PaginationControls'
+import { ITEMS_PER_PAGE } from '~/api/namespaces/product'
export default function Catalog(): ReactElement {
useTitle('Catalog')
+ const { products, filters, status, categories, fetchProducts, total } = useCatalogData()
+ const hasNoProducts = status === CATALOG_STATUS.READY && products.length === 0
+ const [currentPage, setCurrentPage] = useState(1)
+ const [searchText, setSearchText] = useState('')
+ const pagesCount = Math.ceil(total / ITEMS_PER_PAGE)
- const { products, filters, status, categories, fetchProducts } = useCatalogData()
-
+ const handlePageChange = (page: number): void => {
+ setCurrentPage(page)
+ void fetchProducts({ limit: ITEMS_PER_PAGE, offset: (page - 1) * ITEMS_PER_PAGE }, [], [], searchText)
+ }
if (filters.length === 0) {
return
}
-
return (
-
+
{
+ setCurrentPage(1)
+ setSearchText('')
+ }}
+ />
-
-
+
setCurrentPage(1)} />
+ {hasNoProducts ? (
+
+ ) : (
+
+ )}
)