astro-tractstack 2.3.1 → 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.
Files changed (53) hide show
  1. package/dist/index.js +36 -3
  2. package/package.json +1 -1
  3. package/templates/custom/shopify/Cart.tsx +16 -5
  4. package/templates/custom/shopify/CheckoutModal.tsx +4 -4
  5. package/templates/custom/shopify/ShopifyCartManager.tsx +27 -36
  6. package/templates/custom/shopify/ShopifyCheckout.tsx +4 -33
  7. package/templates/custom/shopify/ShopifyProductGrid.tsx +42 -14
  8. package/templates/custom/shopify/ShopifyServiceList.tsx +94 -50
  9. package/templates/src/components/Footer.astro +2 -2
  10. package/templates/src/components/Header.astro +14 -8
  11. package/templates/src/components/Menu.tsx +157 -135
  12. package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
  13. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +27 -6
  14. package/templates/src/components/codehooks/EpinetTableView.tsx +153 -112
  15. package/templates/src/components/codehooks/EpinetWrapper.tsx +4 -1
  16. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +8 -1
  17. package/templates/src/components/codehooks/ProductCardSetup.tsx +9 -1
  18. package/templates/src/components/codehooks/ProductGridSetup.tsx +9 -1
  19. package/templates/src/components/compositor/nodes/BgPaneWrapper.tsx +2 -1
  20. package/templates/src/components/compositor/nodes/GhostInsertBlock.tsx +1 -1
  21. package/templates/src/components/edit/ToolBar.tsx +2 -1
  22. package/templates/src/components/edit/context/ContextPaneConfig_slug.tsx +2 -2
  23. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +13 -0
  24. package/templates/src/components/edit/pane/AddPanePanel_newCustomCopy.tsx +2 -2
  25. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
  26. package/templates/src/components/edit/state/SaveModal.tsx +1 -1
  27. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +8 -3
  28. package/templates/src/components/form/DateTimeInput.tsx +10 -3
  29. package/templates/src/components/form/FileUpload.tsx +11 -5
  30. package/templates/src/components/form/NumberInput.tsx +2 -2
  31. package/templates/src/components/form/advanced/APIConfigSection.tsx +2 -38
  32. package/templates/src/components/form/brand/SiteConfigSection.tsx +10 -0
  33. package/templates/src/components/storykeep/Dashboard_Shopify.tsx +7 -8
  34. package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +2 -2
  35. package/templates/src/components/storykeep/controls/content/ProductTable.tsx +2 -2
  36. package/templates/src/components/storykeep/controls/content/ResourceBulkIngest.tsx +79 -51
  37. package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +1 -0
  38. package/templates/src/components/storykeep/email-builder/Blocks.tsx +169 -0
  39. package/templates/src/components/storykeep/email-builder/EmailBuilder.tsx +223 -0
  40. package/templates/src/components/storykeep/email-builder/PreviewModal.tsx +136 -0
  41. package/templates/src/components/storykeep/email-builder/PropertyPanel.tsx +154 -0
  42. package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +1 -8
  43. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +32 -6
  44. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Emails.tsx +105 -0
  45. package/templates/src/layouts/Layout.astro +8 -5
  46. package/templates/src/stores/shopify.ts +16 -0
  47. package/templates/src/types/formTypes.ts +4 -2
  48. package/templates/src/types/tractstack.ts +5 -2
  49. package/templates/src/utils/api/brandConfig.ts +2 -0
  50. package/templates/src/utils/api/brandHelpers.ts +16 -0
  51. package/templates/src/utils/api/emailHelpers.ts +105 -0
  52. package/templates/src/utils/tenantResolver.ts +1 -1
  53. package/utils/inject-files.ts +34 -1
@@ -0,0 +1,223 @@
1
+ import { useState, useEffect } from 'react';
2
+ import {
3
+ emailHelpers,
4
+ type EmailTemplate,
5
+ type EmailBlock,
6
+ } from '@/utils/api/emailHelpers';
7
+ import Blocks from './Blocks';
8
+ import PropertyPanel from './PropertyPanel';
9
+ import PreviewModal from './PreviewModal';
10
+
11
+ interface EmailBuilderProps {
12
+ category: string;
13
+ templateName: string;
14
+ onClose: () => void;
15
+ }
16
+
17
+ export default function EmailBuilder({
18
+ category,
19
+ templateName,
20
+ onClose,
21
+ }: EmailBuilderProps) {
22
+ const [template, setTemplate] = useState<EmailTemplate | null>(null);
23
+ const [isLoading, setIsLoading] = useState(true);
24
+ const [isSaving, setIsSaving] = useState(false);
25
+ const [error, setError] = useState<string | null>(null);
26
+ const [selectedIdx, setSelectedIdx] = useState<number | null>(null);
27
+ const [isPreviewOpen, setIsPreviewOpen] = useState(false);
28
+
29
+ useEffect(() => {
30
+ const load = async () => {
31
+ try {
32
+ const data = await emailHelpers.getTemplate(category, templateName);
33
+ setTemplate(data);
34
+ } catch (err) {
35
+ setError(
36
+ err instanceof Error ? err.message : 'Failed to load template'
37
+ );
38
+ } finally {
39
+ setIsLoading(false);
40
+ }
41
+ };
42
+ load();
43
+ }, [category, templateName]);
44
+
45
+ const handleSave = async () => {
46
+ if (!template) return;
47
+ try {
48
+ setIsSaving(true);
49
+ await emailHelpers.saveTemplate(category, templateName, template);
50
+ onClose();
51
+ } catch (err) {
52
+ alert(err instanceof Error ? err.message : 'Failed to save');
53
+ } finally {
54
+ setIsSaving(false);
55
+ }
56
+ };
57
+
58
+ const updateBlock = (index: number, newBlock: EmailBlock) => {
59
+ if (!template) return;
60
+ const newBlocks = [...template.blocks];
61
+ newBlocks[index] = newBlock;
62
+ setTemplate({ ...template, blocks: newBlocks });
63
+ };
64
+
65
+ const addBlock = (type: 'text' | 'button' | 'divider') => {
66
+ if (!template) return;
67
+ let newBlock: EmailBlock;
68
+ if (type === 'text') {
69
+ newBlock = {
70
+ type: 'text',
71
+ content: 'New Text',
72
+ align: 'left',
73
+ color: '#333333',
74
+ isBold: false,
75
+ };
76
+ } else if (type === 'button') {
77
+ newBlock = {
78
+ type: 'button',
79
+ label: 'Click Here',
80
+ url: 'https://',
81
+ bgColor: '#0867ec',
82
+ textColor: '#ffffff',
83
+ };
84
+ } else {
85
+ newBlock = { type: 'divider', color: '#e5e7eb' };
86
+ }
87
+ setTemplate({ ...template, blocks: [...template.blocks, newBlock] });
88
+ setSelectedIdx(template.blocks.length);
89
+ };
90
+
91
+ const deleteBlock = (index: number) => {
92
+ if (!template) return;
93
+ const newBlocks = [...template.blocks];
94
+ newBlocks.splice(index, 1);
95
+ setTemplate({ ...template, blocks: newBlocks });
96
+ setSelectedIdx(null);
97
+ };
98
+
99
+ const moveBlock = (index: number, direction: 'up' | 'down') => {
100
+ if (!template) return;
101
+ if (direction === 'up' && index === 0) return;
102
+ if (direction === 'down' && index === template.blocks.length - 1) return;
103
+ const newBlocks = [...template.blocks];
104
+ const targetIdx = direction === 'up' ? index - 1 : index + 1;
105
+ const temp = newBlocks[index];
106
+ newBlocks[index] = newBlocks[targetIdx];
107
+ newBlocks[targetIdx] = temp;
108
+ setTemplate({ ...template, blocks: newBlocks });
109
+ setSelectedIdx(targetIdx);
110
+ };
111
+
112
+ if (isLoading) return <div>Loading...</div>;
113
+ if (error) return <div className="text-red-600">{error}</div>;
114
+ if (!template) return null;
115
+
116
+ return (
117
+ <div className="flex max-h-screen min-h-96 flex-col overflow-hidden rounded-lg border border-gray-200 bg-white">
118
+ <div className="flex items-center justify-between border-b border-gray-200 px-4 py-3">
119
+ <div className="flex min-w-0 flex-1 items-center gap-4">
120
+ <button
121
+ onClick={onClose}
122
+ className="text-sm font-bold text-gray-500 hover:text-gray-900"
123
+ >
124
+ &larr; Back
125
+ </button>
126
+ <div className="hidden shrink-0 flex-col border-r border-gray-200 pr-4 md:flex">
127
+ <span className="text-xs font-bold uppercase tracking-wide text-gray-400">
128
+ Template file
129
+ </span>
130
+ <span className="max-w-xs truncate text-sm font-bold text-gray-700">
131
+ {category}/{templateName}.json
132
+ </span>
133
+ </div>
134
+ <div className="flex min-w-0 flex-1 flex-col">
135
+ <label className="text-xs font-bold text-gray-500">
136
+ Subject Line
137
+ </label>
138
+ <input
139
+ type="text"
140
+ value={template.subject}
141
+ onChange={(e) =>
142
+ setTemplate({ ...template, subject: e.target.value })
143
+ }
144
+ className="w-full min-w-0 border-0 bg-transparent p-0 text-sm font-bold focus:ring-0"
145
+ placeholder="Email Subject..."
146
+ />
147
+ </div>
148
+ </div>
149
+ <div className="flex gap-2">
150
+ <button
151
+ onClick={() => setIsPreviewOpen(true)}
152
+ className="rounded-md bg-gray-100 px-4 py-2 text-sm font-bold text-gray-700 hover:bg-gray-200"
153
+ >
154
+ Preview
155
+ </button>
156
+ <button
157
+ onClick={handleSave}
158
+ disabled={isSaving}
159
+ className="rounded-md bg-cyan-600 px-4 py-2 text-sm font-bold text-white hover:bg-cyan-500 disabled:opacity-50"
160
+ >
161
+ {isSaving ? 'Saving...' : 'Save Template'}
162
+ </button>
163
+ </div>
164
+ </div>
165
+
166
+ <div className="flex min-h-0 flex-1 overflow-hidden">
167
+ <div className="flex min-h-0 flex-1 flex-col overflow-y-auto bg-gray-50 p-8">
168
+ <div className="mx-auto min-h-96 w-full max-w-2xl bg-white shadow-sm">
169
+ <Blocks
170
+ blocks={template.blocks}
171
+ selectedIdx={selectedIdx}
172
+ onSelect={setSelectedIdx}
173
+ onChange={updateBlock}
174
+ />
175
+ </div>
176
+ <div className="mx-auto mt-8 flex max-w-2xl gap-2">
177
+ <button
178
+ onClick={() => addBlock('text')}
179
+ className="rounded border px-3 py-1 text-sm font-bold text-gray-600"
180
+ >
181
+ + Text
182
+ </button>
183
+ <button
184
+ onClick={() => addBlock('button')}
185
+ className="rounded border px-3 py-1 text-sm font-bold text-gray-600"
186
+ >
187
+ + Button
188
+ </button>
189
+ <button
190
+ onClick={() => addBlock('divider')}
191
+ className="rounded border px-3 py-1 text-sm font-bold text-gray-600"
192
+ >
193
+ + Divider
194
+ </button>
195
+ </div>
196
+ </div>
197
+
198
+ <div className="w-80 shrink-0 overflow-y-auto border-l border-gray-200 bg-white p-4">
199
+ {selectedIdx !== null && template.blocks[selectedIdx] ? (
200
+ <PropertyPanel
201
+ block={template.blocks[selectedIdx]}
202
+ onChange={(b) => updateBlock(selectedIdx, b)}
203
+ onDelete={() => deleteBlock(selectedIdx)}
204
+ onMoveUp={() => moveBlock(selectedIdx, 'up')}
205
+ onMoveDown={() => moveBlock(selectedIdx, 'down')}
206
+ />
207
+ ) : (
208
+ <div className="text-sm text-gray-500">
209
+ Select a block to edit its properties.
210
+ </div>
211
+ )}
212
+ </div>
213
+ </div>
214
+
215
+ {isPreviewOpen && (
216
+ <PreviewModal
217
+ template={template}
218
+ onClose={() => setIsPreviewOpen(false)}
219
+ />
220
+ )}
221
+ </div>
222
+ );
223
+ }
@@ -0,0 +1,136 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { emailHelpers, type EmailTemplate } from '@/utils/api/emailHelpers';
3
+
4
+ interface PreviewModalProps {
5
+ template: EmailTemplate;
6
+ onClose: () => void;
7
+ }
8
+
9
+ export default function PreviewModal({ template, onClose }: PreviewModalProps) {
10
+ const [variables, setVariables] = useState<string[]>([]);
11
+ const [mockData, setMockData] = useState<Record<string, string>>({});
12
+ const [html, setHtml] = useState<string>('');
13
+ const [subject, setSubject] = useState<string>('');
14
+ const [error, setError] = useState<string | null>(null);
15
+
16
+ useEffect(() => {
17
+ const rawString = JSON.stringify(template);
18
+ const regex = /\{\{\.([a-zA-Z0-9_]+)\}\}/g;
19
+ const found = new Set<string>();
20
+
21
+ let match;
22
+ while ((match = regex.exec(rawString)) !== null) {
23
+ found.add(match[1]);
24
+ }
25
+
26
+ setVariables(Array.from(found));
27
+
28
+ const initialData: Record<string, string> = {};
29
+ found.forEach((v) => {
30
+ initialData[v] = `[${v}]`;
31
+ });
32
+ setMockData(initialData);
33
+ }, [template]);
34
+
35
+ const handleGenerate = async () => {
36
+ try {
37
+ setError(null);
38
+ const res = await emailHelpers.previewTemplate(template, mockData);
39
+ setHtml(res.html);
40
+ setSubject(res.subject);
41
+ } catch (err) {
42
+ setError(
43
+ err instanceof Error ? err.message : 'Preview generation failed'
44
+ );
45
+ }
46
+ };
47
+
48
+ return (
49
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900 bg-opacity-75 p-4 backdrop-blur-sm">
50
+ <div
51
+ className="flex h-full w-full max-w-6xl flex-col overflow-hidden rounded-lg bg-white shadow-xl md:flex-row"
52
+ style={{ maxHeight: '90vh' }}
53
+ >
54
+ <div className="w-full border-r border-gray-200 bg-gray-50 p-6 md:w-80">
55
+ <div className="mb-6 flex items-center justify-between">
56
+ <h3 className="text-lg font-bold text-gray-900">Mock Data</h3>
57
+ <button
58
+ onClick={onClose}
59
+ className="text-gray-400 hover:text-gray-900 md:hidden"
60
+ >
61
+ &times;
62
+ </button>
63
+ </div>
64
+
65
+ {variables.length === 0 ? (
66
+ <p className="text-sm text-gray-500">
67
+ No template variables found.
68
+ </p>
69
+ ) : (
70
+ <div className="space-y-4">
71
+ {variables.map((v) => (
72
+ <div key={v}>
73
+ <label className="mb-1 block text-xs font-bold text-gray-700">
74
+ {v}
75
+ </label>
76
+ <input
77
+ type="text"
78
+ value={mockData[v] || ''}
79
+ onChange={(e) =>
80
+ setMockData({ ...mockData, [v]: e.target.value })
81
+ }
82
+ className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-cyan-500 focus:ring-cyan-500"
83
+ />
84
+ </div>
85
+ ))}
86
+ </div>
87
+ )}
88
+
89
+ <button
90
+ onClick={handleGenerate}
91
+ className="mt-8 w-full rounded-md bg-cyan-600 px-4 py-2 text-sm font-bold text-white hover:bg-cyan-500"
92
+ >
93
+ Generate Preview
94
+ </button>
95
+
96
+ {error && (
97
+ <p className="mt-4 text-xs font-bold text-red-600">{error}</p>
98
+ )}
99
+ </div>
100
+
101
+ <div className="flex flex-1 flex-col overflow-hidden bg-white">
102
+ <div className="flex items-center justify-between border-b border-gray-200 px-6 py-4">
103
+ <div className="flex flex-col">
104
+ <span className="text-xs font-bold text-gray-500">Subject</span>
105
+ <span className="text-sm font-bold text-gray-900">
106
+ {subject || 'Generate preview to view subject...'}
107
+ </span>
108
+ </div>
109
+ <button
110
+ onClick={onClose}
111
+ className="hidden text-gray-400 hover:text-gray-900 md:block"
112
+ >
113
+ &times; Close
114
+ </button>
115
+ </div>
116
+
117
+ <div className="flex-1 bg-gray-100 p-8">
118
+ <div className="mx-auto h-full w-full max-w-2xl overflow-hidden rounded bg-white shadow-lg">
119
+ {html ? (
120
+ <iframe
121
+ srcDoc={html}
122
+ className="h-full w-full border-0"
123
+ title="Email Preview"
124
+ />
125
+ ) : (
126
+ <div className="flex h-full items-center justify-center text-sm font-bold text-gray-400">
127
+ Awaiting preview generation...
128
+ </div>
129
+ )}
130
+ </div>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ );
136
+ }
@@ -0,0 +1,154 @@
1
+ import type { EmailBlock } from '@/utils/api/emailHelpers';
2
+
3
+ interface PropertyPanelProps {
4
+ block: EmailBlock;
5
+ onChange: (block: EmailBlock) => void;
6
+ onDelete: () => void;
7
+ onMoveUp: () => void;
8
+ onMoveDown: () => void;
9
+ }
10
+
11
+ export default function PropertyPanel({
12
+ block,
13
+ onChange,
14
+ onDelete,
15
+ onMoveUp,
16
+ onMoveDown,
17
+ }: PropertyPanelProps) {
18
+ return (
19
+ <div className="space-y-6">
20
+ <div className="flex min-w-0 items-center justify-between border-b border-gray-200 pb-4">
21
+ <h3 className="min-w-0 flex-1 truncate text-sm font-bold capitalize text-gray-900">
22
+ {block.type} Settings
23
+ </h3>
24
+ <div className="flex shrink-0 gap-2 text-gray-400">
25
+ <button onClick={onMoveUp} className="hover:text-gray-900">
26
+ &uarr;
27
+ </button>
28
+ <button onClick={onMoveDown} className="hover:text-gray-900">
29
+ &darr;
30
+ </button>
31
+ <button onClick={onDelete} className="hover:text-red-600">
32
+ &times;
33
+ </button>
34
+ </div>
35
+ </div>
36
+
37
+ {block.type === 'text' && (
38
+ <div className="space-y-4">
39
+ <div>
40
+ <label className="mb-1 block text-xs font-bold text-gray-700">
41
+ Alignment
42
+ </label>
43
+ <select
44
+ value={block.align}
45
+ onChange={(e) =>
46
+ onChange({ ...block, align: e.target.value as any })
47
+ }
48
+ className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-cyan-500 focus:ring-cyan-500"
49
+ >
50
+ <option value="left">Left</option>
51
+ <option value="center">Center</option>
52
+ <option value="right">Right</option>
53
+ </select>
54
+ </div>
55
+ <div>
56
+ <label className="mb-1 block text-xs font-bold text-gray-700">
57
+ Text Color
58
+ </label>
59
+ <div className="flex gap-2">
60
+ <input
61
+ type="color"
62
+ value={block.color}
63
+ onChange={(e) => onChange({ ...block, color: e.target.value })}
64
+ className="h-8 w-8 cursor-pointer rounded"
65
+ />
66
+ <input
67
+ type="text"
68
+ value={block.color}
69
+ onChange={(e) => onChange({ ...block, color: e.target.value })}
70
+ className="flex-1 rounded-md border border-gray-300 px-3 py-1 text-sm"
71
+ />
72
+ </div>
73
+ </div>
74
+ <div className="flex items-center gap-2">
75
+ <input
76
+ type="checkbox"
77
+ checked={block.isBold}
78
+ onChange={(e) => onChange({ ...block, isBold: e.target.checked })}
79
+ className="rounded border-gray-300 text-cyan-600 focus:ring-cyan-500"
80
+ />
81
+ <label className="text-sm font-bold text-gray-700">Bold Text</label>
82
+ </div>
83
+ </div>
84
+ )}
85
+
86
+ {block.type === 'button' && (
87
+ <div className="space-y-4">
88
+ <div>
89
+ <label className="mb-1 block text-xs font-bold text-gray-700">
90
+ Label
91
+ </label>
92
+ <input
93
+ type="text"
94
+ value={block.label}
95
+ onChange={(e) => onChange({ ...block, label: e.target.value })}
96
+ className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-cyan-500 focus:ring-cyan-500"
97
+ />
98
+ </div>
99
+ <div>
100
+ <label className="mb-1 block text-xs font-bold text-gray-700">
101
+ URL
102
+ </label>
103
+ <input
104
+ type="text"
105
+ value={block.url}
106
+ onChange={(e) => onChange({ ...block, url: e.target.value })}
107
+ className="w-full rounded-md border border-gray-300 px-3 py-2 font-mono text-sm focus:border-cyan-500 focus:ring-cyan-500"
108
+ />
109
+ </div>
110
+ <div>
111
+ <label className="mb-1 block text-xs font-bold text-gray-700">
112
+ Background Color
113
+ </label>
114
+ <input
115
+ type="color"
116
+ value={block.bgColor}
117
+ onChange={(e) => onChange({ ...block, bgColor: e.target.value })}
118
+ className="h-8 w-full cursor-pointer rounded"
119
+ />
120
+ </div>
121
+ <div>
122
+ <label className="mb-1 block text-xs font-bold text-gray-700">
123
+ Text Color
124
+ </label>
125
+ <input
126
+ type="color"
127
+ value={block.textColor}
128
+ onChange={(e) =>
129
+ onChange({ ...block, textColor: e.target.value })
130
+ }
131
+ className="h-8 w-full cursor-pointer rounded"
132
+ />
133
+ </div>
134
+ </div>
135
+ )}
136
+
137
+ {block.type === 'divider' && (
138
+ <div className="space-y-4">
139
+ <div>
140
+ <label className="mb-1 block text-xs font-bold text-gray-700">
141
+ Line Color
142
+ </label>
143
+ <input
144
+ type="color"
145
+ value={block.color}
146
+ onChange={(e) => onChange({ ...block, color: e.target.value })}
147
+ className="h-8 w-full cursor-pointer rounded"
148
+ />
149
+ </div>
150
+ </div>
151
+ )}
152
+ </div>
153
+ );
154
+ }
@@ -1,15 +1,8 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { bookingHelpers } from '@/utils/api/bookingHelpers';
3
3
  import type { BookingMetricsResponse } from '@/types/tractstack';
4
- import type { ResourceNode } from '@/types/compositorTypes';
5
4
 
6
- interface ShopifyDashboardProps {
7
- existingResources: ResourceNode[];
8
- }
9
-
10
- export default function ShopifyDashboard({
11
- existingResources,
12
- }: ShopifyDashboardProps) {
5
+ export default function ShopifyDashboard({}) {
13
6
  const [metrics, setMetrics] = useState<BookingMetricsResponse | null>(null);
14
7
  const [isLoading, setIsLoading] = useState(true);
15
8
  const [error, setError] = useState<string | null>(null);
@@ -1,4 +1,10 @@
1
- import { useState, useEffect, useMemo, useCallback } from 'react';
1
+ import {
2
+ useState,
3
+ useEffect,
4
+ useMemo,
5
+ useCallback,
6
+ type ChangeEvent,
7
+ } from 'react';
2
8
  import { Toggle } from '@ark-ui/react/toggle';
3
9
  import CalendarIcon from '@heroicons/react/24/outline/CalendarIcon';
4
10
  import TableCellsIcon from '@heroicons/react/24/outline/TableCellsIcon';
@@ -68,7 +74,7 @@ export default function ShopifyDashboard_Bookings({
68
74
  setCurrentPage(newPage);
69
75
  };
70
76
 
71
- const handleStatusChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
77
+ const handleStatusChange = (e: ChangeEvent<HTMLSelectElement>) => {
72
78
  setStatusFilter(e.target.value);
73
79
  setCurrentPage(0);
74
80
  };
@@ -247,10 +253,20 @@ export default function ShopifyDashboard_Bookings({
247
253
  )}
248
254
  </div>
249
255
  <div className="mt-3 space-y-1 text-xs text-gray-500">
250
- <div className="font-medium text-gray-900">
251
- {renderCustomerInfo(booking)}
256
+ <div className="flex items-center justify-between text-gray-900">
257
+ <span>{renderCustomerInfo(booking)}</span>
258
+ {booking.shopifyOrderId && (
259
+ <a
260
+ href={`https://admin.shopify.com/orders/${booking.shopifyOrderId}`}
261
+ target="_blank"
262
+ rel="noopener noreferrer"
263
+ className="text-cyan-600 hover:text-cyan-800 hover:underline"
264
+ >
265
+ Order #{booking.shopifyOrderId}
266
+ </a>
267
+ )}
252
268
  </div>
253
- <div className="font-medium text-gray-700">
269
+ <div className="text-gray-700">
254
270
  {booking.resourceIds
255
271
  .map(
256
272
  (id) => resourceMap.get(id) || 'Unknown Service'
@@ -324,7 +340,17 @@ export default function ShopifyDashboard_Bookings({
324
340
  .join(', ')}
325
341
  </td>
326
342
  <td className="px-6 py-4 text-sm text-gray-500">
327
- {renderCustomerInfo(booking)}
343
+ <div>{renderCustomerInfo(booking)}</div>
344
+ {booking.shopifyOrderId && (
345
+ <a
346
+ href={`https://admin.shopify.com/orders/${booking.shopifyOrderId}`}
347
+ target="_blank"
348
+ rel="noopener noreferrer"
349
+ className="mt-1 inline-block text-xs font-bold text-cyan-600 hover:text-cyan-800 hover:underline"
350
+ >
351
+ Order #{booking.shopifyOrderId}
352
+ </a>
353
+ )}
328
354
  </td>
329
355
  <td className="whitespace-nowrap px-6 py-4 text-sm text-gray-900">
330
356
  <div>