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
|
@@ -595,7 +595,7 @@ export default function ResourceBulkIngest({
|
|
|
595
595
|
|
|
596
596
|
{/* Status Display */}
|
|
597
597
|
<div className="mb-6 rounded-md border border-gray-200 bg-gray-50 p-4">
|
|
598
|
-
<div className="mb-2 flex items-center justify-between">
|
|
598
|
+
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
|
|
599
599
|
<span className="font-bold text-gray-900">
|
|
600
600
|
{validationResult.resources.length} resources found
|
|
601
601
|
</span>
|
|
@@ -638,7 +638,7 @@ export default function ResourceBulkIngest({
|
|
|
638
638
|
Validation Errors:
|
|
639
639
|
</p>
|
|
640
640
|
<div className="max-h-32 overflow-y-auto">
|
|
641
|
-
<ul className="space-y-1 text-sm text-red-600">
|
|
641
|
+
<ul className="space-y-1 break-words text-sm text-red-600">
|
|
642
642
|
{validationResult.errors
|
|
643
643
|
.slice(0, 10)
|
|
644
644
|
.map((error, idx) => (
|
|
@@ -648,8 +648,12 @@ export default function ResourceBulkIngest({
|
|
|
648
648
|
? `Item ${error.index + 1}:`
|
|
649
649
|
: 'JSON:'}
|
|
650
650
|
</span>
|
|
651
|
-
<span
|
|
652
|
-
|
|
651
|
+
<span
|
|
652
|
+
className="truncate"
|
|
653
|
+
title={`${error.field} - ${error.message}`}
|
|
654
|
+
>
|
|
655
|
+
<span className="font-bold">{error.field}</span> -{' '}
|
|
656
|
+
{error.message}
|
|
653
657
|
</span>
|
|
654
658
|
</li>
|
|
655
659
|
))}
|
|
@@ -674,7 +678,7 @@ export default function ResourceBulkIngest({
|
|
|
674
678
|
{/* Progress indicator */}
|
|
675
679
|
{isProcessing && progress && (
|
|
676
680
|
<div className="mb-6">
|
|
677
|
-
<div className="mb-2 flex items-center justify-between text-sm">
|
|
681
|
+
<div className="mb-2 flex flex-wrap items-center justify-between gap-2 text-sm">
|
|
678
682
|
<span>
|
|
679
683
|
Processing resource {progress.current + 1} of{' '}
|
|
680
684
|
{progress.total}
|
|
@@ -9,6 +9,10 @@ import BooleanToggle from '@/components/form/BooleanToggle';
|
|
|
9
9
|
import DateTimeInput from '@/components/form/DateTimeInput';
|
|
10
10
|
import FileUpload from '@/components/form/FileUpload';
|
|
11
11
|
import EnumSelect from '@/components/form/EnumSelect';
|
|
12
|
+
import {
|
|
13
|
+
resourceFormHideFields,
|
|
14
|
+
resourceJsonifyFields,
|
|
15
|
+
} from '@/utils/customHelpers';
|
|
12
16
|
import type {
|
|
13
17
|
ResourceConfig,
|
|
14
18
|
ResourceState,
|
|
@@ -46,15 +50,16 @@ export default function ResourceForm({
|
|
|
46
50
|
actionLisp: '',
|
|
47
51
|
};
|
|
48
52
|
|
|
49
|
-
// Initialize optionsPayload with default values for all schema fields
|
|
53
|
+
// 1. Initialize optionsPayload with default values for all schema fields
|
|
54
|
+
// (Only runs if NO existing data is provided)
|
|
50
55
|
if (!resourceData) {
|
|
51
|
-
// Only for new resources
|
|
52
56
|
const defaultOptionsPayload: Record<string, any> = {};
|
|
53
57
|
|
|
54
58
|
Object.entries(categorySchema).forEach(([fieldName, fieldDef]) => {
|
|
55
59
|
switch (fieldDef.type) {
|
|
56
60
|
case 'number':
|
|
57
|
-
defaultOptionsPayload[fieldName] =
|
|
61
|
+
defaultOptionsPayload[fieldName] =
|
|
62
|
+
fieldDef.defaultValue ?? fieldDef.minNumber ?? 0;
|
|
58
63
|
break;
|
|
59
64
|
case 'boolean':
|
|
60
65
|
defaultOptionsPayload[fieldName] = fieldDef.defaultValue ?? false;
|
|
@@ -79,6 +84,27 @@ export default function ResourceForm({
|
|
|
79
84
|
initialData.optionsPayload = defaultOptionsPayload;
|
|
80
85
|
}
|
|
81
86
|
|
|
87
|
+
// 2. Pre-process JSON fields for display (Pretty Print)
|
|
88
|
+
// This runs for both new and existing records to ensure readability
|
|
89
|
+
if (initialData.optionsPayload) {
|
|
90
|
+
resourceJsonifyFields.forEach((field) => {
|
|
91
|
+
const val = initialData.optionsPayload[field];
|
|
92
|
+
if (val && typeof val === 'string') {
|
|
93
|
+
try {
|
|
94
|
+
// Parse and re-stringify with indentation
|
|
95
|
+
initialData.optionsPayload[field] = JSON.stringify(
|
|
96
|
+
JSON.parse(val),
|
|
97
|
+
null,
|
|
98
|
+
2
|
|
99
|
+
);
|
|
100
|
+
} catch (e) {
|
|
101
|
+
// If it's not valid JSON, leave it as is
|
|
102
|
+
console.warn(`Failed to pretty-print field ${field}`, e);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
82
108
|
const validator = (state: ResourceState): FieldErrors => {
|
|
83
109
|
const errors: FieldErrors = {};
|
|
84
110
|
|
|
@@ -114,9 +140,34 @@ export default function ResourceForm({
|
|
|
114
140
|
validator,
|
|
115
141
|
onSave: async (data) => {
|
|
116
142
|
try {
|
|
143
|
+
// 3. Post-process JSON fields for saving (Minify)
|
|
144
|
+
const dataToSave = { ...data };
|
|
145
|
+
if (dataToSave.optionsPayload) {
|
|
146
|
+
// Create a shallow copy of optionsPayload to avoid mutating form state directly
|
|
147
|
+
dataToSave.optionsPayload = { ...dataToSave.optionsPayload };
|
|
148
|
+
|
|
149
|
+
resourceJsonifyFields.forEach((field) => {
|
|
150
|
+
const val = dataToSave.optionsPayload[field];
|
|
151
|
+
if (val && typeof val === 'string') {
|
|
152
|
+
try {
|
|
153
|
+
// Minify back to a compact string
|
|
154
|
+
dataToSave.optionsPayload[field] = JSON.stringify(
|
|
155
|
+
JSON.parse(val)
|
|
156
|
+
);
|
|
157
|
+
} catch (e) {
|
|
158
|
+
console.error(`Failed to minify field ${field}`, e);
|
|
159
|
+
// Throwing here would stop the save if the user typed invalid JSON
|
|
160
|
+
throw new Error(
|
|
161
|
+
`Invalid JSON in field "${field}". Please check your syntax.`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
117
168
|
const updatedState = await saveResourceWithStateUpdate(
|
|
118
169
|
window.TRACTSTACK_CONFIG?.tenantId || 'default',
|
|
119
|
-
|
|
170
|
+
dataToSave
|
|
120
171
|
);
|
|
121
172
|
|
|
122
173
|
// Call success callback after save (original pattern)
|
|
@@ -137,14 +188,19 @@ export default function ResourceForm({
|
|
|
137
188
|
// Helper to get category reference options for a field
|
|
138
189
|
const getCategoryReferenceOptions = (belongsToCategory: string) => {
|
|
139
190
|
return fullContentMap
|
|
140
|
-
.filter(
|
|
191
|
+
.filter(
|
|
192
|
+
(item) =>
|
|
193
|
+
item.categorySlug === belongsToCategory &&
|
|
194
|
+
!(
|
|
195
|
+
belongsToCategory === 'service' && (item as any).optionsPayload?.gid
|
|
196
|
+
)
|
|
197
|
+
)
|
|
141
198
|
.map((item) => ({
|
|
142
199
|
value: item.slug,
|
|
143
200
|
label: item.title,
|
|
144
201
|
}));
|
|
145
202
|
};
|
|
146
203
|
|
|
147
|
-
// Helper to update optionsPayload field
|
|
148
204
|
const updateOptionsField = (fieldName: string, value: any) => {
|
|
149
205
|
updateField('optionsPayload', {
|
|
150
206
|
...state.optionsPayload,
|
|
@@ -156,11 +212,51 @@ export default function ResourceForm({
|
|
|
156
212
|
onClose?.(false);
|
|
157
213
|
};
|
|
158
214
|
|
|
159
|
-
// Render dynamic field based on field definition
|
|
160
215
|
const renderDynamicField = (fieldName: string, fieldDef: FieldDefinition) => {
|
|
216
|
+
if (
|
|
217
|
+
resourceFormHideFields.includes(fieldName)
|
|
218
|
+
// && initialData.optionsPayload?.[fieldName]
|
|
219
|
+
) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
161
223
|
const fieldValue = state.optionsPayload[fieldName];
|
|
162
224
|
const fieldError = errors?.[`optionsPayload.${fieldName}`];
|
|
163
225
|
|
|
226
|
+
if (resourceJsonifyFields.includes(fieldName)) {
|
|
227
|
+
return (
|
|
228
|
+
<div key={fieldName} className="space-y-1">
|
|
229
|
+
<label
|
|
230
|
+
htmlFor={`field-${fieldName}`}
|
|
231
|
+
className="block text-sm font-bold text-gray-700"
|
|
232
|
+
>
|
|
233
|
+
{fieldName.charAt(0).toUpperCase() + fieldName.slice(1)}
|
|
234
|
+
</label>
|
|
235
|
+
<div className="relative">
|
|
236
|
+
<textarea
|
|
237
|
+
id={`field-${fieldName}`}
|
|
238
|
+
rows={12}
|
|
239
|
+
className={`block w-full rounded-md font-mono text-xs shadow-sm ${
|
|
240
|
+
fieldError
|
|
241
|
+
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
|
242
|
+
: 'border-gray-300 focus:border-cyan-500 focus:ring-cyan-500'
|
|
243
|
+
}`}
|
|
244
|
+
value={fieldValue || ''}
|
|
245
|
+
onChange={(e) => updateOptionsField(fieldName, e.target.value)}
|
|
246
|
+
placeholder="{}"
|
|
247
|
+
/>
|
|
248
|
+
</div>
|
|
249
|
+
{fieldError ? (
|
|
250
|
+
<p className="mt-1 text-sm text-red-600">{fieldError}</p>
|
|
251
|
+
) : (
|
|
252
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
253
|
+
Raw JSON configuration. Edits are validated on save.
|
|
254
|
+
</p>
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
164
260
|
switch (fieldDef.type) {
|
|
165
261
|
case 'string':
|
|
166
262
|
// Check if this string field references another category
|
|
@@ -349,7 +445,11 @@ export default function ResourceForm({
|
|
|
349
445
|
|
|
350
446
|
{/* Save/Cancel Bar */}
|
|
351
447
|
<UnsavedChangesBar
|
|
352
|
-
formState={
|
|
448
|
+
formState={{
|
|
449
|
+
...formState,
|
|
450
|
+
isDirty: isCreate || formState.isDirty,
|
|
451
|
+
cancel: handleCancel,
|
|
452
|
+
}}
|
|
353
453
|
message="You have unsaved resource changes"
|
|
354
454
|
saveLabel="Save Resource"
|
|
355
455
|
cancelLabel="Discard Changes"
|
|
@@ -117,7 +117,7 @@ export default function ResourceTable({
|
|
|
117
117
|
</div>
|
|
118
118
|
|
|
119
119
|
{/* Table */}
|
|
120
|
-
<div className="overflow-
|
|
120
|
+
<div className="overflow-x-auto shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
|
|
121
121
|
<table className="min-w-full divide-y divide-gray-300">
|
|
122
122
|
<thead className="bg-gray-50">
|
|
123
123
|
<tr>
|
|
@@ -153,13 +153,22 @@ export default function ResourceTable({
|
|
|
153
153
|
className="cursor-pointer hover:bg-gray-50"
|
|
154
154
|
onClick={() => onEdit(resource.id)}
|
|
155
155
|
>
|
|
156
|
-
<td
|
|
156
|
+
<td
|
|
157
|
+
className="max-w-xs truncate whitespace-nowrap px-6 py-4 text-sm font-bold text-gray-900"
|
|
158
|
+
title={resource.title}
|
|
159
|
+
>
|
|
157
160
|
{resource.title}
|
|
158
161
|
</td>
|
|
159
|
-
<td
|
|
162
|
+
<td
|
|
163
|
+
className="max-w-xs truncate whitespace-nowrap px-6 py-4 text-sm text-gray-500"
|
|
164
|
+
title={resource.slug}
|
|
165
|
+
>
|
|
160
166
|
{resource.slug}
|
|
161
167
|
</td>
|
|
162
|
-
<td
|
|
168
|
+
<td
|
|
169
|
+
className="max-w-xs truncate px-6 py-4 text-sm text-gray-500"
|
|
170
|
+
title={(resource as any).oneliner || '-'}
|
|
171
|
+
>
|
|
163
172
|
{(resource as any).oneliner || '-'}
|
|
164
173
|
</td>
|
|
165
174
|
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-bold md:pr-6">
|
|
@@ -199,7 +199,7 @@ const StoryFragmentTable = ({
|
|
|
199
199
|
</div>
|
|
200
200
|
|
|
201
201
|
{/* Table Container */}
|
|
202
|
-
<div className="overflow-
|
|
202
|
+
<div className="overflow-x-auto rounded-lg border border-gray-200 bg-white shadow">
|
|
203
203
|
{filteredFragments.length === 0 ? (
|
|
204
204
|
<div className="px-6 py-12 text-center">
|
|
205
205
|
<svg
|
|
@@ -241,7 +241,7 @@ const StoryFragmentTable = ({
|
|
|
241
241
|
)}
|
|
242
242
|
</div>
|
|
243
243
|
) : (
|
|
244
|
-
<div className="
|
|
244
|
+
<div className="inline-block min-w-full align-middle">
|
|
245
245
|
<table className="min-w-full divide-y divide-gray-200">
|
|
246
246
|
<thead className="bg-gray-50">
|
|
247
247
|
<tr>
|
|
@@ -271,15 +271,24 @@ const StoryFragmentTable = ({
|
|
|
271
271
|
<tr key={item.id} className="hover:bg-gray-50">
|
|
272
272
|
<td className="px-3 py-4 md:px-6">
|
|
273
273
|
<div className="flex flex-col">
|
|
274
|
-
<div
|
|
274
|
+
<div
|
|
275
|
+
className="max-w-xs truncate text-sm font-bold text-gray-900"
|
|
276
|
+
title={item.title}
|
|
277
|
+
>
|
|
275
278
|
{item.title}
|
|
276
279
|
</div>
|
|
277
|
-
<div
|
|
280
|
+
<div
|
|
281
|
+
className="max-w-xs truncate text-sm text-gray-500 md:hidden"
|
|
282
|
+
title={`/${item.slug}`}
|
|
283
|
+
>
|
|
278
284
|
/{item.slug}
|
|
279
285
|
</div>
|
|
280
286
|
</div>
|
|
281
287
|
</td>
|
|
282
|
-
<td
|
|
288
|
+
<td
|
|
289
|
+
className="hidden max-w-xs truncate whitespace-nowrap px-3 py-4 text-sm text-gray-500 md:table-cell md:px-6"
|
|
290
|
+
title={`/${item.slug}`}
|
|
291
|
+
>
|
|
283
292
|
/{item.slug}
|
|
284
293
|
</td>
|
|
285
294
|
<td className="hidden whitespace-nowrap px-3 py-4 text-sm md:table-cell md:px-6">
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { bookingHelpers } from '@/utils/api/bookingHelpers';
|
|
3
|
+
import type { BookingMetricsResponse } from '@/types/tractstack';
|
|
4
|
+
import type { ResourceNode } from '@/types/compositorTypes';
|
|
5
|
+
|
|
6
|
+
interface ShopifyDashboardProps {
|
|
7
|
+
existingResources: ResourceNode[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function ShopifyDashboard({
|
|
11
|
+
existingResources,
|
|
12
|
+
}: ShopifyDashboardProps) {
|
|
13
|
+
const [metrics, setMetrics] = useState<BookingMetricsResponse | null>(null);
|
|
14
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
15
|
+
const [error, setError] = useState<string | null>(null);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const loadMetrics = async () => {
|
|
19
|
+
try {
|
|
20
|
+
setIsLoading(true);
|
|
21
|
+
const data = await bookingHelpers.getMetrics();
|
|
22
|
+
setMetrics(data);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error('Failed to fetch metrics:', err);
|
|
25
|
+
setError('Failed to load dashboard metrics.');
|
|
26
|
+
} finally {
|
|
27
|
+
setIsLoading(false);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
loadMetrics();
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
if (isLoading) {
|
|
35
|
+
return (
|
|
36
|
+
<div className="flex h-48 items-center justify-center rounded-lg border-2 border-dashed border-gray-200">
|
|
37
|
+
<div className="h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-cyan-600" />
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (error) {
|
|
43
|
+
return (
|
|
44
|
+
<div className="rounded-lg border-2 border-red-200 bg-red-50 p-6 text-center">
|
|
45
|
+
<p className="font-bold text-red-600">{error}</p>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const totalLast24h =
|
|
51
|
+
(metrics?.confirmedLast24h || 0) + (metrics?.pendingLast24h || 0);
|
|
52
|
+
const intentRatio =
|
|
53
|
+
totalLast24h > 0
|
|
54
|
+
? Math.round(((metrics?.confirmedLast24h || 0) / totalLast24h) * 100)
|
|
55
|
+
: 0;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="space-y-6">
|
|
59
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
60
|
+
<div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
|
|
61
|
+
<h3 className="text-sm font-bold text-gray-500">Monthly Confirmed</h3>
|
|
62
|
+
<p className="mt-2 text-3xl font-bold text-gray-900">
|
|
63
|
+
{metrics?.totalMonthlyConfirmed || 0}
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
|
|
68
|
+
<h3 className="text-sm font-bold text-gray-500">Weekly Confirmed</h3>
|
|
69
|
+
<p className="mt-2 text-3xl font-bold text-gray-900">
|
|
70
|
+
{metrics?.totalWeeklyConfirmed || 0}
|
|
71
|
+
</p>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
|
|
75
|
+
<h3 className="text-sm font-bold text-gray-500">Annual Confirmed</h3>
|
|
76
|
+
<p className="mt-2 text-3xl font-bold text-gray-900">
|
|
77
|
+
{metrics?.totalAnnualConfirmed || 0}
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
|
|
82
|
+
<h3 className="text-sm font-bold text-gray-500">
|
|
83
|
+
Total Leads Converted
|
|
84
|
+
</h3>
|
|
85
|
+
<p className="mt-2 text-3xl font-bold text-gray-900">
|
|
86
|
+
{metrics?.leadConversionAnchor || 0}
|
|
87
|
+
</p>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
|
|
91
|
+
<h3 className="text-sm font-bold text-gray-500">
|
|
92
|
+
Pending (Last 24h)
|
|
93
|
+
</h3>
|
|
94
|
+
<p className="mt-2 text-3xl font-bold text-gray-900">
|
|
95
|
+
{metrics?.pendingLast24h || 0}
|
|
96
|
+
</p>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
|
|
100
|
+
<h3 className="text-sm font-bold text-gray-500">
|
|
101
|
+
Checkout Intent Ratio
|
|
102
|
+
</h3>
|
|
103
|
+
<div className="mt-2 flex items-baseline gap-2">
|
|
104
|
+
<p className="text-3xl font-bold text-gray-900">{intentRatio}%</p>
|
|
105
|
+
<p className="text-sm font-bold text-gray-500">conversion</p>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|