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
@@ -0,0 +1,354 @@
1
+ import { useState } from 'react';
2
+ import type { BrandConfigState, TimeBlock } from '@/types/tractstack';
3
+ import type { FormStateReturn } from '@/hooks/useFormState';
4
+
5
+ interface SchedulingSectionProps {
6
+ formState: FormStateReturn<BrandConfigState>;
7
+ }
8
+
9
+ const DAYS = [
10
+ 'monday',
11
+ 'tuesday',
12
+ 'wednesday',
13
+ 'thursday',
14
+ 'friday',
15
+ 'saturday',
16
+ 'sunday',
17
+ ];
18
+
19
+ const TIMEZONE_OPTIONS = [
20
+ {
21
+ group: 'North America (Canada & US)',
22
+ zones: [
23
+ { label: 'Pacific Time (Vancouver / BC)', value: 'America/Vancouver' },
24
+ {
25
+ label: 'Pacific Time (Los Angeles / Seattle)',
26
+ value: 'America/Los_Angeles',
27
+ },
28
+ {
29
+ label: 'Mountain Time (Edmonton / Calgary)',
30
+ value: 'America/Edmonton',
31
+ },
32
+ { label: 'Mountain Time (Denver / Salt Lake)', value: 'America/Denver' },
33
+ { label: 'Mountain Time - No DST (Phoenix)', value: 'America/Phoenix' },
34
+ { label: 'Central Time (Winnipeg)', value: 'America/Winnipeg' },
35
+ { label: 'Central Time (Chicago / Dallas)', value: 'America/Chicago' },
36
+ { label: 'Eastern Time (Toronto / Montreal)', value: 'America/Toronto' },
37
+ { label: 'Eastern Time (New York / Miami)', value: 'America/New_York' },
38
+ { label: 'Atlantic Time (Halifax)', value: 'America/Halifax' },
39
+ { label: "Newfoundland Time (St. John's)", value: 'America/St_Johns' },
40
+ { label: 'Alaska Time (Anchorage)', value: 'America/Anchorage' },
41
+ { label: 'Hawaii Time (Honolulu)', value: 'Pacific/Honolulu' },
42
+ ],
43
+ },
44
+ {
45
+ group: 'Latin America',
46
+ zones: [
47
+ { label: 'Mexico City', value: 'America/Mexico_City' },
48
+ { label: 'Bogota / Lima', value: 'America/Bogota' },
49
+ { label: 'São Paulo', value: 'America/Sao_Paulo' },
50
+ ],
51
+ },
52
+ {
53
+ group: 'Europe & Africa',
54
+ zones: [
55
+ { label: 'London / Dublin', value: 'Europe/London' },
56
+ { label: 'Paris / Berlin / Rome', value: 'Europe/Paris' },
57
+ { label: 'Athens / Cairo', value: 'Europe/Athens' },
58
+ { label: 'Lagos', value: 'Africa/Lagos' },
59
+ { label: 'Johannesburg', value: 'Africa/Johannesburg' },
60
+ ],
61
+ },
62
+ {
63
+ group: 'Asia & Oceania',
64
+ zones: [
65
+ { label: 'Dubai', value: 'Asia/Dubai' },
66
+ { label: 'Singapore / HK', value: 'Asia/Singapore' },
67
+ { label: 'Tokyo', value: 'Asia/Tokyo' },
68
+ { label: 'Sydney / Melbourne', value: 'Australia/Sydney' },
69
+ { label: 'Auckland', value: 'Pacific/Auckland' },
70
+ ],
71
+ },
72
+ { label: 'UTC / GMT', value: 'UTC' },
73
+ ];
74
+
75
+ function getUtcFromWallTime(wallTimeIso: string, timeZone: string): string {
76
+ const [datePart, timePart] = wallTimeIso.split('T');
77
+ const [year, month, day] = datePart.split('-').map(Number);
78
+ const [hour, minute] = timePart.split(':').map(Number);
79
+
80
+ const pad = (n: number) => n.toString().padStart(2, '0');
81
+ const pseudoUtc = new Date(
82
+ `${year}-${pad(month)}-${pad(day)}T${pad(hour)}:${pad(minute)}:00Z`
83
+ );
84
+
85
+ const formatter = new Intl.DateTimeFormat('en-US', {
86
+ timeZone,
87
+ year: 'numeric',
88
+ month: '2-digit',
89
+ day: '2-digit',
90
+ hour: '2-digit',
91
+ minute: '2-digit',
92
+ hour12: false,
93
+ });
94
+
95
+ const parts = formatter.formatToParts(pseudoUtc);
96
+ const getPart = (type: string) => parts.find((p) => p.type === type)?.value;
97
+
98
+ const tzDate = new Date(
99
+ `${getPart('year')}-${getPart('month')}-${getPart('day')}T${getPart('hour')}:${getPart('minute')}:00Z`
100
+ );
101
+ const diff = pseudoUtc.getTime() - tzDate.getTime();
102
+
103
+ return new Date(pseudoUtc.getTime() + diff).toISOString();
104
+ }
105
+
106
+ export default function SchedulingSection({
107
+ formState,
108
+ }: SchedulingSectionProps) {
109
+ const { state, updateField } = formState;
110
+ const config = state.scheduling;
111
+ const [newBlock, setNewBlock] = useState<TimeBlock>({ start: '', end: '' });
112
+
113
+ const updateHours = (day: string, field: keyof TimeBlock, value: string) => {
114
+ const updatedHours = { ...config.businessHours };
115
+ if (!updatedHours[day]) {
116
+ updatedHours[day] = { start: '09:00', end: '17:00' };
117
+ }
118
+ updatedHours[day] = { ...updatedHours[day], [field]: value };
119
+ updateField('scheduling', { ...config, businessHours: updatedHours });
120
+ };
121
+
122
+ const toggleDay = (day: string) => {
123
+ const updatedHours = { ...config.businessHours };
124
+ if (updatedHours[day]) {
125
+ delete updatedHours[day];
126
+ } else {
127
+ updatedHours[day] = { start: '09:00', end: '17:00' };
128
+ }
129
+ updateField('scheduling', { ...config, businessHours: updatedHours });
130
+ };
131
+
132
+ const addUnavailableBlock = () => {
133
+ if (!newBlock.start || !newBlock.end) return;
134
+
135
+ const startUtc = getUtcFromWallTime(newBlock.start, config.timezone);
136
+ const endUtc = getUtcFromWallTime(newBlock.end, config.timezone);
137
+
138
+ const updated = [
139
+ ...config.unavailableHours,
140
+ { start: startUtc, end: endUtc },
141
+ ];
142
+ updateField('scheduling', { ...config, unavailableHours: updated });
143
+ setNewBlock({ start: '', end: '' });
144
+ };
145
+
146
+ const removeUnavailableBlock = (index: number) => {
147
+ const updated = config.unavailableHours.filter((_, i) => i !== index);
148
+ updateField('scheduling', { ...config, unavailableHours: updated });
149
+ };
150
+
151
+ return (
152
+ <div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
153
+ <h3 className="mb-6 text-xl font-extrabold text-gray-900">
154
+ Shopify Store
155
+ </h3>
156
+ <div className="space-y-10 divide-y divide-gray-100">
157
+ <div className="pt-2">
158
+ <div className="grid grid-cols-1 gap-x-6 gap-y-8 md:grid-cols-6 xl:grid-cols-12">
159
+ <div className="md:col-span-3 xl:col-span-4">
160
+ <label className="block text-xs font-black uppercase tracking-widest text-gray-500">
161
+ Store Timezone
162
+ </label>
163
+ <select
164
+ value={config.timezone}
165
+ onChange={(e) =>
166
+ updateField('scheduling', {
167
+ ...config,
168
+ timezone: e.target.value,
169
+ })
170
+ }
171
+ className="mt-2 block w-full rounded-md border-gray-300 bg-white px-2 py-3 focus:border-cyan-700 focus:ring-cyan-700 md:text-sm"
172
+ >
173
+ {TIMEZONE_OPTIONS.map((opt) =>
174
+ opt.group ? (
175
+ <optgroup key={opt.group} label={opt.group}>
176
+ {opt.zones.map((z) => (
177
+ <option key={z.value} value={z.value}>
178
+ {z.label}
179
+ </option>
180
+ ))}
181
+ </optgroup>
182
+ ) : (
183
+ <option key={opt.value} value={opt.value}>
184
+ {opt.label}
185
+ </option>
186
+ )
187
+ )}
188
+ </select>
189
+ <p className="mt-1 text-xs font-bold uppercase text-gray-400">
190
+ Stored: {config.timezone}
191
+ </p>
192
+ </div>
193
+ <div className="md:col-span-3 xl:col-span-4">
194
+ <label className="block text-xs font-black uppercase tracking-widest text-gray-500">
195
+ Buffer Gap (Minutes)
196
+ </label>
197
+ <input
198
+ type="number"
199
+ value={config.bufferGapsMinutes}
200
+ onChange={(e) =>
201
+ updateField('scheduling', {
202
+ ...config,
203
+ bufferGapsMinutes: parseInt(e.target.value) || 0,
204
+ })
205
+ }
206
+ className="mt-2 block w-full rounded-md border-gray-300 px-2 py-3 focus:border-cyan-700 focus:ring-cyan-700 md:text-sm"
207
+ />
208
+ </div>
209
+ <div className="md:col-span-3 xl:col-span-4">
210
+ <label className="block text-xs font-black uppercase tracking-widest text-gray-500">
211
+ Max Duration (Minutes)
212
+ </label>
213
+ <input
214
+ type="number"
215
+ value={config.maxLengthMinutes}
216
+ onChange={(e) =>
217
+ updateField('scheduling', {
218
+ ...config,
219
+ maxLengthMinutes: parseInt(e.target.value) || 0,
220
+ })
221
+ }
222
+ className="mt-2 block w-full rounded-md border-gray-300 px-2 py-3 focus:border-cyan-700 focus:ring-cyan-700 md:text-sm"
223
+ />
224
+ </div>
225
+ </div>
226
+ </div>
227
+
228
+ <div className="pt-10">
229
+ <h4 className="text-sm font-black uppercase tracking-widest text-gray-400">
230
+ Weekly Business Hours
231
+ </h4>
232
+ <div className="mt-6 space-y-6">
233
+ {DAYS.map((day) => (
234
+ <div
235
+ key={day}
236
+ className="flex flex-wrap items-center gap-4 md:flex-nowrap"
237
+ >
238
+ <div className="w-24 text-sm font-bold capitalize text-gray-900 md:w-32">
239
+ {day}
240
+ </div>
241
+ <button
242
+ type="button"
243
+ onClick={() => toggleDay(day)}
244
+ className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-cyan-700 focus:ring-offset-2 ${config.businessHours[day] ? 'bg-cyan-600' : 'bg-gray-200'}`}
245
+ >
246
+ <span
247
+ className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${config.businessHours[day] ? 'translate-x-5' : 'translate-x-0'}`}
248
+ />
249
+ </button>
250
+ {config.businessHours[day] && (
251
+ <div className="flex items-center space-x-3">
252
+ <input
253
+ type="time"
254
+ value={config.businessHours[day].start}
255
+ onChange={(e) =>
256
+ updateHours(day, 'start', e.target.value)
257
+ }
258
+ className="rounded-md border-gray-300 px-2 shadow-sm focus:border-cyan-700 focus:ring-cyan-700 md:text-sm"
259
+ />
260
+ <span className="text-xs font-bold text-gray-400">TO</span>
261
+ <input
262
+ type="time"
263
+ value={config.businessHours[day].end}
264
+ onChange={(e) => updateHours(day, 'end', e.target.value)}
265
+ className="rounded-md border-gray-300 px-2 shadow-sm focus:border-cyan-700 focus:ring-cyan-700 md:text-sm"
266
+ />
267
+ </div>
268
+ )}
269
+ </div>
270
+ ))}
271
+ </div>
272
+ </div>
273
+
274
+ <div className="pt-10">
275
+ <h4 className="text-sm font-black uppercase tracking-widest text-gray-400">
276
+ Blackout Dates
277
+ </h4>
278
+ <div className="mt-6 space-y-6">
279
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-3 xl:grid-cols-4">
280
+ <input
281
+ type="datetime-local"
282
+ value={newBlock.start}
283
+ onChange={(e) =>
284
+ setNewBlock({ ...newBlock, start: e.target.value })
285
+ }
286
+ className="rounded-md border-gray-300 px-2 py-3 focus:border-cyan-700 focus:ring-cyan-700 md:text-sm"
287
+ />
288
+ <input
289
+ type="datetime-local"
290
+ value={newBlock.end}
291
+ onChange={(e) =>
292
+ setNewBlock({ ...newBlock, end: e.target.value })
293
+ }
294
+ className="rounded-md border-gray-300 px-2 py-3 focus:border-cyan-700 focus:ring-cyan-700 md:text-sm"
295
+ />
296
+ <button
297
+ type="button"
298
+ onClick={addUnavailableBlock}
299
+ className="inline-flex justify-center rounded-md border border-transparent bg-cyan-600 px-6 py-3 text-sm font-black uppercase tracking-widest text-white shadow-sm hover:bg-cyan-500 md:col-span-1"
300
+ >
301
+ Add Blackout
302
+ </button>
303
+ </div>
304
+ <div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3">
305
+ {config.unavailableHours.map((block: TimeBlock, idx: number) => (
306
+ <div
307
+ key={idx}
308
+ className="flex items-center justify-between rounded-lg border border-gray-100 bg-gray-50 p-4 shadow-sm"
309
+ >
310
+ <div className="flex flex-col">
311
+ <span className="text-xs font-black uppercase text-gray-400">
312
+ Closed Period
313
+ </span>
314
+ <span className="text-xs font-bold text-gray-700">
315
+ {new Date(block.start).toLocaleString('en-US', {
316
+ dateStyle: 'short',
317
+ timeStyle: 'short',
318
+ timeZone: config.timezone,
319
+ })}{' '}
320
+ —{' '}
321
+ {new Date(block.end).toLocaleString('en-US', {
322
+ dateStyle: 'short',
323
+ timeStyle: 'short',
324
+ timeZone: config.timezone,
325
+ })}
326
+ </span>
327
+ </div>
328
+ <button
329
+ onClick={() => removeUnavailableBlock(idx)}
330
+ className="ml-4 rounded-full p-1 text-gray-400 transition-colors hover:bg-red-50 hover:text-red-600"
331
+ >
332
+ <svg
333
+ className="h-5 w-5"
334
+ fill="none"
335
+ viewBox="0 0 24 24"
336
+ stroke="currentColor"
337
+ >
338
+ <path
339
+ strokeLinecap="round"
340
+ strokeLinejoin="round"
341
+ strokeWidth={2}
342
+ d="M6 18L18 6M6 6l12 12"
343
+ />
344
+ </svg>
345
+ </button>
346
+ </div>
347
+ ))}
348
+ </div>
349
+ </div>
350
+ </div>
351
+ </div>
352
+ </div>
353
+ );
354
+ }
@@ -149,7 +149,7 @@ export default function StoryKeepDashboard({
149
149
 
150
150
  {/* Tab Navigation */}
151
151
  <div className="border-b border-gray-200">
152
- <nav className="-mb-px flex gap-x-8" aria-label="Tabs">
152
+ <nav className="-mb-px flex flex-wrap gap-x-8" aria-label="Tabs">
153
153
  {tabs.map((tab) => (
154
154
  <a
155
155
  key={tab.id}