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.
Files changed (85) hide show
  1. package/README.md +1 -1
  2. package/bin/create-tractstack.js +2 -2
  3. package/dist/index.js +177 -18
  4. package/package.json +4 -2
  5. package/templates/custom/minimal/CodeHook.astro +22 -5
  6. package/templates/custom/shopify/Cart.tsx +372 -0
  7. package/templates/custom/shopify/CartIcon.tsx +47 -0
  8. package/templates/custom/shopify/CartModal.tsx +63 -0
  9. package/templates/custom/shopify/CheckoutModal.tsx +576 -0
  10. package/templates/custom/shopify/NativeBookingCalendar.tsx +375 -0
  11. package/templates/custom/shopify/ShopifyCartManager.tsx +200 -0
  12. package/templates/custom/shopify/ShopifyCheckout.tsx +167 -0
  13. package/templates/custom/shopify/ShopifyProductGrid.tsx +247 -0
  14. package/templates/custom/shopify/ShopifyServiceList.tsx +135 -0
  15. package/templates/custom/shopify/cart.astro +23 -0
  16. package/templates/custom/with-examples/CodeHook.astro +17 -1
  17. package/templates/custom/with-examples/ProductGrid.astro +1 -1
  18. package/templates/src/client/app.js +4 -2
  19. package/templates/src/components/Footer.astro +4 -4
  20. package/templates/src/components/Header.astro +44 -12
  21. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +3 -3
  22. package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +2 -2
  23. package/templates/src/components/edit/pane/steps/AiCreativeDesignStep.tsx +2 -2
  24. package/templates/src/components/edit/pane/steps/AiLibraryCopyStep.tsx +3 -3
  25. package/templates/src/components/edit/pane/steps/AiRefineDesignStep.tsx +2 -2
  26. package/templates/src/components/edit/pane/steps/AiStandardDesignStep.tsx +7 -7
  27. package/templates/src/components/form/advanced/APIConfigSection.tsx +407 -38
  28. package/templates/src/components/form/shopify/SchedulingSection.tsx +354 -0
  29. package/templates/src/components/storykeep/Dashboard.tsx +18 -4
  30. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -0
  31. package/templates/src/components/storykeep/Dashboard_Content.tsx +5 -96
  32. package/templates/src/components/storykeep/Dashboard_Shopify.tsx +668 -0
  33. package/templates/src/components/storykeep/StoryKeepBackdrop.astro +43 -23
  34. package/templates/src/components/storykeep/controls/content/BeliefTable.tsx +14 -5
  35. package/templates/src/components/storykeep/controls/content/ContentBrowser.tsx +0 -14
  36. package/templates/src/components/storykeep/controls/content/KnownResourceForm.tsx +36 -13
  37. package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +5 -2
  38. package/templates/src/components/storykeep/controls/content/ManageContent.tsx +4 -11
  39. package/templates/src/components/storykeep/controls/content/MenuTable.tsx +14 -5
  40. package/templates/src/components/storykeep/controls/content/ProductTable.tsx +333 -0
  41. package/templates/src/components/storykeep/controls/content/ResourceBulkIngest.tsx +9 -5
  42. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +108 -8
  43. package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +13 -4
  44. package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +14 -5
  45. package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +111 -0
  46. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +393 -0
  47. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Products.tsx +46 -0
  48. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Schedule.tsx +78 -0
  49. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +55 -0
  50. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Services.tsx +47 -0
  51. package/templates/src/lib/resources.ts +11 -21
  52. package/templates/src/pages/api/auth/lookup-lead.ts +72 -0
  53. package/templates/src/pages/api/booking/availability.ts +72 -0
  54. package/templates/src/pages/api/booking/cancel.ts +73 -0
  55. package/templates/src/pages/api/booking/confirm.ts +82 -0
  56. package/templates/src/pages/api/booking/hold.ts +75 -0
  57. package/templates/src/pages/api/booking/list.ts +66 -0
  58. package/templates/src/pages/api/booking/metrics.ts +60 -0
  59. package/templates/src/pages/api/booking/release.ts +76 -0
  60. package/templates/src/pages/api/sandbox.ts +2 -2
  61. package/templates/src/pages/api/shopify/createCart.ts +69 -0
  62. package/templates/src/pages/api/shopify/getProducts.ts +64 -0
  63. package/templates/src/pages/storykeep/login.astro +26 -24
  64. package/templates/src/pages/storykeep/logout.astro +1 -10
  65. package/templates/src/pages/storykeep/manage.astro +69 -0
  66. package/templates/src/pages/storykeep/{content.astro → pages.astro} +4 -8
  67. package/templates/src/pages/storykeep/shopify.astro +101 -0
  68. package/templates/src/stores/navigation.ts +3 -42
  69. package/templates/src/stores/nodes.ts +3 -1
  70. package/templates/src/stores/resources.ts +7 -10
  71. package/templates/src/stores/shopify.ts +266 -0
  72. package/templates/src/types/tractstack.ts +75 -0
  73. package/templates/src/utils/api/advancedConfig.ts +7 -1
  74. package/templates/src/utils/api/advancedHelpers.ts +87 -7
  75. package/templates/src/utils/api/bookingHelpers.ts +125 -0
  76. package/templates/src/utils/api/brandHelpers.ts +14 -0
  77. package/templates/src/utils/api/resourceConfig.ts +13 -5
  78. package/templates/src/utils/auth.ts +29 -9
  79. package/templates/src/utils/compositor/aiGeneration.ts +3 -3
  80. package/templates/src/utils/compositor/aiPaneParser.ts +2 -2
  81. package/templates/src/utils/customHelpers.ts +49 -0
  82. package/templates/src/utils/helpers.ts +59 -0
  83. package/templates/src/utils/profileStorage.ts +5 -0
  84. package/templates/src/utils/tenantResolver.ts +2 -1
  85. package/utils/inject-files.ts +161 -2
@@ -0,0 +1,668 @@
1
+ import { useEffect, useState, useMemo } from 'react';
2
+ import { useStore } from '@nanostores/react';
3
+ import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
4
+ import ClipboardDocumentIcon from '@heroicons/react/24/outline/ClipboardDocumentIcon';
5
+ import CheckIcon from '@heroicons/react/24/outline/CheckIcon';
6
+ import {
7
+ shopifyStatus,
8
+ shopifyActiveTabStore,
9
+ type ShopifyProduct,
10
+ } from '@/stores/shopify';
11
+ import ResourceForm from './controls/content/ResourceForm';
12
+ import { saveBrandConfigWithStateUpdate } from '@/utils/api/brandConfig';
13
+ import {
14
+ deleteResource,
15
+ getResource,
16
+ getResourcesByCategory,
17
+ } from '@/utils/api/resourceConfig';
18
+ import { convertToLocalState } from '@/utils/api/brandHelpers';
19
+ import BooleanToggle from '@/components/form/BooleanToggle';
20
+ import type { BrandConfig, BrandConfigState } from '@/types/tractstack';
21
+ import type { ResourceNode } from '@/types/compositorTypes';
22
+ import type { ResourceConfig } from '@/types/tractstack';
23
+ import ShopifyDashboard from './shopify/ShopifyDashboard';
24
+ import ShopifyDashboard_Products from './shopify/ShopifyDashboard_Products';
25
+ import ShopifyDashboard_Services from './shopify/ShopifyDashboard_Services';
26
+ import ShopifyDashboard_Schedule from './shopify/ShopifyDashboard_Schedule';
27
+ import ShopifyDashboard_Search from './shopify/ShopifyDashboard_Search';
28
+ import ShopifyDashboard_Bookings from './shopify/ShopifyDashboard_Bookings';
29
+
30
+ interface DashboardShopifyProps {
31
+ brandConfig: BrandConfig;
32
+ existingResources: ResourceNode[];
33
+ }
34
+
35
+ type MachineState = 'INIT' | 'CONFIG' | 'UPDATE' | 'READY';
36
+
37
+ export default function StoryKeepDashboard_Shopify({
38
+ brandConfig,
39
+ existingResources,
40
+ }: DashboardShopifyProps) {
41
+ const status = useStore(shopifyStatus);
42
+ const activeTab = useStore(shopifyActiveTabStore);
43
+
44
+ const [selectedProduct, setSelectedProduct] = useState<ShopifyProduct | null>(
45
+ null
46
+ );
47
+ const [copied, setCopied] = useState(false);
48
+
49
+ const [resources, setResources] = useState<ResourceNode[]>(existingResources);
50
+
51
+ const [draftResource, setDraftResource] =
52
+ useState<Partial<ResourceConfig> | null>(null);
53
+ const [showResourceModal, setShowResourceModal] = useState(false);
54
+ const [isCreateMode, setIsCreateMode] = useState(true);
55
+ const [targetProduct, setTargetProduct] = useState<ShopifyProduct | null>(
56
+ null
57
+ );
58
+ const [showTypeSelector, setShowTypeSelector] = useState(false);
59
+
60
+ const [showSmartCartWarning, setShowSmartCartWarning] = useState(false);
61
+ const [pendingImport, setPendingImport] = useState<{
62
+ category: string;
63
+ product: ShopifyProduct;
64
+ } | null>(null);
65
+
66
+ const [machineState, setMachineState] = useState<MachineState>('INIT');
67
+ const [internalBrandConfig, setInternalBrandConfig] =
68
+ useState<BrandConfigState | null>(null);
69
+
70
+ const [wantProduct, setWantProduct] = useState(true);
71
+ const [wantService, setWantService] = useState(true);
72
+ const [isSaving, setIsSaving] = useState(false);
73
+
74
+ // Tab definitions
75
+ const tabs = [
76
+ { id: 'dashboards', name: 'Dashboard' },
77
+ { id: 'bookings', name: 'Bookings' },
78
+ { id: 'products', name: 'Products' },
79
+ { id: 'services', name: 'Services' },
80
+ { id: 'schedule', name: 'Schedule' },
81
+ { id: 'search', name: 'Search' },
82
+ ];
83
+
84
+ useEffect(() => {
85
+ if (brandConfig) {
86
+ const localState = convertToLocalState(brandConfig);
87
+ setInternalBrandConfig(localState);
88
+
89
+ const hasProduct = !!localState.knownResources['product'];
90
+ if (hasProduct) {
91
+ setMachineState('READY');
92
+ } else {
93
+ setMachineState('CONFIG');
94
+ }
95
+ }
96
+ }, [brandConfig]);
97
+
98
+ const linkedResourceMap = useMemo(() => {
99
+ const map = new Map<string, ResourceNode>();
100
+ resources.forEach((r) => {
101
+ if (r.optionsPayload?.gid) {
102
+ map.set(r.optionsPayload.gid, r);
103
+ }
104
+ });
105
+ return map;
106
+ }, [resources]);
107
+
108
+ const refreshResources = async () => {
109
+ const tenantId = window.TRACTSTACK_CONFIG?.tenantId || 'default';
110
+ try {
111
+ const promises = [];
112
+ if (internalBrandConfig?.knownResources['product']) {
113
+ promises.push(getResourcesByCategory(tenantId, 'product'));
114
+ }
115
+ if (internalBrandConfig?.knownResources['service']) {
116
+ promises.push(getResourcesByCategory(tenantId, 'service'));
117
+ }
118
+
119
+ const results = await Promise.all(promises);
120
+ const flattened = results.flat() as ResourceNode[];
121
+
122
+ setResources((prev) => {
123
+ const otherTypes = prev.filter(
124
+ (r) => r.categorySlug !== 'product' && r.categorySlug !== 'service'
125
+ );
126
+ return [...otherTypes, ...flattened];
127
+ });
128
+ } catch (e) {
129
+ console.error('Failed to refresh resources', e);
130
+ }
131
+ };
132
+
133
+ const handleLink = (product: ShopifyProduct) => {
134
+ const hasProductSchema = !!internalBrandConfig?.knownResources['product'];
135
+ const hasServiceSchema = !!internalBrandConfig?.knownResources['service'];
136
+
137
+ if (hasProductSchema && hasServiceSchema) {
138
+ setTargetProduct(product);
139
+ setShowTypeSelector(true);
140
+ } else if (hasServiceSchema) {
141
+ executePreFlightCheck('service', product);
142
+ } else {
143
+ executePreFlightCheck('product', product);
144
+ }
145
+ };
146
+
147
+ // Handler for ProductTable (External Catalog)
148
+ const handleEditFromCatalog = (
149
+ _product: ShopifyProduct,
150
+ resource: ResourceNode
151
+ ) => {
152
+ setDraftResource(resource as any);
153
+ setIsCreateMode(false);
154
+ setShowResourceModal(true);
155
+ };
156
+
157
+ // Handler for ResourceTable (Local Management)
158
+ const handleEditResource = async (resourceId: string) => {
159
+ try {
160
+ const resource = await getResource(
161
+ window.TRACTSTACK_CONFIG?.tenantId || 'default',
162
+ resourceId
163
+ );
164
+ setDraftResource(resource as any);
165
+ setIsCreateMode(false);
166
+ setShowResourceModal(true);
167
+ } catch (error) {
168
+ console.error('Failed to load resource for editing:', error);
169
+ }
170
+ };
171
+
172
+ const executePreFlightCheck = (category: string, product: ShopifyProduct) => {
173
+ const hasMode = product.options.some((opt) => opt.name === 'Mode');
174
+ if (hasMode) {
175
+ startCreateFlow(category, product);
176
+ } else {
177
+ setPendingImport({ category, product });
178
+ setShowSmartCartWarning(true);
179
+ setShowTypeSelector(false);
180
+ }
181
+ };
182
+
183
+ const startCreateFlow = (category: string, product: ShopifyProduct) => {
184
+ const schema = internalBrandConfig?.knownResources[category] || {};
185
+ const mergedOptions: Record<string, any> = {
186
+ gid: product.id,
187
+ shopifyData: JSON.stringify(product),
188
+ };
189
+
190
+ Object.entries(schema).forEach(([key, def]) => {
191
+ if (mergedOptions[key] === undefined) {
192
+ if (def.type === 'number') {
193
+ mergedOptions[key] = def.defaultValue ?? def.minNumber ?? 0;
194
+ } else if (def.type === 'boolean') {
195
+ mergedOptions[key] = def.defaultValue ?? false;
196
+ } else if (def.type === 'string') {
197
+ mergedOptions[key] = def.defaultValue ?? '';
198
+ } else if (def.type === 'multi') {
199
+ mergedOptions[key] = def.defaultValue ?? [];
200
+ }
201
+ }
202
+ });
203
+
204
+ setDraftResource({
205
+ title: product.title,
206
+ oneliner: product.description || '',
207
+ slug: `${category}-${product.handle}`.toLowerCase(),
208
+ categorySlug: category,
209
+ optionsPayload: mergedOptions,
210
+ });
211
+
212
+ setIsCreateMode(true);
213
+ setShowTypeSelector(false);
214
+ setShowResourceModal(true);
215
+ };
216
+
217
+ const handleUnlink = async (resourceId: string) => {
218
+ if (
219
+ !confirm(
220
+ 'Are you sure you want to unlink this resource? Content on your site relying on this link may break.'
221
+ )
222
+ ) {
223
+ return;
224
+ }
225
+
226
+ try {
227
+ await deleteResource(
228
+ window.TRACTSTACK_CONFIG?.tenantId || 'default',
229
+ resourceId
230
+ );
231
+ setResources((prev) => prev.filter((r) => r.id !== resourceId));
232
+ } catch (error) {
233
+ console.error('Unlink failed', error);
234
+ alert('Failed to delete resource');
235
+ }
236
+ };
237
+
238
+ const handleCopy = () => {
239
+ if (selectedProduct) {
240
+ navigator.clipboard.writeText(JSON.stringify(selectedProduct, null, 2));
241
+ setCopied(true);
242
+ setTimeout(() => setCopied(false), 2000);
243
+ }
244
+ };
245
+
246
+ const handleConfigContinue = async () => {
247
+ if (!internalBrandConfig) return;
248
+ setIsSaving(true);
249
+ setMachineState('UPDATE');
250
+
251
+ try {
252
+ const updatedKnownResources = { ...internalBrandConfig.knownResources };
253
+
254
+ if (wantProduct) {
255
+ updatedKnownResources['product'] = {
256
+ gid: { type: 'string', optional: false },
257
+ allowMultiple: { type: 'boolean', optional: false },
258
+ group: { type: 'string', optional: true },
259
+ shopifyData: { type: 'string', optional: false },
260
+ shopifyImage: { type: 'string', optional: true, defaultValue: '{}' },
261
+ ...(wantService
262
+ ? {
263
+ serviceBound: {
264
+ type: 'string',
265
+ optional: true,
266
+ belongsToCategory: 'service',
267
+ },
268
+ }
269
+ : {}),
270
+ };
271
+ }
272
+
273
+ if (wantService) {
274
+ updatedKnownResources['service'] = {
275
+ gid: { type: 'string', optional: true },
276
+ group: { type: 'string', optional: true },
277
+ shopifyData: { type: 'string', optional: true },
278
+ shopifyImage: { type: 'string', optional: true, defaultValue: '{}' },
279
+ bookingLengthMinutes: {
280
+ type: 'number',
281
+ optional: false,
282
+ minNumber: 15,
283
+ maxNumber: 120,
284
+ defaultValue: 15,
285
+ },
286
+ };
287
+ }
288
+
289
+ const updatedState = {
290
+ ...internalBrandConfig,
291
+ knownResources: updatedKnownResources,
292
+ };
293
+
294
+ const freshConfig = await saveBrandConfigWithStateUpdate(
295
+ window.TRACTSTACK_CONFIG?.tenantId || 'default',
296
+ updatedState
297
+ );
298
+
299
+ setInternalBrandConfig(freshConfig);
300
+ setMachineState('READY');
301
+ } catch (error) {
302
+ console.error('Failed to configure Shopify resources:', error);
303
+ setMachineState('CONFIG');
304
+ } finally {
305
+ setIsSaving(false);
306
+ }
307
+ };
308
+
309
+ const handleDismissHelper = async () => {
310
+ if (!internalBrandConfig) return;
311
+ try {
312
+ const updatedState = {
313
+ ...internalBrandConfig,
314
+ showShopifyHelper: false,
315
+ };
316
+ await saveBrandConfigWithStateUpdate(
317
+ window.TRACTSTACK_CONFIG?.tenantId || 'default',
318
+ updatedState
319
+ );
320
+ setInternalBrandConfig(updatedState);
321
+ } catch (error) {
322
+ console.error('Failed to dismiss Shopify helper:', error);
323
+ }
324
+ };
325
+
326
+ if (machineState === 'INIT') return null;
327
+
328
+ if (machineState === 'CONFIG' || machineState === 'UPDATE') {
329
+ return (
330
+ <div className="mt-8 rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
331
+ <div className="mb-6 border-b border-gray-100 pb-4">
332
+ <h2 className="text-xl font-bold text-gray-900">Shopify Setup</h2>
333
+ <p className="mt-1 text-sm text-gray-500">
334
+ Configure how your store interacts with StoryKeep content.
335
+ </p>
336
+ </div>
337
+
338
+ <div className="space-y-6">
339
+ <div className="rounded-md bg-blue-50 p-4">
340
+ <div className="flex">
341
+ <div className="flex-shrink-0">
342
+ <span className="text-blue-400">ℹ️</span>
343
+ </div>
344
+ <div className="ml-3">
345
+ <h3 className="text-sm font-bold text-blue-800">
346
+ Resource Configuration
347
+ </h3>
348
+ <div className="mt-2 text-sm text-blue-700">
349
+ <p>
350
+ We need to create content definitions for your store. Select
351
+ the types of content you plan to manage.
352
+ </p>
353
+ </div>
354
+ </div>
355
+ </div>
356
+ </div>
357
+
358
+ <div className="space-y-4">
359
+ <BooleanToggle
360
+ label="Enable Products"
361
+ description="Creates a 'product' resource type to map Shopify items."
362
+ value={wantProduct}
363
+ onChange={setWantProduct}
364
+ disabled={isSaving}
365
+ />
366
+
367
+ <BooleanToggle
368
+ label="Enable Services"
369
+ description="Creates a 'service' resource type for bookings and appointments."
370
+ value={wantService}
371
+ onChange={setWantService}
372
+ disabled={isSaving}
373
+ />
374
+ </div>
375
+
376
+ <div className="pt-4">
377
+ <button
378
+ onClick={handleConfigContinue}
379
+ disabled={(!wantProduct && !wantService) || isSaving}
380
+ className="inline-flex items-center rounded-md bg-cyan-600 px-4 py-2 text-sm font-bold text-white shadow-sm hover:bg-cyan-500 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2 disabled:opacity-50"
381
+ >
382
+ {isSaving ? 'Configuring...' : 'Continue'}
383
+ </button>
384
+ </div>
385
+ </div>
386
+ </div>
387
+ );
388
+ }
389
+
390
+ return (
391
+ <div className="mt-8 rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
392
+ <div className="mb-6 border-b border-gray-100 pb-4">
393
+ <h2 className="text-xl font-bold text-gray-900">Shopify Integration</h2>
394
+ <p className="mt-1 text-sm text-gray-500">
395
+ Manage your connected Shopify store.
396
+ </p>
397
+ </div>
398
+
399
+ {/* Persistent Onboarding Helper Banner */}
400
+ {internalBrandConfig?.showShopifyHelper && (
401
+ <div className="relative mb-8 rounded-lg border border-cyan-200 bg-cyan-50 p-6 pr-12 shadow-sm">
402
+ <button
403
+ onClick={handleDismissHelper}
404
+ className="absolute right-4 top-4 text-cyan-600 hover:text-cyan-800"
405
+ title="Dismiss instructions"
406
+ >
407
+ <XMarkIcon className="h-6 w-6" />
408
+ </button>
409
+ <h3 className="text-lg font-bold text-cyan-900">
410
+ Smart Cart Architecture: "Mode" Options
411
+ </h3>
412
+ <div className="mt-2 space-y-3 text-sm text-cyan-800">
413
+ <p>
414
+ To enable automatic shipping fee bypass for local pickup, your
415
+ Shopify products must be configured with a specific architectural
416
+ pattern:
417
+ </p>
418
+ <ul className="list-inside list-disc space-y-1 font-bold">
419
+ <li>
420
+ Option Name: Must be exactly{' '}
421
+ <code className="rounded bg-cyan-100 px-1">Mode</code>
422
+ </li>
423
+ <li>
424
+ Option Values: Must include{' '}
425
+ <code className="rounded bg-cyan-100 px-1">Shipped</code> and{' '}
426
+ <code className="rounded bg-cyan-100 px-1">Pickup</code>
427
+ </li>
428
+ </ul>
429
+ <p>
430
+ Without this "Mode" option, items will always be treated as
431
+ standard shipped products.
432
+ </p>
433
+ </div>
434
+ </div>
435
+ )}
436
+
437
+ {/* Tab Navigation Shell */}
438
+ <div className="mb-8 border-b border-gray-200">
439
+ <nav className="-mb-px flex flex-wrap gap-x-8" aria-label="Tabs">
440
+ {tabs.map((tab) => (
441
+ <button
442
+ key={tab.id}
443
+ onClick={() => shopifyActiveTabStore.set(tab.id)}
444
+ className={`whitespace-nowrap border-b-2 px-1 py-4 text-sm font-bold ${
445
+ activeTab === tab.id
446
+ ? 'border-cyan-500 text-cyan-600'
447
+ : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'
448
+ } `}
449
+ >
450
+ {tab.name}
451
+ </button>
452
+ ))}
453
+ </nav>
454
+ </div>
455
+
456
+ <div className="space-y-6">
457
+ {status.error && (
458
+ <div className="mb-6 rounded-md bg-red-50 p-4">
459
+ <div className="ml-3">
460
+ <h3 className="text-sm font-bold text-red-800">Error</h3>
461
+ <div className="mt-2 text-sm text-red-700">
462
+ <p>{status.error}</p>
463
+ </div>
464
+ </div>
465
+ </div>
466
+ )}
467
+
468
+ {activeTab === 'dashboards' && (
469
+ <ShopifyDashboard existingResources={resources} />
470
+ )}
471
+
472
+ {activeTab === 'bookings' && (
473
+ <ShopifyDashboard_Bookings existingResources={resources} />
474
+ )}
475
+
476
+ {/* Local Management Tabs */}
477
+ {activeTab === 'products' && (
478
+ <ShopifyDashboard_Products
479
+ resources={resources}
480
+ onEdit={handleEditResource}
481
+ onCreate={() => shopifyActiveTabStore.set('search')}
482
+ onRefresh={refreshResources}
483
+ />
484
+ )}
485
+
486
+ {activeTab === 'services' && (
487
+ <ShopifyDashboard_Services
488
+ resources={resources}
489
+ onEdit={handleEditResource}
490
+ onCreate={() => shopifyActiveTabStore.set('search')}
491
+ onRefresh={refreshResources}
492
+ />
493
+ )}
494
+
495
+ {/* Schedule Tab */}
496
+ {activeTab === 'schedule' && (
497
+ <ShopifyDashboard_Schedule brandConfig={brandConfig} />
498
+ )}
499
+
500
+ {/* Catalog Discovery Tab */}
501
+ {activeTab === 'search' && (
502
+ <ShopifyDashboard_Search
503
+ linkedResourceMap={linkedResourceMap}
504
+ onSelectProduct={setSelectedProduct}
505
+ onLink={handleLink}
506
+ onUnlink={handleUnlink}
507
+ onEdit={handleEditFromCatalog}
508
+ />
509
+ )}
510
+ </div>
511
+
512
+ {/* Shared Modals */}
513
+ {selectedProduct && (
514
+ <div className="relative z-50" aria-modal="true">
515
+ <div className="fixed inset-0 bg-black bg-opacity-75" />
516
+ <div className="fixed inset-0 flex items-end justify-center p-4 md:items-center">
517
+ <div
518
+ className="flex w-full flex-col overflow-hidden rounded-lg bg-white shadow-xl md:max-w-4xl"
519
+ style={{ maxHeight: '90vh' }}
520
+ >
521
+ <div className="flex items-center justify-between border-b border-gray-200 px-6 py-4">
522
+ <h3 className="text-lg font-bold text-gray-900">
523
+ {selectedProduct.title}
524
+ </h3>
525
+ <button
526
+ type="button"
527
+ className="rounded-md bg-white text-gray-400 hover:text-gray-50"
528
+ onClick={() => setSelectedProduct(null)}
529
+ >
530
+ <XMarkIcon className="h-6 w-6" />
531
+ </button>
532
+ </div>
533
+ <div className="overflow-y-auto p-6">
534
+ <div className="mb-2 flex items-center justify-between">
535
+ <h4 className="text-sm font-bold text-gray-900">
536
+ Raw API Data
537
+ </h4>
538
+ <button
539
+ onClick={handleCopy}
540
+ className="flex items-center gap-1 text-xs text-cyan-600 hover:text-cyan-800"
541
+ >
542
+ {copied ? (
543
+ <CheckIcon className="h-4 w-4" />
544
+ ) : (
545
+ <ClipboardDocumentIcon className="h-4 w-4" />
546
+ )}
547
+ {copied ? 'Copied' : 'Copy JSON'}
548
+ </button>
549
+ </div>
550
+ <pre
551
+ className="overflow-auto rounded-md bg-gray-50 p-4 text-xs text-gray-800"
552
+ style={{ maxHeight: '40vh' }}
553
+ >
554
+ {JSON.stringify(selectedProduct, null, 2)}
555
+ </pre>
556
+ </div>
557
+ </div>
558
+ </div>
559
+ </div>
560
+ )}
561
+
562
+ {showTypeSelector && targetProduct && (
563
+ <div className="relative z-50" aria-modal="true">
564
+ <div className="fixed inset-0 bg-black bg-opacity-50" />
565
+ <div className="fixed inset-0 flex items-center justify-center p-4">
566
+ <div className="w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-xl">
567
+ <div className="px-6 py-4">
568
+ <h3 className="text-lg font-bold text-gray-900">
569
+ Import as...
570
+ </h3>
571
+ <p className="mt-2 text-sm text-gray-500">
572
+ Should "{targetProduct.title}" be a Product or Service?
573
+ </p>
574
+ <div className="mt-6 flex flex-col gap-3">
575
+ <button
576
+ onClick={() =>
577
+ executePreFlightCheck('product', targetProduct)
578
+ }
579
+ className="flex w-full items-center justify-center rounded-md bg-cyan-600 px-4 py-2 text-sm font-bold text-white shadow-sm hover:bg-cyan-500"
580
+ >
581
+ Product
582
+ </button>
583
+ <button
584
+ onClick={() =>
585
+ executePreFlightCheck('service', targetProduct)
586
+ }
587
+ className="flex w-full items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-bold text-gray-700 shadow-sm hover:bg-gray-50"
588
+ >
589
+ Service (Bookable)
590
+ </button>
591
+ </div>
592
+ </div>
593
+ </div>
594
+ </div>
595
+ </div>
596
+ )}
597
+
598
+ {showSmartCartWarning && pendingImport && (
599
+ <div className="relative z-50" aria-modal="true">
600
+ <div className="fixed inset-0 bg-black bg-opacity-50" />
601
+ <div className="fixed inset-0 flex items-center justify-center p-4">
602
+ <div className="w-full max-w-md overflow-hidden rounded-lg bg-white shadow-xl">
603
+ <div className="px-6 py-4">
604
+ <h3 className="text-lg font-bold text-gray-900">
605
+ Missing Smart Cart Architecture
606
+ </h3>
607
+ <p className="mt-2 text-sm text-gray-500">
608
+ To enable Smart Cart pickup, the product must have a "Mode"
609
+ option containing "Shipped" and "Pickup". If you import now,
610
+ it will be standard shipping only.
611
+ </p>
612
+ <div className="mt-6 flex flex-col gap-3">
613
+ <button
614
+ onClick={() => {
615
+ setShowSmartCartWarning(false);
616
+ startCreateFlow(
617
+ pendingImport.category,
618
+ pendingImport.product
619
+ );
620
+ setPendingImport(null);
621
+ }}
622
+ className="flex w-full items-center justify-center rounded-md bg-cyan-600 px-4 py-2 text-sm font-bold text-white shadow-sm hover:bg-cyan-500"
623
+ >
624
+ Acknowledge & Import
625
+ </button>
626
+ <button
627
+ onClick={() => {
628
+ setShowSmartCartWarning(false);
629
+ setPendingImport(null);
630
+ }}
631
+ className="flex w-full items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-bold text-gray-700 shadow-sm hover:bg-gray-50"
632
+ >
633
+ Cancel
634
+ </button>
635
+ </div>
636
+ </div>
637
+ </div>
638
+ </div>
639
+ </div>
640
+ )}
641
+
642
+ {showResourceModal && draftResource && (
643
+ <div className="fixed inset-0 z-50 overflow-y-auto bg-gray-900 bg-opacity-50 backdrop-blur-sm">
644
+ <div className="flex min-h-full items-center justify-center p-4">
645
+ <div className="w-full max-w-2xl rounded-lg bg-white p-6 shadow-xl">
646
+ <ResourceForm
647
+ resourceData={draftResource as any}
648
+ fullContentMap={resources as any}
649
+ categorySlug={draftResource.categorySlug || ''}
650
+ categorySchema={
651
+ internalBrandConfig?.knownResources[
652
+ draftResource.categorySlug || ''
653
+ ] || {}
654
+ }
655
+ isCreate={isCreateMode}
656
+ onClose={(saved) => {
657
+ setShowResourceModal(false);
658
+ setIsCreateMode(true);
659
+ if (saved) refreshResources();
660
+ }}
661
+ />
662
+ </div>
663
+ </div>
664
+ </div>
665
+ )}
666
+ </div>
667
+ );
668
+ }