From 398feac176df5c2bf3f6858c0a5ef38765bbac0a Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Wed, 14 Oct 2020 10:53:03 -0500 Subject: [PATCH 1/5] Add sorting to the API for search --- .../api/catalog/handlers/get-products.ts | 20 ++++++++++++++++++- lib/bigcommerce/api/catalog/products.ts | 2 +- pages/search.tsx | 1 + 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/bigcommerce/api/catalog/handlers/get-products.ts b/lib/bigcommerce/api/catalog/handlers/get-products.ts index 6bc3a8fbf..3d0832409 100644 --- a/lib/bigcommerce/api/catalog/handlers/get-products.ts +++ b/lib/bigcommerce/api/catalog/handlers/get-products.ts @@ -1,21 +1,39 @@ import getAllProducts from '../../operations/get-all-products' import type { ProductsHandlers } from '../products' +const SORT: { [key: string]: string | undefined } = { + latest: 'date_modified', + trending: 'total_sold', + price: 'price', +} + // 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') 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') 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/pages/search.tsx b/pages/search.tsx index ded6957b4..e1fa17cfa 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -114,6 +114,7 @@ export default function Home({ /> ) : ( + // TODO: add a proper loading state
Searching...
)} From cdb2cbdebda73fd6a15541b192279a3a4b2ee4ec Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Wed, 14 Oct 2020 11:50:38 -0500 Subject: [PATCH 2/5] Added sort option to the UI --- lib/bigcommerce/products/use-search.tsx | 5 ++++- pages/search.tsx | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) 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 e1fa17cfa..f2303417c 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -17,7 +17,7 @@ export async function getStaticProps({ preview }: GetStaticPropsContext) { } } -export default function Home({ +export default function Search({ categories, brands, }: InferGetStaticPropsType) { @@ -39,6 +39,7 @@ export default function Home({ search: typeof q === 'string' ? q : '', categoryId: activeCategory?.entityId, brandId: activeBrand?.entityId, + sort: typeof sort === 'string' ? sort : '', }) return ( @@ -178,7 +179,7 @@ export default function Home({ ) } -Home.Layout = Layout +Search.Layout = Layout function useSearchMeta(asPath: string) { const [category, setCategory] = useState() From 32da7ddcc1a4cb4de81452d2f0cc35e23935469e Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Wed, 14 Oct 2020 12:47:22 -0500 Subject: [PATCH 3/5] Updated sorting logic --- .../api/catalog/handlers/get-products.ts | 28 ++++++++-- pages/search.tsx | 52 +++---------------- tsconfig.json | 3 +- utils/search.tsx | 45 ++++++++++++++++ 4 files changed, 79 insertions(+), 49 deletions(-) create mode 100644 utils/search.tsx diff --git a/lib/bigcommerce/api/catalog/handlers/get-products.ts b/lib/bigcommerce/api/catalog/handlers/get-products.ts index 3d0832409..4ae1f4f6e 100644 --- a/lib/bigcommerce/api/catalog/handlers/get-products.ts +++ b/lib/bigcommerce/api/catalog/handlers/get-products.ts @@ -1,4 +1,7 @@ -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 } = { @@ -16,6 +19,9 @@ const getProducts: ProductsHandlers['getProducts'] = async ({ // Use a dummy base as we only care about the relative path const url = new URL('/v3/catalog/products', 'http://a') + // The limit should math the number of products returned by `getAllProducts` + url.searchParams.set('limit', '10') + if (search) url.searchParams.set('keyword', search) if (category && Number.isInteger(Number(category))) @@ -35,7 +41,7 @@ const getProducts: ProductsHandlers['getProducts'] = async ({ } // We only want the id of each product - url.searchParams.set('include_fields', 'id') + url.searchParams.set('include_fields', 'id,price') const { data } = await config.storeApiFetch<{ data: { id: number }[] }>( url.pathname + url.search @@ -43,7 +49,23 @@ 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: { 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 = [] + + // 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/pages/search.tsx b/pages/search.tsx index f2303417c..6fa720288 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() @@ -180,47 +186,3 @@ export default function Search({ } Search.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}` : '' - }` -} 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..192c1e0b6 --- /dev/null +++ b/utils/search.tsx @@ -0,0 +1,45 @@ +import { useEffect, useState } from 'react' + +export 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 +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}` : '' + }` +} From 8905089fd7cc390f5bbfa1d2b10b9dde25763fed Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Wed, 14 Oct 2020 13:12:34 -0500 Subject: [PATCH 4/5] bug fixes --- lib/bigcommerce/api/catalog/handlers/get-products.ts | 2 +- pages/search.tsx | 3 +-- utils/search.tsx | 10 ++++++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/bigcommerce/api/catalog/handlers/get-products.ts b/lib/bigcommerce/api/catalog/handlers/get-products.ts index 4ae1f4f6e..c334dd97b 100644 --- a/lib/bigcommerce/api/catalog/handlers/get-products.ts +++ b/lib/bigcommerce/api/catalog/handlers/get-products.ts @@ -58,7 +58,7 @@ const getProducts: ProductsHandlers['getProducts'] = async ({ }, {} ) - const products: Products = [] + const products: Products = found ? [] : graphqlData.products // Populate the products array with the graphql products, in the order // assigned by the list of entity ids diff --git a/pages/search.tsx b/pages/search.tsx index 6fa720288..3a0bdea67 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -31,9 +31,8 @@ export default function Search({ 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 ) diff --git a/utils/search.tsx b/utils/search.tsx index 192c1e0b6..6ef54e9e4 100644 --- a/utils/search.tsx +++ b/utils/search.tsx @@ -1,11 +1,16 @@ import { useEffect, useState } from 'react' export function useSearchMeta(asPath: string) { + const [pathname, setPathname] = useState('/search') const [category, setCategory] = useState() const [brand, setBrand] = useState() useEffect(() => { - const parts = asPath.split('/') + // 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] @@ -14,11 +19,12 @@ export function useSearchMeta(asPath: string) { c = parts[4] } + setPathname(path) if (c !== category) setCategory(c) if (b !== brand) setBrand(b) }, [asPath]) - return { category, brand } + return { pathname, category, brand } } // Removes empty query parameters from the query object From df24786d1845ebd481a59388893d33810fe4bb5b Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Wed, 14 Oct 2020 13:50:56 -0500 Subject: [PATCH 5/5] Added selected state to sort options --- .../api/catalog/handlers/get-products.ts | 11 +-- pages/search.tsx | 70 +++++++------------ 2 files changed, 31 insertions(+), 50 deletions(-) diff --git a/lib/bigcommerce/api/catalog/handlers/get-products.ts b/lib/bigcommerce/api/catalog/handlers/get-products.ts index c334dd97b..6cb289c3d 100644 --- a/lib/bigcommerce/api/catalog/handlers/get-products.ts +++ b/lib/bigcommerce/api/catalog/handlers/get-products.ts @@ -9,6 +9,7 @@ const SORT: { [key: string]: string | undefined } = { trending: 'total_sold', price: 'price', } +const LIMIT = 12 // Return current cart info const getProducts: ProductsHandlers['getProducts'] = async ({ @@ -19,8 +20,8 @@ const getProducts: ProductsHandlers['getProducts'] = async ({ // Use a dummy base as we only care about the relative path const url = new URL('/v3/catalog/products', 'http://a') - // The limit should math the number of products returned by `getAllProducts` - url.searchParams.set('limit', '10') + url.searchParams.set('is_visible', 'true') + url.searchParams.set('limit', String(LIMIT)) if (search) url.searchParams.set('keyword', search) @@ -41,7 +42,7 @@ const getProducts: ProductsHandlers['getProducts'] = async ({ } // We only want the id of each product - url.searchParams.set('include_fields', 'id,price') + url.searchParams.set('include_fields', 'id') const { data } = await config.storeApiFetch<{ data: { id: number }[] }>( url.pathname + url.search @@ -49,7 +50,9 @@ 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 graphqlData = 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) => { diff --git a/pages/search.tsx b/pages/search.tsx index 3a0bdea67..c84fa8d39 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -23,6 +23,13 @@ export async function getStaticProps({ preview }: GetStaticPropsContext) { } } +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, @@ -127,56 +134,27 @@ export default function Search({