@ticketboothapp/booking 0.1.22 → 1.2.24

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 +2 -29
  2. package/src/index.ts +0 -79
  3. package/tsconfig.json +2 -8
  4. package/src/assets/icons/minus.svg +0 -7
  5. package/src/assets/icons/partner-logos/getyourguide.svg +0 -8
  6. package/src/assets/icons/plus.svg +0 -3
  7. package/src/colours.css +0 -23
  8. package/src/components/BookingDetails.module.css +0 -1591
  9. package/src/components/BookingDetails.tsx +0 -2264
  10. package/src/components/BookingWidget.tsx +0 -302
  11. package/src/components/ManageBookingView.tsx +0 -437
  12. package/src/components/PhoneInputWithCountry.module.css +0 -131
  13. package/src/components/PhoneInputWithCountry.tsx +0 -44
  14. package/src/components/PickupLocationDialog.module.css +0 -360
  15. package/src/components/PickupLocationDialog.tsx +0 -357
  16. package/src/components/PostBookingDependentAddOnUpsell.module.css +0 -174
  17. package/src/components/PostBookingDependentAddOnUpsell.tsx +0 -407
  18. package/src/components/booking/AddOnsSection.module.css +0 -10
  19. package/src/components/booking/AddOnsSection.tsx +0 -184
  20. package/src/components/booking/AdminPaymentChoiceModal.tsx +0 -98
  21. package/src/components/booking/BookingDialog.module.css +0 -643
  22. package/src/components/booking/BookingDialog.tsx +0 -356
  23. package/src/components/booking/BookingFlow.tsx +0 -4385
  24. package/src/components/booking/BookingFlowCollage.module.css +0 -148
  25. package/src/components/booking/BookingFlowCollage.tsx +0 -184
  26. package/src/components/booking/BookingFlowPlaceholder.module.css +0 -27
  27. package/src/components/booking/BookingFlowPlaceholder.tsx +0 -25
  28. package/src/components/booking/BookingFlowPreview.tsx +0 -51
  29. package/src/components/booking/BookingProductGrid.module.css +0 -359
  30. package/src/components/booking/BookingProductGrid.tsx +0 -497
  31. package/src/components/booking/Calendar.module.css +0 -616
  32. package/src/components/booking/Calendar.tsx +0 -1123
  33. package/src/components/booking/CancellationPolicySelector.module.css +0 -124
  34. package/src/components/booking/CancellationPolicySelector.tsx +0 -142
  35. package/src/components/booking/ChangeBookingDialog.tsx +0 -562
  36. package/src/components/booking/CheckoutForm.module.css +0 -244
  37. package/src/components/booking/CheckoutForm.tsx +0 -364
  38. package/src/components/booking/CheckoutModal.tsx +0 -451
  39. package/src/components/booking/CurrencySwitcher.tsx +0 -81
  40. package/src/components/booking/DapFlowCollage.tsx +0 -88
  41. package/src/components/booking/DapTourDescription.tsx +0 -35
  42. package/src/components/booking/DependentAddOnBookingDialog.tsx +0 -1350
  43. package/src/components/booking/DependentAddOnPaymentForm.tsx +0 -124
  44. package/src/components/booking/ErrorBoundary.tsx +0 -63
  45. package/src/components/booking/InfoTooltip.tsx +0 -108
  46. package/src/components/booking/ItineraryBox.module.css +0 -258
  47. package/src/components/booking/ItineraryBox.tsx +0 -550
  48. package/src/components/booking/ItineraryBuilder.tsx +0 -82
  49. package/src/components/booking/ItineraryPlaceholder.module.css +0 -45
  50. package/src/components/booking/ItineraryPlaceholder.tsx +0 -26
  51. package/src/components/booking/MealDrinkAddOnSelector.tsx +0 -338
  52. package/src/components/booking/PickupLocationSelector.module.css +0 -124
  53. package/src/components/booking/PickupLocationSelector.tsx +0 -1566
  54. package/src/components/booking/PickupTimeSelector.module.css +0 -134
  55. package/src/components/booking/PickupTimeSelector.tsx +0 -112
  56. package/src/components/booking/PriceBreakdown.tsx +0 -154
  57. package/src/components/booking/PriceSummary.tsx +0 -234
  58. package/src/components/booking/PrivateShuttleBookingFlow.module.css +0 -357
  59. package/src/components/booking/PrivateShuttleBookingFlow.tsx +0 -2662
  60. package/src/components/booking/PromoCodeInput.module.css +0 -166
  61. package/src/components/booking/PromoCodeInput.tsx +0 -99
  62. package/src/components/booking/ReturnTimeSelector.module.css +0 -173
  63. package/src/components/booking/ReturnTimeSelector.tsx +0 -145
  64. package/src/components/booking/TermsAcceptance.tsx +0 -111
  65. package/src/components/booking/TicketSelector.module.css +0 -164
  66. package/src/components/booking/TicketSelector.tsx +0 -199
  67. package/src/components/booking/TourDescription.module.css +0 -304
  68. package/src/components/booking/TourDescription.tsx +0 -273
  69. package/src/components/booking/booking-flow-ui.ts +0 -38
  70. package/src/components/booking/booking-flow.css +0 -944
  71. package/src/components/button.css +0 -245
  72. package/src/components/button.tsx +0 -152
  73. package/src/components/colorable-svg.tsx +0 -29
  74. package/src/components/image.css +0 -29
  75. package/src/components/image.tsx +0 -113
  76. package/src/components/partner/PartnerBookingPage.module.css +0 -130
  77. package/src/components/partner/PartnerBookingPage.tsx +0 -390
  78. package/src/components/partner/PartnerBookingPageWithBrowserMetadata.tsx +0 -45
  79. package/src/components/product-tag.module.css +0 -30
  80. package/src/components/product-tag.tsx +0 -34
  81. package/src/components/product-theme-pages/image-modal.tsx +0 -248
  82. package/src/components/product-theme-pages/photo-gallery.module.css +0 -200
  83. package/src/components/terms/TermsContent.tsx +0 -178
  84. package/src/components/value-pill.module.css +0 -59
  85. package/src/components/value-pill.tsx +0 -46
  86. package/src/constants/images.ts +0 -556
  87. package/src/constants/pill-values.ts +0 -210
  88. package/src/constants/products.ts +0 -155
  89. package/src/contexts/AvailabilitiesCacheContext.tsx +0 -125
  90. package/src/contexts/BookingAppContext.tsx +0 -134
  91. package/src/contexts/CompanyContext.tsx +0 -70
  92. package/src/data/dap-descriptions/session-couples-families-friends.en.json +0 -61
  93. package/src/data/dap-descriptions/session-elopements.en.json +0 -60
  94. package/src/data/dap-descriptions/session-proposals.en.json +0 -60
  95. package/src/data/product-descriptions/afternoon-delight.en.json +0 -35
  96. package/src/data/product-descriptions/emerald-lake-escape.en.json +0 -68
  97. package/src/data/product-descriptions/lake-louise-adventure.en.json +0 -74
  98. package/src/data/product-descriptions/moraine-lake-adventure.en.json +0 -78
  99. package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +0 -65
  100. package/src/data/product-descriptions/moraine-lake-sunrise.en.json +0 -64
  101. package/src/data/product-descriptions/private-tour.en.json +0 -80
  102. package/src/data/product-descriptions/two-lakes-combo.en.json +0 -65
  103. package/src/data/products-config.json +0 -101
  104. package/src/hooks/useBookingSourceMetadataFromLocation.ts +0 -21
  105. package/src/hooks/useIsBookingLaunchLive.ts +0 -49
  106. package/src/lib/analytics.ts +0 -197
  107. package/src/lib/booking/booking-source.ts +0 -51
  108. package/src/lib/booking/checkout-breakdown.ts +0 -69
  109. package/src/lib/booking/correlation-id.ts +0 -46
  110. package/src/lib/booking/i18n/config.ts +0 -21
  111. package/src/lib/booking/i18n/index.tsx +0 -144
  112. package/src/lib/booking/i18n/messages/en.json +0 -236
  113. package/src/lib/booking/i18n/messages/fr.json +0 -236
  114. package/src/lib/booking/itinerary-display.ts +0 -36
  115. package/src/lib/booking/itinerary-labels.ts +0 -70
  116. package/src/lib/booking/location-calculations.ts +0 -43
  117. package/src/lib/booking/location-utils.ts +0 -165
  118. package/src/lib/booking/map-utils.ts +0 -153
  119. package/src/lib/booking/marker-icons.ts +0 -113
  120. package/src/lib/booking/normalize-booking-product-id.ts +0 -21
  121. package/src/lib/booking/pickup-location-types.ts +0 -25
  122. package/src/lib/booking/places-api.ts +0 -154
  123. package/src/lib/booking/pricing.ts +0 -466
  124. package/src/lib/booking/product-option-id.ts +0 -35
  125. package/src/lib/booking/source-metadata.ts +0 -226
  126. package/src/lib/booking/sunday-week.ts +0 -14
  127. package/src/lib/booking/theme.ts +0 -83
  128. package/src/lib/booking/trace-context.ts +0 -62
  129. package/src/lib/booking/utils.ts +0 -9
  130. package/src/lib/booking-api.ts +0 -1793
  131. package/src/lib/booking-constants.ts +0 -23
  132. package/src/lib/booking-ref.ts +0 -13
  133. package/src/lib/booking-types.ts +0 -36
  134. package/src/lib/currency.ts +0 -81
  135. package/src/lib/dap-descriptions.ts +0 -50
  136. package/src/lib/dap-itinerary-preview.ts +0 -315
  137. package/src/lib/dependent-add-on-api.ts +0 -434
  138. package/src/lib/env.ts +0 -96
  139. package/src/lib/firebase.ts +0 -20
  140. package/src/lib/job-application-api.ts +0 -83
  141. package/src/lib/manage-booking-embed-print.ts +0 -16
  142. package/src/lib/manage-booking-post-checkout.ts +0 -68
  143. package/src/lib/photo-dap-config.ts +0 -228
  144. package/src/lib/photo-packages.ts +0 -75
  145. package/src/lib/pickup/map-utils.ts +0 -56
  146. package/src/lib/pickup/marker-icons.ts +0 -19
  147. package/src/lib/product-descriptions.ts +0 -66
  148. package/src/lib/products-config.ts +0 -73
  149. package/src/providers/booking-dialog-provider.tsx +0 -282
  150. package/src/providers/dependent-add-on-dialog-provider.tsx +0 -105
  151. package/src/radius.css +0 -5
  152. package/src/spacing.css +0 -7
  153. package/src/strings/en.json +0 -1774
  154. package/src/strings/es.json +0 -1573
  155. package/src/strings/fr.json +0 -1573
  156. package/src/strings/index.js +0 -23
  157. package/src/text-style.css +0 -97
  158. package/src/utils/currency-converter.ts +0 -101
@@ -1,360 +0,0 @@
1
- /* Root class for scoping - nested selectors override Tailwind */
2
- .overlay {
3
- position: fixed;
4
- inset: 0;
5
- background: rgba(0, 0, 0, 0.5);
6
- display: flex;
7
- align-items: center;
8
- justify-content: center;
9
- z-index: 1000;
10
- padding: 1rem;
11
- }
12
-
13
- .modal {
14
- background: var(--primary-background, #fff);
15
- border-radius: 24px;
16
- max-width: 900px;
17
- width: 100%;
18
- max-height: 90vh;
19
- overflow: hidden;
20
- display: flex;
21
- flex-direction: column;
22
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
23
- }
24
-
25
- .pickupDialogRoot .header {
26
- display: grid !important;
27
- grid-template-columns: auto 1fr auto !important;
28
- align-items: center !important;
29
- gap: 0.75rem !important;
30
- padding: 1rem 0.75rem !important;
31
- border-bottom: 1px solid #e5e7eb !important;
32
- }
33
-
34
- .pickupDialogRoot .headerSpacer {
35
- display: block;
36
- width: 2.5rem;
37
- height: 2.5rem;
38
- flex-shrink: 0;
39
- }
40
-
41
- .pickupDialogRoot .title {
42
- font-family: 'Poppins', sans-serif !important;
43
- font-weight: 800 !important;
44
- font-size: 1.5rem !important;
45
- color: var(--accent-orange, #e85a2e) !important;
46
- margin: 0 !important;
47
- justify-self: center !important;
48
- text-align: center !important;
49
- }
50
-
51
- .pickupDialogRoot .closeBtn {
52
- background: none !important;
53
- border: none !important;
54
- padding: 0.5rem !important;
55
- cursor: pointer !important;
56
- color: var(--grey-text, #6b7280) !important;
57
- }
58
-
59
- .pickupDialogRoot .closeBtn:hover {
60
- color: var(--primary-text, #1f2937) !important;
61
- }
62
-
63
- .pickupDialogRoot .content {
64
- padding: var(--spacing-large);
65
- overflow-y: auto;
66
- overflow-x: hidden;
67
- flex: 1;
68
- min-width: 0;
69
- }
70
-
71
- .pickupDialogRoot .loading,
72
- .pickupDialogRoot .error,
73
- .pickupDialogRoot .empty {
74
- text-align: center;
75
- padding: 2rem;
76
- color: var(--grey-text, #6b7280);
77
- }
78
-
79
- .pickupDialogRoot .error {
80
- color: #dc2626;
81
- }
82
-
83
- /* Current pickup section - shown when dialog opens */
84
- .pickupDialogRoot .currentPickupSection {
85
- margin-bottom: 1.25rem;
86
- padding: 1rem;
87
- background: var(--accent-orange-10, rgba(0, 0, 0, 0.04));
88
- border-radius: 12px;
89
- border: 1px solid var(--accent-orange-10, rgba(0, 0, 0, 0.08));
90
- }
91
-
92
- .pickupDialogRoot .currentPickupLabel {
93
- font-size: 0.75rem;
94
- font-weight: 600;
95
- text-transform: uppercase;
96
- letter-spacing: 0.05em;
97
- color: var(--grey-text, #6b7280);
98
- margin: 0 0 0.25rem 0;
99
- }
100
-
101
- .pickupDialogRoot .currentPickupName {
102
- font-size: 1rem;
103
- font-weight: 600;
104
- color: var(--primary-text, #1f2937);
105
- margin: 0 0 0.25rem 0;
106
- }
107
-
108
- .pickupDialogRoot .currentPickupTimes {
109
- font-size: 0.875rem;
110
- color: var(--grey-text, #6b7280);
111
- margin: 0;
112
- }
113
-
114
- .pickupDialogRoot .selectorWrapper {
115
- margin-bottom: 1.5rem;
116
- }
117
-
118
- /* Prevent input overflow - constrain width in flex/grid layouts */
119
- .pickupDialogRoot .selectorWrapperConstrained {
120
- min-width: 0;
121
- overflow: hidden;
122
- }
123
-
124
- .pickupDialogRoot .selectorWrapperConstrained > div {
125
- min-width: 0;
126
- }
127
-
128
- /* Override Tailwind resets for input and filter pills when inside dialog */
129
- .pickupDialogRoot .selectorWrapper input[data-pickup-search],
130
- .pickupDialogRoot .selectorWrapper input[type="text"] {
131
- width: 100% !important;
132
- max-width: 100% !important;
133
- box-sizing: border-box !important;
134
- padding: 0.5rem 1rem 0.5rem 2.5rem !important;
135
- font-family: 'Figtree', sans-serif !important;
136
- font-size: 1rem !important;
137
- line-height: 1.5 !important;
138
- color: #1c1917 !important;
139
- background-color: #fff !important;
140
- border: 1px solid #d6d3d1 !important;
141
- border-radius: 0.5rem !important;
142
- }
143
-
144
- .pickupDialogRoot .selectorWrapper input::placeholder {
145
- color: #a8a29e !important;
146
- }
147
-
148
- .pickupDialogRoot .selectorWrapper input:focus {
149
- outline: none !important;
150
- border-color: #78716c !important;
151
- }
152
-
153
- .pickupDialogRoot .cancelBtn {
154
- border-color: var(--grey-text);
155
- color: var(--grey-text);
156
- }
157
-
158
- /* Filter pills - match booking flow (PrivateShuttleBookingFlow / CheckoutForm) */
159
- .pickupDialogRoot .selectorWrapper [class*="filterPillsScroll"] {
160
- display: flex !important;
161
- flex-wrap: nowrap !important;
162
- align-items: center !important;
163
- gap: 0.5rem !important;
164
- margin-bottom: 1rem !important;
165
- }
166
-
167
- .pickupDialogRoot .selectorWrapper [class*="filterPillsScroll"] > div:first-child {
168
- display: flex !important;
169
- flex-wrap: wrap !important;
170
- gap: 0.5rem !important;
171
- }
172
-
173
- .pickupDialogRoot .selectorWrapper [class*="filterPillsScroll"] button {
174
- padding: 0.875rem 1.25rem !important;
175
- font-family: 'Figtree', sans-serif !important;
176
- font-size: 0.875rem !important;
177
- font-weight: 500 !important;
178
- border-radius: 9999px !important;
179
- transition: background-color 0.15s, color 0.15s !important;
180
- flex-shrink: 0 !important;
181
- border: none !important;
182
- cursor: pointer !important;
183
- }
184
-
185
- /* Unselected - stone/grey to match booking flow */
186
- .pickupDialogRoot .selectorWrapper [class*="filterPillsScroll"] button:not([class*="bg-emerald"]) {
187
- background-color: #f5f5f4 !important;
188
- color: #44403c !important;
189
- }
190
-
191
- .pickupDialogRoot .selectorWrapper [class*="filterPillsScroll"] button:hover:not([class*="bg-emerald"]) {
192
- background-color: #e7e5e4 !important;
193
- }
194
-
195
- /* Selected - emerald green to match booking flow */
196
- .pickupDialogRoot .selectorWrapper [class*="filterPillsScroll"] button[class*="bg-emerald"] {
197
- background-color: #059669 !important;
198
- color: #fff !important;
199
- }
200
-
201
- .pickupDialogRoot .selectorWrapper [class*="filterPillsScroll"] button[class*="bg-emerald"]:hover {
202
- background-color: #047857 !important;
203
- }
204
-
205
- .pickupDialogRoot .mapContainer {
206
- margin-bottom: var(--spacing-medium);
207
- border-radius: 12px;
208
- overflow: hidden;
209
- border: 1px solid var(--accent-orange-10, rgba(0, 0, 0, 0.08));
210
- }
211
-
212
- .map {
213
- width: 100%;
214
- height: 320px;
215
- }
216
-
217
- .mapFallback {
218
- font-size: 0.875rem;
219
- color: var(--grey-text);
220
- margin-bottom: var(--spacing-medium);
221
- }
222
-
223
- .infoWindow {
224
- padding: 0.25rem;
225
- min-width: 200px;
226
- }
227
-
228
- .infoTitle {
229
- font-weight: 600;
230
- margin: 0 0 0.25rem 0;
231
- font-size: 1rem;
232
- }
233
-
234
- .infoAddress {
235
- font-size: 0.875rem;
236
- color: var(--grey-text);
237
- margin: 0 0 0.5rem 0;
238
- }
239
-
240
- .selectBtn {
241
- width: 100%;
242
- padding: 0.5rem 0.75rem;
243
- background: var(--accent-orange, #e85a2e);
244
- color: white;
245
- border: none;
246
- border-radius: 8px;
247
- font-weight: 600;
248
- font-size: 0.875rem;
249
- cursor: pointer;
250
- }
251
-
252
- .selectBtn:hover:not(:disabled) {
253
- background: #e85a2e;
254
- opacity: 0.9;
255
- }
256
-
257
- .selectBtn:disabled {
258
- opacity: 0.7;
259
- cursor: not-allowed;
260
- }
261
-
262
- .list {
263
- display: flex;
264
- flex-direction: column;
265
- gap: 0.5rem;
266
- margin-bottom: var(--spacing-large);
267
- }
268
-
269
- .listItem {
270
- display: flex;
271
- flex-direction: column;
272
- align-items: flex-start;
273
- padding: 1rem;
274
- border: 2px solid var(--accent-orange-10, rgba(0, 0, 0, 0.08));
275
- border-radius: 12px;
276
- background: transparent;
277
- cursor: pointer;
278
- text-align: left;
279
- transition: border-color 0.15s, background 0.15s;
280
- }
281
-
282
- .listItem:hover {
283
- border-color: var(--accent-orange, #e85a2e);
284
- background: rgba(232, 90, 46, 0.05);
285
- }
286
-
287
- .listItemSelected {
288
- border-color: var(--accent-orange, #e85a2e);
289
- background: rgba(232, 90, 46, 0.08);
290
- }
291
-
292
- .listName {
293
- font-weight: 600;
294
- color: var(--primary-text, #1f2937);
295
- }
296
-
297
- .listAddress {
298
- font-size: 0.875rem;
299
- color: var(--grey-text);
300
- margin-top: 0.25rem;
301
- }
302
-
303
- .pickupDialogRoot .footer {
304
- display: flex;
305
- justify-content: flex-end;
306
- gap: 0.75rem;
307
- padding-top: var(--spacing-medium);
308
- border-top: 1px solid #e5e7eb !important;
309
- }
310
-
311
- /* Itinerary preview before save */
312
- .pickupDialogRoot .itineraryPreview {
313
- margin-top: 1rem;
314
- padding: 1rem;
315
- background: var(--accent-orange-10, rgba(0, 0, 0, 0.04));
316
- border-radius: 12px;
317
- border: 1px solid var(--accent-orange-10, rgba(0, 0, 0, 0.08));
318
- }
319
-
320
- .pickupDialogRoot .previewTitle {
321
- font-size: 0.875rem;
322
- font-weight: 600;
323
- margin: 0 0 0.5rem 0;
324
- color: var(--primary-text, #1f2937);
325
- }
326
-
327
- .pickupDialogRoot .previewIntro {
328
- font-size: 0.8125rem;
329
- color: var(--grey-text, #6b7280);
330
- margin: 0 0 0.75rem 0;
331
- }
332
-
333
- .pickupDialogRoot .previewList {
334
- margin: 0;
335
- padding-left: 1.25rem;
336
- font-size: 0.8125rem;
337
- }
338
-
339
- .pickupDialogRoot .previewItem {
340
- margin-bottom: 0.5rem;
341
- display: flex;
342
- flex-wrap: wrap;
343
- align-items: baseline;
344
- gap: 0.5rem;
345
- }
346
-
347
- .pickupDialogRoot .previewOld {
348
- color: var(--grey-text, #6b7280);
349
- text-decoration: line-through;
350
- }
351
-
352
- .pickupDialogRoot .previewArrow {
353
- color: var(--accent-orange, #e85a2e);
354
- font-weight: 600;
355
- }
356
-
357
- .pickupDialogRoot .previewNew {
358
- color: var(--primary-text, #1f2937);
359
- font-weight: 500;
360
- }
@@ -1,357 +0,0 @@
1
- 'use client';
2
-
3
- import React, { useState, useEffect, useCallback, useMemo } from 'react';
4
- import { useSearchParams } from 'next/navigation';
5
- import { formatBookingRefForDisplay } from '@/lib/booking-ref';
6
- import { fetchProducts, updatePickupLocation, previewPickupLocationChange, type PickupLocation, type Product } from '@/lib/booking-api';
7
- import type { BookingData, ItineraryDisplayStep } from '@/components/BookingDetails';
8
- import { PickupLocationSelector } from '@/components/booking/PickupLocationSelector';
9
- import Button, { ButtonHoverColor } from '@/components/button';
10
- import { useTranslations } from '@/lib/booking/i18n';
11
- import styles from './PickupLocationDialog.module.css';
12
-
13
- function findProductWithPickupLocations(
14
- products: Product[],
15
- bookingProductId: string
16
- ): Product | null {
17
- for (const p of products) {
18
- if (p.productId === bookingProductId) return p;
19
- if (p.pickupLocations?.some((loc) => loc.id === bookingProductId)) return p;
20
- const opt = (p as Product & { options?: { optionId: string }[] })?.options?.find(
21
- (o) => o.optionId === bookingProductId
22
- );
23
- if (opt) return p;
24
- }
25
- return null;
26
- }
27
-
28
- function getPickupTimeFromItinerary(itinerary: ItineraryDisplayStep[] | null | undefined): string {
29
- if (!itinerary?.length) return '—';
30
- const pickupStep = itinerary.find(s => s.stepType === 'pickup');
31
- return pickupStep?.time?.trim() || 'TBD';
32
- }
33
-
34
- function getDropoffTimeFromItinerary(itinerary: ItineraryDisplayStep[] | null | undefined): string {
35
- if (!itinerary?.length) return '—';
36
- const dropOffStep = [...itinerary].reverse().find(s => s.stepType === 'drop_off');
37
- return dropOffStep?.time?.trim() || 'TBD';
38
- }
39
-
40
- function getItineraryStepLabel(step: ItineraryDisplayStep, includeTime = false): string {
41
- if (step.label?.trim()) {
42
- const base = step.label.trim();
43
- return includeTime && step.time?.trim() ? `${base} ${step.time.trim()}` : base;
44
- }
45
- const place = step.place?.trim();
46
- const placeDisplay =
47
- place === 'your_pickup_location'
48
- ? 'your pickup location'
49
- : place === 'the_destination'
50
- ? 'the destination'
51
- : place ?? '';
52
- const timePart = includeTime && step.time?.trim() ? ` ${step.time.trim()}` : '';
53
- switch (step.stepType) {
54
- case 'pickup':
55
- return placeDisplay ? `Pickup at ${placeDisplay}${timePart}` : `Pickup${timePart}`;
56
- case 'drop_off':
57
- return placeDisplay ? `Drop off at ${placeDisplay}${timePart}` : `Drop-off${timePart}`;
58
- case 'arrive':
59
- return placeDisplay ? `Arrive at ${placeDisplay}${timePart}` : `Arrive${timePart}`;
60
- case 'depart':
61
- return placeDisplay ? `Depart ${placeDisplay}${timePart}` : `Depart${timePart}`;
62
- case 'trip_end':
63
- return 'Trip ends';
64
- case 'draft':
65
- return placeDisplay || 'Stop';
66
- default:
67
- return placeDisplay || (step.stepType ?? 'Step');
68
- }
69
- }
70
-
71
- interface PickupLocationDialogProps {
72
- isOpen: boolean;
73
- onClose: () => void;
74
- booking: BookingData;
75
- onSuccess: (updatedBooking: BookingData) => void;
76
- }
77
-
78
- export default function PickupLocationDialog({
79
- isOpen,
80
- onClose,
81
- booking,
82
- onSuccess,
83
- }: PickupLocationDialogProps) {
84
- const { t } = useTranslations();
85
- const [products, setProducts] = useState<Product[]>([]);
86
- const [loading, setLoading] = useState(false);
87
- const [error, setError] = useState<string | null>(null);
88
- const [selectedLocationId, setSelectedLocationId] = useState<string | null>(
89
- booking.pickupLocationId ?? null
90
- );
91
- const [selectedLocationName, setSelectedLocationName] = useState<string | null>(null);
92
- const [selectedCustomAddress, setSelectedCustomAddress] = useState<string | null>(
93
- booking.travelerHotel ?? null
94
- );
95
- const [saving, setSaving] = useState(false);
96
- const [previewItinerary, setPreviewItinerary] = useState<ItineraryDisplayStep[] | null>(null);
97
-
98
- const searchParams = useSearchParams();
99
- const refFromUrl = searchParams.get('ref') || searchParams.get('bookingReference') || '';
100
- const refFromBooking = formatBookingRefForDisplay(booking.bookingReference) || booking.bookingReference || '';
101
- const effectiveBookingRef = (refFromUrl.trim() || refFromBooking).trim();
102
-
103
- const companyId = booking.companyId ?? '';
104
- const lastName = booking.customer?.lastName ?? '';
105
- const isPrivateShuttle = booking.productType === 'PRIVATE_SHUTTLE';
106
-
107
- const product = useMemo(
108
- () => findProductWithPickupLocations(products, booking.productId),
109
- [products, booking.productId]
110
- );
111
- const pickupLocations = product?.pickupLocations ?? [];
112
- const destinations = product?.destinations ?? [];
113
-
114
- const loadProducts = useCallback(async () => {
115
- if (!companyId?.trim()) {
116
- setError('Missing company information');
117
- return;
118
- }
119
- setLoading(true);
120
- setError(null);
121
- try {
122
- const data = await fetchProducts(companyId);
123
- setProducts(data);
124
- } catch (e) {
125
- setError(e instanceof Error ? e.message : 'Failed to load pickup locations');
126
- } finally {
127
- setLoading(false);
128
- }
129
- }, [companyId]);
130
-
131
- useEffect(() => {
132
- if (isOpen && companyId) loadProducts();
133
- }, [isOpen, companyId, loadProducts]);
134
-
135
- useEffect(() => {
136
- if (isOpen) {
137
- setSelectedLocationId(booking.pickupLocationId ?? null);
138
- setSelectedLocationName(null);
139
- setSelectedCustomAddress(booking.travelerHotel ?? null);
140
- setPreviewItinerary(null);
141
- }
142
- }, [isOpen, booking.pickupLocationId, booking.travelerHotel]);
143
-
144
- const selectedPickupName = useMemo(() => {
145
- if (selectedCustomAddress) return selectedCustomAddress;
146
- if (selectedLocationId) {
147
- return selectedLocationName ?? pickupLocations.find((l) => l.id === selectedLocationId)?.name ?? selectedLocationId;
148
- }
149
- return null;
150
- }, [selectedLocationId, selectedLocationName, selectedCustomAddress, pickupLocations]);
151
-
152
- const hasValidSelection = selectedLocationId || selectedCustomAddress;
153
- const hasChanged =
154
- selectedLocationId !== (booking.pickupLocationId ?? null) ||
155
- selectedCustomAddress !== (booking.travelerHotel ?? null);
156
-
157
- // Fetch preview itinerary when user selects a new pickup (to show new times)
158
- useEffect(() => {
159
- if (!isOpen || !hasValidSelection || !hasChanged || !lastName?.trim()) {
160
- setPreviewItinerary(null);
161
- return;
162
- }
163
- let cancelled = false;
164
- const payload = selectedCustomAddress
165
- ? { travelerHotel: selectedCustomAddress }
166
- : selectedLocationId
167
- ? { pickupLocationId: selectedLocationId }
168
- : null;
169
- if (!payload) return;
170
- previewPickupLocationChange(effectiveBookingRef, lastName, payload)
171
- .then((data) => {
172
- if (!cancelled && data?.itineraryDisplay) {
173
- setPreviewItinerary(data.itineraryDisplay as ItineraryDisplayStep[]);
174
- } else {
175
- setPreviewItinerary(null);
176
- }
177
- })
178
- .catch(() => setPreviewItinerary(null));
179
- return () => { cancelled = true; };
180
- }, [isOpen, hasValidSelection, hasChanged, selectedLocationId, selectedCustomAddress, effectiveBookingRef, lastName]);
181
-
182
- const currentPickupName =
183
- booking.travelerHotel ||
184
- (booking.pickupLocationId
185
- ? pickupLocations.find((l) => l.id === booking.pickupLocationId)?.name ?? booking.pickupLocationId
186
- : null);
187
-
188
- const itineraryDisplay = booking.itineraryDisplay ?? [];
189
- const pickupOrDropOffSteps = itineraryDisplay.filter(
190
- (s) => s.stepType === 'pickup' || s.stepType === 'drop_off'
191
- );
192
-
193
- const handleSave = async () => {
194
- if (!lastName?.trim()) {
195
- setError('Last name is required to update pickup');
196
- return;
197
- }
198
- if (!hasValidSelection) {
199
- setError('Please select a pickup location');
200
- return;
201
- }
202
-
203
- setSaving(true);
204
- setError(null);
205
- try {
206
- let payload: { pickupLocationId?: string; travelerHotel?: string };
207
- if (selectedCustomAddress) {
208
- payload = { travelerHotel: selectedCustomAddress };
209
- } else if (selectedLocationId) {
210
- payload = { pickupLocationId: selectedLocationId, travelerHotel: selectedLocationName ?? undefined };
211
- } else {
212
- setError('Please select a pickup location');
213
- setSaving(false);
214
- return;
215
- }
216
-
217
- const updated = await updatePickupLocation(
218
- effectiveBookingRef,
219
- lastName,
220
- payload
221
- );
222
- onSuccess(updated as BookingData);
223
- onClose();
224
- } catch (e) {
225
- setError(e instanceof Error ? e.message : 'Failed to update pickup location');
226
- } finally {
227
- setSaving(false);
228
- }
229
- };
230
-
231
- if (!isOpen) return null;
232
-
233
- return (
234
- <div
235
- className={styles.overlay}
236
- onClick={onClose}
237
- role="dialog"
238
- aria-modal="true"
239
- aria-labelledby="pickup-dialog-title"
240
- >
241
- <div className={`${styles.modal} ${styles.pickupDialogRoot}`} onClick={(e) => e.stopPropagation()}>
242
- <div className={styles.header} style={{ display: 'grid', gridTemplateColumns: 'auto 1fr auto', alignItems: 'center' }}>
243
- <span className={styles.headerSpacer} aria-hidden />
244
- <h2 id="pickup-dialog-title" className={styles.title} style={{ textAlign: 'center', justifySelf: 'center' }}>
245
- {t('pickup.title') || 'Select pickup location'}
246
- </h2>
247
- <button type="button" onClick={onClose} className={styles.closeBtn} aria-label="Close">
248
- <svg width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
249
- <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
250
- </svg>
251
- </button>
252
- </div>
253
-
254
- <div className={styles.content}>
255
- {loading ? (
256
- <div className={styles.loading}>Loading pickup locations…</div>
257
- ) : error && !pickupLocations.length ? (
258
- <div className={styles.error}>{error}</div>
259
- ) : pickupLocations.length === 0 ? (
260
- <div className={styles.empty}>No pickup locations available for this tour.</div>
261
- ) : (
262
- <>
263
- <div className={styles.currentPickupSection}>
264
- <p className={styles.currentPickupLabel}>Current pickup</p>
265
- <p className={styles.currentPickupName}>
266
- {currentPickupName ?? (t('booking.pickupLocationUnknown') || "I don't know")}
267
- </p>
268
- {(booking.pickupLocationId || booking.travelerHotel) && (
269
- <p className={styles.currentPickupTimes}>
270
- Pickup: {getPickupTimeFromItinerary(itineraryDisplay)} · Drop-off: {getDropoffTimeFromItinerary(itineraryDisplay)}
271
- </p>
272
- )}
273
- </div>
274
- <div
275
- className={`${styles.selectorWrapper} ${styles.selectorWrapperConstrained}`}
276
- style={{ fontFamily: "'Figtree', sans-serif" }}
277
- >
278
- <PickupLocationSelector
279
- hideTitle
280
- hideSkipOption
281
- pickupLocations={pickupLocations}
282
- selectedLocationId={selectedLocationId}
283
- selectedCustomAddress={selectedCustomAddress}
284
- allowCustomLocation={isPrivateShuttle}
285
- restrictCustomLocationToServiceArea={isPrivateShuttle}
286
- destinations={destinations}
287
- onLocationSelect={(locationId, customLocation, locationName) => {
288
- setError('');
289
- if (customLocation) {
290
- setSelectedLocationId(null);
291
- setSelectedLocationName(null);
292
- setSelectedCustomAddress(customLocation.address);
293
- } else {
294
- setSelectedLocationId(locationId);
295
- setSelectedLocationName(locationName ?? null);
296
- setSelectedCustomAddress(null);
297
- }
298
- }}
299
- />
300
- </div>
301
-
302
- {hasValidSelection && hasChanged && pickupOrDropOffSteps.length > 0 && (
303
- <div className={styles.itineraryPreview}>
304
- <h3 className={styles.previewTitle}>Itinerary changes</h3>
305
- <p className={styles.previewIntro}>
306
- Your itinerary will be updated with the new pickup location:
307
- </p>
308
- <ul className={styles.previewList}>
309
- {pickupOrDropOffSteps.map((step, i) => {
310
- const previewStep = previewItinerary?.find(s => s.stepType === step.stepType);
311
- const newTime = previewStep?.time?.trim();
312
- const newLabel =
313
- step.stepType === 'pickup'
314
- ? selectedPickupName
315
- ? `Pickup at ${selectedPickupName}${newTime ? ` ${newTime}` : ''}`
316
- : t('booking.pickupLocationUnknown') || "I don't know"
317
- : step.stepType === 'drop_off'
318
- ? selectedPickupName
319
- ? `Drop off at ${selectedPickupName}${newTime ? ` ${newTime}` : ''}`
320
- : t('booking.pickupLocationUnknown') || "I don't know"
321
- : getItineraryStepLabel(previewStep ?? step, true);
322
- return (
323
- <li key={i} className={styles.previewItem}>
324
- <span className={styles.previewOld}>
325
- {getItineraryStepLabel(step, true)}
326
- </span>
327
- <span className={styles.previewArrow}>→</span>
328
- <span className={styles.previewNew}>{newLabel}</span>
329
- </li>
330
- );
331
- })}
332
- </ul>
333
- </div>
334
- )}
335
-
336
- {error && <div className={styles.error}>{error}</div>}
337
-
338
- <div className={styles.footer}>
339
- <Button variant="outline" className={styles.cancelBtn} onClick={onClose}>
340
- Cancel
341
- </Button>
342
- <Button
343
- variant="primary"
344
- hoverColor={ButtonHoverColor.Turquoise}
345
- onClick={handleSave}
346
- disabled={!hasValidSelection || !hasChanged || saving}
347
- >
348
- {saving ? 'Saving…' : 'Save pickup location'}
349
- </Button>
350
- </div>
351
- </>
352
- )}
353
- </div>
354
- </div>
355
- </div>
356
- );
357
- }