@ticketboothapp/booking 1.2.189 → 1.2.191

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 (50) hide show
  1. package/package.json +3 -1
  2. package/src/components/BackgroundVideo.tsx +6 -3
  3. package/src/components/booking/AddOnsSection.module.css +129 -1
  4. package/src/components/booking/AddOnsSection.tsx +21 -25
  5. package/src/components/booking/AdminChangeBookingContent.tsx +1 -0
  6. package/src/components/booking/AdminChangeBookingFlow.tsx +1 -0
  7. package/src/components/booking/AdminChangeReceiptComparison.module.css +16 -0
  8. package/src/components/booking/AdminChangeReceiptComparison.tsx +14 -4
  9. package/src/components/booking/BookingDialog.module.css +43 -0
  10. package/src/components/booking/BookingFlowCollage.tsx +43 -16
  11. package/src/components/booking/Calendar.module.css +132 -13
  12. package/src/components/booking/Calendar.tsx +46 -20
  13. package/src/components/booking/CancellationPolicySelector.module.css +33 -4
  14. package/src/components/booking/CancellationPolicySelector.tsx +30 -28
  15. package/src/components/booking/ChangeBookingDialog.tsx +116 -55
  16. package/src/components/booking/ChangeBookingFlow.tsx +2 -0
  17. package/src/components/booking/ChangeBookingItineraryPanel.tsx +3 -0
  18. package/src/components/booking/CheckoutForm.module.css +7 -0
  19. package/src/components/booking/CollapsibleAddOnItem.module.css +4 -3
  20. package/src/components/booking/ItineraryBox.module.css +14 -0
  21. package/src/components/booking/ItineraryBox.tsx +75 -7
  22. package/src/components/booking/MealDrinkAddOnSelector.tsx +14 -16
  23. package/src/components/booking/NewBookingFlow.tsx +1 -0
  24. package/src/components/booking/PickupLocationDialog.tsx +19 -23
  25. package/src/components/booking/PickupLocationSelector.tsx +20 -25
  26. package/src/components/booking/PriceSummary.module.css +19 -0
  27. package/src/components/booking/PriceSummary.tsx +10 -7
  28. package/src/components/booking/PrivateShuttlePassengerSection.tsx +7 -1
  29. package/src/components/booking/ReturnTimeSelector.module.css +23 -4
  30. package/src/components/booking/ReturnTimeSelector.tsx +5 -5
  31. package/src/components/booking/StandardBookingItineraryPanel.tsx +3 -0
  32. package/src/components/booking/TicketSelector.module.css +30 -3
  33. package/src/components/booking/TicketSelector.tsx +6 -6
  34. package/src/components/booking/TourDescription.module.css +6 -1
  35. package/src/components/booking/TourDescription.tsx +25 -16
  36. package/src/components/booking/change-booking-compare.module.css +154 -0
  37. package/src/components/booking/change-booking-compare.tsx +141 -1
  38. package/src/constants/images.ts +1 -1
  39. package/src/hooks/useIsBookingLaunchLive.ts +4 -1
  40. package/src/index.ts +11 -0
  41. package/src/lib/booking/i18n/messages/en.json +2 -0
  42. package/src/lib/booking/i18n/messages/fr.json +2 -0
  43. package/src/lib/booking/pickup-location-disclaimer.ts +67 -0
  44. package/src/lib/booking-api.ts +1 -0
  45. package/src/lib/booking-constants.ts +1 -1
  46. package/src/strings/en.json +140 -68
  47. package/src/strings/es.json +181 -68
  48. package/src/strings/fr.json +181 -68
  49. package/test/pickup-location-disclaimer.test.ts +44 -0
  50. package/CHANGE_BOOKING_BE_HANDOFF.md +0 -160
@@ -5,7 +5,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
5
5
  import { AnimatePresence, motion } from 'framer-motion';
6
6
  import { getProduct, type Product } from '../../lib/booking-api';
7
7
  import { useBookingHost } from '../../runtime';
8
- import { formatCurrencyAmount, type Currency } from '../../lib/currency';
8
+ import { formatCurrencyAmount, toMoneyCents, type Currency } from '../../lib/currency';
9
9
  import { CURRENCIES, DEFAULT_CURRENCY } from './CurrencySwitcher';
10
10
  import { ChangeBookingFlow } from './ChangeBookingFlow';
11
11
  import { VIAVIA_TICKET_FINE_PRINT_BY_CATEGORY } from './booking-flow-ui';
@@ -18,6 +18,7 @@ import { getItineraryStepLabel } from '../../lib/booking/itinerary-display';
18
18
  import { effectiveProductOptionIdForChangeFlow } from '../../lib/booking/product-option-id';
19
19
  import {
20
20
  BookingChangeSummaryColumn,
21
+ MobileBookingChangeDigest,
21
22
  changeBookingCompareColumnsVisuallyMatch,
22
23
  computeItineraryStepChanged,
23
24
  formatBookingDateForChangeCompare,
@@ -85,6 +86,7 @@ export default function ChangeBookingDialog({
85
86
  const [product, setProduct] = useState<Product | null>(null);
86
87
  const [error, setError] = useState<string | null>(null);
87
88
  const [newBookingPreview, setNewBookingPreview] = useState<ChangeFlowSelectionPreview | null>(null);
89
+ const [isMobileCompareExpanded, setIsMobileCompareExpanded] = useState(false);
88
90
  const dialogRef = useRef<HTMLDivElement>(null);
89
91
  const contentRef = useRef<HTMLDivElement>(null);
90
92
  const overlayRef = useRef<HTMLDivElement>(null);
@@ -189,7 +191,10 @@ export default function ChangeBookingDialog({
189
191
  }, [isOpen]);
190
192
 
191
193
  useEffect(() => {
192
- if (!isOpen) setNewBookingPreview(null);
194
+ if (!isOpen) {
195
+ setNewBookingPreview(null);
196
+ setIsMobileCompareExpanded(false);
197
+ }
193
198
  }, [isOpen]);
194
199
 
195
200
  const handleChangeFlowSelectionPreview = useCallback((preview: ChangeFlowSelectionPreview | null) => {
@@ -303,6 +308,35 @@ export default function ChangeBookingDialog({
303
308
  const itineraryStepChangedNew = showNewBookingColumn
304
309
  ? computeItineraryStepChanged(newItinerarySteps, currentItinerarySteps)
305
310
  : undefined;
311
+ const itineraryChangeCount = showNewBookingColumn
312
+ ? Math.max(
313
+ itineraryStepChangedCurrent?.filter(Boolean).length ?? 0,
314
+ itineraryStepChangedNew?.filter(Boolean).length ?? 0,
315
+ )
316
+ : 0;
317
+ const totalDeltaCents =
318
+ newBookingPreview?.selectionTotal == null
319
+ ? null
320
+ : toMoneyCents(newBookingPreview.selectionTotal) - toMoneyCents(booking.receipt.totalAmount);
321
+ const priceDeltaDirection =
322
+ totalDeltaCents == null
323
+ ? null
324
+ : totalDeltaCents > 0
325
+ ? 'increase'
326
+ : totalDeltaCents < 0
327
+ ? 'decrease'
328
+ : 'same';
329
+ const priceDeltaFormatted =
330
+ totalDeltaCents == null
331
+ ? null
332
+ : totalDeltaCents === 0
333
+ ? 'No price change'
334
+ : `${totalDeltaCents > 0 ? '+' : '−'}${formatCurrencyAmount(
335
+ Math.abs(totalDeltaCents) / 100,
336
+ receiptCurrency,
337
+ 'en',
338
+ )}`;
339
+ const mobileCompareDetailsId = 'mobile-booking-change-details';
306
340
 
307
341
  return (
308
342
  <div ref={overlayRef} className={`${styles.overlay} booking-dialog-overlay`} onClick={onClose}>
@@ -363,63 +397,90 @@ export default function ChangeBookingDialog({
363
397
  {product && (
364
398
  <div className={`${styles.screen} booking-flow-preflight`}>
365
399
  <div
366
- className="sticky top-0 z-10 mb-3 flex w-full flex-col gap-0 rounded-2xl py-2 md:flex-row md:items-stretch"
367
- style={{ background: 'var(--light-orange-background)' }}
400
+ className={styles.bookingCompareSticky}
368
401
  >
369
- <motion.div
370
- layout
371
- transition={{ type: 'spring', stiffness: 380, damping: 34 }}
372
- className={
373
- showNewBookingColumn
374
- ? 'min-w-0 md:flex-1 md:basis-0'
375
- : 'min-w-0 w-full'
376
- }
377
- >
378
- <BookingChangeSummaryColumn
379
- highlightVariant="current"
380
- kicker="Current booking"
381
- tourName={tourName}
382
- dateStr={currentDateStr}
383
- ticketsStr={ticketsLine}
384
- itinerarySteps={currentItinerarySteps}
385
- totalFormatted={currentTotalFormatted}
402
+ <div className={styles.mobileCompareDigestContainer}>
403
+ <MobileBookingChangeDigest
404
+ expanded={isMobileCompareExpanded}
405
+ detailsId={mobileCompareDetailsId}
406
+ showChanges={showNewBookingColumn}
407
+ currentDate={currentDateStr}
408
+ newDate={newDateStr}
409
+ currentTickets={ticketsLine}
410
+ newTickets={newTicketsStr}
411
+ currentTotal={currentTotalFormatted}
412
+ newTotal={newTotalFormatted}
386
413
  dateChanged={dateChanged}
387
414
  ticketsChanged={ticketsChanged}
388
- itineraryStepChanged={itineraryStepChangedCurrent}
415
+ itineraryChangeCount={itineraryChangeCount}
416
+ priceDeltaFormatted={priceDeltaFormatted}
417
+ priceDeltaDirection={priceDeltaDirection}
418
+ pricePending={showNewBookingColumn && newBookingPreview?.selectionTotal == null}
419
+ onToggle={() => setIsMobileCompareExpanded((expanded) => !expanded)}
389
420
  />
390
- </motion.div>
391
- {showNewBookingColumn ? (
392
- <div
393
- aria-hidden
394
- className="booking-change-compare-divider max-md:my-3 md:my-0"
395
- />
396
- ) : null}
397
- <AnimatePresence initial={false} mode="popLayout">
398
- {showNewBookingColumn && (
399
- <motion.div
400
- key="new-booking-summary"
401
- layout
402
- initial={{ opacity: 0, x: 16, scale: 0.99 }}
403
- animate={{ opacity: 1, x: 0, scale: 1 }}
404
- exit={{ opacity: 0, x: 16, scale: 0.99 }}
405
- transition={{ type: 'spring', stiffness: 380, damping: 34 }}
406
- className="min-w-0 md:flex-1 md:basis-0"
407
- >
408
- <BookingChangeSummaryColumn
409
- highlightVariant="new"
410
- kicker="New booking"
411
- tourName={tourName}
412
- dateStr={newDateStr}
413
- ticketsStr={newTicketsStr}
414
- itinerarySteps={newItinerarySteps}
415
- totalFormatted={newTotalFormatted}
416
- dateChanged={dateChanged}
417
- ticketsChanged={ticketsChanged}
418
- itineraryStepChanged={itineraryStepChangedNew}
419
- />
420
- </motion.div>
421
- )}
422
- </AnimatePresence>
421
+ </div>
422
+ <div
423
+ id={mobileCompareDetailsId}
424
+ className={`${styles.bookingCompareDetails} ${isMobileCompareExpanded ? 'flex' : 'hidden'} w-full flex-col gap-0 md:flex md:flex-row md:items-stretch`}
425
+ >
426
+ <motion.div
427
+ layout
428
+ transition={{ type: 'spring', stiffness: 380, damping: 34 }}
429
+ className={
430
+ showNewBookingColumn ? 'min-w-0 md:flex-1 md:basis-0' : 'min-w-0 w-full'
431
+ }
432
+ >
433
+ <BookingChangeSummaryColumn
434
+ highlightVariant="current"
435
+ kicker="Current booking"
436
+ tourName={tourName}
437
+ dateStr={currentDateStr}
438
+ ticketsStr={ticketsLine}
439
+ itinerarySteps={currentItinerarySteps}
440
+ totalFormatted={currentTotalFormatted}
441
+ dateChanged={dateChanged}
442
+ ticketsChanged={ticketsChanged}
443
+ itineraryStepChanged={itineraryStepChangedCurrent}
444
+ hideKickerOnMobile={!showNewBookingColumn}
445
+ />
446
+ </motion.div>
447
+ {showNewBookingColumn ? (
448
+ <div
449
+ aria-hidden
450
+ className="booking-change-compare-divider max-md:my-3 md:my-0"
451
+ />
452
+ ) : null}
453
+ <AnimatePresence initial={false} mode="popLayout">
454
+ {showNewBookingColumn && (
455
+ <motion.div
456
+ key="new-booking-summary"
457
+ layout
458
+ initial={{ opacity: 0, x: 16, scale: 0.99 }}
459
+ animate={{ opacity: 1, x: 0, scale: 1 }}
460
+ exit={{ opacity: 0, x: 16, scale: 0.99 }}
461
+ transition={{
462
+ type: 'spring',
463
+ stiffness: 380,
464
+ damping: 34,
465
+ }}
466
+ className="min-w-0 md:flex-1 md:basis-0"
467
+ >
468
+ <BookingChangeSummaryColumn
469
+ highlightVariant="new"
470
+ kicker="New booking"
471
+ tourName={tourName}
472
+ dateStr={newDateStr}
473
+ ticketsStr={newTicketsStr}
474
+ itinerarySteps={newItinerarySteps}
475
+ totalFormatted={newTotalFormatted}
476
+ dateChanged={dateChanged}
477
+ ticketsChanged={ticketsChanged}
478
+ itineraryStepChanged={itineraryStepChangedNew}
479
+ />
480
+ </motion.div>
481
+ )}
482
+ </AnimatePresence>
483
+ </div>
423
484
  </div>
424
485
  <ChangeBookingFlow
425
486
  product={product}
@@ -617,6 +617,7 @@ export function ChangeBookingFlow({
617
617
  locale={locale}
618
618
  t={t}
619
619
  taxRate={pricingConfig?.taxRate}
620
+ compactMobile
620
621
  />
621
622
  ) : null;
622
623
 
@@ -893,6 +894,7 @@ export function ChangeBookingFlow({
893
894
  >
894
895
  <div className="mt-6 space-y-6">
895
896
  <ChangeBookingItineraryPanel
897
+ productId={product.productId}
896
898
  selectedDate={selectedDate}
897
899
  hideItineraryBox={hideItineraryBox}
898
900
  activeOptions={activeOptions}
@@ -13,6 +13,7 @@ import { ItineraryPlaceholder } from './ItineraryPlaceholder';
13
13
  type TranslationFn = (key: string, params?: Record<string, string>) => string;
14
14
 
15
15
  export interface ChangeBookingItineraryPanelProps {
16
+ productId: string;
16
17
  selectedDate: string;
17
18
  hideItineraryBox: boolean;
18
19
  activeOptions: ProductOption[];
@@ -40,6 +41,7 @@ export interface ChangeBookingItineraryPanelProps {
40
41
  }
41
42
 
42
43
  export function ChangeBookingItineraryPanel({
44
+ productId,
43
45
  selectedDate,
44
46
  hideItineraryBox,
45
47
  activeOptions,
@@ -94,6 +96,7 @@ export function ChangeBookingItineraryPanel({
94
96
 
95
97
  return (
96
98
  <ItineraryBox
99
+ productId={productId}
97
100
  selectedDate={selectedDate}
98
101
  formattedDate={formattedDate}
99
102
  itineraryItems={itineraryItems}
@@ -17,6 +17,11 @@
17
17
  display: flex;
18
18
  flex-direction: column;
19
19
  gap: 1rem;
20
+ min-width: 0;
21
+ }
22
+
23
+ .contactSection > * {
24
+ min-width: 0;
20
25
  }
21
26
 
22
27
  .contactGrid {
@@ -170,10 +175,12 @@
170
175
 
171
176
  .contactStaticValue {
172
177
  margin: 0;
178
+ max-width: 100%;
173
179
  font-size: 1rem;
174
180
  line-height: 1.4;
175
181
  color: var(--booking-stone-900, #1c1917);
176
182
  font-weight: 500;
183
+ overflow-wrap: anywhere;
177
184
  }
178
185
 
179
186
  .pickupSection {
@@ -1,6 +1,7 @@
1
1
  .card {
2
2
  overflow: hidden;
3
- border: 1px solid var(--booking-stone-200, #e7e5e4);
3
+ /* The scoped booking preflight can be emitted after CSS modules in local dev. */
4
+ border: 1px solid var(--booking-stone-200, #e7e5e4) !important;
4
5
  border-radius: 0.75rem;
5
6
  background: #fff;
6
7
  }
@@ -34,7 +35,7 @@
34
35
  flex: 1;
35
36
  font-family: 'Figtree', var(--booking-font-sans, ui-sans-serif), sans-serif !important;
36
37
  font-size: 1rem;
37
- font-weight: 700;
38
+ font-weight: 500;
38
39
  line-height: 1.35;
39
40
  color: var(--booking-stone-900, #1c1917) !important;
40
41
  }
@@ -67,6 +68,6 @@
67
68
  }
68
69
 
69
70
  .content {
70
- border-top: 1px solid var(--booking-stone-100, #f5f5f4);
71
+ border-top: 1px solid var(--booking-stone-100, #f5f5f4) !important;
71
72
  padding: 0.75rem 0.875rem;
72
73
  }
@@ -186,6 +186,20 @@
186
186
  font-size: 0.75rem;
187
187
  }
188
188
 
189
+ .compactStopRange {
190
+ display: inline-flex;
191
+ align-items: baseline;
192
+ gap: 0.375rem;
193
+ flex-wrap: wrap;
194
+ }
195
+
196
+ .compactStopRangeTime {
197
+ flex-shrink: 0;
198
+ white-space: nowrap;
199
+ font-weight: 700;
200
+ color: var(--booking-stone-900, #1c1917);
201
+ }
202
+
189
203
  @media (min-width: 640px) {
190
204
  .itemSticky {
191
205
  font-size: 0.875rem;
@@ -13,6 +13,63 @@ import styles from './ItineraryBox.module.css';
13
13
  type TranslationFn = (key: string, params?: Record<string, string>) => string;
14
14
 
15
15
  const TOOLTIP_TEXT = 'Approximate time - will be finalized when you select a pickup location';
16
+ const BIG_3_LAKES_PRODUCT_ID = 'p_C5jHohBUCwrd';
17
+
18
+ type ItineraryRenderEntry = {
19
+ item: ItineraryDisplayStep;
20
+ sourceIndex: number;
21
+ rangeEndTime?: string;
22
+ };
23
+
24
+ function formatItineraryTime(time: string): string {
25
+ return time
26
+ .replace(/^at /i, '')
27
+ .replace(/:00(?=\s*(AM|PM))/i, '');
28
+ }
29
+
30
+ function formatCompactStopTimeRange(startTime: string, endTime: string): string {
31
+ const start = formatItineraryTime(startTime);
32
+ const end = formatItineraryTime(endTime);
33
+ const startMatch = start.match(/^(.+?)\s*(AM|PM)$/i);
34
+ const endMatch = end.match(/^(.+?)\s*(AM|PM)$/i);
35
+
36
+ if (startMatch && endMatch && startMatch[2].toUpperCase() === endMatch[2].toUpperCase()) {
37
+ return `${startMatch[1]} - ${endMatch[1]} ${endMatch[2].toUpperCase()}`;
38
+ }
39
+ return `${start} - ${end}`;
40
+ }
41
+
42
+ function buildCompactStickyItinerary(items: ItineraryDisplayStep[]): ItineraryRenderEntry[] {
43
+ const entries: ItineraryRenderEntry[] = [];
44
+
45
+ for (let index = 0; index < items.length; index += 1) {
46
+ const item = items[index];
47
+ const next = items[index + 1];
48
+ const place = item.place?.trim().toLowerCase();
49
+ const nextPlace = next?.place?.trim().toLowerCase();
50
+ const hasDefiniteRange =
51
+ item.time &&
52
+ next?.time &&
53
+ item.time.toUpperCase() !== 'TBD' &&
54
+ next.time.toUpperCase() !== 'TBD';
55
+
56
+ if (
57
+ item.stepType === StepType.arrive &&
58
+ next?.stepType === StepType.depart &&
59
+ place &&
60
+ place === nextPlace &&
61
+ hasDefiniteRange
62
+ ) {
63
+ entries.push({ item, sourceIndex: index, rangeEndTime: next.time });
64
+ index += 1;
65
+ continue;
66
+ }
67
+
68
+ entries.push({ item, sourceIndex: index });
69
+ }
70
+
71
+ return entries;
72
+ }
16
73
 
17
74
  function TooltipAnchor({
18
75
  showTooltip,
@@ -107,6 +164,7 @@ function TooltipAnchor({
107
164
  }
108
165
 
109
166
  interface ItineraryBoxProps {
167
+ productId: string;
110
168
  selectedDate: string;
111
169
  formattedDate: string;
112
170
  itineraryItems: ItineraryDisplayStep[] | null;
@@ -127,6 +185,7 @@ interface ItineraryBoxProps {
127
185
  }
128
186
 
129
187
  export function ItineraryBox({
188
+ productId,
130
189
  selectedDate,
131
190
  formattedDate,
132
191
  itineraryItems,
@@ -134,7 +193,6 @@ export function ItineraryBox({
134
193
  isItinerarySticky,
135
194
  stickyTopPx = 0,
136
195
  isMobile,
137
- useWindowScroll = false,
138
196
  showTooltip,
139
197
  selectedPickupLocation,
140
198
  pickupLocationSkipped,
@@ -178,6 +236,10 @@ export function ItineraryBox({
178
236
  };
179
237
 
180
238
  const stickyTop = Math.max(0, stickyTopPx ?? 0);
239
+ const useCompactBig3Itinerary = productId === BIG_3_LAKES_PRODUCT_ID;
240
+ const itineraryEntries: ItineraryRenderEntry[] = useCompactBig3Itinerary
241
+ ? buildCompactStickyItinerary(itineraryItems)
242
+ : itineraryItems.map((item, sourceIndex) => ({ item, sourceIndex }));
181
243
 
182
244
  return (
183
245
  <div
@@ -196,9 +258,9 @@ export function ItineraryBox({
196
258
  <span className={styles.dateSubtitle}> · {formattedDate}</span>
197
259
  )}
198
260
  </h3>
199
- {itineraryItems.length > 0 && (
261
+ {itineraryEntries.length > 0 && (
200
262
  <div className={isItinerarySticky ? styles.itemsSticky : styles.itemsExpanded}>
201
- {itineraryItems.map((item, index) => {
263
+ {itineraryEntries.map(({ item, sourceIndex: index, rangeEndTime }, displayIndex) => {
202
264
  const itemLabel = getStepLabel(item, t);
203
265
  const isUncertain =
204
266
  !item.time ||
@@ -212,8 +274,7 @@ export function ItineraryBox({
212
274
 
213
275
  let formattedTime = '';
214
276
  if (item.time) {
215
- formattedTime = item.time.replace(/^at /i, '');
216
- formattedTime = formattedTime.replace(/:00(?=\s*(AM|PM))/i, '');
277
+ formattedTime = formatItineraryTime(item.time);
217
278
  }
218
279
 
219
280
  const isUncertainPickupOrDropOff =
@@ -224,7 +285,14 @@ export function ItineraryBox({
224
285
  const prefixKey =
225
286
  item.stepType === StepType.pickup ? 'booking.pickupAtPrefix' : 'booking.dropOffAtPrefix';
226
287
 
227
- const content = (
288
+ const content = rangeEndTime ? (
289
+ <span className={styles.compactStopRange}>
290
+ <span className={styles.compactStopRangeTime}>
291
+ {formatCompactStopTimeRange(item.time, rangeEndTime)}
292
+ </span>
293
+ <span className="text-stone-600">{item.place ?? itemLabel}</span>
294
+ </span>
295
+ ) : (
228
296
  <>
229
297
  {hasTime ? (
230
298
  <span className="inline-flex items-center gap-1.5 flex-wrap">
@@ -302,7 +370,7 @@ export function ItineraryBox({
302
370
  return (
303
371
  <span key={index} className={`${styles.item} ${styles.itemSticky}`}>
304
372
  {content}
305
- {index < itineraryItems.length - 1 && (
373
+ {displayIndex < itineraryEntries.length - 1 && (
306
374
  <span className={styles.separator}>
307
375
  <svg className="w-3 h-3 sm:w-4 sm:h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
308
376
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
@@ -5,6 +5,7 @@ import { formatCurrencyAmount } from '../../lib/currency';
5
5
  import type { AddOn, AddOnVariant } from '../../lib/booking-api';
6
6
  import type { Currency } from './CurrencySwitcher';
7
7
  import type { Locale } from '../../lib/booking/i18n/config';
8
+ import styles from './AddOnsSection.module.css';
8
9
 
9
10
  export interface AddOnSelection {
10
11
  addOnId: string;
@@ -229,7 +230,6 @@ export function MealDrinkAddOnSelector({
229
230
  const price = addOn.price ?? 0;
230
231
  const totalStep1 = Object.values(step1Counts).reduce((a, b) => a + b, 0);
231
232
  const totalStep2 = Object.values(step2Counts).reduce((a, b) => a + b, 0);
232
- const step1Ids = parsed.step1Options.map((o) => o.id);
233
233
  const step2Ids = parsed.step2Options.map((o) => o.id);
234
234
 
235
235
  return (
@@ -255,19 +255,12 @@ export function MealDrinkAddOnSelector({
255
255
  const qty = step1Counts[opt.id] ?? 0;
256
256
  const canDecrementTotal = totalStep1 > minimumTotal;
257
257
  return (
258
- <div key={opt.id} className="flex flex-wrap items-center gap-3 p-3 bg-stone-50 rounded-lg">
259
- <span className="min-w-0 flex-1">
260
- <span className="block font-medium text-stone-800">{opt.label}</span>
261
- {opt.description ? (
262
- <span className="mt-0.5 block text-xs font-normal leading-snug text-stone-500">
263
- {opt.description}
264
- </span>
265
- ) : null}
266
- </span>
267
- <span className="text-sm text-stone-500 shrink-0">
258
+ <div key={opt.id} className={styles.optionRow}>
259
+ <span className={`${styles.optionLabel} font-medium`}>{opt.label}</span>
260
+ <span className={styles.optionPrice}>
268
261
  {suppressPrices ? '—' : `${formatCurrencyAmount(price, currency, locale)} ea`}
269
262
  </span>
270
- <div className="flex items-center gap-1 shrink-0">
263
+ <div className={`${styles.optionControls} !gap-1`}>
271
264
  <button
272
265
  type="button"
273
266
  onClick={() => {
@@ -287,7 +280,7 @@ export function MealDrinkAddOnSelector({
287
280
  updateSelections(next, drinks);
288
281
  }}
289
282
  disabled={qty <= 0 || !canDecrementTotal}
290
- className="h-8 w-8 rounded-full border border-stone-300 bg-white text-stone-600 hover:bg-stone-50 disabled:opacity-50 disabled:cursor-not-allowed text-sm"
283
+ className={`${styles.quantityButton} h-8 w-8 rounded-full bg-white text-stone-600 hover:bg-stone-50 disabled:opacity-50 disabled:cursor-not-allowed text-sm`}
291
284
  >
292
285
 
293
286
  </button>
@@ -302,11 +295,16 @@ export function MealDrinkAddOnSelector({
302
295
  };
303
296
  updateSelections(next, drinks);
304
297
  }}
305
- className="h-8 w-8 rounded-full border border-stone-300 bg-white text-stone-600 hover:bg-stone-50 text-sm"
298
+ className={`${styles.quantityButton} h-8 w-8 rounded-full bg-white text-stone-600 hover:bg-stone-50 text-sm`}
306
299
  >
307
300
  +
308
301
  </button>
309
302
  </div>
303
+ {opt.description ? (
304
+ <span className={styles.optionDescription}>
305
+ {opt.description}
306
+ </span>
307
+ ) : null}
310
308
  </div>
311
309
  );
312
310
  })}
@@ -340,7 +338,7 @@ export function MealDrinkAddOnSelector({
340
338
  });
341
339
  }}
342
340
  disabled={qty <= 0}
343
- className="h-8 w-8 rounded-full border border-stone-300 bg-white text-stone-600 hover:bg-stone-50 disabled:opacity-50 disabled:cursor-not-allowed text-sm"
341
+ className={`${styles.quantityButton} h-8 w-8 rounded-full bg-white text-stone-600 hover:bg-stone-50 disabled:opacity-50 disabled:cursor-not-allowed text-sm`}
344
342
  >
345
343
 
346
344
  </button>
@@ -359,7 +357,7 @@ export function MealDrinkAddOnSelector({
359
357
  });
360
358
  }}
361
359
  disabled={otherQty <= 0}
362
- className="h-8 w-8 rounded-full border border-stone-300 bg-white text-stone-600 hover:bg-stone-50 disabled:opacity-50 disabled:cursor-not-allowed text-sm"
360
+ className={`${styles.quantityButton} h-8 w-8 rounded-full bg-white text-stone-600 hover:bg-stone-50 disabled:opacity-50 disabled:cursor-not-allowed text-sm`}
363
361
  >
364
362
  +
365
363
  </button>
@@ -891,6 +891,7 @@ export function NewBookingFlow({
891
891
  {/* Form sections - equal spacing between each */}
892
892
  <div className="mt-6 space-y-6">
893
893
  <StandardBookingItineraryPanel
894
+ productId={product.productId}
894
895
  selectedDate={selectedDate}
895
896
  hideItineraryBox={hideItineraryBox}
896
897
  activeOptions={activeOptions}
@@ -10,12 +10,9 @@ import {
10
10
  } from '../../lib/booking-api';
11
11
  import { PickupLocationSelector } from './PickupLocationSelector';
12
12
  import { useTranslations } from '../../lib/booking/i18n';
13
+ import { getPickupLocationDisclaimer } from '../../lib/booking/pickup-location-disclaimer';
13
14
  import styles from './PickupLocationDialog.module.css';
14
15
 
15
- const SAMSON_MALL_PICKUP_LOCATION_ID = 'loc_afkvmlNRwRqZ';
16
- const SAMSON_MALL_PARKING_DISCLAIMER =
17
- '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.';
18
-
19
16
  function findProductWithPickupLocations(
20
17
  products: Product[],
21
18
  bookingProductId: string
@@ -31,13 +28,6 @@ function findProductWithPickupLocations(
31
28
  return null;
32
29
  }
33
30
 
34
- function hasSamsonMallParkingDisclaimer(location: { id?: string | null; name?: string | null }): boolean {
35
- return (
36
- location.id === SAMSON_MALL_PICKUP_LOCATION_ID ||
37
- location.name?.trim().toLowerCase() === 'samson mall'
38
- );
39
- }
40
-
41
31
  /** Itinerary rows shown on manage-booking (may include optional label/time from API). */
42
32
  export interface PickupDialogItineraryStep {
43
33
  label?: string | null;
@@ -228,20 +218,26 @@ export function PickupLocationDialog({
228
218
  lastName,
229
219
  ]);
230
220
 
221
+ const currentPickupLocation = booking.pickupLocationId
222
+ ? pickupLocations.find((l) => l.id === booking.pickupLocationId) ?? null
223
+ : null;
231
224
  const currentPickupName =
232
225
  booking.travelerHotel ||
233
226
  (booking.pickupLocationId
234
- ? pickupLocations.find((l) => l.id === booking.pickupLocationId)?.name ?? booking.pickupLocationId
227
+ ? currentPickupLocation?.name ?? booking.pickupLocationId
235
228
  : null);
236
- const currentPickupRequiresParkingDisclaimer = hasSamsonMallParkingDisclaimer({
237
- id: booking.pickupLocationId,
238
- name: currentPickupName,
239
- });
229
+ const currentPickupDisclaimer = getPickupLocationDisclaimer(
230
+ currentPickupLocation ?? {
231
+ id: booking.pickupLocationId,
232
+ name: currentPickupName,
233
+ }
234
+ );
240
235
  const selectedPickupLocation = selectedLocationId
241
236
  ? pickupLocations.find((l) => l.id === selectedLocationId) ?? null
242
237
  : null;
243
- const selectedPickupRequiresParkingDisclaimer =
244
- selectedPickupLocation ? hasSamsonMallParkingDisclaimer(selectedPickupLocation) : false;
238
+ const selectedPickupDisclaimer = selectedPickupLocation
239
+ ? getPickupLocationDisclaimer(selectedPickupLocation)
240
+ : null;
245
241
 
246
242
  const itineraryDisplay = booking.itineraryDisplay ?? [];
247
243
  const pickupOrDropOffSteps = itineraryDisplay.filter(
@@ -332,9 +328,9 @@ export function PickupLocationDialog({
332
328
  {getDropoffTimeFromItinerary(itineraryDisplay)}
333
329
  </p>
334
330
  )}
335
- {currentPickupRequiresParkingDisclaimer && (
331
+ {currentPickupDisclaimer && (
336
332
  <p className={styles.parkingNotice} role="note">
337
- {SAMSON_MALL_PARKING_DISCLAIMER}
333
+ {currentPickupDisclaimer.message}
338
334
  </p>
339
335
  )}
340
336
  </div>
@@ -366,10 +362,10 @@ export function PickupLocationDialog({
366
362
  />
367
363
  </div>
368
364
 
369
- {selectedPickupRequiresParkingDisclaimer && (
365
+ {selectedPickupDisclaimer && (
370
366
  <div className={styles.selectedParkingNotice} role="note">
371
- <strong>Samson Mall pickup note</strong>
372
- <span>{SAMSON_MALL_PARKING_DISCLAIMER}</span>
367
+ <strong>{selectedPickupDisclaimer.title}</strong>
368
+ <span>{selectedPickupDisclaimer.message}</span>
373
369
  </div>
374
370
  )}
375
371