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,60 @@
|
|
|
1
|
+
import type { APIRoute } from '@/types/astro';
|
|
2
|
+
import { getAdminToken } from '@/utils/auth';
|
|
3
|
+
|
|
4
|
+
export const GET: APIRoute = async (context) => {
|
|
5
|
+
const { request, locals } = context;
|
|
6
|
+
const GO_BACKEND =
|
|
7
|
+
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const controller = new AbortController();
|
|
11
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
12
|
+
const tenantId =
|
|
13
|
+
locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default';
|
|
14
|
+
const token = getAdminToken(context);
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const response = await fetch(`${GO_BACKEND}/api/v1/sales/metrics`, {
|
|
18
|
+
method: 'GET',
|
|
19
|
+
headers: {
|
|
20
|
+
'Content-Type': 'application/json',
|
|
21
|
+
'X-Tenant-ID': tenantId,
|
|
22
|
+
...(token && { Authorization: `Bearer ${token}` }),
|
|
23
|
+
...(request.headers.get('Authorization') && {
|
|
24
|
+
Authorization: request.headers.get('Authorization')!,
|
|
25
|
+
}),
|
|
26
|
+
},
|
|
27
|
+
signal: controller.signal,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
clearTimeout(timeoutId);
|
|
31
|
+
const data = await response.json();
|
|
32
|
+
|
|
33
|
+
return new Response(JSON.stringify(data), {
|
|
34
|
+
status: response.status,
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
});
|
|
37
|
+
} catch (fetchError) {
|
|
38
|
+
clearTimeout(timeoutId);
|
|
39
|
+
if (fetchError instanceof Error && fetchError.name === 'AbortError') {
|
|
40
|
+
return new Response(
|
|
41
|
+
JSON.stringify({
|
|
42
|
+
success: false,
|
|
43
|
+
error: 'Sales metrics lookup timeout',
|
|
44
|
+
}),
|
|
45
|
+
{ status: 408, headers: { 'Content-Type': 'application/json' } }
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
throw fetchError;
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('Sales metrics API proxy error:', error);
|
|
52
|
+
return new Response(
|
|
53
|
+
JSON.stringify({
|
|
54
|
+
success: false,
|
|
55
|
+
error: 'Failed to connect to backend service',
|
|
56
|
+
}),
|
|
57
|
+
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Layout from '@/layouts/Layout.astro';
|
|
3
|
-
import CodeHook from '@/custom/CodeHook.astro';
|
|
4
3
|
import { getBrandConfig } from '@/utils/api/brandConfig';
|
|
5
4
|
import { handleFailedResponse } from '@/utils/backend';
|
|
6
|
-
import { getFullContentMap } from '@/stores/analytics';
|
|
7
5
|
import { preHealthCheck } from '@/utils/backend';
|
|
8
6
|
|
|
9
7
|
const tenantId =
|
|
@@ -58,8 +56,6 @@ try {
|
|
|
58
56
|
|
|
59
57
|
const paneId = contextPaneData.id;
|
|
60
58
|
const paneTitle = contextPaneData.title || 'Context';
|
|
61
|
-
const codeHookTarget = contextPaneData.codeHookTarget || null;
|
|
62
|
-
const resourcesPayload = storyData.resourcesPayload || {};
|
|
63
59
|
|
|
64
60
|
// Get rendered fragment for the context pane
|
|
65
61
|
let fragmentData = '';
|
|
@@ -93,8 +89,6 @@ try {
|
|
|
93
89
|
);
|
|
94
90
|
}
|
|
95
91
|
|
|
96
|
-
// Get supporting data
|
|
97
|
-
const fullContentMap = await getFullContentMap(tenantId);
|
|
98
92
|
const brandConfig = await getBrandConfig(tenantId);
|
|
99
93
|
|
|
100
94
|
if (!brandConfig.SITE_INIT) {
|
|
@@ -122,18 +116,7 @@ if (!brandConfig.SITE_INIT) {
|
|
|
122
116
|
hx-trigger="refresh"
|
|
123
117
|
hx-swap="innerHTML scroll:none"
|
|
124
118
|
>
|
|
125
|
-
{
|
|
126
|
-
codeHookTarget ? (
|
|
127
|
-
<CodeHook
|
|
128
|
-
target={codeHookTarget}
|
|
129
|
-
paneId={paneId}
|
|
130
|
-
resourcesPayload={resourcesPayload}
|
|
131
|
-
fullContentMap={fullContentMap}
|
|
132
|
-
/>
|
|
133
|
-
) : (
|
|
134
|
-
<Fragment set:html={fragmentData} />
|
|
135
|
-
)
|
|
136
|
-
}
|
|
119
|
+
<Fragment set:html={fragmentData} />
|
|
137
120
|
</div>
|
|
138
121
|
|
|
139
122
|
<div class="py-12 text-center text-2xl md:text-3xl">
|
|
@@ -148,20 +131,56 @@ if (!brandConfig.SITE_INIT) {
|
|
|
148
131
|
</Layout>
|
|
149
132
|
|
|
150
133
|
<script>
|
|
151
|
-
|
|
134
|
+
const IN_SITE_FROM_KEY = 'tractstack:inSiteFrom';
|
|
135
|
+
|
|
136
|
+
function getInSiteFrom() {
|
|
137
|
+
const rawFromPath = sessionStorage.getItem(IN_SITE_FROM_KEY);
|
|
138
|
+
if (!rawFromPath) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const fromUrl = new URL(rawFromPath, window.location.origin);
|
|
144
|
+
if (fromUrl.origin !== window.location.origin) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const fromPath = fromUrl.pathname + fromUrl.search + fromUrl.hash;
|
|
149
|
+
const currentPath =
|
|
150
|
+
window.location.pathname +
|
|
151
|
+
window.location.search +
|
|
152
|
+
window.location.hash;
|
|
153
|
+
|
|
154
|
+
if (fromPath === currentPath) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return fromPath;
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function setupContextCloseButton() {
|
|
152
165
|
const closeBtn = document.getElementById('context-close-btn');
|
|
153
166
|
|
|
154
|
-
if (closeBtn) {
|
|
155
|
-
|
|
156
|
-
// Check if we have history to go back to
|
|
157
|
-
if (window.history.length > 1 && document.referrer) {
|
|
158
|
-
// Go back to previous page
|
|
159
|
-
window.history.back();
|
|
160
|
-
} else {
|
|
161
|
-
// Fallback to home page
|
|
162
|
-
window.location.href = '/';
|
|
163
|
-
}
|
|
164
|
-
});
|
|
167
|
+
if (!closeBtn) {
|
|
168
|
+
return;
|
|
165
169
|
}
|
|
166
|
-
|
|
170
|
+
|
|
171
|
+
const fromPath = getInSiteFrom();
|
|
172
|
+
closeBtn.textContent = fromPath ? 'Close' : 'Home';
|
|
173
|
+
|
|
174
|
+
closeBtn.onclick = function () {
|
|
175
|
+
if (fromPath) {
|
|
176
|
+
window.location.assign(fromPath);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
window.location.assign('/');
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
setupContextCloseButton();
|
|
185
|
+
document.addEventListener('astro:page-load', setupContextCloseButton);
|
|
167
186
|
</script>
|
|
@@ -1876,6 +1876,14 @@ export class NodesContext {
|
|
|
1876
1876
|
if (ownerNode && 'slug' in ownerNode && typeof ownerNode.slug === `string`)
|
|
1877
1877
|
duplicatedPane.slug = ownerNode.slug;
|
|
1878
1878
|
|
|
1879
|
+
this.deleteChildren(ownerId);
|
|
1880
|
+
|
|
1881
|
+
if (pane.htmlAst) {
|
|
1882
|
+
duplicatedPane.htmlAst = pane.htmlAst;
|
|
1883
|
+
} else {
|
|
1884
|
+
delete duplicatedPane.htmlAst;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1879
1887
|
// Track all nodes that need to be added
|
|
1880
1888
|
// Call the new helper to process markdown, gridLayout, and bgPane
|
|
1881
1889
|
const allNodes: BaseNode[] = this._processPaneTemplate(
|
|
@@ -207,6 +207,7 @@ export interface BrandConfig {
|
|
|
207
207
|
HAS_HYDRATION_TOKEN?: boolean;
|
|
208
208
|
SCHEDULING?: SchedulingConfig;
|
|
209
209
|
ADMIN_EMAIL?: string;
|
|
210
|
+
ADMIN_EMAIL_NAME?: string;
|
|
210
211
|
}
|
|
211
212
|
|
|
212
213
|
export interface BrandConfigState {
|
|
@@ -246,6 +247,7 @@ export interface BrandConfigState {
|
|
|
246
247
|
hasHydrationToken: boolean;
|
|
247
248
|
scheduling: SchedulingConfig;
|
|
248
249
|
adminEmail: string;
|
|
250
|
+
adminEmailName: string;
|
|
249
251
|
}
|
|
250
252
|
|
|
251
253
|
// Form validation types
|
|
@@ -294,6 +296,7 @@ export interface AdvancedConfigStatus {
|
|
|
294
296
|
googleRefreshTokenSet: boolean;
|
|
295
297
|
googleTokenExpirySet: boolean;
|
|
296
298
|
hasGoogleSync: boolean;
|
|
299
|
+
hasResend: boolean;
|
|
297
300
|
}
|
|
298
301
|
|
|
299
302
|
export interface AdvancedConfigState {
|
|
@@ -310,6 +313,8 @@ export interface AdvancedConfigState {
|
|
|
310
313
|
shopifyAdminSlug: string;
|
|
311
314
|
userSetupWebhooks: boolean;
|
|
312
315
|
resendApiKey: string;
|
|
316
|
+
adminEmail: string;
|
|
317
|
+
adminEmailName: string;
|
|
313
318
|
googleOauthClientId: string;
|
|
314
319
|
googleOauthClientSecret: string;
|
|
315
320
|
googleCalendarId: string;
|
|
@@ -569,6 +574,58 @@ export interface BookingListResponse {
|
|
|
569
574
|
totalCount: number;
|
|
570
575
|
}
|
|
571
576
|
|
|
577
|
+
export type SaleStatus = 'PAID';
|
|
578
|
+
export type SaleTag = 'local-pickup' | 'orphan' | 'in-person' | 'remote';
|
|
579
|
+
|
|
580
|
+
export interface SaleProductLine {
|
|
581
|
+
resourceId: string;
|
|
582
|
+
gid: string;
|
|
583
|
+
variantId: string;
|
|
584
|
+
quantity: number;
|
|
585
|
+
title: string;
|
|
586
|
+
price: string;
|
|
587
|
+
currencyCode: string;
|
|
588
|
+
isLocalPickup: boolean;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export interface SaleEntity {
|
|
592
|
+
id: string; // traceId
|
|
593
|
+
leadId: string;
|
|
594
|
+
leadEmail?: string;
|
|
595
|
+
leadName?: string;
|
|
596
|
+
bookingId: string | null;
|
|
597
|
+
shopifyOrderId: string;
|
|
598
|
+
totalAmount: string;
|
|
599
|
+
status: SaleStatus;
|
|
600
|
+
products: SaleProductLine[];
|
|
601
|
+
appointmentIntent: boolean;
|
|
602
|
+
tags: SaleTag[];
|
|
603
|
+
booking: BookingEntity | null;
|
|
604
|
+
createdAt: string; // ISO-8601 UTC string
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export interface SaleListResponse {
|
|
608
|
+
data: SaleEntity[];
|
|
609
|
+
totalCount: number;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export interface SaleMetricsResponse {
|
|
613
|
+
paidOrderTotalMonth: string;
|
|
614
|
+
paidOrderTotalYear: string;
|
|
615
|
+
paidOrderTotalAllTime: string;
|
|
616
|
+
paidOrdersMonth: number;
|
|
617
|
+
paidOrdersYear: number;
|
|
618
|
+
paidOrdersAllTime: number;
|
|
619
|
+
averagePaidOrderMonth: string;
|
|
620
|
+
uniquePayingCustomers: number;
|
|
621
|
+
localPickupLineTotalMonth: string;
|
|
622
|
+
localPickupOrdersMonth: number;
|
|
623
|
+
appointmentOrdersMonth: number;
|
|
624
|
+
productOnlyOrdersMonth: number;
|
|
625
|
+
orphanOrdersMonth: number;
|
|
626
|
+
currencyCode: string;
|
|
627
|
+
}
|
|
628
|
+
|
|
572
629
|
export interface BookingMetricsResponse {
|
|
573
630
|
totalMonthlyConfirmed: number;
|
|
574
631
|
totalAnnualConfirmed: number;
|
|
@@ -34,7 +34,8 @@ export async function getAdvancedConfigStatus(
|
|
|
34
34
|
typeof data.shopifyStoreDomainSet !== 'boolean' ||
|
|
35
35
|
typeof data.shopifyAdminSlugSet !== 'boolean' ||
|
|
36
36
|
typeof data.userSetupWebhooks !== 'boolean' ||
|
|
37
|
-
typeof data.resendApiKeySet !== 'boolean'
|
|
37
|
+
typeof data.resendApiKeySet !== 'boolean' ||
|
|
38
|
+
typeof data.hasResend !== 'boolean'
|
|
38
39
|
) {
|
|
39
40
|
throw new Error('Invalid response format from server');
|
|
40
41
|
}
|
|
@@ -30,6 +30,8 @@ export function convertToLocalState(
|
|
|
30
30
|
googleOauthClientId: '',
|
|
31
31
|
googleOauthClientSecret: '',
|
|
32
32
|
googleCalendarId: '',
|
|
33
|
+
adminEmail: '',
|
|
34
|
+
adminEmailName: '',
|
|
33
35
|
};
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -53,6 +55,8 @@ export function convertToLocalState(
|
|
|
53
55
|
googleOauthClientId: '',
|
|
54
56
|
googleOauthClientSecret: '',
|
|
55
57
|
googleCalendarId: '',
|
|
58
|
+
adminEmail: '',
|
|
59
|
+
adminEmailName: '',
|
|
56
60
|
};
|
|
57
61
|
}
|
|
58
62
|
|
|
@@ -67,6 +67,7 @@ export async function getBrandConfig(tenantId: string): Promise<BrandConfig> {
|
|
|
67
67
|
DESIGN_LIBRARY: [],
|
|
68
68
|
HAS_AAI: false,
|
|
69
69
|
ADMIN_EMAIL: '',
|
|
70
|
+
ADMIN_EMAIL_NAME: '',
|
|
70
71
|
} as BrandConfig;
|
|
71
72
|
}
|
|
72
73
|
throw new Error(response.error || 'Failed to get brand configuration');
|
|
@@ -102,6 +103,7 @@ export async function getBrandConfig(tenantId: string): Promise<BrandConfig> {
|
|
|
102
103
|
DESIGN_LIBRARY: [],
|
|
103
104
|
HAS_AAI: false,
|
|
104
105
|
ADMIN_EMAIL: '',
|
|
106
|
+
ADMIN_EMAIL_NAME: '',
|
|
105
107
|
} as BrandConfig;
|
|
106
108
|
}
|
|
107
109
|
throw error;
|
|
@@ -45,6 +45,7 @@ export function convertToLocalState(
|
|
|
45
45
|
hasResend: brandConfig.HAS_RESEND ?? false,
|
|
46
46
|
hasHydrationToken: brandConfig.HAS_HYDRATION_TOKEN ?? false,
|
|
47
47
|
adminEmail: brandConfig.ADMIN_EMAIL ?? '',
|
|
48
|
+
adminEmailName: brandConfig.ADMIN_EMAIL_NAME ?? '',
|
|
48
49
|
scheduling: brandConfig.SCHEDULING ?? {
|
|
49
50
|
timezone: 'UTC',
|
|
50
51
|
bufferGapsMinutes: 15,
|
|
@@ -95,6 +96,7 @@ export function convertToBackendFormat(
|
|
|
95
96
|
HAS_RESEND: localState.hasResend,
|
|
96
97
|
SCHEDULING: scheduling,
|
|
97
98
|
ADMIN_EMAIL: localState.adminEmail,
|
|
99
|
+
ADMIN_EMAIL_NAME: localState.adminEmailName,
|
|
98
100
|
|
|
99
101
|
// ALWAYS send asset paths (current state)
|
|
100
102
|
LOGO: localState.logo,
|
|
@@ -139,6 +141,10 @@ export function validateBrandConfig(state: BrandConfigState): FieldErrors {
|
|
|
139
141
|
errors.adminEmail = 'Please enter a valid email address';
|
|
140
142
|
}
|
|
141
143
|
|
|
144
|
+
if (!state.adminEmailName?.trim()) {
|
|
145
|
+
errors.adminEmailName = 'Admin Email Name is required';
|
|
146
|
+
}
|
|
147
|
+
|
|
142
148
|
// Validate brand colors (must have exactly 8)
|
|
143
149
|
if (!state.brandColours || state.brandColours.length !== 8) {
|
|
144
150
|
errors.brandColours = 'Must have exactly 8 brand colors';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const salesHelpers = {
|
|
2
|
+
/**
|
|
3
|
+
* Retrieves a paginated list of paid Shopify sales for the administrative dashboard.
|
|
4
|
+
*/
|
|
5
|
+
listSales: async (limit: number = 50, offset: number = 0) => {
|
|
6
|
+
const query = new URLSearchParams({
|
|
7
|
+
limit: limit.toString(),
|
|
8
|
+
offset: offset.toString(),
|
|
9
|
+
});
|
|
10
|
+
const response = await fetch(`/api/sales/list?${query.toString()}`);
|
|
11
|
+
return await response.json();
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Retrieves paid Shopify sales metrics for the administrative dashboard.
|
|
16
|
+
*/
|
|
17
|
+
getMetrics: async () => {
|
|
18
|
+
const response = await fetch('/api/sales/metrics');
|
|
19
|
+
return await response.json();
|
|
20
|
+
},
|
|
21
|
+
};
|
package/utils/inject-files.ts
CHANGED
|
@@ -822,6 +822,10 @@ export async function injectTemplateFiles(
|
|
|
822
822
|
src: resolve('../templates/src/utils/api/bookingHelpers.ts'),
|
|
823
823
|
dest: 'src/utils/api/bookingHelpers.ts',
|
|
824
824
|
},
|
|
825
|
+
{
|
|
826
|
+
src: resolve('../templates/src/utils/api/salesHelpers.ts'),
|
|
827
|
+
dest: 'src/utils/api/salesHelpers.ts',
|
|
828
|
+
},
|
|
825
829
|
{
|
|
826
830
|
src: resolve('../templates/src/utils/api/menuHelpers.ts'),
|
|
827
831
|
dest: 'src/utils/api/menuHelpers.ts',
|
|
@@ -960,6 +964,14 @@ export async function injectTemplateFiles(
|
|
|
960
964
|
src: resolve('../templates/src/pages/api/booking/list.ts'),
|
|
961
965
|
dest: 'src/pages/api/booking/list.ts',
|
|
962
966
|
},
|
|
967
|
+
{
|
|
968
|
+
src: resolve('../templates/src/pages/api/sales/list.ts'),
|
|
969
|
+
dest: 'src/pages/api/sales/list.ts',
|
|
970
|
+
},
|
|
971
|
+
{
|
|
972
|
+
src: resolve('../templates/src/pages/api/sales/metrics.ts'),
|
|
973
|
+
dest: 'src/pages/api/sales/metrics.ts',
|
|
974
|
+
},
|
|
963
975
|
{
|
|
964
976
|
src: resolve('../templates/src/pages/api/booking/metrics.ts'),
|
|
965
977
|
dest: 'src/pages/api/booking/metrics.ts',
|
|
@@ -1284,6 +1296,12 @@ export async function injectTemplateFiles(
|
|
|
1284
1296
|
),
|
|
1285
1297
|
dest: 'src/components/storykeep/shopify/ShopifyDashboard_Emails.tsx',
|
|
1286
1298
|
},
|
|
1299
|
+
{
|
|
1300
|
+
src: resolve(
|
|
1301
|
+
'../templates/src/components/storykeep/shopify/ShopifyDashboard_Sales.tsx'
|
|
1302
|
+
),
|
|
1303
|
+
dest: 'src/components/storykeep/shopify/ShopifyDashboard_Sales.tsx',
|
|
1304
|
+
},
|
|
1287
1305
|
{
|
|
1288
1306
|
src: resolve(
|
|
1289
1307
|
'../templates/src/components/storykeep/email-builder/EmailBuilder.tsx'
|
|
@@ -2367,13 +2385,23 @@ export async function injectTemplateFiles(
|
|
|
2367
2385
|
protected: true,
|
|
2368
2386
|
},
|
|
2369
2387
|
{
|
|
2370
|
-
src: resolve('../templates/
|
|
2371
|
-
dest: 'src/
|
|
2388
|
+
src: resolve('../templates/custom/customHelpers.ts'),
|
|
2389
|
+
dest: 'src/custom/customHelpers.ts',
|
|
2390
|
+
protected: true,
|
|
2391
|
+
},
|
|
2392
|
+
{
|
|
2393
|
+
src: resolve('../templates/custom/shopify/shopifyCustomHelper.ts'),
|
|
2394
|
+
dest: 'src/custom/shopify/shopifyCustomHelper.ts',
|
|
2395
|
+
protected: true,
|
|
2396
|
+
},
|
|
2397
|
+
{
|
|
2398
|
+
src: resolve('../templates/custom/shopify/shopifyHelpers.ts'),
|
|
2399
|
+
dest: 'src/custom/shopify/shopifyHelpers.ts',
|
|
2372
2400
|
protected: true,
|
|
2373
2401
|
},
|
|
2374
2402
|
{
|
|
2375
|
-
src: resolve('../templates/
|
|
2376
|
-
dest: 'src/
|
|
2403
|
+
src: resolve('../templates/custom/shopify/appointmentMode.ts'),
|
|
2404
|
+
dest: 'src/custom/shopify/appointmentMode.ts',
|
|
2377
2405
|
protected: true,
|
|
2378
2406
|
},
|
|
2379
2407
|
{
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import type { ResourceNode } from '@/types/compositorTypes';
|
|
2
|
-
import type { CartItemState } from '@/stores/shopify';
|
|
3
|
-
|
|
4
|
-
// URL Helper: Strip category prefix from slug
|
|
5
|
-
// e.g., "people-bleako" -> "bleako"
|
|
6
|
-
export function getCleanSlug(categorySlug: string, fullSlug: string): string {
|
|
7
|
-
const prefix = `${categorySlug}-`;
|
|
8
|
-
return fullSlug.startsWith(prefix) ? fullSlug.slice(prefix.length) : fullSlug;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// Build proper URL for resource
|
|
12
|
-
// e.g., category="people", slug="people-bleako" -> "/people/bleako"
|
|
13
|
-
export function getResourceUrl(categorySlug: string, fullSlug: string): string {
|
|
14
|
-
const cleanSlug = getCleanSlug(categorySlug, fullSlug);
|
|
15
|
-
return `/${categorySlug}/${cleanSlug}`;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Image Helper: Placeholder implementation
|
|
19
|
-
export function getResourceImage(
|
|
20
|
-
id: string,
|
|
21
|
-
slug: string,
|
|
22
|
-
category: string
|
|
23
|
-
): string {
|
|
24
|
-
console.log(`please define getResourceImage`, id, slug, category);
|
|
25
|
-
return '/static.jpg';
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function getResourceDescription(
|
|
29
|
-
id: string,
|
|
30
|
-
slug: string,
|
|
31
|
-
category: string
|
|
32
|
-
): string | null {
|
|
33
|
-
console.log(`please define getResourceDescription`, id, slug, category);
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Initialize search data - override in custom implementation
|
|
38
|
-
export function initSearch(): void {
|
|
39
|
-
// Default implementation does nothing
|
|
40
|
-
// Override this function in your custom implementation to load search data
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Field Visibility Controls for ResourceForm
|
|
44
|
-
export const resourceFormHideFields = ['gid', 'shopifyImage'];
|
|
45
|
-
|
|
46
|
-
// Field Formatting Controls for ResourceForm
|
|
47
|
-
// Fields listed here will be treated as JSON objects but rendered as stringified text areas
|
|
48
|
-
export const resourceJsonifyFields = ['shopifyData', 'shopifyImage'];
|
|
49
|
-
|
|
50
|
-
export const RESTRICTION_MESSAGES = {
|
|
51
|
-
BOOKING: (duration: number) =>
|
|
52
|
-
`This is a ${duration} minute service. On checkout we'll help you book at your convenience.`,
|
|
53
|
-
TERMS: 'Please review the terms for this item before adding it to your cart.',
|
|
54
|
-
MAX_DURATION: (max: number) =>
|
|
55
|
-
`You cannot book more than ${max} minutes of services in one session.`,
|
|
56
|
-
INCOMPATIBLE_REMOTE:
|
|
57
|
-
'This service cannot be combined with the services already in your cart. Some require remote-only delivery while others can only be delivered in person.',
|
|
58
|
-
DEFAULT_ADD: (title: string) => `${title} has been added to your cart.`,
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// For CartModal.tsx
|
|
62
|
-
export function checkRestrictions(resource: ResourceNode): boolean {
|
|
63
|
-
// 1. Service / Booking Requirement
|
|
64
|
-
// We check for the explicit option payload value used by services
|
|
65
|
-
if (resource.optionsPayload?.bookingLengthMinutes) {
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// 2. Final Sale / Terms Check
|
|
70
|
-
// Placeholder: In the future, check for flags like resource.optionsPayload?.finalSale
|
|
71
|
-
// if (resource.optionsPayload?.finalSale) {
|
|
72
|
-
// return true;
|
|
73
|
-
// }
|
|
74
|
-
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function calculateCartDuration(
|
|
79
|
-
cart: Record<string, CartItemState>,
|
|
80
|
-
resources: ResourceNode[]
|
|
81
|
-
): number {
|
|
82
|
-
return Object.values(cart).reduce((total, item) => {
|
|
83
|
-
const resource = resources.find((r) => r.id === item.resourceId);
|
|
84
|
-
const duration = Number(
|
|
85
|
-
resource?.optionsPayload?.bookingLengthMinutes || 0
|
|
86
|
-
);
|
|
87
|
-
return total + (isNaN(duration) ? 0 : duration * item.quantity);
|
|
88
|
-
}, 0);
|
|
89
|
-
}
|
|
File without changes
|