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,14 @@
1
+ import type {Route} from './+types/api.$version.[graphql.json]';
2
+
3
+ export async function action({params, context, request}: Route.ActionArgs) {
4
+ const response = await fetch(
5
+ `https://${context.env.PUBLIC_CHECKOUT_DOMAIN}/api/${params.version}/graphql.json`,
6
+ {
7
+ method: 'POST',
8
+ body: request.body,
9
+ headers: request.headers,
10
+ },
11
+ );
12
+
13
+ return new Response(response.body, {headers: new Headers(response.headers)});
14
+ }
@@ -0,0 +1,129 @@
1
+ import {useLoaderData} from 'react-router';
2
+ import type {Route} from './+types/blogs.$blogHandle.$articleHandle';
3
+ import {Image} from '@shopify/hydrogen';
4
+ import {redirectIfHandleIsLocalized} from '~/lib/redirect';
5
+
6
+ export const meta: Route.MetaFunction = ({data}) => {
7
+ return [{title: `Hydrogen | ${data?.article.title ?? ''} article`}];
8
+ };
9
+
10
+ export async function loader(args: Route.LoaderArgs) {
11
+ // Start fetching non-critical data without blocking time to first byte
12
+ const deferredData = loadDeferredData(args);
13
+
14
+ // Await the critical data required to render initial state of the page
15
+ const criticalData = await loadCriticalData(args);
16
+
17
+ return {...deferredData, ...criticalData};
18
+ }
19
+
20
+ /**
21
+ * Load data necessary for rendering content above the fold. This is the critical data
22
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
23
+ */
24
+ async function loadCriticalData({context, request, params}: Route.LoaderArgs) {
25
+ const {blogHandle, articleHandle} = params;
26
+
27
+ if (!articleHandle || !blogHandle) {
28
+ throw new Response('Not found', {status: 404});
29
+ }
30
+
31
+ const [{blog}] = await Promise.all([
32
+ context.storefront.query(ARTICLE_QUERY, {
33
+ variables: {blogHandle, articleHandle},
34
+ }),
35
+ // Add other queries here, so that they are loaded in parallel
36
+ ]);
37
+
38
+ if (!blog?.articleByHandle) {
39
+ throw new Response(null, {status: 404});
40
+ }
41
+
42
+ redirectIfHandleIsLocalized(
43
+ request,
44
+ {
45
+ handle: articleHandle,
46
+ data: blog.articleByHandle,
47
+ },
48
+ {
49
+ handle: blogHandle,
50
+ data: blog,
51
+ },
52
+ );
53
+
54
+ const article = blog.articleByHandle;
55
+
56
+ return {article};
57
+ }
58
+
59
+ /**
60
+ * Load data for rendering content below the fold. This data is deferred and will be
61
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
62
+ * Make sure to not throw any errors here, as it will cause the page to 500.
63
+ */
64
+ function loadDeferredData({context}: Route.LoaderArgs) {
65
+ return {};
66
+ }
67
+
68
+ export default function Article() {
69
+ const {article} = useLoaderData<typeof loader>();
70
+ const {title, image, contentHtml, author} = article;
71
+
72
+ const publishedDate = new Intl.DateTimeFormat('en-US', {
73
+ year: 'numeric',
74
+ month: 'long',
75
+ day: 'numeric',
76
+ }).format(new Date(article.publishedAt));
77
+
78
+ return (
79
+ <div className="article">
80
+ <h1>
81
+ {title}
82
+ <div>
83
+ <time dateTime={article.publishedAt}>{publishedDate}</time> &middot;{' '}
84
+ <address>{author?.name}</address>
85
+ </div>
86
+ </h1>
87
+
88
+ {image && <Image data={image} sizes="90vw" loading="eager" />}
89
+ <div
90
+ dangerouslySetInnerHTML={{__html: contentHtml}}
91
+ className="article"
92
+ />
93
+ </div>
94
+ );
95
+ }
96
+
97
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/objects/blog#field-blog-articlebyhandle
98
+ const ARTICLE_QUERY = `#graphql
99
+ query Article(
100
+ $articleHandle: String!
101
+ $blogHandle: String!
102
+ $country: CountryCode
103
+ $language: LanguageCode
104
+ ) @inContext(language: $language, country: $country) {
105
+ blog(handle: $blogHandle) {
106
+ handle
107
+ articleByHandle(handle: $articleHandle) {
108
+ handle
109
+ title
110
+ contentHtml
111
+ publishedAt
112
+ author: authorV2 {
113
+ name
114
+ }
115
+ image {
116
+ id
117
+ altText
118
+ url
119
+ width
120
+ height
121
+ }
122
+ seo {
123
+ description
124
+ title
125
+ }
126
+ }
127
+ }
128
+ }
129
+ ` as const;
@@ -0,0 +1,175 @@
1
+ import {Link, useLoaderData} from 'react-router';
2
+ import type {Route} from './+types/blogs.$blogHandle._index';
3
+ import {Image, getPaginationVariables} from '@shopify/hydrogen';
4
+ import type {ArticleItemFragment} from 'storefrontapi.generated';
5
+ import {PaginatedResourceSection} from '~/components/PaginatedResourceSection';
6
+ import {redirectIfHandleIsLocalized} from '~/lib/redirect';
7
+
8
+ export const meta: Route.MetaFunction = ({data}) => {
9
+ return [{title: `Hydrogen | ${data?.blog.title ?? ''} blog`}];
10
+ };
11
+
12
+ export async function loader(args: Route.LoaderArgs) {
13
+ // Start fetching non-critical data without blocking time to first byte
14
+ const deferredData = loadDeferredData(args);
15
+
16
+ // Await the critical data required to render initial state of the page
17
+ const criticalData = await loadCriticalData(args);
18
+
19
+ return {...deferredData, ...criticalData};
20
+ }
21
+
22
+ /**
23
+ * Load data necessary for rendering content above the fold. This is the critical data
24
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
25
+ */
26
+ async function loadCriticalData({context, request, params}: Route.LoaderArgs) {
27
+ const paginationVariables = getPaginationVariables(request, {
28
+ pageBy: 4,
29
+ });
30
+
31
+ if (!params.blogHandle) {
32
+ throw new Response(`blog not found`, {status: 404});
33
+ }
34
+
35
+ const [{blog}] = await Promise.all([
36
+ context.storefront.query(BLOGS_QUERY, {
37
+ variables: {
38
+ blogHandle: params.blogHandle,
39
+ ...paginationVariables,
40
+ },
41
+ }),
42
+ // Add other queries here, so that they are loaded in parallel
43
+ ]);
44
+
45
+ if (!blog?.articles) {
46
+ throw new Response('Not found', {status: 404});
47
+ }
48
+
49
+ redirectIfHandleIsLocalized(request, {handle: params.blogHandle, data: blog});
50
+
51
+ return {blog};
52
+ }
53
+
54
+ /**
55
+ * Load data for rendering content below the fold. This data is deferred and will be
56
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
57
+ * Make sure to not throw any errors here, as it will cause the page to 500.
58
+ */
59
+ function loadDeferredData({context}: Route.LoaderArgs) {
60
+ return {};
61
+ }
62
+
63
+ export default function Blog() {
64
+ const {blog} = useLoaderData<typeof loader>();
65
+ const {articles} = blog;
66
+
67
+ return (
68
+ <div className="blog">
69
+ <h1>{blog.title}</h1>
70
+ <div className="blog-grid">
71
+ <PaginatedResourceSection<ArticleItemFragment> connection={articles}>
72
+ {({node: article, index}) => (
73
+ <ArticleItem
74
+ article={article}
75
+ key={article.id}
76
+ loading={index < 2 ? 'eager' : 'lazy'}
77
+ />
78
+ )}
79
+ </PaginatedResourceSection>
80
+ </div>
81
+ </div>
82
+ );
83
+ }
84
+
85
+ function ArticleItem({
86
+ article,
87
+ loading,
88
+ }: {
89
+ article: ArticleItemFragment;
90
+ loading?: HTMLImageElement['loading'];
91
+ }) {
92
+ const publishedAt = new Intl.DateTimeFormat('en-US', {
93
+ year: 'numeric',
94
+ month: 'long',
95
+ day: 'numeric',
96
+ }).format(new Date(article.publishedAt!));
97
+ return (
98
+ <div className="blog-article" key={article.id}>
99
+ <Link to={`/blogs/${article.blog.handle}/${article.handle}`}>
100
+ {article.image && (
101
+ <div className="blog-article-image">
102
+ <Image
103
+ alt={article.image.altText || article.title}
104
+ aspectRatio="3/2"
105
+ data={article.image}
106
+ loading={loading}
107
+ sizes="(min-width: 768px) 50vw, 100vw"
108
+ />
109
+ </div>
110
+ )}
111
+ <h3>{article.title}</h3>
112
+ <small>{publishedAt}</small>
113
+ </Link>
114
+ </div>
115
+ );
116
+ }
117
+
118
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/objects/blog
119
+ const BLOGS_QUERY = `#graphql
120
+ query Blog(
121
+ $language: LanguageCode
122
+ $blogHandle: String!
123
+ $first: Int
124
+ $last: Int
125
+ $startCursor: String
126
+ $endCursor: String
127
+ ) @inContext(language: $language) {
128
+ blog(handle: $blogHandle) {
129
+ title
130
+ handle
131
+ seo {
132
+ title
133
+ description
134
+ }
135
+ articles(
136
+ first: $first,
137
+ last: $last,
138
+ before: $startCursor,
139
+ after: $endCursor
140
+ ) {
141
+ nodes {
142
+ ...ArticleItem
143
+ }
144
+ pageInfo {
145
+ hasPreviousPage
146
+ hasNextPage
147
+ hasNextPage
148
+ endCursor
149
+ startCursor
150
+ }
151
+
152
+ }
153
+ }
154
+ }
155
+ fragment ArticleItem on Article {
156
+ author: authorV2 {
157
+ name
158
+ }
159
+ contentHtml
160
+ handle
161
+ id
162
+ image {
163
+ id
164
+ altText
165
+ url
166
+ width
167
+ height
168
+ }
169
+ publishedAt
170
+ title
171
+ blog {
172
+ handle
173
+ }
174
+ }
175
+ ` as const;
@@ -0,0 +1,109 @@
1
+ import {Link, useLoaderData} from 'react-router';
2
+ import type {Route} from './+types/blogs._index';
3
+ import {getPaginationVariables} from '@shopify/hydrogen';
4
+ import {PaginatedResourceSection} from '~/components/PaginatedResourceSection';
5
+ import type {BlogsQuery} from 'storefrontapi.generated';
6
+
7
+ type BlogNode = BlogsQuery['blogs']['nodes'][0];
8
+
9
+ export const meta: Route.MetaFunction = () => {
10
+ return [{title: `Hydrogen | Blogs`}];
11
+ };
12
+
13
+ export async function loader(args: Route.LoaderArgs) {
14
+ // Start fetching non-critical data without blocking time to first byte
15
+ const deferredData = loadDeferredData(args);
16
+
17
+ // Await the critical data required to render initial state of the page
18
+ const criticalData = await loadCriticalData(args);
19
+
20
+ return {...deferredData, ...criticalData};
21
+ }
22
+
23
+ /**
24
+ * Load data necessary for rendering content above the fold. This is the critical data
25
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
26
+ */
27
+ async function loadCriticalData({context, request}: Route.LoaderArgs) {
28
+ const paginationVariables = getPaginationVariables(request, {
29
+ pageBy: 10,
30
+ });
31
+
32
+ const [{blogs}] = await Promise.all([
33
+ context.storefront.query(BLOGS_QUERY, {
34
+ variables: {
35
+ ...paginationVariables,
36
+ },
37
+ }),
38
+ // Add other queries here, so that they are loaded in parallel
39
+ ]);
40
+
41
+ return {blogs};
42
+ }
43
+
44
+ /**
45
+ * Load data for rendering content below the fold. This data is deferred and will be
46
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
47
+ * Make sure to not throw any errors here, as it will cause the page to 500.
48
+ */
49
+ function loadDeferredData({context}: Route.LoaderArgs) {
50
+ return {};
51
+ }
52
+
53
+ export default function Blogs() {
54
+ const {blogs} = useLoaderData<typeof loader>();
55
+
56
+ return (
57
+ <div className="blogs">
58
+ <h1>Blogs</h1>
59
+ <div className="blogs-grid">
60
+ <PaginatedResourceSection<BlogNode> connection={blogs}>
61
+ {({node: blog}) => (
62
+ <Link
63
+ className="blog"
64
+ key={blog.handle}
65
+ prefetch="intent"
66
+ to={`/blogs/${blog.handle}`}
67
+ >
68
+ <h2>{blog.title}</h2>
69
+ </Link>
70
+ )}
71
+ </PaginatedResourceSection>
72
+ </div>
73
+ </div>
74
+ );
75
+ }
76
+
77
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/objects/blog
78
+ const BLOGS_QUERY = `#graphql
79
+ query Blogs(
80
+ $country: CountryCode
81
+ $endCursor: String
82
+ $first: Int
83
+ $language: LanguageCode
84
+ $last: Int
85
+ $startCursor: String
86
+ ) @inContext(country: $country, language: $language) {
87
+ blogs(
88
+ first: $first,
89
+ last: $last,
90
+ before: $startCursor,
91
+ after: $endCursor
92
+ ) {
93
+ pageInfo {
94
+ hasNextPage
95
+ hasPreviousPage
96
+ startCursor
97
+ endCursor
98
+ }
99
+ nodes {
100
+ title
101
+ handle
102
+ seo {
103
+ title
104
+ description
105
+ }
106
+ }
107
+ }
108
+ }
109
+ ` as const;
@@ -0,0 +1,70 @@
1
+ import {redirect} from 'react-router';
2
+ import type {Route} from './+types/cart.$lines';
3
+
4
+ /**
5
+ * Automatically creates a new cart based on the URL and redirects straight to checkout.
6
+ * Expected URL structure:
7
+ * ```js
8
+ * /cart/<variant_id>:<quantity>
9
+ *
10
+ * ```
11
+ *
12
+ * More than one `<variant_id>:<quantity>` separated by a comma, can be supplied in the URL, for
13
+ * carts with more than one product variant.
14
+ *
15
+ * @example
16
+ * Example path creating a cart with two product variants, different quantities, and a discount code in the querystring:
17
+ * ```js
18
+ * /cart/41007289663544:1,41007289696312:2?discount=HYDROBOARD
19
+ *
20
+ * ```
21
+ */
22
+ export async function loader({request, context, params}: Route.LoaderArgs) {
23
+ const {cart} = context;
24
+ const {lines} = params;
25
+ if (!lines) return redirect('/cart');
26
+ const linesMap = lines.split(',').map((line) => {
27
+ const lineDetails = line.split(':');
28
+ const variantId = lineDetails[0];
29
+ const quantity = parseInt(lineDetails[1], 10);
30
+
31
+ return {
32
+ merchandiseId: `gid://shopify/ProductVariant/${variantId}`,
33
+ quantity,
34
+ };
35
+ });
36
+
37
+ const url = new URL(request.url);
38
+ const searchParams = new URLSearchParams(url.search);
39
+
40
+ const discount = searchParams.get('discount');
41
+ const discountArray = discount ? [discount] : [];
42
+
43
+ // create a cart
44
+ const result = await cart.create({
45
+ lines: linesMap,
46
+ discountCodes: discountArray,
47
+ });
48
+
49
+ const cartResult = result.cart;
50
+
51
+ if (result.errors?.length || !cartResult) {
52
+ throw new Response('Link may be expired. Try checking the URL.', {
53
+ status: 410,
54
+ });
55
+ }
56
+
57
+ // Update cart id in cookie
58
+ const headers = cart.setCartId(cartResult.id);
59
+
60
+ // redirect to checkout
61
+ if (cartResult.checkoutUrl) {
62
+ return redirect(cartResult.checkoutUrl, {headers});
63
+ } else {
64
+ throw new Error('No checkout URL found');
65
+ }
66
+ }
67
+
68
+ export default function Component() {
69
+ return null;
70
+ }
@@ -0,0 +1,117 @@
1
+ import {useLoaderData, data, type HeadersFunction} from 'react-router';
2
+ import type {Route} from './+types/cart';
3
+ import type {CartQueryDataReturn} from '@shopify/hydrogen';
4
+ import {CartForm} from '@shopify/hydrogen';
5
+ import {CartMain} from '~/components/CartMain';
6
+
7
+ export const meta: Route.MetaFunction = () => {
8
+ return [{title: `Hydrogen | Cart`}];
9
+ };
10
+
11
+ export const headers: HeadersFunction = ({actionHeaders}) => actionHeaders;
12
+
13
+ export async function action({request, context}: Route.ActionArgs) {
14
+ const {cart} = context;
15
+
16
+ const formData = await request.formData();
17
+
18
+ const {action, inputs} = CartForm.getFormInput(formData);
19
+
20
+ if (!action) {
21
+ throw new Error('No action provided');
22
+ }
23
+
24
+ let status = 200;
25
+ let result: CartQueryDataReturn;
26
+
27
+ switch (action) {
28
+ case CartForm.ACTIONS.LinesAdd:
29
+ result = await cart.addLines(inputs.lines);
30
+ break;
31
+ case CartForm.ACTIONS.LinesUpdate:
32
+ result = await cart.updateLines(inputs.lines);
33
+ break;
34
+ case CartForm.ACTIONS.LinesRemove:
35
+ result = await cart.removeLines(inputs.lineIds);
36
+ break;
37
+ case CartForm.ACTIONS.DiscountCodesUpdate: {
38
+ const formDiscountCode = inputs.discountCode;
39
+
40
+ // User inputted discount code
41
+ const discountCodes = (
42
+ formDiscountCode ? [formDiscountCode] : []
43
+ ) as string[];
44
+
45
+ // Combine discount codes already applied on cart
46
+ discountCodes.push(...inputs.discountCodes);
47
+
48
+ result = await cart.updateDiscountCodes(discountCodes);
49
+ break;
50
+ }
51
+ case CartForm.ACTIONS.GiftCardCodesUpdate: {
52
+ const formGiftCardCode = inputs.giftCardCode;
53
+
54
+ // User inputted gift card code
55
+ const giftCardCodes = (
56
+ formGiftCardCode ? [formGiftCardCode] : []
57
+ ) as string[];
58
+
59
+ // Combine gift card codes already applied on cart
60
+ giftCardCodes.push(...inputs.giftCardCodes);
61
+
62
+ result = await cart.updateGiftCardCodes(giftCardCodes);
63
+ break;
64
+ }
65
+ case CartForm.ACTIONS.GiftCardCodesRemove: {
66
+ const appliedGiftCardIds = inputs.giftCardCodes as string[];
67
+ result = await cart.removeGiftCardCodes(appliedGiftCardIds);
68
+ break;
69
+ }
70
+ case CartForm.ACTIONS.BuyerIdentityUpdate: {
71
+ result = await cart.updateBuyerIdentity({
72
+ ...inputs.buyerIdentity,
73
+ });
74
+ break;
75
+ }
76
+ default:
77
+ throw new Error(`${action} cart action is not defined`);
78
+ }
79
+
80
+ const cartId = result?.cart?.id;
81
+ const headers = cartId ? cart.setCartId(result.cart.id) : new Headers();
82
+ const {cart: cartResult, errors, warnings} = result;
83
+
84
+ const redirectTo = formData.get('redirectTo') ?? null;
85
+ if (typeof redirectTo === 'string') {
86
+ status = 303;
87
+ headers.set('Location', redirectTo);
88
+ }
89
+
90
+ return data(
91
+ {
92
+ cart: cartResult,
93
+ errors,
94
+ warnings,
95
+ analytics: {
96
+ cartId,
97
+ },
98
+ },
99
+ {status, headers},
100
+ );
101
+ }
102
+
103
+ export async function loader({context}: Route.LoaderArgs) {
104
+ const {cart} = context;
105
+ return await cart.get();
106
+ }
107
+
108
+ export default function Cart() {
109
+ const cart = useLoaderData<typeof loader>();
110
+
111
+ return (
112
+ <div className="cart">
113
+ <h1>Cart</h1>
114
+ <CartMain layout="page" cart={cart} />
115
+ </div>
116
+ );
117
+ }