astro-tractstack 2.4.0 → 2.4.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/dist/index.js +8 -0
- package/package.json +1 -1
- package/templates/custom/shopify/CartModal.tsx +11 -1
- package/templates/custom/shopify/CartReset.tsx +38 -0
- package/templates/custom/shopify/ShopifyCartManager.tsx +2 -0
- package/templates/custom/shopify/ShopifyServiceList.tsx +52 -39
- package/templates/custom/shopify/cart-reset.astro +19 -0
- package/templates/src/components/Header.astro +1 -1
- package/templates/src/pages/codehooks/shopify-product-grid.astro +5 -7
- package/templates/src/pages/codehooks/shopify-service-list.astro +5 -7
- package/templates/src/stores/shopify.ts +34 -4
- package/utils/inject-files.ts +8 -0
package/dist/index.js
CHANGED
|
@@ -891,6 +891,14 @@ async function y(t, e, c) {
|
|
|
891
891
|
src: t("../templates/custom/shopify/cart.astro"),
|
|
892
892
|
dest: "src/pages/cart.astro"
|
|
893
893
|
},
|
|
894
|
+
{
|
|
895
|
+
src: t("../templates/custom/shopify/cart-reset.astro"),
|
|
896
|
+
dest: "src/pages/cart/reset.astro"
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
src: t("../templates/custom/shopify/CartReset.tsx"),
|
|
900
|
+
dest: "src/custom/shopify/CartReset.tsx"
|
|
901
|
+
},
|
|
894
902
|
{
|
|
895
903
|
src: t("../templates/src/pages/privacy.astro"),
|
|
896
904
|
dest: "src/pages/privacy.astro"
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import { useStore } from '@nanostores/react';
|
|
|
2
2
|
import { Dialog } from '@ark-ui/react/dialog';
|
|
3
3
|
import { Portal } from '@ark-ui/react/portal';
|
|
4
4
|
import { modalState } from '@/stores/shopify';
|
|
5
|
+
import { classNames } from '@/utils/helpers';
|
|
5
6
|
|
|
6
7
|
export default function CartModal() {
|
|
7
8
|
const state = useStore(modalState);
|
|
@@ -20,15 +21,24 @@ export default function CartModal() {
|
|
|
20
21
|
const isCartPage =
|
|
21
22
|
typeof window !== 'undefined' && window.location.pathname === '/cart';
|
|
22
23
|
|
|
24
|
+
const isMaxDurationRestriction =
|
|
25
|
+
state.type === 'restriction' && state.restrictionReason === 'maxDuration';
|
|
26
|
+
|
|
23
27
|
return (
|
|
24
28
|
<Dialog.Root
|
|
25
29
|
open={state.isOpen}
|
|
30
|
+
closeOnInteractOutside={false}
|
|
26
31
|
onOpenChange={(e) => !e.open && handleClose()}
|
|
27
32
|
>
|
|
28
33
|
<Portal>
|
|
29
34
|
<Dialog.Backdrop className="fixed inset-0 z-50 bg-black bg-opacity-75 backdrop-blur-sm" />
|
|
30
35
|
<Dialog.Positioner className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
31
|
-
<Dialog.Content
|
|
36
|
+
<Dialog.Content
|
|
37
|
+
className={classNames(
|
|
38
|
+
`w-full max-w-md overflow-hidden rounded-lg bg-white shadow-xl`,
|
|
39
|
+
isMaxDurationRestriction ? 'border-4 border-brand-3' : ''
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
32
42
|
<div className="p-6">
|
|
33
43
|
<Dialog.Title className="text-xl font-bold text-gray-900">
|
|
34
44
|
{state.title}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
resetShopifyCommerceState,
|
|
4
|
+
transactionTraceId,
|
|
5
|
+
} from '@/stores/shopify';
|
|
6
|
+
import { bookingHelpers } from '@/utils/api/bookingHelpers';
|
|
7
|
+
|
|
8
|
+
export default function CartReset() {
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
let cancelled = false;
|
|
11
|
+
|
|
12
|
+
const run = async () => {
|
|
13
|
+
const traceId = transactionTraceId.get();
|
|
14
|
+
if (traceId) {
|
|
15
|
+
try {
|
|
16
|
+
await bookingHelpers.releaseHold(traceId);
|
|
17
|
+
} catch (err) {
|
|
18
|
+
console.error('Failed to release hold during cart reset:', err);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (cancelled) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
resetShopifyCommerceState();
|
|
27
|
+
window.location.replace('/cart');
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
void run();
|
|
31
|
+
|
|
32
|
+
return () => {
|
|
33
|
+
cancelled = true;
|
|
34
|
+
};
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
return <p className="text-center text-sm text-gray-600">Resetting cart…</p>;
|
|
38
|
+
}
|
|
@@ -182,6 +182,7 @@ export default function ShopifyCartManager({
|
|
|
182
182
|
type: 'restriction',
|
|
183
183
|
title: 'Incompatible Booking Modes',
|
|
184
184
|
message: RESTRICTION_MESSAGES.INCOMPATIBLE_REMOTE,
|
|
185
|
+
restrictionReason: 'incompatibleRemote',
|
|
185
186
|
});
|
|
186
187
|
} else {
|
|
187
188
|
const rawDuration = calculateCartDuration(nextCart, resources);
|
|
@@ -196,6 +197,7 @@ export default function ShopifyCartManager({
|
|
|
196
197
|
type: 'restriction',
|
|
197
198
|
title: 'Appointment Length Limit Reached',
|
|
198
199
|
message: RESTRICTION_MESSAGES.MAX_DURATION(dynamicMax),
|
|
200
|
+
restrictionReason: 'maxDuration',
|
|
199
201
|
});
|
|
200
202
|
} else {
|
|
201
203
|
cartStore.set(nextCart);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { useStore } from '@nanostores/react';
|
|
2
|
+
import { Switch } from '@ark-ui/react/switch';
|
|
2
3
|
import { cartStore, addQueue, type CartAction } from '@/stores/shopify';
|
|
3
4
|
import {
|
|
4
5
|
getCartItemKey,
|
|
5
6
|
getServiceVariantIdFromCanonicalProduct,
|
|
6
7
|
isSharedFeeService,
|
|
7
8
|
} from '@/custom/shopify/shopifyHelpers';
|
|
9
|
+
import { classNames } from '@/utils/helpers';
|
|
8
10
|
import type { ResourceNode } from '@/types/compositorTypes';
|
|
9
11
|
|
|
10
12
|
interface Props {
|
|
@@ -51,12 +53,12 @@ export default function ShopifyServiceList({ resources = [], options }: Props) {
|
|
|
51
53
|
.filter((s): s is string => !!s)
|
|
52
54
|
);
|
|
53
55
|
|
|
54
|
-
const displayServices = services
|
|
55
|
-
(s) => !boundServiceSlugs.has(s.slug)
|
|
56
|
-
|
|
56
|
+
const displayServices = services
|
|
57
|
+
.filter((s) => !boundServiceSlugs.has(s.slug))
|
|
58
|
+
.sort((a, b) => a.title.localeCompare(b.title));
|
|
57
59
|
|
|
58
|
-
const
|
|
59
|
-
const actionType =
|
|
60
|
+
const handleCheckedChange = (resource: ResourceNode, checked: boolean) => {
|
|
61
|
+
const actionType = checked ? 'add' : 'remove';
|
|
60
62
|
const gid =
|
|
61
63
|
typeof resource.optionsPayload?.gid === 'string'
|
|
62
64
|
? resource.optionsPayload.gid
|
|
@@ -116,47 +118,58 @@ export default function ShopifyServiceList({ resources = [], options }: Props) {
|
|
|
116
118
|
const duration = resource.optionsPayload?.bookingLengthMinutes;
|
|
117
119
|
|
|
118
120
|
return (
|
|
119
|
-
<
|
|
121
|
+
<Switch.Root
|
|
120
122
|
key={resource.id}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
checked={isSelected}
|
|
124
|
+
onCheckedChange={(details) =>
|
|
125
|
+
handleCheckedChange(resource, details.checked)
|
|
126
|
+
}
|
|
127
|
+
className="block w-full"
|
|
126
128
|
>
|
|
127
|
-
<div
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
<div
|
|
130
|
+
className={`flex cursor-pointer items-center justify-between rounded-lg border p-4 transition-colors ${
|
|
131
|
+
isSelected
|
|
132
|
+
? 'border-black bg-gray-50'
|
|
133
|
+
: 'border-gray-200 bg-white hover:border-gray-300'
|
|
134
|
+
}`}
|
|
135
|
+
>
|
|
136
|
+
<Switch.Label className="min-w-0 flex-grow cursor-pointer">
|
|
137
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
138
|
+
<h3 className="font-bold text-gray-900">
|
|
139
|
+
{resource.title}
|
|
140
|
+
</h3>
|
|
141
|
+
{duration ? (
|
|
142
|
+
<span className="inline-flex items-center rounded-sm bg-blue-50 px-2 py-0.5 text-xs font-bold text-blue-700">
|
|
143
|
+
{duration} mins
|
|
144
|
+
</span>
|
|
145
|
+
) : null}
|
|
146
|
+
{isSelected ? (
|
|
147
|
+
<span className="inline-flex items-center rounded-sm bg-gray-100 px-2 py-0.5 text-xs font-bold text-gray-800">
|
|
148
|
+
In Cart
|
|
149
|
+
</span>
|
|
150
|
+
) : null}
|
|
151
|
+
</div>
|
|
152
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
153
|
+
{resource.oneliner}
|
|
154
|
+
</p>
|
|
155
|
+
</Switch.Label>
|
|
156
|
+
|
|
157
|
+
<Switch.Control
|
|
158
|
+
className={classNames(
|
|
159
|
+
`relative ml-4 inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none`,
|
|
160
|
+
isSelected ? 'bg-brand-5' : 'bg-gray-200'
|
|
136
161
|
)}
|
|
137
|
-
</div>
|
|
138
|
-
<p className="mt-1 text-sm text-gray-500">
|
|
139
|
-
{resource.oneliner}
|
|
140
|
-
</p>
|
|
141
|
-
</div>
|
|
142
|
-
|
|
143
|
-
<div className="ml-4 flex-shrink-0">
|
|
144
|
-
<button
|
|
145
|
-
onClick={() => handleToggle(resource, selectedQuantity)}
|
|
146
|
-
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
|
147
|
-
isSelected ? 'bg-black' : 'bg-gray-200'
|
|
148
|
-
}`}
|
|
149
|
-
role="switch"
|
|
150
|
-
aria-checked={isSelected}
|
|
151
162
|
>
|
|
152
|
-
<
|
|
153
|
-
className={
|
|
163
|
+
<Switch.Thumb
|
|
164
|
+
className={classNames(
|
|
165
|
+
`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out`,
|
|
154
166
|
isSelected ? 'translate-x-5' : 'translate-x-0'
|
|
155
|
-
}
|
|
167
|
+
)}
|
|
156
168
|
/>
|
|
157
|
-
</
|
|
169
|
+
</Switch.Control>
|
|
170
|
+
<Switch.HiddenInput />
|
|
158
171
|
</div>
|
|
159
|
-
</
|
|
172
|
+
</Switch.Root>
|
|
160
173
|
);
|
|
161
174
|
})}
|
|
162
175
|
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Layout from '@/layouts/Layout.astro';
|
|
3
|
+
import CartReset from '@/custom/shopify/CartReset';
|
|
4
|
+
import { getBrandConfig } from '@/utils/api/brandConfig';
|
|
5
|
+
|
|
6
|
+
const tenantId =
|
|
7
|
+
Astro.locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default';
|
|
8
|
+
|
|
9
|
+
const brandConfig = await getBrandConfig(tenantId);
|
|
10
|
+
if (!brandConfig.HAS_SHOPIFY) {
|
|
11
|
+
return Astro.redirect('/storykeep');
|
|
12
|
+
}
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<Layout title="Reset Cart" slug="cart">
|
|
16
|
+
<main class="mx-auto max-w-7xl px-4 py-16 md:px-6 xl:px-8">
|
|
17
|
+
<CartReset client:only="react" />
|
|
18
|
+
</main>
|
|
19
|
+
</Layout>
|
|
@@ -141,7 +141,7 @@ if (hasShopify) {
|
|
|
141
141
|
<div
|
|
142
142
|
class="flex flex-row flex-nowrap justify-between bg-mywhite px-4 pb-3 pt-4 shadow-inner md:px-8"
|
|
143
143
|
>
|
|
144
|
-
<h1 class="truncate text-2xl text-mydarkgrey">{title}</h1>
|
|
144
|
+
<h1 id="page-title" class="truncate text-2xl text-mydarkgrey">{title}</h1>
|
|
145
145
|
<div class="flex flex-row flex-nowrap items-center gap-x-2">
|
|
146
146
|
{
|
|
147
147
|
!isHome ? (
|
|
@@ -16,10 +16,8 @@ const resources = await resolveCodeHookResources(
|
|
|
16
16
|
);
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
/>
|
|
25
|
-
</div>
|
|
19
|
+
<ShopifyProductGrid
|
|
20
|
+
options={options}
|
|
21
|
+
resources={resources}
|
|
22
|
+
client:only="react"
|
|
23
|
+
/>
|
|
@@ -16,10 +16,8 @@ const resources = await resolveCodeHookResources(
|
|
|
16
16
|
);
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
/>
|
|
25
|
-
</div>
|
|
19
|
+
<ShopifyServiceList
|
|
20
|
+
options={options}
|
|
21
|
+
resources={resources}
|
|
22
|
+
client:only="react"
|
|
23
|
+
/>
|
|
@@ -26,6 +26,7 @@ export const modalState = atom<{
|
|
|
26
26
|
type: 'success' | 'restriction';
|
|
27
27
|
title: string;
|
|
28
28
|
message: string;
|
|
29
|
+
restrictionReason?: 'maxDuration' | 'incompatibleRemote';
|
|
29
30
|
}>({
|
|
30
31
|
isOpen: false,
|
|
31
32
|
type: 'success',
|
|
@@ -258,16 +259,45 @@ export function setCustomerDetails(details: Partial<CustomerDetails>) {
|
|
|
258
259
|
});
|
|
259
260
|
}
|
|
260
261
|
|
|
261
|
-
|
|
262
|
+
const SHOPIFY_PERSISTENCE_KEYS = [
|
|
263
|
+
'tractstack_shopify_queue',
|
|
264
|
+
'tractstack_shopify_cart',
|
|
265
|
+
'tractstack_shopify_queue_state',
|
|
266
|
+
'tractstack_shopify_cart_state',
|
|
267
|
+
'tractstack_shopify_trace_id',
|
|
268
|
+
'tractstack_shopify_appointment_mode',
|
|
269
|
+
'tractstack_shopify_customer',
|
|
270
|
+
] as const;
|
|
271
|
+
|
|
272
|
+
export function resetShopifyCommerceState() {
|
|
273
|
+
addQueue.set([]);
|
|
262
274
|
cartStore.set({});
|
|
275
|
+
cartState.set(CART_STATES.READY);
|
|
276
|
+
queueState.set(QUEUE_STATES.READY);
|
|
277
|
+
transactionTraceId.set('');
|
|
278
|
+
preferredAppointmentMode.set('IN_PERSON');
|
|
263
279
|
customerDetails.set({
|
|
264
280
|
name: '',
|
|
265
281
|
email: '',
|
|
266
282
|
leadId: '',
|
|
267
283
|
});
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
284
|
+
modalState.set({
|
|
285
|
+
isOpen: false,
|
|
286
|
+
type: 'success',
|
|
287
|
+
title: '',
|
|
288
|
+
message: '',
|
|
289
|
+
restrictionReason: undefined,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
if (typeof localStorage !== 'undefined') {
|
|
293
|
+
SHOPIFY_PERSISTENCE_KEYS.forEach((key) => {
|
|
294
|
+
localStorage.removeItem(key);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function clearCommerceState() {
|
|
300
|
+
resetShopifyCommerceState();
|
|
271
301
|
}
|
|
272
302
|
|
|
273
303
|
export interface CartKeyParams {
|
package/utils/inject-files.ts
CHANGED
|
@@ -900,6 +900,14 @@ export async function injectTemplateFiles(
|
|
|
900
900
|
src: resolve('../templates/custom/shopify/cart.astro'),
|
|
901
901
|
dest: 'src/pages/cart.astro',
|
|
902
902
|
},
|
|
903
|
+
{
|
|
904
|
+
src: resolve('../templates/custom/shopify/cart-reset.astro'),
|
|
905
|
+
dest: 'src/pages/cart/reset.astro',
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
src: resolve('../templates/custom/shopify/CartReset.tsx'),
|
|
909
|
+
dest: 'src/custom/shopify/CartReset.tsx',
|
|
910
|
+
},
|
|
903
911
|
{
|
|
904
912
|
src: resolve('../templates/src/pages/privacy.astro'),
|
|
905
913
|
dest: 'src/pages/privacy.astro',
|