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.
Files changed (63) hide show
  1. package/README.md +1 -1
  2. package/bin/create-tractstack.js +2 -2
  3. package/dist/index.js +94 -16
  4. package/package.json +2 -2
  5. package/templates/custom/minimal/CodeHook.astro +10 -2
  6. package/templates/custom/shopify/Cart.tsx +100 -73
  7. package/templates/custom/shopify/CheckoutModal.tsx +509 -120
  8. package/templates/custom/shopify/NativeBookingCalendar.tsx +375 -0
  9. package/templates/custom/shopify/ShopifyCartManager.tsx +92 -37
  10. package/templates/custom/shopify/ShopifyProductGrid.tsx +139 -173
  11. package/templates/custom/shopify/ShopifyServiceList.tsx +20 -3
  12. package/templates/custom/with-examples/CodeHook.astro +10 -2
  13. package/templates/src/components/Footer.astro +4 -4
  14. package/templates/src/components/Header.astro +9 -3
  15. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +3 -3
  16. package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +2 -2
  17. package/templates/src/components/edit/pane/steps/AiCreativeDesignStep.tsx +2 -2
  18. package/templates/src/components/edit/pane/steps/AiLibraryCopyStep.tsx +3 -3
  19. package/templates/src/components/edit/pane/steps/AiRefineDesignStep.tsx +2 -2
  20. package/templates/src/components/edit/pane/steps/AiStandardDesignStep.tsx +7 -7
  21. package/templates/src/components/form/advanced/APIConfigSection.tsx +244 -2
  22. package/templates/src/components/form/shopify/SchedulingSection.tsx +354 -0
  23. package/templates/src/components/storykeep/Dashboard.tsx +1 -1
  24. package/templates/src/components/storykeep/Dashboard_Shopify.tsx +253 -110
  25. package/templates/src/components/storykeep/controls/content/BeliefTable.tsx +14 -5
  26. package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +5 -2
  27. package/templates/src/components/storykeep/controls/content/MenuTable.tsx +14 -5
  28. package/templates/src/components/storykeep/controls/content/ProductTable.tsx +180 -101
  29. package/templates/src/components/storykeep/controls/content/ResourceBulkIngest.tsx +9 -5
  30. package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +13 -4
  31. package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +14 -5
  32. package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +111 -0
  33. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +393 -0
  34. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Products.tsx +46 -0
  35. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Schedule.tsx +78 -0
  36. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +55 -0
  37. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Services.tsx +47 -0
  38. package/templates/src/pages/api/auth/lookup-lead.ts +72 -0
  39. package/templates/src/pages/api/booking/availability.ts +72 -0
  40. package/templates/src/pages/api/booking/cancel.ts +73 -0
  41. package/templates/src/pages/api/booking/confirm.ts +82 -0
  42. package/templates/src/pages/api/booking/hold.ts +75 -0
  43. package/templates/src/pages/api/booking/list.ts +66 -0
  44. package/templates/src/pages/api/booking/metrics.ts +60 -0
  45. package/templates/src/pages/api/booking/release.ts +76 -0
  46. package/templates/src/pages/api/sandbox.ts +2 -2
  47. package/templates/src/pages/api/shopify/createCart.ts +4 -8
  48. package/templates/src/pages/api/shopify/getProducts.ts +15 -15
  49. package/templates/src/pages/storykeep/login.astro +21 -14
  50. package/templates/src/stores/shopify.ts +81 -25
  51. package/templates/src/types/tractstack.ts +54 -0
  52. package/templates/src/utils/api/advancedConfig.ts +2 -0
  53. package/templates/src/utils/api/advancedHelpers.ts +40 -3
  54. package/templates/src/utils/api/bookingHelpers.ts +125 -0
  55. package/templates/src/utils/api/brandHelpers.ts +10 -0
  56. package/templates/src/utils/auth.ts +29 -9
  57. package/templates/src/utils/compositor/aiGeneration.ts +3 -3
  58. package/templates/src/utils/compositor/aiPaneParser.ts +2 -2
  59. package/templates/src/utils/customHelpers.ts +0 -21
  60. package/templates/src/utils/profileStorage.ts +5 -0
  61. package/templates/src/utils/tenantResolver.ts +2 -1
  62. package/utils/inject-files.ts +82 -4
  63. package/templates/custom/shopify/CalDotComBooking.tsx +0 -44
@@ -1,5 +1,5 @@
1
- import { useState } from 'react';
2
- import { Pagination } from '@ark-ui/react/pagination';
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 type { ShopifyProduct } from '@/stores/shopify';
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 [currentPage, setCurrentPage] = useState(1);
43
+ const [isDebouncing, setIsDebouncing] = useState(false);
38
44
 
39
- const filteredProducts = products.filter(
40
- (product) =>
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 totalResults = filteredProducts.length;
46
- const totalPages = Math.ceil(totalResults / ITEMS_PER_PAGE);
47
- const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
48
- const paginatedProducts = filteredProducts.slice(
49
- startIndex,
50
- startIndex + ITEMS_PER_PAGE
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 handlePageChange = (page: number) => {
54
- setCurrentPage(page);
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 items-center gap-4">
60
- <div className="flex-1">
61
- <input
62
- type="text"
63
- placeholder="Search products..."
64
- value={searchTerm}
65
- onChange={(e) => {
66
- setSearchTerm(e.target.value);
67
- setCurrentPage(1);
68
- }}
69
- className="w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-cyan-700 focus:ring-cyan-700"
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
- <button
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-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
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
- {paginatedProducts.length === 0 ? (
159
+ {status.error ? (
102
160
  <tr>
103
161
  <td colSpan={3} className="px-6 py-12 text-center">
104
- <div className="text-gray-500">
105
- {products.length === 0
106
- ? 'No products found in store'
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
- paginatedProducts.map((product) => {
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 className="font-bold text-gray-900">
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: {linkedResource.title}
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 className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
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
- {totalPages > 1 && (
203
- <div className="flex justify-center pt-4">
204
- <Pagination.Root
205
- count={totalResults}
206
- pageSize={ITEMS_PER_PAGE}
207
- page={currentPage}
208
- onPageChange={(details) => handlePageChange(details.page)}
209
- >
210
- <Pagination.PrevTrigger className="mr-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">
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
- </Pagination.PrevTrigger>
214
-
215
- <div className="flex items-center gap-1">
216
- <Pagination.Context>
217
- {(pagination) =>
218
- pagination.pages.map((page, index) =>
219
- page.type === 'page' ? (
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
- </Pagination.NextTrigger>
249
- </Pagination.Root>
250
- </div>
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
- {error.field} - {error.message}
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-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
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 className="whitespace-nowrap px-6 py-4 text-sm font-bold text-gray-900">
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 className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
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 className="px-6 py-4 text-sm text-gray-500">
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-hidden rounded-lg border border-gray-200 bg-white shadow">
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="overflow-hidden">
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 className="text-sm font-bold text-gray-900">
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 className="text-sm text-gray-500 md:hidden">
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 className="hidden whitespace-nowrap px-3 py-4 text-sm text-gray-500 md:table-cell md:px-6">
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
+ }