Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add search history & top searches #112

Merged
merged 16 commits into from
Jun 16, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 42 additions & 33 deletions @generated/graphql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -600,39 +600,6 @@ export type Filter_FacetsFragment = {
}>
}

export type SearchSuggestionsQueryQueryVariables = Exact<{
term: Scalars['String']
selectedFacets: InputMaybe<Array<IStoreSelectedFacet> | IStoreSelectedFacet>
}>

export type SearchSuggestionsQueryQuery = {
search: {
suggestions: {
terms: Array<{ value: string }>
products: Array<{
slug: string
sku: string
name: string
gtin: string
id: string
brand: { name: string; brandName: string }
isVariantOf: { productGroupID: string; name: string }
image: Array<{ url: string; alternateName: string }>
offers: {
lowPrice: number
offers: Array<{
availability: string
price: number
listPrice: number
quantity: number
seller: { identifier: string }
}>
}
}>
}
}
}

export type ProductDetailsFragment_ProductFragment = {
sku: string
name: string
Expand Down Expand Up @@ -871,6 +838,48 @@ export type ProductsQueryQuery = {
}
}

export type SearchSuggestionsQueryQueryVariables = Exact<{
term: Scalars['String']
selectedFacets: InputMaybe<Array<IStoreSelectedFacet> | IStoreSelectedFacet>
}>

export type SearchSuggestionsQueryQuery = {
search: {
suggestions: {
terms: Array<{ value: string }>
products: Array<{
slug: string
sku: string
name: string
gtin: string
id: string
brand: { name: string; brandName: string }
isVariantOf: { productGroupID: string; name: string }
image: Array<{ url: string; alternateName: string }>
offers: {
lowPrice: number
offers: Array<{
availability: string
price: number
listPrice: number
quantity: number
seller: { identifier: string }
}>
}
}>
}
}
}

export type TopSearchSuggestionsQueryQueryVariables = Exact<{
term: Scalars['String']
selectedFacets: InputMaybe<Array<IStoreSelectedFacet> | IStoreSelectedFacet>
}>

export type TopSearchSuggestionsQueryQuery = {
search: { suggestions: { terms: Array<{ value: string }> } }
}

export type ValidateSessionMutationVariables = Exact<{
session: IStoreSession
search: Scalars['String']
Expand Down
3 changes: 2 additions & 1 deletion @generated/graphql/persisted.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
{
"SearchSuggestionsQuery": "query SearchSuggestionsQuery($term: String!, $selectedFacets: [IStoreSelectedFacet!]) {\n search(first: 5, term: $term, selectedFacets: $selectedFacets) {\n suggestions {\n terms {\n value\n }\n products {\n id: productID\n slug\n sku\n brand {\n brandName: name\n name\n }\n name\n gtin\n isVariantOf {\n productGroupID\n name\n }\n image {\n url\n alternateName\n }\n offers {\n lowPrice\n offers {\n availability\n price\n listPrice\n quantity\n seller {\n identifier\n }\n }\n }\n }\n }\n }\n}\n",
"ProductGalleryQuery": "query ProductGalleryQuery($first: Int!, $after: String!, $sort: StoreSort!, $term: String!, $selectedFacets: [IStoreSelectedFacet!]!) {\n search(\n first: $first\n after: $after\n sort: $sort\n term: $term\n selectedFacets: $selectedFacets\n ) {\n products {\n pageInfo {\n totalCount\n }\n }\n facets {\n key\n label\n type\n values {\n label\n value\n selected\n quantity\n }\n }\n }\n}\n",
"ServerCollectionPageQuery": "query ServerCollectionPageQuery($slug: String!) {\n collection(slug: $slug) {\n seo {\n title\n description\n }\n breadcrumbList {\n itemListElement {\n item\n name\n position\n }\n }\n meta {\n selectedFacets {\n key\n value\n }\n }\n }\n}\n",
"ServerProductPageQuery": "query ServerProductPageQuery($slug: String!) {\n product(locator: [{key: \"slug\", value: $slug}]) {\n id: productID\n seo {\n title\n description\n canonical\n }\n brand {\n name\n }\n sku\n gtin\n name\n description\n breadcrumbList {\n itemListElement {\n item\n name\n position\n }\n }\n image {\n url\n alternateName\n }\n offers {\n lowPrice\n highPrice\n priceCurrency\n offers {\n availability\n price\n priceValidUntil\n priceCurrency\n itemCondition\n seller {\n identifier\n }\n listPrice\n }\n }\n isVariantOf {\n productGroupID\n name\n }\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n }\n}\n",
"ValidateCartMutation": "mutation ValidateCartMutation($cart: IStoreCart!) {\n validateCart(cart: $cart) {\n order {\n orderNumber\n acceptedOffer {\n seller {\n identifier\n }\n quantity\n price\n listPrice\n itemOffered {\n sku\n name\n image {\n url\n alternateName\n }\n brand {\n name\n }\n isVariantOf {\n productGroupID\n name\n }\n gtin\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n }\n }\n }\n messages {\n text\n status\n }\n }\n}\n",
"BrowserProductQuery": "query BrowserProductQuery($locator: [IStoreSelectedFacet!]!) {\n product(locator: $locator) {\n id: productID\n sku\n name\n gtin\n description\n isVariantOf {\n productGroupID\n name\n }\n image {\n url\n alternateName\n }\n brand {\n name\n }\n offers {\n lowPrice\n offers {\n availability\n price\n listPrice\n seller {\n identifier\n }\n }\n }\n breadcrumbList {\n itemListElement {\n item\n name\n position\n }\n }\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n }\n}\n",
"ProductsQuery": "query ProductsQuery($first: Int!, $after: String, $sort: StoreSort!, $term: String!, $selectedFacets: [IStoreSelectedFacet!]!) {\n search(\n first: $first\n after: $after\n sort: $sort\n term: $term\n selectedFacets: $selectedFacets\n ) {\n products {\n pageInfo {\n totalCount\n }\n edges {\n node {\n id: productID\n slug\n sku\n brand {\n brandName: name\n name\n }\n name\n gtin\n isVariantOf {\n productGroupID\n name\n }\n image {\n url\n alternateName\n }\n offers {\n lowPrice\n offers {\n availability\n price\n listPrice\n quantity\n seller {\n identifier\n }\n }\n }\n }\n }\n }\n }\n}\n",
"SearchSuggestionsQuery": "query SearchSuggestionsQuery($term: String!, $selectedFacets: [IStoreSelectedFacet!]) {\n search(first: 5, term: $term, selectedFacets: $selectedFacets) {\n suggestions {\n terms {\n value\n }\n products {\n id: productID\n slug\n sku\n brand {\n brandName: name\n name\n }\n name\n gtin\n isVariantOf {\n productGroupID\n name\n }\n image {\n url\n alternateName\n }\n offers {\n lowPrice\n offers {\n availability\n price\n listPrice\n quantity\n seller {\n identifier\n }\n }\n }\n }\n }\n }\n}\n",
"TopSearchSuggestionsQuery": "query TopSearchSuggestionsQuery($term: String!, $selectedFacets: [IStoreSelectedFacet!]) {\n search(first: 5, term: $term, selectedFacets: $selectedFacets) {\n suggestions {\n terms {\n value\n }\n }\n }\n}\n",
"ValidateSession": "mutation ValidateSession($session: IStoreSession!, $search: String!) {\n validateSession(session: $session, search: $search) {\n locale\n channel\n country\n postalCode\n currency {\n code\n symbol\n }\n person {\n id\n email\n givenName\n familyName\n }\n }\n}\n"
}
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- 301 redirects when visiting old VTEX product routes ([#93](https://github.com/vtex-sites/nextjs.store/pull/93))

- The search input now includes the last 4 previously searched terms (`SearchHistory`) ([#112](https://github.com/vtex-sites/nextjs.store/pull/112)).
- The top 5 searches (`SuggestionsTopSearch`) are now integrated into the search input ([#112](https://github.com/vtex-sites/nextjs.store/pull/112)).
- 301 redirects when visiting old VTEX product routes ([#93](https://github.com/vtex-sites/nextjs.store/pull/93))
- Support for the new type definitions from React 18 ([#113](https://github.com/vtex-sites/nextjs.store/pull/113)).

### Changed
Expand All @@ -21,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed

### Fixed

- The search input's dropdown not closing when a suggested product was selected ([#112](https://github.com/vtex-sites/nextjs.store/pull/112).
- Status code when error occurs (404/500) ([#116](https://github.com/vtex-sites/nextjs.store/pull/116))

### Security
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/Navbar/navbar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
order: 0;
width: min-content;

[data-store-input] {
&[data-store-search-input-dropdown-open="false"] [data-store-input] {
width: 0;
padding: 0;
border-width: 0;
Expand Down
110 changes: 53 additions & 57 deletions src/components/common/SearchInput/SearchInput.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import {
formatSearchState,
initSearchState,
sendAnalyticsEvent,
} from '@faststore/sdk'
import { sendAnalyticsEvent } from '@faststore/sdk'
import { SearchInput as UISearchInput } from '@faststore/ui'
import { useRouter } from 'next/router'
import {
forwardRef,
lazy,
Suspense,
useCallback,
useRef,
useState,
useDeferredValue,
} from 'react'
import type { SearchEvent } from '@faststore/sdk'
import type {
Expand All @@ -21,6 +17,11 @@ import type {

import Icon from 'src/components/ui/Icon'
import useSearchHistory from 'src/sdk/search/useSearchHistory'
import {
formatSearchPath,
SearchInputProvider,
} from 'src/sdk/search/useSearchInput'
import type { SearchInputContextValue } from 'src/sdk/search/useSearchInput'
import useOnClickOutside from 'src/sdk/ui/useOnClickOutside'

const Suggestions = lazy(() => import('src/components/search/Suggestions'))
Expand All @@ -30,30 +31,11 @@ declare type SearchInputProps = {
buttonTestId?: string
} & Omit<UISearchInputProps, 'onSubmit'>

const useSearchHandler = (callback: (term: string) => void) => {
const router = useRouter()
const { addToSearchHistory } = useSearchHistory()

return useCallback(
(term: string) => {
const { pathname, search } = formatSearchState(
initSearchState({
term,
base: '/s',
})
)

sendAnalyticsEvent<SearchEvent>({
name: 'search',
params: { search_term: term },
})

addToSearchHistory(term)
callback(term)
router.push(`${pathname}${search}`)
},
[addToSearchHistory, callback, router]
)
const sendAnalytics = async (term: string) => {
sendAnalyticsEvent<SearchEvent>({
name: 'search',
params: { search_term: term },
})
}

const SearchInput = forwardRef<SearchInputRef, SearchInputProps>(
Expand All @@ -62,12 +44,19 @@ const SearchInput = forwardRef<SearchInputRef, SearchInputProps>(
ref
) {
const [searchQuery, setSearchQuery] = useState<string>('')
const searchQueryDeferred = useDeferredValue(searchQuery)
const [suggestionsOpen, setSuggestionsOpen] = useState<boolean>(false)
const searchRef = useRef<HTMLDivElement>(null)
const handleSearch = useSearchHandler((term: string) => {
setSuggestionsOpen(false)
setSearchQuery(term)
})
const { addToSearchHistory } = useSearchHistory()
const router = useRouter()

const onSearchInputSelection: SearchInputContextValue['onSearchInputSelection'] =
(term, path) => {
addToSearchHistory({ term, path })
sendAnalytics(term)
setSuggestionsOpen(false)
setSearchQuery(term)
}

useOnClickOutside(searchRef, () => setSuggestionsOpen(false))

Expand All @@ -77,29 +66,36 @@ const SearchInput = forwardRef<SearchInputRef, SearchInputProps>(
data-store-search-input-wrapper
data-store-search-input-dropdown-open={suggestionsOpen}
>
<UISearchInput
ref={ref}
icon={
<Icon
name="MagnifyingGlass"
onClick={onSearchClick}
data-testid={buttonTestId}
/>
}
placeholder="Search everything at the store"
onChange={(e) => setSearchQuery(e.target.value)}
onSubmit={handleSearch}
onFocus={() => setSuggestionsOpen(true)}
value={searchQuery}
{...props}
/>
{suggestionsOpen && (
<Suspense fallback={null}>
<div data-store-search-input-dropdown-wrapper>
<Suggestions term={searchQuery} onSearch={handleSearch} />
</div>
</Suspense>
)}
<SearchInputProvider onSearchInputSelection={onSearchInputSelection}>
<UISearchInput
ref={ref}
icon={
<Icon
name="MagnifyingGlass"
onClick={onSearchClick}
data-testid={buttonTestId}
/>
}
placeholder="Search everything at the store"
onChange={(e) => setSearchQuery(e.target.value)}
onSubmit={(term) => {
const path = formatSearchPath(term)

onSearchInputSelection(term, path)
router.push(path)
}}
onFocus={() => setSuggestionsOpen(true)}
value={searchQuery}
{...props}
/>
{suggestionsOpen && (
<Suspense fallback={null}>
<div data-store-search-input-dropdown-wrapper>
<Suggestions term={searchQueryDeferred} />
</div>
</Suspense>
)}
</SearchInputProvider>
</div>
)
}
Expand Down
8 changes: 8 additions & 0 deletions src/components/common/SearchInput/search-input.scss
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@

[data-store-search-input-wrapper] {
position: relative;

[data-fs-search-suggestion-section] + [data-fs-search-input-loading-text] {
padding-top: var(--fs-spacing-3);
}
}

[data-store-search-input-dropdown-wrapper] {
Expand All @@ -90,6 +94,10 @@
padding: var(--fs-spacing-3);
background-color: var(--fs-color-neutral-0);

&:empty {
display: none;
}

@include media(">=tablet") {
top: calc(var(--top) - 1px);
left: calc(var(--left) - var(--fs-spacing-1));
Expand Down
37 changes: 24 additions & 13 deletions src/components/search/History/SearchHistory.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { SearchInputProvider } from 'src/sdk/search/useSearchInput'

import { SearchHistory } from '.'
import type { SearchHistoryProps } from '.'

Expand All @@ -6,23 +8,32 @@ const meta = {
title: 'Organisms/Search/History',
}

const Template = (props: SearchHistoryProps) => (
<div
style={{
maxWidth: '600px',
margin: '0 auto',
padding: '0 16px',
background: 'white',
}}
>
<SearchHistory {...props} />
</div>
)
const Template = (props: SearchHistoryProps) => {
return (
<div
style={{
maxWidth: '600px',
margin: '0 auto',
padding: '0 16px',
background: 'white',
}}
>
<SearchInputProvider>
<SearchHistory {...props} />
</SearchInputProvider>
</div>
)
}

export const Default = Template.bind({})

Default.args = {
history: ['headphone', 'audio & video', 'mh-7000', 'jbl go'],
history: [
{ term: 'headphone', path: '/' },
{ term: 'audio & video', path: '/' },
{ term: 'mh-7000', path: '/' },
{ term: 'jbl go', path: '/' },
],
}

Default.parameters = {
Expand Down
Loading