@ticketboothapp/booking 0.1.10 → 0.1.12

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 (252) hide show
  1. package/package.json +1 -1
  2. package/src/colours.css +23 -0
  3. package/src/components/BookingDetails.module.css +1591 -0
  4. package/src/components/BookingDetails.tsx +2072 -354
  5. package/src/components/BookingWidget.tsx +28 -248
  6. package/src/components/JobApplicationDialog.module.css +440 -0
  7. package/src/components/JobApplicationDialog.tsx +620 -0
  8. package/src/components/ManageBookingView.tsx +344 -34
  9. package/src/components/PhoneInputWithCountry.module.css +131 -0
  10. package/src/components/PhoneInputWithCountry.tsx +44 -0
  11. package/src/components/PickupLocationDialog.module.css +360 -0
  12. package/src/components/PickupLocationDialog.tsx +357 -0
  13. package/src/components/PickupLocationMap.tsx +110 -0
  14. package/src/components/PostBookingDependentAddOnUpsell.module.css +174 -0
  15. package/src/components/PostBookingDependentAddOnUpsell.tsx +407 -0
  16. package/src/components/accordion.css +27 -0
  17. package/src/components/accordion.tsx +29 -0
  18. package/src/components/analytics/AnalyticsConsentRestore.tsx +19 -0
  19. package/src/components/analytics/AnalyticsScripts.tsx +106 -0
  20. package/src/components/analytics/CookieConsentBanner.css +86 -0
  21. package/src/components/analytics/CookieConsentBanner.tsx +102 -0
  22. package/src/components/booking/AddOnsSection.module.css +10 -0
  23. package/src/components/booking/AddOnsSection.tsx +184 -0
  24. package/src/components/booking/AdminPaymentChoiceModal.tsx +98 -0
  25. package/src/components/booking/BookingDialog.module.css +643 -0
  26. package/src/components/booking/BookingDialog.tsx +356 -0
  27. package/src/components/booking/BookingFlow.tsx +4385 -0
  28. package/src/components/booking/BookingFlowCollage.module.css +148 -0
  29. package/src/components/booking/BookingFlowCollage.tsx +184 -0
  30. package/src/components/booking/BookingFlowPlaceholder.module.css +27 -0
  31. package/src/components/booking/BookingFlowPlaceholder.tsx +25 -0
  32. package/src/components/booking/BookingFlowPreview.tsx +51 -0
  33. package/src/components/booking/BookingProductGrid.module.css +359 -0
  34. package/src/components/booking/BookingProductGrid.tsx +497 -0
  35. package/src/components/booking/Calendar.module.css +616 -0
  36. package/src/components/{Calendar.tsx → booking/Calendar.tsx} +464 -247
  37. package/src/components/booking/CancellationPolicySelector.module.css +124 -0
  38. package/src/components/booking/CancellationPolicySelector.tsx +142 -0
  39. package/src/components/booking/ChangeBookingDialog.tsx +562 -0
  40. package/src/components/booking/CheckoutForm.module.css +244 -0
  41. package/src/components/booking/CheckoutForm.tsx +364 -0
  42. package/src/components/{CheckoutModal.tsx → booking/CheckoutModal.tsx} +176 -19
  43. package/src/components/booking/DapFlowCollage.tsx +88 -0
  44. package/src/components/booking/DapTourDescription.tsx +35 -0
  45. package/src/components/booking/DependentAddOnBookingDialog.tsx +1350 -0
  46. package/src/components/booking/DependentAddOnPaymentForm.tsx +124 -0
  47. package/src/components/booking/InfoTooltip.tsx +108 -0
  48. package/src/components/booking/ItineraryBox.module.css +258 -0
  49. package/src/components/booking/ItineraryBox.tsx +550 -0
  50. package/src/components/{ItineraryBuilder.tsx → booking/ItineraryBuilder.tsx} +1 -2
  51. package/src/components/booking/ItineraryPlaceholder.module.css +45 -0
  52. package/src/components/booking/ItineraryPlaceholder.tsx +26 -0
  53. package/src/components/{MealDrinkAddOnSelector.tsx → booking/MealDrinkAddOnSelector.tsx} +21 -13
  54. package/src/components/booking/PickupLocationSelector.module.css +124 -0
  55. package/src/components/{PickupLocationSelector.tsx → booking/PickupLocationSelector.tsx} +315 -290
  56. package/src/components/booking/PickupTimeSelector.module.css +134 -0
  57. package/src/components/booking/PickupTimeSelector.tsx +112 -0
  58. package/src/components/{PriceBreakdown.tsx → booking/PriceBreakdown.tsx} +3 -3
  59. package/src/components/{PriceSummary.tsx → booking/PriceSummary.tsx} +51 -28
  60. package/src/components/booking/PrivateShuttleBookingFlow.module.css +357 -0
  61. package/src/components/booking/PrivateShuttleBookingFlow.tsx +2662 -0
  62. package/src/components/booking/PromoCodeInput.module.css +166 -0
  63. package/src/components/booking/PromoCodeInput.tsx +99 -0
  64. package/src/components/booking/ReturnTimeSelector.module.css +173 -0
  65. package/src/components/booking/ReturnTimeSelector.tsx +145 -0
  66. package/src/components/{TermsAcceptance.tsx → booking/TermsAcceptance.tsx} +9 -8
  67. package/src/components/booking/TicketSelector.module.css +164 -0
  68. package/src/components/booking/TicketSelector.tsx +199 -0
  69. package/src/components/booking/TourDescription.module.css +304 -0
  70. package/src/components/booking/TourDescription.tsx +273 -0
  71. package/src/components/booking/booking-flow-ui.ts +15 -1
  72. package/src/components/booking/booking-flow.css +944 -0
  73. package/src/components/bottom-sheet.module.css +78 -0
  74. package/src/components/bottom-sheet.tsx +60 -0
  75. package/src/components/breadcrumb.module.css +40 -0
  76. package/src/components/breadcrumb.tsx +36 -0
  77. package/src/components/button.css +245 -0
  78. package/src/components/button.tsx +152 -0
  79. package/src/components/client-bottom-sheet.tsx +14 -0
  80. package/src/components/colorable-svg.tsx +29 -0
  81. package/src/components/conditional-footer.tsx +27 -0
  82. package/src/components/contact-us.module.css +147 -0
  83. package/src/components/contact-us.tsx +49 -0
  84. package/src/components/email-signup.css +151 -0
  85. package/src/components/email-signup.tsx +63 -0
  86. package/src/components/faq-wrapper.module.css +47 -0
  87. package/src/components/faq-wrapper.tsx +15 -0
  88. package/src/components/footer.css +187 -0
  89. package/src/components/footer.tsx +143 -0
  90. package/src/components/global-simple-modal.tsx +33 -0
  91. package/src/components/google-review-summary.module.css +77 -0
  92. package/src/components/google-review-summary.tsx +50 -0
  93. package/src/components/hero-image.css +13 -0
  94. package/src/components/hero-image.tsx +44 -0
  95. package/src/components/image.css +29 -0
  96. package/src/components/image.tsx +113 -0
  97. package/src/components/language-aware-link.tsx +72 -0
  98. package/src/components/language-switcher.module.css +124 -0
  99. package/src/components/language-switcher.tsx +75 -0
  100. package/src/components/map-section.css +59 -0
  101. package/src/components/map-section.tsx +63 -0
  102. package/src/components/navbar.module.css +152 -0
  103. package/src/components/navbar.tsx +125 -0
  104. package/src/components/parallax-provider.tsx +11 -0
  105. package/src/components/partner/PartnerBookingPage.module.css +130 -0
  106. package/src/components/partner/PartnerBookingPage.tsx +390 -0
  107. package/src/components/partner/PartnerBookingPageWithBrowserMetadata.tsx +19 -35
  108. package/src/components/product-tag.module.css +30 -0
  109. package/src/components/product-tag.tsx +34 -0
  110. package/src/components/product-theme-pages/best-option.module.css +70 -0
  111. package/src/components/product-theme-pages/best-option.tsx +35 -0
  112. package/src/components/product-theme-pages/extended-tour-options.module.css +22 -0
  113. package/src/components/product-theme-pages/extended-tour-options.tsx +11 -0
  114. package/src/components/product-theme-pages/image-modal.tsx +248 -0
  115. package/src/components/product-theme-pages/photo-gallery.module.css +200 -0
  116. package/src/components/product-theme-pages/photo-gallery.tsx +90 -0
  117. package/src/components/product-theme-pages/product-theme-page-layout.module.css +13 -0
  118. package/src/components/product-theme-pages/product-theme-page-layout.tsx +67 -0
  119. package/src/components/product-theme-pages/top-of-fold.module.css +179 -0
  120. package/src/components/product-theme-pages/top-of-fold.tsx +80 -0
  121. package/src/components/product-tile/image-only-product-tile-desktop.module.css +106 -0
  122. package/src/components/product-tile/image-only-product-tile-desktop.tsx +56 -0
  123. package/src/components/product-tile/image-only-product-tile-mobile.module.css +122 -0
  124. package/src/components/product-tile/image-only-product-tile-mobile.tsx +89 -0
  125. package/src/components/product-tile/image-only-product-tile.tsx +44 -0
  126. package/src/components/product-tile/product-tile-card.module.css +84 -0
  127. package/src/components/product-tile/product-tile-card.tsx +61 -0
  128. package/src/components/review-highlights-section.css +85 -0
  129. package/src/components/review-highlights-section.tsx +127 -0
  130. package/src/components/season-closure-overlay.module.css +99 -0
  131. package/src/components/season-closure-overlay.tsx +98 -0
  132. package/src/components/simple-modal.tsx +69 -0
  133. package/src/components/simple-top-of-fold.module.css +76 -0
  134. package/src/components/simple-top-of-fold.tsx +34 -0
  135. package/src/components/spacer.css +41 -0
  136. package/src/components/spacer.tsx +23 -0
  137. package/src/components/star-rating.module.css +74 -0
  138. package/src/components/star-rating.tsx +48 -0
  139. package/src/components/terms/TermsContent.tsx +178 -0
  140. package/src/components/title-subtitle.module.css +10 -0
  141. package/src/components/title-subtitle.tsx +30 -0
  142. package/src/components/translatable-reviews.tsx +75 -0
  143. package/src/components/value-pill.module.css +59 -0
  144. package/src/components/value-pill.tsx +46 -0
  145. package/src/components/value-props.css +185 -0
  146. package/src/components/value-props.tsx +88 -0
  147. package/src/constants/booking-guide-quiz.ts +64 -0
  148. package/src/constants/contact-info.ts +2 -0
  149. package/src/constants/faq.ts +44 -0
  150. package/src/constants/images.ts +556 -0
  151. package/src/constants/json-ld/faq-json-ld.tsx +170 -0
  152. package/src/constants/json-ld/homepage-json-ld.tsx +138 -0
  153. package/src/constants/json-ld/job-posting-json-ld.tsx +92 -0
  154. package/src/constants/json-ld/organization-json-ld.tsx +62 -0
  155. package/src/constants/json-ld/page-json-ld.tsx +6 -0
  156. package/src/constants/json-ld/product-json-ld.tsx +154 -0
  157. package/src/constants/json-ld/review-json-ld.tsx +377 -0
  158. package/src/constants/navigation-links/footer-links.ts +48 -0
  159. package/src/constants/navigation-links/nav-bar-links.ts +41 -0
  160. package/src/constants/navigation-links/navigation-link.ts +6 -0
  161. package/src/constants/pill-values.ts +210 -0
  162. package/src/constants/products.ts +155 -0
  163. package/src/constants/quiz-recommendations.ts +506 -0
  164. package/src/constants/reviews.ts +75 -0
  165. package/src/constants/staff.ts +197 -0
  166. package/src/constants/value-props.ts +58 -0
  167. package/src/data/dap-descriptions/session-couples-families-friends.en.json +61 -0
  168. package/src/data/dap-descriptions/session-elopements.en.json +60 -0
  169. package/src/data/dap-descriptions/session-proposals.en.json +60 -0
  170. package/src/data/product-descriptions/afternoon-delight.en.json +35 -0
  171. package/src/data/product-descriptions/emerald-lake-escape.en.json +68 -0
  172. package/src/data/product-descriptions/lake-louise-adventure.en.json +74 -0
  173. package/src/data/product-descriptions/moraine-lake-adventure.en.json +78 -0
  174. package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +65 -0
  175. package/src/data/product-descriptions/moraine-lake-sunrise.en.json +64 -0
  176. package/src/data/product-descriptions/private-tour.en.json +80 -0
  177. package/src/data/product-descriptions/two-lakes-combo.en.json +65 -0
  178. package/src/data/products-config.json +101 -0
  179. package/src/hooks/use-bottom-sheet.tsx +15 -0
  180. package/src/hooks/use-simple-modal.tsx +27 -0
  181. package/src/hooks/useBookingSourceMetadataFromLocation.ts +21 -0
  182. package/src/hooks/useEmailSubscription.tsx +103 -0
  183. package/src/hooks/useEmbeddedInIframe.ts +16 -0
  184. package/src/hooks/useIsBookingLaunchLive.ts +49 -0
  185. package/src/hooks/useQuiz.tsx +210 -0
  186. package/src/index.ts +27 -2
  187. package/src/lib/analytics.ts +197 -0
  188. package/src/lib/booking/booking-source.ts +20 -2
  189. package/src/lib/{checkout-breakdown.ts → booking/checkout-breakdown.ts} +1 -1
  190. package/src/lib/booking/correlation-id.ts +46 -0
  191. package/src/lib/{i18n → booking/i18n}/messages/en.json +48 -4
  192. package/src/lib/{i18n → booking/i18n}/messages/fr.json +48 -4
  193. package/src/lib/booking/itinerary-display.ts +36 -0
  194. package/src/lib/{itinerary-labels.ts → booking/itinerary-labels.ts} +1 -1
  195. package/src/lib/{location-calculations.ts → booking/location-calculations.ts} +4 -4
  196. package/src/lib/{location-utils.ts → booking/location-utils.ts} +26 -0
  197. package/src/lib/{map-utils.ts → booking/map-utils.ts} +3 -3
  198. package/src/lib/booking/normalize-booking-product-id.ts +7 -0
  199. package/src/lib/{pickup-location-types.ts → booking/pickup-location-types.ts} +2 -2
  200. package/src/lib/{pricing.ts → booking/pricing.ts} +2 -2
  201. package/src/lib/booking/product-option-id.ts +35 -0
  202. package/src/lib/booking/source-metadata.ts +72 -7
  203. package/src/lib/booking/sunday-week.ts +14 -0
  204. package/src/lib/booking/trace-context.ts +62 -0
  205. package/src/lib/booking-api.ts +1793 -0
  206. package/src/lib/{constants.ts → booking-constants.ts} +11 -5
  207. package/src/lib/booking-types.ts +36 -0
  208. package/src/lib/currency.ts +38 -45
  209. package/src/lib/dap-descriptions.ts +50 -0
  210. package/src/lib/dap-itinerary-preview.ts +315 -0
  211. package/src/lib/dependent-add-on-api.ts +434 -0
  212. package/src/lib/env.ts +89 -5
  213. package/src/lib/firebase.ts +20 -0
  214. package/src/lib/job-application-api.ts +83 -0
  215. package/src/lib/manage-booking-embed-print.ts +16 -0
  216. package/src/lib/manage-booking-post-checkout.ts +68 -0
  217. package/src/lib/photo-dap-config.ts +228 -0
  218. package/src/lib/pickup/map-utils.ts +56 -0
  219. package/src/lib/pickup/marker-icons.ts +19 -0
  220. package/src/lib/product-descriptions.ts +66 -0
  221. package/src/lib/products-config.ts +73 -0
  222. package/src/providers/booking-dialog-provider.tsx +107 -38
  223. package/src/providers/bottom-sheet-provider.tsx +40 -0
  224. package/src/providers/dependent-add-on-dialog-provider.tsx +105 -0
  225. package/src/radius.css +5 -0
  226. package/src/spacing.css +7 -0
  227. package/src/strings/en.json +1774 -0
  228. package/src/strings/es.json +1573 -0
  229. package/src/strings/fr.json +1573 -0
  230. package/src/strings/index.js +23 -0
  231. package/src/text-style.css +97 -0
  232. package/src/types/fareharbor.d.ts +12 -0
  233. package/src/types/quiz.ts +59 -0
  234. package/src/utils/currency-converter.ts +101 -0
  235. package/src/components/BookingFlow.tsx +0 -2952
  236. package/src/components/LanguageSwitcher.tsx +0 -30
  237. package/src/components/PrivateShuttleBookingFlow.tsx +0 -2290
  238. package/src/components/ProductList.tsx +0 -78
  239. package/src/components/WhatsAppPhoneInput.tsx +0 -224
  240. package/src/components/index.ts +0 -31
  241. package/src/lib/api.ts +0 -801
  242. package/src/lib/booking-api-auth.ts +0 -9
  243. package/src/lib/checkout-breakdown.test.ts +0 -70
  244. package/src/types/google-maps.d.ts +0 -2
  245. /package/src/components/{CurrencySwitcher.tsx → booking/CurrencySwitcher.tsx} +0 -0
  246. /package/src/components/{ErrorBoundary.tsx → booking/ErrorBoundary.tsx} +0 -0
  247. /package/src/lib/{i18n → booking/i18n}/config.ts +0 -0
  248. /package/src/lib/{i18n → booking/i18n}/index.tsx +0 -0
  249. /package/src/lib/{marker-icons.ts → booking/marker-icons.ts} +0 -0
  250. /package/src/lib/{places-api.ts → booking/places-api.ts} +0 -0
  251. /package/src/lib/{theme.ts → booking/theme.ts} +0 -0
  252. /package/src/lib/{utils.ts → booking/utils.ts} +0 -0
@@ -1,8 +1,14 @@
1
1
  'use client';
2
2
 
3
- import { useMemo, type CSSProperties } from 'react';
3
+ import { useState, useEffect, useLayoutEffect } from 'react';
4
+ import { formatBookingRefForDisplay } from '@/lib/booking-ref';
5
+ import { BookingDetails, type BookingData } from '@/components/BookingDetails';
6
+ import { type Currency } from '@/components/CurrencySwitcher';
7
+ import { ENV } from '@/lib/env';
8
+ import { formatCurrencyAmount } from '@/lib/currency';
9
+
10
+ const API_URL = ENV.API_URL;
4
11
 
5
- /** Provider-only fields merged after public manage lookup (e.g. embedded in provider dashboard). */
6
12
  export interface StaffBookingAttribution {
7
13
  reportingSource: string;
8
14
  partnerBookingPortal: boolean;
@@ -17,6 +23,19 @@ export interface StaffBookingAttribution {
17
23
  commissionUsesPartnerRate?: boolean;
18
24
  }
19
25
 
26
+ function staffChannelLabel(source: string): string {
27
+ const labels: Record<string, string> = {
28
+ DASHBOARD: 'Dashboard',
29
+ GYG: 'GetYourGuide',
30
+ WEBSITE: 'Website (main site)',
31
+ PARTNER_PORTAL: 'Website (main site)',
32
+ WEBSITE_PARTNER_PORTAL: 'Partner booking portal',
33
+ AFFILIATE: 'Affiliate',
34
+ VIATOR: 'Viator',
35
+ };
36
+ return labels[source] ?? source;
37
+ }
38
+
20
39
  export interface ManageBookingViewProps {
21
40
  initialRef?: string;
22
41
  initialLastName?: string;
@@ -28,48 +47,339 @@ export interface ManageBookingViewProps {
28
47
  fetchStaffAttribution?: (bookingReference: string) => Promise<StaffBookingAttribution | null>;
29
48
  }
30
49
 
31
- const getBookingHost = (): string =>
32
- process.env.NEXT_PUBLIC_BOOKING_WIDGET_URL || 'https://viaviamorainelake.com';
33
-
34
50
  export function ManageBookingView({
35
- initialRef = '',
36
- initialLastName = '',
37
- initialReservationRef = '',
51
+ initialRef: initialRefProp = '',
52
+ initialLastName: initialLastNameProp = '',
53
+ initialReservationRef: initialReservationRefProp = '',
38
54
  initialPaymentSuccess = false,
55
+ replaceUrl,
39
56
  onClose,
40
57
  showCloseButton = true,
58
+ fetchStaffAttribution,
41
59
  }: ManageBookingViewProps) {
42
- const src = useMemo(() => {
43
- const url = new URL('/manage', getBookingHost());
44
- if (initialRef) url.searchParams.set('ref', initialRef);
45
- if (initialLastName) url.searchParams.set('lastName', initialLastName);
46
- if (initialReservationRef) url.searchParams.set('reservationRef', initialReservationRef);
47
- if (initialPaymentSuccess) url.searchParams.set('payment', 'balance_success');
48
- return url.toString();
49
- }, [initialLastName, initialPaymentSuccess, initialRef, initialReservationRef]);
50
-
51
- const frameStyle: CSSProperties = {
52
- width: '100%',
53
- minHeight: '700px',
54
- border: 0,
55
- display: 'block',
56
- backgroundColor: 'transparent',
57
- };
60
+ const [bookingReference, setBookingReference] = useState(initialRefProp);
61
+ const [lastName, setLastName] = useState(initialLastNameProp);
62
+ const [booking, setBooking] = useState<BookingData | null>(null);
63
+ const [staffAttribution, setStaffAttribution] = useState<StaffBookingAttribution | null>(null);
64
+ const [loading, setLoading] = useState(false);
65
+ const [error, setError] = useState('');
66
+
67
+ useLayoutEffect(() => {
68
+ setBookingReference(initialRefProp ? formatBookingRefForDisplay(initialRefProp) || initialRefProp : initialRefProp);
69
+ setLastName(initialLastNameProp);
70
+ }, [initialRefProp, initialLastNameProp]);
71
+
72
+ const initialRef = initialRefProp.trim();
73
+ const initialLastName = initialLastNameProp.trim();
74
+ const initialReservationRef = (initialReservationRefProp || '').trim();
75
+
76
+ useEffect(() => {
77
+ if (!booking?.bookingReference || !fetchStaffAttribution) {
78
+ setStaffAttribution(null);
79
+ return;
80
+ }
81
+ let cancelled = false;
82
+ const ref = formatBookingRefForDisplay(booking.bookingReference) || booking.bookingReference;
83
+ fetchStaffAttribution(ref)
84
+ .then((a) => {
85
+ if (!cancelled) setStaffAttribution(a);
86
+ })
87
+ .catch(() => {
88
+ if (!cancelled) setStaffAttribution(null);
89
+ });
90
+ return () => {
91
+ cancelled = true;
92
+ };
93
+ }, [booking?.bookingReference, fetchStaffAttribution]);
94
+
95
+ useEffect(() => {
96
+ if (!initialRef || !initialLastName) return;
97
+ let cancelled = false;
98
+ const POLL_DELAY_MS = 2000;
99
+ const MAX_POLL_ATTEMPTS = 3;
100
+
101
+ const fetchBooking = async (): Promise<BookingData | null> => {
102
+ const response = await fetch(
103
+ `${API_URL}/1/public/bookings/${encodeURIComponent(initialRef)}?lastName=${encodeURIComponent(initialLastName)}`,
104
+ { method: 'GET', headers: { 'Content-Type': 'application/json' } }
105
+ );
106
+ if (!response.ok) {
107
+ if (response.status === 404) throw new Error('Booking not found or last name does not match');
108
+ const err = await response.json();
109
+ throw new Error(err.errorMessage || err.error || 'Failed to lookup booking');
110
+ }
111
+ const data = await response.json();
112
+ return data.data as BookingData;
113
+ };
114
+
115
+ const doLookup = async () => {
116
+ setLoading(true);
117
+ setError('');
118
+ setBooking(null);
119
+ try {
120
+ let b = await fetchBooking();
121
+ if (cancelled) return;
122
+ setBooking(b);
123
+
124
+ const hasPaymentOwing = (bookingData: BookingData) => {
125
+ const status = bookingData?.payment?.status;
126
+ const balanceAmount = bookingData?.payment?.plan?.balanceAmount ?? 0;
127
+ const depositAmount = bookingData?.payment?.plan?.depositAmount ?? 0;
128
+ return (status === 'DEPOSIT_PAID' && balanceAmount > 0) ||
129
+ (status === 'AWAITING_PAYMENT' && (depositAmount > 0 || balanceAmount > 0));
130
+ };
131
+
132
+ if (initialPaymentSuccess && b && hasPaymentOwing(b)) {
133
+ for (let attempt = 1; attempt < MAX_POLL_ATTEMPTS && !cancelled; attempt++) {
134
+ await new Promise((r) => setTimeout(r, POLL_DELAY_MS));
135
+ if (cancelled) return;
136
+ b = await fetchBooking();
137
+ if (cancelled) return;
138
+ setBooking(b);
139
+ if (!b || !hasPaymentOwing(b)) break;
140
+ }
141
+ replaceUrl?.(`/manage?ref=${encodeURIComponent(initialRef)}&lastName=${encodeURIComponent(initialLastName)}`);
142
+ }
143
+ } catch (err) {
144
+ if (!cancelled) setError(err instanceof Error ? err.message : 'Failed to lookup booking');
145
+ } finally {
146
+ if (!cancelled) setLoading(false);
147
+ }
148
+ };
149
+ doLookup();
150
+ return () => { cancelled = true; };
151
+ }, [initialRef, initialLastName, initialPaymentSuccess, replaceUrl]);
152
+
153
+ useEffect(() => {
154
+ if (!initialReservationRef || !initialLastName || initialRef) return;
155
+ let cancelled = false;
156
+ const POLL_MS = 1500;
157
+ const MAX_MS = 30000;
158
+ let attempt = 0;
159
+ const doLookup = async () => {
160
+ setLoading(true);
161
+ setError('');
162
+ setBooking(null);
163
+ const poll = async (): Promise<void> => {
164
+ if (cancelled) return;
165
+ try {
166
+ const response = await fetch(
167
+ `${API_URL}/1/public/bookings/by-reservation?reservationRef=${encodeURIComponent(initialReservationRef)}&lastName=${encodeURIComponent(initialLastName)}`,
168
+ { method: 'GET', headers: { 'Content-Type': 'application/json' } }
169
+ );
170
+ if (cancelled) return;
171
+ if (response.ok) {
172
+ const data = await response.json();
173
+ const b = data.data as BookingData;
174
+ if (b?.bookingReference && !cancelled) {
175
+ const shortRef = formatBookingRefForDisplay(b.bookingReference);
176
+ replaceUrl?.(`/manage?ref=${encodeURIComponent(shortRef)}&lastName=${encodeURIComponent(initialLastName)}`);
177
+ setBookingReference(shortRef);
178
+ setBooking(b);
179
+ setLoading(false);
180
+ return;
181
+ }
182
+ }
183
+ } catch { /* ignore */ }
184
+ if (cancelled) return;
185
+ attempt += 1;
186
+ if (attempt * POLL_MS < MAX_MS) {
187
+ setTimeout(poll, POLL_MS);
188
+ } else {
189
+ if (!cancelled) setError('Booking not found. Enter your booking reference from the confirmation email when it arrives.');
190
+ setLoading(false);
191
+ }
192
+ };
193
+ await poll();
194
+ };
195
+ doLookup();
196
+ return () => { cancelled = true; };
197
+ }, [initialReservationRef, initialLastName, initialRef, replaceUrl]);
198
+
199
+ async function handleLookup() {
200
+ if (!bookingReference.trim() || !lastName.trim()) {
201
+ setError('Please enter both booking reference and last name');
202
+ return;
203
+ }
204
+
205
+ setLoading(true);
206
+ setError('');
207
+ setBooking(null);
208
+
209
+ try {
210
+ const response = await fetch(
211
+ `${API_URL}/1/public/bookings/${encodeURIComponent(bookingReference.trim())}?lastName=${encodeURIComponent(lastName.trim())}`,
212
+ { method: 'GET', headers: { 'Content-Type': 'application/json' } }
213
+ );
214
+
215
+ if (!response.ok) {
216
+ if (response.status === 404) throw new Error('Booking not found or last name does not match');
217
+ const err = await response.json();
218
+ throw new Error(err.errorMessage || err.error || 'Failed to lookup booking');
219
+ }
220
+
221
+ const data = await response.json();
222
+ setBooking(data.data);
223
+ } catch (err) {
224
+ setError(err instanceof Error ? err.message : 'Failed to lookup booking');
225
+ } finally {
226
+ setLoading(false);
227
+ }
228
+ }
229
+
230
+ async function handleRefetch() {
231
+ if (!bookingReference.trim() || !lastName.trim()) return;
232
+ try {
233
+ const response = await fetch(
234
+ `${API_URL}/1/public/bookings/${encodeURIComponent(bookingReference.trim())}?lastName=${encodeURIComponent(lastName.trim())}`,
235
+ { method: 'GET', headers: { 'Content-Type': 'application/json' } }
236
+ );
237
+ if (response.ok) {
238
+ const data = await response.json();
239
+ setBooking(data.data);
240
+ }
241
+ } catch {
242
+ // ignore
243
+ }
244
+ }
245
+
246
+ const isEmbed = !!onClose;
247
+ const showClose = isEmbed && showCloseButton;
248
+ const containerClass = isEmbed ? 'flex flex-col p-4' : 'min-h-screen bg-gradient-to-b from-stone-100 to-stone-200 flex flex-col items-center justify-center gap-4 p-4';
249
+ const formOuterClass = isEmbed ? '' : 'min-h-screen bg-gradient-to-b from-stone-100 to-stone-200 flex items-center justify-center p-4';
250
+
251
+ if (booking) {
252
+ return (
253
+ <div className={isEmbed ? 'w-full max-w-2xl' : 'min-h-screen bg-gradient-to-b from-stone-100 to-stone-200'}>
254
+ {showClose && (
255
+ <div className="flex justify-end mb-2">
256
+ <button
257
+ type="button"
258
+ onClick={onClose}
259
+ className="text-sm hover:opacity-80"
260
+ style={{ color: 'var(--booking-text-muted)' }}
261
+ >
262
+ Close
263
+ </button>
264
+ </div>
265
+ )}
266
+ {staffAttribution ? (
267
+ <div className="mb-4 rounded-xl border border-stone-200 bg-stone-50 px-4 py-3 text-sm text-stone-800" role="region" aria-label="Staff booking attribution">
268
+ <p className="font-semibold text-stone-900 mb-2">Booking attribution</p>
269
+ <p>
270
+ <span className="text-stone-600">Channel:</span> {staffChannelLabel(staffAttribution.reportingSource)}
271
+ </p>
272
+ {staffAttribution.partnerId ? (
273
+ <p>
274
+ <span className="text-stone-600">Partner:</span>{' '}
275
+ {staffAttribution.partnerName ? `${staffAttribution.partnerName} (${staffAttribution.partnerId})` : staffAttribution.partnerId}
276
+ </p>
277
+ ) : null}
278
+ {staffAttribution.agentDisplay || staffAttribution.agentId ? (
279
+ <p>
280
+ <span className="text-stone-600">Agent:</span>{' '}
281
+ {staffAttribution.agentDisplay ?? staffAttribution.agentId}
282
+ </p>
283
+ ) : null}
284
+ {staffAttribution.commissionAmount != null && staffAttribution.commissionAmount > 0 ? (
285
+ <p>
286
+ <span className="text-stone-600">Commission:</span>{' '}
287
+ {formatCurrencyAmount(staffAttribution.commissionAmount, staffAttribution.bookingCurrency as Currency)}
288
+ {staffAttribution.commissionRate != null && staffAttribution.commissionRate > 0 ? (
289
+ <>
290
+ {' '}
291
+ ({Math.round(staffAttribution.commissionRate * 100)}% of{' '}
292
+ {staffAttribution.commissionUsesPartnerRate && staffAttribution.commissionBaseAmount != null
293
+ ? `${formatCurrencyAmount(staffAttribution.commissionBaseAmount, staffAttribution.bookingCurrency as Currency)} pre-tax`
294
+ : 'booking total'}
295
+ )
296
+ </>
297
+ ) : null}
298
+ </p>
299
+ ) : null}
300
+ </div>
301
+ ) : null}
302
+ <BookingDetails booking={booking} currency={booking.receipt.currency as Currency} onRefetch={handleRefetch} />
303
+ </div>
304
+ );
305
+ }
306
+
307
+ const waitingForBooking = initialReservationRef && initialLastName && !initialRef;
308
+ if (waitingForBooking && loading) {
309
+ return (
310
+ <div className={containerClass}>
311
+ <div className="w-10 h-10 border-2 border-t-transparent rounded-full animate-spin" style={{ borderColor: 'var(--booking-primary)' }} aria-hidden />
312
+ <p style={{ color: 'var(--booking-text-muted)' }}>Confirming your booking…</p>
313
+ </div>
314
+ );
315
+ }
58
316
 
59
317
  return (
60
- <div className="w-full">
61
- {onClose && showCloseButton ? (
62
- <div className="mb-2 flex justify-end">
318
+ <div className={formOuterClass || undefined}>
319
+ <div className="p-8 max-w-md w-full shadow-xl" style={{ backgroundColor: 'var(--booking-surface)', borderRadius: 'var(--booking-radius)' }}>
320
+ {isEmbed && (
321
+ <div className={`flex justify-between items-center mb-4 ${showClose ? '' : 'justify-start'}`}>
322
+ <h1 className="text-2xl font-bold" style={{ color: 'var(--booking-text)' }}>Manage Your Booking</h1>
323
+ {showClose && (
324
+ <button type="button" onClick={onClose} className="text-sm hover:opacity-80" style={{ color: 'var(--booking-text-muted)' }}>
325
+ Close
326
+ </button>
327
+ )}
328
+ </div>
329
+ )}
330
+ {!isEmbed && <h1 className="text-2xl font-bold mb-6" style={{ color: 'var(--booking-text)' }}>Manage Your Booking</h1>}
331
+
332
+ <form onSubmit={(e) => { e.preventDefault(); handleLookup(); }} className="space-y-4">
333
+ <div>
334
+ <label htmlFor="manage-booking-ref" className="block text-sm font-medium mb-1" style={{ color: 'var(--booking-text)' }}>
335
+ Booking Reference
336
+ </label>
337
+ <input
338
+ id="manage-booking-ref"
339
+ type="text"
340
+ value={bookingReference}
341
+ onChange={(e) => setBookingReference(formatBookingRefForDisplay(e.target.value) || e.target.value)}
342
+ className="w-full px-4 py-2 rounded-lg focus:ring-2"
343
+ style={{ borderColor: 'var(--booking-border-input)', borderWidth: '1px', borderStyle: 'solid' }}
344
+ placeholder="e.g., ABC12345"
345
+ required
346
+ />
347
+ </div>
348
+ <div>
349
+ <label htmlFor="manage-booking-lastname" className="block text-sm font-medium mb-1" style={{ color: 'var(--booking-text)' }}>
350
+ Last Name
351
+ </label>
352
+ <input
353
+ id="manage-booking-lastname"
354
+ type="text"
355
+ value={lastName}
356
+ onChange={(e) => setLastName(e.target.value)}
357
+ className="w-full px-4 py-2 rounded-lg focus:ring-2"
358
+ style={{ borderColor: 'var(--booking-border-input)', borderWidth: '1px', borderStyle: 'solid' }}
359
+ placeholder="As entered at booking"
360
+ required
361
+ />
362
+ </div>
363
+ {error && (
364
+ <div className="rounded-lg p-3 text-sm" style={{ backgroundColor: 'var(--booking-error-bg)', border: '1px solid var(--booking-error-border)', color: 'var(--booking-error-text)' }}>{error}</div>
365
+ )}
63
366
  <button
64
- type="button"
65
- onClick={onClose}
66
- className="rounded border border-stone-300 px-3 py-1 text-sm hover:bg-stone-100"
367
+ type="submit"
368
+ disabled={loading}
369
+ className="w-full py-3 text-white font-semibold rounded-lg transition-colors disabled:cursor-not-allowed"
370
+ style={{ backgroundColor: loading ? 'var(--booking-border-input)' : 'var(--booking-primary)' }}
67
371
  >
68
- Close
372
+ {loading ? 'Looking up...' : 'View Booking'}
69
373
  </button>
70
- </div>
71
- ) : null}
72
- <iframe title="Manage booking" src={src} style={frameStyle} loading="lazy" />
374
+ </form>
375
+
376
+ {!isEmbed && (
377
+ <div className="mt-6 pt-6 text-center text-sm" style={{ borderColor: 'var(--booking-border)', borderTopWidth: '1px', borderTopStyle: 'solid', color: 'var(--booking-text-muted)' }}>
378
+ <p>Enter your booking reference and last name to view your booking details.</p>
379
+ <p className="mt-1" style={{ color: 'var(--booking-text-muted)' }}>You can also share a link: /manage?ref=ABC12345&amp;lastName=Smith</p>
380
+ </div>
381
+ )}
382
+ </div>
73
383
  </div>
74
384
  );
75
385
  }
@@ -0,0 +1,131 @@
1
+ /* Clean, minimal phone input matching the reference design */
2
+ .wrapper {
3
+ width: 100%;
4
+ position: relative;
5
+ overflow: visible;
6
+ --react-international-phone-height: 44px;
7
+ --react-international-phone-border-color: rgba(0, 0, 0, 0.12);
8
+ --react-international-phone-border-radius: 12px;
9
+ --react-international-phone-background-color: #fff;
10
+ --react-international-phone-text-color: var(--primary-text, #333);
11
+ --react-international-phone-font-size: 1rem;
12
+ --react-international-phone-dropdown-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
13
+ --react-international-phone-dropdown-item-height: 40px;
14
+ --react-international-phone-dropdown-item-font-size: 14px;
15
+ --react-international-phone-dropdown-item-text-color: var(--primary-text, #333);
16
+ --react-international-phone-dropdown-item-dial-code-color: var(--grey-text, #6b7280);
17
+ --react-international-phone-selected-dropdown-item-background-color: #f5f5f5;
18
+ --react-international-phone-country-selector-border-color: transparent;
19
+ --react-international-phone-country-selector-background-color: transparent;
20
+ }
21
+
22
+ .phoneInput {
23
+ width: 100%;
24
+ }
25
+
26
+ /* Container: single unified border around flag + input */
27
+ .wrapper :global(.react-international-phone-input-container) {
28
+ display: flex;
29
+ align-items: center;
30
+ width: 100%;
31
+ border: 2px solid var(--accent-orange-10, rgba(0, 0, 0, 0.08));
32
+ border-radius: 12px;
33
+ background: var(--primary-background, #fff);
34
+ transition: border-color 0.15s;
35
+ overflow: visible;
36
+ }
37
+
38
+ /* Country selector needs overflow visible so dropdown can escape */
39
+ .wrapper :global(.react-international-phone-country-selector) {
40
+ overflow: visible;
41
+ }
42
+
43
+ .wrapper:focus-within :global(.react-international-phone-input-container) {
44
+ border-color: var(--accent-orange);
45
+ outline: none;
46
+ }
47
+
48
+ /* Country selector button - no separate border, blends into container */
49
+ .wrapper :global(.react-international-phone-country-selector-button) {
50
+ border: none;
51
+ border-radius: 0;
52
+ background: transparent;
53
+ padding: 0 0.5rem 0 0.75rem;
54
+ height: 100%;
55
+ }
56
+
57
+ .wrapper :global(.react-international-phone-country-selector-button:hover) {
58
+ background: rgba(0, 0, 0, 0.03);
59
+ }
60
+
61
+ .wrapper :global(.react-international-phone-country-selector-button__dropdown-arrow) {
62
+ border-top-color: var(--grey-text, #6b7280);
63
+ margin-left: 0.25rem;
64
+ }
65
+
66
+ /* Input field */
67
+ .wrapper :global(.react-international-phone-input) {
68
+ flex: 1;
69
+ min-width: 0;
70
+ border: none;
71
+ border-radius: 0;
72
+ padding: 0.625rem 0.875rem;
73
+ font-size: 1rem;
74
+ font-family: inherit;
75
+ color: var(--primary-text, #333);
76
+ background: transparent;
77
+ }
78
+
79
+ .wrapper :global(.react-international-phone-input):focus {
80
+ outline: none;
81
+ }
82
+
83
+ .wrapper :global(.react-international-phone-input)::placeholder {
84
+ color: var(--grey-text, #878686);
85
+ }
86
+
87
+ /* Dropdown - floating panel BELOW the input, not inside */
88
+ .wrapper :global(.react-international-phone-country-selector-dropdown),
89
+ .dropdown {
90
+ position: absolute !important;
91
+ top: 100% !important;
92
+ left: 0 !important;
93
+ margin-top: 6px !important;
94
+ width: 100% !important;
95
+ min-width: 280px;
96
+ max-height: 280px;
97
+ border-radius: 12px;
98
+ border: 1px solid rgba(0, 0, 0, 0.08);
99
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
100
+ background: #fff;
101
+ padding: 8px 0;
102
+ z-index: 1000;
103
+ }
104
+
105
+ .wrapper :global(.react-international-phone-country-selector-dropdown__list-item) {
106
+ padding: 0.5rem 1rem;
107
+ min-height: 40px;
108
+ cursor: pointer;
109
+ }
110
+
111
+ .wrapper :global(.react-international-phone-country-selector-dropdown__list-item):hover,
112
+ .wrapper :global(.react-international-phone-country-selector-dropdown__list-item--focused) {
113
+ background: #f5f5f5;
114
+ }
115
+
116
+ .wrapper :global(.react-international-phone-country-selector-dropdown__list-item-country-name) {
117
+ font-size: 14px;
118
+ color: var(--primary-text, #333);
119
+ }
120
+
121
+ .wrapper :global(.react-international-phone-country-selector-dropdown__list-item-dial-code) {
122
+ font-size: 14px;
123
+ color: var(--grey-text, #6b7280);
124
+ }
125
+
126
+ /* Flag sizing */
127
+ .wrapper :global(.react-international-phone-country-selector-button__flag-emoji),
128
+ .wrapper :global(.react-international-phone-country-selector-dropdown__list-item-flag-emoji) {
129
+ width: 20px;
130
+ height: 14px;
131
+ }
@@ -0,0 +1,44 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { PhoneInput } from 'react-international-phone';
5
+ import 'react-international-phone/style.css';
6
+ import styles from './PhoneInputWithCountry.module.css';
7
+
8
+ interface PhoneInputWithCountryProps {
9
+ id?: string;
10
+ value: string;
11
+ onChange: (value: string | undefined) => void;
12
+ placeholder?: string;
13
+ disabled?: boolean;
14
+ className?: string;
15
+ }
16
+
17
+ export default function PhoneInputWithCountryComponent({
18
+ id,
19
+ value,
20
+ onChange,
21
+ placeholder = 'Phone number',
22
+ disabled = false,
23
+ className,
24
+ }: PhoneInputWithCountryProps) {
25
+ return (
26
+ <div className={`${styles.wrapper} ${className ?? ''}`}>
27
+ <PhoneInput
28
+ defaultCountry="ca"
29
+ preferredCountries={['ca', 'us']}
30
+ value={value || ''}
31
+ onChange={(phone) => onChange(phone || undefined)}
32
+ placeholder={placeholder}
33
+ disabled={disabled}
34
+ inputProps={{ id }}
35
+ className={styles.phoneInput}
36
+ countrySelectorStyleProps={{
37
+ dropdownStyleProps: {
38
+ className: styles.dropdown,
39
+ },
40
+ }}
41
+ />
42
+ </div>
43
+ );
44
+ }