@shopify/cli-hydrogen 12.0.0 → 12.0.1
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/README.md +1 -1
- package/dist/assets/hydrogen/routes/locale-check.ts +3 -3
- package/dist/assets/hydrogen/starter/.graphqlrc.ts +4 -2
- package/dist/assets/hydrogen/starter/CHANGELOG.md +271 -0
- package/dist/assets/hydrogen/starter/app/components/Aside.tsx +4 -2
- package/dist/assets/hydrogen/starter/app/components/CartLineItem.tsx +67 -38
- package/dist/assets/hydrogen/starter/app/components/CartMain.tsx +49 -8
- package/dist/assets/hydrogen/starter/app/components/CartSummary.tsx +133 -57
- package/dist/assets/hydrogen/starter/app/components/Header.tsx +3 -3
- package/dist/assets/hydrogen/starter/app/components/MockShopNotice.tsx +20 -0
- package/dist/assets/hydrogen/starter/app/components/PaginatedResourceSection.tsx +24 -4
- package/dist/assets/hydrogen/starter/app/components/ProductPrice.tsx +1 -1
- package/dist/assets/hydrogen/starter/app/entry.client.tsx +2 -3
- package/dist/assets/hydrogen/starter/app/lib/context.ts +5 -0
- package/dist/assets/hydrogen/starter/app/lib/fragments.ts +8 -0
- package/dist/assets/hydrogen/starter/app/routes/[robots.txt].tsx +14 -53
- package/dist/assets/hydrogen/starter/app/routes/_index.tsx +15 -9
- package/dist/assets/hydrogen/starter/app/routes/account.$.tsx +1 -1
- package/dist/assets/hydrogen/starter/app/routes/account.addresses.tsx +14 -14
- package/dist/assets/hydrogen/starter/app/routes/account.profile.tsx +1 -1
- package/dist/assets/hydrogen/starter/app/routes/account.tsx +4 -7
- package/dist/assets/hydrogen/starter/app/routes/account_.login.tsx +10 -0
- package/dist/assets/hydrogen/starter/app/routes/blogs.$blogHandle._index.tsx +2 -9
- package/dist/assets/hydrogen/starter/app/routes/blogs._index.tsx +1 -4
- package/dist/assets/hydrogen/starter/app/routes/cart.tsx +3 -11
- package/dist/assets/hydrogen/starter/app/routes/collections.all.tsx +1 -3
- package/dist/assets/hydrogen/starter/app/routes/pages.$handle.tsx +2 -8
- package/dist/assets/hydrogen/starter/app/routes/policies.$handle.tsx +1 -4
- package/dist/assets/hydrogen/starter/app/routes/policies._index.tsx +1 -1
- package/dist/assets/hydrogen/starter/app/routes/products.$handle.tsx +2 -9
- package/dist/assets/hydrogen/starter/app/routes/search.tsx +18 -12
- package/dist/assets/hydrogen/starter/app/styles/app.css +72 -1
- package/dist/assets/hydrogen/starter/eslint.config.js +1 -0
- package/dist/assets/hydrogen/starter/guides/predictiveSearch/predictiveSearch.md +3 -3
- package/dist/assets/hydrogen/starter/guides/search/search.md +1 -1
- package/dist/assets/hydrogen/starter/package.json +16 -16
- package/dist/assets/hydrogen/starter/server.ts +5 -7
- package/dist/assets/hydrogen/starter/storefrontapi.generated.d.ts +89 -11
- package/dist/assets/hydrogen/starter/vite.config.ts +10 -4
- package/dist/assets/hydrogen/vite/package.json +1 -2
- package/dist/assets/hydrogen/vite/vite.config.js +4 -3
- package/dist/commands/hydrogen/deploy.js +19 -13
- package/dist/commands/hydrogen/dev.js +6 -6
- package/dist/commands/hydrogen/generate/route.js +1 -1
- package/dist/commands/hydrogen/init.d.ts +2 -4
- package/dist/commands/hydrogen/init.js +2 -8
- package/dist/commands/hydrogen/preview.js +1 -1
- package/dist/commands/hydrogen/setup/css.js +1 -1
- package/dist/commands/hydrogen/setup/markets.js +1 -1
- package/dist/commands/hydrogen/setup/vite.js +1 -1
- package/dist/commands/hydrogen/setup.js +31 -33
- package/dist/commands/hydrogen/upgrade.js +173 -39
- package/dist/lib/build.js +4 -5
- package/dist/lib/bundle/vite-plugin.js +7 -21
- package/dist/lib/check-lockfile.js +16 -7
- package/dist/lib/codegen.js +2 -2
- package/dist/lib/format-code.js +3 -3
- package/dist/lib/graphql/admin/client.js +48 -23
- package/dist/lib/mini-oxygen/workerd.js +3 -5
- package/dist/lib/onboarding/common.js +21 -26
- package/dist/lib/onboarding/local.js +46 -59
- package/dist/lib/package-managers.js +2 -0
- package/dist/lib/patch-cli.js +198 -0
- package/dist/lib/setups/routes/generate.js +5 -3
- package/dist/lib/template-pack.js +93 -0
- package/dist/lib/transpile/morph/functions.js +1 -1
- package/dist/lib/transpile/morph/typedefs.js +2 -3
- package/dist/lib/upgrade-e2e-utils.js +50 -0
- package/oclif.manifest.json +7 -15
- package/package.json +22 -19
- package/dist/assets/hydrogen/starter/app/routes/api.$version.[graphql.json].tsx +0 -14
- package/dist/lib/react-router-version-check.js +0 -82
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type {CartApiQueryFragment} from 'storefrontapi.generated';
|
|
2
2
|
import type {CartLayout} from '~/components/CartMain';
|
|
3
3
|
import {CartForm, Money, type OptimisticCart} from '@shopify/hydrogen';
|
|
4
|
-
import {useEffect, useRef} from 'react';
|
|
4
|
+
import {useEffect, useId, useRef, useState} from 'react';
|
|
5
5
|
import {useFetcher} from 'react-router';
|
|
6
|
-
import type {FetcherWithComponents} from 'react-router';
|
|
7
6
|
|
|
8
7
|
type CartSummaryProps = {
|
|
9
8
|
cart: OptimisticCart<CartApiQueryFragment | null>;
|
|
@@ -13,11 +12,16 @@ type CartSummaryProps = {
|
|
|
13
12
|
export function CartSummary({cart, layout}: CartSummaryProps) {
|
|
14
13
|
const className =
|
|
15
14
|
layout === 'page' ? 'cart-summary-page' : 'cart-summary-aside';
|
|
15
|
+
const summaryId = useId();
|
|
16
|
+
const discountsHeadingId = useId();
|
|
17
|
+
const discountCodeInputId = useId();
|
|
18
|
+
const giftCardHeadingId = useId();
|
|
19
|
+
const giftCardInputId = useId();
|
|
16
20
|
|
|
17
21
|
return (
|
|
18
|
-
<div aria-labelledby=
|
|
19
|
-
<h4>Totals</h4>
|
|
20
|
-
<dl className="cart-subtotal">
|
|
22
|
+
<div aria-labelledby={summaryId} className={className}>
|
|
23
|
+
<h4 id={summaryId}>Totals</h4>
|
|
24
|
+
<dl role="group" className="cart-subtotal">
|
|
21
25
|
<dt>Subtotal</dt>
|
|
22
26
|
<dd>
|
|
23
27
|
{cart?.cost?.subtotalAmount?.amount ? (
|
|
@@ -27,8 +31,16 @@ export function CartSummary({cart, layout}: CartSummaryProps) {
|
|
|
27
31
|
)}
|
|
28
32
|
</dd>
|
|
29
33
|
</dl>
|
|
30
|
-
<CartDiscounts
|
|
31
|
-
|
|
34
|
+
<CartDiscounts
|
|
35
|
+
discountCodes={cart?.discountCodes}
|
|
36
|
+
discountsHeadingId={discountsHeadingId}
|
|
37
|
+
discountCodeInputId={discountCodeInputId}
|
|
38
|
+
/>
|
|
39
|
+
<CartGiftCard
|
|
40
|
+
giftCardCodes={cart?.appliedGiftCards}
|
|
41
|
+
giftCardHeadingId={giftCardHeadingId}
|
|
42
|
+
giftCardInputId={giftCardInputId}
|
|
43
|
+
/>
|
|
32
44
|
<CartCheckoutActions checkoutUrl={cart?.checkoutUrl} />
|
|
33
45
|
</div>
|
|
34
46
|
);
|
|
@@ -49,8 +61,12 @@ function CartCheckoutActions({checkoutUrl}: {checkoutUrl?: string}) {
|
|
|
49
61
|
|
|
50
62
|
function CartDiscounts({
|
|
51
63
|
discountCodes,
|
|
64
|
+
discountsHeadingId,
|
|
65
|
+
discountCodeInputId,
|
|
52
66
|
}: {
|
|
53
67
|
discountCodes?: CartApiQueryFragment['discountCodes'];
|
|
68
|
+
discountsHeadingId: string;
|
|
69
|
+
discountCodeInputId: string;
|
|
54
70
|
}) {
|
|
55
71
|
const codes: string[] =
|
|
56
72
|
discountCodes
|
|
@@ -58,16 +74,22 @@ function CartDiscounts({
|
|
|
58
74
|
?.map(({code}) => code) || [];
|
|
59
75
|
|
|
60
76
|
return (
|
|
61
|
-
<
|
|
77
|
+
<section aria-label="Discounts">
|
|
62
78
|
{/* Have existing discount, display it with a remove option */}
|
|
63
79
|
<dl hidden={!codes.length}>
|
|
64
80
|
<div>
|
|
65
|
-
<dt>
|
|
81
|
+
<dt id={discountsHeadingId}>Discounts</dt>
|
|
66
82
|
<UpdateDiscountForm>
|
|
67
|
-
<div
|
|
83
|
+
<div
|
|
84
|
+
className="cart-discount"
|
|
85
|
+
role="group"
|
|
86
|
+
aria-labelledby={discountsHeadingId}
|
|
87
|
+
>
|
|
68
88
|
<code>{codes?.join(', ')}</code>
|
|
69
89
|
|
|
70
|
-
<button
|
|
90
|
+
<button type="submit" aria-label="Remove discount">
|
|
91
|
+
Remove
|
|
92
|
+
</button>
|
|
71
93
|
</div>
|
|
72
94
|
</UpdateDiscountForm>
|
|
73
95
|
</div>
|
|
@@ -76,12 +98,22 @@ function CartDiscounts({
|
|
|
76
98
|
{/* Show an input to apply a discount */}
|
|
77
99
|
<UpdateDiscountForm discountCodes={codes}>
|
|
78
100
|
<div>
|
|
79
|
-
<
|
|
101
|
+
<label htmlFor={discountCodeInputId} className="sr-only">
|
|
102
|
+
Discount code
|
|
103
|
+
</label>
|
|
104
|
+
<input
|
|
105
|
+
id={discountCodeInputId}
|
|
106
|
+
type="text"
|
|
107
|
+
name="discountCode"
|
|
108
|
+
placeholder="Discount code"
|
|
109
|
+
/>
|
|
80
110
|
|
|
81
|
-
<button type="submit"
|
|
111
|
+
<button type="submit" aria-label="Apply discount code">
|
|
112
|
+
Apply
|
|
113
|
+
</button>
|
|
82
114
|
</div>
|
|
83
115
|
</UpdateDiscountForm>
|
|
84
|
-
</
|
|
116
|
+
</section>
|
|
85
117
|
);
|
|
86
118
|
}
|
|
87
119
|
|
|
@@ -107,78 +139,117 @@ function UpdateDiscountForm({
|
|
|
107
139
|
|
|
108
140
|
function CartGiftCard({
|
|
109
141
|
giftCardCodes,
|
|
142
|
+
giftCardHeadingId,
|
|
143
|
+
giftCardInputId,
|
|
110
144
|
}: {
|
|
111
145
|
giftCardCodes: CartApiQueryFragment['appliedGiftCards'] | undefined;
|
|
146
|
+
giftCardHeadingId: string;
|
|
147
|
+
giftCardInputId: string;
|
|
112
148
|
}) {
|
|
113
|
-
const appliedGiftCardCodes = useRef<string[]>([]);
|
|
114
149
|
const giftCardCodeInput = useRef<HTMLInputElement>(null);
|
|
150
|
+
const removeButtonRefs = useRef<Map<string, HTMLButtonElement>>(new Map());
|
|
151
|
+
const previousCardIdsRef = useRef<string[]>([]);
|
|
115
152
|
const giftCardAddFetcher = useFetcher({key: 'gift-card-add'});
|
|
153
|
+
const [removedCardIndex, setRemovedCardIndex] = useState<number | null>(null);
|
|
116
154
|
|
|
117
|
-
// Clear the gift card code input after the gift card is added
|
|
118
155
|
useEffect(() => {
|
|
119
156
|
if (giftCardAddFetcher.data) {
|
|
120
|
-
giftCardCodeInput.current
|
|
157
|
+
if (giftCardCodeInput.current !== null) {
|
|
158
|
+
giftCardCodeInput.current.value = '';
|
|
159
|
+
}
|
|
121
160
|
}
|
|
122
161
|
}, [giftCardAddFetcher.data]);
|
|
123
162
|
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
const currentCardIds = giftCardCodes?.map((card) => card.id) || [];
|
|
165
|
+
|
|
166
|
+
if (removedCardIndex !== null && giftCardCodes) {
|
|
167
|
+
const focusTargetIndex = Math.min(
|
|
168
|
+
removedCardIndex,
|
|
169
|
+
giftCardCodes.length - 1,
|
|
170
|
+
);
|
|
171
|
+
const focusTargetCard = giftCardCodes[focusTargetIndex];
|
|
172
|
+
const focusButton = focusTargetCard
|
|
173
|
+
? removeButtonRefs.current.get(focusTargetCard.id)
|
|
174
|
+
: null;
|
|
175
|
+
|
|
176
|
+
if (focusButton) {
|
|
177
|
+
focusButton.focus();
|
|
178
|
+
} else if (giftCardCodeInput.current) {
|
|
179
|
+
giftCardCodeInput.current.focus();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
setRemovedCardIndex(null);
|
|
128
183
|
}
|
|
129
|
-
|
|
184
|
+
|
|
185
|
+
previousCardIdsRef.current = currentCardIds;
|
|
186
|
+
}, [giftCardCodes, removedCardIndex]);
|
|
187
|
+
|
|
188
|
+
const handleRemoveClick = (cardId: string) => {
|
|
189
|
+
const index = previousCardIdsRef.current.indexOf(cardId);
|
|
190
|
+
if (index !== -1) {
|
|
191
|
+
setRemovedCardIndex(index);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
130
194
|
|
|
131
195
|
return (
|
|
132
|
-
<
|
|
133
|
-
{/* Display applied gift cards with individual remove buttons */}
|
|
196
|
+
<section aria-label="Gift cards">
|
|
134
197
|
{giftCardCodes && giftCardCodes.length > 0 && (
|
|
135
198
|
<dl>
|
|
136
|
-
<dt>Applied Gift Card(s)</dt>
|
|
199
|
+
<dt id={giftCardHeadingId}>Applied Gift Card(s)</dt>
|
|
137
200
|
{giftCardCodes.map((giftCard) => (
|
|
138
|
-
<
|
|
139
|
-
<
|
|
201
|
+
<dd key={giftCard.id} className="cart-discount">
|
|
202
|
+
<RemoveGiftCardForm
|
|
203
|
+
giftCardId={giftCard.id}
|
|
204
|
+
lastCharacters={giftCard.lastCharacters}
|
|
205
|
+
onRemoveClick={() => handleRemoveClick(giftCard.id)}
|
|
206
|
+
buttonRef={(el: HTMLButtonElement | null) => {
|
|
207
|
+
if (el) {
|
|
208
|
+
removeButtonRefs.current.set(giftCard.id, el);
|
|
209
|
+
} else {
|
|
210
|
+
removeButtonRefs.current.delete(giftCard.id);
|
|
211
|
+
}
|
|
212
|
+
}}
|
|
213
|
+
>
|
|
140
214
|
<code>***{giftCard.lastCharacters}</code>
|
|
141
215
|
|
|
142
216
|
<Money data={giftCard.amountUsed} />
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
</div>
|
|
146
|
-
</RemoveGiftCardForm>
|
|
217
|
+
</RemoveGiftCardForm>
|
|
218
|
+
</dd>
|
|
147
219
|
))}
|
|
148
220
|
</dl>
|
|
149
221
|
)}
|
|
150
222
|
|
|
151
|
-
|
|
152
|
-
<UpdateGiftCardForm
|
|
153
|
-
giftCardCodes={appliedGiftCardCodes.current}
|
|
154
|
-
saveAppliedCode={saveAppliedCode}
|
|
155
|
-
fetcherKey="gift-card-add"
|
|
156
|
-
>
|
|
223
|
+
<AddGiftCardForm fetcherKey="gift-card-add">
|
|
157
224
|
<div>
|
|
225
|
+
<label htmlFor={giftCardInputId} className="sr-only">
|
|
226
|
+
Gift card code
|
|
227
|
+
</label>
|
|
158
228
|
<input
|
|
229
|
+
id={giftCardInputId}
|
|
159
230
|
type="text"
|
|
160
231
|
name="giftCardCode"
|
|
161
232
|
placeholder="Gift card code"
|
|
162
233
|
ref={giftCardCodeInput}
|
|
163
234
|
/>
|
|
164
235
|
|
|
165
|
-
<button
|
|
236
|
+
<button
|
|
237
|
+
type="submit"
|
|
238
|
+
disabled={giftCardAddFetcher.state !== 'idle'}
|
|
239
|
+
aria-label="Apply gift card code"
|
|
240
|
+
>
|
|
166
241
|
Apply
|
|
167
242
|
</button>
|
|
168
243
|
</div>
|
|
169
|
-
</
|
|
170
|
-
</
|
|
244
|
+
</AddGiftCardForm>
|
|
245
|
+
</section>
|
|
171
246
|
);
|
|
172
247
|
}
|
|
173
248
|
|
|
174
|
-
function
|
|
175
|
-
giftCardCodes,
|
|
176
|
-
saveAppliedCode,
|
|
249
|
+
function AddGiftCardForm({
|
|
177
250
|
fetcherKey,
|
|
178
251
|
children,
|
|
179
252
|
}: {
|
|
180
|
-
giftCardCodes?: string[];
|
|
181
|
-
saveAppliedCode?: (code: string) => void;
|
|
182
253
|
fetcherKey?: string;
|
|
183
254
|
children: React.ReactNode;
|
|
184
255
|
}) {
|
|
@@ -186,28 +257,25 @@ function UpdateGiftCardForm({
|
|
|
186
257
|
<CartForm
|
|
187
258
|
fetcherKey={fetcherKey}
|
|
188
259
|
route="/cart"
|
|
189
|
-
action={CartForm.ACTIONS.
|
|
190
|
-
inputs={{
|
|
191
|
-
giftCardCodes: giftCardCodes || [],
|
|
192
|
-
}}
|
|
260
|
+
action={CartForm.ACTIONS.GiftCardCodesAdd}
|
|
193
261
|
>
|
|
194
|
-
{
|
|
195
|
-
const code = fetcher.formData?.get('giftCardCode');
|
|
196
|
-
if (code && saveAppliedCode) {
|
|
197
|
-
saveAppliedCode(code as string);
|
|
198
|
-
}
|
|
199
|
-
return children;
|
|
200
|
-
}}
|
|
262
|
+
{children}
|
|
201
263
|
</CartForm>
|
|
202
264
|
);
|
|
203
265
|
}
|
|
204
266
|
|
|
205
267
|
function RemoveGiftCardForm({
|
|
206
268
|
giftCardId,
|
|
269
|
+
lastCharacters,
|
|
207
270
|
children,
|
|
271
|
+
onRemoveClick,
|
|
272
|
+
buttonRef,
|
|
208
273
|
}: {
|
|
209
274
|
giftCardId: string;
|
|
275
|
+
lastCharacters: string;
|
|
210
276
|
children: React.ReactNode;
|
|
277
|
+
onRemoveClick?: () => void;
|
|
278
|
+
buttonRef?: (el: HTMLButtonElement | null) => void;
|
|
211
279
|
}) {
|
|
212
280
|
return (
|
|
213
281
|
<CartForm
|
|
@@ -218,7 +286,15 @@ function RemoveGiftCardForm({
|
|
|
218
286
|
}}
|
|
219
287
|
>
|
|
220
288
|
{children}
|
|
289
|
+
|
|
290
|
+
<button
|
|
291
|
+
type="submit"
|
|
292
|
+
aria-label={`Remove gift card ending in ${lastCharacters}`}
|
|
293
|
+
onClick={onRemoveClick}
|
|
294
|
+
ref={buttonRef}
|
|
295
|
+
>
|
|
296
|
+
Remove
|
|
297
|
+
</button>
|
|
221
298
|
</CartForm>
|
|
222
299
|
);
|
|
223
300
|
}
|
|
224
|
-
|
|
@@ -136,7 +136,7 @@ function SearchToggle() {
|
|
|
136
136
|
);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
function CartBadge({count}: {count: number
|
|
139
|
+
function CartBadge({count}: {count: number}) {
|
|
140
140
|
const {open} = useAside();
|
|
141
141
|
const {publish, shop, cart, prevCart} = useAnalytics();
|
|
142
142
|
|
|
@@ -154,14 +154,14 @@ function CartBadge({count}: {count: number | null}) {
|
|
|
154
154
|
} as CartViewPayload);
|
|
155
155
|
}}
|
|
156
156
|
>
|
|
157
|
-
Cart
|
|
157
|
+
Cart <span aria-label={`(items: ${count})`}>{count}</span>
|
|
158
158
|
</a>
|
|
159
159
|
);
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
function CartToggle({cart}: Pick<HeaderProps, 'cart'>) {
|
|
163
163
|
return (
|
|
164
|
-
<Suspense fallback={<CartBadge count={
|
|
164
|
+
<Suspense fallback={<CartBadge count={0} />}>
|
|
165
165
|
<Await resolve={cart}>
|
|
166
166
|
<CartBanner />
|
|
167
167
|
</Await>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function MockShopNotice() {
|
|
2
|
+
return (
|
|
3
|
+
<section
|
|
4
|
+
className="mock-shop-notice"
|
|
5
|
+
aria-labelledby="mock-shop-notice-heading"
|
|
6
|
+
>
|
|
7
|
+
<div className="inner">
|
|
8
|
+
<h2 id="mock-shop-notice-heading">Welcome to Hydrogen!</h2>
|
|
9
|
+
<p>
|
|
10
|
+
You’re seeing mocked products because no store is connected to
|
|
11
|
+
this project yet.
|
|
12
|
+
</p>
|
|
13
|
+
<p>
|
|
14
|
+
Link a store by running <code>npx shopify hydrogen link</code> in your
|
|
15
|
+
terminal.
|
|
16
|
+
</p>
|
|
17
|
+
</div>
|
|
18
|
+
</section>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -2,15 +2,17 @@ import * as React from 'react';
|
|
|
2
2
|
import {Pagination} from '@shopify/hydrogen';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* <PaginatedResourceSection
|
|
5
|
+
* <PaginatedResourceSection> encapsulates the previous and next pagination behaviors throughout your application.
|
|
6
6
|
*/
|
|
7
7
|
export function PaginatedResourceSection<NodesType>({
|
|
8
8
|
connection,
|
|
9
9
|
children,
|
|
10
|
+
ariaLabel,
|
|
10
11
|
resourcesClassName,
|
|
11
12
|
}: {
|
|
12
13
|
connection: React.ComponentProps<typeof Pagination<NodesType>>['connection'];
|
|
13
14
|
children: React.FunctionComponent<{node: NodesType; index: number}>;
|
|
15
|
+
ariaLabel?: string;
|
|
14
16
|
resourcesClassName?: string;
|
|
15
17
|
}) {
|
|
16
18
|
return (
|
|
@@ -23,15 +25,33 @@ export function PaginatedResourceSection<NodesType>({
|
|
|
23
25
|
return (
|
|
24
26
|
<div>
|
|
25
27
|
<PreviousLink>
|
|
26
|
-
{isLoading ?
|
|
28
|
+
{isLoading ? (
|
|
29
|
+
'Loading...'
|
|
30
|
+
) : (
|
|
31
|
+
<span>
|
|
32
|
+
<span aria-hidden="true">↑</span> Load previous
|
|
33
|
+
</span>
|
|
34
|
+
)}
|
|
27
35
|
</PreviousLink>
|
|
28
36
|
{resourcesClassName ? (
|
|
29
|
-
<div
|
|
37
|
+
<div
|
|
38
|
+
aria-label={ariaLabel}
|
|
39
|
+
className={resourcesClassName}
|
|
40
|
+
role={ariaLabel ? 'region' : undefined}
|
|
41
|
+
>
|
|
42
|
+
{resourcesMarkup}
|
|
43
|
+
</div>
|
|
30
44
|
) : (
|
|
31
45
|
resourcesMarkup
|
|
32
46
|
)}
|
|
33
47
|
<NextLink>
|
|
34
|
-
{isLoading ?
|
|
48
|
+
{isLoading ? (
|
|
49
|
+
'Loading...'
|
|
50
|
+
) : (
|
|
51
|
+
<span>
|
|
52
|
+
Load more <span aria-hidden="true">↓</span>
|
|
53
|
+
</span>
|
|
54
|
+
)}
|
|
35
55
|
</NextLink>
|
|
36
56
|
</div>
|
|
37
57
|
);
|
|
@@ -9,7 +9,7 @@ export function ProductPrice({
|
|
|
9
9
|
compareAtPrice?: MoneyV2 | null;
|
|
10
10
|
}) {
|
|
11
11
|
return (
|
|
12
|
-
<div className="product-price">
|
|
12
|
+
<div aria-label="Price" className="product-price" role="group">
|
|
13
13
|
{compareAtPrice ? (
|
|
14
14
|
<div className="product-price-on-sale">
|
|
15
15
|
{price ? <Money data={price} /> : null}
|
|
@@ -6,9 +6,8 @@ import {NonceProvider} from '@shopify/hydrogen';
|
|
|
6
6
|
if (!window.location.origin.includes('webcache.googleusercontent.com')) {
|
|
7
7
|
startTransition(() => {
|
|
8
8
|
// Extract nonce from existing script tags
|
|
9
|
-
const existingNonce =
|
|
10
|
-
.querySelector<HTMLScriptElement>('script[nonce]')
|
|
11
|
-
?.nonce;
|
|
9
|
+
const existingNonce =
|
|
10
|
+
document.querySelector<HTMLScriptElement>('script[nonce]')?.nonce;
|
|
12
11
|
|
|
13
12
|
hydrateRoot(
|
|
14
13
|
document,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {createHydrogenContext} from '@shopify/hydrogen';
|
|
2
2
|
import {AppSession} from '~/lib/session';
|
|
3
3
|
import {CART_QUERY_FRAGMENT} from '~/lib/fragments';
|
|
4
|
+
import type {CartApiQueryFragment} from 'storefrontapi.generated';
|
|
4
5
|
|
|
5
6
|
// Define the additional context object
|
|
6
7
|
const additionalContext = {
|
|
@@ -16,6 +17,10 @@ type AdditionalContextType = typeof additionalContext;
|
|
|
16
17
|
|
|
17
18
|
declare global {
|
|
18
19
|
interface HydrogenAdditionalContext extends AdditionalContextType {}
|
|
20
|
+
|
|
21
|
+
// Augment HydrogenCustomCartFragment with the codegen'd cart fragment type so
|
|
22
|
+
// that context.cart.get() and all cart mutations return the extended cart type.
|
|
23
|
+
interface HydrogenCustomCartFragment extends CartApiQueryFragment {}
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
/**
|
|
@@ -54,6 +54,11 @@ export const CART_QUERY_FRAGMENT = `#graphql
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
parentRelationship {
|
|
58
|
+
parent {
|
|
59
|
+
id
|
|
60
|
+
}
|
|
61
|
+
}
|
|
57
62
|
}
|
|
58
63
|
fragment CartLineComponent on ComponentizableCartLine {
|
|
59
64
|
id
|
|
@@ -104,6 +109,9 @@ export const CART_QUERY_FRAGMENT = `#graphql
|
|
|
104
109
|
}
|
|
105
110
|
}
|
|
106
111
|
}
|
|
112
|
+
lineComponents {
|
|
113
|
+
...CartLine
|
|
114
|
+
}
|
|
107
115
|
}
|
|
108
116
|
fragment CartApiQuery on Cart {
|
|
109
117
|
updatedAt
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import type {Route} from './+types/[robots.txt]';
|
|
2
|
-
import {parseGid} from '@shopify/hydrogen';
|
|
3
2
|
|
|
4
|
-
export
|
|
3
|
+
export function loader({request}: Route.LoaderArgs) {
|
|
5
4
|
const url = new URL(request.url);
|
|
6
|
-
|
|
7
|
-
const {shop} = await context.storefront.query(ROBOTS_QUERY);
|
|
8
|
-
|
|
9
|
-
const shopId = parseGid(shop.id).id;
|
|
10
|
-
const body = robotsTxtData({url: url.origin, shopId});
|
|
5
|
+
const body = robotsTxtData({url: url.origin});
|
|
11
6
|
|
|
12
7
|
return new Response(body, {
|
|
13
8
|
status: 200,
|
|
@@ -19,35 +14,31 @@ export async function loader({request, context}: Route.LoaderArgs) {
|
|
|
19
14
|
});
|
|
20
15
|
}
|
|
21
16
|
|
|
22
|
-
function robotsTxtData({url
|
|
17
|
+
function robotsTxtData({url}: {url?: string}) {
|
|
23
18
|
const sitemapUrl = url ? `${url}/sitemap.xml` : undefined;
|
|
24
19
|
|
|
25
20
|
return `
|
|
26
21
|
User-agent: *
|
|
27
|
-
${generalDisallowRules({sitemapUrl
|
|
22
|
+
${generalDisallowRules({sitemapUrl})}
|
|
28
23
|
|
|
29
24
|
# Google adsbot ignores robots.txt unless specifically named!
|
|
30
25
|
User-agent: adsbot-google
|
|
31
|
-
Disallow: /
|
|
32
|
-
Disallow: /
|
|
33
|
-
Disallow: /
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
${shopId ? `Disallow: /${shopId}/orders` : ''}
|
|
37
|
-
Disallow: /*?*oseid=*
|
|
38
|
-
Disallow: /*preview_theme_id*
|
|
39
|
-
Disallow: /*preview_script_id*
|
|
26
|
+
Disallow: /cart
|
|
27
|
+
Disallow: /account
|
|
28
|
+
Disallow: /search
|
|
29
|
+
Allow: /search/
|
|
30
|
+
Disallow: /search/?*
|
|
40
31
|
|
|
41
32
|
User-agent: Nutch
|
|
42
33
|
Disallow: /
|
|
43
34
|
|
|
44
35
|
User-agent: AhrefsBot
|
|
45
36
|
Crawl-delay: 10
|
|
46
|
-
${generalDisallowRules({sitemapUrl
|
|
37
|
+
${generalDisallowRules({sitemapUrl})}
|
|
47
38
|
|
|
48
39
|
User-agent: AhrefsSiteAudit
|
|
49
40
|
Crawl-delay: 10
|
|
50
|
-
${generalDisallowRules({sitemapUrl
|
|
41
|
+
${generalDisallowRules({sitemapUrl})}
|
|
51
42
|
|
|
52
43
|
User-agent: MJ12bot
|
|
53
44
|
Crawl-Delay: 10
|
|
@@ -61,21 +52,8 @@ Crawl-delay: 1
|
|
|
61
52
|
* This function generates disallow rules that generally follow what Shopify's
|
|
62
53
|
* Online Store has as defaults for their robots.txt
|
|
63
54
|
*/
|
|
64
|
-
function generalDisallowRules({
|
|
65
|
-
|
|
66
|
-
sitemapUrl,
|
|
67
|
-
}: {
|
|
68
|
-
shopId?: string;
|
|
69
|
-
sitemapUrl?: string;
|
|
70
|
-
}) {
|
|
71
|
-
return `Disallow: /admin
|
|
72
|
-
Disallow: /cart
|
|
73
|
-
Disallow: /orders
|
|
74
|
-
Disallow: /checkouts/
|
|
75
|
-
Disallow: /checkout
|
|
76
|
-
${shopId ? `Disallow: /${shopId}/checkouts` : ''}
|
|
77
|
-
${shopId ? `Disallow: /${shopId}/orders` : ''}
|
|
78
|
-
Disallow: /carts
|
|
55
|
+
function generalDisallowRules({sitemapUrl}: {sitemapUrl?: string}) {
|
|
56
|
+
return `Disallow: /cart
|
|
79
57
|
Disallow: /account
|
|
80
58
|
Disallow: /collections/*sort_by*
|
|
81
59
|
Disallow: /*/collections/*sort_by*
|
|
@@ -85,33 +63,16 @@ Disallow: /collections/*%2b*
|
|
|
85
63
|
Disallow: /*/collections/*+*
|
|
86
64
|
Disallow: /*/collections/*%2B*
|
|
87
65
|
Disallow: /*/collections/*%2b*
|
|
88
|
-
Disallow:
|
|
66
|
+
Disallow: /*/collections/*filter*&*filter*
|
|
89
67
|
Disallow: /blogs/*+*
|
|
90
68
|
Disallow: /blogs/*%2B*
|
|
91
69
|
Disallow: /blogs/*%2b*
|
|
92
70
|
Disallow: /*/blogs/*+*
|
|
93
71
|
Disallow: /*/blogs/*%2B*
|
|
94
72
|
Disallow: /*/blogs/*%2b*
|
|
95
|
-
Disallow: /*?*oseid=*
|
|
96
|
-
Disallow: /*preview_theme_id*
|
|
97
|
-
Disallow: /*preview_script_id*
|
|
98
73
|
Disallow: /policies/
|
|
99
|
-
Disallow: /*/*?*ls=*&ls=*
|
|
100
|
-
Disallow: /*/*?*ls%3D*%3Fls%3D*
|
|
101
|
-
Disallow: /*/*?*ls%3d*%3fls%3d*
|
|
102
74
|
Disallow: /search
|
|
103
75
|
Allow: /search/
|
|
104
76
|
Disallow: /search/?*
|
|
105
|
-
Disallow: /apple-app-site-association
|
|
106
|
-
Disallow: /.well-known/shopify/monorail
|
|
107
77
|
${sitemapUrl ? `Sitemap: ${sitemapUrl}` : ''}`;
|
|
108
78
|
}
|
|
109
|
-
|
|
110
|
-
const ROBOTS_QUERY = `#graphql
|
|
111
|
-
query StoreRobots($country: CountryCode, $language: LanguageCode)
|
|
112
|
-
@inContext(country: $country, language: $language) {
|
|
113
|
-
shop {
|
|
114
|
-
id
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
` as const;
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Await,
|
|
3
|
-
useLoaderData,
|
|
4
|
-
Link,
|
|
5
|
-
} from 'react-router';
|
|
1
|
+
import {Await, useLoaderData, Link} from 'react-router';
|
|
6
2
|
import type {Route} from './+types/_index';
|
|
7
3
|
import {Suspense} from 'react';
|
|
8
4
|
import {Image} from '@shopify/hydrogen';
|
|
@@ -11,6 +7,7 @@ import type {
|
|
|
11
7
|
RecommendedProductsQuery,
|
|
12
8
|
} from 'storefrontapi.generated';
|
|
13
9
|
import {ProductItem} from '~/components/ProductItem';
|
|
10
|
+
import {MockShopNotice} from '~/components/MockShopNotice';
|
|
14
11
|
|
|
15
12
|
export const meta: Route.MetaFunction = () => {
|
|
16
13
|
return [{title: 'Hydrogen | Home'}];
|
|
@@ -37,6 +34,7 @@ async function loadCriticalData({context}: Route.LoaderArgs) {
|
|
|
37
34
|
]);
|
|
38
35
|
|
|
39
36
|
return {
|
|
37
|
+
isShopLinked: Boolean(context.env.PUBLIC_STORE_DOMAIN),
|
|
40
38
|
featuredCollection: collections.nodes[0],
|
|
41
39
|
};
|
|
42
40
|
}
|
|
@@ -64,6 +62,7 @@ export default function Homepage() {
|
|
|
64
62
|
const data = useLoaderData<typeof loader>();
|
|
65
63
|
return (
|
|
66
64
|
<div className="home">
|
|
65
|
+
{data.isShopLinked ? null : <MockShopNotice />}
|
|
67
66
|
<FeaturedCollection collection={data.featuredCollection} />
|
|
68
67
|
<RecommendedProducts products={data.recommendedProducts} />
|
|
69
68
|
</div>
|
|
@@ -84,7 +83,11 @@ function FeaturedCollection({
|
|
|
84
83
|
>
|
|
85
84
|
{image && (
|
|
86
85
|
<div className="featured-collection-image">
|
|
87
|
-
<Image
|
|
86
|
+
<Image
|
|
87
|
+
data={image}
|
|
88
|
+
sizes="100vw"
|
|
89
|
+
alt={image.altText || collection.title}
|
|
90
|
+
/>
|
|
88
91
|
</div>
|
|
89
92
|
)}
|
|
90
93
|
<h1>{collection.title}</h1>
|
|
@@ -98,8 +101,11 @@ function RecommendedProducts({
|
|
|
98
101
|
products: Promise<RecommendedProductsQuery | null>;
|
|
99
102
|
}) {
|
|
100
103
|
return (
|
|
101
|
-
<
|
|
102
|
-
|
|
104
|
+
<section
|
|
105
|
+
className="recommended-products"
|
|
106
|
+
aria-labelledby="recommended-products"
|
|
107
|
+
>
|
|
108
|
+
<h2 id="recommended-products">Recommended Products</h2>
|
|
103
109
|
<Suspense fallback={<div>Loading...</div>}>
|
|
104
110
|
<Await resolve={products}>
|
|
105
111
|
{(response) => (
|
|
@@ -114,7 +120,7 @@ function RecommendedProducts({
|
|
|
114
120
|
</Await>
|
|
115
121
|
</Suspense>
|
|
116
122
|
<br />
|
|
117
|
-
</
|
|
123
|
+
</section>
|
|
118
124
|
);
|
|
119
125
|
}
|
|
120
126
|
|
|
@@ -3,7 +3,7 @@ import type {Route} from './+types/account.$';
|
|
|
3
3
|
|
|
4
4
|
// fallback wild card for all unauthenticated routes in account section
|
|
5
5
|
export async function loader({context}: Route.LoaderArgs) {
|
|
6
|
-
context.customerAccount.handleAuthStatus();
|
|
6
|
+
await context.customerAccount.handleAuthStatus();
|
|
7
7
|
|
|
8
8
|
return redirect('/account');
|
|
9
9
|
}
|