@graphcommerce/algolia-search 7.0.0-canary.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/Config.graphqls +72 -0
- package/README.md +111 -0
- package/components/Chip/FilterChip/RefinementFilterChip.tsx +86 -0
- package/components/Chip/FilterChip/RefinementRangeChip.tsx +79 -0
- package/components/Chip/RenderChip.tsx +24 -0
- package/components/Chip/SortChip/SortChip.tsx +85 -0
- package/components/Filters/AlgoliaFilters.tsx +70 -0
- package/components/Pagination/AlgoliaPagination.tsx +30 -0
- package/components/SearchBox/SearchBox.tsx +58 -0
- package/hooks/useAlgoliaCategoryResults.ts +25 -0
- package/hooks/useAlgoliaPageResults.ts +26 -0
- package/hooks/useAlgoliaProductResults.ts +59 -0
- package/hooks/useAlgoliaSearchIndexConfig.ts +8 -0
- package/hooks/useSearchRoute.ts +6 -0
- package/index.ts +0 -0
- package/lib/configuration.ts +2 -0
- package/lib/types.ts +58 -0
- package/next-env.d.ts +4 -0
- package/package.json +40 -0
- package/plugins/AlgoliaCategorySearchResults.tsx +45 -0
- package/plugins/AlgoliaFiltersPlugin.tsx +14 -0
- package/plugins/AlgoliaPageSearchResults.tsx +52 -0
- package/plugins/AlgoliaPaginationPlugin.tsx +18 -0
- package/plugins/AlgoliaProductListCountPlugin.tsx +24 -0
- package/plugins/AlgoliaProductSearchResultsPlugin.tsx +26 -0
- package/plugins/AlgoliaProductSortPlugin.tsx +28 -0
- package/plugins/AlgoliaSearchContextPlugin.tsx +32 -0
- package/plugins/AlgoliaSearchFieldPlugin.tsx +14 -0
- package/tsconfig.json +5 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @graphcommerce/algolia-search
|
|
2
|
+
|
|
3
|
+
## 7.0.0-canary.12
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- [#1909](https://github.com/graphcommerce-org/graphcommerce/pull/1909) [`7a1f1bb38`](https://github.com/graphcommerce-org/graphcommerce/commit/7a1f1bb382ece4167bd3816d6f2cc41ffae56710) - New Algolia search package! ([@mikekeehnen](https://github.com/mikekeehnen))
|
package/Config.graphqls
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Type for sortable algolia options
|
|
3
|
+
"""
|
|
4
|
+
input AlgoliaSortableOption {
|
|
5
|
+
"""
|
|
6
|
+
The label of the index to display
|
|
7
|
+
"""
|
|
8
|
+
label: String!
|
|
9
|
+
"""
|
|
10
|
+
The name of the index to target.
|
|
11
|
+
"""
|
|
12
|
+
value: String!
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Type for conversion of Magento 2 aggregations to Algolia filterable attributes
|
|
17
|
+
"""
|
|
18
|
+
input AlgoliaFilterAttribute {
|
|
19
|
+
"""
|
|
20
|
+
Stores the default aggregation uid
|
|
21
|
+
"""
|
|
22
|
+
aggregation: String!
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
Stores the algolia attribute that should be connected to the magento aggregation
|
|
26
|
+
"""
|
|
27
|
+
toAlgoliaAttribute: String!
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
Type for search index config
|
|
32
|
+
"""
|
|
33
|
+
input AlgoliaSearchIndexConfig {
|
|
34
|
+
"""
|
|
35
|
+
Configure your Algolia Search index for Magento products
|
|
36
|
+
"""
|
|
37
|
+
searchIndex: String!
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
Configures Algolia filterable attributes
|
|
41
|
+
"""
|
|
42
|
+
filterAttributes: [AlgoliaFilterAttribute!]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
extend input GraphCommerceStorefrontConfig {
|
|
46
|
+
"""
|
|
47
|
+
Configure your Algolia index configurations
|
|
48
|
+
"""
|
|
49
|
+
algoliaSearchIndexConfig: [AlgoliaSearchIndexConfig!]!
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
Configure the sortable attributes
|
|
53
|
+
"""
|
|
54
|
+
sortOptions: [AlgoliaSortableOption!]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
extend input GraphCommerceConfig {
|
|
58
|
+
"""
|
|
59
|
+
Configure your Algolia application ID.
|
|
60
|
+
"""
|
|
61
|
+
algoliaApplicationId: String!
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
Configure your Algolia Search Only API Key
|
|
65
|
+
"""
|
|
66
|
+
algoliaSearchOnlyApiKey: String!
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
Configures algolia search debounce time. This will slow down the search response.
|
|
70
|
+
"""
|
|
71
|
+
algoliaSearchDebounceTime: Int
|
|
72
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Algolia Search
|
|
2
|
+
|
|
3
|
+
Implementation of Algolia Instant Search inside Graphcommerce. Add client or
|
|
4
|
+
server side product, category and pages search to your project!
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
1. Find current version of your `@graphcommerce/next-ui` in your package.json.
|
|
9
|
+
2. `yarn add @graphcommerce/algolia-search@1.2.3` (replace 1.2.3 with the
|
|
10
|
+
version of the step above)
|
|
11
|
+
|
|
12
|
+
## Add config values to Graphcommerce configuration
|
|
13
|
+
|
|
14
|
+
This plugin contains different app and storefront configuration values.
|
|
15
|
+
|
|
16
|
+
App configuration values:
|
|
17
|
+
|
|
18
|
+
- algoliaApplicationId
|
|
19
|
+
- algoliaSearchOnlyApiKey
|
|
20
|
+
- algoliaSearchDebounceTime,
|
|
21
|
+
|
|
22
|
+
Storefront configuration values:
|
|
23
|
+
|
|
24
|
+
- algoliaSearchIndexConfig (containing a list of the following values)
|
|
25
|
+
- searchIndex
|
|
26
|
+
- filterAttributes (containing a list of the following values)
|
|
27
|
+
- aggregation
|
|
28
|
+
- toAlgoliaAttribute
|
|
29
|
+
|
|
30
|
+
## Add server side hydration to Algolia Search
|
|
31
|
+
|
|
32
|
+
1. Add `react-instantsearch-hooks-server` package to your project
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
yarn add react-instantsearch-hooks-server
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
or
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
npm install react-instantsearch-hooks-server
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
2. Add the new serverState property to the `SearchResultProps` type
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
type SearchResultProps = DefaultPageQuery &
|
|
49
|
+
ProductListQuery &
|
|
50
|
+
ProductFiltersQuery &
|
|
51
|
+
CategorySearchQuery & {
|
|
52
|
+
filterTypes: FilterTypes
|
|
53
|
+
params: ProductListParams
|
|
54
|
+
+ serverState?: unknown
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
3. Add the `getServerState` method from the `react-instantsearch-hooks-server`
|
|
59
|
+
package to the imports of your search page
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
...
|
|
63
|
+
import { getServerState } from 'react-instantsearch-hooks-server'
|
|
64
|
+
...
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
4. Assign the result of the `getServerState` method to the `serverState`
|
|
68
|
+
attribute inside of the return statement.
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
return {
|
|
72
|
+
props: {
|
|
73
|
+
...(await page).data,
|
|
74
|
+
...(await products).data,
|
|
75
|
+
...(await filters).data,
|
|
76
|
+
...(await categories)?.data,
|
|
77
|
+
...(await layout)?.data,
|
|
78
|
+
filterTypes: await filterTypes,
|
|
79
|
+
params: productListParams,
|
|
80
|
+
up: { href: '/', title: 'Home' },
|
|
81
|
+
apolloState: await conf.then(() => client.cache.extract()),
|
|
82
|
+
+ serverState: await getServerState(<SearchContext />, {
|
|
83
|
+
+ renderToString,
|
|
84
|
+
+ }),
|
|
85
|
+
},
|
|
86
|
+
revalidate: 60 * 20,
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
5. Add the `serverState` to the `SearchContext` component.
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
+ const { products, categories, params, filters, filterTypes, serverState } = props
|
|
94
|
+
const search = params.url.split('/')[1]
|
|
95
|
+
const totalSearchResults = (categories?.items?.length ?? 0) + (products?.total_count ?? 0)
|
|
96
|
+
const noSearchResults = search && (!products || (products.items && products?.items?.length <= 0))
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<>
|
|
100
|
+
<PageMeta
|
|
101
|
+
title={
|
|
102
|
+
search
|
|
103
|
+
? i18n._(/* i18n */ 'Results for ‘{search}’', { search })
|
|
104
|
+
: i18n._(/* i18n */ 'Search')
|
|
105
|
+
}
|
|
106
|
+
metaRobots={['noindex']}
|
|
107
|
+
canonical='/search'
|
|
108
|
+
/>
|
|
109
|
+
|
|
110
|
+
+ <SearchContext serverProps={serverState}>
|
|
111
|
+
```
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ChipMenu, extendableComponent, responsiveVal } from '@graphcommerce/next-ui'
|
|
2
|
+
import { SxProps, Theme } from '@mui/material'
|
|
3
|
+
import Box from '@mui/material/Box'
|
|
4
|
+
import Checkbox from '@mui/material/Checkbox'
|
|
5
|
+
import ListItem from '@mui/material/ListItem'
|
|
6
|
+
import ListItemText from '@mui/material/ListItemText'
|
|
7
|
+
import {
|
|
8
|
+
useClearRefinements,
|
|
9
|
+
UseClearRefinementsProps,
|
|
10
|
+
useRefinementList,
|
|
11
|
+
UseRefinementListProps,
|
|
12
|
+
} from 'react-instantsearch-hooks-web'
|
|
13
|
+
|
|
14
|
+
const name = 'RefinementFilterChip' as const
|
|
15
|
+
const parts = ['menu', 'item'] as const
|
|
16
|
+
const { classes } = extendableComponent(name, parts)
|
|
17
|
+
|
|
18
|
+
export interface RefinementFilterChipProps
|
|
19
|
+
extends Omit<UseRefinementListProps, 'transformItems'>,
|
|
20
|
+
Omit<UseClearRefinementsProps, 'transformItems'> {
|
|
21
|
+
transformItems?: UseClearRefinementsProps['transformItems']
|
|
22
|
+
title: string
|
|
23
|
+
sx?: SxProps<Theme>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function RefinementFilterChip(props: RefinementFilterChipProps) {
|
|
27
|
+
const { title, sx, attribute, transformItems } = props
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
29
|
+
const { items, refine } = useRefinementList({
|
|
30
|
+
attribute,
|
|
31
|
+
})
|
|
32
|
+
const clearRefinementApi = useClearRefinements({
|
|
33
|
+
includedAttributes: [attribute],
|
|
34
|
+
transformItems,
|
|
35
|
+
})
|
|
36
|
+
const selectedOptions = items.filter((option) => option.isRefined).map((option) => option.label)
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<ChipMenu
|
|
40
|
+
className={classes.menu}
|
|
41
|
+
variant='outlined'
|
|
42
|
+
selected={selectedOptions.length > 0}
|
|
43
|
+
label={title}
|
|
44
|
+
selectedLabel={selectedOptions.length ? selectedOptions.join(', ') : title}
|
|
45
|
+
onDelete={selectedOptions.length > 0 ? () => clearRefinementApi.refine() : undefined}
|
|
46
|
+
sx={Array.isArray(sx) ? sx : [sx]}
|
|
47
|
+
>
|
|
48
|
+
<Box
|
|
49
|
+
sx={{
|
|
50
|
+
display: 'grid',
|
|
51
|
+
gridTemplateColumns: { xs: 'repeat(1, minmax(0, 1fr))', md: 'repeat(2, 1fr)' },
|
|
52
|
+
columnGap: responsiveVal(2, 10),
|
|
53
|
+
minWidth: 0,
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
{items.map((option) => (
|
|
57
|
+
<ListItem className={classes.item} key={option?.value ?? ''} dense>
|
|
58
|
+
<ListItemText
|
|
59
|
+
onClick={() => {
|
|
60
|
+
refine(option?.value)
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
{option?.label} <span>({option?.count})</span>
|
|
64
|
+
<Checkbox
|
|
65
|
+
edge='start'
|
|
66
|
+
checked={option?.isRefined}
|
|
67
|
+
tabIndex={-1}
|
|
68
|
+
size='medium'
|
|
69
|
+
color='primary'
|
|
70
|
+
disableRipple
|
|
71
|
+
inputProps={{ 'aria-labelledby': `filter-equal-${attribute}-${option?.value}` }}
|
|
72
|
+
sx={[
|
|
73
|
+
{
|
|
74
|
+
padding: 0,
|
|
75
|
+
margin: '0 0 0 0',
|
|
76
|
+
float: 'right',
|
|
77
|
+
},
|
|
78
|
+
]}
|
|
79
|
+
/>
|
|
80
|
+
</ListItemText>
|
|
81
|
+
</ListItem>
|
|
82
|
+
))}
|
|
83
|
+
</Box>
|
|
84
|
+
</ChipMenu>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Money } from '@graphcommerce/magento-store'
|
|
2
|
+
import { ChipMenu, extendableComponent } from '@graphcommerce/next-ui'
|
|
3
|
+
import Box from '@mui/material/Box'
|
|
4
|
+
import Slider from '@mui/material/Slider'
|
|
5
|
+
import { useEffect, useState } from 'react'
|
|
6
|
+
import { useRange, UseRangeProps } from 'react-instantsearch-hooks-web'
|
|
7
|
+
|
|
8
|
+
export interface RefinementRangeChipProps extends UseRangeProps {
|
|
9
|
+
attribute: string
|
|
10
|
+
title: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { classes } = extendableComponent('RefinementRangeChip', [
|
|
14
|
+
'root',
|
|
15
|
+
'container',
|
|
16
|
+
'slider',
|
|
17
|
+
] as const)
|
|
18
|
+
|
|
19
|
+
export function RefinementRangeChip(props: RefinementRangeChipProps) {
|
|
20
|
+
const { attribute, title } = props
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
22
|
+
const { range, refine } = useRange({
|
|
23
|
+
attribute,
|
|
24
|
+
precision: 2,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const numberMin = Number(range.min)
|
|
28
|
+
const numberMax = Number(range.max)
|
|
29
|
+
|
|
30
|
+
const [value, setValue] = useState([numberMin, numberMax])
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
setValue([numberMin, numberMax])
|
|
34
|
+
}, [numberMax, numberMin])
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<ChipMenu
|
|
38
|
+
variant='outlined'
|
|
39
|
+
label={title}
|
|
40
|
+
className={classes.root}
|
|
41
|
+
selected={numberMin !== value[0]}
|
|
42
|
+
labelRight={
|
|
43
|
+
<>
|
|
44
|
+
<Money round value={value[0] === 0 ? 0.1 : value[0]} />
|
|
45
|
+
{' - '}
|
|
46
|
+
<Money round value={value[1]} />
|
|
47
|
+
</>
|
|
48
|
+
}
|
|
49
|
+
onDelete={() => {
|
|
50
|
+
refine([undefined, undefined])
|
|
51
|
+
setValue([numberMin, numberMax])
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
<Box
|
|
55
|
+
sx={(theme) => ({
|
|
56
|
+
padding: `${theme.spacings.xxs} ${theme.spacings.xxs} !important`,
|
|
57
|
+
width: '100%',
|
|
58
|
+
})}
|
|
59
|
+
className={classes.container}
|
|
60
|
+
>
|
|
61
|
+
<Slider
|
|
62
|
+
min={range.min}
|
|
63
|
+
max={range.max}
|
|
64
|
+
size='large'
|
|
65
|
+
aria-labelledby='range-slider'
|
|
66
|
+
value={value}
|
|
67
|
+
onChange={(_, newValue) => {
|
|
68
|
+
setValue([newValue[0], newValue[1]])
|
|
69
|
+
}}
|
|
70
|
+
onChangeCommitted={(_, newValue) => {
|
|
71
|
+
refine([Number(newValue[0]), Number(newValue[1])])
|
|
72
|
+
}}
|
|
73
|
+
valueLabelDisplay='off'
|
|
74
|
+
className={classes.slider}
|
|
75
|
+
/>
|
|
76
|
+
</Box>
|
|
77
|
+
</ChipMenu>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { RenderType, TypeRenderer } from '@graphcommerce/next-ui'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { RefinementFilterChip, RefinementFilterChipProps } from './FilterChip/RefinementFilterChip'
|
|
4
|
+
import { RefinementRangeChip, RefinementRangeChipProps } from './FilterChip/RefinementRangeChip'
|
|
5
|
+
import { SortChip, SortChipProps } from './SortChip/SortChip'
|
|
6
|
+
|
|
7
|
+
interface RenderChipProps
|
|
8
|
+
extends RefinementFilterChipProps,
|
|
9
|
+
RefinementRangeChipProps,
|
|
10
|
+
SortChipProps {
|
|
11
|
+
__typename: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const renderer: TypeRenderer<{ __typename: string }, Omit<RenderChipProps, '__typename'>> = {
|
|
15
|
+
FilterEqualTypeInput: RefinementFilterChip,
|
|
16
|
+
FilterRangeTypeInput: RefinementRangeChip,
|
|
17
|
+
FilterMatchTypeInput: () => <div>Not implemented</div>,
|
|
18
|
+
Sort: SortChip,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function RenderChip(props: RenderChipProps) {
|
|
22
|
+
const { __typename, ...rest } = props
|
|
23
|
+
return <RenderType renderer={renderer} __typename={__typename} {...rest} />
|
|
24
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { ChipMenu, extendableComponent, responsiveVal } from '@graphcommerce/next-ui'
|
|
2
|
+
import { SxProps, Theme } from '@mui/material'
|
|
3
|
+
import Box from '@mui/material/Box'
|
|
4
|
+
import Checkbox from '@mui/material/Checkbox'
|
|
5
|
+
import ListItem from '@mui/material/ListItem'
|
|
6
|
+
import ListItemText from '@mui/material/ListItemText'
|
|
7
|
+
|
|
8
|
+
const name = 'SortChip' as const
|
|
9
|
+
const parts = ['menu', 'item'] as const
|
|
10
|
+
const { classes } = extendableComponent(name, parts)
|
|
11
|
+
|
|
12
|
+
export type SortByItem = {
|
|
13
|
+
value: string
|
|
14
|
+
label: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type SortByRenderState = {
|
|
18
|
+
initialIndex?: string
|
|
19
|
+
currentRefinement: string
|
|
20
|
+
options: SortByItem[]
|
|
21
|
+
refine: (value: string) => void
|
|
22
|
+
canRefine: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SortChipProps extends SortByRenderState {
|
|
26
|
+
title: string
|
|
27
|
+
sx?: SxProps<Theme>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function SortChip(props: SortChipProps) {
|
|
31
|
+
const { initialIndex, currentRefinement, options, refine, canRefine, title, sx } = props
|
|
32
|
+
|
|
33
|
+
const selectedOption = options.find((option) => option.value === currentRefinement)
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<ChipMenu
|
|
37
|
+
className={classes.menu}
|
|
38
|
+
variant='outlined'
|
|
39
|
+
selected={Boolean(selectedOption)}
|
|
40
|
+
label={title}
|
|
41
|
+
selectedLabel={selectedOption ? selectedOption.label : title}
|
|
42
|
+
onDelete={
|
|
43
|
+
selectedOption ? () => canRefine && refine(initialIndex ?? options[0].value) : undefined
|
|
44
|
+
}
|
|
45
|
+
sx={Array.isArray(sx) ? sx : [sx]}
|
|
46
|
+
>
|
|
47
|
+
<Box
|
|
48
|
+
sx={{
|
|
49
|
+
display: 'grid',
|
|
50
|
+
gridTemplateColumns: { xs: 'repeat(1, minmax(0, 1fr))', md: 'repeat(2, 1fr)' },
|
|
51
|
+
columnGap: responsiveVal(2, 10),
|
|
52
|
+
minWidth: 0,
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
{options.map((option) => (
|
|
56
|
+
<ListItem className={classes.item} key={option?.value ?? ''} dense>
|
|
57
|
+
<ListItemText
|
|
58
|
+
onClick={() => {
|
|
59
|
+
refine(option?.value)
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
{option?.label}
|
|
63
|
+
<Checkbox
|
|
64
|
+
edge='start'
|
|
65
|
+
checked={Boolean(options.find((o) => o.value === selectedOption?.value))}
|
|
66
|
+
tabIndex={-1}
|
|
67
|
+
size='medium'
|
|
68
|
+
color='primary'
|
|
69
|
+
disableRipple
|
|
70
|
+
inputProps={{ 'aria-labelledby': `sort-${option?.value}` }}
|
|
71
|
+
sx={[
|
|
72
|
+
{
|
|
73
|
+
padding: 0,
|
|
74
|
+
margin: '0 0 0 0',
|
|
75
|
+
float: 'right',
|
|
76
|
+
},
|
|
77
|
+
]}
|
|
78
|
+
/>
|
|
79
|
+
</ListItemText>
|
|
80
|
+
</ListItem>
|
|
81
|
+
))}
|
|
82
|
+
</Box>
|
|
83
|
+
</ChipMenu>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { AlgoliaFilterAttribute } from '@graphcommerce/graphql-mesh'
|
|
2
|
+
import { FilterTypes, ProductFiltersProps } from '@graphcommerce/magento-product'
|
|
3
|
+
import { useMemo } from 'react'
|
|
4
|
+
import { useAlgoliaSearchIndexConfig } from '../../hooks/useAlgoliaSearchIndexConfig'
|
|
5
|
+
import { RenderChip } from '../Chip/RenderChip'
|
|
6
|
+
|
|
7
|
+
const systemFilters = [
|
|
8
|
+
{ key: 'category_uid', algoliaKey: 'categories.level0' },
|
|
9
|
+
{ key: 'price', algoliaKey: 'price.EUR.default' },
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
interface FilterWithTypes extends AlgoliaFilterAttribute {
|
|
13
|
+
type: FilterTypes[number]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function AlgoliaFilters(props: ProductFiltersProps) {
|
|
17
|
+
const { filterTypes, aggregations } = props
|
|
18
|
+
|
|
19
|
+
const filtersFromConfig = useAlgoliaSearchIndexConfig('_products')?.filterAttributes
|
|
20
|
+
|
|
21
|
+
const filters = useMemo(() => {
|
|
22
|
+
const allValues: FilterWithTypes[] = []
|
|
23
|
+
const filterTypesKeys = Object.keys(filterTypes)
|
|
24
|
+
const reducedSystemFilters = systemFilters.filter((sf) =>
|
|
25
|
+
filterTypesKeys.some((ftk) => sf.key === ftk),
|
|
26
|
+
)
|
|
27
|
+
const availableFilters = reducedSystemFilters.filter((rsf) =>
|
|
28
|
+
aggregations?.some((a) => a?.attribute_code === rsf.key),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
// Get all items from the system filters and convert them to FilterWithTypes
|
|
32
|
+
availableFilters.forEach((item) => {
|
|
33
|
+
allValues.push({
|
|
34
|
+
aggregation: item.key,
|
|
35
|
+
toAlgoliaAttribute: item.algoliaKey,
|
|
36
|
+
type: filterTypes[item.key],
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// Get all items from the config and convert them to FilterWithTypes
|
|
41
|
+
filtersFromConfig?.forEach((af) => {
|
|
42
|
+
allValues.push({
|
|
43
|
+
aggregation: af?.aggregation ?? '',
|
|
44
|
+
toAlgoliaAttribute: af?.toAlgoliaAttribute ?? '',
|
|
45
|
+
type: filterTypes[af?.aggregation ?? ''],
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Return all values that are included in the default aggregations
|
|
50
|
+
return allValues
|
|
51
|
+
}, [aggregations, filterTypes, filtersFromConfig])
|
|
52
|
+
|
|
53
|
+
if (!aggregations || !filters) return null
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<>
|
|
57
|
+
{filters.map((v) => (
|
|
58
|
+
<RenderChip
|
|
59
|
+
__typename={v.type ?? 'FilterMatchTypeInput'}
|
|
60
|
+
key={v.aggregation}
|
|
61
|
+
attribute={v.toAlgoliaAttribute}
|
|
62
|
+
title={
|
|
63
|
+
aggregations.find((a) => a?.attribute_code === v.aggregation)?.label ??
|
|
64
|
+
v.aggregation.charAt(0).toUpperCase() + v.aggregation.slice(1)
|
|
65
|
+
}
|
|
66
|
+
/>
|
|
67
|
+
))}
|
|
68
|
+
</>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ProductPaginationProps } from '@graphcommerce/magento-product'
|
|
2
|
+
import { Pagination } from '@graphcommerce/next-ui'
|
|
3
|
+
import { Box } from '@mui/material'
|
|
4
|
+
import { usePagination } from 'react-instantsearch-hooks-web'
|
|
5
|
+
|
|
6
|
+
export function AlgoliaPagination({
|
|
7
|
+
page_info,
|
|
8
|
+
params,
|
|
9
|
+
...paginationProps
|
|
10
|
+
}: ProductPaginationProps) {
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
12
|
+
const { nbPages, refine, currentRefinement } = usePagination()
|
|
13
|
+
|
|
14
|
+
const handlePagination = (destinationPage: number) => {
|
|
15
|
+
refine(destinationPage - 1)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Pagination
|
|
20
|
+
count={nbPages ?? 0}
|
|
21
|
+
page={currentRefinement + 1}
|
|
22
|
+
renderLink={(page, icon, btnProps) => (
|
|
23
|
+
<Box {...btnProps} color='inherit' onClick={() => handlePagination(page)}>
|
|
24
|
+
{icon}
|
|
25
|
+
</Box>
|
|
26
|
+
)}
|
|
27
|
+
{...paginationProps}
|
|
28
|
+
/>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Trans } from '@lingui/react'
|
|
2
|
+
import { Box, debounce } from '@mui/material'
|
|
3
|
+
import TextField from '@mui/material/TextField'
|
|
4
|
+
import { ChangeEvent, useCallback, useEffect, useRef } from 'react'
|
|
5
|
+
import { useHits, useSearchBox, UseSearchBoxProps } from 'react-instantsearch-hooks-web'
|
|
6
|
+
|
|
7
|
+
type SearchBoxProps = {
|
|
8
|
+
defaultValue?: string
|
|
9
|
+
} & UseSearchBoxProps
|
|
10
|
+
|
|
11
|
+
export function SearchBox(props: SearchBoxProps) {
|
|
12
|
+
const { defaultValue } = props
|
|
13
|
+
const searchInputElement = useRef<HTMLInputElement>(null)
|
|
14
|
+
|
|
15
|
+
const { refine } = useSearchBox()
|
|
16
|
+
const { results } = useHits()
|
|
17
|
+
|
|
18
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
19
|
+
const debounceSearch = useCallback(
|
|
20
|
+
debounce(
|
|
21
|
+
(e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => refine(e.target.value),
|
|
22
|
+
import.meta.graphCommerce.algoliaSearchDebounceTime ?? 0,
|
|
23
|
+
),
|
|
24
|
+
[refine],
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (defaultValue) refine(defaultValue)
|
|
29
|
+
}, [defaultValue, refine])
|
|
30
|
+
|
|
31
|
+
const totalResults = results?.nbHits ?? 0
|
|
32
|
+
|
|
33
|
+
const endAdornment = (
|
|
34
|
+
<Box
|
|
35
|
+
sx={(theme) => ({
|
|
36
|
+
minWidth: 'max-content',
|
|
37
|
+
color: theme.palette.text.disabled,
|
|
38
|
+
paddingRight: '7px',
|
|
39
|
+
})}
|
|
40
|
+
>
|
|
41
|
+
{totalResults === 1 && <Trans id='{totalResults} result' values={{ totalResults }} />}
|
|
42
|
+
{totalResults > 1 && <Trans id='{totalResults} results' values={{ totalResults }} />}
|
|
43
|
+
</Box>
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<TextField
|
|
48
|
+
variant='outlined'
|
|
49
|
+
type='text'
|
|
50
|
+
name='search'
|
|
51
|
+
InputProps={{ endAdornment }}
|
|
52
|
+
inputRef={searchInputElement}
|
|
53
|
+
onChange={debounceSearch}
|
|
54
|
+
fullWidth
|
|
55
|
+
sx={{ mt: 1 }}
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useHits } from 'react-instantsearch-hooks-web'
|
|
2
|
+
import { AlgoliaCategoryHit } from '../lib/types'
|
|
3
|
+
|
|
4
|
+
function hitToCategory(hits: AlgoliaCategoryHit[]) {
|
|
5
|
+
return hits.map((h) => {
|
|
6
|
+
const urlSplit = h.url.split('/')
|
|
7
|
+
const categoryUrl = urlSplit.reduce((prev, curr, currIndex) => {
|
|
8
|
+
if (currIndex > 2) return `${prev}/${curr}`
|
|
9
|
+
return ''
|
|
10
|
+
})
|
|
11
|
+
const url_key = categoryUrl.substring(0, categoryUrl.length - 5)
|
|
12
|
+
return {
|
|
13
|
+
category_uid: h.objectID,
|
|
14
|
+
category_level: h.level,
|
|
15
|
+
category_name: h.name,
|
|
16
|
+
category_url_path: url_key,
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useAlgoliaCategoryResults() {
|
|
22
|
+
const { hits, results } = useHits<AlgoliaCategoryHit>()
|
|
23
|
+
const categories = hitToCategory(hits)
|
|
24
|
+
return { categories, search: results?.query }
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useHits } from 'react-instantsearch-hooks-web'
|
|
2
|
+
import { AlgoliaPageHit } from '../lib/types'
|
|
3
|
+
|
|
4
|
+
function hitToPage(hits: AlgoliaPageHit[]) {
|
|
5
|
+
return hits.map((h) => {
|
|
6
|
+
const urlSplit = h.url.split('/')
|
|
7
|
+
const url = urlSplit.reduce((prev, curr, currIndex) => {
|
|
8
|
+
if (currIndex > 2) return `${prev}/${curr}`
|
|
9
|
+
return ''
|
|
10
|
+
})
|
|
11
|
+
return {
|
|
12
|
+
objectID: h.objectID,
|
|
13
|
+
name: h.name,
|
|
14
|
+
slug: h.slug,
|
|
15
|
+
url,
|
|
16
|
+
content: h.content,
|
|
17
|
+
algoliaLastUpdateAtCET: h.algoliaLastUpdateAtCET,
|
|
18
|
+
} satisfies AlgoliaPageHit
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useAlgoliaPageResults() {
|
|
23
|
+
const { hits, results } = useHits<AlgoliaPageHit>()
|
|
24
|
+
const pages = hitToPage(hits)
|
|
25
|
+
return { pages, search: results?.query }
|
|
26
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useQuery } from '@graphcommerce/graphql'
|
|
2
|
+
import { CurrencyEnum } from '@graphcommerce/graphql-mesh'
|
|
3
|
+
import { ProductListItemFragment, ProductListItemProps } from '@graphcommerce/magento-product'
|
|
4
|
+
import { StoreConfigDocument } from '@graphcommerce/magento-store'
|
|
5
|
+
import { useHits } from 'react-instantsearch-hooks-web'
|
|
6
|
+
import { AlgoliaProductHit } from '../lib/types'
|
|
7
|
+
|
|
8
|
+
function hitsToProduct(
|
|
9
|
+
items: AlgoliaProductHit[],
|
|
10
|
+
currency?: string | null,
|
|
11
|
+
productUrlSuffix?: string | null,
|
|
12
|
+
) {
|
|
13
|
+
const mapHits = items.map((item) => {
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
15
|
+
const currentCurrency = (currency ?? Object.keys(item.price)[0]) as CurrencyEnum
|
|
16
|
+
const price = item.price[currentCurrency]
|
|
17
|
+
const productUrlSplit = item.url.split('/')
|
|
18
|
+
const productUrl = productUrlSplit[productUrlSplit.length - 1]
|
|
19
|
+
const url_key = productUrl.substring(0, productUrl.length - (productUrlSuffix?.length ?? 0))
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
__typename: 'SimpleProduct',
|
|
23
|
+
uid: item.objectID,
|
|
24
|
+
small_image: {
|
|
25
|
+
url: item.image_url,
|
|
26
|
+
},
|
|
27
|
+
sku: item.sku,
|
|
28
|
+
price_range: {
|
|
29
|
+
minimum_price: {
|
|
30
|
+
final_price: {
|
|
31
|
+
value: price.default,
|
|
32
|
+
currency: currentCurrency,
|
|
33
|
+
},
|
|
34
|
+
regular_price: {
|
|
35
|
+
value: price.default,
|
|
36
|
+
currency: currentCurrency,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
rating_summary: item.rating_summary ?? 0,
|
|
41
|
+
url_key,
|
|
42
|
+
name: item.name,
|
|
43
|
+
}
|
|
44
|
+
}) satisfies Array<ProductListItemFragment & ProductListItemProps>
|
|
45
|
+
|
|
46
|
+
return mapHits
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function useAlgoliaProductResults() {
|
|
50
|
+
const { hits } = useHits<AlgoliaProductHit>()
|
|
51
|
+
const { data } = useQuery(StoreConfigDocument)
|
|
52
|
+
const products = hitsToProduct(
|
|
53
|
+
hits,
|
|
54
|
+
data?.storeConfig?.base_currency_code,
|
|
55
|
+
data?.storeConfig?.product_url_suffix,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return { products }
|
|
59
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { storefrontConfig } from '@graphcommerce/next-ui'
|
|
2
|
+
import { i18n } from '@lingui/core'
|
|
3
|
+
|
|
4
|
+
export function useAlgoliaSearchIndexConfig(suffix: string) {
|
|
5
|
+
return storefrontConfig(i18n.locale)?.algoliaSearchIndexConfig.find((ai) =>
|
|
6
|
+
ai.searchIndex.includes(suffix),
|
|
7
|
+
)
|
|
8
|
+
}
|
package/index.ts
ADDED
|
File without changes
|
package/lib/types.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type AlgoliaProductHit = {
|
|
2
|
+
algoliaLastUpdateAtCET: string
|
|
3
|
+
categories: { [key: string]: string[] }
|
|
4
|
+
categories_without_path: string[]
|
|
5
|
+
categoryIds: string[]
|
|
6
|
+
created_at: string
|
|
7
|
+
image_url: string
|
|
8
|
+
name: string
|
|
9
|
+
objectID: string
|
|
10
|
+
price: {
|
|
11
|
+
[key: string]: {
|
|
12
|
+
default: number
|
|
13
|
+
default_formated: string
|
|
14
|
+
default_original_formated: string
|
|
15
|
+
special_from_date: number
|
|
16
|
+
special_to_date: string
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
price_with_tax: {
|
|
20
|
+
[key: string]: {
|
|
21
|
+
default: number
|
|
22
|
+
default_formated: string
|
|
23
|
+
default_original_formated: string
|
|
24
|
+
special_from_date: number
|
|
25
|
+
special_to_date: string
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
rating_summary: number | null
|
|
29
|
+
sku: string
|
|
30
|
+
thumbnail_url: string
|
|
31
|
+
type_id: string
|
|
32
|
+
url: string
|
|
33
|
+
visibility_catalog: number
|
|
34
|
+
visibility_search: number
|
|
35
|
+
__position: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type AlgoliaCategoryHit = {
|
|
39
|
+
objectID: string
|
|
40
|
+
name: string
|
|
41
|
+
path: string
|
|
42
|
+
product_count: number
|
|
43
|
+
level: number
|
|
44
|
+
url: string
|
|
45
|
+
include_in_menu: number
|
|
46
|
+
_tags: string[]
|
|
47
|
+
popularity: number
|
|
48
|
+
algoliaLastUpdateAtCET: string | Date
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type AlgoliaPageHit = {
|
|
52
|
+
objectID: string
|
|
53
|
+
name: string
|
|
54
|
+
slug: string
|
|
55
|
+
url: string
|
|
56
|
+
content: string
|
|
57
|
+
algoliaLastUpdateAtCET: string
|
|
58
|
+
}
|
package/next-env.d.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@graphcommerce/algolia-search",
|
|
3
|
+
"homepage": "https://www.graphcommerce.org/",
|
|
4
|
+
"repository": "github:graphcommerce-org/graphcommerce",
|
|
5
|
+
"version": "7.0.0-canary.12",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"prettier": "@graphcommerce/prettier-config-pwa",
|
|
8
|
+
"eslintConfig": {
|
|
9
|
+
"extends": "@graphcommerce/eslint-config-pwa",
|
|
10
|
+
"parserOptions": {
|
|
11
|
+
"project": "./tsconfig.json"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@graphcommerce/eslint-config-pwa": "7.0.0-canary.12",
|
|
16
|
+
"@graphcommerce/next-config": "^7.0.0-canary.12",
|
|
17
|
+
"@graphcommerce/prettier-config-pwa": "7.0.0-canary.12",
|
|
18
|
+
"@graphcommerce/typescript-config-pwa": "7.0.0-canary.12"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@graphcommerce/graphql": "7.0.0-canary.12",
|
|
22
|
+
"@graphcommerce/ecommerce-ui": "7.0.0-canary.12",
|
|
23
|
+
"@graphcommerce/magento-search": "7.0.0-canary.12",
|
|
24
|
+
"@graphcommerce/next-config": "^7.0.0-canary.12",
|
|
25
|
+
"@graphcommerce/next-ui": "7.0.0-canary.12",
|
|
26
|
+
"@graphcommerce/magento-product": "7.0.0-canary.12",
|
|
27
|
+
"@graphcommerce/graphql-mesh": "7.0.0-canary.12",
|
|
28
|
+
"@graphcommerce/magento-store": "7.0.0-canary.12",
|
|
29
|
+
"algoliasearch": "^4.15.0",
|
|
30
|
+
"react-instantsearch-hooks-web": "^6.41.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@lingui/core": "^3.13.2",
|
|
34
|
+
"@lingui/react": "^3.13.2",
|
|
35
|
+
"@mui/material": "^5.10.16",
|
|
36
|
+
"next": "^13.2.0",
|
|
37
|
+
"react": "^18.2.0",
|
|
38
|
+
"react-dom": "^18.2.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { CategorySearchResult, SearchFormProps } from '@graphcommerce/magento-search'
|
|
2
|
+
import { IfConfig, PluginProps } from '@graphcommerce/next-config'
|
|
3
|
+
import { Index } from 'react-instantsearch-hooks-web'
|
|
4
|
+
import { useAlgoliaCategoryResults } from '../hooks/useAlgoliaCategoryResults'
|
|
5
|
+
import { useAlgoliaSearchIndexConfig } from '../hooks/useAlgoliaSearchIndexConfig'
|
|
6
|
+
|
|
7
|
+
export const component = 'SearchForm'
|
|
8
|
+
export const exported = '@graphcommerce/magento-search'
|
|
9
|
+
export const ifConfig: IfConfig = 'algoliaApplicationId'
|
|
10
|
+
|
|
11
|
+
function CategoryHits() {
|
|
12
|
+
const { categories, search } = useAlgoliaCategoryResults()
|
|
13
|
+
|
|
14
|
+
if (!search || search.length <= 0) return null
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
{categories.map((category) => (
|
|
19
|
+
<CategorySearchResult
|
|
20
|
+
breadcrumbs={[category]}
|
|
21
|
+
search={search}
|
|
22
|
+
url_path={category.category_url_path}
|
|
23
|
+
/>
|
|
24
|
+
))}
|
|
25
|
+
</>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function AlgoliaCategorySearchPlugin(props: PluginProps<SearchFormProps>) {
|
|
30
|
+
const { Prev, ...rest } = props
|
|
31
|
+
const searchIndex = useAlgoliaSearchIndexConfig('_categories')?.searchIndex
|
|
32
|
+
|
|
33
|
+
if (!searchIndex) return <Prev {...rest} />
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
<Prev {...rest} />
|
|
38
|
+
<Index indexName={searchIndex}>
|
|
39
|
+
<CategoryHits />
|
|
40
|
+
</Index>
|
|
41
|
+
</>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const Plugin = AlgoliaCategorySearchPlugin
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ProductFiltersProps } from '@graphcommerce/magento-product'
|
|
2
|
+
import { IfConfig, PluginProps } from '@graphcommerce/next-config'
|
|
3
|
+
import { AlgoliaFilters } from '../components/Filters/AlgoliaFilters'
|
|
4
|
+
|
|
5
|
+
export const component = 'ProductListFiltersSearch'
|
|
6
|
+
export const exported = '@graphcommerce/magento-search'
|
|
7
|
+
export const ifConfig: IfConfig = 'algoliaApplicationId'
|
|
8
|
+
|
|
9
|
+
function AlgoliaFiltersPlugin(props: PluginProps<ProductFiltersProps>) {
|
|
10
|
+
const { Prev, ...rest } = props
|
|
11
|
+
return <AlgoliaFilters {...rest} />
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const Plugin = AlgoliaFiltersPlugin
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { CategorySearchResult, SearchFormProps } from '@graphcommerce/magento-search'
|
|
2
|
+
import { IfConfig, PluginProps } from '@graphcommerce/next-config'
|
|
3
|
+
import { Index } from 'react-instantsearch-hooks-web'
|
|
4
|
+
import { useAlgoliaPageResults } from '../hooks/useAlgoliaPageResults'
|
|
5
|
+
import { useAlgoliaSearchIndexConfig } from '../hooks/useAlgoliaSearchIndexConfig'
|
|
6
|
+
|
|
7
|
+
export const component = 'SearchForm'
|
|
8
|
+
export const exported = '@graphcommerce/magento-search'
|
|
9
|
+
export const ifConfig: IfConfig = 'algoliaApplicationId'
|
|
10
|
+
|
|
11
|
+
function PageHits() {
|
|
12
|
+
const { pages, search } = useAlgoliaPageResults()
|
|
13
|
+
|
|
14
|
+
if (!search || search.length <= 0) return null
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
{pages.map((page) => (
|
|
19
|
+
<CategorySearchResult
|
|
20
|
+
breadcrumbs={[
|
|
21
|
+
{
|
|
22
|
+
category_uid: page.objectID,
|
|
23
|
+
category_level: page.url.split('/').length,
|
|
24
|
+
category_name: page.name,
|
|
25
|
+
category_url_path: page.url,
|
|
26
|
+
},
|
|
27
|
+
]}
|
|
28
|
+
search={search}
|
|
29
|
+
url_path={page.url}
|
|
30
|
+
/>
|
|
31
|
+
))}
|
|
32
|
+
</>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function AlgoliaPageSearchPlugin(props: PluginProps<SearchFormProps>) {
|
|
37
|
+
const { Prev, ...rest } = props
|
|
38
|
+
const searchIndex = useAlgoliaSearchIndexConfig('_pages')?.searchIndex
|
|
39
|
+
|
|
40
|
+
if (!searchIndex) return <Prev {...rest} />
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
<Prev {...rest} />
|
|
45
|
+
<Index indexName={searchIndex}>
|
|
46
|
+
<PageHits />
|
|
47
|
+
</Index>
|
|
48
|
+
</>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const Plugin = AlgoliaPageSearchPlugin
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ProductPaginationProps } from '@graphcommerce/magento-product'
|
|
2
|
+
import { IfConfig, PluginProps } from '@graphcommerce/next-config'
|
|
3
|
+
import { useRouter } from 'next/router'
|
|
4
|
+
import { AlgoliaPagination } from '../components/Pagination/AlgoliaPagination'
|
|
5
|
+
|
|
6
|
+
export const component = 'ProductListPaginationSearch'
|
|
7
|
+
export const exported = '@graphcommerce/magento-search'
|
|
8
|
+
export const ifConfig: IfConfig = 'algoliaApplicationId'
|
|
9
|
+
|
|
10
|
+
function AlgoliaPaginationPlugin(props: PluginProps<ProductPaginationProps>) {
|
|
11
|
+
const { Prev, ...rest } = props
|
|
12
|
+
const router = useRouter()
|
|
13
|
+
if (!router.asPath.includes('/search')) return <Prev {...props} />
|
|
14
|
+
|
|
15
|
+
return <AlgoliaPagination {...rest} />
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const Plugin = AlgoliaPaginationPlugin
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ProductCountProps } from '@graphcommerce/magento-product'
|
|
2
|
+
import { IfConfig, PluginProps } from '@graphcommerce/next-config'
|
|
3
|
+
import { Index, usePagination } from 'react-instantsearch-hooks-web'
|
|
4
|
+
import { useAlgoliaSearchIndexConfig } from '../hooks/useAlgoliaSearchIndexConfig'
|
|
5
|
+
|
|
6
|
+
export const component = 'ProductListCountSearch'
|
|
7
|
+
export const exported = '@graphcommerce/magento-search'
|
|
8
|
+
export const ifConfig: IfConfig = 'algoliaApplicationId'
|
|
9
|
+
|
|
10
|
+
function AlgoliaProductListCountPlugin(props: PluginProps<ProductCountProps>) {
|
|
11
|
+
const { Prev, ...rest } = props
|
|
12
|
+
const { nbHits } = usePagination()
|
|
13
|
+
const searchIndex = useAlgoliaSearchIndexConfig('_products')?.searchIndex
|
|
14
|
+
|
|
15
|
+
if (!searchIndex) return <Prev {...rest} />
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Index indexName={searchIndex}>
|
|
19
|
+
<Prev {...rest} total_count={nbHits} />
|
|
20
|
+
</Index>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const Plugin = AlgoliaProductListCountPlugin
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
2
|
+
import { ProductItemsGridProps } from '@graphcommerce/magento-product'
|
|
3
|
+
import { IfConfig, PluginProps } from '@graphcommerce/next-config'
|
|
4
|
+
import { Index } from 'react-instantsearch-hooks-web'
|
|
5
|
+
import { useAlgoliaProductResults } from '../hooks/useAlgoliaProductResults'
|
|
6
|
+
import { useAlgoliaSearchIndexConfig } from '../hooks/useAlgoliaSearchIndexConfig'
|
|
7
|
+
|
|
8
|
+
export const component = 'ProductListItemsSearch'
|
|
9
|
+
export const exported = '@graphcommerce/magento-search'
|
|
10
|
+
export const ifConfig: IfConfig = 'algoliaApplicationId'
|
|
11
|
+
|
|
12
|
+
function AlgoliaProductSearchPlugin(props: PluginProps<ProductItemsGridProps>) {
|
|
13
|
+
const { Prev, ...rest } = props
|
|
14
|
+
const { products } = useAlgoliaProductResults()
|
|
15
|
+
const searchIndex = useAlgoliaSearchIndexConfig('_products')?.searchIndex
|
|
16
|
+
|
|
17
|
+
if (!searchIndex) return <Prev {...rest} />
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Index indexName={searchIndex}>
|
|
21
|
+
<Prev {...rest} items={products} />
|
|
22
|
+
</Index>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const Plugin = AlgoliaProductSearchPlugin
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ProductFiltersProps } from '@graphcommerce/magento-product'
|
|
2
|
+
import { IfConfig, PluginProps } from '@graphcommerce/next-config'
|
|
3
|
+
import { storefrontConfig } from '@graphcommerce/next-ui'
|
|
4
|
+
import { i18n } from '@lingui/core'
|
|
5
|
+
import { useSortBy } from 'react-instantsearch-hooks-web'
|
|
6
|
+
import { RenderChip } from '../components/Chip/RenderChip'
|
|
7
|
+
|
|
8
|
+
export const component = 'ProductListSortSearch'
|
|
9
|
+
export const exported = '@graphcommerce/magento-search'
|
|
10
|
+
export const ifConfig: IfConfig = 'algoliaApplicationId'
|
|
11
|
+
|
|
12
|
+
function AlgoliaProductSortPlugin(props: PluginProps<ProductFiltersProps>) {
|
|
13
|
+
const { Prev, ...rest } = props
|
|
14
|
+
const sort = useSortBy({
|
|
15
|
+
items: storefrontConfig(i18n.locale)?.sortOptions ?? [],
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<RenderChip
|
|
20
|
+
__typename='Sort'
|
|
21
|
+
title={rest.title ?? i18n._(/* i18n */ 'Sort')}
|
|
22
|
+
attribute='sort'
|
|
23
|
+
{...sort}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const Plugin = AlgoliaProductSortPlugin
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SearchContextProps } from '@graphcommerce/magento-search'
|
|
2
|
+
import { IfConfig, PluginProps } from '@graphcommerce/next-config'
|
|
3
|
+
import algoliasearch from 'algoliasearch/lite'
|
|
4
|
+
import { InstantSearch, InstantSearchSSRProvider } from 'react-instantsearch-hooks-web'
|
|
5
|
+
import { useAlgoliaSearchIndexConfig } from '../hooks/useAlgoliaSearchIndexConfig'
|
|
6
|
+
import { applicationId, searchOnlyApiKey } from '../lib/configuration'
|
|
7
|
+
|
|
8
|
+
export const component = 'SearchContext'
|
|
9
|
+
export const exported = '@graphcommerce/magento-search'
|
|
10
|
+
export const ifConfig: IfConfig = 'algoliaApplicationId'
|
|
11
|
+
|
|
12
|
+
const searchClient = algoliasearch(applicationId, searchOnlyApiKey)
|
|
13
|
+
|
|
14
|
+
function AlgoliaSearchContextPlugin(props: PluginProps<SearchContextProps>) {
|
|
15
|
+
const { Prev, serverProps, ...rest } = props
|
|
16
|
+
const searchIndex = useAlgoliaSearchIndexConfig('_products')?.searchIndex
|
|
17
|
+
|
|
18
|
+
if (!searchIndex)
|
|
19
|
+
throw Error(
|
|
20
|
+
'(@graphcommerce/algolia-plugin): No search index with "_products" suffix provided. Please add the search index to the Graphcommerce config',
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<InstantSearchSSRProvider {...(typeof serverProps === 'object' ? serverProps : {})}>
|
|
25
|
+
<InstantSearch searchClient={searchClient} indexName={searchIndex}>
|
|
26
|
+
<Prev {...rest} />
|
|
27
|
+
</InstantSearch>
|
|
28
|
+
</InstantSearchSSRProvider>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const Plugin = AlgoliaSearchContextPlugin
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SearchFormProps } from '@graphcommerce/magento-search'
|
|
2
|
+
import { IfConfig, PluginProps } from '@graphcommerce/next-config'
|
|
3
|
+
import { SearchBox } from '../components/SearchBox/SearchBox'
|
|
4
|
+
|
|
5
|
+
export const component = 'SearchForm'
|
|
6
|
+
export const exported = '@graphcommerce/magento-search'
|
|
7
|
+
export const ifConfig: IfConfig = 'algoliaApplicationId'
|
|
8
|
+
|
|
9
|
+
function AlgoliaSearchFieldPlugin(props: PluginProps<SearchFormProps>) {
|
|
10
|
+
const { search } = props
|
|
11
|
+
return <SearchBox defaultValue={search} />
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const Plugin = AlgoliaSearchFieldPlugin
|