mirror of
https://github.com/vercel/commerce.git
synced 2025-07-23 04:36:49 +00:00
✨ feat: filter product
:%s
This commit is contained in:
@@ -14,7 +14,7 @@ export default function getAllProductsOperation({
|
||||
variables?: ProductVariables
|
||||
config?: Partial<VendureConfig>
|
||||
preview?: boolean
|
||||
}): Promise<{ products: Product[] }>
|
||||
}): Promise<{ products: Product[], totalItems: number }>
|
||||
|
||||
async function getAllProducts({
|
||||
query = getAllProductsQuery,
|
||||
@@ -25,7 +25,7 @@ export default function getAllProductsOperation({
|
||||
variables?: ProductVariables
|
||||
config?: Partial<VendureConfig>
|
||||
preview?: boolean
|
||||
} = {}): Promise<{ products: Product[] | any[] }> {
|
||||
} = {}): Promise<{ products: Product[] | any[], totalItems: number }> {
|
||||
const config = commerce.getConfig(cfg)
|
||||
const variables = {
|
||||
input: {
|
||||
@@ -40,6 +40,7 @@ export default function getAllProductsOperation({
|
||||
|
||||
return {
|
||||
products: data.search.items.map((item) => normalizeSearchResult(item)),
|
||||
totalItems: data.search.totalItems as number,
|
||||
}
|
||||
}
|
||||
|
||||
|
3
framework/vendure/schema.d.ts
vendored
3
framework/vendure/schema.d.ts
vendored
@@ -3219,7 +3219,8 @@ export type GetAllProductsQueryVariables = Exact<{
|
||||
|
||||
export type GetAllProductsQuery = { __typename?: 'Query' } & {
|
||||
search: { __typename?: 'SearchResponse' } & {
|
||||
items: Array<{ __typename?: 'SearchResult' } & SearchResultFragment>
|
||||
items: Array<{ __typename?: 'SearchResult' } & SearchResultFragment>,
|
||||
'totalItems'
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -3,6 +3,7 @@ import { searchResultFragment } from '../fragments/search-result-fragment'
|
||||
export const getAllProductsQuery = /* GraphQL */ `
|
||||
query getAllProducts($input: SearchInput!) {
|
||||
search(input: $input) {
|
||||
totalItems
|
||||
items {
|
||||
...SearchResult
|
||||
}
|
||||
|
@@ -13,16 +13,19 @@ import ProductListBanner from '../src/components/modules/product-list/ProductLis
|
||||
interface Props {
|
||||
facets: Facet[],
|
||||
collections: Collection[],
|
||||
products: ProductCard[],
|
||||
productsResult: { products: ProductCard[], totalItems: number },
|
||||
|
||||
}
|
||||
|
||||
export default function Products({ facets, collections, products }: Props) {
|
||||
// console.log("facets: ", products)
|
||||
export default function Products({ facets, collections, productsResult }: Props) {
|
||||
return (
|
||||
<>
|
||||
<ProductListBanner />
|
||||
<ProductListFilter collections={collections} facets={facets} products={products} />
|
||||
<ProductListFilter
|
||||
collections={collections}
|
||||
facets={facets}
|
||||
products={productsResult.products}
|
||||
total={productsResult.totalItems} />
|
||||
<ViewedProducts />
|
||||
</>
|
||||
)
|
||||
@@ -70,7 +73,7 @@ export async function getStaticProps({
|
||||
config,
|
||||
preview,
|
||||
})
|
||||
promisesWithKey.push({ key: 'products', promise: productsPromise, keyResult: 'products' })
|
||||
promisesWithKey.push({ key: 'productsResult', promise: productsPromise })
|
||||
|
||||
|
||||
try {
|
||||
|
@@ -4,13 +4,16 @@
|
||||
padding: 1.6rem;
|
||||
margin: auto;
|
||||
.imgWrap {
|
||||
min-width: 10rem;
|
||||
min-height: 10rem;
|
||||
text-align: center;
|
||||
img {
|
||||
min-height: 10rem;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--disabled);
|
||||
text-align: center;
|
||||
margin-top: .8rem;
|
||||
margin-top: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
@@ -6,9 +6,10 @@ interface Props {
|
||||
heading: string,
|
||||
queryKey: string,
|
||||
categories: { name: string, slug?: string, code?: string }[]
|
||||
isSingleSelect?: boolean
|
||||
}
|
||||
|
||||
const MenuNavigation = ({ heading, queryKey, categories }: Props) => {
|
||||
const MenuNavigation = ({ heading, queryKey, categories, isSingleSelect }: Props) => {
|
||||
return (
|
||||
<section className={s.menuNavigationWrapper}>
|
||||
<h2 className={s.menuNavigationHeading}>{heading}({categories.length})</h2>
|
||||
@@ -19,6 +20,7 @@ const MenuNavigation = ({ heading, queryKey, categories }: Props) => {
|
||||
name={item.name}
|
||||
value={item.slug || item.code || ''}
|
||||
queryKey={queryKey}
|
||||
isSingleSelect={isSingleSelect}
|
||||
/>)
|
||||
}
|
||||
</ul>
|
||||
|
@@ -1,16 +1,18 @@
|
||||
import classNames from 'classnames'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { QUERY_SPLIT_SEPERATOR, ROUTE } from 'src/utils/constanst.utils'
|
||||
import { QUERY_KEY, QUERY_SPLIT_SEPERATOR, ROUTE } from 'src/utils/constanst.utils'
|
||||
import s from './MenuNavigationItem.module.scss'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
value: string
|
||||
queryKey: string,
|
||||
queryKey: string
|
||||
isSingleSelect?: boolean
|
||||
|
||||
}
|
||||
|
||||
const MenuNavigationItem = ({ name, value, queryKey }: Props) => {
|
||||
const MenuNavigationItem = ({ name, value, queryKey, isSingleSelect }: Props) => {
|
||||
const router = useRouter()
|
||||
const [isActive, setIsActive] = useState<boolean>()
|
||||
|
||||
@@ -27,20 +29,29 @@ const MenuNavigationItem = ({ name, value, queryKey }: Props) => {
|
||||
const queryString = router.query[queryKey] as string || ''
|
||||
const prevQuery = queryString.split(QUERY_SPLIT_SEPERATOR)
|
||||
|
||||
let newQuery = [] as string[]
|
||||
if (isActive) {
|
||||
newQuery = prevQuery.filter(item => item !== value)
|
||||
let newQuery = ''
|
||||
if (isSingleSelect) {
|
||||
newQuery = isActive ? '' : value
|
||||
} else {
|
||||
newQuery = [...prevQuery, value]
|
||||
if (isActive) {
|
||||
newQuery = prevQuery.filter(item => item !== value).join(QUERY_SPLIT_SEPERATOR)
|
||||
} else {
|
||||
newQuery = [...prevQuery, value].join(QUERY_SPLIT_SEPERATOR)
|
||||
}
|
||||
}
|
||||
|
||||
const query = {
|
||||
...router.query,
|
||||
[queryKey]: newQuery
|
||||
}
|
||||
|
||||
if (queryKey === QUERY_KEY.CATEGORY) {
|
||||
query[QUERY_KEY.PAGE] = "0"
|
||||
}
|
||||
// setIsActive(!isActive)
|
||||
|
||||
router.push({
|
||||
pathname: ROUTE.PRODUCTS,
|
||||
query: {
|
||||
...router.query,
|
||||
[queryKey]: newQuery.join(QUERY_SPLIT_SEPERATOR)
|
||||
}
|
||||
query
|
||||
},
|
||||
undefined, { shallow: true }
|
||||
)
|
||||
@@ -50,8 +61,6 @@ const MenuNavigationItem = ({ name, value, queryKey }: Props) => {
|
||||
onClick={handleClick}>
|
||||
{name}
|
||||
</li>)
|
||||
|
||||
|
||||
}
|
||||
|
||||
export default MenuNavigationItem
|
||||
|
@@ -2,9 +2,18 @@
|
||||
margin-top: 4rem;
|
||||
.list {
|
||||
@apply flex flex-wrap justify-around;
|
||||
.empty {
|
||||
button {
|
||||
margin-top: 1.6rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.pagination {
|
||||
padding-top: 4.8rem;
|
||||
@apply flex justify-center items-center;
|
||||
&.hide {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,28 +1,50 @@
|
||||
import React, { useState } from 'react'
|
||||
import { DEFAULT_PAGE_SIZE } from 'src/utils/constanst.utils'
|
||||
import classNames from 'classnames'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { DEFAULT_PAGE_SIZE, ROUTE } from 'src/utils/constanst.utils'
|
||||
import { ButtonCommon, EmptyCommon } from '..'
|
||||
import PaginationCommon from '../PaginationCommon/PaginationCommon'
|
||||
import ProductCard, { ProductCardProps } from '../ProductCard/ProductCard'
|
||||
import s from "./ProductList.module.scss"
|
||||
|
||||
interface ProductListProps {
|
||||
data: ProductCardProps[]
|
||||
data: ProductCardProps[],
|
||||
total?: number,
|
||||
defaultCurrentPage?: number
|
||||
onPageChange?: (page: number) => void
|
||||
}
|
||||
|
||||
const ProductList = ({data}: ProductListProps) => {
|
||||
const [currentPage, setCurrentPage] = useState(0)
|
||||
const onPageChange = (page:number) => {
|
||||
setCurrentPage(page)
|
||||
const ProductList = ({ data, total = data.length, defaultCurrentPage, onPageChange }: ProductListProps) => {
|
||||
const router = useRouter()
|
||||
const handlePageChange = (page: number) => {
|
||||
onPageChange && onPageChange(page)
|
||||
}
|
||||
|
||||
const handleShowAllProduct = () => {
|
||||
router.push({
|
||||
pathname: ROUTE.PRODUCTS,
|
||||
},
|
||||
undefined, { shallow: true }
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={s.wrapper}>
|
||||
<div className={s.list}>
|
||||
{
|
||||
data.slice(currentPage*DEFAULT_PAGE_SIZE,(currentPage+1)* DEFAULT_PAGE_SIZE).map((product,index)=>{
|
||||
data.map((product, index) => {
|
||||
return <ProductCard {...product} key={index} />
|
||||
})
|
||||
}
|
||||
{
|
||||
data.length === 0 && <div className={s.empty}>
|
||||
<EmptyCommon />
|
||||
<ButtonCommon onClick={handleShowAllProduct}>Show all products</ButtonCommon>
|
||||
</div>
|
||||
<div className={s.pagination}>
|
||||
<PaginationCommon total={data.length} pageSize={DEFAULT_PAGE_SIZE} onChange={onPageChange}/>
|
||||
}
|
||||
</div>
|
||||
<div className={classNames(s.pagination, { [s.hide]: data.length === 0 })}>
|
||||
<PaginationCommon defaultCurrent={defaultCurrentPage} total={total} pageSize={DEFAULT_PAGE_SIZE} onChange={handlePageChange} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
3
src/components/hooks/product/index.ts
Normal file
3
src/components/hooks/product/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as useSearchProducts } from './useSearchProducts'
|
||||
|
||||
|
14
src/components/hooks/product/useSearchProducts.tsx
Normal file
14
src/components/hooks/product/useSearchProducts.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { GetAllProductsQuery, QuerySearchArgs } from '@framework/schema'
|
||||
import { normalizeSearchResult } from '@framework/utils/normalize'
|
||||
import { getAllProductsQuery } from '@framework/utils/queries/get-all-products-query'
|
||||
import gglFetcher from 'src/utils/gglFetcher'
|
||||
import useSWR from 'swr'
|
||||
|
||||
const useSearchProducts = (options?: QuerySearchArgs) => {
|
||||
const { data, isValidating, ...rest } = useSWR<GetAllProductsQuery>([getAllProductsQuery, options], gglFetcher)
|
||||
console.log("on search ", data?.search.totalItems, options, data?.search.items)
|
||||
|
||||
return { products: data?.search.items.map((item) => normalizeSearchResult(item)), totalItems: data?.search.totalItems, loading: isValidating, ...rest }
|
||||
}
|
||||
|
||||
export default useSearchProducts
|
@@ -14,12 +14,12 @@
|
||||
@apply flex;
|
||||
}
|
||||
.list{
|
||||
@apply w-full;
|
||||
.top {
|
||||
@screen md {
|
||||
@apply flex justify-between flex-wrap w-full;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
@screen xl {
|
||||
width:75%;
|
||||
}
|
||||
.inner{
|
||||
@screen md {
|
||||
|
@@ -1,8 +1,13 @@
|
||||
import { ProductCard } from '@commerce/types/product'
|
||||
import { Collection, Facet } from '@framework/schema'
|
||||
import React from 'react'
|
||||
import { Collection, Facet, FacetValue, QuerySearchArgs } from '@framework/schema'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { HeadingCommon, ProductList } from 'src/components/common'
|
||||
import BreadcrumbCommon from 'src/components/common/BreadcrumbCommon/BreadcrumbCommon'
|
||||
import SkeletonImage from 'src/components/common/SkeletonCommon/SkeletonImage/SkeletonImage'
|
||||
import { useSearchProducts } from 'src/components/hooks/product'
|
||||
import { DEFAULT_PAGE_SIZE, QUERY_KEY, QUERY_SPLIT_SEPERATOR, ROUTE } from 'src/utils/constanst.utils'
|
||||
import { getFacetIdsFromCodes, getPageFromQuery } from 'src/utils/funtion.utils'
|
||||
import s from './ProductListFilter.module.scss'
|
||||
import ProductsMenuNavigationTablet from './ProductsMenuNavigationTablet/ProductsMenuNavigationTablet'
|
||||
import ProductSort from './ProductSort/ProductSort'
|
||||
@@ -11,6 +16,7 @@ interface ProductListFilterProps {
|
||||
facets: Facet[]
|
||||
collections: Collection[]
|
||||
products: ProductCard[]
|
||||
total: number
|
||||
|
||||
}
|
||||
|
||||
@@ -21,7 +27,64 @@ const BREADCRUMB = [
|
||||
},
|
||||
]
|
||||
|
||||
const ProductListFilter = ({ facets, collections, products }: ProductListFilterProps) => {
|
||||
|
||||
const DEFAULT_SEARCH_ARGS = {
|
||||
groupByProduct: true, take: DEFAULT_PAGE_SIZE
|
||||
}
|
||||
|
||||
const ProductListFilter = ({ facets, collections, products, total }: ProductListFilterProps) => {
|
||||
const router = useRouter()
|
||||
const [initialQueryFlag, setInitialQueryFlag] = useState<boolean>(true)
|
||||
const [optionQueryProduct, setOptionQueryProduct] = useState<QuerySearchArgs>({ input: DEFAULT_SEARCH_ARGS })
|
||||
const { products: productSearchResult, totalItems, loading } = useSearchProducts(optionQueryProduct)
|
||||
const [currentPage, setCurrentPage] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const page = getPageFromQuery(router.query[QUERY_KEY.PAGE] as string)
|
||||
setCurrentPage(page)
|
||||
}, [router.query])
|
||||
|
||||
const onPageChange = (page: number) => {
|
||||
setCurrentPage(page)
|
||||
|
||||
router.push({
|
||||
pathname: ROUTE.PRODUCTS,
|
||||
query: {
|
||||
...router.query,
|
||||
[QUERY_KEY.PAGE]: page
|
||||
}
|
||||
},
|
||||
undefined, { shallow: true }
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const query = { input: { ...DEFAULT_SEARCH_ARGS } } as QuerySearchArgs
|
||||
|
||||
const page = getPageFromQuery(router.query[QUERY_KEY.PAGE] as string)
|
||||
query.input.skip = page * DEFAULT_PAGE_SIZE
|
||||
|
||||
// collections
|
||||
const categoryQuery = router.query[QUERY_KEY.CATEGORY] as string
|
||||
if (categoryQuery) {
|
||||
query.input.collectionSlug = categoryQuery
|
||||
}
|
||||
|
||||
// facets
|
||||
const facetsQuery = [router.query[QUERY_KEY.FEATURED] as string, router.query[QUERY_KEY.BRAND] as string].join(QUERY_SPLIT_SEPERATOR)
|
||||
if (facetsQuery) {
|
||||
const facetsValue = [] as FacetValue[]
|
||||
facets.map((item: Facet) => {
|
||||
facetsValue.push(...item.values)
|
||||
return null
|
||||
})
|
||||
|
||||
query.input.facetValueIds = getFacetIdsFromCodes(facetsValue, facetsQuery.split(QUERY_SPLIT_SEPERATOR))
|
||||
}
|
||||
|
||||
setOptionQueryProduct(query)
|
||||
setInitialQueryFlag(false)
|
||||
}, [router.query, facets])
|
||||
|
||||
return (
|
||||
<div className={s.warpper}>
|
||||
@@ -31,12 +94,17 @@ const ProductListFilter = ({ facets, collections, products }: ProductListFilterP
|
||||
<div className={s.main}>
|
||||
<ProductsMenuNavigationTablet facets={facets} collections={collections} />
|
||||
<div className={s.list}>
|
||||
<div className={s.top}>
|
||||
<HeadingCommon align="left">SPECIAL RECIPES</HeadingCommon>
|
||||
|
||||
<div className={s.boxSelect}>
|
||||
<ProductSort />
|
||||
</div>
|
||||
<ProductList data={products} />
|
||||
</div>
|
||||
{
|
||||
(!initialQueryFlag && loading && !productSearchResult) && <SkeletonImage />
|
||||
}
|
||||
<ProductList data={initialQueryFlag ? products : (productSearchResult || [])} total={totalItems !== undefined ? totalItems : total} onPageChange={onPageChange} defaultCurrentPage={currentPage} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -13,7 +13,11 @@ interface Props {
|
||||
const ProductsMenuNavigationTablet = ({ facets, collections }: Props) => {
|
||||
return (
|
||||
<div className={s.productsMenuNavigationTablet}>
|
||||
<MenuNavigation categories={collections} heading="Categories" queryKey={QUERY_KEY.CATEGORY} />
|
||||
<MenuNavigation
|
||||
heading="Categories"
|
||||
categories={collections}
|
||||
queryKey={QUERY_KEY.CATEGORY}
|
||||
isSingleSelect={true} />
|
||||
{
|
||||
facets.map(item => <MenuNavigation
|
||||
key={item.id}
|
||||
|
@@ -52,7 +52,8 @@ export const QUERY_KEY = {
|
||||
BRAND: 'brand',
|
||||
FEATURED: 'featured',
|
||||
SORTBY: 'sortby',
|
||||
RECIPES: 'recipes'
|
||||
RECIPES: 'recipes',
|
||||
PAGE: 'page',
|
||||
}
|
||||
|
||||
export const PRODUCT_SORT_OPTION_VALUE = {
|
||||
|
@@ -7,6 +7,19 @@ export function isMobile() {
|
||||
return window.innerWidth < 768
|
||||
}
|
||||
|
||||
export function getPageFromQuery(pageQuery: string) {
|
||||
let page = 0
|
||||
try {
|
||||
page = +pageQuery
|
||||
if (isNaN(page)) {
|
||||
page = 0
|
||||
}
|
||||
} catch (err) {
|
||||
page = 0
|
||||
}
|
||||
return page
|
||||
}
|
||||
|
||||
export function removeItem<T>(arr: Array<T>, value: T): Array<T> {
|
||||
const index = arr.indexOf(value);
|
||||
if (index > -1) {
|
||||
@@ -58,6 +71,16 @@ export function getFacetNamesFromIds(facets: FacetValue[], ids?: string[]): stri
|
||||
return names.join(", ")
|
||||
}
|
||||
|
||||
export function getFacetIdsFromCodes(facets: FacetValue[], codes?: string[]): string[] {
|
||||
if (!codes || codes?.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const facetItems = facets.filter((item: FacetValue) => codes.includes(item.code))
|
||||
const ids = facetItems.map((item: FacetValue) => item.id)
|
||||
return ids
|
||||
}
|
||||
|
||||
export function getAllPromies(promies: PromiseWithKey[]) {
|
||||
return promies.map(item => item.promise)
|
||||
}
|
||||
|
Reference in New Issue
Block a user