hydrogen-forge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +212 -0
  2. package/dist/commands/add.d.ts +7 -0
  3. package/dist/commands/add.d.ts.map +1 -0
  4. package/dist/commands/add.js +123 -0
  5. package/dist/commands/add.js.map +1 -0
  6. package/dist/commands/create.d.ts +8 -0
  7. package/dist/commands/create.d.ts.map +1 -0
  8. package/dist/commands/create.js +160 -0
  9. package/dist/commands/create.js.map +1 -0
  10. package/dist/commands/setup-mcp.d.ts +7 -0
  11. package/dist/commands/setup-mcp.d.ts.map +1 -0
  12. package/dist/commands/setup-mcp.js +179 -0
  13. package/dist/commands/setup-mcp.js.map +1 -0
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +50 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/lib/generators.d.ts +6 -0
  19. package/dist/lib/generators.d.ts.map +1 -0
  20. package/dist/lib/generators.js +470 -0
  21. package/dist/lib/generators.js.map +1 -0
  22. package/dist/lib/utils.d.ts +17 -0
  23. package/dist/lib/utils.d.ts.map +1 -0
  24. package/dist/lib/utils.js +101 -0
  25. package/dist/lib/utils.js.map +1 -0
  26. package/package.json +54 -0
  27. package/templates/starter/.env.example +21 -0
  28. package/templates/starter/.graphqlrc.ts +27 -0
  29. package/templates/starter/README.md +117 -0
  30. package/templates/starter/app/assets/favicon.svg +28 -0
  31. package/templates/starter/app/components/AddToCartButton.tsx +102 -0
  32. package/templates/starter/app/components/Aside.tsx +136 -0
  33. package/templates/starter/app/components/CartLineItem.tsx +229 -0
  34. package/templates/starter/app/components/CartMain.tsx +131 -0
  35. package/templates/starter/app/components/CartSummary.tsx +315 -0
  36. package/templates/starter/app/components/CollectionFilters.tsx +330 -0
  37. package/templates/starter/app/components/CollectionGrid.tsx +141 -0
  38. package/templates/starter/app/components/Footer.tsx +218 -0
  39. package/templates/starter/app/components/Header.tsx +296 -0
  40. package/templates/starter/app/components/PageLayout.tsx +174 -0
  41. package/templates/starter/app/components/PaginatedResourceSection.tsx +41 -0
  42. package/templates/starter/app/components/ProductCard.tsx +151 -0
  43. package/templates/starter/app/components/ProductForm.tsx +156 -0
  44. package/templates/starter/app/components/ProductGallery.tsx +164 -0
  45. package/templates/starter/app/components/ProductGrid.tsx +64 -0
  46. package/templates/starter/app/components/ProductImage.tsx +23 -0
  47. package/templates/starter/app/components/ProductItem.tsx +44 -0
  48. package/templates/starter/app/components/ProductPrice.tsx +97 -0
  49. package/templates/starter/app/components/SearchDialog.tsx +599 -0
  50. package/templates/starter/app/components/SearchForm.tsx +68 -0
  51. package/templates/starter/app/components/SearchFormPredictive.tsx +76 -0
  52. package/templates/starter/app/components/SearchResults.tsx +161 -0
  53. package/templates/starter/app/components/SearchResultsPredictive.tsx +461 -0
  54. package/templates/starter/app/entry.client.tsx +21 -0
  55. package/templates/starter/app/entry.server.tsx +53 -0
  56. package/templates/starter/app/graphql/customer-account/CustomerAddressMutations.ts +64 -0
  57. package/templates/starter/app/graphql/customer-account/CustomerDetailsQuery.ts +40 -0
  58. package/templates/starter/app/graphql/customer-account/CustomerOrderQuery.ts +90 -0
  59. package/templates/starter/app/graphql/customer-account/CustomerOrdersQuery.ts +63 -0
  60. package/templates/starter/app/graphql/customer-account/CustomerUpdateMutation.ts +25 -0
  61. package/templates/starter/app/lib/context.ts +60 -0
  62. package/templates/starter/app/lib/fragments.ts +234 -0
  63. package/templates/starter/app/lib/orderFilters.ts +90 -0
  64. package/templates/starter/app/lib/redirect.ts +23 -0
  65. package/templates/starter/app/lib/search.ts +79 -0
  66. package/templates/starter/app/lib/session.ts +72 -0
  67. package/templates/starter/app/lib/variants.ts +46 -0
  68. package/templates/starter/app/root.tsx +209 -0
  69. package/templates/starter/app/routes/$.tsx +11 -0
  70. package/templates/starter/app/routes/[robots.txt].tsx +117 -0
  71. package/templates/starter/app/routes/[sitemap.xml].tsx +16 -0
  72. package/templates/starter/app/routes/_index.tsx +167 -0
  73. package/templates/starter/app/routes/account.$.tsx +9 -0
  74. package/templates/starter/app/routes/account._index.tsx +5 -0
  75. package/templates/starter/app/routes/account.addresses.tsx +516 -0
  76. package/templates/starter/app/routes/account.orders.$id.tsx +222 -0
  77. package/templates/starter/app/routes/account.orders._index.tsx +222 -0
  78. package/templates/starter/app/routes/account.profile.tsx +133 -0
  79. package/templates/starter/app/routes/account.tsx +97 -0
  80. package/templates/starter/app/routes/account_.authorize.tsx +5 -0
  81. package/templates/starter/app/routes/account_.login.tsx +7 -0
  82. package/templates/starter/app/routes/account_.logout.tsx +11 -0
  83. package/templates/starter/app/routes/api.$version.[graphql.json].tsx +14 -0
  84. package/templates/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +129 -0
  85. package/templates/starter/app/routes/blogs.$blogHandle._index.tsx +175 -0
  86. package/templates/starter/app/routes/blogs._index.tsx +109 -0
  87. package/templates/starter/app/routes/cart.$lines.tsx +70 -0
  88. package/templates/starter/app/routes/cart.tsx +117 -0
  89. package/templates/starter/app/routes/collections.$handle.tsx +161 -0
  90. package/templates/starter/app/routes/collections._index.tsx +133 -0
  91. package/templates/starter/app/routes/collections.all.tsx +122 -0
  92. package/templates/starter/app/routes/discount.$code.tsx +48 -0
  93. package/templates/starter/app/routes/pages.$handle.tsx +88 -0
  94. package/templates/starter/app/routes/policies.$handle.tsx +93 -0
  95. package/templates/starter/app/routes/policies._index.tsx +69 -0
  96. package/templates/starter/app/routes/products.$handle.tsx +232 -0
  97. package/templates/starter/app/routes/search.tsx +426 -0
  98. package/templates/starter/app/routes/sitemap.$type.$page[.xml].tsx +23 -0
  99. package/templates/starter/app/routes.ts +9 -0
  100. package/templates/starter/app/styles/app.css +574 -0
  101. package/templates/starter/app/styles/reset.css +139 -0
  102. package/templates/starter/app/styles/tailwind.css +116 -0
  103. package/templates/starter/customer-accountapi.generated.d.ts +543 -0
  104. package/templates/starter/env.d.ts +7 -0
  105. package/templates/starter/eslint.config.js +247 -0
  106. package/templates/starter/guides/predictiveSearch/predictiveSearch.jpg +0 -0
  107. package/templates/starter/guides/predictiveSearch/predictiveSearch.md +394 -0
  108. package/templates/starter/guides/search/search.jpg +0 -0
  109. package/templates/starter/guides/search/search.md +335 -0
  110. package/templates/starter/package.json +71 -0
  111. package/templates/starter/postcss.config.js +6 -0
  112. package/templates/starter/public/.gitkeep +0 -0
  113. package/templates/starter/react-router.config.ts +13 -0
  114. package/templates/starter/server.ts +59 -0
  115. package/templates/starter/storefrontapi.generated.d.ts +1264 -0
  116. package/templates/starter/tailwind.config.js +83 -0
  117. package/templates/starter/tsconfig.json +67 -0
  118. package/templates/starter/vite.config.ts +32 -0
@@ -0,0 +1,167 @@
1
+ import {Await, useLoaderData, Link} from 'react-router';
2
+ import type {Route} from './+types/_index';
3
+ import {Suspense} from 'react';
4
+ import {Image} from '@shopify/hydrogen';
5
+ import type {
6
+ FeaturedCollectionFragment,
7
+ RecommendedProductsQuery,
8
+ } from 'storefrontapi.generated';
9
+ import {ProductItem} from '~/components/ProductItem';
10
+
11
+ export const meta: Route.MetaFunction = () => {
12
+ return [{title: 'Hydrogen | Home'}];
13
+ };
14
+
15
+ export async function loader(args: Route.LoaderArgs) {
16
+ // Start fetching non-critical data without blocking time to first byte
17
+ const deferredData = loadDeferredData(args);
18
+
19
+ // Await the critical data required to render initial state of the page
20
+ const criticalData = await loadCriticalData(args);
21
+
22
+ return {...deferredData, ...criticalData};
23
+ }
24
+
25
+ /**
26
+ * Load data necessary for rendering content above the fold. This is the critical data
27
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
28
+ */
29
+ async function loadCriticalData({context}: Route.LoaderArgs) {
30
+ const [{collections}] = await Promise.all([
31
+ context.storefront.query(FEATURED_COLLECTION_QUERY),
32
+ // Add other queries here, so that they are loaded in parallel
33
+ ]);
34
+
35
+ return {
36
+ featuredCollection: collections.nodes[0],
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Load data for rendering content below the fold. This data is deferred and will be
42
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
43
+ * Make sure to not throw any errors here, as it will cause the page to 500.
44
+ */
45
+ function loadDeferredData({context}: Route.LoaderArgs) {
46
+ const recommendedProducts = context.storefront
47
+ .query(RECOMMENDED_PRODUCTS_QUERY)
48
+ .catch((error: Error) => {
49
+ // Log query errors, but don't throw them so the page can still render
50
+ console.error(error);
51
+ return null;
52
+ });
53
+
54
+ return {
55
+ recommendedProducts,
56
+ };
57
+ }
58
+
59
+ export default function Homepage() {
60
+ const data = useLoaderData<typeof loader>();
61
+ return (
62
+ <div className="home">
63
+ <FeaturedCollection collection={data.featuredCollection} />
64
+ <RecommendedProducts products={data.recommendedProducts} />
65
+ </div>
66
+ );
67
+ }
68
+
69
+ function FeaturedCollection({
70
+ collection,
71
+ }: {
72
+ collection: FeaturedCollectionFragment;
73
+ }) {
74
+ if (!collection) return null;
75
+ const image = collection?.image;
76
+ return (
77
+ <Link
78
+ className="featured-collection"
79
+ to={`/collections/${collection.handle}`}
80
+ >
81
+ {image && (
82
+ <div className="featured-collection-image">
83
+ <Image data={image} sizes="100vw" />
84
+ </div>
85
+ )}
86
+ <h1>{collection.title}</h1>
87
+ </Link>
88
+ );
89
+ }
90
+
91
+ function RecommendedProducts({
92
+ products,
93
+ }: {
94
+ products: Promise<RecommendedProductsQuery | null>;
95
+ }) {
96
+ return (
97
+ <div className="recommended-products">
98
+ <h2>Recommended Products</h2>
99
+ <Suspense fallback={<div>Loading...</div>}>
100
+ <Await resolve={products}>
101
+ {(response) => (
102
+ <div className="recommended-products-grid">
103
+ {response
104
+ ? response.products.nodes.map((product) => (
105
+ <ProductItem key={product.id} product={product} />
106
+ ))
107
+ : null}
108
+ </div>
109
+ )}
110
+ </Await>
111
+ </Suspense>
112
+ <br />
113
+ </div>
114
+ );
115
+ }
116
+
117
+ const FEATURED_COLLECTION_QUERY = `#graphql
118
+ fragment FeaturedCollection on Collection {
119
+ id
120
+ title
121
+ image {
122
+ id
123
+ url
124
+ altText
125
+ width
126
+ height
127
+ }
128
+ handle
129
+ }
130
+ query FeaturedCollection($country: CountryCode, $language: LanguageCode)
131
+ @inContext(country: $country, language: $language) {
132
+ collections(first: 1, sortKey: UPDATED_AT, reverse: true) {
133
+ nodes {
134
+ ...FeaturedCollection
135
+ }
136
+ }
137
+ }
138
+ ` as const;
139
+
140
+ const RECOMMENDED_PRODUCTS_QUERY = `#graphql
141
+ fragment RecommendedProduct on Product {
142
+ id
143
+ title
144
+ handle
145
+ priceRange {
146
+ minVariantPrice {
147
+ amount
148
+ currencyCode
149
+ }
150
+ }
151
+ featuredImage {
152
+ id
153
+ url
154
+ altText
155
+ width
156
+ height
157
+ }
158
+ }
159
+ query RecommendedProducts ($country: CountryCode, $language: LanguageCode)
160
+ @inContext(country: $country, language: $language) {
161
+ products(first: 4, sortKey: UPDATED_AT, reverse: true) {
162
+ nodes {
163
+ ...RecommendedProduct
164
+ }
165
+ }
166
+ }
167
+ ` as const;
@@ -0,0 +1,9 @@
1
+ import {redirect} from 'react-router';
2
+ import type {Route} from './+types/account.$';
3
+
4
+ // fallback wild card for all unauthenticated routes in account section
5
+ export async function loader({context}: Route.LoaderArgs) {
6
+ context.customerAccount.handleAuthStatus();
7
+
8
+ return redirect('/account');
9
+ }
@@ -0,0 +1,5 @@
1
+ import {redirect} from 'react-router';
2
+
3
+ export async function loader() {
4
+ return redirect('/account/orders');
5
+ }
@@ -0,0 +1,516 @@
1
+ import type {CustomerAddressInput} from '@shopify/hydrogen/customer-account-api-types';
2
+ import type {
3
+ AddressFragment,
4
+ CustomerFragment,
5
+ } from 'customer-accountapi.generated';
6
+ import {
7
+ data,
8
+ Form,
9
+ useActionData,
10
+ useNavigation,
11
+ useOutletContext,
12
+ type Fetcher,
13
+ } from 'react-router';
14
+ import type {Route} from './+types/account.addresses';
15
+ import {
16
+ UPDATE_ADDRESS_MUTATION,
17
+ DELETE_ADDRESS_MUTATION,
18
+ CREATE_ADDRESS_MUTATION,
19
+ } from '~/graphql/customer-account/CustomerAddressMutations';
20
+
21
+ export type ActionResponse = {
22
+ addressId?: string | null;
23
+ createdAddress?: AddressFragment;
24
+ defaultAddress?: string | null;
25
+ deletedAddress?: string | null;
26
+ error: Record<AddressFragment['id'], string> | null;
27
+ updatedAddress?: AddressFragment;
28
+ };
29
+
30
+ export const meta: Route.MetaFunction = () => {
31
+ return [{title: 'Addresses'}];
32
+ };
33
+
34
+ export async function loader({context}: Route.LoaderArgs) {
35
+ context.customerAccount.handleAuthStatus();
36
+
37
+ return {};
38
+ }
39
+
40
+ export async function action({request, context}: Route.ActionArgs) {
41
+ const {customerAccount} = context;
42
+
43
+ try {
44
+ const form = await request.formData();
45
+
46
+ const addressId = form.has('addressId')
47
+ ? String(form.get('addressId'))
48
+ : null;
49
+ if (!addressId) {
50
+ throw new Error('You must provide an address id.');
51
+ }
52
+
53
+ // this will ensure redirecting to login never happen for mutatation
54
+ const isLoggedIn = await customerAccount.isLoggedIn();
55
+ if (!isLoggedIn) {
56
+ return data(
57
+ {error: {[addressId]: 'Unauthorized'}},
58
+ {
59
+ status: 401,
60
+ },
61
+ );
62
+ }
63
+
64
+ const defaultAddress = form.has('defaultAddress')
65
+ ? String(form.get('defaultAddress')) === 'on'
66
+ : false;
67
+ const address: CustomerAddressInput = {};
68
+ const keys: (keyof CustomerAddressInput)[] = [
69
+ 'address1',
70
+ 'address2',
71
+ 'city',
72
+ 'company',
73
+ 'territoryCode',
74
+ 'firstName',
75
+ 'lastName',
76
+ 'phoneNumber',
77
+ 'zoneCode',
78
+ 'zip',
79
+ ];
80
+
81
+ for (const key of keys) {
82
+ const value = form.get(key);
83
+ if (typeof value === 'string') {
84
+ address[key] = value;
85
+ }
86
+ }
87
+
88
+ switch (request.method) {
89
+ case 'POST': {
90
+ // handle new address creation
91
+ try {
92
+ const {data, errors} = await customerAccount.mutate(
93
+ CREATE_ADDRESS_MUTATION,
94
+ {
95
+ variables: {
96
+ address,
97
+ defaultAddress,
98
+ language: customerAccount.i18n.language,
99
+ },
100
+ },
101
+ );
102
+
103
+ if (errors?.length) {
104
+ throw new Error(errors[0].message);
105
+ }
106
+
107
+ if (data?.customerAddressCreate?.userErrors?.length) {
108
+ throw new Error(data?.customerAddressCreate?.userErrors[0].message);
109
+ }
110
+
111
+ if (!data?.customerAddressCreate?.customerAddress) {
112
+ throw new Error('Customer address create failed.');
113
+ }
114
+
115
+ return {
116
+ error: null,
117
+ createdAddress: data?.customerAddressCreate?.customerAddress,
118
+ defaultAddress,
119
+ };
120
+ } catch (error: unknown) {
121
+ if (error instanceof Error) {
122
+ return data(
123
+ {error: {[addressId]: error.message}},
124
+ {
125
+ status: 400,
126
+ },
127
+ );
128
+ }
129
+ return data(
130
+ {error: {[addressId]: error}},
131
+ {
132
+ status: 400,
133
+ },
134
+ );
135
+ }
136
+ }
137
+
138
+ case 'PUT': {
139
+ // handle address updates
140
+ try {
141
+ const {data, errors} = await customerAccount.mutate(
142
+ UPDATE_ADDRESS_MUTATION,
143
+ {
144
+ variables: {
145
+ address,
146
+ addressId: decodeURIComponent(addressId),
147
+ defaultAddress,
148
+ language: customerAccount.i18n.language,
149
+ },
150
+ },
151
+ );
152
+
153
+ if (errors?.length) {
154
+ throw new Error(errors[0].message);
155
+ }
156
+
157
+ if (data?.customerAddressUpdate?.userErrors?.length) {
158
+ throw new Error(data?.customerAddressUpdate?.userErrors[0].message);
159
+ }
160
+
161
+ if (!data?.customerAddressUpdate?.customerAddress) {
162
+ throw new Error('Customer address update failed.');
163
+ }
164
+
165
+ return {
166
+ error: null,
167
+ updatedAddress: address,
168
+ defaultAddress,
169
+ };
170
+ } catch (error: unknown) {
171
+ if (error instanceof Error) {
172
+ return data(
173
+ {error: {[addressId]: error.message}},
174
+ {
175
+ status: 400,
176
+ },
177
+ );
178
+ }
179
+ return data(
180
+ {error: {[addressId]: error}},
181
+ {
182
+ status: 400,
183
+ },
184
+ );
185
+ }
186
+ }
187
+
188
+ case 'DELETE': {
189
+ // handles address deletion
190
+ try {
191
+ const {data, errors} = await customerAccount.mutate(
192
+ DELETE_ADDRESS_MUTATION,
193
+ {
194
+ variables: {
195
+ addressId: decodeURIComponent(addressId),
196
+ language: customerAccount.i18n.language,
197
+ },
198
+ },
199
+ );
200
+
201
+ if (errors?.length) {
202
+ throw new Error(errors[0].message);
203
+ }
204
+
205
+ if (data?.customerAddressDelete?.userErrors?.length) {
206
+ throw new Error(data?.customerAddressDelete?.userErrors[0].message);
207
+ }
208
+
209
+ if (!data?.customerAddressDelete?.deletedAddressId) {
210
+ throw new Error('Customer address delete failed.');
211
+ }
212
+
213
+ return {error: null, deletedAddress: addressId};
214
+ } catch (error: unknown) {
215
+ if (error instanceof Error) {
216
+ return data(
217
+ {error: {[addressId]: error.message}},
218
+ {
219
+ status: 400,
220
+ },
221
+ );
222
+ }
223
+ return data(
224
+ {error: {[addressId]: error}},
225
+ {
226
+ status: 400,
227
+ },
228
+ );
229
+ }
230
+ }
231
+
232
+ default: {
233
+ return data(
234
+ {error: {[addressId]: 'Method not allowed'}},
235
+ {
236
+ status: 405,
237
+ },
238
+ );
239
+ }
240
+ }
241
+ } catch (error: unknown) {
242
+ if (error instanceof Error) {
243
+ return data(
244
+ {error: error.message},
245
+ {
246
+ status: 400,
247
+ },
248
+ );
249
+ }
250
+ return data(
251
+ {error},
252
+ {
253
+ status: 400,
254
+ },
255
+ );
256
+ }
257
+ }
258
+
259
+ export default function Addresses() {
260
+ const {customer} = useOutletContext<{customer: CustomerFragment}>();
261
+ const {defaultAddress, addresses} = customer;
262
+
263
+ return (
264
+ <div className="account-addresses">
265
+ <h2>Addresses</h2>
266
+ <br />
267
+ {!addresses.nodes.length ? (
268
+ <p>You have no addresses saved.</p>
269
+ ) : (
270
+ <div>
271
+ <div>
272
+ <legend>Create address</legend>
273
+ <NewAddressForm />
274
+ </div>
275
+ <br />
276
+ <hr />
277
+ <br />
278
+ <ExistingAddresses
279
+ addresses={addresses}
280
+ defaultAddress={defaultAddress}
281
+ />
282
+ </div>
283
+ )}
284
+ </div>
285
+ );
286
+ }
287
+
288
+ function NewAddressForm() {
289
+ const newAddress = {
290
+ address1: '',
291
+ address2: '',
292
+ city: '',
293
+ company: '',
294
+ territoryCode: '',
295
+ firstName: '',
296
+ id: 'new',
297
+ lastName: '',
298
+ phoneNumber: '',
299
+ zoneCode: '',
300
+ zip: '',
301
+ } as CustomerAddressInput;
302
+
303
+ return (
304
+ <AddressForm
305
+ addressId={'NEW_ADDRESS_ID'}
306
+ address={newAddress}
307
+ defaultAddress={null}
308
+ >
309
+ {({stateForMethod}) => (
310
+ <div>
311
+ <button
312
+ disabled={stateForMethod('POST') !== 'idle'}
313
+ formMethod="POST"
314
+ type="submit"
315
+ >
316
+ {stateForMethod('POST') !== 'idle' ? 'Creating' : 'Create'}
317
+ </button>
318
+ </div>
319
+ )}
320
+ </AddressForm>
321
+ );
322
+ }
323
+
324
+ function ExistingAddresses({
325
+ addresses,
326
+ defaultAddress,
327
+ }: Pick<CustomerFragment, 'addresses' | 'defaultAddress'>) {
328
+ return (
329
+ <div>
330
+ <legend>Existing addresses</legend>
331
+ {addresses.nodes.map((address) => (
332
+ <AddressForm
333
+ key={address.id}
334
+ addressId={address.id}
335
+ address={address}
336
+ defaultAddress={defaultAddress}
337
+ >
338
+ {({stateForMethod}) => (
339
+ <div>
340
+ <button
341
+ disabled={stateForMethod('PUT') !== 'idle'}
342
+ formMethod="PUT"
343
+ type="submit"
344
+ >
345
+ {stateForMethod('PUT') !== 'idle' ? 'Saving' : 'Save'}
346
+ </button>
347
+ <button
348
+ disabled={stateForMethod('DELETE') !== 'idle'}
349
+ formMethod="DELETE"
350
+ type="submit"
351
+ >
352
+ {stateForMethod('DELETE') !== 'idle' ? 'Deleting' : 'Delete'}
353
+ </button>
354
+ </div>
355
+ )}
356
+ </AddressForm>
357
+ ))}
358
+ </div>
359
+ );
360
+ }
361
+
362
+ export function AddressForm({
363
+ addressId,
364
+ address,
365
+ defaultAddress,
366
+ children,
367
+ }: {
368
+ addressId: AddressFragment['id'];
369
+ address: CustomerAddressInput;
370
+ defaultAddress: CustomerFragment['defaultAddress'];
371
+ children: (props: {
372
+ stateForMethod: (method: 'PUT' | 'POST' | 'DELETE') => Fetcher['state'];
373
+ }) => React.ReactNode;
374
+ }) {
375
+ const {state, formMethod} = useNavigation();
376
+ const action = useActionData<ActionResponse>();
377
+ const error = action?.error?.[addressId];
378
+ const isDefaultAddress = defaultAddress?.id === addressId;
379
+ return (
380
+ <Form id={addressId}>
381
+ <fieldset>
382
+ <input type="hidden" name="addressId" defaultValue={addressId} />
383
+ <label htmlFor="firstName">First name*</label>
384
+ <input
385
+ aria-label="First name"
386
+ autoComplete="given-name"
387
+ defaultValue={address?.firstName ?? ''}
388
+ id="firstName"
389
+ name="firstName"
390
+ placeholder="First name"
391
+ required
392
+ type="text"
393
+ />
394
+ <label htmlFor="lastName">Last name*</label>
395
+ <input
396
+ aria-label="Last name"
397
+ autoComplete="family-name"
398
+ defaultValue={address?.lastName ?? ''}
399
+ id="lastName"
400
+ name="lastName"
401
+ placeholder="Last name"
402
+ required
403
+ type="text"
404
+ />
405
+ <label htmlFor="company">Company</label>
406
+ <input
407
+ aria-label="Company"
408
+ autoComplete="organization"
409
+ defaultValue={address?.company ?? ''}
410
+ id="company"
411
+ name="company"
412
+ placeholder="Company"
413
+ type="text"
414
+ />
415
+ <label htmlFor="address1">Address line*</label>
416
+ <input
417
+ aria-label="Address line 1"
418
+ autoComplete="address-line1"
419
+ defaultValue={address?.address1 ?? ''}
420
+ id="address1"
421
+ name="address1"
422
+ placeholder="Address line 1*"
423
+ required
424
+ type="text"
425
+ />
426
+ <label htmlFor="address2">Address line 2</label>
427
+ <input
428
+ aria-label="Address line 2"
429
+ autoComplete="address-line2"
430
+ defaultValue={address?.address2 ?? ''}
431
+ id="address2"
432
+ name="address2"
433
+ placeholder="Address line 2"
434
+ type="text"
435
+ />
436
+ <label htmlFor="city">City*</label>
437
+ <input
438
+ aria-label="City"
439
+ autoComplete="address-level2"
440
+ defaultValue={address?.city ?? ''}
441
+ id="city"
442
+ name="city"
443
+ placeholder="City"
444
+ required
445
+ type="text"
446
+ />
447
+ <label htmlFor="zoneCode">State / Province*</label>
448
+ <input
449
+ aria-label="State/Province"
450
+ autoComplete="address-level1"
451
+ defaultValue={address?.zoneCode ?? ''}
452
+ id="zoneCode"
453
+ name="zoneCode"
454
+ placeholder="State / Province"
455
+ required
456
+ type="text"
457
+ />
458
+ <label htmlFor="zip">Zip / Postal Code*</label>
459
+ <input
460
+ aria-label="Zip"
461
+ autoComplete="postal-code"
462
+ defaultValue={address?.zip ?? ''}
463
+ id="zip"
464
+ name="zip"
465
+ placeholder="Zip / Postal Code"
466
+ required
467
+ type="text"
468
+ />
469
+ <label htmlFor="territoryCode">Country Code*</label>
470
+ <input
471
+ aria-label="territoryCode"
472
+ autoComplete="country"
473
+ defaultValue={address?.territoryCode ?? ''}
474
+ id="territoryCode"
475
+ name="territoryCode"
476
+ placeholder="Country"
477
+ required
478
+ type="text"
479
+ maxLength={2}
480
+ />
481
+ <label htmlFor="phoneNumber">Phone</label>
482
+ <input
483
+ aria-label="Phone Number"
484
+ autoComplete="tel"
485
+ defaultValue={address?.phoneNumber ?? ''}
486
+ id="phoneNumber"
487
+ name="phoneNumber"
488
+ placeholder="+16135551111"
489
+ pattern="^\+?[1-9]\d{3,14}$"
490
+ type="tel"
491
+ />
492
+ <div>
493
+ <input
494
+ defaultChecked={isDefaultAddress}
495
+ id="defaultAddress"
496
+ name="defaultAddress"
497
+ type="checkbox"
498
+ />
499
+ <label htmlFor="defaultAddress">Set as default address</label>
500
+ </div>
501
+ {error ? (
502
+ <p>
503
+ <mark>
504
+ <small>{error}</small>
505
+ </mark>
506
+ </p>
507
+ ) : (
508
+ <br />
509
+ )}
510
+ {children({
511
+ stateForMethod: (method) => (formMethod === method ? state : 'idle'),
512
+ })}
513
+ </fieldset>
514
+ </Form>
515
+ );
516
+ }