@shopify/create-hydrogen 4.3.13 → 5.0.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 (127) hide show
  1. package/dist/assets/hydrogen/bundle/analyzer.html +2045 -0
  2. package/dist/assets/hydrogen/i18n/domains.ts +28 -0
  3. package/dist/assets/hydrogen/i18n/mock-i18n-types.ts +3 -0
  4. package/dist/assets/hydrogen/i18n/subdomains.ts +27 -0
  5. package/dist/assets/hydrogen/i18n/subfolders.ts +29 -0
  6. package/dist/assets/hydrogen/routes/locale-check.ts +16 -0
  7. package/dist/assets/hydrogen/starter/.eslintignore +5 -0
  8. package/dist/assets/hydrogen/starter/.eslintrc.cjs +19 -0
  9. package/dist/assets/hydrogen/starter/.graphqlrc.yml +12 -0
  10. package/dist/assets/hydrogen/starter/CHANGELOG.md +709 -0
  11. package/dist/assets/hydrogen/starter/README.md +45 -0
  12. package/dist/assets/hydrogen/starter/app/assets/favicon.svg +28 -0
  13. package/dist/assets/hydrogen/starter/app/components/AddToCartButton.tsx +37 -0
  14. package/dist/assets/hydrogen/starter/app/components/Aside.tsx +76 -0
  15. package/dist/assets/hydrogen/starter/app/components/CartLineItem.tsx +150 -0
  16. package/dist/assets/hydrogen/starter/app/components/CartMain.tsx +68 -0
  17. package/dist/assets/hydrogen/starter/app/components/CartSummary.tsx +101 -0
  18. package/dist/assets/hydrogen/starter/app/components/Footer.tsx +129 -0
  19. package/dist/assets/hydrogen/starter/app/components/Header.tsx +230 -0
  20. package/dist/assets/hydrogen/starter/app/components/PageLayout.tsx +126 -0
  21. package/dist/assets/hydrogen/starter/app/components/ProductForm.tsx +80 -0
  22. package/dist/assets/hydrogen/starter/app/components/ProductImage.tsx +23 -0
  23. package/dist/assets/hydrogen/starter/app/components/ProductPrice.tsx +27 -0
  24. package/dist/assets/hydrogen/starter/app/components/Search.tsx +514 -0
  25. package/dist/assets/hydrogen/starter/app/entry.client.tsx +12 -0
  26. package/dist/assets/hydrogen/starter/app/entry.server.tsx +47 -0
  27. package/dist/assets/hydrogen/starter/app/graphql/customer-account/CustomerAddressMutations.ts +61 -0
  28. package/dist/assets/hydrogen/starter/app/graphql/customer-account/CustomerDetailsQuery.ts +40 -0
  29. package/dist/assets/hydrogen/starter/app/graphql/customer-account/CustomerOrderQuery.ts +87 -0
  30. package/dist/assets/hydrogen/starter/app/graphql/customer-account/CustomerOrdersQuery.ts +58 -0
  31. package/dist/assets/hydrogen/starter/app/graphql/customer-account/CustomerUpdateMutation.ts +24 -0
  32. package/dist/assets/hydrogen/starter/app/lib/fragments.ts +174 -0
  33. package/dist/assets/hydrogen/starter/app/lib/search.ts +29 -0
  34. package/dist/assets/hydrogen/starter/app/lib/session.ts +72 -0
  35. package/dist/assets/hydrogen/starter/app/lib/variants.ts +46 -0
  36. package/dist/assets/hydrogen/starter/app/root.tsx +191 -0
  37. package/dist/assets/hydrogen/starter/app/routes/$.tsx +11 -0
  38. package/dist/assets/hydrogen/starter/app/routes/[robots.txt].tsx +118 -0
  39. package/dist/assets/hydrogen/starter/app/routes/[sitemap.xml].tsx +177 -0
  40. package/dist/assets/hydrogen/starter/app/routes/_index.tsx +182 -0
  41. package/dist/assets/hydrogen/starter/app/routes/account.$.tsx +8 -0
  42. package/dist/assets/hydrogen/starter/app/routes/account._index.tsx +5 -0
  43. package/dist/assets/hydrogen/starter/app/routes/account.addresses.tsx +513 -0
  44. package/dist/assets/hydrogen/starter/app/routes/account.orders.$id.tsx +195 -0
  45. package/dist/assets/hydrogen/starter/app/routes/account.orders._index.tsx +107 -0
  46. package/dist/assets/hydrogen/starter/app/routes/account.profile.tsx +136 -0
  47. package/dist/assets/hydrogen/starter/app/routes/account.tsx +88 -0
  48. package/dist/assets/hydrogen/starter/app/routes/account_.authorize.tsx +5 -0
  49. package/dist/assets/hydrogen/starter/app/routes/account_.login.tsx +5 -0
  50. package/dist/assets/hydrogen/starter/app/routes/account_.logout.tsx +10 -0
  51. package/dist/assets/hydrogen/starter/app/routes/api.predictive-search.tsx +318 -0
  52. package/dist/assets/hydrogen/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +113 -0
  53. package/dist/assets/hydrogen/starter/app/routes/blogs.$blogHandle._index.tsx +188 -0
  54. package/dist/assets/hydrogen/starter/app/routes/blogs._index.tsx +119 -0
  55. package/dist/assets/hydrogen/starter/app/routes/cart.$lines.tsx +69 -0
  56. package/dist/assets/hydrogen/starter/app/routes/cart.tsx +102 -0
  57. package/dist/assets/hydrogen/starter/app/routes/collections.$handle.tsx +225 -0
  58. package/dist/assets/hydrogen/starter/app/routes/collections._index.tsx +146 -0
  59. package/dist/assets/hydrogen/starter/app/routes/collections.all.tsx +185 -0
  60. package/dist/assets/hydrogen/starter/app/routes/discount.$code.tsx +47 -0
  61. package/dist/assets/hydrogen/starter/app/routes/pages.$handle.tsx +84 -0
  62. package/dist/assets/hydrogen/starter/app/routes/policies.$handle.tsx +93 -0
  63. package/dist/assets/hydrogen/starter/app/routes/policies._index.tsx +63 -0
  64. package/dist/assets/hydrogen/starter/app/routes/products.$handle.tsx +299 -0
  65. package/dist/assets/hydrogen/starter/app/routes/search.tsx +177 -0
  66. package/dist/assets/hydrogen/starter/app/styles/app.css +486 -0
  67. package/dist/assets/hydrogen/starter/app/styles/reset.css +129 -0
  68. package/dist/assets/hydrogen/starter/customer-accountapi.generated.d.ts +509 -0
  69. package/dist/assets/hydrogen/starter/env.d.ts +54 -0
  70. package/dist/assets/hydrogen/starter/package.json +50 -0
  71. package/dist/assets/hydrogen/starter/public/.gitkeep +0 -0
  72. package/dist/assets/hydrogen/starter/server.ts +119 -0
  73. package/dist/assets/hydrogen/starter/storefrontapi.generated.d.ts +1211 -0
  74. package/dist/assets/hydrogen/starter/tsconfig.json +23 -0
  75. package/dist/assets/hydrogen/starter/vite.config.ts +41 -0
  76. package/dist/assets/hydrogen/tailwind/package.json +8 -0
  77. package/dist/assets/hydrogen/tailwind/tailwind.css +6 -0
  78. package/dist/assets/hydrogen/vanilla-extract/package.json +8 -0
  79. package/dist/assets/hydrogen/virtual-routes/assets/debug-network.css +592 -0
  80. package/dist/assets/hydrogen/virtual-routes/assets/favicon-dark.svg +20 -0
  81. package/dist/assets/hydrogen/virtual-routes/assets/favicon.svg +28 -0
  82. package/dist/assets/hydrogen/virtual-routes/assets/inter-variable-font.woff2 +0 -0
  83. package/dist/assets/hydrogen/virtual-routes/assets/jetbrainsmono-variable-font.woff2 +0 -0
  84. package/dist/assets/hydrogen/virtual-routes/assets/styles.css +238 -0
  85. package/dist/assets/hydrogen/virtual-routes/components/FlameChartWrapper.jsx +123 -0
  86. package/dist/assets/hydrogen/virtual-routes/components/HydrogenLogoBaseBW.jsx +32 -0
  87. package/dist/assets/hydrogen/virtual-routes/components/HydrogenLogoBaseColor.jsx +47 -0
  88. package/dist/assets/hydrogen/virtual-routes/components/IconBanner.jsx +292 -0
  89. package/dist/assets/hydrogen/virtual-routes/components/IconClose.jsx +38 -0
  90. package/dist/assets/hydrogen/virtual-routes/components/IconDiscard.jsx +44 -0
  91. package/dist/assets/hydrogen/virtual-routes/components/IconError.jsx +61 -0
  92. package/dist/assets/hydrogen/virtual-routes/components/IconGithub.jsx +23 -0
  93. package/dist/assets/hydrogen/virtual-routes/components/IconTwitter.jsx +21 -0
  94. package/dist/assets/hydrogen/virtual-routes/components/PageLayout.jsx +7 -0
  95. package/dist/assets/hydrogen/virtual-routes/components/RequestDetails.jsx +178 -0
  96. package/dist/assets/hydrogen/virtual-routes/components/RequestTable.jsx +91 -0
  97. package/dist/assets/hydrogen/virtual-routes/components/RequestWaterfall.jsx +151 -0
  98. package/dist/assets/hydrogen/virtual-routes/lib/useDebugNetworkServer.jsx +178 -0
  99. package/dist/assets/hydrogen/virtual-routes/routes/graphiql.jsx +5 -0
  100. package/dist/assets/hydrogen/virtual-routes/routes/index.jsx +265 -0
  101. package/dist/assets/hydrogen/virtual-routes/routes/subrequest-profiler.jsx +243 -0
  102. package/dist/assets/hydrogen/virtual-routes/virtual-root.jsx +64 -0
  103. package/dist/assets/hydrogen/vite/package.json +14 -0
  104. package/dist/assets/hydrogen/vite/vite.config.js +41 -0
  105. package/dist/chokidar-2CKIHN27.js +12 -0
  106. package/dist/chunk-EO6F7WJJ.js +2 -0
  107. package/dist/chunk-FB327AH7.js +5 -0
  108. package/dist/chunk-FJPX4XUR.js +2 -0
  109. package/dist/chunk-JKOXGRAA.js +10 -0
  110. package/dist/chunk-LNQWGFTB.js +45 -0
  111. package/dist/chunk-M6JXYI3V.js +23 -0
  112. package/dist/chunk-MNT4XW23.js +2 -0
  113. package/dist/chunk-N7HFZHSO.js +1145 -0
  114. package/dist/chunk-PMDMUCNY.js +2 -0
  115. package/dist/chunk-QGLB6FFL.js +3 -0
  116. package/dist/chunk-VMIOG46Y.js +2 -0
  117. package/dist/create-app.js +1867 -34
  118. package/dist/del-CZGKV5SQ.js +11 -0
  119. package/dist/devtools-ZCRGQE64.js +8 -0
  120. package/dist/error-handler-GEQXZJ25.js +2 -0
  121. package/dist/lib-NJYCLW6W.js +22 -0
  122. package/dist/morph-ZJCCGFNC.js +30499 -0
  123. package/dist/multipart-parser-6HGDQWV7.js +3 -0
  124. package/dist/open-OD6DRFEG.js +2 -0
  125. package/dist/out-7KAQXZLP.js +2 -0
  126. package/dist/yoga.wasm +0 -0
  127. package/package.json +7 -3
@@ -0,0 +1,107 @@
1
+ import {Link, useLoaderData, type MetaFunction} from '@remix-run/react';
2
+ import {
3
+ Money,
4
+ Pagination,
5
+ getPaginationVariables,
6
+ flattenConnection,
7
+ } from '@shopify/hydrogen';
8
+ import {json, redirect, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
9
+ import {CUSTOMER_ORDERS_QUERY} from '~/graphql/customer-account/CustomerOrdersQuery';
10
+ import type {
11
+ CustomerOrdersFragment,
12
+ OrderItemFragment,
13
+ } from 'customer-accountapi.generated';
14
+
15
+ export const meta: MetaFunction = () => {
16
+ return [{title: 'Orders'}];
17
+ };
18
+
19
+ export async function loader({request, context}: LoaderFunctionArgs) {
20
+ const paginationVariables = getPaginationVariables(request, {
21
+ pageBy: 20,
22
+ });
23
+
24
+ const {data, errors} = await context.customerAccount.query(
25
+ CUSTOMER_ORDERS_QUERY,
26
+ {
27
+ variables: {
28
+ ...paginationVariables,
29
+ },
30
+ },
31
+ );
32
+
33
+ if (errors?.length || !data?.customer) {
34
+ throw Error('Customer orders not found');
35
+ }
36
+
37
+ return json({customer: data.customer});
38
+ }
39
+
40
+ export default function Orders() {
41
+ const {customer} = useLoaderData<{customer: CustomerOrdersFragment}>();
42
+ const {orders} = customer;
43
+ return (
44
+ <div className="orders">
45
+ {orders.nodes.length ? <OrdersTable orders={orders} /> : <EmptyOrders />}
46
+ </div>
47
+ );
48
+ }
49
+
50
+ function OrdersTable({orders}: Pick<CustomerOrdersFragment, 'orders'>) {
51
+ return (
52
+ <div className="acccount-orders">
53
+ {orders?.nodes.length ? (
54
+ <Pagination connection={orders}>
55
+ {({nodes, isLoading, PreviousLink, NextLink}) => {
56
+ return (
57
+ <>
58
+ <PreviousLink>
59
+ {isLoading ? 'Loading...' : <span>↑ Load previous</span>}
60
+ </PreviousLink>
61
+ {nodes.map((order) => {
62
+ return <OrderItem key={order.id} order={order} />;
63
+ })}
64
+ <NextLink>
65
+ {isLoading ? 'Loading...' : <span>Load more ↓</span>}
66
+ </NextLink>
67
+ </>
68
+ );
69
+ }}
70
+ </Pagination>
71
+ ) : (
72
+ <EmptyOrders />
73
+ )}
74
+ </div>
75
+ );
76
+ }
77
+
78
+ function EmptyOrders() {
79
+ return (
80
+ <div>
81
+ <p>You haven&apos;t placed any orders yet.</p>
82
+ <br />
83
+ <p>
84
+ <Link to="/collections">Start Shopping →</Link>
85
+ </p>
86
+ </div>
87
+ );
88
+ }
89
+
90
+ function OrderItem({order}: {order: OrderItemFragment}) {
91
+ const fulfillmentStatus = flattenConnection(order.fulfillments)[0]?.status;
92
+ return (
93
+ <>
94
+ <fieldset>
95
+ <Link to={`/account/orders/${btoa(order.id)}`}>
96
+ <strong>#{order.number}</strong>
97
+ </Link>
98
+ <p>{new Date(order.processedAt).toDateString()}</p>
99
+ <p>{order.financialStatus}</p>
100
+ {fulfillmentStatus && <p>{fulfillmentStatus}</p>}
101
+ <Money data={order.totalPrice} />
102
+ <Link to={`/account/orders/${btoa(order.id)}`}>View Order →</Link>
103
+ </fieldset>
104
+ <br />
105
+ </>
106
+ );
107
+ }
@@ -0,0 +1,136 @@
1
+ import type {CustomerFragment} from 'customer-accountapi.generated';
2
+ import type {CustomerUpdateInput} from '@shopify/hydrogen/customer-account-api-types';
3
+ import {CUSTOMER_UPDATE_MUTATION} from '~/graphql/customer-account/CustomerUpdateMutation';
4
+ import {
5
+ json,
6
+ type ActionFunctionArgs,
7
+ type LoaderFunctionArgs,
8
+ } from '@shopify/remix-oxygen';
9
+ import {
10
+ Form,
11
+ useActionData,
12
+ useNavigation,
13
+ useOutletContext,
14
+ type MetaFunction,
15
+ } from '@remix-run/react';
16
+
17
+ export type ActionResponse = {
18
+ error: string | null;
19
+ customer: CustomerFragment | null;
20
+ };
21
+
22
+ export const meta: MetaFunction = () => {
23
+ return [{title: 'Profile'}];
24
+ };
25
+
26
+ export async function loader({context}: LoaderFunctionArgs) {
27
+ await context.customerAccount.handleAuthStatus();
28
+
29
+ return json({});
30
+ }
31
+
32
+ export async function action({request, context}: ActionFunctionArgs) {
33
+ const {customerAccount} = context;
34
+
35
+ if (request.method !== 'PUT') {
36
+ return json({error: 'Method not allowed'}, {status: 405});
37
+ }
38
+
39
+ const form = await request.formData();
40
+
41
+ try {
42
+ const customer: CustomerUpdateInput = {};
43
+ const validInputKeys = ['firstName', 'lastName'] as const;
44
+ for (const [key, value] of form.entries()) {
45
+ if (!validInputKeys.includes(key as any)) {
46
+ continue;
47
+ }
48
+ if (typeof value === 'string' && value.length) {
49
+ customer[key as (typeof validInputKeys)[number]] = value;
50
+ }
51
+ }
52
+
53
+ // update customer and possibly password
54
+ const {data, errors} = await customerAccount.mutate(
55
+ CUSTOMER_UPDATE_MUTATION,
56
+ {
57
+ variables: {
58
+ customer,
59
+ },
60
+ },
61
+ );
62
+
63
+ if (errors?.length) {
64
+ throw new Error(errors[0].message);
65
+ }
66
+
67
+ if (!data?.customerUpdate?.customer) {
68
+ throw new Error('Customer profile update failed.');
69
+ }
70
+
71
+ return json({
72
+ error: null,
73
+ customer: data?.customerUpdate?.customer,
74
+ });
75
+ } catch (error: any) {
76
+ return json(
77
+ {error: error.message, customer: null},
78
+ {
79
+ status: 400,
80
+ },
81
+ );
82
+ }
83
+ }
84
+
85
+ export default function AccountProfile() {
86
+ const account = useOutletContext<{customer: CustomerFragment}>();
87
+ const {state} = useNavigation();
88
+ const action = useActionData<ActionResponse>();
89
+ const customer = action?.customer ?? account?.customer;
90
+
91
+ return (
92
+ <div className="account-profile">
93
+ <h2>My profile</h2>
94
+ <br />
95
+ <Form method="PUT">
96
+ <legend>Personal information</legend>
97
+ <fieldset>
98
+ <label htmlFor="firstName">First name</label>
99
+ <input
100
+ id="firstName"
101
+ name="firstName"
102
+ type="text"
103
+ autoComplete="given-name"
104
+ placeholder="First name"
105
+ aria-label="First name"
106
+ defaultValue={customer.firstName ?? ''}
107
+ minLength={2}
108
+ />
109
+ <label htmlFor="lastName">Last name</label>
110
+ <input
111
+ id="lastName"
112
+ name="lastName"
113
+ type="text"
114
+ autoComplete="family-name"
115
+ placeholder="Last name"
116
+ aria-label="Last name"
117
+ defaultValue={customer.lastName ?? ''}
118
+ minLength={2}
119
+ />
120
+ </fieldset>
121
+ {action?.error ? (
122
+ <p>
123
+ <mark>
124
+ <small>{action.error}</small>
125
+ </mark>
126
+ </p>
127
+ ) : (
128
+ <br />
129
+ )}
130
+ <button type="submit" disabled={state !== 'idle'}>
131
+ {state !== 'idle' ? 'Updating' : 'Update'}
132
+ </button>
133
+ </Form>
134
+ </div>
135
+ );
136
+ }
@@ -0,0 +1,88 @@
1
+ import {json, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
2
+ import {Form, NavLink, Outlet, useLoaderData} from '@remix-run/react';
3
+ import {CUSTOMER_DETAILS_QUERY} from '~/graphql/customer-account/CustomerDetailsQuery';
4
+
5
+ export function shouldRevalidate() {
6
+ return true;
7
+ }
8
+
9
+ export async function loader({context}: LoaderFunctionArgs) {
10
+ const {data, errors} = await context.customerAccount.query(
11
+ CUSTOMER_DETAILS_QUERY,
12
+ );
13
+
14
+ if (errors?.length || !data?.customer) {
15
+ throw new Error('Customer not found');
16
+ }
17
+
18
+ return json(
19
+ {customer: data.customer},
20
+ {
21
+ headers: {
22
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
23
+ },
24
+ },
25
+ );
26
+ }
27
+
28
+ export default function AccountLayout() {
29
+ const {customer} = useLoaderData<typeof loader>();
30
+
31
+ const heading = customer
32
+ ? customer.firstName
33
+ ? `Welcome, ${customer.firstName}`
34
+ : `Welcome to your account.`
35
+ : 'Account Details';
36
+
37
+ return (
38
+ <div className="account">
39
+ <h1>{heading}</h1>
40
+ <br />
41
+ <AccountMenu />
42
+ <br />
43
+ <br />
44
+ <Outlet context={{customer}} />
45
+ </div>
46
+ );
47
+ }
48
+
49
+ function AccountMenu() {
50
+ function isActiveStyle({
51
+ isActive,
52
+ isPending,
53
+ }: {
54
+ isActive: boolean;
55
+ isPending: boolean;
56
+ }) {
57
+ return {
58
+ fontWeight: isActive ? 'bold' : undefined,
59
+ color: isPending ? 'grey' : 'black',
60
+ };
61
+ }
62
+
63
+ return (
64
+ <nav role="navigation">
65
+ <NavLink to="/account/orders" style={isActiveStyle}>
66
+ Orders &nbsp;
67
+ </NavLink>
68
+ &nbsp;|&nbsp;
69
+ <NavLink to="/account/profile" style={isActiveStyle}>
70
+ &nbsp; Profile &nbsp;
71
+ </NavLink>
72
+ &nbsp;|&nbsp;
73
+ <NavLink to="/account/addresses" style={isActiveStyle}>
74
+ &nbsp; Addresses &nbsp;
75
+ </NavLink>
76
+ &nbsp;|&nbsp;
77
+ <Logout />
78
+ </nav>
79
+ );
80
+ }
81
+
82
+ function Logout() {
83
+ return (
84
+ <Form className="account-logout" method="POST" action="/account/logout">
85
+ &nbsp;<button type="submit">Sign out</button>
86
+ </Form>
87
+ );
88
+ }
@@ -0,0 +1,5 @@
1
+ import type {LoaderFunctionArgs} from '@shopify/remix-oxygen';
2
+
3
+ export async function loader({context}: LoaderFunctionArgs) {
4
+ return context.customerAccount.authorize();
5
+ }
@@ -0,0 +1,5 @@
1
+ import type {LoaderFunctionArgs} from '@shopify/remix-oxygen';
2
+
3
+ export async function loader({request, context}: LoaderFunctionArgs) {
4
+ return context.customerAccount.login();
5
+ }
@@ -0,0 +1,10 @@
1
+ import {redirect, type ActionFunctionArgs} from '@shopify/remix-oxygen';
2
+
3
+ // if we dont implement this, /account/logout will get caught by account.$.tsx to do login
4
+ export async function loader() {
5
+ return redirect('/');
6
+ }
7
+
8
+ export async function action({context}: ActionFunctionArgs) {
9
+ return context.customerAccount.logout();
10
+ }
@@ -0,0 +1,318 @@
1
+ import {json, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
2
+ import type {
3
+ NormalizedPredictiveSearch,
4
+ NormalizedPredictiveSearchResults,
5
+ } from '~/components/Search';
6
+ import {NO_PREDICTIVE_SEARCH_RESULTS} from '~/components/Search';
7
+ import {applyTrackingParams} from '~/lib/search';
8
+
9
+ import type {
10
+ PredictiveArticleFragment,
11
+ PredictiveCollectionFragment,
12
+ PredictivePageFragment,
13
+ PredictiveProductFragment,
14
+ PredictiveQueryFragment,
15
+ PredictiveSearchQuery,
16
+ } from 'storefrontapi.generated';
17
+
18
+ type PredictiveSearchTypes =
19
+ | 'ARTICLE'
20
+ | 'COLLECTION'
21
+ | 'PAGE'
22
+ | 'PRODUCT'
23
+ | 'QUERY';
24
+
25
+ const DEFAULT_SEARCH_TYPES: PredictiveSearchTypes[] = [
26
+ 'ARTICLE',
27
+ 'COLLECTION',
28
+ 'PAGE',
29
+ 'PRODUCT',
30
+ 'QUERY',
31
+ ];
32
+
33
+ export type PredictiveSearchAPILoader = typeof loader;
34
+
35
+ /**
36
+ * Fetches the search results from the predictive search API
37
+ * requested by the SearchForm component
38
+ */
39
+ export async function loader({request, params, context}: LoaderFunctionArgs) {
40
+ const search = await fetchPredictiveSearchResults({
41
+ params,
42
+ request,
43
+ context,
44
+ });
45
+
46
+ return json(search, {
47
+ headers: {'Cache-Control': `max-age=${search.searchTerm ? 60 : 3600}`},
48
+ });
49
+ }
50
+
51
+ async function fetchPredictiveSearchResults({
52
+ params,
53
+ request,
54
+ context,
55
+ }: Pick<LoaderFunctionArgs, 'params' | 'context' | 'request'>) {
56
+ const url = new URL(request.url);
57
+ const searchParams = new URLSearchParams(url.search);
58
+ const searchTerm = searchParams.get('q') || '';
59
+ const limit = Number(searchParams.get('limit') || 10);
60
+ const rawTypes = String(searchParams.get('type') || 'ANY');
61
+
62
+ const searchTypes =
63
+ rawTypes === 'ANY'
64
+ ? DEFAULT_SEARCH_TYPES
65
+ : rawTypes
66
+ .split(',')
67
+ .map((t) => t.toUpperCase() as PredictiveSearchTypes)
68
+ .filter((t) => DEFAULT_SEARCH_TYPES.includes(t));
69
+
70
+ if (!searchTerm) {
71
+ return {
72
+ searchResults: {results: null, totalResults: 0},
73
+ searchTerm,
74
+ searchTypes,
75
+ };
76
+ }
77
+
78
+ const data = await context.storefront.query(PREDICTIVE_SEARCH_QUERY, {
79
+ variables: {
80
+ limit,
81
+ limitScope: 'EACH',
82
+ searchTerm,
83
+ types: searchTypes,
84
+ },
85
+ });
86
+
87
+ if (!data) {
88
+ throw new Error('No data returned from Shopify API');
89
+ }
90
+
91
+ const searchResults = normalizePredictiveSearchResults(
92
+ data.predictiveSearch,
93
+ params.locale,
94
+ );
95
+
96
+ return {searchResults, searchTerm, searchTypes};
97
+ }
98
+
99
+ /**
100
+ * Normalize results and apply tracking qurery parameters to each result url
101
+ */
102
+ export function normalizePredictiveSearchResults(
103
+ predictiveSearch: PredictiveSearchQuery['predictiveSearch'],
104
+ locale: LoaderFunctionArgs['params']['locale'],
105
+ ): NormalizedPredictiveSearch {
106
+ let totalResults = 0;
107
+ if (!predictiveSearch) {
108
+ return {
109
+ results: NO_PREDICTIVE_SEARCH_RESULTS,
110
+ totalResults,
111
+ };
112
+ }
113
+
114
+ const localePrefix = locale ? `/${locale}` : '';
115
+ const results: NormalizedPredictiveSearchResults = [];
116
+
117
+ if (predictiveSearch.queries.length) {
118
+ results.push({
119
+ type: 'queries',
120
+ items: predictiveSearch.queries.map((query: PredictiveQueryFragment) => {
121
+ const trackingParams = applyTrackingParams(
122
+ query,
123
+ `q=${encodeURIComponent(query.text)}`,
124
+ );
125
+
126
+ totalResults++;
127
+ return {
128
+ __typename: query.__typename,
129
+ handle: '',
130
+ id: query.text,
131
+ image: undefined,
132
+ title: query.text,
133
+ styledTitle: query.styledText,
134
+ url: `${localePrefix}/search${trackingParams}`,
135
+ };
136
+ }),
137
+ });
138
+ }
139
+
140
+ if (predictiveSearch.products.length) {
141
+ results.push({
142
+ type: 'products',
143
+ items: predictiveSearch.products.map(
144
+ (product: PredictiveProductFragment) => {
145
+ totalResults++;
146
+ const trackingParams = applyTrackingParams(product);
147
+ return {
148
+ __typename: product.__typename,
149
+ handle: product.handle,
150
+ id: product.id,
151
+ image: product.variants?.nodes?.[0]?.image,
152
+ title: product.title,
153
+ url: `${localePrefix}/products/${product.handle}${trackingParams}`,
154
+ price: product.variants.nodes[0].price,
155
+ };
156
+ },
157
+ ),
158
+ });
159
+ }
160
+
161
+ if (predictiveSearch.collections.length) {
162
+ results.push({
163
+ type: 'collections',
164
+ items: predictiveSearch.collections.map(
165
+ (collection: PredictiveCollectionFragment) => {
166
+ totalResults++;
167
+ const trackingParams = applyTrackingParams(collection);
168
+ return {
169
+ __typename: collection.__typename,
170
+ handle: collection.handle,
171
+ id: collection.id,
172
+ image: collection.image,
173
+ title: collection.title,
174
+ url: `${localePrefix}/collections/${collection.handle}${trackingParams}`,
175
+ };
176
+ },
177
+ ),
178
+ });
179
+ }
180
+
181
+ if (predictiveSearch.pages.length) {
182
+ results.push({
183
+ type: 'pages',
184
+ items: predictiveSearch.pages.map((page: PredictivePageFragment) => {
185
+ totalResults++;
186
+ const trackingParams = applyTrackingParams(page);
187
+ return {
188
+ __typename: page.__typename,
189
+ handle: page.handle,
190
+ id: page.id,
191
+ image: undefined,
192
+ title: page.title,
193
+ url: `${localePrefix}/pages/${page.handle}${trackingParams}`,
194
+ };
195
+ }),
196
+ });
197
+ }
198
+
199
+ if (predictiveSearch.articles.length) {
200
+ results.push({
201
+ type: 'articles',
202
+ items: predictiveSearch.articles.map(
203
+ (article: PredictiveArticleFragment) => {
204
+ totalResults++;
205
+ const trackingParams = applyTrackingParams(article);
206
+ return {
207
+ __typename: article.__typename,
208
+ handle: article.handle,
209
+ id: article.id,
210
+ image: article.image,
211
+ title: article.title,
212
+ url: `${localePrefix}/blogs/${article.blog.handle}/${article.handle}/${trackingParams}`,
213
+ };
214
+ },
215
+ ),
216
+ });
217
+ }
218
+
219
+ return {results, totalResults};
220
+ }
221
+
222
+ const PREDICTIVE_SEARCH_QUERY = `#graphql
223
+ fragment PredictiveArticle on Article {
224
+ __typename
225
+ id
226
+ title
227
+ handle
228
+ blog {
229
+ handle
230
+ }
231
+ image {
232
+ url
233
+ altText
234
+ width
235
+ height
236
+ }
237
+ trackingParameters
238
+ }
239
+ fragment PredictiveCollection on Collection {
240
+ __typename
241
+ id
242
+ title
243
+ handle
244
+ image {
245
+ url
246
+ altText
247
+ width
248
+ height
249
+ }
250
+ trackingParameters
251
+ }
252
+ fragment PredictivePage on Page {
253
+ __typename
254
+ id
255
+ title
256
+ handle
257
+ trackingParameters
258
+ }
259
+ fragment PredictiveProduct on Product {
260
+ __typename
261
+ id
262
+ title
263
+ handle
264
+ trackingParameters
265
+ variants(first: 1) {
266
+ nodes {
267
+ id
268
+ image {
269
+ url
270
+ altText
271
+ width
272
+ height
273
+ }
274
+ price {
275
+ amount
276
+ currencyCode
277
+ }
278
+ }
279
+ }
280
+ }
281
+ fragment PredictiveQuery on SearchQuerySuggestion {
282
+ __typename
283
+ text
284
+ styledText
285
+ trackingParameters
286
+ }
287
+ query predictiveSearch(
288
+ $country: CountryCode
289
+ $language: LanguageCode
290
+ $limit: Int!
291
+ $limitScope: PredictiveSearchLimitScope!
292
+ $searchTerm: String!
293
+ $types: [PredictiveSearchType!]
294
+ ) @inContext(country: $country, language: $language) {
295
+ predictiveSearch(
296
+ limit: $limit,
297
+ limitScope: $limitScope,
298
+ query: $searchTerm,
299
+ types: $types,
300
+ ) {
301
+ articles {
302
+ ...PredictiveArticle
303
+ }
304
+ collections {
305
+ ...PredictiveCollection
306
+ }
307
+ pages {
308
+ ...PredictivePage
309
+ }
310
+ products {
311
+ ...PredictiveProduct
312
+ }
313
+ queries {
314
+ ...PredictiveQuery
315
+ }
316
+ }
317
+ }
318
+ ` as const;