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
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
emailHelpers,
|
|
4
|
+
type EmailTemplateListEntry,
|
|
5
|
+
} from '@/utils/api/emailHelpers';
|
|
6
|
+
import EmailBuilder from '../email-builder/EmailBuilder';
|
|
7
|
+
|
|
8
|
+
export default function ShopifyDashboard_Emails() {
|
|
9
|
+
const [templates, setTemplates] = useState<
|
|
10
|
+
Record<string, EmailTemplateListEntry[]>
|
|
11
|
+
>({});
|
|
12
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
13
|
+
const [error, setError] = useState<string | null>(null);
|
|
14
|
+
const [editingTemplate, setEditingTemplate] = useState<{
|
|
15
|
+
category: string;
|
|
16
|
+
name: string;
|
|
17
|
+
} | null>(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
loadTemplates();
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
const loadTemplates = async () => {
|
|
24
|
+
try {
|
|
25
|
+
setIsLoading(true);
|
|
26
|
+
setError(null);
|
|
27
|
+
const data = await emailHelpers.getTemplates();
|
|
28
|
+
setTemplates(data);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
setError(err instanceof Error ? err.message : 'Failed to load templates');
|
|
31
|
+
} finally {
|
|
32
|
+
setIsLoading(false);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if (editingTemplate) {
|
|
37
|
+
return (
|
|
38
|
+
<EmailBuilder
|
|
39
|
+
category={editingTemplate.category}
|
|
40
|
+
templateName={editingTemplate.name}
|
|
41
|
+
onClose={() => setEditingTemplate(null)}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (isLoading) {
|
|
47
|
+
return (
|
|
48
|
+
<div className="flex h-48 items-center justify-center">
|
|
49
|
+
<div className="h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-cyan-600" />
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (error) {
|
|
55
|
+
return (
|
|
56
|
+
<div className="rounded-md bg-red-50 p-4 text-sm text-red-700">
|
|
57
|
+
{error}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="space-y-8">
|
|
64
|
+
{Object.entries(templates).map(([category, entries]) => (
|
|
65
|
+
<div
|
|
66
|
+
key={category}
|
|
67
|
+
className="rounded-lg border border-gray-200 bg-white"
|
|
68
|
+
>
|
|
69
|
+
<div className="border-b border-gray-200 bg-gray-50 px-4 py-3">
|
|
70
|
+
<h3 className="text-sm font-bold capitalize text-gray-900">
|
|
71
|
+
{category}
|
|
72
|
+
</h3>
|
|
73
|
+
</div>
|
|
74
|
+
<ul className="divide-y divide-gray-200">
|
|
75
|
+
{entries.map((entry) => (
|
|
76
|
+
<li
|
|
77
|
+
key={entry.name}
|
|
78
|
+
className="flex items-center justify-between px-4 py-4 md:px-6"
|
|
79
|
+
>
|
|
80
|
+
<div className="flex min-w-0 flex-col">
|
|
81
|
+
<p className="truncate text-sm font-bold text-gray-900">
|
|
82
|
+
{entry.adminTitle}
|
|
83
|
+
</p>
|
|
84
|
+
<p className="truncate text-xs text-gray-500">
|
|
85
|
+
{entry.name}.json
|
|
86
|
+
</p>
|
|
87
|
+
</div>
|
|
88
|
+
<div className="ml-4 flex flex-shrink-0">
|
|
89
|
+
<button
|
|
90
|
+
onClick={() =>
|
|
91
|
+
setEditingTemplate({ category, name: entry.name })
|
|
92
|
+
}
|
|
93
|
+
className="rounded-md bg-white font-bold text-cyan-600 hover:text-cyan-500 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2"
|
|
94
|
+
>
|
|
95
|
+
Edit
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
98
|
+
</li>
|
|
99
|
+
))}
|
|
100
|
+
</ul>
|
|
101
|
+
</div>
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import ResourceTable from '@/components/storykeep/controls/content/ResourceTable';
|
|
2
|
+
import type { ResourceNode } from '@/types/compositorTypes';
|
|
3
|
+
import type { FullContentMapItem } from '@/types/tractstack';
|
|
4
|
+
|
|
5
|
+
interface ShopifyDashboardProductsProps {
|
|
6
|
+
resources: ResourceNode[];
|
|
7
|
+
onEdit: (resourceId: string) => void;
|
|
8
|
+
onCreate: () => void;
|
|
9
|
+
onRefresh: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function ShopifyDashboard_Products({
|
|
13
|
+
resources,
|
|
14
|
+
onEdit,
|
|
15
|
+
onCreate,
|
|
16
|
+
onRefresh,
|
|
17
|
+
}: ShopifyDashboardProductsProps) {
|
|
18
|
+
/**
|
|
19
|
+
* Convert local ResourceNode[] into FullContentMapItem[] to satisfy the
|
|
20
|
+
* ResourceTable interface requirements.
|
|
21
|
+
*/
|
|
22
|
+
const resourceItems = resources.map((r) => ({
|
|
23
|
+
...r,
|
|
24
|
+
type: 'Resource' as const,
|
|
25
|
+
})) as FullContentMapItem[];
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="space-y-6">
|
|
29
|
+
<div className="border-b border-gray-200 pb-4">
|
|
30
|
+
<h2 className="text-2xl font-bold text-gray-900">Imported Products</h2>
|
|
31
|
+
<p className="mt-2 text-sm text-gray-600">
|
|
32
|
+
Manage the Shopify products you have already imported into StoryKeep.
|
|
33
|
+
Edit their metadata, SEO, and layout parameters.
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<ResourceTable
|
|
38
|
+
categorySlug="product"
|
|
39
|
+
fullContentMap={resourceItems}
|
|
40
|
+
onEdit={onEdit}
|
|
41
|
+
onCreate={onCreate}
|
|
42
|
+
onRefresh={onRefresh}
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useFormState } from '@/hooks/useFormState';
|
|
3
|
+
import {
|
|
4
|
+
convertToLocalState,
|
|
5
|
+
convertToBackendFormat,
|
|
6
|
+
validateBrandConfig,
|
|
7
|
+
} from '@/utils/api/brandHelpers';
|
|
8
|
+
import { saveBrandConfigWithStateUpdate } from '@/utils/api/brandConfig';
|
|
9
|
+
import SchedulingSection from '@/components/form/shopify/SchedulingSection';
|
|
10
|
+
import UnsavedChangesBar from '@/components/form/UnsavedChangesBar';
|
|
11
|
+
import type { BrandConfig, BrandConfigState } from '@/types/tractstack';
|
|
12
|
+
|
|
13
|
+
interface ShopifyDashboardScheduleProps {
|
|
14
|
+
brandConfig: BrandConfig;
|
|
15
|
+
onBrandConfigUpdate?: (config: BrandConfig) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function ShopifyDashboard_Schedule({
|
|
19
|
+
brandConfig,
|
|
20
|
+
onBrandConfigUpdate,
|
|
21
|
+
}: ShopifyDashboardScheduleProps) {
|
|
22
|
+
const [currentBrandConfig, setCurrentBrandConfig] = useState(brandConfig);
|
|
23
|
+
const initialState: BrandConfigState =
|
|
24
|
+
convertToLocalState(currentBrandConfig);
|
|
25
|
+
|
|
26
|
+
const formState = useFormState({
|
|
27
|
+
initialData: initialState,
|
|
28
|
+
validator: validateBrandConfig,
|
|
29
|
+
onSave: async (data) => {
|
|
30
|
+
try {
|
|
31
|
+
const updatedState = await saveBrandConfigWithStateUpdate(
|
|
32
|
+
window.TRACTSTACK_CONFIG?.tenantId || 'default',
|
|
33
|
+
data
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// Preserve existing paths when updating parent state
|
|
37
|
+
const updatedBrandConfig = {
|
|
38
|
+
...currentBrandConfig,
|
|
39
|
+
...convertToBackendFormat(updatedState),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Update local state
|
|
43
|
+
setCurrentBrandConfig(updatedBrandConfig);
|
|
44
|
+
|
|
45
|
+
if (onBrandConfigUpdate) {
|
|
46
|
+
onBrandConfigUpdate(updatedBrandConfig);
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('Save failed:', error);
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
unsavedChanges: {
|
|
54
|
+
enableBrowserWarning: true,
|
|
55
|
+
browserWarningMessage: 'Your scheduling changes will be lost!',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className="space-y-8">
|
|
61
|
+
<div className="border-b border-gray-200 pb-4">
|
|
62
|
+
<h2 className="text-2xl font-bold text-gray-900">Booking Schedule</h2>
|
|
63
|
+
<p className="mt-2 text-sm text-gray-600">
|
|
64
|
+
Manage your store timezone, business hours, and blackout dates.
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<SchedulingSection formState={formState} />
|
|
69
|
+
|
|
70
|
+
<UnsavedChangesBar
|
|
71
|
+
formState={formState}
|
|
72
|
+
message="You have unsaved scheduling changes"
|
|
73
|
+
saveLabel="Save Schedule"
|
|
74
|
+
cancelLabel="Discard Changes"
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useStore } from '@nanostores/react';
|
|
2
|
+
import {
|
|
3
|
+
shopifyData,
|
|
4
|
+
shopifyStatus,
|
|
5
|
+
fetchShopifyProducts,
|
|
6
|
+
type ShopifyProduct,
|
|
7
|
+
} from '@/stores/shopify';
|
|
8
|
+
import ProductTable from '@/components/storykeep/controls/content/ProductTable';
|
|
9
|
+
import type { ResourceNode } from '@/types/compositorTypes';
|
|
10
|
+
|
|
11
|
+
interface ShopifyDashboardSearchProps {
|
|
12
|
+
linkedResourceMap: Map<string, ResourceNode>;
|
|
13
|
+
onSelectProduct: (product: ShopifyProduct) => void;
|
|
14
|
+
onLink: (product: ShopifyProduct) => void;
|
|
15
|
+
onUnlink: (resourceId: string) => void;
|
|
16
|
+
onEdit: (product: ShopifyProduct, resource: ResourceNode) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function ShopifyDashboard_Search({
|
|
20
|
+
linkedResourceMap,
|
|
21
|
+
onSelectProduct,
|
|
22
|
+
onLink,
|
|
23
|
+
onUnlink,
|
|
24
|
+
onEdit,
|
|
25
|
+
}: ShopifyDashboardSearchProps) {
|
|
26
|
+
const data = useStore(shopifyData);
|
|
27
|
+
const status = useStore(shopifyStatus);
|
|
28
|
+
|
|
29
|
+
const handleRefresh = () => {
|
|
30
|
+
fetchShopifyProducts();
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="space-y-6">
|
|
35
|
+
<div className="border-b border-gray-200 pb-4">
|
|
36
|
+
<h2 className="text-2xl font-bold text-gray-900">Catalog Search</h2>
|
|
37
|
+
<p className="mt-2 text-sm text-gray-600">
|
|
38
|
+
Search your live Shopify store to import products and services into
|
|
39
|
+
your StoryKeep.
|
|
40
|
+
</p>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<ProductTable
|
|
44
|
+
products={data.products}
|
|
45
|
+
linkedResourceMap={linkedResourceMap}
|
|
46
|
+
onRefresh={handleRefresh}
|
|
47
|
+
isRefreshing={status.isLoading}
|
|
48
|
+
onSelectProduct={onSelectProduct}
|
|
49
|
+
onLink={onLink}
|
|
50
|
+
onUnlink={onUnlink}
|
|
51
|
+
onEdit={onEdit}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import ResourceTable from '@/components/storykeep/controls/content/ResourceTable';
|
|
2
|
+
import type { ResourceNode } from '@/types/compositorTypes';
|
|
3
|
+
import type { FullContentMapItem } from '@/types/tractstack';
|
|
4
|
+
|
|
5
|
+
interface ShopifyDashboardServicesProps {
|
|
6
|
+
resources: ResourceNode[];
|
|
7
|
+
onEdit: (resourceId: string) => void;
|
|
8
|
+
onCreate: () => void;
|
|
9
|
+
onRefresh: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function ShopifyDashboard_Services({
|
|
13
|
+
resources,
|
|
14
|
+
onEdit,
|
|
15
|
+
onCreate,
|
|
16
|
+
onRefresh,
|
|
17
|
+
}: ShopifyDashboardServicesProps) {
|
|
18
|
+
/**
|
|
19
|
+
* Convert local ResourceNode[] into FullContentMapItem[] to satisfy the
|
|
20
|
+
* ResourceTable interface requirements.
|
|
21
|
+
*/
|
|
22
|
+
const resourceItems = resources.map((r) => ({
|
|
23
|
+
...r,
|
|
24
|
+
type: 'Resource' as const,
|
|
25
|
+
})) as FullContentMapItem[];
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="space-y-6">
|
|
29
|
+
<div className="border-b border-gray-200 pb-4">
|
|
30
|
+
<h2 className="text-2xl font-bold text-gray-900">Imported Services</h2>
|
|
31
|
+
<p className="mt-2 text-sm text-gray-600">
|
|
32
|
+
Manage the services and bookable appointments you have already
|
|
33
|
+
imported into StoryKeep. Edit metadata, scheduling requirements, and
|
|
34
|
+
SEO.
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<ResourceTable
|
|
39
|
+
categorySlug="service"
|
|
40
|
+
fullContentMap={resourceItems}
|
|
41
|
+
onEdit={onEdit}
|
|
42
|
+
onCreate={onCreate}
|
|
43
|
+
onRefresh={onRefresh}
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -50,15 +50,18 @@ const {
|
|
|
50
50
|
|
|
51
51
|
const isInitialized = !freshInstallStore.get().needsSetup;
|
|
52
52
|
const goBackend = import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
53
|
+
const isMultiTenant = import.meta.env.PUBLIC_ENABLE_MULTI_TENANT === 'true';
|
|
54
|
+
const tenantId = isMultiTenant
|
|
55
|
+
? (await resolveTenantId(Astro.request)).id
|
|
56
|
+
: import.meta.env.PUBLIC_TENANTID || 'default';
|
|
57
|
+
|
|
56
58
|
if (!Astro.locals.tenant) {
|
|
57
59
|
Astro.locals.tenant = {
|
|
58
60
|
id: tenantId,
|
|
59
61
|
domain: Astro.url.hostname,
|
|
60
|
-
isMultiTenant:
|
|
61
|
-
isLocalhost:
|
|
62
|
+
isMultiTenant: isMultiTenant,
|
|
63
|
+
isLocalhost:
|
|
64
|
+
Astro.url.hostname === 'localhost' || Astro.url.hostname === '127.0.0.1',
|
|
62
65
|
};
|
|
63
66
|
}
|
|
64
67
|
const brandConfig = propBrandConfig || (await getBrandConfig(tenantId));
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { APIRoute } from '@/types/astro';
|
|
2
|
+
|
|
3
|
+
export const POST: APIRoute = async ({ request, locals }) => {
|
|
4
|
+
const GO_BACKEND =
|
|
5
|
+
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const body = await request.text();
|
|
9
|
+
|
|
10
|
+
const controller = new AbortController();
|
|
11
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
12
|
+
const tenantId =
|
|
13
|
+
locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default';
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const response = await fetch(`${GO_BACKEND}/api/v1/auth/lookup-lead`, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: {
|
|
19
|
+
'Content-Type': 'application/json',
|
|
20
|
+
'X-Tenant-ID': tenantId,
|
|
21
|
+
},
|
|
22
|
+
body: body,
|
|
23
|
+
signal: controller.signal,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
clearTimeout(timeoutId);
|
|
27
|
+
|
|
28
|
+
const data = await response.json();
|
|
29
|
+
|
|
30
|
+
return new Response(JSON.stringify(data), {
|
|
31
|
+
status: response.status,
|
|
32
|
+
headers: {
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
} catch (fetchError) {
|
|
37
|
+
clearTimeout(timeoutId);
|
|
38
|
+
|
|
39
|
+
if (fetchError instanceof Error && fetchError.name === 'AbortError') {
|
|
40
|
+
console.error('Lookup-lead request timeout');
|
|
41
|
+
return new Response(
|
|
42
|
+
JSON.stringify({
|
|
43
|
+
success: false,
|
|
44
|
+
error: 'Request timeout - please try again',
|
|
45
|
+
}),
|
|
46
|
+
{
|
|
47
|
+
status: 408,
|
|
48
|
+
headers: {
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
throw fetchError;
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('Lookup-lead API proxy error:', error);
|
|
58
|
+
|
|
59
|
+
return new Response(
|
|
60
|
+
JSON.stringify({
|
|
61
|
+
success: false,
|
|
62
|
+
error: 'Failed to connect to backend service',
|
|
63
|
+
}),
|
|
64
|
+
{
|
|
65
|
+
status: 500,
|
|
66
|
+
headers: {
|
|
67
|
+
'Content-Type': 'application/json',
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { APIRoute } from '@/types/astro';
|
|
2
|
+
|
|
3
|
+
export const GET: APIRoute = async ({ request, locals }) => {
|
|
4
|
+
const GO_BACKEND =
|
|
5
|
+
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const url = new URL(request.url);
|
|
9
|
+
const searchParams = url.searchParams;
|
|
10
|
+
|
|
11
|
+
const controller = new AbortController();
|
|
12
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
13
|
+
const tenantId =
|
|
14
|
+
locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default';
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const response = await fetch(
|
|
18
|
+
`${GO_BACKEND}/api/v1/bookings/availability?${searchParams.toString()}`,
|
|
19
|
+
{
|
|
20
|
+
method: 'GET',
|
|
21
|
+
headers: {
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
'X-Tenant-ID': tenantId,
|
|
24
|
+
...(request.headers.get('Authorization') && {
|
|
25
|
+
Authorization: request.headers.get('Authorization')!,
|
|
26
|
+
}),
|
|
27
|
+
},
|
|
28
|
+
signal: controller.signal,
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
clearTimeout(timeoutId);
|
|
33
|
+
|
|
34
|
+
const data = await response.json();
|
|
35
|
+
|
|
36
|
+
return new Response(JSON.stringify(data), {
|
|
37
|
+
status: response.status,
|
|
38
|
+
headers: {
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
} catch (fetchError) {
|
|
43
|
+
clearTimeout(timeoutId);
|
|
44
|
+
|
|
45
|
+
if (fetchError instanceof Error && fetchError.name === 'AbortError') {
|
|
46
|
+
return new Response(
|
|
47
|
+
JSON.stringify({
|
|
48
|
+
success: false,
|
|
49
|
+
error: 'Availability lookup timeout',
|
|
50
|
+
}),
|
|
51
|
+
{
|
|
52
|
+
status: 408,
|
|
53
|
+
headers: { 'Content-Type': 'application/json' },
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
throw fetchError;
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Availability API proxy error:', error);
|
|
61
|
+
return new Response(
|
|
62
|
+
JSON.stringify({
|
|
63
|
+
success: false,
|
|
64
|
+
error: 'Failed to connect to backend service',
|
|
65
|
+
}),
|
|
66
|
+
{
|
|
67
|
+
status: 500,
|
|
68
|
+
headers: { 'Content-Type': 'application/json' },
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { APIRoute } from '@/types/astro';
|
|
2
|
+
import { getAdminToken } from '@/utils/auth';
|
|
3
|
+
|
|
4
|
+
export const POST: APIRoute = async (context) => {
|
|
5
|
+
const { request, locals } = context;
|
|
6
|
+
const GO_BACKEND =
|
|
7
|
+
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const body = await request.json();
|
|
11
|
+
const { traceId } = body;
|
|
12
|
+
|
|
13
|
+
if (!traceId) {
|
|
14
|
+
return new Response(
|
|
15
|
+
JSON.stringify({ success: false, error: 'traceId is required' }),
|
|
16
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
22
|
+
const tenantId =
|
|
23
|
+
locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default';
|
|
24
|
+
const token = getAdminToken(context);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch(
|
|
28
|
+
`${GO_BACKEND}/api/v1/bookings/${traceId}/cancel`,
|
|
29
|
+
{
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
'X-Tenant-ID': tenantId,
|
|
34
|
+
...(token && { Authorization: `Bearer ${token}` }),
|
|
35
|
+
...(request.headers.get('Authorization') && {
|
|
36
|
+
Authorization: request.headers.get('Authorization')!,
|
|
37
|
+
}),
|
|
38
|
+
},
|
|
39
|
+
signal: controller.signal,
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
clearTimeout(timeoutId);
|
|
44
|
+
const data = await response.json();
|
|
45
|
+
|
|
46
|
+
return new Response(JSON.stringify(data), {
|
|
47
|
+
status: response.status,
|
|
48
|
+
headers: { 'Content-Type': 'application/json' },
|
|
49
|
+
});
|
|
50
|
+
} catch (fetchError) {
|
|
51
|
+
clearTimeout(timeoutId);
|
|
52
|
+
if (fetchError instanceof Error && fetchError.name === 'AbortError') {
|
|
53
|
+
return new Response(
|
|
54
|
+
JSON.stringify({
|
|
55
|
+
success: false,
|
|
56
|
+
error: 'Booking cancellation timeout',
|
|
57
|
+
}),
|
|
58
|
+
{ status: 408, headers: { 'Content-Type': 'application/json' } }
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
throw fetchError;
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Booking cancellation API proxy error:', error);
|
|
65
|
+
return new Response(
|
|
66
|
+
JSON.stringify({
|
|
67
|
+
success: false,
|
|
68
|
+
error: 'Failed to connect to backend service',
|
|
69
|
+
}),
|
|
70
|
+
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { APIRoute } from '@/types/astro';
|
|
2
|
+
|
|
3
|
+
export const POST: APIRoute = async ({ request, locals }) => {
|
|
4
|
+
const GO_BACKEND =
|
|
5
|
+
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const { traceId } = await request.json();
|
|
9
|
+
|
|
10
|
+
if (!traceId) {
|
|
11
|
+
return new Response(JSON.stringify({ error: 'traceId is required' }), {
|
|
12
|
+
status: 400,
|
|
13
|
+
headers: { 'Content-Type': 'application/json' },
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
19
|
+
const tenantId =
|
|
20
|
+
locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default';
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const response = await fetch(`${GO_BACKEND}/api/v1/bookings/confirm`, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
'X-Tenant-ID': tenantId,
|
|
28
|
+
...(request.headers.get('Authorization') && {
|
|
29
|
+
Authorization: request.headers.get('Authorization')!,
|
|
30
|
+
}),
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify({ traceId }),
|
|
33
|
+
signal: controller.signal,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
clearTimeout(timeoutId);
|
|
37
|
+
|
|
38
|
+
const data = await response.json();
|
|
39
|
+
|
|
40
|
+
return new Response(JSON.stringify(data), {
|
|
41
|
+
status: response.status,
|
|
42
|
+
headers: {
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
} catch (fetchError) {
|
|
47
|
+
clearTimeout(timeoutId);
|
|
48
|
+
|
|
49
|
+
if (fetchError instanceof Error && fetchError.name === 'AbortError') {
|
|
50
|
+
console.error('Confirm-booking request timeout');
|
|
51
|
+
return new Response(
|
|
52
|
+
JSON.stringify({
|
|
53
|
+
success: false,
|
|
54
|
+
error: 'Request timeout - please try again',
|
|
55
|
+
}),
|
|
56
|
+
{
|
|
57
|
+
status: 408,
|
|
58
|
+
headers: {
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
throw fetchError;
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error('Confirm-booking API proxy error:', error);
|
|
68
|
+
|
|
69
|
+
return new Response(
|
|
70
|
+
JSON.stringify({
|
|
71
|
+
success: false,
|
|
72
|
+
error: 'Failed to connect to backend service',
|
|
73
|
+
}),
|
|
74
|
+
{
|
|
75
|
+
status: 500,
|
|
76
|
+
headers: {
|
|
77
|
+
'Content-Type': 'application/json',
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
};
|