@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.
- package/package.json +3 -1
- package/src/components/BackgroundVideo.tsx +6 -3
- package/src/components/booking/AddOnsSection.module.css +129 -1
- package/src/components/booking/AddOnsSection.tsx +21 -25
- package/src/components/booking/AdminChangeBookingContent.tsx +1 -0
- package/src/components/booking/AdminChangeBookingFlow.tsx +1 -0
- package/src/components/booking/AdminChangeReceiptComparison.module.css +16 -0
- package/src/components/booking/AdminChangeReceiptComparison.tsx +14 -4
- package/src/components/booking/BookingDialog.module.css +43 -0
- package/src/components/booking/BookingFlowCollage.tsx +43 -16
- package/src/components/booking/Calendar.module.css +132 -13
- package/src/components/booking/Calendar.tsx +46 -20
- package/src/components/booking/CancellationPolicySelector.module.css +33 -4
- package/src/components/booking/CancellationPolicySelector.tsx +30 -28
- package/src/components/booking/ChangeBookingDialog.tsx +116 -55
- package/src/components/booking/ChangeBookingFlow.tsx +2 -0
- package/src/components/booking/ChangeBookingItineraryPanel.tsx +3 -0
- package/src/components/booking/CheckoutForm.module.css +7 -0
- package/src/components/booking/CollapsibleAddOnItem.module.css +4 -3
- package/src/components/booking/ItineraryBox.module.css +14 -0
- package/src/components/booking/ItineraryBox.tsx +75 -7
- package/src/components/booking/MealDrinkAddOnSelector.tsx +14 -16
- package/src/components/booking/NewBookingFlow.tsx +1 -0
- package/src/components/booking/PickupLocationDialog.tsx +19 -23
- package/src/components/booking/PickupLocationSelector.tsx +20 -25
- package/src/components/booking/PriceSummary.module.css +19 -0
- package/src/components/booking/PriceSummary.tsx +10 -7
- package/src/components/booking/PrivateShuttlePassengerSection.tsx +7 -1
- package/src/components/booking/ReturnTimeSelector.module.css +23 -4
- package/src/components/booking/ReturnTimeSelector.tsx +5 -5
- package/src/components/booking/StandardBookingItineraryPanel.tsx +3 -0
- package/src/components/booking/TicketSelector.module.css +30 -3
- package/src/components/booking/TicketSelector.tsx +6 -6
- package/src/components/booking/TourDescription.module.css +6 -1
- package/src/components/booking/TourDescription.tsx +25 -16
- package/src/components/booking/change-booking-compare.module.css +154 -0
- package/src/components/booking/change-booking-compare.tsx +141 -1
- package/src/constants/images.ts +1 -1
- package/src/hooks/useIsBookingLaunchLive.ts +4 -1
- package/src/index.ts +11 -0
- package/src/lib/booking/i18n/messages/en.json +2 -0
- package/src/lib/booking/i18n/messages/fr.json +2 -0
- package/src/lib/booking/pickup-location-disclaimer.ts +67 -0
- package/src/lib/booking-api.ts +1 -0
- package/src/lib/booking-constants.ts +1 -1
- package/src/strings/en.json +140 -68
- package/src/strings/es.json +181 -68
- package/src/strings/fr.json +181 -68
- package/test/pickup-location-disclaimer.test.ts +44 -0
- 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)
|
|
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=
|
|
367
|
-
style={{ background: 'var(--light-orange-background)' }}
|
|
400
|
+
className={styles.bookingCompareSticky}
|
|
368
401
|
>
|
|
369
|
-
<
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
showNewBookingColumn
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
415
|
+
itineraryChangeCount={itineraryChangeCount}
|
|
416
|
+
priceDeltaFormatted={priceDeltaFormatted}
|
|
417
|
+
priceDeltaDirection={priceDeltaDirection}
|
|
418
|
+
pricePending={showNewBookingColumn && newBookingPreview?.selectionTotal == null}
|
|
419
|
+
onToggle={() => setIsMobileCompareExpanded((expanded) => !expanded)}
|
|
389
420
|
/>
|
|
390
|
-
</
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
)}
|
|
422
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
{
|
|
261
|
+
{itineraryEntries.length > 0 && (
|
|
200
262
|
<div className={isItinerarySticky ? styles.itemsSticky : styles.itemsExpanded}>
|
|
201
|
-
{
|
|
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
|
|
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
|
-
{
|
|
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=
|
|
259
|
-
<span className=
|
|
260
|
-
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
-
?
|
|
227
|
+
? currentPickupLocation?.name ?? booking.pickupLocationId
|
|
235
228
|
: null);
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
244
|
-
|
|
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
|
-
{
|
|
331
|
+
{currentPickupDisclaimer && (
|
|
336
332
|
<p className={styles.parkingNotice} role="note">
|
|
337
|
-
{
|
|
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
|
-
{
|
|
365
|
+
{selectedPickupDisclaimer && (
|
|
370
366
|
<div className={styles.selectedParkingNotice} role="note">
|
|
371
|
-
<strong>
|
|
372
|
-
<span>{
|
|
367
|
+
<strong>{selectedPickupDisclaimer.title}</strong>
|
|
368
|
+
<span>{selectedPickupDisclaimer.message}</span>
|
|
373
369
|
</div>
|
|
374
370
|
)}
|
|
375
371
|
|