astro-tractstack 2.3.0 → 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 +94 -16
- package/package.json +2 -2
- package/templates/custom/minimal/CodeHook.astro +10 -2
- package/templates/custom/shopify/Cart.tsx +100 -73
- package/templates/custom/shopify/CheckoutModal.tsx +509 -120
- package/templates/custom/shopify/NativeBookingCalendar.tsx +375 -0
- package/templates/custom/shopify/ShopifyCartManager.tsx +92 -37
- package/templates/custom/shopify/ShopifyProductGrid.tsx +139 -173
- package/templates/custom/shopify/ShopifyServiceList.tsx +20 -3
- package/templates/custom/with-examples/CodeHook.astro +10 -2
- package/templates/src/components/Footer.astro +4 -4
- package/templates/src/components/Header.astro +9 -3
- 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 +244 -2
- package/templates/src/components/form/shopify/SchedulingSection.tsx +354 -0
- package/templates/src/components/storykeep/Dashboard.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Shopify.tsx +253 -110
- package/templates/src/components/storykeep/controls/content/BeliefTable.tsx +14 -5
- package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +5 -2
- package/templates/src/components/storykeep/controls/content/MenuTable.tsx +14 -5
- package/templates/src/components/storykeep/controls/content/ProductTable.tsx +180 -101
- package/templates/src/components/storykeep/controls/content/ResourceBulkIngest.tsx +9 -5
- 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/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 +4 -8
- package/templates/src/pages/api/shopify/getProducts.ts +15 -15
- package/templates/src/pages/storykeep/login.astro +21 -14
- package/templates/src/stores/shopify.ts +81 -25
- package/templates/src/types/tractstack.ts +54 -0
- package/templates/src/utils/api/advancedConfig.ts +2 -0
- package/templates/src/utils/api/advancedHelpers.ts +40 -3
- package/templates/src/utils/api/bookingHelpers.ts +125 -0
- package/templates/src/utils/api/brandHelpers.ts +10 -0
- 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 +0 -21
- package/templates/src/utils/profileStorage.ts +5 -0
- package/templates/src/utils/tenantResolver.ts +2 -1
- package/utils/inject-files.ts +82 -4
- package/templates/custom/shopify/CalDotComBooking.tsx +0 -44
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import {
|
|
1
|
+
import { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { useStore } from '@nanostores/react';
|
|
3
3
|
import ChevronRightIcon from '@heroicons/react/24/outline/ChevronRightIcon';
|
|
4
4
|
import ChevronLeftIcon from '@heroicons/react/24/outline/ChevronLeftIcon';
|
|
5
5
|
import ArrowPathIcon from '@heroicons/react/24/outline/ArrowPathIcon';
|
|
@@ -7,7 +7,13 @@ import MagnifyingGlassIcon from '@heroicons/react/24/outline/MagnifyingGlassIcon
|
|
|
7
7
|
import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
|
|
8
8
|
import TrashIcon from '@heroicons/react/24/outline/TrashIcon';
|
|
9
9
|
import PencilIcon from '@heroicons/react/24/outline/PencilIcon';
|
|
10
|
-
import
|
|
10
|
+
import {
|
|
11
|
+
shopifyData,
|
|
12
|
+
shopifyStatus,
|
|
13
|
+
fetchShopifyProducts,
|
|
14
|
+
clearShopifySearch,
|
|
15
|
+
type ShopifyProduct,
|
|
16
|
+
} from '@/stores/shopify';
|
|
11
17
|
import type { ResourceNode } from '@/types/compositorTypes';
|
|
12
18
|
|
|
13
19
|
interface ProductTableProps {
|
|
@@ -21,68 +27,120 @@ interface ProductTableProps {
|
|
|
21
27
|
onEdit: (product: ShopifyProduct, resource: ResourceNode) => void;
|
|
22
28
|
}
|
|
23
29
|
|
|
24
|
-
const ITEMS_PER_PAGE = 10;
|
|
25
|
-
|
|
26
30
|
export default function ProductTable({
|
|
27
31
|
products,
|
|
28
32
|
linkedResourceMap,
|
|
29
|
-
onRefresh,
|
|
30
|
-
isRefreshing,
|
|
31
33
|
onSelectProduct,
|
|
32
34
|
onLink,
|
|
33
35
|
onUnlink,
|
|
34
36
|
onEdit,
|
|
35
37
|
}: ProductTableProps) {
|
|
38
|
+
const data = useStore(shopifyData);
|
|
39
|
+
const status = useStore(shopifyStatus);
|
|
40
|
+
|
|
41
|
+
const [inputValue, setInputValue] = useState('');
|
|
36
42
|
const [searchTerm, setSearchTerm] = useState('');
|
|
37
|
-
const [
|
|
43
|
+
const [isDebouncing, setIsDebouncing] = useState(false);
|
|
38
44
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
product.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
42
|
-
product.handle.toLowerCase().includes(searchTerm.toLowerCase())
|
|
43
|
-
);
|
|
45
|
+
const [cursorStack, setCursorStack] = useState<string[]>([]);
|
|
46
|
+
const [currentCursor, setCurrentCursor] = useState<string | null>(null);
|
|
44
47
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
);
|
|
48
|
+
const debounceTimer = useRef<NodeJS.Timeout | null>(null);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
return () => {
|
|
52
|
+
if (debounceTimer.current) clearTimeout(debounceTimer.current);
|
|
53
|
+
};
|
|
54
|
+
}, []);
|
|
52
55
|
|
|
53
|
-
const
|
|
54
|
-
|
|
56
|
+
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
57
|
+
const val = e.target.value;
|
|
58
|
+
setInputValue(val);
|
|
59
|
+
setIsDebouncing(true);
|
|
60
|
+
|
|
61
|
+
if (debounceTimer.current) {
|
|
62
|
+
clearTimeout(debounceTimer.current);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (val.length === 0) {
|
|
66
|
+
setIsDebouncing(false);
|
|
67
|
+
setSearchTerm('');
|
|
68
|
+
setCursorStack([]);
|
|
69
|
+
setCurrentCursor(null);
|
|
70
|
+
clearShopifySearch();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
debounceTimer.current = setTimeout(() => {
|
|
75
|
+
setIsDebouncing(false);
|
|
76
|
+
setSearchTerm(val);
|
|
77
|
+
setCursorStack([]);
|
|
78
|
+
setCurrentCursor(null);
|
|
79
|
+
|
|
80
|
+
if (val.length >= 3) {
|
|
81
|
+
fetchShopifyProducts(val, null);
|
|
82
|
+
} else {
|
|
83
|
+
clearShopifySearch();
|
|
84
|
+
}
|
|
85
|
+
}, 1000);
|
|
55
86
|
};
|
|
56
87
|
|
|
88
|
+
const handleRefreshSearch = () => {
|
|
89
|
+
if (searchTerm.length >= 3) {
|
|
90
|
+
fetchShopifyProducts(searchTerm, currentCursor);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handleNext = () => {
|
|
95
|
+
if (data.pageInfo?.hasNextPage && data.pageInfo?.endCursor) {
|
|
96
|
+
const nextCursor = data.pageInfo.endCursor;
|
|
97
|
+
setCursorStack((prev) => [...prev, currentCursor || '']);
|
|
98
|
+
setCurrentCursor(nextCursor);
|
|
99
|
+
fetchShopifyProducts(searchTerm, nextCursor);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const handlePrev = () => {
|
|
104
|
+
if (cursorStack.length > 0) {
|
|
105
|
+
const newStack = [...cursorStack];
|
|
106
|
+
const prevCursor = newStack.pop() || null;
|
|
107
|
+
setCursorStack(newStack);
|
|
108
|
+
setCurrentCursor(prevCursor || null);
|
|
109
|
+
fetchShopifyProducts(searchTerm, prevCursor || null);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const isLoading = status.isLoading || isDebouncing;
|
|
114
|
+
|
|
57
115
|
return (
|
|
58
116
|
<div className="space-y-4">
|
|
59
|
-
<div className="flex
|
|
60
|
-
<div className="flex-
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
117
|
+
<div className="flex flex-col gap-1">
|
|
118
|
+
<div className="flex items-center gap-4">
|
|
119
|
+
<div className="flex-1">
|
|
120
|
+
<input
|
|
121
|
+
type="text"
|
|
122
|
+
placeholder="Search products..."
|
|
123
|
+
value={inputValue}
|
|
124
|
+
onChange={handleSearchChange}
|
|
125
|
+
className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-cyan-700 focus:ring-cyan-700"
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
<button
|
|
129
|
+
onClick={handleRefreshSearch}
|
|
130
|
+
disabled={isLoading || searchTerm.length < 3}
|
|
131
|
+
className="inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-bold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 disabled:opacity-50"
|
|
132
|
+
title="Refresh current search"
|
|
133
|
+
>
|
|
134
|
+
<ArrowPathIcon
|
|
135
|
+
className={`mr-1.5 h-5 w-5 ${isLoading ? 'animate-spin' : ''}`}
|
|
136
|
+
/>
|
|
137
|
+
Sync
|
|
138
|
+
</button>
|
|
71
139
|
</div>
|
|
72
|
-
<
|
|
73
|
-
onClick={onRefresh}
|
|
74
|
-
disabled={isRefreshing}
|
|
75
|
-
className="inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-bold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 disabled:opacity-50"
|
|
76
|
-
title="Refresh from Shopify"
|
|
77
|
-
>
|
|
78
|
-
<ArrowPathIcon
|
|
79
|
-
className={`mr-1.5 h-5 w-5 ${isRefreshing ? 'animate-spin' : ''}`}
|
|
80
|
-
/>
|
|
81
|
-
Sync
|
|
82
|
-
</button>
|
|
140
|
+
<p className="text-xs text-gray-500">Search by product title.</p>
|
|
83
141
|
</div>
|
|
84
142
|
|
|
85
|
-
<div className="overflow-
|
|
143
|
+
<div className="overflow-x-auto shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
|
|
86
144
|
<table className="min-w-full divide-y divide-gray-300">
|
|
87
145
|
<thead className="bg-gray-50">
|
|
88
146
|
<tr>
|
|
@@ -98,25 +156,63 @@ export default function ProductTable({
|
|
|
98
156
|
</tr>
|
|
99
157
|
</thead>
|
|
100
158
|
<tbody className="divide-y divide-gray-200 bg-white">
|
|
101
|
-
{
|
|
159
|
+
{status.error ? (
|
|
102
160
|
<tr>
|
|
103
161
|
<td colSpan={3} className="px-6 py-12 text-center">
|
|
104
|
-
<div className="text-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
: 'No products match your search'}
|
|
162
|
+
<div className="font-bold text-red-600">{status.error}</div>
|
|
163
|
+
<div className="mt-1 text-sm text-red-500">
|
|
164
|
+
Please check your Shopify integration settings or API token.
|
|
108
165
|
</div>
|
|
109
166
|
</td>
|
|
110
167
|
</tr>
|
|
168
|
+
) : isLoading ? (
|
|
169
|
+
<tr>
|
|
170
|
+
<td colSpan={3} className="px-6 py-12 text-center">
|
|
171
|
+
<div className="mx-auto h-8 w-8 animate-spin rounded-full border-2 border-gray-300 border-t-cyan-600"></div>
|
|
172
|
+
<p className="mt-2 text-sm text-gray-500">
|
|
173
|
+
Searching Shopify...
|
|
174
|
+
</p>
|
|
175
|
+
</td>
|
|
176
|
+
</tr>
|
|
177
|
+
) : inputValue.length === 0 ? (
|
|
178
|
+
<tr>
|
|
179
|
+
<td
|
|
180
|
+
colSpan={3}
|
|
181
|
+
className="px-6 py-12 text-center text-gray-500"
|
|
182
|
+
>
|
|
183
|
+
Search to discover products.
|
|
184
|
+
</td>
|
|
185
|
+
</tr>
|
|
186
|
+
) : inputValue.length < 3 ? (
|
|
187
|
+
<tr>
|
|
188
|
+
<td
|
|
189
|
+
colSpan={3}
|
|
190
|
+
className="px-6 py-12 text-center text-gray-500"
|
|
191
|
+
>
|
|
192
|
+
Please enter at least 3 characters to search.
|
|
193
|
+
</td>
|
|
194
|
+
</tr>
|
|
195
|
+
) : products.length === 0 ? (
|
|
196
|
+
<tr>
|
|
197
|
+
<td
|
|
198
|
+
colSpan={3}
|
|
199
|
+
className="px-6 py-12 text-center text-gray-500"
|
|
200
|
+
>
|
|
201
|
+
No products match your search.
|
|
202
|
+
</td>
|
|
203
|
+
</tr>
|
|
111
204
|
) : (
|
|
112
|
-
|
|
205
|
+
products.map((product) => {
|
|
113
206
|
const linkedResource = linkedResourceMap.get(product.id);
|
|
114
207
|
const isLinked = !!linkedResource;
|
|
115
208
|
|
|
116
209
|
return (
|
|
117
210
|
<tr key={product.id} className="hover:bg-gray-50">
|
|
118
211
|
<td className="px-6 py-4">
|
|
119
|
-
<div
|
|
212
|
+
<div
|
|
213
|
+
className="max-w-xs truncate font-bold text-gray-900"
|
|
214
|
+
title={product.title}
|
|
215
|
+
>
|
|
120
216
|
{product.title}
|
|
121
217
|
</div>
|
|
122
218
|
{isLinked && (
|
|
@@ -127,11 +223,20 @@ export default function ProductTable({
|
|
|
127
223
|
: 'bg-cyan-50 text-cyan-700 ring-cyan-600/20'
|
|
128
224
|
}`}
|
|
129
225
|
>
|
|
130
|
-
Synced:
|
|
226
|
+
Synced:{' '}
|
|
227
|
+
<span
|
|
228
|
+
className="max-w-xs truncate"
|
|
229
|
+
title={linkedResource.title}
|
|
230
|
+
>
|
|
231
|
+
{linkedResource.title}
|
|
232
|
+
</span>
|
|
131
233
|
</span>
|
|
132
234
|
)}
|
|
133
235
|
</td>
|
|
134
|
-
<td
|
|
236
|
+
<td
|
|
237
|
+
className="max-w-xs truncate whitespace-nowrap px-6 py-4 text-sm text-gray-500"
|
|
238
|
+
title={product.handle}
|
|
239
|
+
>
|
|
135
240
|
{product.handle}
|
|
136
241
|
</td>
|
|
137
242
|
<td className="whitespace-nowrap px-6 py-4 text-right text-sm font-bold">
|
|
@@ -199,56 +304,30 @@ export default function ProductTable({
|
|
|
199
304
|
</table>
|
|
200
305
|
</div>
|
|
201
306
|
|
|
202
|
-
{
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
307
|
+
{(cursorStack.length > 0 || data.pageInfo?.hasNextPage) &&
|
|
308
|
+
!isLoading &&
|
|
309
|
+
inputValue.length >= 3 &&
|
|
310
|
+
products.length > 0 && (
|
|
311
|
+
<div className="flex items-center justify-center space-x-4 pt-4">
|
|
312
|
+
<button
|
|
313
|
+
onClick={handlePrev}
|
|
314
|
+
disabled={cursorStack.length === 0}
|
|
315
|
+
className="flex items-center gap-1 rounded px-3 py-2 text-sm font-bold text-gray-700 transition-colors hover:text-cyan-600 disabled:opacity-30 disabled:hover:text-gray-700"
|
|
316
|
+
>
|
|
211
317
|
<ChevronLeftIcon className="h-4 w-4" />
|
|
212
318
|
Previous
|
|
213
|
-
</
|
|
214
|
-
|
|
215
|
-
<
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
<Pagination.Item
|
|
221
|
-
key={index}
|
|
222
|
-
type="page"
|
|
223
|
-
value={page.value}
|
|
224
|
-
className={`rounded px-3 py-2 text-sm font-bold transition-colors ${
|
|
225
|
-
page.value === currentPage
|
|
226
|
-
? 'bg-myblue text-white'
|
|
227
|
-
: 'text-mydarkgrey hover:text-myblue'
|
|
228
|
-
}`}
|
|
229
|
-
>
|
|
230
|
-
{page.value}
|
|
231
|
-
</Pagination.Item>
|
|
232
|
-
) : (
|
|
233
|
-
<span
|
|
234
|
-
key={index}
|
|
235
|
-
className="px-2 text-sm text-mydarkgrey"
|
|
236
|
-
>
|
|
237
|
-
{page.type === 'ellipsis' ? '...' : ''}
|
|
238
|
-
</span>
|
|
239
|
-
)
|
|
240
|
-
)
|
|
241
|
-
}
|
|
242
|
-
</Pagination.Context>
|
|
243
|
-
</div>
|
|
244
|
-
|
|
245
|
-
<Pagination.NextTrigger className="ml-2 flex items-center gap-1 rounded px-3 py-2 text-sm font-bold text-mydarkgrey transition-colors hover:text-myblue disabled:opacity-50">
|
|
319
|
+
</button>
|
|
320
|
+
|
|
321
|
+
<button
|
|
322
|
+
onClick={handleNext}
|
|
323
|
+
disabled={!data.pageInfo?.hasNextPage}
|
|
324
|
+
className="flex items-center gap-1 rounded px-3 py-2 text-sm font-bold text-gray-700 transition-colors hover:text-cyan-600 disabled:opacity-30 disabled:hover:text-gray-700"
|
|
325
|
+
>
|
|
246
326
|
Next
|
|
247
327
|
<ChevronRightIcon className="h-4 w-4" />
|
|
248
|
-
</
|
|
249
|
-
</
|
|
250
|
-
|
|
251
|
-
)}
|
|
328
|
+
</button>
|
|
329
|
+
</div>
|
|
330
|
+
)}
|
|
252
331
|
</div>
|
|
253
332
|
);
|
|
254
333
|
}
|
|
@@ -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}
|
|
@@ -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
|
+
}
|