astro-tractstack 2.3.0 → 2.3.2
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 +130 -19
- package/package.json +2 -2
- package/templates/custom/minimal/CodeHook.astro +10 -2
- package/templates/custom/shopify/Cart.tsx +115 -77
- package/templates/custom/shopify/CheckoutModal.tsx +509 -120
- package/templates/custom/shopify/NativeBookingCalendar.tsx +375 -0
- package/templates/custom/shopify/ShopifyCartManager.tsx +91 -45
- package/templates/custom/shopify/ShopifyCheckout.tsx +4 -33
- package/templates/custom/shopify/ShopifyProductGrid.tsx +170 -176
- package/templates/custom/shopify/ShopifyServiceList.tsx +112 -51
- package/templates/custom/with-examples/CodeHook.astro +10 -2
- package/templates/src/components/Footer.astro +6 -6
- package/templates/src/components/Header.astro +23 -11
- package/templates/src/components/Menu.tsx +157 -135
- package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
- package/templates/src/components/codehooks/EpinetDurationSelector.tsx +27 -6
- package/templates/src/components/codehooks/EpinetTableView.tsx +153 -112
- package/templates/src/components/codehooks/EpinetWrapper.tsx +4 -1
- package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +8 -1
- package/templates/src/components/codehooks/ProductCardSetup.tsx +9 -1
- package/templates/src/components/codehooks/ProductGridSetup.tsx +9 -1
- package/templates/src/components/compositor/nodes/BgPaneWrapper.tsx +2 -1
- package/templates/src/components/compositor/nodes/GhostInsertBlock.tsx +1 -1
- package/templates/src/components/edit/ToolBar.tsx +2 -1
- package/templates/src/components/edit/context/ContextPaneConfig_slug.tsx +2 -2
- package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +13 -0
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +3 -3
- package/templates/src/components/edit/pane/AddPanePanel_newCustomCopy.tsx +2 -2
- package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +2 -2
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
- 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/edit/state/SaveModal.tsx +1 -1
- package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +8 -3
- package/templates/src/components/form/DateTimeInput.tsx +10 -3
- package/templates/src/components/form/FileUpload.tsx +11 -5
- package/templates/src/components/form/NumberInput.tsx +2 -2
- package/templates/src/components/form/advanced/APIConfigSection.tsx +208 -2
- package/templates/src/components/form/brand/SiteConfigSection.tsx +10 -0
- 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 +252 -110
- package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +2 -2
- 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 +88 -56
- package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +14 -4
- package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +14 -5
- package/templates/src/components/storykeep/email-builder/Blocks.tsx +169 -0
- package/templates/src/components/storykeep/email-builder/EmailBuilder.tsx +223 -0
- package/templates/src/components/storykeep/email-builder/PreviewModal.tsx +136 -0
- package/templates/src/components/storykeep/email-builder/PropertyPanel.tsx +154 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +104 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +419 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Emails.tsx +105 -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/layouts/Layout.astro +8 -5
- 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 +97 -25
- package/templates/src/types/formTypes.ts +4 -2
- package/templates/src/types/tractstack.ts +59 -2
- 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/brandConfig.ts +2 -0
- package/templates/src/utils/api/brandHelpers.ts +26 -0
- package/templates/src/utils/api/emailHelpers.ts +105 -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 +3 -2
- package/utils/inject-files.ts +116 -5
- package/templates/custom/shopify/CalDotComBooking.tsx +0 -44
|
@@ -183,7 +183,7 @@ export default function MenuTable({
|
|
|
183
183
|
</div>
|
|
184
184
|
|
|
185
185
|
{/* Table Container */}
|
|
186
|
-
<div className="overflow-
|
|
186
|
+
<div className="overflow-x-auto rounded-lg border border-gray-200 bg-white shadow">
|
|
187
187
|
{filteredMenus.length === 0 ? (
|
|
188
188
|
<div className="px-6 py-12 text-center">
|
|
189
189
|
<svg
|
|
@@ -217,7 +217,7 @@ export default function MenuTable({
|
|
|
217
217
|
)}
|
|
218
218
|
</div>
|
|
219
219
|
) : (
|
|
220
|
-
<div className="
|
|
220
|
+
<div className="inline-block min-w-full align-middle">
|
|
221
221
|
<table className="min-w-full divide-y divide-gray-200">
|
|
222
222
|
<thead className="bg-gray-50">
|
|
223
223
|
<tr>
|
|
@@ -247,15 +247,24 @@ export default function MenuTable({
|
|
|
247
247
|
<tr key={menu.id} className="hover:bg-gray-50">
|
|
248
248
|
<td className="px-3 py-4 md:px-6">
|
|
249
249
|
<div className="flex flex-col">
|
|
250
|
-
<div
|
|
250
|
+
<div
|
|
251
|
+
className="max-w-xs truncate text-sm font-bold text-gray-900"
|
|
252
|
+
title={menu.title}
|
|
253
|
+
>
|
|
251
254
|
{menu.title}
|
|
252
255
|
</div>
|
|
253
|
-
<div
|
|
256
|
+
<div
|
|
257
|
+
className="max-w-xs truncate text-sm text-gray-500 md:hidden"
|
|
258
|
+
title={menu.theme || 'No theme'}
|
|
259
|
+
>
|
|
254
260
|
{menu.theme || 'No theme'}
|
|
255
261
|
</div>
|
|
256
262
|
</div>
|
|
257
263
|
</td>
|
|
258
|
-
<td
|
|
264
|
+
<td
|
|
265
|
+
className="hidden max-w-xs truncate whitespace-nowrap px-3 py-4 text-sm text-gray-500 md:table-cell md:px-6"
|
|
266
|
+
title={menu.theme || 'No theme'}
|
|
267
|
+
>
|
|
259
268
|
{menu.theme || 'No theme'}
|
|
260
269
|
</td>
|
|
261
270
|
<td className="hidden whitespace-nowrap px-3 py-4 text-sm xl:table-cell xl:px-6">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import {
|
|
1
|
+
import { useState, useRef, useEffect, type ChangeEvent } 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: 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
|
}
|
|
@@ -7,6 +7,8 @@ interface ResourceBulkIngestProps {
|
|
|
7
7
|
onClose: (saved: boolean) => void;
|
|
8
8
|
onRefresh: () => void;
|
|
9
9
|
fullContentMap: FullContentMapItem[];
|
|
10
|
+
/** When set, placeholder shows one example for this category only (validation still accepts all types). */
|
|
11
|
+
exampleCategorySlug?: string;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
interface ParsedResource {
|
|
@@ -40,10 +42,63 @@ interface FieldDefinition {
|
|
|
40
42
|
maxNumber?: number;
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
function buildExampleObjectForCategory(
|
|
46
|
+
categorySlug: string,
|
|
47
|
+
index: number,
|
|
48
|
+
knownResources: Record<string, Record<string, FieldDefinition>>,
|
|
49
|
+
categoryKeys: string[]
|
|
50
|
+
): Record<string, unknown> {
|
|
51
|
+
const schema = knownResources[categorySlug];
|
|
52
|
+
const example: Record<string, unknown> = {
|
|
53
|
+
title: `Example ${categorySlug.charAt(0).toUpperCase() + categorySlug.slice(1)} ${index + 1}`,
|
|
54
|
+
slug: `${categorySlug}-example-${index + 1}`,
|
|
55
|
+
category: categorySlug,
|
|
56
|
+
oneliner: `A brief description of this ${categorySlug}`,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
Object.entries(schema).forEach(([key, def]: [string, FieldDefinition]) => {
|
|
60
|
+
switch (def.type) {
|
|
61
|
+
case 'string':
|
|
62
|
+
if (
|
|
63
|
+
def.belongsToCategory &&
|
|
64
|
+
categoryKeys.includes(def.belongsToCategory)
|
|
65
|
+
) {
|
|
66
|
+
example[key] = `${def.belongsToCategory}-example-slug`;
|
|
67
|
+
} else {
|
|
68
|
+
example[key] = def.defaultValue || `example ${key}`;
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
case 'number':
|
|
72
|
+
example[key] = def.defaultValue ?? (def.minNumber || 0);
|
|
73
|
+
break;
|
|
74
|
+
case 'boolean':
|
|
75
|
+
example[key] = def.defaultValue ?? true;
|
|
76
|
+
break;
|
|
77
|
+
case 'multi':
|
|
78
|
+
example[key] = def.defaultValue || [
|
|
79
|
+
`example ${key} 1`,
|
|
80
|
+
`example ${key} 2`,
|
|
81
|
+
];
|
|
82
|
+
break;
|
|
83
|
+
case 'date':
|
|
84
|
+
example[key] = new Date().toISOString();
|
|
85
|
+
break;
|
|
86
|
+
case 'image':
|
|
87
|
+
example[key] = 'file-id-placeholder';
|
|
88
|
+
break;
|
|
89
|
+
default:
|
|
90
|
+
example[key] = def.defaultValue || `example ${key}`;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return example;
|
|
95
|
+
}
|
|
96
|
+
|
|
43
97
|
export default function ResourceBulkIngest({
|
|
44
98
|
onClose,
|
|
45
99
|
onRefresh,
|
|
46
100
|
fullContentMap,
|
|
101
|
+
exampleCategorySlug,
|
|
47
102
|
}: ResourceBulkIngestProps) {
|
|
48
103
|
const [brandConfig, setBrandConfig] = useState<BrandConfig | null>(null);
|
|
49
104
|
const [loading, setLoading] = useState(false);
|
|
@@ -418,7 +473,7 @@ export default function ResourceBulkIngest({
|
|
|
418
473
|
};
|
|
419
474
|
}, [jsonInput, knownResources, fullContentMap]);
|
|
420
475
|
|
|
421
|
-
//
|
|
476
|
+
// Placeholder JSON: one category when exampleCategorySlug is set, else one object per known category
|
|
422
477
|
const exampleJson = useMemo(() => {
|
|
423
478
|
const categories = Object.keys(knownResources);
|
|
424
479
|
if (categories.length === 0) {
|
|
@@ -436,59 +491,32 @@ export default function ResourceBulkIngest({
|
|
|
436
491
|
);
|
|
437
492
|
}
|
|
438
493
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
case 'string':
|
|
454
|
-
if (
|
|
455
|
-
def.belongsToCategory &&
|
|
456
|
-
categories.includes(def.belongsToCategory)
|
|
457
|
-
) {
|
|
458
|
-
example[key] = `${def.belongsToCategory}-example-slug`;
|
|
459
|
-
} else {
|
|
460
|
-
example[key] = def.defaultValue || `example ${key}`;
|
|
461
|
-
}
|
|
462
|
-
break;
|
|
463
|
-
case 'number':
|
|
464
|
-
example[key] = def.defaultValue ?? (def.minNumber || 0);
|
|
465
|
-
break;
|
|
466
|
-
case 'boolean':
|
|
467
|
-
example[key] = def.defaultValue ?? true;
|
|
468
|
-
break;
|
|
469
|
-
case 'multi':
|
|
470
|
-
example[key] = def.defaultValue || [
|
|
471
|
-
`example ${key} 1`,
|
|
472
|
-
`example ${key} 2`,
|
|
473
|
-
];
|
|
474
|
-
break;
|
|
475
|
-
case 'date':
|
|
476
|
-
example[key] = new Date().toISOString();
|
|
477
|
-
break;
|
|
478
|
-
case 'image':
|
|
479
|
-
example[key] = 'file-id-placeholder';
|
|
480
|
-
break;
|
|
481
|
-
default:
|
|
482
|
-
example[key] = def.defaultValue || `example ${key}`;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
);
|
|
494
|
+
if (
|
|
495
|
+
exampleCategorySlug &&
|
|
496
|
+
knownResources[exampleCategorySlug] !== undefined
|
|
497
|
+
) {
|
|
498
|
+
const examples = [
|
|
499
|
+
buildExampleObjectForCategory(
|
|
500
|
+
exampleCategorySlug,
|
|
501
|
+
0,
|
|
502
|
+
knownResources,
|
|
503
|
+
categories
|
|
504
|
+
),
|
|
505
|
+
];
|
|
506
|
+
return JSON.stringify(examples, null, 2);
|
|
507
|
+
}
|
|
486
508
|
|
|
487
|
-
|
|
488
|
-
|
|
509
|
+
const examples = categories.map((categorySlug, index) =>
|
|
510
|
+
buildExampleObjectForCategory(
|
|
511
|
+
categorySlug,
|
|
512
|
+
index,
|
|
513
|
+
knownResources,
|
|
514
|
+
categories
|
|
515
|
+
)
|
|
516
|
+
);
|
|
489
517
|
|
|
490
518
|
return JSON.stringify(examples, null, 2);
|
|
491
|
-
}, [knownResources]);
|
|
519
|
+
}, [knownResources, exampleCategorySlug]);
|
|
492
520
|
|
|
493
521
|
const handleSave = useCallback(async () => {
|
|
494
522
|
if (validationResult.validResources.length === 0 || isProcessing) return;
|
|
@@ -595,7 +623,7 @@ export default function ResourceBulkIngest({
|
|
|
595
623
|
|
|
596
624
|
{/* Status Display */}
|
|
597
625
|
<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">
|
|
626
|
+
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
|
|
599
627
|
<span className="font-bold text-gray-900">
|
|
600
628
|
{validationResult.resources.length} resources found
|
|
601
629
|
</span>
|
|
@@ -638,7 +666,7 @@ export default function ResourceBulkIngest({
|
|
|
638
666
|
Validation Errors:
|
|
639
667
|
</p>
|
|
640
668
|
<div className="max-h-32 overflow-y-auto">
|
|
641
|
-
<ul className="space-y-1 text-sm text-red-600">
|
|
669
|
+
<ul className="space-y-1 break-words text-sm text-red-600">
|
|
642
670
|
{validationResult.errors
|
|
643
671
|
.slice(0, 10)
|
|
644
672
|
.map((error, idx) => (
|
|
@@ -648,8 +676,12 @@ export default function ResourceBulkIngest({
|
|
|
648
676
|
? `Item ${error.index + 1}:`
|
|
649
677
|
: 'JSON:'}
|
|
650
678
|
</span>
|
|
651
|
-
<span
|
|
652
|
-
|
|
679
|
+
<span
|
|
680
|
+
className="truncate"
|
|
681
|
+
title={`${error.field} - ${error.message}`}
|
|
682
|
+
>
|
|
683
|
+
<span className="font-bold">{error.field}</span> -{' '}
|
|
684
|
+
{error.message}
|
|
653
685
|
</span>
|
|
654
686
|
</li>
|
|
655
687
|
))}
|
|
@@ -674,7 +706,7 @@ export default function ResourceBulkIngest({
|
|
|
674
706
|
{/* Progress indicator */}
|
|
675
707
|
{isProcessing && progress && (
|
|
676
708
|
<div className="mb-6">
|
|
677
|
-
<div className="mb-2 flex items-center justify-between text-sm">
|
|
709
|
+
<div className="mb-2 flex flex-wrap items-center justify-between gap-2 text-sm">
|
|
678
710
|
<span>
|
|
679
711
|
Processing resource {progress.current + 1} of{' '}
|
|
680
712
|
{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">
|
|
@@ -207,6 +216,7 @@ export default function ResourceTable({
|
|
|
207
216
|
</div>
|
|
208
217
|
{showBulkIngest && (
|
|
209
218
|
<ResourceBulkIngest
|
|
219
|
+
exampleCategorySlug={categorySlug}
|
|
210
220
|
fullContentMap={fullContentMap}
|
|
211
221
|
onClose={(saved) => {
|
|
212
222
|
setShowBulkIngest(false);
|