@shopify/cli 3.65.1 → 3.65.3
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/dist/assets/hydrogen/i18n/domains.ts +4 -11
- package/dist/assets/hydrogen/i18n/mock-i18n-types.ts +4 -2
- package/dist/assets/hydrogen/i18n/subdomains.ts +4 -11
- package/dist/assets/hydrogen/i18n/subfolders.ts +4 -11
- package/dist/assets/hydrogen/starter/CHANGELOG.md +165 -0
- package/dist/assets/hydrogen/starter/app/components/CartLineItem.tsx +5 -2
- package/dist/assets/hydrogen/starter/app/components/CartMain.tsx +2 -2
- package/dist/assets/hydrogen/starter/app/components/PageLayout.tsx +65 -19
- package/dist/assets/hydrogen/starter/app/components/PaginatedResourceSection.tsx +42 -0
- package/dist/assets/hydrogen/starter/app/components/SearchForm.tsx +68 -0
- package/dist/assets/hydrogen/starter/app/components/SearchFormPredictive.tsx +76 -0
- package/dist/assets/hydrogen/starter/app/components/SearchResults.tsx +164 -0
- package/dist/assets/hydrogen/starter/app/components/SearchResultsPredictive.tsx +322 -0
- package/dist/assets/hydrogen/starter/app/entry.client.tsx +10 -8
- package/dist/assets/hydrogen/starter/app/entry.server.tsx +1 -1
- package/dist/assets/hydrogen/starter/app/lib/context.ts +43 -0
- package/dist/assets/hydrogen/starter/app/lib/fragments.ts +53 -0
- package/dist/assets/hydrogen/starter/app/lib/search.ts +74 -24
- package/dist/assets/hydrogen/starter/app/root.tsx +4 -7
- package/dist/assets/hydrogen/starter/app/routes/account.addresses.tsx +2 -3
- package/dist/assets/hydrogen/starter/app/routes/account.orders._index.tsx +5 -19
- package/dist/assets/hydrogen/starter/app/routes/blogs.$blogHandle._index.tsx +11 -24
- package/dist/assets/hydrogen/starter/app/routes/blogs._index.tsx +14 -27
- package/dist/assets/hydrogen/starter/app/routes/collections.$handle.tsx +12 -30
- package/dist/assets/hydrogen/starter/app/routes/collections._index.tsx +13 -27
- package/dist/assets/hydrogen/starter/app/routes/collections.all.tsx +9 -31
- package/dist/assets/hydrogen/starter/app/routes/search.tsx +312 -73
- package/dist/assets/hydrogen/starter/app/styles/reset.css +12 -2
- package/dist/assets/hydrogen/starter/env.d.ts +11 -30
- package/dist/assets/hydrogen/starter/guides/predictiveSearch/predictiveSearch.jpg +0 -0
- package/dist/assets/hydrogen/starter/guides/predictiveSearch/predictiveSearch.md +391 -0
- package/dist/assets/hydrogen/starter/guides/search/search.jpg +0 -0
- package/dist/assets/hydrogen/starter/guides/search/search.md +333 -0
- package/dist/assets/hydrogen/starter/package.json +4 -4
- package/dist/assets/hydrogen/starter/server.ts +18 -74
- package/dist/assets/hydrogen/starter/storefrontapi.generated.d.ts +242 -172
- package/dist/assets/hydrogen/virtual-routes/components/{PageLayout.jsx → Layout.jsx} +2 -2
- package/dist/assets/hydrogen/virtual-routes/virtual-root.jsx +7 -6
- package/dist/{chunk-H42RFZDD.js → chunk-3ABSSTBQ.js} +4 -4
- package/dist/{chunk-M7WMYV4S.js → chunk-3D4VZQOH.js} +2 -2
- package/dist/{chunk-J7BYFGNJ.js → chunk-3GSKXZGY.js} +2 -2
- package/dist/{chunk-TDWX3KIR.js → chunk-3LDWVYMD.js} +2 -2
- package/dist/{chunk-N2BXKOJG.js → chunk-646BIVHE.js} +4 -4
- package/dist/{chunk-CZ3SHYYH.js → chunk-7WAEFADN.js} +4 -4
- package/dist/{chunk-EKT2GUGH.js → chunk-7WGBIPDW.js} +2 -2
- package/dist/{chunk-M6KGRVDD.js → chunk-AX77SAMU.js} +3 -3
- package/dist/{chunk-4HAEQQTQ.js → chunk-BQBBVYYU.js} +4 -4
- package/dist/{chunk-5YD4FDOS.js → chunk-BZLNTDGG.js} +3 -3
- package/dist/{chunk-VWALMO2Z.js → chunk-CSCEGIBZ.js} +3 -3
- package/dist/{chunk-F2Y7KYHZ.js → chunk-EIUQV76I.js} +5 -5
- package/dist/{chunk-MODBIZ4R.js → chunk-GN74L7IW.js} +2 -2
- package/dist/{chunk-5EAVIJTQ.js → chunk-HYCRESCR.js} +2 -2
- package/dist/{chunk-GDARYUPU.js → chunk-K7KD247K.js} +188 -243
- package/dist/{chunk-PZM45AUI.js → chunk-KIUXMPTX.js} +3 -3
- package/dist/{chunk-PYMSCBPA.js → chunk-LAJ4OEME.js} +2 -2
- package/dist/{chunk-YVHV3H5H.js → chunk-MIQBXNSN.js} +4 -4
- package/dist/{chunk-BLKDGMHM.js → chunk-MV6A3QHA.js} +4 -4
- package/dist/{chunk-CFFAWVDL.js → chunk-N3YORLAS.js} +2 -2
- package/dist/{chunk-EU5ZOEUT.js → chunk-NBTEOGQW.js} +2 -2
- package/dist/{chunk-ZXJU6UP4.js → chunk-O3JOUAA5.js} +4 -4
- package/dist/{chunk-EZ5DG73H.js → chunk-PEAIOYXD.js} +4 -4
- package/dist/{chunk-YDS7NZBQ.js → chunk-R5GT4GBL.js} +4 -4
- package/dist/{chunk-6M65VRAT.js → chunk-S7FJTFYR.js} +5 -5
- package/dist/{chunk-DX2RXOQ5.js → chunk-S7RH664J.js} +3 -3
- package/dist/{chunk-WMECC32P.js → chunk-SKF2SKWO.js} +3 -3
- package/dist/{chunk-27HGZPUX.js → chunk-SMKCVFDT.js} +3 -3
- package/dist/{chunk-EID6L4PR.js → chunk-T4Y7NDNJ.js} +2 -2
- package/dist/{chunk-PY33KMCK.js → chunk-TWWJNMTO.js} +2 -2
- package/dist/{chunk-YXPGPWR2.js → chunk-U2PN6QZ2.js} +5 -5
- package/dist/{chunk-3REVOIEW.js → chunk-UBCH575K.js} +5 -5
- package/dist/{chunk-A4NQWDPT.js → chunk-XLURAR5E.js} +3 -3
- package/dist/{chunk-ZZKUI3DP.js → chunk-YPG7LXPN.js} +3 -3
- package/dist/cli/commands/auth/logout.js +10 -10
- package/dist/cli/commands/auth/logout.test.js +11 -11
- package/dist/cli/commands/debug/command-flags.js +9 -9
- package/dist/cli/commands/demo/catalog.js +10 -10
- package/dist/cli/commands/demo/generate-file.js +10 -10
- package/dist/cli/commands/demo/index.js +10 -10
- package/dist/cli/commands/demo/print-ai-prompt.js +10 -10
- package/dist/cli/commands/docs/generate.js +9 -9
- package/dist/cli/commands/docs/generate.test.js +9 -9
- package/dist/cli/commands/help.js +9 -9
- package/dist/cli/commands/kitchen-sink/async.js +10 -10
- package/dist/cli/commands/kitchen-sink/async.test.js +10 -10
- package/dist/cli/commands/kitchen-sink/index.js +12 -12
- package/dist/cli/commands/kitchen-sink/index.test.js +12 -12
- package/dist/cli/commands/kitchen-sink/prompts.js +10 -10
- package/dist/cli/commands/kitchen-sink/prompts.test.js +10 -10
- package/dist/cli/commands/kitchen-sink/static.js +10 -10
- package/dist/cli/commands/kitchen-sink/static.test.js +10 -10
- package/dist/cli/commands/search.js +10 -10
- package/dist/cli/commands/upgrade.js +9 -9
- package/dist/cli/commands/version.js +10 -10
- package/dist/cli/commands/version.test.js +10 -10
- package/dist/cli/services/commands/search.js +2 -2
- package/dist/cli/services/commands/search.test.js +2 -2
- package/dist/cli/services/commands/version.js +4 -4
- package/dist/cli/services/commands/version.test.js +5 -5
- package/dist/cli/services/demo.js +2 -2
- package/dist/cli/services/demo.test.js +2 -2
- package/dist/cli/services/kitchen-sink/async.js +2 -2
- package/dist/cli/services/kitchen-sink/prompts.js +2 -2
- package/dist/cli/services/kitchen-sink/static.js +2 -2
- package/dist/cli/services/upgrade.js +3 -3
- package/dist/cli/services/upgrade.test.js +5 -5
- package/dist/{custom-oclif-loader-JHNX2EGV.js → custom-oclif-loader-BT7EH2NN.js} +3 -3
- package/dist/{error-handler-4UJ6363X.js → error-handler-OSEY6KVA.js} +8 -8
- package/dist/hooks/postrun.js +6 -6
- package/dist/hooks/prerun.js +4 -4
- package/dist/index.js +1333 -1279
- package/dist/{local-V7RONWNU.js → local-OQXN5NM2.js} +2 -2
- package/dist/{morph-DN4AZJZW.js → morph-IQTWRBBT.js} +16 -12
- package/dist/{node-3H4OKRLA.js → node-YQVH3Y7J.js} +13 -13
- package/dist/{node-package-manager-XM7EXHQA.js → node-package-manager-VW2DN7R4.js} +3 -3
- package/dist/{system-F63VIZ5U.js → system-347PZWVP.js} +2 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/{ui-BXWWRIFS.js → ui-S7L55PBH.js} +2 -2
- package/dist/{workerd-A5NCF6UA.js → workerd-OLKE7G4X.js} +12 -12
- package/oclif.manifest.json +39 -2
- package/package.json +7 -7
- package/dist/assets/hydrogen/starter/app/components/Search.tsx +0 -514
- package/dist/assets/hydrogen/starter/app/routes/api.predictive-search.tsx +0 -318
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import {Link} from '@remix-run/react';
|
|
2
|
+
import {Image, Money, Pagination} from '@shopify/hydrogen';
|
|
3
|
+
import {urlWithTrackingParams, type RegularSearchReturn} from '~/lib/search';
|
|
4
|
+
|
|
5
|
+
type SearchItems = RegularSearchReturn['result']['items'];
|
|
6
|
+
type PartialSearchResult<ItemType extends keyof SearchItems> = Pick<
|
|
7
|
+
SearchItems,
|
|
8
|
+
ItemType
|
|
9
|
+
> &
|
|
10
|
+
Pick<RegularSearchReturn, 'term'>;
|
|
11
|
+
|
|
12
|
+
type SearchResultsProps = RegularSearchReturn & {
|
|
13
|
+
children: (args: SearchItems & {term: string}) => React.ReactNode;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function SearchResults({
|
|
17
|
+
term,
|
|
18
|
+
result,
|
|
19
|
+
children,
|
|
20
|
+
}: Omit<SearchResultsProps, 'error' | 'type'>) {
|
|
21
|
+
if (!result?.total) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return children({...result.items, term});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
SearchResults.Articles = SearchResultsArticles;
|
|
29
|
+
SearchResults.Pages = SearchResultsPages;
|
|
30
|
+
SearchResults.Products = SearchResultsProducts;
|
|
31
|
+
SearchResults.Empty = SearchResultsEmpty;
|
|
32
|
+
|
|
33
|
+
function SearchResultsArticles({
|
|
34
|
+
term,
|
|
35
|
+
articles,
|
|
36
|
+
}: PartialSearchResult<'articles'>) {
|
|
37
|
+
if (!articles?.nodes.length) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="search-result">
|
|
43
|
+
<h2>Articles</h2>
|
|
44
|
+
<div>
|
|
45
|
+
{articles?.nodes?.map((article) => {
|
|
46
|
+
const articleUrl = urlWithTrackingParams({
|
|
47
|
+
baseUrl: `/blogs/${article.handle}`,
|
|
48
|
+
trackingParams: article.trackingParameters,
|
|
49
|
+
term,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="search-results-item" key={article.id}>
|
|
54
|
+
<Link prefetch="intent" to={articleUrl}>
|
|
55
|
+
{article.title}
|
|
56
|
+
</Link>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
})}
|
|
60
|
+
</div>
|
|
61
|
+
<br />
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function SearchResultsPages({term, pages}: PartialSearchResult<'pages'>) {
|
|
67
|
+
if (!pages?.nodes.length) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="search-result">
|
|
73
|
+
<h2>Pages</h2>
|
|
74
|
+
<div>
|
|
75
|
+
{pages?.nodes?.map((page) => {
|
|
76
|
+
const pageUrl = urlWithTrackingParams({
|
|
77
|
+
baseUrl: `/pages/${page.handle}`,
|
|
78
|
+
trackingParams: page.trackingParameters,
|
|
79
|
+
term,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className="search-results-item" key={page.id}>
|
|
84
|
+
<Link prefetch="intent" to={pageUrl}>
|
|
85
|
+
{page.title}
|
|
86
|
+
</Link>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
})}
|
|
90
|
+
</div>
|
|
91
|
+
<br />
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function SearchResultsProducts({
|
|
97
|
+
term,
|
|
98
|
+
products,
|
|
99
|
+
}: PartialSearchResult<'products'>) {
|
|
100
|
+
if (!products?.nodes.length) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className="search-result">
|
|
106
|
+
<h2>Products</h2>
|
|
107
|
+
<Pagination connection={products}>
|
|
108
|
+
{({nodes, isLoading, NextLink, PreviousLink}) => {
|
|
109
|
+
const ItemsMarkup = nodes.map((product) => {
|
|
110
|
+
const productUrl = urlWithTrackingParams({
|
|
111
|
+
baseUrl: `/products/${product.handle}`,
|
|
112
|
+
trackingParams: product.trackingParameters,
|
|
113
|
+
term,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div className="search-results-item" key={product.id}>
|
|
118
|
+
<Link prefetch="intent" to={productUrl}>
|
|
119
|
+
{product.variants.nodes[0].image && (
|
|
120
|
+
<Image
|
|
121
|
+
data={product.variants.nodes[0].image}
|
|
122
|
+
alt={product.title}
|
|
123
|
+
width={50}
|
|
124
|
+
/>
|
|
125
|
+
)}
|
|
126
|
+
<div>
|
|
127
|
+
<p>{product.title}</p>
|
|
128
|
+
<small>
|
|
129
|
+
<Money data={product.variants.nodes[0].price} />
|
|
130
|
+
</small>
|
|
131
|
+
</div>
|
|
132
|
+
</Link>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div>
|
|
139
|
+
<div>
|
|
140
|
+
<PreviousLink>
|
|
141
|
+
{isLoading ? 'Loading...' : <span>↑ Load previous</span>}
|
|
142
|
+
</PreviousLink>
|
|
143
|
+
</div>
|
|
144
|
+
<div>
|
|
145
|
+
{ItemsMarkup}
|
|
146
|
+
<br />
|
|
147
|
+
</div>
|
|
148
|
+
<div>
|
|
149
|
+
<NextLink>
|
|
150
|
+
{isLoading ? 'Loading...' : <span>Load more ↓</span>}
|
|
151
|
+
</NextLink>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}}
|
|
156
|
+
</Pagination>
|
|
157
|
+
<br />
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function SearchResultsEmpty() {
|
|
163
|
+
return <p>No results, try a different search.</p>;
|
|
164
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import {Link, useFetcher, type Fetcher} from '@remix-run/react';
|
|
2
|
+
import {Image, Money} from '@shopify/hydrogen';
|
|
3
|
+
import React, {useRef, useEffect} from 'react';
|
|
4
|
+
import {
|
|
5
|
+
getEmptyPredictiveSearchResult,
|
|
6
|
+
urlWithTrackingParams,
|
|
7
|
+
type PredictiveSearchReturn,
|
|
8
|
+
} from '~/lib/search';
|
|
9
|
+
import {useAside} from './Aside';
|
|
10
|
+
|
|
11
|
+
type PredictiveSearchItems = PredictiveSearchReturn['result']['items'];
|
|
12
|
+
|
|
13
|
+
type UsePredictiveSearchReturn = {
|
|
14
|
+
term: React.MutableRefObject<string>;
|
|
15
|
+
total: number;
|
|
16
|
+
inputRef: React.MutableRefObject<HTMLInputElement | null>;
|
|
17
|
+
items: PredictiveSearchItems;
|
|
18
|
+
fetcher: Fetcher<PredictiveSearchReturn>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type SearchResultsPredictiveArgs = Pick<
|
|
22
|
+
UsePredictiveSearchReturn,
|
|
23
|
+
'term' | 'total' | 'inputRef' | 'items'
|
|
24
|
+
> & {
|
|
25
|
+
state: Fetcher['state'];
|
|
26
|
+
closeSearch: () => void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type PartialPredictiveSearchResult<
|
|
30
|
+
ItemType extends keyof PredictiveSearchItems,
|
|
31
|
+
ExtraProps extends keyof SearchResultsPredictiveArgs = 'term' | 'closeSearch',
|
|
32
|
+
> = Pick<PredictiveSearchItems, ItemType> &
|
|
33
|
+
Pick<SearchResultsPredictiveArgs, ExtraProps>;
|
|
34
|
+
|
|
35
|
+
type SearchResultsPredictiveProps = {
|
|
36
|
+
children: (args: SearchResultsPredictiveArgs) => React.ReactNode;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Component that renders predictive search results
|
|
41
|
+
*/
|
|
42
|
+
export function SearchResultsPredictive({
|
|
43
|
+
children,
|
|
44
|
+
}: SearchResultsPredictiveProps) {
|
|
45
|
+
const aside = useAside();
|
|
46
|
+
const {term, inputRef, fetcher, total, items} = usePredictiveSearch();
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
* Utility that resets the search input
|
|
50
|
+
*/
|
|
51
|
+
function resetInput() {
|
|
52
|
+
if (inputRef.current) {
|
|
53
|
+
inputRef.current.blur();
|
|
54
|
+
inputRef.current.value = '';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Utility that resets the search input and closes the search aside
|
|
60
|
+
*/
|
|
61
|
+
function closeSearch() {
|
|
62
|
+
resetInput();
|
|
63
|
+
aside.close();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return children({
|
|
67
|
+
items,
|
|
68
|
+
closeSearch,
|
|
69
|
+
inputRef,
|
|
70
|
+
state: fetcher.state,
|
|
71
|
+
term,
|
|
72
|
+
total,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
SearchResultsPredictive.Articles = SearchResultsPredictiveArticles;
|
|
77
|
+
SearchResultsPredictive.Collections = SearchResultsPredictiveCollections;
|
|
78
|
+
SearchResultsPredictive.Pages = SearchResultsPredictivePages;
|
|
79
|
+
SearchResultsPredictive.Products = SearchResultsPredictiveProducts;
|
|
80
|
+
SearchResultsPredictive.Queries = SearchResultsPredictiveQueries;
|
|
81
|
+
SearchResultsPredictive.Empty = SearchResultsPredictiveEmpty;
|
|
82
|
+
|
|
83
|
+
function SearchResultsPredictiveArticles({
|
|
84
|
+
term,
|
|
85
|
+
articles,
|
|
86
|
+
closeSearch,
|
|
87
|
+
}: PartialPredictiveSearchResult<'articles'>) {
|
|
88
|
+
if (!articles.length) return null;
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="predictive-search-result" key="articles">
|
|
92
|
+
<h5>Articles</h5>
|
|
93
|
+
<ul>
|
|
94
|
+
{articles.map((article) => {
|
|
95
|
+
const articleUrl = urlWithTrackingParams({
|
|
96
|
+
baseUrl: `/blogs/${article.blog.handle}/${article.handle}`,
|
|
97
|
+
trackingParams: article.trackingParameters,
|
|
98
|
+
term: term.current ?? '',
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<li className="predictive-search-result-item" key={article.id}>
|
|
103
|
+
<Link onClick={closeSearch} to={articleUrl}>
|
|
104
|
+
{article.image?.url && (
|
|
105
|
+
<Image
|
|
106
|
+
alt={article.image.altText ?? ''}
|
|
107
|
+
src={article.image.url}
|
|
108
|
+
width={50}
|
|
109
|
+
height={50}
|
|
110
|
+
/>
|
|
111
|
+
)}
|
|
112
|
+
<div>
|
|
113
|
+
<span>{article.title}</span>
|
|
114
|
+
</div>
|
|
115
|
+
</Link>
|
|
116
|
+
</li>
|
|
117
|
+
);
|
|
118
|
+
})}
|
|
119
|
+
</ul>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function SearchResultsPredictiveCollections({
|
|
125
|
+
term,
|
|
126
|
+
collections,
|
|
127
|
+
closeSearch,
|
|
128
|
+
}: PartialPredictiveSearchResult<'collections'>) {
|
|
129
|
+
if (!collections.length) return null;
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div className="predictive-search-result" key="collections">
|
|
133
|
+
<h5>Collections</h5>
|
|
134
|
+
<ul>
|
|
135
|
+
{collections.map((collection) => {
|
|
136
|
+
const colllectionUrl = urlWithTrackingParams({
|
|
137
|
+
baseUrl: `/collections/${collection.handle}`,
|
|
138
|
+
trackingParams: collection.trackingParameters,
|
|
139
|
+
term: term.current,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<li className="predictive-search-result-item" key={collection.id}>
|
|
144
|
+
<Link onClick={closeSearch} to={colllectionUrl}>
|
|
145
|
+
{collection.image?.url && (
|
|
146
|
+
<Image
|
|
147
|
+
alt={collection.image.altText ?? ''}
|
|
148
|
+
src={collection.image.url}
|
|
149
|
+
width={50}
|
|
150
|
+
height={50}
|
|
151
|
+
/>
|
|
152
|
+
)}
|
|
153
|
+
<div>
|
|
154
|
+
<span>{collection.title}</span>
|
|
155
|
+
</div>
|
|
156
|
+
</Link>
|
|
157
|
+
</li>
|
|
158
|
+
);
|
|
159
|
+
})}
|
|
160
|
+
</ul>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function SearchResultsPredictivePages({
|
|
166
|
+
term,
|
|
167
|
+
pages,
|
|
168
|
+
closeSearch,
|
|
169
|
+
}: PartialPredictiveSearchResult<'pages'>) {
|
|
170
|
+
if (!pages.length) return null;
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div className="predictive-search-result" key="pages">
|
|
174
|
+
<h5>Pages</h5>
|
|
175
|
+
<ul>
|
|
176
|
+
{pages.map((page) => {
|
|
177
|
+
const pageUrl = urlWithTrackingParams({
|
|
178
|
+
baseUrl: `/pages/${page.handle}`,
|
|
179
|
+
trackingParams: page.trackingParameters,
|
|
180
|
+
term: term.current,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<li className="predictive-search-result-item" key={page.id}>
|
|
185
|
+
<Link onClick={closeSearch} to={pageUrl}>
|
|
186
|
+
<div>
|
|
187
|
+
<span>{page.title}</span>
|
|
188
|
+
</div>
|
|
189
|
+
</Link>
|
|
190
|
+
</li>
|
|
191
|
+
);
|
|
192
|
+
})}
|
|
193
|
+
</ul>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function SearchResultsPredictiveProducts({
|
|
199
|
+
term,
|
|
200
|
+
products,
|
|
201
|
+
closeSearch,
|
|
202
|
+
}: PartialPredictiveSearchResult<'products'>) {
|
|
203
|
+
if (!products.length) return null;
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<div className="predictive-search-result" key="products">
|
|
207
|
+
<h5>Products</h5>
|
|
208
|
+
<ul>
|
|
209
|
+
{products.map((product) => {
|
|
210
|
+
const productUrl = urlWithTrackingParams({
|
|
211
|
+
baseUrl: `/products/${product.handle}`,
|
|
212
|
+
trackingParams: product.trackingParameters,
|
|
213
|
+
term: term.current,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const image = product?.variants?.nodes?.[0].image;
|
|
217
|
+
return (
|
|
218
|
+
<li className="predictive-search-result-item" key={product.id}>
|
|
219
|
+
<Link to={productUrl} onClick={closeSearch}>
|
|
220
|
+
{image && (
|
|
221
|
+
<Image
|
|
222
|
+
alt={image.altText ?? ''}
|
|
223
|
+
src={image.url}
|
|
224
|
+
width={50}
|
|
225
|
+
height={50}
|
|
226
|
+
/>
|
|
227
|
+
)}
|
|
228
|
+
<div>
|
|
229
|
+
<p>{product.title}</p>
|
|
230
|
+
<small>
|
|
231
|
+
{product?.variants?.nodes?.[0].price && (
|
|
232
|
+
<Money data={product.variants.nodes[0].price} />
|
|
233
|
+
)}
|
|
234
|
+
</small>
|
|
235
|
+
</div>
|
|
236
|
+
</Link>
|
|
237
|
+
</li>
|
|
238
|
+
);
|
|
239
|
+
})}
|
|
240
|
+
</ul>
|
|
241
|
+
</div>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function SearchResultsPredictiveQueries({
|
|
246
|
+
queries,
|
|
247
|
+
inputRef,
|
|
248
|
+
}: PartialPredictiveSearchResult<'queries', 'inputRef'>) {
|
|
249
|
+
if (!queries.length) return null;
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<div className="predictive-search-result" key="queries">
|
|
253
|
+
<h5>Queries</h5>
|
|
254
|
+
<ul>
|
|
255
|
+
{queries.map((suggestion) => {
|
|
256
|
+
if (!suggestion) return null;
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<li className="predictive-search-result-item" key={suggestion.text}>
|
|
260
|
+
<div
|
|
261
|
+
role="presentation"
|
|
262
|
+
onClick={() => {
|
|
263
|
+
if (!inputRef.current) return;
|
|
264
|
+
inputRef.current.value = suggestion.text;
|
|
265
|
+
inputRef.current.focus();
|
|
266
|
+
}}
|
|
267
|
+
dangerouslySetInnerHTML={{
|
|
268
|
+
__html: suggestion?.styledText,
|
|
269
|
+
}}
|
|
270
|
+
/>
|
|
271
|
+
</li>
|
|
272
|
+
);
|
|
273
|
+
})}
|
|
274
|
+
</ul>
|
|
275
|
+
</div>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function SearchResultsPredictiveEmpty({
|
|
280
|
+
term,
|
|
281
|
+
}: {
|
|
282
|
+
term: React.MutableRefObject<string>;
|
|
283
|
+
}) {
|
|
284
|
+
if (!term.current) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<p>
|
|
290
|
+
No results found for <q>{term.current}</q>
|
|
291
|
+
</p>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Hook that returns the predictive search results and fetcher and input ref.
|
|
297
|
+
* @example
|
|
298
|
+
* '''ts
|
|
299
|
+
* const { items, total, inputRef, term, fetcher } = usePredictiveSearch();
|
|
300
|
+
* '''
|
|
301
|
+
**/
|
|
302
|
+
function usePredictiveSearch(): UsePredictiveSearchReturn {
|
|
303
|
+
const fetcher = useFetcher<PredictiveSearchReturn>({key: 'search'});
|
|
304
|
+
const term = useRef<string>('');
|
|
305
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
306
|
+
|
|
307
|
+
if (fetcher?.state === 'loading') {
|
|
308
|
+
term.current = String(fetcher.formData?.get('q') || '');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// capture the search input element as a ref
|
|
312
|
+
useEffect(() => {
|
|
313
|
+
if (!inputRef.current) {
|
|
314
|
+
inputRef.current = document.querySelector('input[type="search"]');
|
|
315
|
+
}
|
|
316
|
+
}, []);
|
|
317
|
+
|
|
318
|
+
const {items, total} =
|
|
319
|
+
fetcher?.data?.result ?? getEmptyPredictiveSearchResult();
|
|
320
|
+
|
|
321
|
+
return {items, total, inputRef, term, fetcher};
|
|
322
|
+
}
|
|
@@ -2,11 +2,13 @@ import {RemixBrowser} from '@remix-run/react';
|
|
|
2
2
|
import {startTransition, StrictMode} from 'react';
|
|
3
3
|
import {hydrateRoot} from 'react-dom/client';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
if (!window.location.origin.includes('webcache.googleusercontent.com')) {
|
|
6
|
+
startTransition(() => {
|
|
7
|
+
hydrateRoot(
|
|
8
|
+
document,
|
|
9
|
+
<StrictMode>
|
|
10
|
+
<RemixBrowser />
|
|
11
|
+
</StrictMode>,
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {createHydrogenContext} from '@shopify/hydrogen';
|
|
2
|
+
import {AppSession} from '~/lib/session';
|
|
3
|
+
import {CART_QUERY_FRAGMENT} from '~/lib/fragments';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The context implementation is separate from server.ts
|
|
7
|
+
* so that type can be extracted for AppLoadContext
|
|
8
|
+
* */
|
|
9
|
+
export async function createAppLoadContext(
|
|
10
|
+
request: Request,
|
|
11
|
+
env: Env,
|
|
12
|
+
executionContext: ExecutionContext,
|
|
13
|
+
) {
|
|
14
|
+
/**
|
|
15
|
+
* Open a cache instance in the worker and a custom session instance.
|
|
16
|
+
*/
|
|
17
|
+
if (!env?.SESSION_SECRET) {
|
|
18
|
+
throw new Error('SESSION_SECRET environment variable is not set');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const waitUntil = executionContext.waitUntil.bind(executionContext);
|
|
22
|
+
const [cache, session] = await Promise.all([
|
|
23
|
+
caches.open('hydrogen'),
|
|
24
|
+
AppSession.init(request, [env.SESSION_SECRET]),
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const hydrogenContext = createHydrogenContext({
|
|
28
|
+
env,
|
|
29
|
+
request,
|
|
30
|
+
cache,
|
|
31
|
+
waitUntil,
|
|
32
|
+
session,
|
|
33
|
+
i18n: {language: 'EN', country: 'US'},
|
|
34
|
+
cart: {
|
|
35
|
+
queryFragment: CART_QUERY_FRAGMENT,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
...hydrogenContext,
|
|
41
|
+
// declare additional Remix loader context
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -55,6 +55,56 @@ export const CART_QUERY_FRAGMENT = `#graphql
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
fragment CartLineComponent on ComponentizableCartLine {
|
|
59
|
+
id
|
|
60
|
+
quantity
|
|
61
|
+
attributes {
|
|
62
|
+
key
|
|
63
|
+
value
|
|
64
|
+
}
|
|
65
|
+
cost {
|
|
66
|
+
totalAmount {
|
|
67
|
+
...Money
|
|
68
|
+
}
|
|
69
|
+
amountPerQuantity {
|
|
70
|
+
...Money
|
|
71
|
+
}
|
|
72
|
+
compareAtAmountPerQuantity {
|
|
73
|
+
...Money
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
merchandise {
|
|
77
|
+
... on ProductVariant {
|
|
78
|
+
id
|
|
79
|
+
availableForSale
|
|
80
|
+
compareAtPrice {
|
|
81
|
+
...Money
|
|
82
|
+
}
|
|
83
|
+
price {
|
|
84
|
+
...Money
|
|
85
|
+
}
|
|
86
|
+
requiresShipping
|
|
87
|
+
title
|
|
88
|
+
image {
|
|
89
|
+
id
|
|
90
|
+
url
|
|
91
|
+
altText
|
|
92
|
+
width
|
|
93
|
+
height
|
|
94
|
+
}
|
|
95
|
+
product {
|
|
96
|
+
handle
|
|
97
|
+
title
|
|
98
|
+
id
|
|
99
|
+
vendor
|
|
100
|
+
}
|
|
101
|
+
selectedOptions {
|
|
102
|
+
name
|
|
103
|
+
value
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
58
108
|
fragment CartApiQuery on Cart {
|
|
59
109
|
updatedAt
|
|
60
110
|
id
|
|
@@ -76,6 +126,9 @@ export const CART_QUERY_FRAGMENT = `#graphql
|
|
|
76
126
|
nodes {
|
|
77
127
|
...CartLine
|
|
78
128
|
}
|
|
129
|
+
nodes {
|
|
130
|
+
...CartLineComponent
|
|
131
|
+
}
|
|
79
132
|
}
|
|
80
133
|
cost {
|
|
81
134
|
subtotalAmount {
|