astro-tractstack 2.3.3 → 2.3.5
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/bin/create-tractstack.js +5 -2
- package/dist/index.js +32 -4
- package/package.json +1 -1
- package/templates/custom/customHelpers.ts +45 -0
- package/templates/custom/shopify/Cart.tsx +197 -105
- package/templates/custom/shopify/CartIcon.tsx +8 -8
- package/templates/custom/shopify/CheckoutModal.tsx +145 -68
- package/templates/custom/shopify/ShopifyCartManager.tsx +67 -22
- package/templates/custom/shopify/ShopifyCheckout.tsx +2 -26
- package/templates/custom/shopify/ShopifyProductGrid.tsx +8 -0
- package/templates/custom/shopify/ShopifyServiceList.tsx +25 -37
- package/templates/custom/shopify/shopifyCustomHelper.ts +10 -0
- package/templates/custom/shopify/shopifyHelpers.ts +298 -0
- package/templates/src/components/Header.astro +2 -2
- package/templates/src/components/codehooks/SearchWidget.tsx +1 -1
- package/templates/src/components/compositor/Node.tsx +39 -9
- package/templates/src/components/compositor/nodes/CreativePane.tsx +175 -88
- package/templates/src/components/edit/pane/AddPanePanel.tsx +2 -2
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +6 -5
- package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +9 -15
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
- package/templates/src/components/form/advanced/APIConfigSection.tsx +35 -8
- package/templates/src/components/form/brand/SiteConfigSection.tsx +9 -0
- package/templates/src/components/search/SearchResults.tsx +1 -1
- package/templates/src/components/search/SearchWrapper.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +59 -23
- package/templates/src/components/storykeep/Dashboard_Shopify.tsx +257 -18
- package/templates/src/components/storykeep/controls/content/ProductTable.tsx +38 -24
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +162 -67
- package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +175 -48
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +5 -2
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Sales.tsx +479 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +7 -3
- package/templates/src/layouts/Layout.astro +26 -0
- package/templates/src/pages/api/auth/logout.ts +35 -2
- package/templates/src/pages/api/sales/list.ts +66 -0
- package/templates/src/pages/api/sales/metrics.ts +60 -0
- package/templates/src/pages/context/[...contextSlug].astro +50 -31
- package/templates/src/pages/storykeep/advanced.astro +4 -1
- package/templates/src/stores/nodes.ts +8 -0
- package/templates/src/types/tractstack.ts +57 -0
- package/templates/src/utils/api/advancedConfig.ts +2 -1
- package/templates/src/utils/api/advancedHelpers.ts +4 -0
- package/templates/src/utils/api/brandConfig.ts +2 -0
- package/templates/src/utils/api/brandHelpers.ts +6 -0
- package/templates/src/utils/api/salesHelpers.ts +21 -0
- package/utils/inject-files.ts +32 -4
- package/templates/src/utils/customHelpers.ts +0 -89
- /package/templates/{src/utils/booking → custom/shopify}/appointmentMode.ts +0 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { getCartItemKey as baseGetCartItemKey } from '@/stores/shopify';
|
|
2
|
+
import type { CartItemState, CartKeyParams } from '@/stores/shopify';
|
|
3
|
+
import type { ResourceNode } from '@/types/compositorTypes';
|
|
4
|
+
|
|
5
|
+
const SERVICES_ATTR_LIMIT = 255;
|
|
6
|
+
|
|
7
|
+
type CheckoutLineAttribute = { key: string; value: string };
|
|
8
|
+
|
|
9
|
+
export type ShopifyCheckoutLine = {
|
|
10
|
+
merchandiseId: string;
|
|
11
|
+
quantity: number;
|
|
12
|
+
attributes?: CheckoutLineAttribute[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type DepositSummary = {
|
|
16
|
+
title: string;
|
|
17
|
+
amount: string;
|
|
18
|
+
currencyCode: string;
|
|
19
|
+
variantId: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type SharedFeeChargeLineSummary = DepositSummary & {
|
|
23
|
+
servicesCount: number;
|
|
24
|
+
description?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function calculateCartDuration(
|
|
28
|
+
cart: Record<string, CartItemState>,
|
|
29
|
+
resources: ResourceNode[]
|
|
30
|
+
): number {
|
|
31
|
+
return Object.values(cart).reduce((total, item) => {
|
|
32
|
+
const resource = resources.find((r) => r.id === item.resourceId);
|
|
33
|
+
const duration = Number(
|
|
34
|
+
resource?.optionsPayload?.bookingLengthMinutes || 0
|
|
35
|
+
);
|
|
36
|
+
return total + (isNaN(duration) ? 0 : duration * item.quantity);
|
|
37
|
+
}, 0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getProductByGid(
|
|
41
|
+
resources: ResourceNode[],
|
|
42
|
+
gid?: string
|
|
43
|
+
): ResourceNode | undefined {
|
|
44
|
+
if (!gid) return undefined;
|
|
45
|
+
return resources.find(
|
|
46
|
+
(r) => r.categorySlug === 'product' && r.optionsPayload?.gid === gid
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getServiceLinkedProduct(
|
|
51
|
+
service: ResourceNode,
|
|
52
|
+
resources: ResourceNode[]
|
|
53
|
+
): ResourceNode | undefined {
|
|
54
|
+
const gid =
|
|
55
|
+
typeof service.optionsPayload?.gid === 'string'
|
|
56
|
+
? service.optionsPayload.gid
|
|
57
|
+
: undefined;
|
|
58
|
+
return getProductByGid(resources, gid);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function parsePrimaryShopifyProductData(
|
|
62
|
+
resource?: ResourceNode
|
|
63
|
+
): any | null {
|
|
64
|
+
if (!resource?.optionsPayload?.shopifyData) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(resource.optionsPayload.shopifyData);
|
|
69
|
+
return parsed.products?.[0] || parsed;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function extractVariantForResource(resource?: ResourceNode): any | null {
|
|
76
|
+
const parsed = parsePrimaryShopifyProductData(resource);
|
|
77
|
+
if (!parsed) return null;
|
|
78
|
+
const variants = parsed.variants || [];
|
|
79
|
+
return variants[0] || null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function parseDepositFromProductResource(
|
|
83
|
+
product?: ResourceNode
|
|
84
|
+
): DepositSummary | null {
|
|
85
|
+
if (!product) return null;
|
|
86
|
+
const parsed = parsePrimaryShopifyProductData(product);
|
|
87
|
+
const variant = extractVariantForResource(product);
|
|
88
|
+
const variantId = variant?.id;
|
|
89
|
+
if (!variantId) return null;
|
|
90
|
+
return {
|
|
91
|
+
title: parsed?.title || product.title,
|
|
92
|
+
amount: variant?.price?.amount || '0.00',
|
|
93
|
+
currencyCode: variant?.price?.currencyCode || 'USD',
|
|
94
|
+
variantId,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function isSharedFeeService(
|
|
99
|
+
service: ResourceNode | undefined,
|
|
100
|
+
resources: ResourceNode[]
|
|
101
|
+
): boolean {
|
|
102
|
+
if (!service || service.categorySlug !== 'service') return false;
|
|
103
|
+
const product = getServiceLinkedProduct(service, resources);
|
|
104
|
+
return product?.optionsPayload?.sharedServiceFee === true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function getServiceDisplayTitle(
|
|
108
|
+
service: ResourceNode | undefined,
|
|
109
|
+
resources: ResourceNode[]
|
|
110
|
+
): string {
|
|
111
|
+
if (!service) return 'Service';
|
|
112
|
+
const product = getServiceLinkedProduct(service, resources);
|
|
113
|
+
const parsed = parsePrimaryShopifyProductData(product);
|
|
114
|
+
return parsed?.title || service.title;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function getServiceVariantIdFromCanonicalProduct(
|
|
118
|
+
service: ResourceNode | undefined,
|
|
119
|
+
resources: ResourceNode[]
|
|
120
|
+
): string | undefined {
|
|
121
|
+
if (!service || service.categorySlug !== 'service') {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
const product = getServiceLinkedProduct(service, resources);
|
|
125
|
+
const variant = extractVariantForResource(product);
|
|
126
|
+
return typeof variant?.id === 'string' ? variant.id : undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function getCartItemKey(
|
|
130
|
+
params: CartKeyParams,
|
|
131
|
+
resource?: ResourceNode,
|
|
132
|
+
resources: ResourceNode[] = []
|
|
133
|
+
): string {
|
|
134
|
+
if (resource && isSharedFeeService(resource, resources)) {
|
|
135
|
+
return params.resourceId;
|
|
136
|
+
}
|
|
137
|
+
return baseGetCartItemKey(params);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function collectServiceGids(services: ResourceNode[]): Set<string> {
|
|
141
|
+
const gids = new Set<string>();
|
|
142
|
+
services.forEach((service) => {
|
|
143
|
+
if (typeof service.optionsPayload?.gid === 'string') {
|
|
144
|
+
gids.add(service.optionsPayload.gid);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
return gids;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function formatServicesAttribute(
|
|
151
|
+
services: ResourceNode[]
|
|
152
|
+
): CheckoutLineAttribute {
|
|
153
|
+
const titles = services.map((s) => s.title);
|
|
154
|
+
const joined = titles.join(', ');
|
|
155
|
+
if (joined.length <= SERVICES_ATTR_LIMIT) {
|
|
156
|
+
return { key: 'Services', value: joined };
|
|
157
|
+
}
|
|
158
|
+
return { key: 'Services', value: `${services.length} services` };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function getDepositLineSummary(
|
|
162
|
+
cart: Record<string, CartItemState>,
|
|
163
|
+
resources: ResourceNode[]
|
|
164
|
+
): DepositSummary | null {
|
|
165
|
+
const chargeLine = getSharedFeeChargeLineSummary(cart, resources);
|
|
166
|
+
if (!chargeLine) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
title: chargeLine.title,
|
|
171
|
+
amount: chargeLine.amount,
|
|
172
|
+
currencyCode: chargeLine.currencyCode,
|
|
173
|
+
variantId: chargeLine.variantId,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function getSharedFeeChargeLineSummary(
|
|
178
|
+
cart: Record<string, CartItemState>,
|
|
179
|
+
resources: ResourceNode[]
|
|
180
|
+
): SharedFeeChargeLineSummary | null {
|
|
181
|
+
const serviceIds = new Set(
|
|
182
|
+
Object.values(cart).map((item) => item.resourceId)
|
|
183
|
+
);
|
|
184
|
+
const sharedServices = resources.filter(
|
|
185
|
+
(r) => serviceIds.has(r.id) && isSharedFeeService(r, resources)
|
|
186
|
+
);
|
|
187
|
+
if (sharedServices.length === 0) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const canonicalProduct = getServiceLinkedProduct(
|
|
191
|
+
sharedServices[0],
|
|
192
|
+
resources
|
|
193
|
+
);
|
|
194
|
+
const deposit = parseDepositFromProductResource(canonicalProduct);
|
|
195
|
+
const canonicalProductData = parsePrimaryShopifyProductData(canonicalProduct);
|
|
196
|
+
const description =
|
|
197
|
+
typeof canonicalProductData?.description === 'string' &&
|
|
198
|
+
canonicalProductData.description.trim().length > 0
|
|
199
|
+
? canonicalProductData.description
|
|
200
|
+
: undefined;
|
|
201
|
+
if (!deposit) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
...deposit,
|
|
206
|
+
servicesCount: sharedServices.length,
|
|
207
|
+
description,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function buildShopifyCheckoutLines(
|
|
212
|
+
cart: Record<string, CartItemState>,
|
|
213
|
+
resources: ResourceNode[]
|
|
214
|
+
): ShopifyCheckoutLine[] {
|
|
215
|
+
const lines: ShopifyCheckoutLine[] = [];
|
|
216
|
+
const cartItems = Object.values(cart);
|
|
217
|
+
const sharedFeeServices: ResourceNode[] = [];
|
|
218
|
+
const sharedFeeServiceIds = new Set<string>();
|
|
219
|
+
|
|
220
|
+
cartItems.forEach((item) => {
|
|
221
|
+
const resource = resources.find((r) => r.id === item.resourceId);
|
|
222
|
+
if (isSharedFeeService(resource, resources) && resource) {
|
|
223
|
+
sharedFeeServices.push(resource);
|
|
224
|
+
sharedFeeServiceIds.add(resource.id);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (sharedFeeServices.length > 0) {
|
|
229
|
+
const canonicalProduct = getServiceLinkedProduct(
|
|
230
|
+
sharedFeeServices[0],
|
|
231
|
+
resources
|
|
232
|
+
);
|
|
233
|
+
const deposit = parseDepositFromProductResource(canonicalProduct);
|
|
234
|
+
if (deposit?.variantId) {
|
|
235
|
+
lines.push({
|
|
236
|
+
merchandiseId: deposit.variantId,
|
|
237
|
+
quantity: 1,
|
|
238
|
+
attributes: [formatServicesAttribute(sharedFeeServices)],
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
cartItems.forEach((item) => {
|
|
244
|
+
const resource = resources.find((r) => r.id === item.resourceId);
|
|
245
|
+
if (!resource) return;
|
|
246
|
+
if (sharedFeeServiceIds.has(resource.id)) return;
|
|
247
|
+
const nonSharedServiceVariant =
|
|
248
|
+
resource.categorySlug === 'service'
|
|
249
|
+
? getServiceVariantIdFromCanonicalProduct(resource, resources)
|
|
250
|
+
: undefined;
|
|
251
|
+
const merchandiseId = item.variantId || nonSharedServiceVariant;
|
|
252
|
+
if (!merchandiseId) return;
|
|
253
|
+
lines.push({
|
|
254
|
+
merchandiseId,
|
|
255
|
+
quantity: item.quantity || 1,
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return lines;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function hasGidBackedCheckout(
|
|
263
|
+
cart: Record<string, CartItemState>,
|
|
264
|
+
resources: ResourceNode[]
|
|
265
|
+
): boolean {
|
|
266
|
+
const cartItems = Object.values(cart);
|
|
267
|
+
return cartItems.some((item) => {
|
|
268
|
+
const resource = resources.find((r) => r.id === item.resourceId);
|
|
269
|
+
if (!resource) return false;
|
|
270
|
+
return (
|
|
271
|
+
typeof resource.optionsPayload?.gid === 'string' &&
|
|
272
|
+
!!resource.optionsPayload.gid
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function getCartIconCount(
|
|
278
|
+
cart: Record<string, CartItemState>,
|
|
279
|
+
resources: ResourceNode[]
|
|
280
|
+
): number {
|
|
281
|
+
const cartValues = Object.values(cart);
|
|
282
|
+
const boundServiceIds = new Set(
|
|
283
|
+
cartValues.map((item) => item.boundResourceId).filter(Boolean)
|
|
284
|
+
);
|
|
285
|
+
let sharedFeeAdded = false;
|
|
286
|
+
|
|
287
|
+
return cartValues
|
|
288
|
+
.filter((item) => !boundServiceIds.has(item.resourceId))
|
|
289
|
+
.reduce((total, item) => {
|
|
290
|
+
const resource = resources.find((r) => r.id === item.resourceId);
|
|
291
|
+
if (isSharedFeeService(resource, resources)) {
|
|
292
|
+
if (sharedFeeAdded) return total;
|
|
293
|
+
sharedFeeAdded = true;
|
|
294
|
+
return total + 1;
|
|
295
|
+
}
|
|
296
|
+
return total + item.quantity;
|
|
297
|
+
}, 0);
|
|
298
|
+
}
|
|
@@ -76,7 +76,7 @@ if (hasShopify) {
|
|
|
76
76
|
<>
|
|
77
77
|
{slug !== `cart` ? (
|
|
78
78
|
<div class="flex w-full justify-end px-4 py-2 md:px-8">
|
|
79
|
-
<CartIcon client:only="react" />
|
|
79
|
+
<CartIcon client:only="react" resources={shopifyResources} />
|
|
80
80
|
</div>
|
|
81
81
|
) : (
|
|
82
82
|
<CheckoutModal
|
|
@@ -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-
|
|
144
|
+
<h1 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 ? (
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
isTopLevelBlockNode,
|
|
9
9
|
parseCodeHook,
|
|
10
10
|
} from '@/utils/compositor/nodesHelper';
|
|
11
|
+
import { isStoryFragmentNode } from '@/utils/compositor/typeGuards';
|
|
11
12
|
import { PaneAddMode } from '@/types/compositorTypes';
|
|
12
13
|
import { NodeOverlay } from './tools/NodeOverlay';
|
|
13
14
|
import PanelVisibilityWrapper from '@/components/compositor/PanelVisibilityWrapper';
|
|
@@ -108,14 +109,6 @@ export const Node = memo((props: NodeProps) => {
|
|
|
108
109
|
case 'Pane':
|
|
109
110
|
{
|
|
110
111
|
const paneNode = node as PaneNode;
|
|
111
|
-
const storyfragmentNodeId = ctx.getClosestNodeTypeFromId(
|
|
112
|
-
node.id,
|
|
113
|
-
'StoryFragment'
|
|
114
|
-
);
|
|
115
|
-
const storyfragmentNode = ctx.allNodes
|
|
116
|
-
.get()
|
|
117
|
-
.get(storyfragmentNodeId) as StoryFragmentNode;
|
|
118
|
-
const first = paneNode.id === storyfragmentNode.paneIds?.[0];
|
|
119
112
|
const isHtmlAstPane = !!paneNode.htmlAst;
|
|
120
113
|
const paneNodes = ctx.getChildNodeIDs(node.id);
|
|
121
114
|
|
|
@@ -140,7 +133,7 @@ export const Node = memo((props: NodeProps) => {
|
|
|
140
133
|
);
|
|
141
134
|
}
|
|
142
135
|
|
|
143
|
-
if (!isPreview && !paneNodes.length) {
|
|
136
|
+
if (!isPreview && !paneNodes.length && !isHtmlAstPane) {
|
|
144
137
|
return (
|
|
145
138
|
<>
|
|
146
139
|
<ContextPanePanel nodeId={node.id} />
|
|
@@ -160,8 +153,45 @@ export const Node = memo((props: NodeProps) => {
|
|
|
160
153
|
</>
|
|
161
154
|
);
|
|
162
155
|
}
|
|
156
|
+
|
|
157
|
+
const contextContent = isHtmlAstPane ? (
|
|
158
|
+
<CreativePane nodeId={props.nodeId} htmlAst={paneNode.htmlAst!} />
|
|
159
|
+
) : (
|
|
160
|
+
<Pane {...props} />
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
element = (
|
|
164
|
+
<>
|
|
165
|
+
<div className="py-0.5">
|
|
166
|
+
<ConfigPanePanel
|
|
167
|
+
nodeId={props.nodeId}
|
|
168
|
+
isHtmlAstPane={isHtmlAstPane}
|
|
169
|
+
isSandboxMode={props.isSandboxMode || false}
|
|
170
|
+
/>
|
|
171
|
+
<PanelVisibilityWrapper
|
|
172
|
+
nodeId={props.nodeId}
|
|
173
|
+
panelType="settings"
|
|
174
|
+
ctx={ctx}
|
|
175
|
+
>
|
|
176
|
+
{contextContent}
|
|
177
|
+
</PanelVisibilityWrapper>
|
|
178
|
+
</div>
|
|
179
|
+
</>
|
|
180
|
+
);
|
|
181
|
+
break;
|
|
163
182
|
}
|
|
164
183
|
|
|
184
|
+
const storyfragmentNodeId = ctx.getClosestNodeTypeFromId(
|
|
185
|
+
node.id,
|
|
186
|
+
'StoryFragment'
|
|
187
|
+
);
|
|
188
|
+
const storyfragmentNode = storyfragmentNodeId
|
|
189
|
+
? (ctx.allNodes.get().get(storyfragmentNodeId) ?? null)
|
|
190
|
+
: null;
|
|
191
|
+
const first =
|
|
192
|
+
isStoryFragmentNode(storyfragmentNode) &&
|
|
193
|
+
paneNode.id === storyfragmentNode.paneIds?.[0];
|
|
194
|
+
|
|
165
195
|
const content = isHtmlAstPane ? (
|
|
166
196
|
<CreativePane nodeId={props.nodeId} htmlAst={paneNode.htmlAst!} />
|
|
167
197
|
) : (
|