@ticketboothapp/booking 1.2.190 → 1.2.193

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 (32) hide show
  1. package/package.json +3 -1
  2. package/src/components/BackgroundVideo.tsx +6 -3
  3. package/src/components/booking/AddOnsSection.module.css +6 -2
  4. package/src/components/booking/AddOnsSection.tsx +4 -4
  5. package/src/components/booking/AdminChangeReceiptComparison.module.css +16 -0
  6. package/src/components/booking/AdminChangeReceiptComparison.tsx +14 -4
  7. package/src/components/booking/BookingDialog.module.css +43 -0
  8. package/src/components/booking/BookingFlowCollage.tsx +43 -16
  9. package/src/components/booking/Calendar.module.css +8 -3
  10. package/src/components/booking/ChangeBookingDialog.tsx +116 -55
  11. package/src/components/booking/ChangeBookingFlow.tsx +1 -0
  12. package/src/components/booking/CheckoutForm.module.css +7 -0
  13. package/src/components/booking/CollapsibleAddOnItem.module.css +3 -2
  14. package/src/components/booking/MealDrinkAddOnSelector.tsx +4 -4
  15. package/src/components/booking/PickupLocationDialog.tsx +63 -59
  16. package/src/components/booking/PickupLocationSelector.tsx +20 -25
  17. package/src/components/booking/PriceSummary.module.css +19 -0
  18. package/src/components/booking/PriceSummary.tsx +10 -7
  19. package/src/components/booking/TourDescription.module.css +6 -1
  20. package/src/components/booking/TourDescription.tsx +25 -16
  21. package/src/components/booking/change-booking-compare.module.css +154 -0
  22. package/src/components/booking/change-booking-compare.tsx +141 -1
  23. package/src/components/booking/pickup-location-product.ts +24 -0
  24. package/src/constants/images.ts +1 -1
  25. package/src/index.ts +11 -0
  26. package/src/lib/booking/pickup-location-disclaimer.ts +69 -0
  27. package/src/lib/booking-api.ts +1 -0
  28. package/src/strings/en.json +140 -68
  29. package/src/strings/es.json +181 -68
  30. package/src/strings/fr.json +181 -68
  31. package/test/change-booking-helpers.test.ts +38 -0
  32. package/test/pickup-location-disclaimer.test.ts +44 -0
@@ -95,3 +95,157 @@
95
95
  line-height: 1.2;
96
96
  color: var(--primary-text, var(--booking-text, #1c1917));
97
97
  }
98
+
99
+ .mobileDigest {
100
+ display: none;
101
+ }
102
+
103
+ @media (max-width: 767px) {
104
+ .kickerMobileHidden {
105
+ display: none;
106
+ }
107
+
108
+ .mobileDigest {
109
+ display: block;
110
+ padding: 0 0.25rem;
111
+ font-family: 'Figtree', var(--booking-font-sans, ui-sans-serif), sans-serif;
112
+ }
113
+
114
+ .mobileDigest .mobileDigestToggle {
115
+ display: block;
116
+ width: 100%;
117
+ min-height: 2.75rem;
118
+ margin: 0;
119
+ padding: 0.625rem 0.75rem;
120
+ border: 0;
121
+ border-radius: 0.75rem;
122
+ background: transparent;
123
+ color: var(--primary-text, var(--booking-text, #1c1917));
124
+ font: inherit;
125
+ text-align: left;
126
+ cursor: pointer;
127
+ appearance: none;
128
+ }
129
+
130
+ .mobileDigest .mobileDigestToggle:focus-visible {
131
+ outline: 2px solid var(--accent-orange, #ff4d00);
132
+ outline-offset: 2px;
133
+ }
134
+
135
+ .mobileDigestExpanded .mobileDigestToggle {
136
+ border-radius: 0.75rem 0.75rem 0 0;
137
+ }
138
+
139
+ .mobileDigestHeading,
140
+ .mobileDigestAction,
141
+ .mobileDigestLine,
142
+ .mobileDigestTotalLine {
143
+ display: flex;
144
+ align-items: baseline;
145
+ }
146
+
147
+ .mobileDigestHeading {
148
+ justify-content: space-between;
149
+ gap: 0.75rem;
150
+ }
151
+
152
+ .mobileDigestKicker {
153
+ font-size: 0.625rem;
154
+ font-weight: 700;
155
+ letter-spacing: 0.07em;
156
+ line-height: 1.2;
157
+ text-transform: uppercase;
158
+ color: var(--grey-text, var(--booking-text-muted, #78716c));
159
+ }
160
+
161
+ .mobileDigestAction {
162
+ flex-shrink: 0;
163
+ gap: 0.125rem;
164
+ font-size: 0.75rem;
165
+ font-weight: 600;
166
+ line-height: 1.2;
167
+ color: var(--accent-orange, #ff4d00);
168
+ }
169
+
170
+ .mobileDigestChevron {
171
+ flex-shrink: 0;
172
+ align-self: center;
173
+ transition: transform 160ms ease;
174
+ }
175
+
176
+ .mobileDigestChevronExpanded {
177
+ transform: rotate(180deg);
178
+ }
179
+
180
+ .mobileDigestChanges {
181
+ display: grid;
182
+ gap: 0.125rem;
183
+ margin-top: 0.375rem;
184
+ font-size: 0.8125rem;
185
+ line-height: 1.3;
186
+ }
187
+
188
+ .mobileDigestLine {
189
+ min-width: 0;
190
+ }
191
+
192
+ .mobileDigestArrow {
193
+ margin: 0 0.35rem;
194
+ color: var(--grey-text, var(--booking-text-muted, #78716c));
195
+ }
196
+
197
+ .mobileDigestNewValue {
198
+ font-weight: 700;
199
+ }
200
+
201
+ .mobileDigestMuted {
202
+ color: var(--grey-text, var(--booking-text-muted, #78716c));
203
+ }
204
+
205
+ .mobileDigestTotalLine {
206
+ justify-content: space-between;
207
+ gap: 0.75rem;
208
+ padding-top: 0.125rem;
209
+ font-variant-numeric: tabular-nums;
210
+ }
211
+
212
+ .deltaIncrease,
213
+ .deltaDecrease,
214
+ .deltaSame {
215
+ flex-shrink: 0;
216
+ font-weight: 700;
217
+ }
218
+
219
+ .deltaIncrease {
220
+ color: var(--accent-orange, #ff4d00);
221
+ }
222
+
223
+ .deltaDecrease {
224
+ color: var(--booking-emerald-700, #047857);
225
+ }
226
+
227
+ .deltaSame {
228
+ color: var(--grey-text, var(--booking-text-muted, #78716c));
229
+ }
230
+
231
+ .mobileDigestBaseline {
232
+ display: block;
233
+ margin-top: 0.375rem;
234
+ font-size: 0.8125rem;
235
+ font-weight: 500;
236
+ line-height: 1.3;
237
+ font-variant-numeric: tabular-nums;
238
+ }
239
+
240
+ .visuallyHidden {
241
+ position: absolute;
242
+ width: 1px;
243
+ height: 1px;
244
+ padding: 0;
245
+ margin: -1px;
246
+ overflow: hidden;
247
+ clip: rect(0, 0, 0, 0);
248
+ white-space: nowrap;
249
+ border: 0;
250
+ }
251
+ }
@@ -6,6 +6,8 @@ export type ItineraryStepLine = { time: string | null; label: string };
6
6
 
7
7
  export type ChangeHighlightVariant = 'current' | 'new';
8
8
 
9
+ export type ChangePriceDeltaDirection = 'increase' | 'decrease' | 'same' | null;
10
+
9
11
  /** Same date formatting as manage-booking / ChangeBookingDialog (Mountain Time label). */
10
12
  export function formatBookingDateForChangeCompare(dateTime: string | null | undefined): string {
11
13
  if (!dateTime) return '—';
@@ -102,6 +104,7 @@ export function BookingChangeSummaryColumn({
102
104
  dateChanged = false,
103
105
  ticketsChanged = false,
104
106
  itineraryStepChanged,
107
+ hideKickerOnMobile = false,
105
108
  }: {
106
109
  kicker: string;
107
110
  tourName: string;
@@ -114,6 +117,7 @@ export function BookingChangeSummaryColumn({
114
117
  dateChanged?: boolean;
115
118
  ticketsChanged?: boolean;
116
119
  itineraryStepChanged?: boolean[];
120
+ hideKickerOnMobile?: boolean;
117
121
  }) {
118
122
  const dateClass =
119
123
  highlightVariant === 'current' && dateChanged
@@ -130,7 +134,11 @@ export function BookingChangeSummaryColumn({
130
134
 
131
135
  return (
132
136
  <div className={`booking-change-summary-column ${styles.column}`}>
133
- <p className={styles.kicker}>{kicker}</p>
137
+ <p
138
+ className={`${styles.kicker} ${hideKickerOnMobile ? styles.kickerMobileHidden : ''}`}
139
+ >
140
+ {kicker}
141
+ </p>
134
142
 
135
143
  {/* Same pattern as manage-booking `BookingDetails`: label + value on one baseline row */}
136
144
  <div className={styles.row}>
@@ -226,3 +234,135 @@ export function BookingChangeSummaryColumn({
226
234
  </div>
227
235
  );
228
236
  }
237
+
238
+ export function MobileBookingChangeDigest({
239
+ expanded,
240
+ detailsId,
241
+ showChanges,
242
+ currentDate,
243
+ newDate,
244
+ currentTickets,
245
+ newTickets,
246
+ currentTotal,
247
+ newTotal,
248
+ dateChanged,
249
+ ticketsChanged,
250
+ itineraryChangeCount,
251
+ priceDeltaFormatted,
252
+ priceDeltaDirection,
253
+ pricePending,
254
+ onToggle,
255
+ }: {
256
+ expanded: boolean;
257
+ detailsId: string;
258
+ showChanges: boolean;
259
+ currentDate: string;
260
+ newDate: string;
261
+ currentTickets: string;
262
+ newTickets: string;
263
+ currentTotal: string;
264
+ newTotal: string;
265
+ dateChanged: boolean;
266
+ ticketsChanged: boolean;
267
+ itineraryChangeCount: number;
268
+ priceDeltaFormatted: string | null;
269
+ priceDeltaDirection: ChangePriceDeltaDirection;
270
+ pricePending: boolean;
271
+ onToggle: () => void;
272
+ }) {
273
+ const actionLabel = expanded ? 'Hide details' : showChanges ? 'Review changes' : 'View details';
274
+ const deltaClass =
275
+ priceDeltaDirection === 'increase'
276
+ ? styles.deltaIncrease
277
+ : priceDeltaDirection === 'decrease'
278
+ ? styles.deltaDecrease
279
+ : styles.deltaSame;
280
+
281
+ return (
282
+ <div
283
+ className={`${styles.mobileDigest} ${expanded ? styles.mobileDigestExpanded : ''}`}
284
+ >
285
+ <button
286
+ type="button"
287
+ className={styles.mobileDigestToggle}
288
+ aria-expanded={expanded}
289
+ aria-controls={detailsId}
290
+ onClick={onToggle}
291
+ >
292
+ <span className={styles.mobileDigestHeading}>
293
+ <span className={styles.mobileDigestKicker}>
294
+ {showChanges ? 'Your changes' : 'Current booking'}
295
+ </span>
296
+ <span className={styles.mobileDigestAction}>
297
+ {actionLabel}
298
+ <svg
299
+ aria-hidden="true"
300
+ className={`${styles.mobileDigestChevron} ${expanded ? styles.mobileDigestChevronExpanded : ''}`}
301
+ width="16"
302
+ height="16"
303
+ viewBox="0 0 20 20"
304
+ fill="none"
305
+ >
306
+ <path
307
+ d="m5 7.5 5 5 5-5"
308
+ stroke="currentColor"
309
+ strokeWidth="1.75"
310
+ strokeLinecap="round"
311
+ strokeLinejoin="round"
312
+ />
313
+ </svg>
314
+ </span>
315
+ </span>
316
+
317
+ {showChanges ? (
318
+ <span className={styles.mobileDigestChanges}>
319
+ {dateChanged ? (
320
+ <span className={styles.mobileDigestLine}>
321
+ <span>{currentDate}</span>
322
+ <span className={styles.mobileDigestArrow} aria-hidden="true">
323
+
324
+ </span>
325
+ <span className={styles.visuallyHidden}>changes to</span>
326
+ <span className={styles.mobileDigestNewValue}>{newDate}</span>
327
+ </span>
328
+ ) : null}
329
+ {ticketsChanged ? (
330
+ <span className={styles.mobileDigestLine}>
331
+ <span>{currentTickets}</span>
332
+ <span className={styles.mobileDigestArrow} aria-hidden="true">
333
+
334
+ </span>
335
+ <span className={styles.visuallyHidden}>changes to</span>
336
+ <span className={styles.mobileDigestNewValue}>{newTickets}</span>
337
+ </span>
338
+ ) : null}
339
+ {itineraryChangeCount > 0 ? (
340
+ <span className={styles.mobileDigestMuted}>
341
+ {itineraryChangeCount}{' '}
342
+ {itineraryChangeCount === 1 ? 'itinerary update' : 'itinerary updates'}
343
+ </span>
344
+ ) : null}
345
+ <span className={styles.mobileDigestTotalLine}>
346
+ <span>
347
+ {currentTotal}
348
+ <span className={styles.mobileDigestArrow} aria-hidden="true">
349
+
350
+ </span>
351
+ <span className={styles.visuallyHidden}>changes to</span>
352
+ <span className={styles.mobileDigestNewValue}>{newTotal}</span>
353
+ </span>
354
+ <span className={pricePending ? styles.deltaSame : deltaClass}>
355
+ {pricePending ? 'Updating price…' : priceDeltaFormatted}
356
+ </span>
357
+ </span>
358
+ </span>
359
+ ) : (
360
+ <span className={styles.mobileDigestBaseline}>
361
+ {currentDate} <span aria-hidden="true">·</span> {currentTickets}{' '}
362
+ <span aria-hidden="true">·</span> {currentTotal}
363
+ </span>
364
+ )}
365
+ </button>
366
+ </div>
367
+ );
368
+ }
@@ -0,0 +1,24 @@
1
+ import type { Product } from '../../lib/booking-api';
2
+
3
+ export function findProductForPickupLocations(
4
+ products: Product[],
5
+ bookingProductId: string,
6
+ ): Product | null {
7
+ for (const product of products) {
8
+ if (product.productId === bookingProductId) return product;
9
+ if (product.pickupLocations?.some((location) => location.id === bookingProductId)) {
10
+ return product;
11
+ }
12
+ if (product.options?.some((option) => option.optionId === bookingProductId)) return product;
13
+ }
14
+ return null;
15
+ }
16
+
17
+ /** Keep manage-booking pickup choices aligned with the normal booking flow's static catalog. */
18
+ export function resolvePickupLocationProduct(
19
+ staticProduct: Product | null,
20
+ liveProducts: Product[],
21
+ bookingProductId: string,
22
+ ): Product | null {
23
+ return staticProduct ?? findProductForPickupLocations(liveProducts, bookingProductId);
24
+ }
@@ -553,4 +553,4 @@ export const IMAGES: Record<string, ImageData> = {
553
553
  } as const;
554
554
 
555
555
  // Type for accessing image keys
556
- export type ImageKey = keyof typeof IMAGES;
556
+ export type ImageKey = keyof typeof IMAGES;
package/src/index.ts CHANGED
@@ -64,6 +64,17 @@ export { default as BookingProductGrid } from './components/booking/BookingProdu
64
64
  export { DefaultTermsContent as TermsContent } from './components/booking/DefaultTermsContent';
65
65
  export { BackgroundVideo } from './components/BackgroundVideo';
66
66
  export type { BackgroundVideoLoading, BackgroundVideoProps } from './components/BackgroundVideo';
67
+ export {
68
+ BookingFlowCollage,
69
+ type BookingFlowCollageProps,
70
+ } from './components/booking/BookingFlowCollage';
71
+ export {
72
+ TourDescription,
73
+ type TourDescriptionProps,
74
+ type TourDescriptionReview,
75
+ type TourDescriptionSection,
76
+ type TourDescriptionSubsection,
77
+ } from './components/booking/TourDescription';
67
78
 
68
79
  export type {
69
80
  BookingAppMode,
@@ -0,0 +1,69 @@
1
+ export interface PickupLocationDisclaimerInput {
2
+ id?: string | null;
3
+ name?: string | null;
4
+ disclaimer?: string | null;
5
+ }
6
+
7
+ export interface PickupLocationDisclaimer {
8
+ title: string;
9
+ message: string;
10
+ }
11
+
12
+ const SAMSON_MALL_PICKUP_LOCATION_ID = 'loc_afkvmlNRwRqZ';
13
+ const SAMSON_MALL_PICKUP_LOCATION_NAME = 'samson mall';
14
+
15
+ const LAKE_LOUISE_HOTEL_PICKUP_LOCATIONS = new Map([
16
+ ['loc_Ll05oVACjjAx', 'HI Lake Louise Alpine Centre'],
17
+ ['loc_VAJ0L8amxnC5', 'Fairmont Lake Louise'],
18
+ ['loc_arPLLVG1xKxw', 'Mountaineer Lodge'],
19
+ ['loc_9ObwCZ7MxfjC', 'Paradise Lodge and Bungalows'],
20
+ ['loc_ieW6EfdVeYd4', 'Post Hotel'],
21
+ ]);
22
+
23
+ const LAKE_LOUISE_HOTEL_NAMES = new Set(
24
+ Array.from(LAKE_LOUISE_HOTEL_PICKUP_LOCATIONS.values(), (name) =>
25
+ name.toLowerCase(),
26
+ ),
27
+ );
28
+
29
+ const SAMSON_MALL_PARKING_DISCLAIMER =
30
+ 'No public parking at this pickup location. This pickup location is meant to serve guests staying at the HI Lake Louise Alpine Hostel, Lake Louise Inn, or Lake Louise Campground and can walk to Samson Mall for pickup due to parking restrictions in the town of Lake Louise.';
31
+
32
+ export function getPickupLocationDisclaimer(
33
+ location: PickupLocationDisclaimerInput,
34
+ ): PickupLocationDisclaimer | null {
35
+ const objectDisclaimer = location.disclaimer?.trim();
36
+ if (objectDisclaimer) {
37
+ return {
38
+ title: 'Pickup location note',
39
+ message: objectDisclaimer,
40
+ };
41
+ }
42
+
43
+ const normalizedName = location.name?.trim().toLowerCase() ?? '';
44
+
45
+ if (
46
+ location.id === SAMSON_MALL_PICKUP_LOCATION_ID ||
47
+ normalizedName === SAMSON_MALL_PICKUP_LOCATION_NAME
48
+ ) {
49
+ return {
50
+ title: 'Samson Mall pickup note',
51
+ message: SAMSON_MALL_PARKING_DISCLAIMER,
52
+ };
53
+ }
54
+
55
+ const hotelName =
56
+ (location.id
57
+ ? LAKE_LOUISE_HOTEL_PICKUP_LOCATIONS.get(location.id)
58
+ : undefined) ??
59
+ (LAKE_LOUISE_HOTEL_NAMES.has(normalizedName)
60
+ ? location.name?.trim()
61
+ : undefined);
62
+
63
+ if (!hotelName) return null;
64
+
65
+ return {
66
+ title: 'Hotel guests only',
67
+ message: `This pickup location is only available to guests staying at ${hotelName}. There is no public parking for shuttle guests anywhere in Lake Louise.`,
68
+ };
69
+ }
@@ -849,6 +849,7 @@ export interface PickupLocation {
849
849
  pickupTimeOffsetMinutes?: number;
850
850
  travelMinutesFromDestination?: Record<string, number>;
851
851
  freeParking?: boolean;
852
+ disclaimer?: string | null;
852
853
  notes?: string;
853
854
  driverNotes?: string;
854
855
  }