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
@@ -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}
@@ -9,6 +9,10 @@ import BooleanToggle from '@/components/form/BooleanToggle';
9
9
  import DateTimeInput from '@/components/form/DateTimeInput';
10
10
  import FileUpload from '@/components/form/FileUpload';
11
11
  import EnumSelect from '@/components/form/EnumSelect';
12
+ import {
13
+ resourceFormHideFields,
14
+ resourceJsonifyFields,
15
+ } from '@/utils/customHelpers';
12
16
  import type {
13
17
  ResourceConfig,
14
18
  ResourceState,
@@ -46,15 +50,16 @@ export default function ResourceForm({
46
50
  actionLisp: '',
47
51
  };
48
52
 
49
- // Initialize optionsPayload with default values for all schema fields
53
+ // 1. Initialize optionsPayload with default values for all schema fields
54
+ // (Only runs if NO existing data is provided)
50
55
  if (!resourceData) {
51
- // Only for new resources
52
56
  const defaultOptionsPayload: Record<string, any> = {};
53
57
 
54
58
  Object.entries(categorySchema).forEach(([fieldName, fieldDef]) => {
55
59
  switch (fieldDef.type) {
56
60
  case 'number':
57
- defaultOptionsPayload[fieldName] = fieldDef.defaultValue ?? 0;
61
+ defaultOptionsPayload[fieldName] =
62
+ fieldDef.defaultValue ?? fieldDef.minNumber ?? 0;
58
63
  break;
59
64
  case 'boolean':
60
65
  defaultOptionsPayload[fieldName] = fieldDef.defaultValue ?? false;
@@ -79,6 +84,27 @@ export default function ResourceForm({
79
84
  initialData.optionsPayload = defaultOptionsPayload;
80
85
  }
81
86
 
87
+ // 2. Pre-process JSON fields for display (Pretty Print)
88
+ // This runs for both new and existing records to ensure readability
89
+ if (initialData.optionsPayload) {
90
+ resourceJsonifyFields.forEach((field) => {
91
+ const val = initialData.optionsPayload[field];
92
+ if (val && typeof val === 'string') {
93
+ try {
94
+ // Parse and re-stringify with indentation
95
+ initialData.optionsPayload[field] = JSON.stringify(
96
+ JSON.parse(val),
97
+ null,
98
+ 2
99
+ );
100
+ } catch (e) {
101
+ // If it's not valid JSON, leave it as is
102
+ console.warn(`Failed to pretty-print field ${field}`, e);
103
+ }
104
+ }
105
+ });
106
+ }
107
+
82
108
  const validator = (state: ResourceState): FieldErrors => {
83
109
  const errors: FieldErrors = {};
84
110
 
@@ -114,9 +140,34 @@ export default function ResourceForm({
114
140
  validator,
115
141
  onSave: async (data) => {
116
142
  try {
143
+ // 3. Post-process JSON fields for saving (Minify)
144
+ const dataToSave = { ...data };
145
+ if (dataToSave.optionsPayload) {
146
+ // Create a shallow copy of optionsPayload to avoid mutating form state directly
147
+ dataToSave.optionsPayload = { ...dataToSave.optionsPayload };
148
+
149
+ resourceJsonifyFields.forEach((field) => {
150
+ const val = dataToSave.optionsPayload[field];
151
+ if (val && typeof val === 'string') {
152
+ try {
153
+ // Minify back to a compact string
154
+ dataToSave.optionsPayload[field] = JSON.stringify(
155
+ JSON.parse(val)
156
+ );
157
+ } catch (e) {
158
+ console.error(`Failed to minify field ${field}`, e);
159
+ // Throwing here would stop the save if the user typed invalid JSON
160
+ throw new Error(
161
+ `Invalid JSON in field "${field}". Please check your syntax.`
162
+ );
163
+ }
164
+ }
165
+ });
166
+ }
167
+
117
168
  const updatedState = await saveResourceWithStateUpdate(
118
169
  window.TRACTSTACK_CONFIG?.tenantId || 'default',
119
- data
170
+ dataToSave
120
171
  );
121
172
 
122
173
  // Call success callback after save (original pattern)
@@ -137,14 +188,19 @@ export default function ResourceForm({
137
188
  // Helper to get category reference options for a field
138
189
  const getCategoryReferenceOptions = (belongsToCategory: string) => {
139
190
  return fullContentMap
140
- .filter((item) => item.categorySlug === belongsToCategory)
191
+ .filter(
192
+ (item) =>
193
+ item.categorySlug === belongsToCategory &&
194
+ !(
195
+ belongsToCategory === 'service' && (item as any).optionsPayload?.gid
196
+ )
197
+ )
141
198
  .map((item) => ({
142
199
  value: item.slug,
143
200
  label: item.title,
144
201
  }));
145
202
  };
146
203
 
147
- // Helper to update optionsPayload field
148
204
  const updateOptionsField = (fieldName: string, value: any) => {
149
205
  updateField('optionsPayload', {
150
206
  ...state.optionsPayload,
@@ -156,11 +212,51 @@ export default function ResourceForm({
156
212
  onClose?.(false);
157
213
  };
158
214
 
159
- // Render dynamic field based on field definition
160
215
  const renderDynamicField = (fieldName: string, fieldDef: FieldDefinition) => {
216
+ if (
217
+ resourceFormHideFields.includes(fieldName)
218
+ // && initialData.optionsPayload?.[fieldName]
219
+ ) {
220
+ return null;
221
+ }
222
+
161
223
  const fieldValue = state.optionsPayload[fieldName];
162
224
  const fieldError = errors?.[`optionsPayload.${fieldName}`];
163
225
 
226
+ if (resourceJsonifyFields.includes(fieldName)) {
227
+ return (
228
+ <div key={fieldName} className="space-y-1">
229
+ <label
230
+ htmlFor={`field-${fieldName}`}
231
+ className="block text-sm font-bold text-gray-700"
232
+ >
233
+ {fieldName.charAt(0).toUpperCase() + fieldName.slice(1)}
234
+ </label>
235
+ <div className="relative">
236
+ <textarea
237
+ id={`field-${fieldName}`}
238
+ rows={12}
239
+ className={`block w-full rounded-md font-mono text-xs shadow-sm ${
240
+ fieldError
241
+ ? 'border-red-300 focus:border-red-500 focus:ring-red-500'
242
+ : 'border-gray-300 focus:border-cyan-500 focus:ring-cyan-500'
243
+ }`}
244
+ value={fieldValue || ''}
245
+ onChange={(e) => updateOptionsField(fieldName, e.target.value)}
246
+ placeholder="{}"
247
+ />
248
+ </div>
249
+ {fieldError ? (
250
+ <p className="mt-1 text-sm text-red-600">{fieldError}</p>
251
+ ) : (
252
+ <p className="mt-1 text-xs text-gray-500">
253
+ Raw JSON configuration. Edits are validated on save.
254
+ </p>
255
+ )}
256
+ </div>
257
+ );
258
+ }
259
+
164
260
  switch (fieldDef.type) {
165
261
  case 'string':
166
262
  // Check if this string field references another category
@@ -349,7 +445,11 @@ export default function ResourceForm({
349
445
 
350
446
  {/* Save/Cancel Bar */}
351
447
  <UnsavedChangesBar
352
- formState={formState}
448
+ formState={{
449
+ ...formState,
450
+ isDirty: isCreate || formState.isDirty,
451
+ cancel: handleCancel,
452
+ }}
353
453
  message="You have unsaved resource changes"
354
454
  saveLabel="Save Resource"
355
455
  cancelLabel="Discard Changes"
@@ -117,7 +117,7 @@ export default function ResourceTable({
117
117
  </div>
118
118
 
119
119
  {/* Table */}
120
- <div className="overflow-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
+ }