@ticketboothapp/booking 1.2.24 → 1.2.25-rc.0

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 (158) hide show
  1. package/package.json +29 -2
  2. package/src/assets/icons/minus.svg +7 -0
  3. package/src/assets/icons/partner-logos/getyourguide.svg +8 -0
  4. package/src/assets/icons/plus.svg +3 -0
  5. package/src/colours.css +23 -0
  6. package/src/components/BookingDetails.module.css +1591 -0
  7. package/src/components/BookingDetails.tsx +2264 -0
  8. package/src/components/BookingWidget.tsx +302 -0
  9. package/src/components/ManageBookingView.tsx +437 -0
  10. package/src/components/PhoneInputWithCountry.module.css +131 -0
  11. package/src/components/PhoneInputWithCountry.tsx +44 -0
  12. package/src/components/PickupLocationDialog.module.css +360 -0
  13. package/src/components/PickupLocationDialog.tsx +357 -0
  14. package/src/components/PostBookingDependentAddOnUpsell.module.css +174 -0
  15. package/src/components/PostBookingDependentAddOnUpsell.tsx +407 -0
  16. package/src/components/booking/AddOnsSection.module.css +10 -0
  17. package/src/components/booking/AddOnsSection.tsx +184 -0
  18. package/src/components/booking/AdminPaymentChoiceModal.tsx +98 -0
  19. package/src/components/booking/BookingDialog.module.css +643 -0
  20. package/src/components/booking/BookingDialog.tsx +356 -0
  21. package/src/components/booking/BookingFlow.tsx +4385 -0
  22. package/src/components/booking/BookingFlowCollage.module.css +148 -0
  23. package/src/components/booking/BookingFlowCollage.tsx +184 -0
  24. package/src/components/booking/BookingFlowPlaceholder.module.css +27 -0
  25. package/src/components/booking/BookingFlowPlaceholder.tsx +25 -0
  26. package/src/components/booking/BookingFlowPreview.tsx +51 -0
  27. package/src/components/booking/BookingProductGrid.module.css +359 -0
  28. package/src/components/booking/BookingProductGrid.tsx +497 -0
  29. package/src/components/booking/Calendar.module.css +616 -0
  30. package/src/components/booking/Calendar.tsx +1123 -0
  31. package/src/components/booking/CancellationPolicySelector.module.css +124 -0
  32. package/src/components/booking/CancellationPolicySelector.tsx +142 -0
  33. package/src/components/booking/ChangeBookingDialog.tsx +562 -0
  34. package/src/components/booking/CheckoutForm.module.css +244 -0
  35. package/src/components/booking/CheckoutForm.tsx +364 -0
  36. package/src/components/booking/CheckoutModal.tsx +451 -0
  37. package/src/components/booking/CurrencySwitcher.tsx +81 -0
  38. package/src/components/booking/DapFlowCollage.tsx +88 -0
  39. package/src/components/booking/DapTourDescription.tsx +35 -0
  40. package/src/components/booking/DependentAddOnBookingDialog.tsx +1350 -0
  41. package/src/components/booking/DependentAddOnPaymentForm.tsx +124 -0
  42. package/src/components/booking/ErrorBoundary.tsx +63 -0
  43. package/src/components/booking/InfoTooltip.tsx +108 -0
  44. package/src/components/booking/ItineraryBox.module.css +258 -0
  45. package/src/components/booking/ItineraryBox.tsx +550 -0
  46. package/src/components/booking/ItineraryBuilder.tsx +82 -0
  47. package/src/components/booking/ItineraryPlaceholder.module.css +45 -0
  48. package/src/components/booking/ItineraryPlaceholder.tsx +26 -0
  49. package/src/components/booking/MealDrinkAddOnSelector.tsx +338 -0
  50. package/src/components/booking/PickupLocationSelector.module.css +124 -0
  51. package/src/components/booking/PickupLocationSelector.tsx +1566 -0
  52. package/src/components/booking/PickupTimeSelector.module.css +134 -0
  53. package/src/components/booking/PickupTimeSelector.tsx +112 -0
  54. package/src/components/booking/PriceBreakdown.tsx +154 -0
  55. package/src/components/booking/PriceSummary.tsx +234 -0
  56. package/src/components/booking/PrivateShuttleBookingFlow.module.css +357 -0
  57. package/src/components/booking/PrivateShuttleBookingFlow.tsx +2662 -0
  58. package/src/components/booking/PromoCodeInput.module.css +166 -0
  59. package/src/components/booking/PromoCodeInput.tsx +99 -0
  60. package/src/components/booking/ReturnTimeSelector.module.css +173 -0
  61. package/src/components/booking/ReturnTimeSelector.tsx +145 -0
  62. package/src/components/booking/TermsAcceptance.tsx +111 -0
  63. package/src/components/booking/TicketSelector.module.css +164 -0
  64. package/src/components/booking/TicketSelector.tsx +199 -0
  65. package/src/components/booking/TourDescription.module.css +304 -0
  66. package/src/components/booking/TourDescription.tsx +273 -0
  67. package/src/components/booking/booking-flow-ui.ts +38 -0
  68. package/src/components/booking/booking-flow.css +944 -0
  69. package/src/components/button.css +245 -0
  70. package/src/components/button.tsx +152 -0
  71. package/src/components/colorable-svg.tsx +29 -0
  72. package/src/components/image.css +29 -0
  73. package/src/components/image.tsx +113 -0
  74. package/src/components/partner/PartnerBookingPage.module.css +130 -0
  75. package/src/components/partner/PartnerBookingPage.tsx +390 -0
  76. package/src/components/partner/PartnerBookingPageWithBrowserMetadata.tsx +45 -0
  77. package/src/components/product-tag.module.css +30 -0
  78. package/src/components/product-tag.tsx +34 -0
  79. package/src/components/product-theme-pages/image-modal.tsx +248 -0
  80. package/src/components/product-theme-pages/photo-gallery.module.css +200 -0
  81. package/src/components/terms/TermsContent.tsx +178 -0
  82. package/src/components/value-pill.module.css +59 -0
  83. package/src/components/value-pill.tsx +46 -0
  84. package/src/constants/images.ts +556 -0
  85. package/src/constants/pill-values.ts +210 -0
  86. package/src/constants/products.ts +155 -0
  87. package/src/contexts/AvailabilitiesCacheContext.tsx +125 -0
  88. package/src/contexts/BookingAppContext.tsx +134 -0
  89. package/src/contexts/CompanyContext.tsx +70 -0
  90. package/src/data/dap-descriptions/session-couples-families-friends.en.json +61 -0
  91. package/src/data/dap-descriptions/session-elopements.en.json +60 -0
  92. package/src/data/dap-descriptions/session-proposals.en.json +60 -0
  93. package/src/data/product-descriptions/afternoon-delight.en.json +35 -0
  94. package/src/data/product-descriptions/emerald-lake-escape.en.json +68 -0
  95. package/src/data/product-descriptions/lake-louise-adventure.en.json +74 -0
  96. package/src/data/product-descriptions/moraine-lake-adventure.en.json +78 -0
  97. package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +65 -0
  98. package/src/data/product-descriptions/moraine-lake-sunrise.en.json +64 -0
  99. package/src/data/product-descriptions/private-tour.en.json +80 -0
  100. package/src/data/product-descriptions/two-lakes-combo.en.json +65 -0
  101. package/src/data/products-config.json +101 -0
  102. package/src/hooks/useBookingSourceMetadataFromLocation.ts +21 -0
  103. package/src/hooks/useIsBookingLaunchLive.ts +49 -0
  104. package/src/index.ts +79 -0
  105. package/src/lib/analytics.ts +197 -0
  106. package/src/lib/booking/booking-source.ts +51 -0
  107. package/src/lib/booking/checkout-breakdown.ts +69 -0
  108. package/src/lib/booking/correlation-id.ts +46 -0
  109. package/src/lib/booking/i18n/config.ts +21 -0
  110. package/src/lib/booking/i18n/index.tsx +144 -0
  111. package/src/lib/booking/i18n/messages/en.json +236 -0
  112. package/src/lib/booking/i18n/messages/fr.json +236 -0
  113. package/src/lib/booking/itinerary-display.ts +36 -0
  114. package/src/lib/booking/itinerary-labels.ts +70 -0
  115. package/src/lib/booking/location-calculations.ts +43 -0
  116. package/src/lib/booking/location-utils.ts +165 -0
  117. package/src/lib/booking/map-utils.ts +153 -0
  118. package/src/lib/booking/marker-icons.ts +113 -0
  119. package/src/lib/booking/normalize-booking-product-id.ts +21 -0
  120. package/src/lib/booking/pickup-location-types.ts +25 -0
  121. package/src/lib/booking/places-api.ts +154 -0
  122. package/src/lib/booking/pricing.ts +466 -0
  123. package/src/lib/booking/product-option-id.ts +35 -0
  124. package/src/lib/booking/source-metadata.ts +226 -0
  125. package/src/lib/booking/sunday-week.ts +14 -0
  126. package/src/lib/booking/theme.ts +83 -0
  127. package/src/lib/booking/trace-context.ts +62 -0
  128. package/src/lib/booking/utils.ts +9 -0
  129. package/src/lib/booking-api.ts +1793 -0
  130. package/src/lib/booking-constants.ts +23 -0
  131. package/src/lib/booking-ref.ts +13 -0
  132. package/src/lib/booking-types.ts +36 -0
  133. package/src/lib/currency.ts +81 -0
  134. package/src/lib/dap-descriptions.ts +50 -0
  135. package/src/lib/dap-itinerary-preview.ts +315 -0
  136. package/src/lib/dependent-add-on-api.ts +434 -0
  137. package/src/lib/env.ts +96 -0
  138. package/src/lib/firebase.ts +20 -0
  139. package/src/lib/job-application-api.ts +83 -0
  140. package/src/lib/manage-booking-embed-print.ts +16 -0
  141. package/src/lib/manage-booking-post-checkout.ts +68 -0
  142. package/src/lib/photo-dap-config.ts +228 -0
  143. package/src/lib/photo-packages.ts +75 -0
  144. package/src/lib/pickup/map-utils.ts +56 -0
  145. package/src/lib/pickup/marker-icons.ts +19 -0
  146. package/src/lib/product-descriptions.ts +66 -0
  147. package/src/lib/products-config.ts +73 -0
  148. package/src/providers/booking-dialog-provider.tsx +282 -0
  149. package/src/providers/dependent-add-on-dialog-provider.tsx +105 -0
  150. package/src/radius.css +5 -0
  151. package/src/spacing.css +7 -0
  152. package/src/strings/en.json +1774 -0
  153. package/src/strings/es.json +1573 -0
  154. package/src/strings/fr.json +1573 -0
  155. package/src/strings/index.js +23 -0
  156. package/src/text-style.css +56 -0
  157. package/src/utils/currency-converter.ts +101 -0
  158. package/tsconfig.json +8 -2
@@ -0,0 +1,210 @@
1
+ const busIconPath = '/pill-value-icons/bus-icon.svg';
2
+ const clockIconPath = '/pill-value-icons/clock-icon.svg';
3
+ const sunriseIconPath = '/pill-value-icons/sun-icon.svg';
4
+ const cameraIconPath = '/pill-value-icons/camera-icon.svg';
5
+ const moneyIconPath = '/pill-value-icons/money-icon.svg';
6
+ const doubleCheckIconPath = '/pill-value-icons/double-check-icon.svg';
7
+ const hikerIconPath = '/pill-value-icons/hiker-icon.svg';
8
+ const waterIconPath = '/pill-value-icons/water-icon.svg';
9
+ const lunchIconPath = '/pill-value-icons/lunch-icon.svg';
10
+ const croissantIconPath = '/pill-value-icons/croissant-icon.svg';
11
+ const locationPinIconPath = '/pill-value-icons/location-pin-icon.svg';
12
+ const addTimeIconPath = '/pill-value-icons/add-time-icon.svg';
13
+ const coffeeIconPath = '/pill-value-icons/coffee-icon.svg';
14
+ const blanketIconPath = '/pill-value-icons/blanket-icon.svg';
15
+ import defaultStrings from '@/strings';
16
+ import { ProductKey } from './products';
17
+
18
+ export type PillValue = {
19
+ icon: string;
20
+ label: string;
21
+ }
22
+
23
+ export function createDeparturePillValue(productKey: ProductKey, strings = defaultStrings): PillValue {
24
+ return {
25
+ icon: busIconPath,
26
+ label: getDepartureLabel(productKey, strings)
27
+ };
28
+ }
29
+
30
+ function getDepartureLabel(productKey: ProductKey, strings: any): string {
31
+ switch (productKey) {
32
+ case 'two-lakes-combo':
33
+ return strings.pillValues.departurePillValues.twoLakesCombo;
34
+ case 'moraine-lake-adventure':
35
+ return strings.pillValues.departurePillValues.moraineLakeAdventure;
36
+ case 'lake-louise-adventure':
37
+ return strings.pillValues.departurePillValues.lakeLouiseAdventure;
38
+ case 'emerald-lake-escape':
39
+ return strings.pillValues.departurePillValues.emeraldLakeEscapeTour;
40
+ case 'private-tour':
41
+ return strings.pillValues.departurePillValues.privateTour;
42
+ case 'afternoon-delight':
43
+ return strings.pillValues.departurePillValues.afternoonDelight;
44
+ default:
45
+ return '';
46
+ }
47
+ }
48
+
49
+ export function createDurationPillValue(productKey: ProductKey, strings = defaultStrings): PillValue {
50
+ return {
51
+ icon: clockIconPath,
52
+ label: getDurationLabel(productKey, strings)
53
+ };
54
+ }
55
+
56
+ function getDurationLabel(productKey: ProductKey, strings: any): string {
57
+ switch (productKey) {
58
+ case 'moraine-lake-sunrise':
59
+ return strings.pillValues.durationPillValues.moraineLakeSunrise;
60
+ case 'moraine-lake-sunrise-lake-louise-golden-hour-1':
61
+ return strings.pillValues.durationPillValues.moraineLakeSunriseLakeLouiseGoldenHour1;
62
+ case 'moraine-lake-sunrise-lake-louise-golden-hour-2':
63
+ return strings.pillValues.durationPillValues.moraineLakeSunriseLakeLouiseGoldenHour2;
64
+ case 'two-lakes-combo-1':
65
+ return strings.pillValues.durationPillValues.twoLakesCombo1;
66
+ case 'two-lakes-combo-2':
67
+ return strings.pillValues.durationPillValues.twoLakesCombo2;
68
+ case 'moraine-lake-adventure':
69
+ return strings.pillValues.durationPillValues.moraineLakeAdventure;
70
+ case 'lake-louise-adventure':
71
+ return strings.pillValues.durationPillValues.lakeLouiseAdventure;
72
+ case 'emerald-lake-escape':
73
+ return strings.pillValues.durationPillValues.emeraldLakeEscapeTour;
74
+ case 'private-tour':
75
+ return strings.pillValues.durationPillValues.privateTour;
76
+ case 'afternoon-delight1':
77
+ return strings.pillValues.durationPillValues.afternoonDelight1;
78
+ case 'afternoon-delight2':
79
+ return strings.pillValues.durationPillValues.afternoonDelight2;
80
+ default:
81
+ return '';
82
+ }
83
+ }
84
+
85
+ export function createAddTimePillValue(productKey: ProductKey, strings = defaultStrings): PillValue {
86
+ return {
87
+ icon: addTimeIconPath,
88
+ label: getAddTimeLabel(productKey, strings)
89
+ };
90
+ }
91
+
92
+ function getAddTimeLabel(productKey: ProductKey, strings: any): string {
93
+ switch (productKey) {
94
+ case 'moraine-lake-sunrise':
95
+ return strings.pillValues.addTimePillValues.moraineLakeSunrise;
96
+ case 'moraine-lake-sunrise-lake-louise-golden-hour':
97
+ return strings.pillValues.addTimePillValues.moraineLakeSunriseLakeLouiseGoldenHour;
98
+ case 'two-lakes-combo':
99
+ return strings.pillValues.addTimePillValues.twoLakesCombo;
100
+ case 'moraine-lake-adventure':
101
+ return strings.pillValues.addTimePillValues.moraineLakeAdventure;
102
+ case 'lake-louise-adventure':
103
+ return strings.pillValues.addTimePillValues.lakeLouiseAdventure;
104
+ case 'emerald-lake-escape':
105
+ return strings.pillValues.addTimePillValues.emeraldLakeEscapeTour;
106
+ case 'private-tour':
107
+ return strings.pillValues.addTimePillValues.privateTour;
108
+ default:
109
+ return '';
110
+ }
111
+ }
112
+
113
+ export function createMoneyPillValue(productKey: ProductKey, strings = defaultStrings): PillValue {
114
+ return {
115
+ icon: moneyIconPath,
116
+ label: getMoneyLabel(productKey, strings)
117
+ };
118
+ }
119
+
120
+ function getMoneyLabel(productKey: ProductKey, strings: any): string {
121
+ switch (productKey) {
122
+ case 'moraine-lake-sunrise':
123
+ return strings.pillValues.moneyPillValues.moraineLakeSunrise;
124
+ case 'moraine-lake-sunrise-lake-louise-golden-hour':
125
+ return strings.pillValues.moneyPillValues.moraineLakeSunriseLakeLouiseGoldenHour;
126
+ case 'two-lakes-combo':
127
+ return strings.pillValues.moneyPillValues.twoLakesCombo;
128
+ case 'moraine-lake-adventure':
129
+ return strings.pillValues.moneyPillValues.moraineLakeAdventure;
130
+ case 'lake-louise-adventure':
131
+ return strings.pillValues.moneyPillValues.lakeLouiseAdventure;
132
+ case 'emerald-lake-escape':
133
+ return strings.pillValues.moneyPillValues.emeraldLakeEscapeTour;
134
+ case 'private-tour':
135
+ return strings.pillValues.moneyPillValues.privateTour;
136
+ case 'afternoon-delight':
137
+ return strings.pillValues.moneyPillValues.afternoonDelight;
138
+ default:
139
+ return '';
140
+ }
141
+ }
142
+
143
+ export function createemeraldLakeEscapeTourLocationsPillValues(strings = defaultStrings): PillValue[] {
144
+ return Object.values(strings.pillValues.emeraldLakeEscapeTourLocations).map(location => ({
145
+ icon: locationPinIconPath,
146
+ label: location as string
147
+ }));
148
+ }
149
+ export const createSunrisePillValue = (strings = defaultStrings): PillValue => {
150
+ return {
151
+ icon: sunriseIconPath,
152
+ label: strings.pillValues.sunrise
153
+ };
154
+ }
155
+
156
+ export const createTwoLakesInOnePillValue = (strings = defaultStrings): PillValue => {
157
+ return {
158
+ icon: doubleCheckIconPath,
159
+ label: strings.pillValues.twoLakesInOne
160
+ };
161
+ }
162
+
163
+ export const createPhotographyPillValue = (strings = defaultStrings): PillValue => {
164
+ return {
165
+ icon: cameraIconPath,
166
+ label: strings.pillValues.photography
167
+ };
168
+ }
169
+
170
+ export const createHikePillValue = (strings = defaultStrings): PillValue => {
171
+ return {
172
+ icon: hikerIconPath,
173
+ label: strings.pillValues.hike
174
+ };
175
+ }
176
+
177
+ export const createCanoePillValue = (strings = defaultStrings): PillValue => {
178
+ return {
179
+ icon: waterIconPath,
180
+ label: strings.pillValues.canoe
181
+ };
182
+ }
183
+
184
+ export const createLunchPillValue = (strings = defaultStrings): PillValue => {
185
+ return {
186
+ icon: lunchIconPath,
187
+ label: strings.pillValues.lunch
188
+ };
189
+ }
190
+
191
+ export const createCroissantPillValue = (strings = defaultStrings): PillValue => {
192
+ return {
193
+ icon: croissantIconPath,
194
+ label: strings.pillValues.croissant
195
+ };
196
+ }
197
+
198
+ export const createHotDrinksPillValue = (strings = defaultStrings): PillValue => {
199
+ return {
200
+ icon: coffeeIconPath,
201
+ label: strings.pillValues.hotDrinks
202
+ };
203
+ }
204
+
205
+ export const createCozyBlanketsPillValue = (strings = defaultStrings): PillValue => {
206
+ return {
207
+ icon: blanketIconPath,
208
+ label: strings.pillValues.blankets
209
+ };
210
+ }
@@ -0,0 +1,155 @@
1
+ import { ImageData, IMAGES } from './images';
2
+ import { PillValue, createDeparturePillValue, createDurationPillValue, createSunrisePillValue, createTwoLakesInOnePillValue, createHikePillValue, createCanoePillValue, createMoneyPillValue, createAddTimePillValue, createHotDrinksPillValue, createLunchPillValue, createCroissantPillValue, createemeraldLakeEscapeTourLocationsPillValues, createCozyBlanketsPillValue } from './pill-values';
3
+ import { ProductTagStyle } from '@/components/product-tag';
4
+
5
+ export interface ProductTag {
6
+ text: string;
7
+ style: ProductTagStyle;
8
+ }
9
+
10
+ export interface VideoSources {
11
+ src: string;
12
+ webm: string;
13
+ /** Long version for BookingFlow collage. Falls back to short (src/webm) if omitted. */
14
+ longSrc?: string;
15
+ longWebm?: string;
16
+ }
17
+
18
+ export interface Product {
19
+ id: string;
20
+ name: string;
21
+ shortName: string;
22
+ images: readonly ImageData[];
23
+ bookingLink: string;
24
+ currentStartTime: string;
25
+ description: string;
26
+ path: string;
27
+ avgPrice: number;
28
+ pillValues: PillValue[];
29
+ tags?: ProductTag[];
30
+ /** Video for expanded view in booking dialog. Place mp4 and webm in public/videos/ named {productId}.mp4 and {productId}.webm */
31
+ videoUrl?: VideoSources;
32
+ }
33
+
34
+ // Type for accessing product keys
35
+ export type ProductKey = keyof ReturnType<typeof getProducts>;
36
+
37
+ export const getProducts = (strings: any): Record<string, Product> => ({
38
+ MORAINE_LAKE_SUNRISE_LAKE_LOUISE_GOLDEN_HOUR: {
39
+ id: 'moraine-lake-sunrise-lake-louise-golden-hour',
40
+ videoUrl: { src: '/videos/moraine-lake-sunrise-lake-louise-golden-hour.mp4', webm: '/videos/moraine-lake-sunrise-lake-louise-golden-hour.webm', longSrc: '/videos/moraine-lake-sunrise-lake-louise-golden-hour-long.mp4', longWebm: '/videos/moraine-lake-sunrise-lake-louise-golden-hour-long.webm' },
41
+ name: strings.products.productNames.moraineLakeSunriseLakeLouiseGoldenHour,
42
+ shortName: strings.products.productShortNames.moraineLakeSunriseLakeLouiseGoldenHourShort,
43
+ images: [
44
+ IMAGES.MORAINE_LAKE_CANOES_SUNRISE,
45
+ ],
46
+ bookingLink: '#book-now/moraine-lake-sunrise-lake-louise-golden-hour',
47
+ currentStartTime: strings.products.startTimes.moraineLakeSunriseLakeLouiseGoldenHour,
48
+ description: strings.productThemePages.sunriseTours.description,
49
+ path: '/moraine-lake-sunrise-shuttle',
50
+ avgPrice: 159,
51
+ pillValues: [createSunrisePillValue(strings), createDurationPillValue('moraine-lake-sunrise-lake-louise-golden-hour-1', strings), createDurationPillValue('moraine-lake-sunrise-lake-louise-golden-hour-2', strings), createAddTimePillValue('moraine-lake-sunrise-lake-louise-golden-hour', strings), createHotDrinksPillValue(strings), createCozyBlanketsPillValue(strings), createTwoLakesInOnePillValue(strings), createMoneyPillValue('moraine-lake-sunrise-lake-louise-golden-hour', strings)],
52
+ tags: [{
53
+ text: strings.products.productTags.mostPopular,
54
+ style: ProductTagStyle.MOST_POPULAR
55
+ }]
56
+ },
57
+ MORAINE_LAKE_SUNRISE: {
58
+ id: 'moraine-lake-sunrise',
59
+ videoUrl: { src: '/videos/moraine-lake-sunrise.mp4', webm: '/videos/moraine-lake-sunrise.webm', longSrc: '/videos/moraine-lake-sunrise-long.mp4', longWebm: '/videos/moraine-lake-sunrise-long.webm' },
60
+ name: strings.products.productNames.moraineLakeSunrise,
61
+ shortName: strings.products.productShortNames.moraineLakeSunriseShort,
62
+ images: [
63
+ IMAGES.MORAINE_LAKE_SUNRISE_CHEERS,
64
+ ],
65
+ bookingLink: '#book-now/moraine-lake-sunrise',
66
+ currentStartTime: strings.products.startTimes.moraineLakeSunrise,
67
+ description: strings.productThemePages.sunriseTours.description,
68
+ path: '/moraine-lake-sunrise-shuttle',
69
+ avgPrice: 129,
70
+ pillValues: [createSunrisePillValue(strings), createDurationPillValue('moraine-lake-sunrise', strings), createAddTimePillValue('moraine-lake-sunrise', strings), createHotDrinksPillValue(strings), createCozyBlanketsPillValue(strings), createMoneyPillValue('moraine-lake-sunrise', strings)]
71
+ },
72
+ TWO_LAKES_COMBO: {
73
+ id: 'two-lakes-combo',
74
+ videoUrl: { src: '/videos/two-lakes-combo.mp4', webm: '/videos/two-lakes-combo.webm', longSrc: '/videos/two-lakes-combo-long.mp4', longWebm: '/videos/two-lakes-combo-long.webm' },
75
+ name: strings.products.productNames.twoLakesCombo,
76
+ shortName: strings.products.productShortNames.twoLakesComboShort,
77
+ images: [
78
+ IMAGES.MORAINE_LAKE_YELLOW_BIKINI,
79
+ ],
80
+ bookingLink: '#book-now/two-lakes-combo',
81
+ currentStartTime: strings.products.startTimes.moraineLakeAdventure,
82
+ description: strings.products.productDescriptions.twoLakesCombo,
83
+ path: '/moraine-lake-shuttle',
84
+ avgPrice: 149,
85
+ pillValues: [createDeparturePillValue('two-lakes-combo', strings), createDurationPillValue('two-lakes-combo-1', strings), createDurationPillValue('two-lakes-combo-2', strings), createAddTimePillValue('two-lakes-combo', strings), createTwoLakesInOnePillValue(strings), createMoneyPillValue('two-lakes-combo', strings)],
86
+ tags: [{
87
+ text: strings.products.productTags.mostPopular,
88
+ style: ProductTagStyle.MOST_POPULAR
89
+ }]
90
+ },
91
+ MORAINE_LAKE_ADVENTURE: {
92
+ id: 'moraine-lake-adventure',
93
+ videoUrl: { src: '/videos/moraine-lake-adventure.mp4', webm: '/videos/moraine-lake-adventure.webm', longSrc: '/videos/moraine-lake-adventure-long.mp4', longWebm: '/videos/moraine-lake-adventure-long.webm' },
94
+ name: strings.products.productNames.moraineLakeAdventure,
95
+ shortName: strings.products.productShortNames.moraineLakeAdventureShort,
96
+ images: [
97
+ IMAGES.MORAINE_LAKE_CANOE,
98
+ ],
99
+ bookingLink: '#book-now/moraine-lake-adventure',
100
+ currentStartTime: strings.products.startTimes.moraineLakeAdventure,
101
+ description: strings.productThemePages.moraineLakeShuttle.description,
102
+ path: '/moraine-lake-shuttle',
103
+ avgPrice: 95,
104
+ pillValues: [createDeparturePillValue('moraine-lake-adventure', strings), createDurationPillValue('moraine-lake-adventure', strings), createAddTimePillValue('moraine-lake-adventure', strings), createHikePillValue(strings), createCanoePillValue(strings), createMoneyPillValue('moraine-lake-adventure', strings)]
105
+ },
106
+ LAKE_LOUISE_ADVENTURE: {
107
+ id: 'lake-louise-adventure',
108
+ videoUrl: { src: '/videos/lake-louise-adventure.mp4', webm: '/videos/lake-louise-adventure.webm', longSrc: '/videos/lake-louise-adventure-long.mp4', longWebm: '/videos/lake-louise-adventure-long.webm' },
109
+ name: strings.products.productNames.lakeLouiseAdventure,
110
+ shortName: strings.products.productShortNames.lakeLouiseAdventureShort,
111
+ images: [
112
+ IMAGES.LAKE_LOUISE_FLORA,
113
+ ],
114
+ bookingLink: '#book-now/lake-louise-adventure',
115
+ currentStartTime: strings.products.startTimes.lakeLouiseAdventure,
116
+ description: strings.productThemePages.lakeLouiseShuttle.description,
117
+ path: '/lake-louise-shuttle',
118
+ avgPrice: 69,
119
+ pillValues: [createDeparturePillValue('lake-louise-adventure', strings), createDurationPillValue('lake-louise-adventure', strings), createAddTimePillValue('lake-louise-adventure', strings), createHikePillValue(strings), createCanoePillValue(strings), createMoneyPillValue('lake-louise-adventure', strings)]
120
+ },
121
+ EMERALD_LAKE_ESCAPE: {
122
+ id: 'emerald-lake-escape',
123
+ videoUrl: { src: '/videos/emerald-lake-escape-tour.mp4', webm: '/videos/emerald-lake-escape-tour.webm', longSrc: '/videos/emerald-lake-escape-tour-long.mp4', longWebm: '/videos/emerald-lake-escape-tour-long.webm' },
124
+ name: strings.products.productNames.emeraldLakeEscapeTour,
125
+ shortName: strings.products.productShortNames.emeraldLakeEscapeTourShort,
126
+ images: [
127
+ IMAGES.EMERALD_LAKE_LODGE_VIEWPOINT,
128
+ ],
129
+ bookingLink: '#book-now/emerald-lake-escape',
130
+ currentStartTime: strings.products.startTimes.emeraldLakeEscape,
131
+ description: strings.productThemePages.emeraldLakeShuttle.description,
132
+ path: '/emerald-lake-shuttle',
133
+ avgPrice: 175,
134
+ pillValues: [createDeparturePillValue('emerald-lake-escape', strings), createDurationPillValue('emerald-lake-escape', strings), ...createemeraldLakeEscapeTourLocationsPillValues(strings), createLunchPillValue(strings), createMoneyPillValue('emerald-lake-escape', strings)]
135
+ },
136
+ PRIVATE_TOUR: {
137
+ id: 'private-tour',
138
+ videoUrl: { src: '/videos/private-tour.mp4', webm: '/videos/private-tour.webm', longSrc: '/videos/private-tour-long.mp4', longWebm: '/videos/private-tour-long.webm' },
139
+ name: strings.products.productNames.privateTour,
140
+ shortName: strings.products.productShortNames.privateTourShort,
141
+ images: [
142
+ IMAGES.PRIVATE_TOUR_FAMILY_MORAINE_LAKE,
143
+ ],
144
+ bookingLink: '#book-now/private-tour',
145
+ currentStartTime: strings.products.startTimes.privateTour,
146
+ description: strings.productThemePages.privateTours.description,
147
+ path: '/private-shuttle',
148
+ avgPrice: 1699,
149
+ pillValues: [createDeparturePillValue('private-tour', strings), createDurationPillValue('private-tour', strings), createCroissantPillValue(strings), createHotDrinksPillValue(strings), createMoneyPillValue('private-tour', strings)]
150
+ }
151
+ });
152
+
153
+ // Default export for backward compatibility (uses English strings)
154
+ import defaultStrings from '@/strings';
155
+ export const PRODUCTS = getProducts(defaultStrings);
@@ -0,0 +1,125 @@
1
+ 'use client';
2
+
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useRef,
7
+ useCallback,
8
+ type ReactNode,
9
+ } from 'react';
10
+ import type { Availability, PricingConfig, PrecomputedPricesByCategory } from '@/lib/booking-api';
11
+
12
+ /** Cache TTL in milliseconds (5 minutes). Entries older than this are considered stale. */
13
+ export const AVAILABILITIES_CACHE_TTL_MS = 5 * 60 * 1000;
14
+
15
+ export interface CachedAvailabilitiesData {
16
+ fetchedRanges: Array<{ start: Date; end: Date }>;
17
+ availabilities: Availability[];
18
+ pricingConfig: PricingConfig | null;
19
+ precomputedPricesByOption: Record<string, PrecomputedPricesByCategory> | null;
20
+ /** Private Shuttle: shared precomputed prices (category -> currency -> price). */
21
+ precomputedPrices?: PrecomputedPricesByCategory | null;
22
+ /** Private Shuttle: resource price by currency. */
23
+ resourcePriceByCurrency?: Record<string, number> | null;
24
+ /** Private Shuttle: resource price by option (optionId -> currency -> price). */
25
+ resourcePriceByOption?: Record<string, Record<string, number>> | null;
26
+ /** Timestamp when this entry was cached (for TTL / stale-while-revalidate). */
27
+ cachedAt: number;
28
+ }
29
+
30
+ interface AvailabilitiesCacheContextValue {
31
+ /** Get cached data for a product+options+promo. Returns undefined if not cached. */
32
+ get: (cacheKey: string) => CachedAvailabilitiesData | undefined;
33
+ /** Check if cached entry is stale (older than TTL). Use for stale-while-revalidate. */
34
+ isStale: (cached: CachedAvailabilitiesData) => boolean;
35
+ /** Replace or set cache entry. */
36
+ set: (cacheKey: string, data: CachedAvailabilitiesData) => void;
37
+ /** Merge new availabilities into existing cache (or create new entry). */
38
+ merge: (
39
+ cacheKey: string,
40
+ update: {
41
+ fetchedRanges?: Array<{ start: Date; end: Date }>;
42
+ availabilities?: Availability[];
43
+ pricingConfig?: PricingConfig | null;
44
+ precomputedPricesByOption?: Record<string, PrecomputedPricesByCategory> | null;
45
+ precomputedPrices?: PrecomputedPricesByCategory | null;
46
+ resourcePriceByCurrency?: Record<string, number> | null;
47
+ resourcePriceByOption?: Record<string, Record<string, number>> | null;
48
+ }
49
+ ) => void;
50
+ }
51
+
52
+ const AvailabilitiesCacheContext = createContext<AvailabilitiesCacheContextValue | null>(null);
53
+
54
+ export function useAvailabilitiesCache() {
55
+ const ctx = useContext(AvailabilitiesCacheContext);
56
+ return ctx; // May be null if not wrapped
57
+ }
58
+
59
+ /** Build cache key from product + options + promo + optional partner pricing profile. */
60
+ export function buildAvailabilitiesCacheKey(
61
+ productId: string,
62
+ optionIdsKey: string,
63
+ promoCode: string | null,
64
+ pricingProfileId?: string | null
65
+ ): string {
66
+ const pp = (pricingProfileId ?? '').trim();
67
+ return `${productId}|${optionIdsKey}|${promoCode ?? ''}|pp:${pp}`;
68
+ }
69
+
70
+ export function AvailabilitiesCacheProvider({ children }: { children: ReactNode }) {
71
+ const cacheRef = useRef<Map<string, CachedAvailabilitiesData>>(new Map());
72
+
73
+ const get = useCallback((cacheKey: string) => {
74
+ return cacheRef.current.get(cacheKey);
75
+ }, []);
76
+
77
+ const isStale = useCallback((cached: CachedAvailabilitiesData) => {
78
+ const age = Date.now() - (cached.cachedAt ?? 0);
79
+ return age > AVAILABILITIES_CACHE_TTL_MS;
80
+ }, []);
81
+
82
+ const set = useCallback((cacheKey: string, data: CachedAvailabilitiesData) => {
83
+ cacheRef.current.set(cacheKey, data);
84
+ }, []);
85
+
86
+ const merge = useCallback(
87
+ (
88
+ cacheKey: string,
89
+ update: {
90
+ fetchedRanges?: Array<{ start: Date; end: Date }>;
91
+ availabilities?: Availability[];
92
+ pricingConfig?: PricingConfig | null;
93
+ precomputedPricesByOption?: Record<string, PrecomputedPricesByCategory> | null;
94
+ precomputedPrices?: PrecomputedPricesByCategory | null;
95
+ resourcePriceByCurrency?: Record<string, number> | null;
96
+ resourcePriceByOption?: Record<string, Record<string, number>> | null;
97
+ }
98
+ ) => {
99
+ const existing = cacheRef.current.get(cacheKey);
100
+ const next: CachedAvailabilitiesData = {
101
+ fetchedRanges: update.fetchedRanges ?? existing?.fetchedRanges ?? [],
102
+ availabilities: update.availabilities ?? existing?.availabilities ?? [],
103
+ pricingConfig: update.pricingConfig !== undefined ? update.pricingConfig : existing?.pricingConfig ?? null,
104
+ precomputedPricesByOption:
105
+ update.precomputedPricesByOption !== undefined
106
+ ? update.precomputedPricesByOption
107
+ : existing?.precomputedPricesByOption ?? null,
108
+ precomputedPrices: update.precomputedPrices !== undefined ? update.precomputedPrices : existing?.precomputedPrices ?? null,
109
+ resourcePriceByCurrency: update.resourcePriceByCurrency !== undefined ? update.resourcePriceByCurrency : existing?.resourcePriceByCurrency ?? null,
110
+ resourcePriceByOption: update.resourcePriceByOption !== undefined ? update.resourcePriceByOption : existing?.resourcePriceByOption ?? null,
111
+ cachedAt: Date.now(),
112
+ };
113
+ cacheRef.current.set(cacheKey, next);
114
+ },
115
+ []
116
+ );
117
+
118
+ const value: AvailabilitiesCacheContextValue = { get, set, merge, isStale };
119
+
120
+ return (
121
+ <AvailabilitiesCacheContext.Provider value={value}>
122
+ {children}
123
+ </AvailabilitiesCacheContext.Provider>
124
+ );
125
+ }
@@ -0,0 +1,134 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext, ReactNode } from 'react';
4
+
5
+ /** Host app / embedding context (standalone site, provider-dashboard, etc.). */
6
+ export type BookingAppMode = 'standalone' | 'provider-dashboard' | (string & Record<never, never>);
7
+
8
+ /**
9
+ * Viewer role for the booking app. Drives pricing display and (in future) other feature levels.
10
+ * - public: customer-facing (e.g. standalone site); simplified pricing (dynamic increases rolled into base).
11
+ * - reseller: same as public for pricing; reserved for future reseller-specific behaviour.
12
+ * - admin: full detail (e.g. provider dashboard); full price breakdown, no roll-up.
13
+ */
14
+ export type ViewerRole = 'public' | 'reseller' | 'admin';
15
+
16
+ /** Feature flags and permissions for the booking app in this context. */
17
+ export interface BookingAppPermissions {
18
+ /** When true, the price breakdown hover tooltip is shown on itinerary line items. */
19
+ canViewPriceBreakdown?: boolean;
20
+ /**
21
+ * Viewer role. For pricing: public and reseller use simplified view; admin uses full detail.
22
+ * Use this for future feature gating (e.g. reseller vs public vs admin).
23
+ */
24
+ viewerRole?: ViewerRole;
25
+ /**
26
+ * @deprecated Prefer viewerRole. When viewerRole is set it takes precedence.
27
+ * How much detail to show in the price breakdown tooltip.
28
+ */
29
+ priceBreakdownDetail?: 'full' | 'simplified';
30
+ }
31
+
32
+ /** Params for showing the manage-booking UI (e.g. in a dialog when embedded in provider-dashboard). */
33
+ export interface ManageParams {
34
+ ref?: string;
35
+ reservationRef?: string;
36
+ lastName: string;
37
+ }
38
+
39
+ export interface BookingAppContextValue {
40
+ mode: BookingAppMode;
41
+ permissions: BookingAppPermissions;
42
+ /** True when pricing should show simplified view (dynamic increases rolled into base). Derived from viewerRole (public/reseller) or priceBreakdownDetail. */
43
+ isSimplifiedPricingView: boolean;
44
+ /** When set (e.g. by host), used for Google Maps instead of NEXT_PUBLIC_GOOGLE_MAPS_API_KEY. Fixes prod when embedded code doesn't get build-time env. */
45
+ googleMapsApiKey?: string;
46
+ /** When set (e.g. provider-dashboard), called instead of redirecting to /manage after free booking or when payment success should show manage UI in-host. */
47
+ onShowManage?: (params: ManageParams) => void;
48
+ /** When set, used as Stripe return_url so payment success lands on the host (e.g. dashboard) instead of /manage. */
49
+ getSuccessUrl?: (params: { reservationRef: string; lastName: string; focusDate?: string }) => string;
50
+ /** When false, the language selector is hidden. Host-controlled; default true. */
51
+ showLanguageSelector: boolean;
52
+ /** Partner portal / embedded host: skip auto-scroll after choosing a calendar date. */
53
+ suppressCalendarDateScroll?: boolean;
54
+ }
55
+
56
+ const BookingAppContext = createContext<BookingAppContextValue | undefined>(undefined);
57
+
58
+ function deriveIsSimplifiedPricingView(permissions: BookingAppPermissions): boolean {
59
+ const role = permissions.viewerRole;
60
+ if (role === 'public' || role === 'reseller') return true;
61
+ if (role === 'admin') return false;
62
+ return permissions.priceBreakdownDetail === 'simplified';
63
+ }
64
+
65
+ const DEFAULT_STANDALONE: BookingAppContextValue = {
66
+ mode: 'standalone',
67
+ permissions: { canViewPriceBreakdown: false },
68
+ isSimplifiedPricingView: false,
69
+ showLanguageSelector: true,
70
+ suppressCalendarDateScroll: false,
71
+ };
72
+
73
+ export interface BookingAppProviderProps {
74
+ children: ReactNode;
75
+ /** Which app or page is hosting the booking UI. */
76
+ mode?: BookingAppMode;
77
+ /** Permissions for this context (e.g. show price breakdown only for staff). */
78
+ permissions?: Partial<BookingAppPermissions>;
79
+ /** Google Maps API key from the host (use when embedding so the key from the host build is used). */
80
+ googleMapsApiKey?: string;
81
+ /** When set (e.g. provider-dashboard), called instead of redirecting to /manage; host can show manage UI in a dialog. */
82
+ onShowManage?: (params: ManageParams) => void;
83
+ /** When set, used as Stripe return_url so payment success lands on the host. */
84
+ getSuccessUrl?: (params: { reservationRef: string; lastName: string; focusDate?: string }) => string;
85
+ /** When false, the language selector is hidden. Default true. */
86
+ showLanguageSelector?: boolean;
87
+ /** When true, calendar date selection does not auto-scroll the page/dialog. */
88
+ suppressCalendarDateScroll?: boolean;
89
+ }
90
+
91
+ /**
92
+ * Provides the current "mode" (which app is using the booking app) and permissions.
93
+ * Wrap the booking app at the host level: standalone page or BookingWidget when embedded.
94
+ */
95
+ export function BookingAppProvider({
96
+ children,
97
+ mode = 'standalone',
98
+ permissions = {},
99
+ googleMapsApiKey,
100
+ onShowManage,
101
+ getSuccessUrl,
102
+ showLanguageSelector = true,
103
+ suppressCalendarDateScroll = false,
104
+ }: BookingAppProviderProps) {
105
+ const mergedPermissions = { ...DEFAULT_STANDALONE.permissions, ...permissions };
106
+ const value: BookingAppContextValue = {
107
+ mode,
108
+ permissions: mergedPermissions,
109
+ isSimplifiedPricingView: deriveIsSimplifiedPricingView(mergedPermissions),
110
+ googleMapsApiKey,
111
+ onShowManage,
112
+ getSuccessUrl,
113
+ showLanguageSelector,
114
+ suppressCalendarDateScroll,
115
+ };
116
+
117
+ return (
118
+ <BookingAppContext.Provider value={value}>
119
+ {children}
120
+ </BookingAppContext.Provider>
121
+ );
122
+ }
123
+
124
+ /**
125
+ * Hook to access booking app context (mode and permissions).
126
+ * Use this to gate features like the price breakdown tooltip based on who is using the app.
127
+ */
128
+ export function useBookingApp(): BookingAppContextValue {
129
+ const context = useContext(BookingAppContext);
130
+ if (context === undefined) {
131
+ return DEFAULT_STANDALONE;
132
+ }
133
+ return context;
134
+ }