@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.
- package/dist/assets/hydrogen/bundle/analyzer.html +2045 -0
- package/dist/assets/hydrogen/i18n/domains.ts +28 -0
- package/dist/assets/hydrogen/i18n/mock-i18n-types.ts +3 -0
- package/dist/assets/hydrogen/i18n/subdomains.ts +27 -0
- package/dist/assets/hydrogen/i18n/subfolders.ts +29 -0
- package/dist/assets/hydrogen/routes/locale-check.ts +16 -0
- package/dist/assets/hydrogen/starter/.eslintignore +5 -0
- package/dist/assets/hydrogen/starter/.eslintrc.cjs +19 -0
- package/dist/assets/hydrogen/starter/.graphqlrc.yml +12 -0
- package/dist/assets/hydrogen/starter/CHANGELOG.md +709 -0
- package/dist/assets/hydrogen/starter/README.md +45 -0
- package/dist/assets/hydrogen/starter/app/assets/favicon.svg +28 -0
- package/dist/assets/hydrogen/starter/app/components/AddToCartButton.tsx +37 -0
- package/dist/assets/hydrogen/starter/app/components/Aside.tsx +76 -0
- package/dist/assets/hydrogen/starter/app/components/CartLineItem.tsx +150 -0
- package/dist/assets/hydrogen/starter/app/components/CartMain.tsx +68 -0
- package/dist/assets/hydrogen/starter/app/components/CartSummary.tsx +101 -0
- package/dist/assets/hydrogen/starter/app/components/Footer.tsx +129 -0
- package/dist/assets/hydrogen/starter/app/components/Header.tsx +230 -0
- package/dist/assets/hydrogen/starter/app/components/PageLayout.tsx +126 -0
- package/dist/assets/hydrogen/starter/app/components/ProductForm.tsx +80 -0
- package/dist/assets/hydrogen/starter/app/components/ProductImage.tsx +23 -0
- package/dist/assets/hydrogen/starter/app/components/ProductPrice.tsx +27 -0
- package/dist/assets/hydrogen/starter/app/components/Search.tsx +514 -0
- package/dist/assets/hydrogen/starter/app/entry.client.tsx +12 -0
- package/dist/assets/hydrogen/starter/app/entry.server.tsx +47 -0
- package/dist/assets/hydrogen/starter/app/graphql/customer-account/CustomerAddressMutations.ts +61 -0
- package/dist/assets/hydrogen/starter/app/graphql/customer-account/CustomerDetailsQuery.ts +40 -0
- package/dist/assets/hydrogen/starter/app/graphql/customer-account/CustomerOrderQuery.ts +87 -0
- package/dist/assets/hydrogen/starter/app/graphql/customer-account/CustomerOrdersQuery.ts +58 -0
- package/dist/assets/hydrogen/starter/app/graphql/customer-account/CustomerUpdateMutation.ts +24 -0
- package/dist/assets/hydrogen/starter/app/lib/fragments.ts +174 -0
- package/dist/assets/hydrogen/starter/app/lib/search.ts +29 -0
- package/dist/assets/hydrogen/starter/app/lib/session.ts +72 -0
- package/dist/assets/hydrogen/starter/app/lib/variants.ts +46 -0
- package/dist/assets/hydrogen/starter/app/root.tsx +191 -0
- package/dist/assets/hydrogen/starter/app/routes/$.tsx +11 -0
- package/dist/assets/hydrogen/starter/app/routes/[robots.txt].tsx +118 -0
- package/dist/assets/hydrogen/starter/app/routes/[sitemap.xml].tsx +177 -0
- package/dist/assets/hydrogen/starter/app/routes/_index.tsx +182 -0
- package/dist/assets/hydrogen/starter/app/routes/account.$.tsx +8 -0
- package/dist/assets/hydrogen/starter/app/routes/account._index.tsx +5 -0
- package/dist/assets/hydrogen/starter/app/routes/account.addresses.tsx +513 -0
- package/dist/assets/hydrogen/starter/app/routes/account.orders.$id.tsx +195 -0
- package/dist/assets/hydrogen/starter/app/routes/account.orders._index.tsx +107 -0
- package/dist/assets/hydrogen/starter/app/routes/account.profile.tsx +136 -0
- package/dist/assets/hydrogen/starter/app/routes/account.tsx +88 -0
- package/dist/assets/hydrogen/starter/app/routes/account_.authorize.tsx +5 -0
- package/dist/assets/hydrogen/starter/app/routes/account_.login.tsx +5 -0
- package/dist/assets/hydrogen/starter/app/routes/account_.logout.tsx +10 -0
- package/dist/assets/hydrogen/starter/app/routes/api.predictive-search.tsx +318 -0
- package/dist/assets/hydrogen/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +113 -0
- package/dist/assets/hydrogen/starter/app/routes/blogs.$blogHandle._index.tsx +188 -0
- package/dist/assets/hydrogen/starter/app/routes/blogs._index.tsx +119 -0
- package/dist/assets/hydrogen/starter/app/routes/cart.$lines.tsx +69 -0
- package/dist/assets/hydrogen/starter/app/routes/cart.tsx +102 -0
- package/dist/assets/hydrogen/starter/app/routes/collections.$handle.tsx +225 -0
- package/dist/assets/hydrogen/starter/app/routes/collections._index.tsx +146 -0
- package/dist/assets/hydrogen/starter/app/routes/collections.all.tsx +185 -0
- package/dist/assets/hydrogen/starter/app/routes/discount.$code.tsx +47 -0
- package/dist/assets/hydrogen/starter/app/routes/pages.$handle.tsx +84 -0
- package/dist/assets/hydrogen/starter/app/routes/policies.$handle.tsx +93 -0
- package/dist/assets/hydrogen/starter/app/routes/policies._index.tsx +63 -0
- package/dist/assets/hydrogen/starter/app/routes/products.$handle.tsx +299 -0
- package/dist/assets/hydrogen/starter/app/routes/search.tsx +177 -0
- package/dist/assets/hydrogen/starter/app/styles/app.css +486 -0
- package/dist/assets/hydrogen/starter/app/styles/reset.css +129 -0
- package/dist/assets/hydrogen/starter/customer-accountapi.generated.d.ts +509 -0
- package/dist/assets/hydrogen/starter/env.d.ts +54 -0
- package/dist/assets/hydrogen/starter/package.json +50 -0
- package/dist/assets/hydrogen/starter/public/.gitkeep +0 -0
- package/dist/assets/hydrogen/starter/server.ts +119 -0
- package/dist/assets/hydrogen/starter/storefrontapi.generated.d.ts +1211 -0
- package/dist/assets/hydrogen/starter/tsconfig.json +23 -0
- package/dist/assets/hydrogen/starter/vite.config.ts +41 -0
- package/dist/assets/hydrogen/tailwind/package.json +8 -0
- package/dist/assets/hydrogen/tailwind/tailwind.css +6 -0
- package/dist/assets/hydrogen/vanilla-extract/package.json +8 -0
- package/dist/assets/hydrogen/virtual-routes/assets/debug-network.css +592 -0
- package/dist/assets/hydrogen/virtual-routes/assets/favicon-dark.svg +20 -0
- package/dist/assets/hydrogen/virtual-routes/assets/favicon.svg +28 -0
- package/dist/assets/hydrogen/virtual-routes/assets/inter-variable-font.woff2 +0 -0
- package/dist/assets/hydrogen/virtual-routes/assets/jetbrainsmono-variable-font.woff2 +0 -0
- package/dist/assets/hydrogen/virtual-routes/assets/styles.css +238 -0
- package/dist/assets/hydrogen/virtual-routes/components/FlameChartWrapper.jsx +123 -0
- package/dist/assets/hydrogen/virtual-routes/components/HydrogenLogoBaseBW.jsx +32 -0
- package/dist/assets/hydrogen/virtual-routes/components/HydrogenLogoBaseColor.jsx +47 -0
- package/dist/assets/hydrogen/virtual-routes/components/IconBanner.jsx +292 -0
- package/dist/assets/hydrogen/virtual-routes/components/IconClose.jsx +38 -0
- package/dist/assets/hydrogen/virtual-routes/components/IconDiscard.jsx +44 -0
- package/dist/assets/hydrogen/virtual-routes/components/IconError.jsx +61 -0
- package/dist/assets/hydrogen/virtual-routes/components/IconGithub.jsx +23 -0
- package/dist/assets/hydrogen/virtual-routes/components/IconTwitter.jsx +21 -0
- package/dist/assets/hydrogen/virtual-routes/components/PageLayout.jsx +7 -0
- package/dist/assets/hydrogen/virtual-routes/components/RequestDetails.jsx +178 -0
- package/dist/assets/hydrogen/virtual-routes/components/RequestTable.jsx +91 -0
- package/dist/assets/hydrogen/virtual-routes/components/RequestWaterfall.jsx +151 -0
- package/dist/assets/hydrogen/virtual-routes/lib/useDebugNetworkServer.jsx +178 -0
- package/dist/assets/hydrogen/virtual-routes/routes/graphiql.jsx +5 -0
- package/dist/assets/hydrogen/virtual-routes/routes/index.jsx +265 -0
- package/dist/assets/hydrogen/virtual-routes/routes/subrequest-profiler.jsx +243 -0
- package/dist/assets/hydrogen/virtual-routes/virtual-root.jsx +64 -0
- package/dist/assets/hydrogen/vite/package.json +14 -0
- package/dist/assets/hydrogen/vite/vite.config.js +41 -0
- package/dist/chokidar-2CKIHN27.js +12 -0
- package/dist/chunk-EO6F7WJJ.js +2 -0
- package/dist/chunk-FB327AH7.js +5 -0
- package/dist/chunk-FJPX4XUR.js +2 -0
- package/dist/chunk-JKOXGRAA.js +10 -0
- package/dist/chunk-LNQWGFTB.js +45 -0
- package/dist/chunk-M6JXYI3V.js +23 -0
- package/dist/chunk-MNT4XW23.js +2 -0
- package/dist/chunk-N7HFZHSO.js +1145 -0
- package/dist/chunk-PMDMUCNY.js +2 -0
- package/dist/chunk-QGLB6FFL.js +3 -0
- package/dist/chunk-VMIOG46Y.js +2 -0
- package/dist/create-app.js +1867 -34
- package/dist/del-CZGKV5SQ.js +11 -0
- package/dist/devtools-ZCRGQE64.js +8 -0
- package/dist/error-handler-GEQXZJ25.js +2 -0
- package/dist/lib-NJYCLW6W.js +22 -0
- package/dist/morph-ZJCCGFNC.js +30499 -0
- package/dist/multipart-parser-6HGDQWV7.js +3 -0
- package/dist/open-OD6DRFEG.js +2 -0
- package/dist/out-7KAQXZLP.js +2 -0
- package/dist/yoga.wasm +0 -0
- 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'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
|
|
67
|
+
</NavLink>
|
|
68
|
+
|
|
|
69
|
+
<NavLink to="/account/profile" style={isActiveStyle}>
|
|
70
|
+
Profile
|
|
71
|
+
</NavLink>
|
|
72
|
+
|
|
|
73
|
+
<NavLink to="/account/addresses" style={isActiveStyle}>
|
|
74
|
+
Addresses
|
|
75
|
+
</NavLink>
|
|
76
|
+
|
|
|
77
|
+
<Logout />
|
|
78
|
+
</nav>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function Logout() {
|
|
83
|
+
return (
|
|
84
|
+
<Form className="account-logout" method="POST" action="/account/logout">
|
|
85
|
+
<button type="submit">Sign out</button>
|
|
86
|
+
</Form>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -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;
|