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.
- package/README.md +212 -0
- package/dist/commands/add.d.ts +7 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +123 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/create.d.ts +8 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +160 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/setup-mcp.d.ts +7 -0
- package/dist/commands/setup-mcp.d.ts.map +1 -0
- package/dist/commands/setup-mcp.js +179 -0
- package/dist/commands/setup-mcp.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/generators.d.ts +6 -0
- package/dist/lib/generators.d.ts.map +1 -0
- package/dist/lib/generators.js +470 -0
- package/dist/lib/generators.js.map +1 -0
- package/dist/lib/utils.d.ts +17 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +101 -0
- package/dist/lib/utils.js.map +1 -0
- package/package.json +54 -0
- package/templates/starter/.env.example +21 -0
- package/templates/starter/.graphqlrc.ts +27 -0
- package/templates/starter/README.md +117 -0
- package/templates/starter/app/assets/favicon.svg +28 -0
- package/templates/starter/app/components/AddToCartButton.tsx +102 -0
- package/templates/starter/app/components/Aside.tsx +136 -0
- package/templates/starter/app/components/CartLineItem.tsx +229 -0
- package/templates/starter/app/components/CartMain.tsx +131 -0
- package/templates/starter/app/components/CartSummary.tsx +315 -0
- package/templates/starter/app/components/CollectionFilters.tsx +330 -0
- package/templates/starter/app/components/CollectionGrid.tsx +141 -0
- package/templates/starter/app/components/Footer.tsx +218 -0
- package/templates/starter/app/components/Header.tsx +296 -0
- package/templates/starter/app/components/PageLayout.tsx +174 -0
- package/templates/starter/app/components/PaginatedResourceSection.tsx +41 -0
- package/templates/starter/app/components/ProductCard.tsx +151 -0
- package/templates/starter/app/components/ProductForm.tsx +156 -0
- package/templates/starter/app/components/ProductGallery.tsx +164 -0
- package/templates/starter/app/components/ProductGrid.tsx +64 -0
- package/templates/starter/app/components/ProductImage.tsx +23 -0
- package/templates/starter/app/components/ProductItem.tsx +44 -0
- package/templates/starter/app/components/ProductPrice.tsx +97 -0
- package/templates/starter/app/components/SearchDialog.tsx +599 -0
- package/templates/starter/app/components/SearchForm.tsx +68 -0
- package/templates/starter/app/components/SearchFormPredictive.tsx +76 -0
- package/templates/starter/app/components/SearchResults.tsx +161 -0
- package/templates/starter/app/components/SearchResultsPredictive.tsx +461 -0
- package/templates/starter/app/entry.client.tsx +21 -0
- package/templates/starter/app/entry.server.tsx +53 -0
- package/templates/starter/app/graphql/customer-account/CustomerAddressMutations.ts +64 -0
- package/templates/starter/app/graphql/customer-account/CustomerDetailsQuery.ts +40 -0
- package/templates/starter/app/graphql/customer-account/CustomerOrderQuery.ts +90 -0
- package/templates/starter/app/graphql/customer-account/CustomerOrdersQuery.ts +63 -0
- package/templates/starter/app/graphql/customer-account/CustomerUpdateMutation.ts +25 -0
- package/templates/starter/app/lib/context.ts +60 -0
- package/templates/starter/app/lib/fragments.ts +234 -0
- package/templates/starter/app/lib/orderFilters.ts +90 -0
- package/templates/starter/app/lib/redirect.ts +23 -0
- package/templates/starter/app/lib/search.ts +79 -0
- package/templates/starter/app/lib/session.ts +72 -0
- package/templates/starter/app/lib/variants.ts +46 -0
- package/templates/starter/app/root.tsx +209 -0
- package/templates/starter/app/routes/$.tsx +11 -0
- package/templates/starter/app/routes/[robots.txt].tsx +117 -0
- package/templates/starter/app/routes/[sitemap.xml].tsx +16 -0
- package/templates/starter/app/routes/_index.tsx +167 -0
- package/templates/starter/app/routes/account.$.tsx +9 -0
- package/templates/starter/app/routes/account._index.tsx +5 -0
- package/templates/starter/app/routes/account.addresses.tsx +516 -0
- package/templates/starter/app/routes/account.orders.$id.tsx +222 -0
- package/templates/starter/app/routes/account.orders._index.tsx +222 -0
- package/templates/starter/app/routes/account.profile.tsx +133 -0
- package/templates/starter/app/routes/account.tsx +97 -0
- package/templates/starter/app/routes/account_.authorize.tsx +5 -0
- package/templates/starter/app/routes/account_.login.tsx +7 -0
- package/templates/starter/app/routes/account_.logout.tsx +11 -0
- package/templates/starter/app/routes/api.$version.[graphql.json].tsx +14 -0
- package/templates/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +129 -0
- package/templates/starter/app/routes/blogs.$blogHandle._index.tsx +175 -0
- package/templates/starter/app/routes/blogs._index.tsx +109 -0
- package/templates/starter/app/routes/cart.$lines.tsx +70 -0
- package/templates/starter/app/routes/cart.tsx +117 -0
- package/templates/starter/app/routes/collections.$handle.tsx +161 -0
- package/templates/starter/app/routes/collections._index.tsx +133 -0
- package/templates/starter/app/routes/collections.all.tsx +122 -0
- package/templates/starter/app/routes/discount.$code.tsx +48 -0
- package/templates/starter/app/routes/pages.$handle.tsx +88 -0
- package/templates/starter/app/routes/policies.$handle.tsx +93 -0
- package/templates/starter/app/routes/policies._index.tsx +69 -0
- package/templates/starter/app/routes/products.$handle.tsx +232 -0
- package/templates/starter/app/routes/search.tsx +426 -0
- package/templates/starter/app/routes/sitemap.$type.$page[.xml].tsx +23 -0
- package/templates/starter/app/routes.ts +9 -0
- package/templates/starter/app/styles/app.css +574 -0
- package/templates/starter/app/styles/reset.css +139 -0
- package/templates/starter/app/styles/tailwind.css +116 -0
- package/templates/starter/customer-accountapi.generated.d.ts +543 -0
- package/templates/starter/env.d.ts +7 -0
- package/templates/starter/eslint.config.js +247 -0
- package/templates/starter/guides/predictiveSearch/predictiveSearch.jpg +0 -0
- package/templates/starter/guides/predictiveSearch/predictiveSearch.md +394 -0
- package/templates/starter/guides/search/search.jpg +0 -0
- package/templates/starter/guides/search/search.md +335 -0
- package/templates/starter/package.json +71 -0
- package/templates/starter/postcss.config.js +6 -0
- package/templates/starter/public/.gitkeep +0 -0
- package/templates/starter/react-router.config.ts +13 -0
- package/templates/starter/server.ts +59 -0
- package/templates/starter/storefrontapi.generated.d.ts +1264 -0
- package/templates/starter/tailwind.config.js +83 -0
- package/templates/starter/tsconfig.json +67 -0
- package/templates/starter/vite.config.ts +32 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {useOptimisticCart} from '@shopify/hydrogen';
|
|
2
|
+
import {Link} from 'react-router';
|
|
3
|
+
import type {CartApiQueryFragment} from 'storefrontapi.generated';
|
|
4
|
+
import {useAside} from '~/components/Aside';
|
|
5
|
+
import {CartLineItem} from '~/components/CartLineItem';
|
|
6
|
+
import {CartSummary} from './CartSummary';
|
|
7
|
+
|
|
8
|
+
export type CartLayout = 'page' | 'aside';
|
|
9
|
+
|
|
10
|
+
export type CartMainProps = {
|
|
11
|
+
cart: CartApiQueryFragment | null;
|
|
12
|
+
layout: CartLayout;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The main cart component that displays the cart items and summary.
|
|
17
|
+
* It is used by both the /cart route and the cart aside dialog.
|
|
18
|
+
*/
|
|
19
|
+
export function CartMain({layout, cart: originalCart}: CartMainProps) {
|
|
20
|
+
// The useOptimisticCart hook applies pending actions to the cart
|
|
21
|
+
// so the user immediately sees feedback when they modify the cart.
|
|
22
|
+
const cart = useOptimisticCart(originalCart);
|
|
23
|
+
|
|
24
|
+
const linesCount = Boolean(cart?.lines?.nodes?.length || 0);
|
|
25
|
+
const cartHasItems = cart?.totalQuantity ? cart.totalQuantity > 0 : false;
|
|
26
|
+
|
|
27
|
+
const containerClasses =
|
|
28
|
+
layout === 'page' ? 'mx-auto max-w-4xl px-4 py-8' : 'flex h-full flex-col';
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className={containerClasses}>
|
|
32
|
+
<CartEmpty hidden={linesCount} layout={layout} />
|
|
33
|
+
|
|
34
|
+
{linesCount && (
|
|
35
|
+
<div
|
|
36
|
+
className={
|
|
37
|
+
layout === 'aside' ? 'flex flex-1 flex-col overflow-hidden' : ''
|
|
38
|
+
}
|
|
39
|
+
>
|
|
40
|
+
{/* Cart items list */}
|
|
41
|
+
<div
|
|
42
|
+
className={layout === 'aside' ? 'flex-1 overflow-y-auto' : 'mb-8'}
|
|
43
|
+
aria-labelledby="cart-lines"
|
|
44
|
+
>
|
|
45
|
+
<ul className="divide-y divide-secondary-200">
|
|
46
|
+
{(cart?.lines?.nodes ?? []).map((line) => (
|
|
47
|
+
<CartLineItem key={line.id} line={line} layout={layout} />
|
|
48
|
+
))}
|
|
49
|
+
</ul>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{/* Cart summary - sticky at bottom for aside layout */}
|
|
53
|
+
{cartHasItems && (
|
|
54
|
+
<div
|
|
55
|
+
className={
|
|
56
|
+
layout === 'aside'
|
|
57
|
+
? 'border-t border-secondary-200 bg-white pt-4'
|
|
58
|
+
: 'rounded-lg border border-secondary-200 bg-secondary-50 p-6'
|
|
59
|
+
}
|
|
60
|
+
>
|
|
61
|
+
<CartSummary cart={cart} layout={layout} />
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function CartEmpty({
|
|
71
|
+
hidden = false,
|
|
72
|
+
layout,
|
|
73
|
+
}: {
|
|
74
|
+
hidden: boolean;
|
|
75
|
+
layout?: CartMainProps['layout'];
|
|
76
|
+
}) {
|
|
77
|
+
const {close} = useAside();
|
|
78
|
+
|
|
79
|
+
if (hidden) return null;
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="flex flex-1 flex-col items-center justify-center py-12 text-center">
|
|
83
|
+
{/* Empty cart icon */}
|
|
84
|
+
<div className="mb-6 rounded-full bg-secondary-100 p-6">
|
|
85
|
+
<svg
|
|
86
|
+
className="h-12 w-12 text-secondary-400"
|
|
87
|
+
fill="none"
|
|
88
|
+
stroke="currentColor"
|
|
89
|
+
viewBox="0 0 24 24"
|
|
90
|
+
>
|
|
91
|
+
<path
|
|
92
|
+
strokeLinecap="round"
|
|
93
|
+
strokeLinejoin="round"
|
|
94
|
+
strokeWidth={1.5}
|
|
95
|
+
d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
|
|
96
|
+
/>
|
|
97
|
+
</svg>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<h3 className="mb-2 text-lg font-semibold text-secondary-900">
|
|
101
|
+
Your cart is empty
|
|
102
|
+
</h3>
|
|
103
|
+
<p className="mb-6 max-w-sm text-secondary-600">
|
|
104
|
+
Looks like you haven’t added anything yet. Let’s get you
|
|
105
|
+
started!
|
|
106
|
+
</p>
|
|
107
|
+
|
|
108
|
+
<Link
|
|
109
|
+
to="/collections"
|
|
110
|
+
onClick={layout === 'aside' ? close : undefined}
|
|
111
|
+
prefetch="viewport"
|
|
112
|
+
className="inline-flex items-center justify-center rounded-md bg-primary-600 px-6 py-3 text-sm font-medium text-white shadow-sm transition-colors hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
|
113
|
+
>
|
|
114
|
+
Continue shopping
|
|
115
|
+
<svg
|
|
116
|
+
className="ml-2 h-4 w-4"
|
|
117
|
+
fill="none"
|
|
118
|
+
stroke="currentColor"
|
|
119
|
+
viewBox="0 0 24 24"
|
|
120
|
+
>
|
|
121
|
+
<path
|
|
122
|
+
strokeLinecap="round"
|
|
123
|
+
strokeLinejoin="round"
|
|
124
|
+
strokeWidth={2}
|
|
125
|
+
d="M14 5l7 7m0 0l-7 7m7-7H3"
|
|
126
|
+
/>
|
|
127
|
+
</svg>
|
|
128
|
+
</Link>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import type {CartApiQueryFragment} from 'storefrontapi.generated';
|
|
2
|
+
import type {CartLayout} from '~/components/CartMain';
|
|
3
|
+
import {CartForm, Money, type OptimisticCart} from '@shopify/hydrogen';
|
|
4
|
+
import {useEffect, useRef} from 'react';
|
|
5
|
+
import {useFetcher} from 'react-router';
|
|
6
|
+
import type {FetcherWithComponents} from 'react-router';
|
|
7
|
+
|
|
8
|
+
type CartSummaryProps = {
|
|
9
|
+
cart: OptimisticCart<CartApiQueryFragment | null>;
|
|
10
|
+
layout: CartLayout;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function CartSummary({cart, layout}: CartSummaryProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div aria-labelledby="cart-summary" className="space-y-4">
|
|
16
|
+
{/* Subtotal */}
|
|
17
|
+
<div className="flex items-center justify-between">
|
|
18
|
+
<span className="text-sm text-secondary-600">Subtotal</span>
|
|
19
|
+
<span className="text-base font-medium text-secondary-900">
|
|
20
|
+
{cart?.cost?.subtotalAmount?.amount ? (
|
|
21
|
+
<Money data={cart?.cost?.subtotalAmount} />
|
|
22
|
+
) : (
|
|
23
|
+
'-'
|
|
24
|
+
)}
|
|
25
|
+
</span>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
{/* Discounts & Gift Cards */}
|
|
29
|
+
<CartDiscounts discountCodes={cart?.discountCodes} />
|
|
30
|
+
<CartGiftCard giftCardCodes={cart?.appliedGiftCards} />
|
|
31
|
+
|
|
32
|
+
{/* Shipping notice */}
|
|
33
|
+
<p className="text-xs text-secondary-500">
|
|
34
|
+
Shipping and taxes calculated at checkout
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
{/* Checkout button */}
|
|
38
|
+
<CartCheckoutActions checkoutUrl={cart?.checkoutUrl} layout={layout} />
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function CartCheckoutActions({
|
|
44
|
+
checkoutUrl,
|
|
45
|
+
layout,
|
|
46
|
+
}: {
|
|
47
|
+
checkoutUrl?: string;
|
|
48
|
+
layout: CartLayout;
|
|
49
|
+
}) {
|
|
50
|
+
if (!checkoutUrl) return null;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="space-y-3">
|
|
54
|
+
<a
|
|
55
|
+
href={checkoutUrl}
|
|
56
|
+
target="_self"
|
|
57
|
+
className="flex w-full items-center justify-center rounded-md bg-primary-600 px-6 py-3 text-sm font-medium text-white shadow-sm transition-colors hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
|
58
|
+
>
|
|
59
|
+
Continue to Checkout
|
|
60
|
+
<svg
|
|
61
|
+
className="ml-2 h-4 w-4"
|
|
62
|
+
fill="none"
|
|
63
|
+
stroke="currentColor"
|
|
64
|
+
viewBox="0 0 24 24"
|
|
65
|
+
>
|
|
66
|
+
<path
|
|
67
|
+
strokeLinecap="round"
|
|
68
|
+
strokeLinejoin="round"
|
|
69
|
+
strokeWidth={2}
|
|
70
|
+
d="M17 8l4 4m0 0l-4 4m4-4H3"
|
|
71
|
+
/>
|
|
72
|
+
</svg>
|
|
73
|
+
</a>
|
|
74
|
+
|
|
75
|
+
{layout === 'aside' && (
|
|
76
|
+
<a
|
|
77
|
+
href="/cart"
|
|
78
|
+
className="flex w-full items-center justify-center rounded-md border border-secondary-300 bg-white px-6 py-3 text-sm font-medium text-secondary-700 transition-colors hover:bg-secondary-50"
|
|
79
|
+
>
|
|
80
|
+
View Cart
|
|
81
|
+
</a>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function CartDiscounts({
|
|
88
|
+
discountCodes,
|
|
89
|
+
}: {
|
|
90
|
+
discountCodes?: CartApiQueryFragment['discountCodes'];
|
|
91
|
+
}) {
|
|
92
|
+
const codes: string[] =
|
|
93
|
+
discountCodes
|
|
94
|
+
?.filter((discount) => discount.applicable)
|
|
95
|
+
?.map(({code}) => code) || [];
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className="space-y-3">
|
|
99
|
+
{/* Display applied discount codes */}
|
|
100
|
+
{codes.length > 0 && (
|
|
101
|
+
<div className="flex items-center justify-between rounded-md bg-green-50 px-3 py-2">
|
|
102
|
+
<div className="flex items-center gap-2">
|
|
103
|
+
<svg
|
|
104
|
+
className="h-4 w-4 text-green-600"
|
|
105
|
+
fill="none"
|
|
106
|
+
stroke="currentColor"
|
|
107
|
+
viewBox="0 0 24 24"
|
|
108
|
+
>
|
|
109
|
+
<path
|
|
110
|
+
strokeLinecap="round"
|
|
111
|
+
strokeLinejoin="round"
|
|
112
|
+
strokeWidth={2}
|
|
113
|
+
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
|
|
114
|
+
/>
|
|
115
|
+
</svg>
|
|
116
|
+
<span className="text-sm font-medium text-green-700">
|
|
117
|
+
{codes.join(', ')}
|
|
118
|
+
</span>
|
|
119
|
+
</div>
|
|
120
|
+
<UpdateDiscountForm>
|
|
121
|
+
<button
|
|
122
|
+
type="submit"
|
|
123
|
+
className="text-xs font-medium text-green-600 hover:text-green-800"
|
|
124
|
+
>
|
|
125
|
+
Remove
|
|
126
|
+
</button>
|
|
127
|
+
</UpdateDiscountForm>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{/* Discount code input */}
|
|
132
|
+
<UpdateDiscountForm discountCodes={codes}>
|
|
133
|
+
<div className="flex gap-2">
|
|
134
|
+
<input
|
|
135
|
+
type="text"
|
|
136
|
+
name="discountCode"
|
|
137
|
+
placeholder="Discount code"
|
|
138
|
+
className="flex-1 rounded-md border border-secondary-300 px-3 py-2 text-sm placeholder:text-secondary-400 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
|
139
|
+
/>
|
|
140
|
+
<button
|
|
141
|
+
type="submit"
|
|
142
|
+
className="rounded-md bg-secondary-100 px-4 py-2 text-sm font-medium text-secondary-700 transition-colors hover:bg-secondary-200"
|
|
143
|
+
>
|
|
144
|
+
Apply
|
|
145
|
+
</button>
|
|
146
|
+
</div>
|
|
147
|
+
</UpdateDiscountForm>
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function UpdateDiscountForm({
|
|
153
|
+
discountCodes,
|
|
154
|
+
children,
|
|
155
|
+
}: {
|
|
156
|
+
discountCodes?: string[];
|
|
157
|
+
children: React.ReactNode;
|
|
158
|
+
}) {
|
|
159
|
+
return (
|
|
160
|
+
<CartForm
|
|
161
|
+
route="/cart"
|
|
162
|
+
action={CartForm.ACTIONS.DiscountCodesUpdate}
|
|
163
|
+
inputs={{
|
|
164
|
+
discountCodes: discountCodes || [],
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
{children}
|
|
168
|
+
</CartForm>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function CartGiftCard({
|
|
173
|
+
giftCardCodes,
|
|
174
|
+
}: {
|
|
175
|
+
giftCardCodes: CartApiQueryFragment['appliedGiftCards'] | undefined;
|
|
176
|
+
}) {
|
|
177
|
+
const appliedGiftCardCodes = useRef<string[]>([]);
|
|
178
|
+
const giftCardCodeInput = useRef<HTMLInputElement>(null);
|
|
179
|
+
const giftCardAddFetcher = useFetcher({key: 'gift-card-add'});
|
|
180
|
+
|
|
181
|
+
// Clear the gift card code input after the gift card is added
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (giftCardAddFetcher.data) {
|
|
184
|
+
giftCardCodeInput.current!.value = '';
|
|
185
|
+
}
|
|
186
|
+
}, [giftCardAddFetcher.data]);
|
|
187
|
+
|
|
188
|
+
function saveAppliedCode(code: string) {
|
|
189
|
+
const formattedCode = code.replace(/\s/g, ''); // Remove spaces
|
|
190
|
+
if (!appliedGiftCardCodes.current.includes(formattedCode)) {
|
|
191
|
+
appliedGiftCardCodes.current.push(formattedCode);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<div className="space-y-3">
|
|
197
|
+
{/* Display applied gift cards */}
|
|
198
|
+
{giftCardCodes && giftCardCodes.length > 0 && (
|
|
199
|
+
<div className="space-y-2">
|
|
200
|
+
<span className="text-xs font-medium text-secondary-500 uppercase tracking-wide">
|
|
201
|
+
Applied Gift Cards
|
|
202
|
+
</span>
|
|
203
|
+
{giftCardCodes.map((giftCard) => (
|
|
204
|
+
<RemoveGiftCardForm key={giftCard.id} giftCardId={giftCard.id}>
|
|
205
|
+
<div className="flex items-center justify-between rounded-md bg-purple-50 px-3 py-2">
|
|
206
|
+
<div className="flex items-center gap-2">
|
|
207
|
+
<svg
|
|
208
|
+
className="h-4 w-4 text-purple-600"
|
|
209
|
+
fill="none"
|
|
210
|
+
stroke="currentColor"
|
|
211
|
+
viewBox="0 0 24 24"
|
|
212
|
+
>
|
|
213
|
+
<path
|
|
214
|
+
strokeLinecap="round"
|
|
215
|
+
strokeLinejoin="round"
|
|
216
|
+
strokeWidth={2}
|
|
217
|
+
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
218
|
+
/>
|
|
219
|
+
</svg>
|
|
220
|
+
<span className="text-sm font-medium text-purple-700">
|
|
221
|
+
***{giftCard.lastCharacters}
|
|
222
|
+
</span>
|
|
223
|
+
<span className="text-sm text-purple-600">
|
|
224
|
+
(<Money data={giftCard.amountUsed} />)
|
|
225
|
+
</span>
|
|
226
|
+
</div>
|
|
227
|
+
<button
|
|
228
|
+
type="submit"
|
|
229
|
+
className="text-xs font-medium text-purple-600 hover:text-purple-800"
|
|
230
|
+
>
|
|
231
|
+
Remove
|
|
232
|
+
</button>
|
|
233
|
+
</div>
|
|
234
|
+
</RemoveGiftCardForm>
|
|
235
|
+
))}
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{/* Gift card input */}
|
|
240
|
+
<UpdateGiftCardForm
|
|
241
|
+
giftCardCodes={appliedGiftCardCodes.current}
|
|
242
|
+
saveAppliedCode={saveAppliedCode}
|
|
243
|
+
fetcherKey="gift-card-add"
|
|
244
|
+
>
|
|
245
|
+
<div className="flex gap-2">
|
|
246
|
+
<input
|
|
247
|
+
type="text"
|
|
248
|
+
name="giftCardCode"
|
|
249
|
+
placeholder="Gift card code"
|
|
250
|
+
ref={giftCardCodeInput}
|
|
251
|
+
className="flex-1 rounded-md border border-secondary-300 px-3 py-2 text-sm placeholder:text-secondary-400 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
|
252
|
+
/>
|
|
253
|
+
<button
|
|
254
|
+
type="submit"
|
|
255
|
+
disabled={giftCardAddFetcher.state !== 'idle'}
|
|
256
|
+
className="rounded-md bg-secondary-100 px-4 py-2 text-sm font-medium text-secondary-700 transition-colors hover:bg-secondary-200 disabled:cursor-not-allowed disabled:opacity-50"
|
|
257
|
+
>
|
|
258
|
+
Apply
|
|
259
|
+
</button>
|
|
260
|
+
</div>
|
|
261
|
+
</UpdateGiftCardForm>
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function UpdateGiftCardForm({
|
|
267
|
+
giftCardCodes,
|
|
268
|
+
saveAppliedCode,
|
|
269
|
+
fetcherKey,
|
|
270
|
+
children,
|
|
271
|
+
}: {
|
|
272
|
+
giftCardCodes?: string[];
|
|
273
|
+
saveAppliedCode?: (code: string) => void;
|
|
274
|
+
fetcherKey?: string;
|
|
275
|
+
children: React.ReactNode;
|
|
276
|
+
}) {
|
|
277
|
+
return (
|
|
278
|
+
<CartForm
|
|
279
|
+
fetcherKey={fetcherKey}
|
|
280
|
+
route="/cart"
|
|
281
|
+
action={CartForm.ACTIONS.GiftCardCodesUpdate}
|
|
282
|
+
inputs={{
|
|
283
|
+
giftCardCodes: giftCardCodes || [],
|
|
284
|
+
}}
|
|
285
|
+
>
|
|
286
|
+
{(fetcher: FetcherWithComponents<any>) => {
|
|
287
|
+
const code = fetcher.formData?.get('giftCardCode');
|
|
288
|
+
if (code && saveAppliedCode) {
|
|
289
|
+
saveAppliedCode(code as string);
|
|
290
|
+
}
|
|
291
|
+
return children;
|
|
292
|
+
}}
|
|
293
|
+
</CartForm>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function RemoveGiftCardForm({
|
|
298
|
+
giftCardId,
|
|
299
|
+
children,
|
|
300
|
+
}: {
|
|
301
|
+
giftCardId: string;
|
|
302
|
+
children: React.ReactNode;
|
|
303
|
+
}) {
|
|
304
|
+
return (
|
|
305
|
+
<CartForm
|
|
306
|
+
route="/cart"
|
|
307
|
+
action={CartForm.ACTIONS.GiftCardCodesRemove}
|
|
308
|
+
inputs={{
|
|
309
|
+
giftCardCodes: [giftCardId],
|
|
310
|
+
}}
|
|
311
|
+
>
|
|
312
|
+
{children}
|
|
313
|
+
</CartForm>
|
|
314
|
+
);
|
|
315
|
+
}
|