diff --git a/components/search.tsx b/components/search.tsx new file mode 100644 index 000000000..9987fd890 --- /dev/null +++ b/components/search.tsx @@ -0,0 +1,439 @@ +import cn from 'classnames' +import type { SearchPropsType } from '@lib/search-props' +import Link from 'next/link' +import { useState } from 'react' +import { useRouter } from 'next/router' + +import { Layout } from '@components/common' +import { ProductCard } from '@components/product' +import type { Product } from '@commerce/types/product' +import { Container, Grid, Skeleton } from '@components/ui' + +import useSearch from '@framework/product/use-search' + +import getSlug from '@lib/get-slug' +import rangeMap from '@lib/range-map' + +const SORT = Object.entries({ + 'trending-desc': 'Trending', + 'latest-desc': 'Latest arrivals', + 'price-asc': 'Price: Low to high', + 'price-desc': 'Price: High to low', +}) + +import { + filterQuery, + getCategoryPath, + getDesignerPath, + useSearchMeta, +} from '@lib/search' + +export default function Search({ categories, brands }: SearchPropsType) { + const [activeFilter, setActiveFilter] = useState('') + const [toggleFilter, setToggleFilter] = useState(false) + + const router = useRouter() + const { asPath, locale } = router + const { q, sort } = router.query + // `q` can be included but because categories and designers can't be searched + // in the same way of products, it's better to ignore the search input if one + // of those is selected + const query = filterQuery({ sort }) + + const { pathname, category, brand } = useSearchMeta(asPath) + const activeCategory = categories.find((cat) => cat.slug === category) + const activeBrand = brands.find( + (b) => getSlug(b.node.path) === `brands/${brand}` + )?.node + + const { data } = useSearch({ + search: typeof q === 'string' ? q : '', + categoryId: activeCategory?.id, + brandId: (activeBrand as any)?.entityId, + sort: typeof sort === 'string' ? sort : '', + locale, + }) + + const handleClick = (event: any, filter: string) => { + if (filter !== activeFilter) { + setToggleFilter(true) + } else { + setToggleFilter(!toggleFilter) + } + setActiveFilter(filter) + } + + return ( + +
+
+ {/* Categories */} +
+
+ + + +
+
+ +
+
+ + {/* Designs */} +
+
+ + + +
+
+ +
+
+
+ {/* Products */} +
+ {(q || activeCategory || activeBrand) && ( +
+ {data ? ( + <> + + Showing {data.products.length} results{' '} + {q && ( + <> + for "{q}" + + )} + + + {q ? ( + <> + There are no products that match "{q}" + + ) : ( + <> + There are no products that match the selected category. + + )} + + + ) : q ? ( + <> + Searching for: "{q}" + + ) : ( + <>Searching... + )} +
+ )} + + {data ? ( +
+ {data.products.map((product: Product) => ( + + ))} +
+ ) : ( +
+ {rangeMap(12, (i) => ( + +
+ + ))} +
+ )}
+ + {/* Sort */} +
+
+
+ + + +
+ +
+
+
+ + ) +} + +Search.Layout = Layout diff --git a/lib/search-props.tsx b/lib/search-props.tsx new file mode 100644 index 000000000..6910e0d83 --- /dev/null +++ b/lib/search-props.tsx @@ -0,0 +1,27 @@ +import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next' + +import commerce from '@lib/api/commerce' + +export async function getSearchStaticProps({ + preview, + locale, + locales, +}: GetStaticPropsContext) { + const config = { locale, locales } + const pagesPromise = commerce.getAllPages({ config, preview }) + const siteInfoPromise = commerce.getSiteInfo({ config, preview }) + const { pages } = await pagesPromise + const { categories, brands } = await siteInfoPromise + return { + props: { + pages, + categories, + brands, + }, + revalidate: 200, + } +} + +export type SearchPropsType = InferGetStaticPropsType< + typeof getSearchStaticProps +> diff --git a/next.config.js b/next.config.js index 607d4eba8..3b3f64c6a 100644 --- a/next.config.js +++ b/next.config.js @@ -35,20 +35,6 @@ module.exports = withCommerceConfig({ source: `${process.env.NEXT_PUBLIC_VENDURE_LOCAL_URL}/:path*`, destination: `${process.env.NEXT_PUBLIC_VENDURE_SHOP_API_URL}/:path*`, }, - // Rewrites for /search - { - source: '/search/designers/:name', - destination: '/search', - }, - { - source: '/search/designers/:name/:category', - destination: '/search', - }, - { - // This rewrite will also handle `/search/designers` - source: '/search/:category', - destination: '/search', - }, ].filter(Boolean) }, }) diff --git a/pages/search.tsx b/pages/search.tsx index 15d1d7571..23f0ee04e 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -1,463 +1,9 @@ -import cn from 'classnames' -import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next' -import Link from 'next/link' -import { useState } from 'react' -import { useRouter } from 'next/router' +import { getSearchStaticProps } from '@lib/search-props' +import type { GetStaticPropsContext } from 'next' +import Search from '@components/search' -import { Layout } from '@components/common' -import { ProductCard } from '@components/product' -import type { Product } from '@commerce/types/product' -import { Container, Skeleton } from '@components/ui' - -import useSearch from '@framework/product/use-search' -import commerce from '@lib/api/commerce' -import rangeMap from '@lib/range-map' - -import { - filterQuery, - getCategoryPath, - getDesignerPath, - useSearchMeta, -} from '@lib/search' - -// TODO(bc) Remove this. This should come from the API -import getSlug from '@lib/get-slug' - -const SORT = Object.entries({ - 'trending-desc': 'Trending', - 'latest-desc': 'Latest arrivals', - 'price-asc': 'Price: Low to high', - 'price-desc': 'Price: High to low', -}) - -export async function getStaticProps({ - preview, - locale, - locales, -}: GetStaticPropsContext) { - const config = { locale, locales } - const { pages } = await commerce.getAllPages({ config, preview }) - const { categories, brands } = await commerce.getSiteInfo({ config, preview }) - return { - props: { - pages, - categories, - brands, - }, - revalidate: 200, - } +export async function getStaticProps(context: GetStaticPropsContext) { + return getSearchStaticProps(context) } -export default function Search({ - categories, - brands, -}: InferGetStaticPropsType) { - const [activeFilter, setActiveFilter] = useState('') - const [toggleFilter, setToggleFilter] = useState(false) - - const router = useRouter() - const { asPath, locale } = router - const { q, sort } = router.query - // `q` can be included but because categories and designers can't be searched - // in the same way of products, it's better to ignore the search input if one - // of those is selected - const query = filterQuery({ sort }) - - const { pathname, category, brand } = useSearchMeta(asPath) - const activeCategory = categories.find((cat) => cat.slug === category) - const activeBrand = brands.find( - (b) => getSlug(b.node.path) === `brands/${brand}` - )?.node - - const { data } = useSearch({ - search: typeof q === 'string' ? q : '', - categoryId: activeCategory?.id, - brandId: (activeBrand as any)?.entityId, - sort: typeof sort === 'string' ? sort : '', - locale, - }) - - const handleClick = (event: any, filter: string) => { - if (filter !== activeFilter) { - setToggleFilter(true) - } else { - setToggleFilter(!toggleFilter) - } - setActiveFilter(filter) - } - - return ( - -
-
- {/* Categories */} -
-
- - - -
- -
- - {/* Designs */} -
-
- - - -
- -
-
- {/* Products */} -
- {(q || activeCategory || activeBrand) && ( -
- {data ? ( - <> - - Showing {data.products.length} results{' '} - {q && ( - <> - for "{q}" - - )} - - - {q ? ( - <> - There are no products that match "{q}" - - ) : ( - <> - There are no products that match the selected category. - - )} - - - ) : q ? ( - <> - Searching for: "{q}" - - ) : ( - <>Searching... - )} -
- )} - - {data ? ( -
- {data.products.map((product: Product) => ( - - ))} -
- ) : ( -
- {rangeMap(12, (i) => ( - -
- - ))} -
- )} -
- - {/* Sort */} -
-
-
- - - -
- -
-
-
- - ) -} - -Search.Layout = Layout +export default Search diff --git a/pages/search/[category].tsx b/pages/search/[category].tsx new file mode 100644 index 000000000..02eddd035 --- /dev/null +++ b/pages/search/[category].tsx @@ -0,0 +1,16 @@ +import { getSearchStaticProps } from '@lib/search-props' +import type { GetStaticPathsResult, GetStaticPropsContext } from 'next' +import Search from '@components/search' + +export async function getStaticProps(context: GetStaticPropsContext) { + return getSearchStaticProps(context) +} + +export function getStaticPaths(): GetStaticPathsResult { + return { + paths: [], + fallback: 'blocking', + } +} + +export default Search diff --git a/pages/search/designers/[name].tsx b/pages/search/designers/[name].tsx new file mode 100644 index 000000000..02eddd035 --- /dev/null +++ b/pages/search/designers/[name].tsx @@ -0,0 +1,16 @@ +import { getSearchStaticProps } from '@lib/search-props' +import type { GetStaticPathsResult, GetStaticPropsContext } from 'next' +import Search from '@components/search' + +export async function getStaticProps(context: GetStaticPropsContext) { + return getSearchStaticProps(context) +} + +export function getStaticPaths(): GetStaticPathsResult { + return { + paths: [], + fallback: 'blocking', + } +} + +export default Search diff --git a/pages/search/designers/[name]/[category].tsx b/pages/search/designers/[name]/[category].tsx new file mode 100644 index 000000000..02eddd035 --- /dev/null +++ b/pages/search/designers/[name]/[category].tsx @@ -0,0 +1,16 @@ +import { getSearchStaticProps } from '@lib/search-props' +import type { GetStaticPathsResult, GetStaticPropsContext } from 'next' +import Search from '@components/search' + +export async function getStaticProps(context: GetStaticPropsContext) { + return getSearchStaticProps(context) +} + +export function getStaticPaths(): GetStaticPathsResult { + return { + paths: [], + fallback: 'blocking', + } +} + +export default Search