hydrogen-forge 0.1.0

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.
Files changed (118) hide show
  1. package/README.md +212 -0
  2. package/dist/commands/add.d.ts +7 -0
  3. package/dist/commands/add.d.ts.map +1 -0
  4. package/dist/commands/add.js +123 -0
  5. package/dist/commands/add.js.map +1 -0
  6. package/dist/commands/create.d.ts +8 -0
  7. package/dist/commands/create.d.ts.map +1 -0
  8. package/dist/commands/create.js +160 -0
  9. package/dist/commands/create.js.map +1 -0
  10. package/dist/commands/setup-mcp.d.ts +7 -0
  11. package/dist/commands/setup-mcp.d.ts.map +1 -0
  12. package/dist/commands/setup-mcp.js +179 -0
  13. package/dist/commands/setup-mcp.js.map +1 -0
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +50 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/lib/generators.d.ts +6 -0
  19. package/dist/lib/generators.d.ts.map +1 -0
  20. package/dist/lib/generators.js +470 -0
  21. package/dist/lib/generators.js.map +1 -0
  22. package/dist/lib/utils.d.ts +17 -0
  23. package/dist/lib/utils.d.ts.map +1 -0
  24. package/dist/lib/utils.js +101 -0
  25. package/dist/lib/utils.js.map +1 -0
  26. package/package.json +54 -0
  27. package/templates/starter/.env.example +21 -0
  28. package/templates/starter/.graphqlrc.ts +27 -0
  29. package/templates/starter/README.md +117 -0
  30. package/templates/starter/app/assets/favicon.svg +28 -0
  31. package/templates/starter/app/components/AddToCartButton.tsx +102 -0
  32. package/templates/starter/app/components/Aside.tsx +136 -0
  33. package/templates/starter/app/components/CartLineItem.tsx +229 -0
  34. package/templates/starter/app/components/CartMain.tsx +131 -0
  35. package/templates/starter/app/components/CartSummary.tsx +315 -0
  36. package/templates/starter/app/components/CollectionFilters.tsx +330 -0
  37. package/templates/starter/app/components/CollectionGrid.tsx +141 -0
  38. package/templates/starter/app/components/Footer.tsx +218 -0
  39. package/templates/starter/app/components/Header.tsx +296 -0
  40. package/templates/starter/app/components/PageLayout.tsx +174 -0
  41. package/templates/starter/app/components/PaginatedResourceSection.tsx +41 -0
  42. package/templates/starter/app/components/ProductCard.tsx +151 -0
  43. package/templates/starter/app/components/ProductForm.tsx +156 -0
  44. package/templates/starter/app/components/ProductGallery.tsx +164 -0
  45. package/templates/starter/app/components/ProductGrid.tsx +64 -0
  46. package/templates/starter/app/components/ProductImage.tsx +23 -0
  47. package/templates/starter/app/components/ProductItem.tsx +44 -0
  48. package/templates/starter/app/components/ProductPrice.tsx +97 -0
  49. package/templates/starter/app/components/SearchDialog.tsx +599 -0
  50. package/templates/starter/app/components/SearchForm.tsx +68 -0
  51. package/templates/starter/app/components/SearchFormPredictive.tsx +76 -0
  52. package/templates/starter/app/components/SearchResults.tsx +161 -0
  53. package/templates/starter/app/components/SearchResultsPredictive.tsx +461 -0
  54. package/templates/starter/app/entry.client.tsx +21 -0
  55. package/templates/starter/app/entry.server.tsx +53 -0
  56. package/templates/starter/app/graphql/customer-account/CustomerAddressMutations.ts +64 -0
  57. package/templates/starter/app/graphql/customer-account/CustomerDetailsQuery.ts +40 -0
  58. package/templates/starter/app/graphql/customer-account/CustomerOrderQuery.ts +90 -0
  59. package/templates/starter/app/graphql/customer-account/CustomerOrdersQuery.ts +63 -0
  60. package/templates/starter/app/graphql/customer-account/CustomerUpdateMutation.ts +25 -0
  61. package/templates/starter/app/lib/context.ts +60 -0
  62. package/templates/starter/app/lib/fragments.ts +234 -0
  63. package/templates/starter/app/lib/orderFilters.ts +90 -0
  64. package/templates/starter/app/lib/redirect.ts +23 -0
  65. package/templates/starter/app/lib/search.ts +79 -0
  66. package/templates/starter/app/lib/session.ts +72 -0
  67. package/templates/starter/app/lib/variants.ts +46 -0
  68. package/templates/starter/app/root.tsx +209 -0
  69. package/templates/starter/app/routes/$.tsx +11 -0
  70. package/templates/starter/app/routes/[robots.txt].tsx +117 -0
  71. package/templates/starter/app/routes/[sitemap.xml].tsx +16 -0
  72. package/templates/starter/app/routes/_index.tsx +167 -0
  73. package/templates/starter/app/routes/account.$.tsx +9 -0
  74. package/templates/starter/app/routes/account._index.tsx +5 -0
  75. package/templates/starter/app/routes/account.addresses.tsx +516 -0
  76. package/templates/starter/app/routes/account.orders.$id.tsx +222 -0
  77. package/templates/starter/app/routes/account.orders._index.tsx +222 -0
  78. package/templates/starter/app/routes/account.profile.tsx +133 -0
  79. package/templates/starter/app/routes/account.tsx +97 -0
  80. package/templates/starter/app/routes/account_.authorize.tsx +5 -0
  81. package/templates/starter/app/routes/account_.login.tsx +7 -0
  82. package/templates/starter/app/routes/account_.logout.tsx +11 -0
  83. package/templates/starter/app/routes/api.$version.[graphql.json].tsx +14 -0
  84. package/templates/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +129 -0
  85. package/templates/starter/app/routes/blogs.$blogHandle._index.tsx +175 -0
  86. package/templates/starter/app/routes/blogs._index.tsx +109 -0
  87. package/templates/starter/app/routes/cart.$lines.tsx +70 -0
  88. package/templates/starter/app/routes/cart.tsx +117 -0
  89. package/templates/starter/app/routes/collections.$handle.tsx +161 -0
  90. package/templates/starter/app/routes/collections._index.tsx +133 -0
  91. package/templates/starter/app/routes/collections.all.tsx +122 -0
  92. package/templates/starter/app/routes/discount.$code.tsx +48 -0
  93. package/templates/starter/app/routes/pages.$handle.tsx +88 -0
  94. package/templates/starter/app/routes/policies.$handle.tsx +93 -0
  95. package/templates/starter/app/routes/policies._index.tsx +69 -0
  96. package/templates/starter/app/routes/products.$handle.tsx +232 -0
  97. package/templates/starter/app/routes/search.tsx +426 -0
  98. package/templates/starter/app/routes/sitemap.$type.$page[.xml].tsx +23 -0
  99. package/templates/starter/app/routes.ts +9 -0
  100. package/templates/starter/app/styles/app.css +574 -0
  101. package/templates/starter/app/styles/reset.css +139 -0
  102. package/templates/starter/app/styles/tailwind.css +116 -0
  103. package/templates/starter/customer-accountapi.generated.d.ts +543 -0
  104. package/templates/starter/env.d.ts +7 -0
  105. package/templates/starter/eslint.config.js +247 -0
  106. package/templates/starter/guides/predictiveSearch/predictiveSearch.jpg +0 -0
  107. package/templates/starter/guides/predictiveSearch/predictiveSearch.md +394 -0
  108. package/templates/starter/guides/search/search.jpg +0 -0
  109. package/templates/starter/guides/search/search.md +335 -0
  110. package/templates/starter/package.json +71 -0
  111. package/templates/starter/postcss.config.js +6 -0
  112. package/templates/starter/public/.gitkeep +0 -0
  113. package/templates/starter/react-router.config.ts +13 -0
  114. package/templates/starter/server.ts +59 -0
  115. package/templates/starter/storefrontapi.generated.d.ts +1264 -0
  116. package/templates/starter/tailwind.config.js +83 -0
  117. package/templates/starter/tsconfig.json +67 -0
  118. package/templates/starter/vite.config.ts +32 -0
@@ -0,0 +1,247 @@
1
+ import {fixupConfigRules, fixupPluginRules} from '@eslint/compat';
2
+ import eslintComments from 'eslint-plugin-eslint-comments';
3
+ import react from 'eslint-plugin-react';
4
+ import reactHooks from 'eslint-plugin-react-hooks';
5
+ import jsxA11Y from 'eslint-plugin-jsx-a11y';
6
+ import globals from 'globals';
7
+ import typescriptEslint from '@typescript-eslint/eslint-plugin';
8
+ import _import from 'eslint-plugin-import';
9
+ import tsParser from '@typescript-eslint/parser';
10
+ import jest from 'eslint-plugin-jest';
11
+ import path from 'node:path';
12
+ import {fileURLToPath} from 'node:url';
13
+ import js from '@eslint/js';
14
+ import {FlatCompat} from '@eslint/eslintrc';
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+ const compat = new FlatCompat({
19
+ baseDirectory: __dirname,
20
+ recommendedConfig: js.configs.recommended,
21
+ allConfig: js.configs.all,
22
+ });
23
+
24
+ export default [
25
+ {
26
+ ignores: [
27
+ '**/node_modules/',
28
+ '**/build/',
29
+ '**/dist/',
30
+ '**/*.graphql.d.ts',
31
+ '**/*.graphql.ts',
32
+ '**/*.generated.d.ts',
33
+ '**/.react-router/',
34
+ '**/packages/hydrogen/dist/',
35
+ ],
36
+ },
37
+ ...fixupConfigRules(
38
+ compat.extends(
39
+ 'eslint:recommended',
40
+ 'plugin:eslint-comments/recommended',
41
+ 'plugin:react/recommended',
42
+ 'plugin:react-hooks/recommended',
43
+ 'plugin:jsx-a11y/recommended',
44
+ ),
45
+ ),
46
+ {
47
+ plugins: {
48
+ 'eslint-comments': fixupPluginRules(eslintComments),
49
+ react: fixupPluginRules(react),
50
+ 'react-hooks': fixupPluginRules(reactHooks),
51
+ 'jsx-a11y': fixupPluginRules(jsxA11Y),
52
+ },
53
+ languageOptions: {
54
+ globals: {
55
+ ...globals.browser,
56
+ ...globals.node,
57
+ },
58
+ ecmaVersion: 'latest',
59
+ sourceType: 'module',
60
+ parserOptions: {
61
+ ecmaFeatures: {
62
+ jsx: true,
63
+ },
64
+ },
65
+ },
66
+ settings: {
67
+ react: {
68
+ version: 'detect',
69
+ },
70
+ },
71
+ rules: {
72
+ 'eslint-comments/no-unused-disable': 'error',
73
+ 'no-console': [
74
+ 'warn',
75
+ {
76
+ allow: ['warn', 'error'],
77
+ },
78
+ ],
79
+ 'no-use-before-define': 'off',
80
+ 'no-warning-comments': 'off',
81
+ 'object-shorthand': [
82
+ 'error',
83
+ 'always',
84
+ {
85
+ avoidQuotes: true,
86
+ },
87
+ ],
88
+ 'no-useless-escape': 'off',
89
+ 'no-case-declarations': 'off',
90
+ },
91
+ },
92
+ ...fixupConfigRules(
93
+ compat.extends(
94
+ 'plugin:react/recommended',
95
+ 'plugin:react/jsx-runtime',
96
+ 'plugin:react-hooks/recommended',
97
+ 'plugin:jsx-a11y/recommended',
98
+ ),
99
+ ).map((config) => ({
100
+ ...config,
101
+ files: ['**/*.{js,jsx,ts,tsx}'],
102
+ })),
103
+ {
104
+ files: ['**/*.{js,jsx,ts,tsx}'],
105
+ plugins: {
106
+ react: fixupPluginRules(react),
107
+ 'jsx-a11y': fixupPluginRules(jsxA11Y),
108
+ },
109
+ settings: {
110
+ react: {
111
+ version: 'detect',
112
+ },
113
+ formComponents: ['Form'],
114
+ linkComponents: [
115
+ {
116
+ name: 'Link',
117
+ linkAttribute: 'to',
118
+ },
119
+ {
120
+ name: 'NavLink',
121
+ linkAttribute: 'to',
122
+ },
123
+ ],
124
+ 'import/resolver': {
125
+ typescript: {},
126
+ },
127
+ },
128
+ rules: {
129
+ 'jsx-a11y/control-has-associated-label': 'off',
130
+ 'jsx-a11y/label-has-for': 'off',
131
+ 'react/display-name': 'off',
132
+ 'react/no-array-index-key': 'warn',
133
+ 'react/prop-types': 'off',
134
+ 'react/react-in-jsx-scope': 'off',
135
+ },
136
+ },
137
+ ...fixupConfigRules(
138
+ compat.extends(
139
+ 'plugin:@typescript-eslint/recommended',
140
+ 'plugin:import/recommended',
141
+ 'plugin:import/typescript',
142
+ ),
143
+ ).map((config) => ({
144
+ ...config,
145
+ files: ['**/*.{ts,tsx}'],
146
+ })),
147
+ {
148
+ files: ['**/*.{ts,tsx}'],
149
+ plugins: {
150
+ '@typescript-eslint': fixupPluginRules(typescriptEslint),
151
+ import: fixupPluginRules(_import),
152
+ },
153
+ languageOptions: {
154
+ parser: tsParser,
155
+ parserOptions: {
156
+ project: './tsconfig.json',
157
+ tsconfigRootDir: __dirname,
158
+ ecmaFeatures: {
159
+ jsx: true,
160
+ },
161
+ },
162
+ },
163
+ settings: {
164
+ 'import/internal-regex': '^~/',
165
+ 'import/resolvers': {
166
+ node: {
167
+ extensions: ['.ts', '.tsx'],
168
+ },
169
+ typescript: {
170
+ alwaysTryTypes: true,
171
+ project: __dirname,
172
+ },
173
+ },
174
+ },
175
+ rules: {
176
+ '@typescript-eslint/ban-ts-comment': 'off',
177
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
178
+ '@typescript-eslint/naming-convention': [
179
+ 'error',
180
+ {
181
+ selector: 'default',
182
+ format: ['camelCase', 'PascalCase', 'UPPER_CASE'],
183
+ leadingUnderscore: 'allowSingleOrDouble',
184
+ trailingUnderscore: 'allowSingleOrDouble',
185
+ },
186
+ {
187
+ selector: 'typeLike',
188
+ format: ['PascalCase'],
189
+ },
190
+ {
191
+ selector: 'typeParameter',
192
+ format: ['PascalCase'],
193
+ leadingUnderscore: 'allow',
194
+ },
195
+ {
196
+ selector: 'interface',
197
+ format: ['PascalCase'],
198
+ },
199
+ {
200
+ selector: 'property',
201
+ format: null,
202
+ },
203
+ ],
204
+ '@typescript-eslint/no-empty-function': 'off',
205
+ '@typescript-eslint/no-empty-interface': 'off',
206
+ '@typescript-eslint/no-empty-object-type': 'off',
207
+ '@typescript-eslint/no-explicit-any': 'off',
208
+ '@typescript-eslint/no-non-null-assertion': 'off',
209
+ '@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
210
+ '@typescript-eslint/no-unused-vars': 'off',
211
+ '@typescript-eslint/no-floating-promises': 'error',
212
+ '@typescript-eslint/no-misused-promises': 'error',
213
+ 'react/prop-types': 'off',
214
+ 'import/no-unresolved': ['error', {ignore: ['^virtual:']}],
215
+ },
216
+ },
217
+ {
218
+ files: ['**/.eslintrc.cjs'],
219
+ languageOptions: {
220
+ globals: {
221
+ ...globals.node,
222
+ },
223
+ },
224
+ },
225
+ ...compat.extends('plugin:jest/recommended').map((config) => ({
226
+ ...config,
227
+ files: ['**/*.test.*'],
228
+ })),
229
+ {
230
+ files: ['**/*.test.*'],
231
+ plugins: {
232
+ jest,
233
+ },
234
+ languageOptions: {
235
+ globals: {
236
+ ...globals.node,
237
+ ...globals.jest,
238
+ },
239
+ },
240
+ },
241
+ {
242
+ files: ['**/*.server.*'],
243
+ rules: {
244
+ 'react-hooks/rules-of-hooks': 'off',
245
+ },
246
+ },
247
+ ];
@@ -0,0 +1,394 @@
1
+ # Hydrogen Predictive Search
2
+
3
+ Our skeleton template ships with predictive search functionality. While [regular search](../search/search.md)
4
+ provides paginated search of `pages`, `articles` and `products` inside the `/search` route,
5
+ predictive provides real-time results in a aside drawer for `pages`, `articles`, `products`, `collections` and
6
+ recommended `queries/suggestions`.
7
+
8
+ This integration uses the storefront API (SFAPI) [predictiveSearch](https://shopify.dev/docs/api/storefront/latest/queries/vpredictiveSearch) endpoint to retrieve predictive search results based on a search term.
9
+
10
+ ## Components Architecture
11
+
12
+ ![alt text](./predictiveSearch.jpg)
13
+
14
+ ## Components
15
+
16
+ | File | Description |
17
+ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
18
+ | [`app/components/SearchFormPredictive.tsx`](../../app/components/SearchFormPredictive.tsx) | A fully customizable form component configured to make form `GET` requests to the `/search` route. |
19
+ | [`app/components/SearchResultsPredictive.tsx`](../../app/components/SearchResultsPredictive.tsx) | A fully customizable search results wrapper, that provides compound components to render `articles`, `pages`, `products`, `collections` and `queries`. |
20
+
21
+ ## Instructions
22
+
23
+ ### 1. Create the search route
24
+
25
+ Create a new file at `/routes/search.tsx` (if not already created)
26
+
27
+ ### 3. Add `predictiveSearch` query and fetcher
28
+
29
+ The predictiveSearch fetcher parses the `q` and `limit` formData properties sent
30
+ by the `<SearchFormPredictive />` component and performs the predictive search
31
+ SFAPI request.
32
+
33
+ ```ts
34
+ /**
35
+ * Predictive search query and fragments
36
+ * (adjust as needed)
37
+ */
38
+ const PREDICTIVE_SEARCH_ARTICLE_FRAGMENT = `#graphql
39
+ fragment PredictiveArticle on Article {
40
+ __typename
41
+ id
42
+ title
43
+ handle
44
+ blog {
45
+ handle
46
+ }
47
+ image {
48
+ url
49
+ altText
50
+ width
51
+ height
52
+ }
53
+ trackingParameters
54
+ }
55
+ ` as const;
56
+
57
+ const PREDICTIVE_SEARCH_COLLECTION_FRAGMENT = `#graphql
58
+ fragment PredictiveCollection on Collection {
59
+ __typename
60
+ id
61
+ title
62
+ handle
63
+ image {
64
+ url
65
+ altText
66
+ width
67
+ height
68
+ }
69
+ trackingParameters
70
+ }
71
+ ` as const;
72
+
73
+ const PREDICTIVE_SEARCH_PAGE_FRAGMENT = `#graphql
74
+ fragment PredictivePage on Page {
75
+ __typename
76
+ id
77
+ title
78
+ handle
79
+ trackingParameters
80
+ }
81
+ ` as const;
82
+
83
+ const PREDICTIVE_SEARCH_PRODUCT_FRAGMENT = `#graphql
84
+ fragment PredictiveProduct on Product {
85
+ __typename
86
+ id
87
+ title
88
+ handle
89
+ trackingParameters
90
+ selectedOrFirstAvailableVariant(
91
+ selectedOptions: []
92
+ ignoreUnknownOptions: true
93
+ caseInsensitiveMatch: true
94
+ ) {
95
+ id
96
+ image {
97
+ url
98
+ altText
99
+ width
100
+ height
101
+ }
102
+ price {
103
+ amount
104
+ currencyCode
105
+ }
106
+ }
107
+ }
108
+ ` as const;
109
+
110
+ const PREDICTIVE_SEARCH_QUERY_FRAGMENT = `#graphql
111
+ fragment PredictiveQuery on SearchQuerySuggestion {
112
+ __typename
113
+ text
114
+ styledText
115
+ trackingParameters
116
+ }
117
+ ` as const;
118
+
119
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/queries/predictiveSearch
120
+ const PREDICTIVE_SEARCH_QUERY = `#graphql
121
+ query predictiveSearch(
122
+ $country: CountryCode
123
+ $language: LanguageCode
124
+ $limit: Int!
125
+ $limitScope: PredictiveSearchLimitScope!
126
+ $term: String!
127
+ $types: [PredictiveSearchType!]
128
+ ) @inContext(country: $country, language: $language) {
129
+ predictiveSearch(
130
+ limit: $limit,
131
+ limitScope: $limitScope,
132
+ query: $term,
133
+ types: $types,
134
+ ) {
135
+ articles {
136
+ ...PredictiveArticle
137
+ }
138
+ collections {
139
+ ...PredictiveCollection
140
+ }
141
+ pages {
142
+ ...PredictivePage
143
+ }
144
+ products {
145
+ ...PredictiveProduct
146
+ }
147
+ queries {
148
+ ...PredictiveQuery
149
+ }
150
+ }
151
+ }
152
+ ${PREDICTIVE_SEARCH_ARTICLE_FRAGMENT}
153
+ ${PREDICTIVE_SEARCH_COLLECTION_FRAGMENT}
154
+ ${PREDICTIVE_SEARCH_PAGE_FRAGMENT}
155
+ ${PREDICTIVE_SEARCH_PRODUCT_FRAGMENT}
156
+ ${PREDICTIVE_SEARCH_QUERY_FRAGMENT}
157
+ ` as const;
158
+
159
+ /**
160
+ * Predictive search fetcher
161
+ */
162
+ async function predictiveSearch({
163
+ request,
164
+ context,
165
+ }: Pick<ActionFunctionArgs, 'request' | 'context'>) {
166
+ const {storefront} = context;
167
+ const formData = await request.formData();
168
+ const term = String(formData.get('q') || '');
169
+
170
+ const limit = Number(formData.get('limit') || 10);
171
+
172
+ // Predictively search articles, collections, pages, products, and queries (suggestions)
173
+ const {predictiveSearch: items, errors} = await storefront.query(
174
+ PREDICTIVE_SEARCH_QUERY,
175
+ {
176
+ variables: {
177
+ // customize search options as needed
178
+ limit,
179
+ limitScope: 'EACH',
180
+ term,
181
+ },
182
+ },
183
+ );
184
+
185
+ if (errors) {
186
+ throw new Error(
187
+ `Shopify API errors: ${errors.map(({message}) => message).join(', ')}`,
188
+ );
189
+ }
190
+
191
+ if (!items) {
192
+ throw new Error('No predictive search data returned');
193
+ }
194
+
195
+ const total = Object.values(items).reduce((acc, {length}) => acc + length, 0);
196
+
197
+ return {term, result: {items, total}, error: null};
198
+ }
199
+ ```
200
+
201
+ ### 3. Add a `loader` export to the route
202
+
203
+ This action receives and processes `GET` requests from the `<SearchFormPredictive />`
204
+ component. These request include the search parameter `predictive` to identify them over
205
+ regular search requests.
206
+
207
+ A `q` URL parameter will be used as the search term and appended automatically by
208
+ the form if present in it's children prop.
209
+
210
+ ```ts
211
+ /**
212
+ * Handles predictive search GET requests
213
+ * requested by the SearchFormPredictive component
214
+ */
215
+ export async function loader({request, context}: LoaderFunctionArgs) {
216
+ const url = new URL(request.url);
217
+ const isPredictive = url.searchParams.has('predictive');
218
+
219
+ if (!isPredictive) {
220
+ return {};
221
+ }
222
+
223
+ const searchPromise = predictiveSearch({request, context});
224
+
225
+ searchPromise.catch((error: Error) => {
226
+ console.error(error);
227
+ return {term: '', result: null, error: error.message};
228
+ });
229
+
230
+ return await searchPromise;
231
+ }
232
+ ```
233
+
234
+ ### 4. Render the predictive search form and results
235
+
236
+ Create a SearchAside or similar component to render the form and results.
237
+
238
+ ```ts
239
+ import { SearchFormPredictive } from '~/components/SearchFormPredictive';
240
+ import { SearchResultsPredictive } from '~/components/SearchResultsPredictive';
241
+
242
+ function SearchAside() {
243
+ return (
244
+ <Aside type="search" heading="SEARCH">
245
+ <div className="predictive-search">
246
+ <br />
247
+ <SearchFormPredictive>
248
+ {({ fetchResults, goToSearch, inputRef }) => (
249
+ <>
250
+ <input
251
+ name="q"
252
+ onChange={fetchResults}
253
+ onFocus={fetchResults}
254
+ placeholder="Search"
255
+ ref={inputRef}
256
+ type="search"
257
+ />
258
+ &nbsp;
259
+ <button onClick={goToSearch}>
260
+ Search
261
+ </button>
262
+ </>
263
+ )}
264
+ </SearchFormPredictive>
265
+
266
+ <SearchResultsPredictive>
267
+ {({ items, total, term, state, inputRef, closeSearch }) => {
268
+ const { articles, collections, pages, products, queries } = items;
269
+
270
+ if (state === 'loading' && term.current) {
271
+ return <div>Loading...</div>;
272
+ }
273
+
274
+ if (!total) {
275
+ return <SearchResultsPredictive.Empty term={term} />;
276
+ }
277
+
278
+ return (
279
+ <>
280
+ <SearchResultsPredictive.Queries
281
+ queries={queries}
282
+ term={term}
283
+ inputRef={inputRef}
284
+ />
285
+ <SearchResultsPredictive.Products
286
+ products={products}
287
+ closeSearch={closeSearch}
288
+ term={term}
289
+ />
290
+ <SearchResultsPredictive.Collections
291
+ collections={collections}
292
+ closeSearch={closeSearch}
293
+ term={term}
294
+ />
295
+ <SearchResultsPredictive.Pages
296
+ pages={pages}
297
+ closeSearch={closeSearch}
298
+ term={term}
299
+ />
300
+ <SearchResultsPredictive.Articles
301
+ articles={articles}
302
+ closeSearch={closeSearch}
303
+ term={term}
304
+ />
305
+ {term.current && total && (
306
+ <Link onClick={closeSearch} to={`/search?q=${term.current}`}>
307
+ <p>
308
+ View all results for <q>{term.current}</q> →
309
+ </p>
310
+ </Link>
311
+ )}
312
+ </>
313
+ );
314
+ }}
315
+ </SearchResultsPredictive>
316
+ </div>
317
+ </Aside>
318
+ );
319
+ }
320
+ ```
321
+
322
+ ## Additional Notes
323
+
324
+ ### How to use a different URL search parameter?
325
+
326
+ - Modify the `name` attribute in the forms input element. e.g
327
+
328
+ ```ts
329
+ <input name="query" />`.
330
+ ```
331
+
332
+ - Modify the fetchers term variable to parse the new name. e.g
333
+
334
+ ```ts
335
+ const term = String(searchParams.get('query') || '');
336
+ ```
337
+
338
+ ### How to customize the way the results look?
339
+
340
+ Simply go to `/app/components/SearchResultsPredictive.tsx` and look for the compound
341
+ component you would like to modify.
342
+
343
+ Here we add images to each predictive product result item
344
+
345
+ ```diff
346
+ SearchResultsPredictive.Products = function ({
347
+ products,
348
+ closeSearch,
349
+ term,
350
+ }: SearchResultsPredictiveProductsProps) {
351
+ if (!products.length) return null;
352
+
353
+ return (
354
+ <div className="predictive-search-result" key="products">
355
+ <h5>Products</h5>
356
+ <ul>
357
+ {products.map((product) => {
358
+ const productUrl = urlWithTrackingParams({
359
+ baseUrl: `/products/${product.handle}`,
360
+ trackingParams: product.trackingParameters,
361
+ term: term.current,
362
+ });
363
+ + const price = product?.selectedOrFirstAvailableVariant?.price;
364
+ + const image = product?.selectedOrFirstAvailableVariant?.image;
365
+ return (
366
+ <li className="predictive-search-result-item" key={product.id}>
367
+ <Link to={productUrl} onClick={closeSearch}>
368
+ + {image && (
369
+ + <Image
370
+ + alt={image.altText ?? ''}
371
+ + src={image.url}
372
+ + width={50}
373
+ + height={50}
374
+ + />
375
+ + )}
376
+ <div>
377
+ <p>{product.title}</p>
378
+ <small>
379
+ + {price && (
380
+ + <Money
381
+ + data={price}
382
+ + />
383
+ + )}
384
+ </small>
385
+ </div>
386
+ </Link>
387
+ </li>
388
+ );
389
+ })}
390
+ </ul>
391
+ </div>
392
+ )
393
+ };
394
+ ```