diff --git a/lib/bigcommerce/api/catalog/handlers/get-products.ts b/lib/bigcommerce/api/catalog/handlers/get-products.ts index 6bc3a8fbf..6cb289c3d 100644 --- a/lib/bigcommerce/api/catalog/handlers/get-products.ts +++ b/lib/bigcommerce/api/catalog/handlers/get-products.ts @@ -1,21 +1,46 @@ -import getAllProducts from '../../operations/get-all-products' +import getAllProducts, { + Products, + Product, +} from '../../operations/get-all-products' import type { ProductsHandlers } from '../products' +const SORT: { [key: string]: string | undefined } = { + latest: 'date_modified', + trending: 'total_sold', + price: 'price', +} +const LIMIT = 12 + // Return current cart info const getProducts: ProductsHandlers['getProducts'] = async ({ res, - body: { search, category, brand }, + body: { search, category, brand, sort }, config, }) => { // Use a dummy base as we only care about the relative path const url = new URL('/v3/catalog/products', 'http://a') + url.searchParams.set('is_visible', 'true') + url.searchParams.set('limit', String(LIMIT)) + if (search) url.searchParams.set('keyword', search) + if (category && Number.isInteger(Number(category))) url.searchParams.set('categories:in', category) + if (brand && Number.isInteger(Number(brand))) url.searchParams.set('brand_id', brand) + if (sort) { + const [_sort, direction] = sort.split('-') + const sortValue = SORT[_sort] + + if (sortValue && direction) { + url.searchParams.set('sort', sortValue) + url.searchParams.set('direction', direction) + } + } + // We only want the id of each product url.searchParams.set('include_fields', 'id') @@ -25,7 +50,25 @@ const getProducts: ProductsHandlers['getProducts'] = async ({ const entityIds = data.map((p) => p.id) const found = entityIds.length > 0 // We want the GraphQL version of each product - const { products } = await getAllProducts({ variables: { entityIds } }) + const graphqlData = await getAllProducts({ + variables: { first: LIMIT, entityIds }, + }) + // Put the products in an object that we can use to get them by id + const productsById = graphqlData.products.reduce<{ [k: number]: Product }>( + (prods, p) => { + prods[p.node.entityId] = p + return prods + }, + {} + ) + const products: Products = found ? [] : graphqlData.products + + // Populate the products array with the graphql products, in the order + // assigned by the list of entity ids + entityIds.forEach((id) => { + const product = productsById[id] + if (product) products.push(product) + }) res.status(200).json({ data: { products, found } }) } diff --git a/lib/bigcommerce/api/catalog/products.ts b/lib/bigcommerce/api/catalog/products.ts index c4a223a35..6732066a4 100644 --- a/lib/bigcommerce/api/catalog/products.ts +++ b/lib/bigcommerce/api/catalog/products.ts @@ -15,7 +15,7 @@ export type SearchProductsData = { export type ProductsHandlers = { getProducts: BigcommerceHandler< SearchProductsData, - { search?: 'string'; category?: string; brand?: string } + { search?: 'string'; category?: string; brand?: string; sort?: string } > } diff --git a/lib/bigcommerce/products/use-search.tsx b/lib/bigcommerce/products/use-search.tsx index 34cc0ed8b..fd3a8ece3 100644 --- a/lib/bigcommerce/products/use-search.tsx +++ b/lib/bigcommerce/products/use-search.tsx @@ -12,11 +12,12 @@ export type SearchProductsInput = { search?: string categoryId?: number brandId?: number + sort?: string } export const fetcher: HookFetcher = ( options, - { search, categoryId, brandId }, + { search, categoryId, brandId, sort }, fetch ) => { // Use a dummy base as we only care about the relative path @@ -27,6 +28,7 @@ export const fetcher: HookFetcher = ( url.searchParams.set('category', String(categoryId)) if (Number.isInteger(categoryId)) url.searchParams.set('brand', String(brandId)) + if (sort) url.searchParams.set('sort', sort) return fetch({ url: url.pathname + url.search, @@ -45,6 +47,7 @@ export function extendHook( ['search', input.search], ['categoryId', input.categoryId], ['brandId', input.brandId], + ['sort', input.sort], ], customFetcher, { revalidateOnFocus: false, ...swrOptions } diff --git a/pages/search.tsx b/pages/search.tsx index ded6957b4..c84fa8d39 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -1,4 +1,3 @@ -import { useEffect, useState } from 'react' import { GetStaticPropsContext, InferGetStaticPropsType } from 'next' import { useRouter } from 'next/router' import Link from 'next/link' @@ -8,6 +7,13 @@ import useSearch from '@lib/bigcommerce/products/use-search' import { Layout } from '@components/core' import { Container, Grid } from '@components/ui' import { ProductCard } from '@components/product' +import { + filterQuery, + getCategoryPath, + getDesignerPath, + getSlug, + useSearchMeta, +} from '@utils/search' export async function getStaticProps({ preview }: GetStaticPropsContext) { const { categories, brands } = await getSiteInfo() @@ -17,7 +23,14 @@ export async function getStaticProps({ preview }: GetStaticPropsContext) { } } -export default function Home({ +const SORT = Object.entries({ + 'latest-desc': 'Latest arrivals', + 'trending-desc': 'Trending', + 'price-asc': 'Price: Low to high', + 'price-desc': 'Price: High to low', +}) + +export default function Search({ categories, brands, }: InferGetStaticPropsType) { @@ -25,9 +38,8 @@ export default function Home({ const { asPath } = router const { q, sort } = router.query const query = filterQuery({ q, sort }) - const pathname = asPath.split('?')[0] - const { category, brand } = useSearchMeta(asPath) + const { pathname, category, brand } = useSearchMeta(asPath) const activeCategory = categories.find( (cat) => getSlug(cat.path) === category ) @@ -39,6 +51,7 @@ export default function Home({ search: typeof q === 'string' ? q : '', categoryId: activeCategory?.entityId, brandId: activeBrand?.entityId, + sort: typeof sort === 'string' ? sort : '', }) return ( @@ -114,62 +127,34 @@ export default function Home({ /> ) : ( + // TODO: add a proper loading state
Searching...
)}
@@ -177,48 +162,4 @@ export default function Home({ ) } -Home.Layout = Layout - -function useSearchMeta(asPath: string) { - const [category, setCategory] = useState() - const [brand, setBrand] = useState() - - useEffect(() => { - const parts = asPath.split('/') - - let c = parts[2] - let b = parts[3] - - if (c === 'designers') { - c = parts[4] - } - - if (c !== category) setCategory(c) - if (b !== brand) setBrand(b) - }, [asPath]) - - return { category, brand } -} - -// Removes empty query parameters from the query object -const filterQuery = (query: any) => - Object.keys(query).reduce((obj, key) => { - if (query[key]?.length) { - obj[key] = query[key] - } - return obj - }, {}) - -// Remove trailing and leading slash -const getSlug = (path: string) => path.replace(/^\/|\/$/g, '') - -const getCategoryPath = (slug: string, brand?: string) => - `/search${brand ? `/designers/${brand}` : ''}${slug ? `/${slug}` : ''}` - -const getDesignerPath = (slug: string, category?: string) => { - const designer = slug.replace(/^brands/, 'designers') - - return `/search${designer ? `/${designer}` : ''}${ - category ? `/${category}` : '' - }` -} +Search.Layout = Layout diff --git a/tsconfig.json b/tsconfig.json index ec924f865..4760611cc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,8 @@ "paths": { "@lib/*": ["lib/*"], "@assets/*": ["assets/*"], - "@components/*": ["components/*"] + "@components/*": ["components/*"], + "@utils/*": ["utils/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"], diff --git a/utils/search.tsx b/utils/search.tsx new file mode 100644 index 000000000..6ef54e9e4 --- /dev/null +++ b/utils/search.tsx @@ -0,0 +1,51 @@ +import { useEffect, useState } from 'react' + +export function useSearchMeta(asPath: string) { + const [pathname, setPathname] = useState('/search') + const [category, setCategory] = useState() + const [brand, setBrand] = useState() + + useEffect(() => { + // Only access asPath after hydration to avoid a server mismatch + const path = asPath.split('?')[0] + const parts = path.split('/') + + console.log('P', parts) + + let c = parts[2] + let b = parts[3] + + if (c === 'designers') { + c = parts[4] + } + + setPathname(path) + if (c !== category) setCategory(c) + if (b !== brand) setBrand(b) + }, [asPath]) + + return { pathname, category, brand } +} + +// Removes empty query parameters from the query object +export const filterQuery = (query: any) => + Object.keys(query).reduce((obj, key) => { + if (query[key]?.length) { + obj[key] = query[key] + } + return obj + }, {}) + +// Remove trailing and leading slash +export const getSlug = (path: string) => path.replace(/^\/|\/$/g, '') + +export const getCategoryPath = (slug: string, brand?: string) => + `/search${brand ? `/designers/${brand}` : ''}${slug ? `/${slug}` : ''}` + +export const getDesignerPath = (slug: string, category?: string) => { + const designer = slug.replace(/^brands/, 'designers') + + return `/search${designer ? `/${designer}` : ''}${ + category ? `/${category}` : '' + }` +}