astro-tractstack 2.2.10 → 2.3.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/bin/create-tractstack.js +2 -2
- package/dist/index.js +177 -18
- package/package.json +4 -2
- package/templates/custom/minimal/CodeHook.astro +22 -5
- package/templates/custom/shopify/Cart.tsx +372 -0
- package/templates/custom/shopify/CartIcon.tsx +47 -0
- package/templates/custom/shopify/CartModal.tsx +63 -0
- package/templates/custom/shopify/CheckoutModal.tsx +576 -0
- package/templates/custom/shopify/NativeBookingCalendar.tsx +375 -0
- package/templates/custom/shopify/ShopifyCartManager.tsx +200 -0
- package/templates/custom/shopify/ShopifyCheckout.tsx +167 -0
- package/templates/custom/shopify/ShopifyProductGrid.tsx +247 -0
- package/templates/custom/shopify/ShopifyServiceList.tsx +135 -0
- package/templates/custom/shopify/cart.astro +23 -0
- package/templates/custom/with-examples/CodeHook.astro +17 -1
- package/templates/custom/with-examples/ProductGrid.astro +1 -1
- package/templates/src/client/app.js +4 -2
- package/templates/src/components/Footer.astro +4 -4
- package/templates/src/components/Header.astro +44 -12
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +3 -3
- package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +2 -2
- package/templates/src/components/edit/pane/steps/AiCreativeDesignStep.tsx +2 -2
- package/templates/src/components/edit/pane/steps/AiLibraryCopyStep.tsx +3 -3
- package/templates/src/components/edit/pane/steps/AiRefineDesignStep.tsx +2 -2
- package/templates/src/components/edit/pane/steps/AiStandardDesignStep.tsx +7 -7
- package/templates/src/components/form/advanced/APIConfigSection.tsx +407 -38
- package/templates/src/components/form/shopify/SchedulingSection.tsx +354 -0
- package/templates/src/components/storykeep/Dashboard.tsx +18 -4
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -0
- package/templates/src/components/storykeep/Dashboard_Content.tsx +5 -96
- package/templates/src/components/storykeep/Dashboard_Shopify.tsx +668 -0
- package/templates/src/components/storykeep/StoryKeepBackdrop.astro +43 -23
- package/templates/src/components/storykeep/controls/content/BeliefTable.tsx +14 -5
- package/templates/src/components/storykeep/controls/content/ContentBrowser.tsx +0 -14
- package/templates/src/components/storykeep/controls/content/KnownResourceForm.tsx +36 -13
- package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +5 -2
- package/templates/src/components/storykeep/controls/content/ManageContent.tsx +4 -11
- package/templates/src/components/storykeep/controls/content/MenuTable.tsx +14 -5
- package/templates/src/components/storykeep/controls/content/ProductTable.tsx +333 -0
- package/templates/src/components/storykeep/controls/content/ResourceBulkIngest.tsx +9 -5
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +108 -8
- package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +13 -4
- package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +14 -5
- package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +111 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +393 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Products.tsx +46 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Schedule.tsx +78 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +55 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Services.tsx +47 -0
- package/templates/src/lib/resources.ts +11 -21
- package/templates/src/pages/api/auth/lookup-lead.ts +72 -0
- package/templates/src/pages/api/booking/availability.ts +72 -0
- package/templates/src/pages/api/booking/cancel.ts +73 -0
- package/templates/src/pages/api/booking/confirm.ts +82 -0
- package/templates/src/pages/api/booking/hold.ts +75 -0
- package/templates/src/pages/api/booking/list.ts +66 -0
- package/templates/src/pages/api/booking/metrics.ts +60 -0
- package/templates/src/pages/api/booking/release.ts +76 -0
- package/templates/src/pages/api/sandbox.ts +2 -2
- package/templates/src/pages/api/shopify/createCart.ts +69 -0
- package/templates/src/pages/api/shopify/getProducts.ts +64 -0
- package/templates/src/pages/storykeep/login.astro +26 -24
- package/templates/src/pages/storykeep/logout.astro +1 -10
- package/templates/src/pages/storykeep/manage.astro +69 -0
- package/templates/src/pages/storykeep/{content.astro → pages.astro} +4 -8
- package/templates/src/pages/storykeep/shopify.astro +101 -0
- package/templates/src/stores/navigation.ts +3 -42
- package/templates/src/stores/nodes.ts +3 -1
- package/templates/src/stores/resources.ts +7 -10
- package/templates/src/stores/shopify.ts +266 -0
- package/templates/src/types/tractstack.ts +75 -0
- package/templates/src/utils/api/advancedConfig.ts +7 -1
- package/templates/src/utils/api/advancedHelpers.ts +87 -7
- package/templates/src/utils/api/bookingHelpers.ts +125 -0
- package/templates/src/utils/api/brandHelpers.ts +14 -0
- package/templates/src/utils/api/resourceConfig.ts +13 -5
- package/templates/src/utils/auth.ts +29 -9
- package/templates/src/utils/compositor/aiGeneration.ts +3 -3
- package/templates/src/utils/compositor/aiPaneParser.ts +2 -2
- package/templates/src/utils/customHelpers.ts +49 -0
- package/templates/src/utils/helpers.ts +59 -0
- package/templates/src/utils/profileStorage.ts +5 -0
- package/templates/src/utils/tenantResolver.ts +2 -1
- package/utils/inject-files.ts +161 -2
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { ulid } from 'ulid';
|
|
3
|
+
import { useStore } from '@nanostores/react';
|
|
4
|
+
import {
|
|
5
|
+
addQueue,
|
|
6
|
+
cartStore,
|
|
7
|
+
cartState,
|
|
8
|
+
CART_STATES,
|
|
9
|
+
isShopifyHandoff,
|
|
10
|
+
transactionTraceId,
|
|
11
|
+
type CartItemState,
|
|
12
|
+
} from '@/stores/shopify';
|
|
13
|
+
import { getShopifyImage } from '@/utils/helpers';
|
|
14
|
+
import type { ResourceNode } from '@/types/compositorTypes';
|
|
15
|
+
|
|
16
|
+
interface CartProps {
|
|
17
|
+
resources: ResourceNode[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const getCleanVariantTitle = (variant: any) => {
|
|
21
|
+
if (variant?.selectedOptions) {
|
|
22
|
+
const filtered = variant.selectedOptions
|
|
23
|
+
.filter(
|
|
24
|
+
(o: any) =>
|
|
25
|
+
o.name !== 'Mode' && o.name !== 'Title' && o.value !== 'Default Title'
|
|
26
|
+
)
|
|
27
|
+
.map((o: any) => o.value)
|
|
28
|
+
.join(' / ');
|
|
29
|
+
|
|
30
|
+
return filtered === 'Default Title' ? '' : filtered;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const title = variant?.title || '';
|
|
34
|
+
return title === 'Default Title' ? '' : title;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default function Cart({ resources = [] }: CartProps) {
|
|
38
|
+
const cart = useStore(cartStore);
|
|
39
|
+
const isHandoff = useStore(isShopifyHandoff);
|
|
40
|
+
const [pickupEnabled, setPickupEnabled] = useState(false);
|
|
41
|
+
|
|
42
|
+
const cartValues = Object.values(cart);
|
|
43
|
+
|
|
44
|
+
const boundServiceIds = new Set(
|
|
45
|
+
cartValues
|
|
46
|
+
.map((item) => item.boundResourceId)
|
|
47
|
+
.filter((id) => !!id) as string[]
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const displayableItems = cartValues.filter(
|
|
51
|
+
(item) => !boundServiceIds.has(item.resourceId)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const groupedItems = displayableItems.reduce(
|
|
55
|
+
(acc, item) => {
|
|
56
|
+
if (!acc[item.resourceId]) {
|
|
57
|
+
acc[item.resourceId] = [];
|
|
58
|
+
}
|
|
59
|
+
acc[item.resourceId].push(item);
|
|
60
|
+
return acc;
|
|
61
|
+
},
|
|
62
|
+
{} as Record<string, CartItemState[]>
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const hasService = cartValues.some((item) => {
|
|
66
|
+
const resource = resources.find((r) => r.id === item.resourceId);
|
|
67
|
+
return !!resource?.optionsPayload?.bookingLengthMinutes;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const hasPhysicalProductWithPickup = cartValues.some(
|
|
71
|
+
(item) =>
|
|
72
|
+
item.variantIdPickup && item.variantIdPickup !== item.variantIdShipped
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const canPickup = hasService && hasPhysicalProductWithPickup;
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (canPickup) {
|
|
79
|
+
setPickupEnabled(true);
|
|
80
|
+
} else {
|
|
81
|
+
setPickupEnabled(false);
|
|
82
|
+
}
|
|
83
|
+
}, [canPickup]);
|
|
84
|
+
|
|
85
|
+
const isPickupMode = canPickup && pickupEnabled;
|
|
86
|
+
|
|
87
|
+
const dispatchAction = (item: CartItemState, action: 'add' | 'remove') => {
|
|
88
|
+
addQueue.set([
|
|
89
|
+
...addQueue.get(),
|
|
90
|
+
{
|
|
91
|
+
resourceId: item.resourceId,
|
|
92
|
+
action,
|
|
93
|
+
variantId: item.variantId,
|
|
94
|
+
variantIdShipped: item.variantIdShipped,
|
|
95
|
+
variantIdPickup: item.variantIdPickup,
|
|
96
|
+
boundResourceId: item.boundResourceId,
|
|
97
|
+
suppressModal: action === 'add' ? true : undefined,
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (isHandoff) {
|
|
103
|
+
return (
|
|
104
|
+
<div
|
|
105
|
+
className="fixed inset-0 flex flex-col items-center justify-center bg-black bg-opacity-75 backdrop-blur-md"
|
|
106
|
+
style={{ zIndex: 9005 }}
|
|
107
|
+
>
|
|
108
|
+
<div className="h-12 w-12 animate-spin rounded-full border-4 border-gray-200 border-t-white"></div>
|
|
109
|
+
<h3 className="mt-4 text-lg font-bold text-white">
|
|
110
|
+
Finalizing Handoff...
|
|
111
|
+
</h3>
|
|
112
|
+
<p className="mt-2 text-sm text-gray-300">
|
|
113
|
+
Redirecting to Shopify secured payment
|
|
114
|
+
</p>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (cartValues.length === 0) {
|
|
120
|
+
return (
|
|
121
|
+
<div className="relative">
|
|
122
|
+
<div className="rounded-lg border bg-gray-50 p-8 text-center">
|
|
123
|
+
<h2 className="text-xl font-bold">Your cart is empty</h2>
|
|
124
|
+
<p className="mt-2 text-gray-600">Add some items to get started.</p>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div className="rounded-lg bg-white shadow">
|
|
132
|
+
<div className="flex items-center justify-between border-b border-gray-200 px-6 py-4">
|
|
133
|
+
<h2 className="text-xl font-bold text-gray-800">Shopping Cart</h2>
|
|
134
|
+
{canPickup && (
|
|
135
|
+
<label className="flex items-center space-x-2 text-sm font-bold text-gray-900">
|
|
136
|
+
<input
|
|
137
|
+
type="checkbox"
|
|
138
|
+
checked={pickupEnabled}
|
|
139
|
+
onChange={(e) => setPickupEnabled(e.target.checked)}
|
|
140
|
+
className="h-4 w-4 rounded border-gray-300 text-black focus:ring-black"
|
|
141
|
+
/>
|
|
142
|
+
<span>Pick up at Store</span>
|
|
143
|
+
</label>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<ul className="divide-y divide-gray-200">
|
|
148
|
+
{Object.keys(groupedItems).map((resourceId) => {
|
|
149
|
+
const items = groupedItems[resourceId];
|
|
150
|
+
const resource = resources.find((r) => r.id === resourceId);
|
|
151
|
+
if (!resource || items.length === 0) return null;
|
|
152
|
+
|
|
153
|
+
const isService = !!resource.optionsPayload?.bookingLengthMinutes;
|
|
154
|
+
const serviceDuration = resource.optionsPayload?.bookingLengthMinutes;
|
|
155
|
+
|
|
156
|
+
const firstItem = items[0];
|
|
157
|
+
const boundServiceId = firstItem.boundResourceId;
|
|
158
|
+
const boundServiceResource = boundServiceId
|
|
159
|
+
? resources.find((r) => r.id === boundServiceId)
|
|
160
|
+
: null;
|
|
161
|
+
|
|
162
|
+
const activeVariantIdFirst = isPickupMode
|
|
163
|
+
? firstItem.variantIdPickup
|
|
164
|
+
: firstItem.variantIdShipped;
|
|
165
|
+
const displayIdFirst =
|
|
166
|
+
activeVariantIdFirst ||
|
|
167
|
+
firstItem.variantIdPickup ||
|
|
168
|
+
firstItem.variantId;
|
|
169
|
+
|
|
170
|
+
const { src, srcSet } = getShopifyImage(
|
|
171
|
+
resource,
|
|
172
|
+
'600',
|
|
173
|
+
displayIdFirst
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
let productData: any = {};
|
|
177
|
+
try {
|
|
178
|
+
if (resource.optionsPayload?.shopifyData) {
|
|
179
|
+
productData = JSON.parse(resource.optionsPayload.shopifyData);
|
|
180
|
+
}
|
|
181
|
+
} catch (e) {
|
|
182
|
+
console.error('Failed to parse Shopify data', resource.id);
|
|
183
|
+
}
|
|
184
|
+
const variants = productData?.variants || [];
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<li key={resourceId} className="p-6">
|
|
188
|
+
<div className="flex items-start">
|
|
189
|
+
{!isService && (
|
|
190
|
+
<div className="h-16 w-16 flex-shrink-0 overflow-hidden rounded-md border border-gray-200">
|
|
191
|
+
<img
|
|
192
|
+
src={src}
|
|
193
|
+
srcSet={srcSet}
|
|
194
|
+
alt={resource.title}
|
|
195
|
+
className="aspect-square h-full w-full object-cover object-center"
|
|
196
|
+
loading="lazy"
|
|
197
|
+
/>
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
<div className="ml-4 flex-1">
|
|
201
|
+
<div className="flex justify-between">
|
|
202
|
+
<div>
|
|
203
|
+
<div className="flex items-center gap-2">
|
|
204
|
+
<h3 className="text-base font-bold text-gray-900">
|
|
205
|
+
{resource.title}
|
|
206
|
+
</h3>
|
|
207
|
+
{isService && (
|
|
208
|
+
<span className="inline-flex items-center rounded-sm bg-blue-50 px-2 py-0.5 text-xs font-bold text-blue-700">
|
|
209
|
+
{serviceDuration} mins
|
|
210
|
+
</span>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
{boundServiceResource && (
|
|
215
|
+
<div className="mt-2 flex items-center gap-2 rounded-md bg-blue-50 px-3 py-2">
|
|
216
|
+
<span className="inline-block h-2 w-2 rounded-full bg-blue-500"></span>
|
|
217
|
+
<div>
|
|
218
|
+
<p className="text-sm font-bold text-blue-900">
|
|
219
|
+
Includes Booking: {boundServiceResource.title}
|
|
220
|
+
</p>
|
|
221
|
+
<p className="text-xs text-blue-700">
|
|
222
|
+
Duration:{' '}
|
|
223
|
+
{boundServiceResource.optionsPayload
|
|
224
|
+
?.bookingLengthMinutes || 0}{' '}
|
|
225
|
+
mins
|
|
226
|
+
</p>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
231
|
+
{resource.oneliner}
|
|
232
|
+
</p>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<div className="mt-4 space-y-4 border-t border-gray-100 pt-4">
|
|
237
|
+
{items.map((item, idx) => {
|
|
238
|
+
const activeVariantId = isPickupMode
|
|
239
|
+
? item.variantIdPickup
|
|
240
|
+
: item.variantIdShipped;
|
|
241
|
+
|
|
242
|
+
const displayId =
|
|
243
|
+
activeVariantId ||
|
|
244
|
+
item.variantIdPickup ||
|
|
245
|
+
item.variantId;
|
|
246
|
+
|
|
247
|
+
let price = '0.00';
|
|
248
|
+
let currency = 'USD';
|
|
249
|
+
let variantTitle = '';
|
|
250
|
+
|
|
251
|
+
const variant = variants.find(
|
|
252
|
+
(v: any) => v.id === displayId
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
if (variant) {
|
|
256
|
+
price = variant.price?.amount || '0.00';
|
|
257
|
+
currency = variant.price?.currencyCode || 'USD';
|
|
258
|
+
variantTitle = getCleanVariantTitle(variant);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<div
|
|
263
|
+
key={`${item.resourceId}_${displayId}_${idx}`}
|
|
264
|
+
className="flex items-center justify-between"
|
|
265
|
+
>
|
|
266
|
+
<div className="flex items-center gap-2">
|
|
267
|
+
{variantTitle && (
|
|
268
|
+
<div className="text-sm font-bold text-gray-700">
|
|
269
|
+
<span>{variantTitle}</span>
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
{isPickupMode &&
|
|
273
|
+
!isService &&
|
|
274
|
+
(item.variantIdPickup &&
|
|
275
|
+
item.variantIdPickup !== item.variantIdShipped ? (
|
|
276
|
+
<span className="inline-flex items-center rounded bg-gray-100 px-2 py-0.5 text-xs font-bold text-gray-800">
|
|
277
|
+
Store Pickup
|
|
278
|
+
</span>
|
|
279
|
+
) : (
|
|
280
|
+
<span className="inline-flex items-center rounded bg-red-50 px-2 py-0.5 text-xs font-bold text-red-700">
|
|
281
|
+
Not available for pickup
|
|
282
|
+
</span>
|
|
283
|
+
))}
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<div className="flex items-center">
|
|
287
|
+
<div className="mr-6 text-right">
|
|
288
|
+
<p className="text-sm font-bold text-gray-900">
|
|
289
|
+
{price && parseFloat(price) > 0
|
|
290
|
+
? `${(parseFloat(price) * item.quantity).toFixed(2)} ${currency}`
|
|
291
|
+
: 'No Charge'}
|
|
292
|
+
</p>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
{isService ? (
|
|
296
|
+
<button
|
|
297
|
+
onClick={() =>
|
|
298
|
+
addQueue.set([
|
|
299
|
+
...addQueue.get(),
|
|
300
|
+
{
|
|
301
|
+
resourceId: item.resourceId,
|
|
302
|
+
action: 'remove',
|
|
303
|
+
variantId: item.variantId,
|
|
304
|
+
},
|
|
305
|
+
])
|
|
306
|
+
}
|
|
307
|
+
className="rounded-md border border-gray-300 px-3 py-1 text-sm font-bold text-gray-600 hover:bg-gray-100"
|
|
308
|
+
>
|
|
309
|
+
Remove
|
|
310
|
+
</button>
|
|
311
|
+
) : (
|
|
312
|
+
<div className="flex items-center rounded-md border border-gray-300">
|
|
313
|
+
<button
|
|
314
|
+
onClick={() => dispatchAction(item, 'remove')}
|
|
315
|
+
className="px-3 py-1 text-gray-600 hover:bg-gray-100"
|
|
316
|
+
>
|
|
317
|
+
-
|
|
318
|
+
</button>
|
|
319
|
+
<span className="border-l border-r border-gray-300 px-3 py-1 text-gray-900">
|
|
320
|
+
{item.quantity}
|
|
321
|
+
</span>
|
|
322
|
+
<button
|
|
323
|
+
onClick={() => dispatchAction(item, 'add')}
|
|
324
|
+
className="px-3 py-1 text-gray-600 hover:bg-gray-100"
|
|
325
|
+
>
|
|
326
|
+
+
|
|
327
|
+
</button>
|
|
328
|
+
</div>
|
|
329
|
+
)}
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
);
|
|
333
|
+
})}
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
</li>
|
|
338
|
+
);
|
|
339
|
+
})}
|
|
340
|
+
</ul>
|
|
341
|
+
|
|
342
|
+
<div className="rounded-b-lg border-t border-gray-200 bg-gray-50 px-6 py-6">
|
|
343
|
+
<div className="flex justify-end">
|
|
344
|
+
<button
|
|
345
|
+
className="rounded-lg bg-black px-6 py-3 font-bold text-white transition-colors hover:bg-gray-800"
|
|
346
|
+
onClick={() => {
|
|
347
|
+
const currentCart = cartStore.get();
|
|
348
|
+
const sanitizedCart = { ...currentCart };
|
|
349
|
+
|
|
350
|
+
Object.keys(sanitizedCart).forEach((key) => {
|
|
351
|
+
const item = sanitizedCart[key];
|
|
352
|
+
|
|
353
|
+
if (isPickupMode && item.variantIdPickup) {
|
|
354
|
+
item.variantId = item.variantIdPickup;
|
|
355
|
+
} else if (!isPickupMode && item.variantIdShipped) {
|
|
356
|
+
item.variantId = item.variantIdShipped;
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
cartStore.set(sanitizedCart);
|
|
361
|
+
transactionTraceId.set(ulid());
|
|
362
|
+
|
|
363
|
+
cartState.set(CART_STATES.CHECKOUT);
|
|
364
|
+
}}
|
|
365
|
+
>
|
|
366
|
+
Proceed to Checkout
|
|
367
|
+
</button>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useStore } from '@nanostores/react';
|
|
2
|
+
import { cartStore } from '@/stores/shopify';
|
|
3
|
+
|
|
4
|
+
export default function CartIcon() {
|
|
5
|
+
const cart = useStore(cartStore);
|
|
6
|
+
const cartValues = Object.values(cart);
|
|
7
|
+
const boundServiceIds = new Set(
|
|
8
|
+
cartValues.map((item) => item.boundResourceId).filter(Boolean)
|
|
9
|
+
);
|
|
10
|
+
const totalQuantity = cartValues
|
|
11
|
+
.filter((item) => !boundServiceIds.has(item.resourceId))
|
|
12
|
+
.reduce((total, item) => total + item.quantity, 0);
|
|
13
|
+
|
|
14
|
+
const handleOpenCart = () => {
|
|
15
|
+
window.location.href = '/cart';
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
if (totalQuantity === 0) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<button
|
|
24
|
+
onClick={handleOpenCart}
|
|
25
|
+
className="relative flex items-center justify-center rounded-full p-2 text-gray-700 transition-colors hover:bg-gray-100"
|
|
26
|
+
aria-label="Open Cart"
|
|
27
|
+
>
|
|
28
|
+
<svg
|
|
29
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
30
|
+
fill="none"
|
|
31
|
+
viewBox="0 0 24 24"
|
|
32
|
+
strokeWidth={1.5}
|
|
33
|
+
stroke="currentColor"
|
|
34
|
+
className="h-6 w-6"
|
|
35
|
+
>
|
|
36
|
+
<path
|
|
37
|
+
strokeLinecap="round"
|
|
38
|
+
strokeLinejoin="round"
|
|
39
|
+
d="M15.75 10.5V6a3.75 3.75 0 10-7.5 0v4.5m11.356-1.993l1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 01-1.12-1.243l1.264-12A1.125 1.125 0 015.513 7.5h12.974c.576 0 1.059.435 1.119 1.007zM8.625 10.5a.375.375 0 11-.75 0 .375.375 0 01.75 0zm7.5 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
|
|
40
|
+
/>
|
|
41
|
+
</svg>
|
|
42
|
+
<span className="absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full bg-black text-xs font-bold text-white ring-2 ring-white">
|
|
43
|
+
{totalQuantity}
|
|
44
|
+
</span>
|
|
45
|
+
</button>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useStore } from '@nanostores/react';
|
|
2
|
+
import { Dialog } from '@ark-ui/react/dialog';
|
|
3
|
+
import { Portal } from '@ark-ui/react/portal';
|
|
4
|
+
import { modalState } from '@/stores/shopify';
|
|
5
|
+
|
|
6
|
+
export default function CartModal() {
|
|
7
|
+
const state = useStore(modalState);
|
|
8
|
+
|
|
9
|
+
const handleClose = () => {
|
|
10
|
+
modalState.set({ ...state, isOpen: false });
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const handleAccept = () => {
|
|
14
|
+
modalState.set({ ...state, isOpen: false });
|
|
15
|
+
window.location.href = '/cart';
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
if (!state.isOpen) return null;
|
|
19
|
+
|
|
20
|
+
const isCartPage =
|
|
21
|
+
typeof window !== 'undefined' && window.location.pathname === '/cart';
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Dialog.Root
|
|
25
|
+
open={state.isOpen}
|
|
26
|
+
onOpenChange={(e) => !e.open && handleClose()}
|
|
27
|
+
>
|
|
28
|
+
<Portal>
|
|
29
|
+
<Dialog.Backdrop className="fixed inset-0 z-50 bg-black bg-opacity-75 backdrop-blur-sm" />
|
|
30
|
+
<Dialog.Positioner className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
31
|
+
<Dialog.Content className="w-full max-w-md overflow-hidden rounded-lg bg-white shadow-xl">
|
|
32
|
+
<div className="p-6">
|
|
33
|
+
<Dialog.Title className="text-xl font-bold text-gray-900">
|
|
34
|
+
{state.title}
|
|
35
|
+
</Dialog.Title>
|
|
36
|
+
|
|
37
|
+
<div className="mt-4 text-gray-600">
|
|
38
|
+
<p>{state.message}</p>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div className="mt-6 flex justify-end gap-3">
|
|
42
|
+
<button
|
|
43
|
+
onClick={handleClose}
|
|
44
|
+
className="rounded-md bg-gray-200 px-4 py-2 text-sm font-bold text-gray-800 hover:bg-gray-300"
|
|
45
|
+
>
|
|
46
|
+
{isCartPage ? 'Close' : 'Continue Shopping'}
|
|
47
|
+
</button>
|
|
48
|
+
{!isCartPage && (
|
|
49
|
+
<button
|
|
50
|
+
onClick={handleAccept}
|
|
51
|
+
className="rounded-md bg-black px-4 py-2 text-sm font-bold text-white hover:bg-gray-800"
|
|
52
|
+
>
|
|
53
|
+
View Cart
|
|
54
|
+
</button>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</Dialog.Content>
|
|
59
|
+
</Dialog.Positioner>
|
|
60
|
+
</Portal>
|
|
61
|
+
</Dialog.Root>
|
|
62
|
+
);
|
|
63
|
+
}
|