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.
Files changed (95) hide show
  1. package/README.md +1 -1
  2. package/bin/create-tractstack.js +2 -2
  3. package/dist/index.js +130 -19
  4. package/package.json +2 -2
  5. package/templates/custom/minimal/CodeHook.astro +10 -2
  6. package/templates/custom/shopify/Cart.tsx +115 -77
  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 +91 -45
  10. package/templates/custom/shopify/ShopifyCheckout.tsx +4 -33
  11. package/templates/custom/shopify/ShopifyProductGrid.tsx +170 -176
  12. package/templates/custom/shopify/ShopifyServiceList.tsx +112 -51
  13. package/templates/custom/with-examples/CodeHook.astro +10 -2
  14. package/templates/src/components/Footer.astro +6 -6
  15. package/templates/src/components/Header.astro +23 -11
  16. package/templates/src/components/Menu.tsx +157 -135
  17. package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
  18. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +27 -6
  19. package/templates/src/components/codehooks/EpinetTableView.tsx +153 -112
  20. package/templates/src/components/codehooks/EpinetWrapper.tsx +4 -1
  21. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +8 -1
  22. package/templates/src/components/codehooks/ProductCardSetup.tsx +9 -1
  23. package/templates/src/components/codehooks/ProductGridSetup.tsx +9 -1
  24. package/templates/src/components/compositor/nodes/BgPaneWrapper.tsx +2 -1
  25. package/templates/src/components/compositor/nodes/GhostInsertBlock.tsx +1 -1
  26. package/templates/src/components/edit/ToolBar.tsx +2 -1
  27. package/templates/src/components/edit/context/ContextPaneConfig_slug.tsx +2 -2
  28. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +13 -0
  29. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +3 -3
  30. package/templates/src/components/edit/pane/AddPanePanel_newCustomCopy.tsx +2 -2
  31. package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +2 -2
  32. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
  33. package/templates/src/components/edit/pane/steps/AiCreativeDesignStep.tsx +2 -2
  34. package/templates/src/components/edit/pane/steps/AiLibraryCopyStep.tsx +3 -3
  35. package/templates/src/components/edit/pane/steps/AiRefineDesignStep.tsx +2 -2
  36. package/templates/src/components/edit/pane/steps/AiStandardDesignStep.tsx +7 -7
  37. package/templates/src/components/edit/state/SaveModal.tsx +1 -1
  38. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +8 -3
  39. package/templates/src/components/form/DateTimeInput.tsx +10 -3
  40. package/templates/src/components/form/FileUpload.tsx +11 -5
  41. package/templates/src/components/form/NumberInput.tsx +2 -2
  42. package/templates/src/components/form/advanced/APIConfigSection.tsx +208 -2
  43. package/templates/src/components/form/brand/SiteConfigSection.tsx +10 -0
  44. package/templates/src/components/form/shopify/SchedulingSection.tsx +354 -0
  45. package/templates/src/components/storykeep/Dashboard.tsx +1 -1
  46. package/templates/src/components/storykeep/Dashboard_Shopify.tsx +252 -110
  47. package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +2 -2
  48. package/templates/src/components/storykeep/controls/content/BeliefTable.tsx +14 -5
  49. package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +5 -2
  50. package/templates/src/components/storykeep/controls/content/MenuTable.tsx +14 -5
  51. package/templates/src/components/storykeep/controls/content/ProductTable.tsx +180 -101
  52. package/templates/src/components/storykeep/controls/content/ResourceBulkIngest.tsx +88 -56
  53. package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +14 -4
  54. package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +14 -5
  55. package/templates/src/components/storykeep/email-builder/Blocks.tsx +169 -0
  56. package/templates/src/components/storykeep/email-builder/EmailBuilder.tsx +223 -0
  57. package/templates/src/components/storykeep/email-builder/PreviewModal.tsx +136 -0
  58. package/templates/src/components/storykeep/email-builder/PropertyPanel.tsx +154 -0
  59. package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +104 -0
  60. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +419 -0
  61. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Emails.tsx +105 -0
  62. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Products.tsx +46 -0
  63. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Schedule.tsx +78 -0
  64. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +55 -0
  65. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Services.tsx +47 -0
  66. package/templates/src/layouts/Layout.astro +8 -5
  67. package/templates/src/pages/api/auth/lookup-lead.ts +72 -0
  68. package/templates/src/pages/api/booking/availability.ts +72 -0
  69. package/templates/src/pages/api/booking/cancel.ts +73 -0
  70. package/templates/src/pages/api/booking/confirm.ts +82 -0
  71. package/templates/src/pages/api/booking/hold.ts +75 -0
  72. package/templates/src/pages/api/booking/list.ts +66 -0
  73. package/templates/src/pages/api/booking/metrics.ts +60 -0
  74. package/templates/src/pages/api/booking/release.ts +76 -0
  75. package/templates/src/pages/api/sandbox.ts +2 -2
  76. package/templates/src/pages/api/shopify/createCart.ts +4 -8
  77. package/templates/src/pages/api/shopify/getProducts.ts +15 -15
  78. package/templates/src/pages/storykeep/login.astro +21 -14
  79. package/templates/src/stores/shopify.ts +97 -25
  80. package/templates/src/types/formTypes.ts +4 -2
  81. package/templates/src/types/tractstack.ts +59 -2
  82. package/templates/src/utils/api/advancedConfig.ts +2 -0
  83. package/templates/src/utils/api/advancedHelpers.ts +40 -3
  84. package/templates/src/utils/api/bookingHelpers.ts +125 -0
  85. package/templates/src/utils/api/brandConfig.ts +2 -0
  86. package/templates/src/utils/api/brandHelpers.ts +26 -0
  87. package/templates/src/utils/api/emailHelpers.ts +105 -0
  88. package/templates/src/utils/auth.ts +29 -9
  89. package/templates/src/utils/compositor/aiGeneration.ts +3 -3
  90. package/templates/src/utils/compositor/aiPaneParser.ts +2 -2
  91. package/templates/src/utils/customHelpers.ts +0 -21
  92. package/templates/src/utils/profileStorage.ts +5 -0
  93. package/templates/src/utils/tenantResolver.ts +3 -2
  94. package/utils/inject-files.ts +116 -5
  95. 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
- // Resolve tenant explicitly "Above the Fences" to guarantee the client gets the truth
54
- const resolution = await resolveTenantId(Astro.request);
55
- const tenantId = resolution.id;
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: true,
61
- isLocalhost: false,
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
+ };