@ticketboothapp/booking 1.2.190 → 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 +6 -2
- package/src/components/booking/AddOnsSection.tsx +4 -4
- 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 +8 -3
- package/src/components/booking/ChangeBookingDialog.tsx +116 -55
- package/src/components/booking/ChangeBookingFlow.tsx +1 -0
- package/src/components/booking/CheckoutForm.module.css +7 -0
- package/src/components/booking/CollapsibleAddOnItem.module.css +3 -2
- package/src/components/booking/MealDrinkAddOnSelector.tsx +4 -4
- 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/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/index.ts +11 -0
- package/src/lib/booking/pickup-location-disclaimer.ts +67 -0
- package/src/lib/booking-api.ts +1 -0
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ticketboothapp/booking",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.191",
|
|
4
4
|
"private": false,
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"**/*.css",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"exports": {
|
|
13
13
|
".": "./src/index.ts",
|
|
14
14
|
"./background-video": "./src/components/BackgroundVideo.tsx",
|
|
15
|
+
"./terms-content": "./src/components/booking/DefaultTermsContent.tsx",
|
|
15
16
|
"./booking-flow.css": "./src/components/booking/booking-flow.css",
|
|
16
17
|
"./runtime": "./src/runtime/index.ts",
|
|
17
18
|
"./contexts/booking-app-context": "./src/contexts/BookingAppContext.tsx",
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
"./hooks/useBookingSourceMetadataFromLocation": "./src/hooks/useBookingSourceMetadataFromLocation.ts",
|
|
22
23
|
"./hooks/useIsBookingLaunchLive": "./src/hooks/useIsBookingLaunchLive.ts",
|
|
23
24
|
"./hooks/usePlacesAutocompleteSearch": "./src/hooks/usePlacesAutocompleteSearch.ts",
|
|
25
|
+
"./pickup-location-disclaimer": "./src/lib/booking/pickup-location-disclaimer.ts",
|
|
24
26
|
"./viavia/catalog": "./src/constants/products.ts",
|
|
25
27
|
"./viavia/images": "./src/constants/images.ts",
|
|
26
28
|
"./viavia/product-descriptions": "./src/lib/product-descriptions.ts",
|
|
@@ -20,6 +20,7 @@ export interface BackgroundVideoProps
|
|
|
20
20
|
poster?: string;
|
|
21
21
|
preload?: 'auto' | 'metadata' | 'none';
|
|
22
22
|
loading?: BackgroundVideoLoading;
|
|
23
|
+
viewportMargin?: string;
|
|
23
24
|
className?: string;
|
|
24
25
|
style?: CSSProperties;
|
|
25
26
|
}
|
|
@@ -41,6 +42,7 @@ export const BackgroundVideo = forwardRef<HTMLVideoElement, BackgroundVideoProps
|
|
|
41
42
|
poster,
|
|
42
43
|
preload,
|
|
43
44
|
loading = 'viewport',
|
|
45
|
+
viewportMargin = '300px 0px',
|
|
44
46
|
className,
|
|
45
47
|
style,
|
|
46
48
|
autoPlay = true,
|
|
@@ -80,11 +82,11 @@ export const BackgroundVideo = forwardRef<HTMLVideoElement, BackgroundVideoProps
|
|
|
80
82
|
if (entry.isIntersecting) setShouldLoad(true);
|
|
81
83
|
setShouldPlay(entry.isIntersecting);
|
|
82
84
|
},
|
|
83
|
-
{ rootMargin:
|
|
85
|
+
{ rootMargin: viewportMargin },
|
|
84
86
|
);
|
|
85
87
|
observer.observe(container);
|
|
86
88
|
return () => observer.disconnect();
|
|
87
|
-
}, [loading]);
|
|
89
|
+
}, [loading, viewportMargin]);
|
|
88
90
|
|
|
89
91
|
useEffect(() => {
|
|
90
92
|
const video = videoRef.current;
|
|
@@ -100,6 +102,7 @@ export const BackgroundVideo = forwardRef<HTMLVideoElement, BackgroundVideoProps
|
|
|
100
102
|
style={{ width: '100%', height: '100%', display: 'grid', ...style }}
|
|
101
103
|
>
|
|
102
104
|
<video
|
|
105
|
+
key={shouldLoad ? `${webm ?? ''}|${src}` : 'deferred'}
|
|
103
106
|
{...videoProps}
|
|
104
107
|
ref={setVideoRef}
|
|
105
108
|
className="next-video-bg-video"
|
|
@@ -107,7 +110,7 @@ export const BackgroundVideo = forwardRef<HTMLVideoElement, BackgroundVideoProps
|
|
|
107
110
|
muted={muted}
|
|
108
111
|
loop={loop}
|
|
109
112
|
playsInline={playsInline}
|
|
110
|
-
poster={poster}
|
|
113
|
+
poster={shouldLoad ? poster : undefined}
|
|
111
114
|
preload={shouldLoad ? (preload ?? (loading === 'eager' ? 'auto' : 'metadata')) : 'none'}
|
|
112
115
|
style={{
|
|
113
116
|
gridArea: '1 / 1',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
.section {
|
|
2
2
|
padding-top: 1.5rem;
|
|
3
3
|
margin-top: 1.5rem;
|
|
4
|
-
border-top: 1px solid var(--booking-stone-200, #e7e5e4);
|
|
4
|
+
border-top: 1px solid var(--booking-stone-200, #e7e5e4) !important;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
.items {
|
|
@@ -79,6 +79,10 @@
|
|
|
79
79
|
gap: 0.5rem;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
.quantityButton {
|
|
83
|
+
border: 1px solid var(--booking-stone-300, #d6d3d1) !important;
|
|
84
|
+
}
|
|
85
|
+
|
|
82
86
|
.optionDescription {
|
|
83
87
|
grid-column: 1;
|
|
84
88
|
grid-row: 2;
|
|
@@ -97,7 +101,7 @@
|
|
|
97
101
|
gap: 0.25rem 0.75rem;
|
|
98
102
|
width: 100%;
|
|
99
103
|
padding: 0.75rem;
|
|
100
|
-
border-width: 2px;
|
|
104
|
+
border-width: 2px !important;
|
|
101
105
|
border-radius: 0.5rem;
|
|
102
106
|
text-align: left;
|
|
103
107
|
transition: border-color 0.2s, background-color 0.2s;
|
|
@@ -94,7 +94,7 @@ export function AddOnsSection({
|
|
|
94
94
|
});
|
|
95
95
|
}}
|
|
96
96
|
disabled={!canDecrement}
|
|
97
|
-
className=
|
|
97
|
+
className={`${styles.quantityButton} h-8 w-8 rounded-full bg-white text-sm text-stone-600 hover:bg-stone-50 disabled:cursor-not-allowed disabled:opacity-50`}
|
|
98
98
|
>
|
|
99
99
|
−
|
|
100
100
|
</button>
|
|
@@ -107,7 +107,7 @@ export function AddOnsSection({
|
|
|
107
107
|
return [...rest, { addOnId: addOn.addOnId, quantity: quantity + 1 }];
|
|
108
108
|
});
|
|
109
109
|
}}
|
|
110
|
-
className=
|
|
110
|
+
className={`${styles.quantityButton} h-8 w-8 rounded-full bg-white text-sm text-stone-600 hover:bg-stone-50`}
|
|
111
111
|
>
|
|
112
112
|
+
|
|
113
113
|
</button>
|
|
@@ -186,7 +186,7 @@ export function AddOnsSection({
|
|
|
186
186
|
});
|
|
187
187
|
}}
|
|
188
188
|
disabled={!canDecrement}
|
|
189
|
-
className=
|
|
189
|
+
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`}
|
|
190
190
|
>
|
|
191
191
|
−
|
|
192
192
|
</button>
|
|
@@ -208,7 +208,7 @@ export function AddOnsSection({
|
|
|
208
208
|
];
|
|
209
209
|
});
|
|
210
210
|
}}
|
|
211
|
-
className=
|
|
211
|
+
className={`${styles.quantityButton} h-8 w-8 rounded-full bg-white text-stone-600 hover:bg-stone-50 text-sm`}
|
|
212
212
|
>
|
|
213
213
|
+
|
|
214
214
|
</button>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
@media (max-width: 639px) {
|
|
2
|
+
.receiptTitle {
|
|
3
|
+
font-size: 0.875rem !important;
|
|
4
|
+
line-height: 1.25rem !important;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.amountDueLabel {
|
|
8
|
+
font-size: 0.875rem !important;
|
|
9
|
+
line-height: 1.25rem !important;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.amountDueValue {
|
|
13
|
+
font-size: 1rem !important;
|
|
14
|
+
line-height: 1.25rem !important;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -9,6 +9,7 @@ import type { Locale } from '../../lib/booking/i18n/config';
|
|
|
9
9
|
import type { ChangeBookingFlowProps } from './booking-flow-types';
|
|
10
10
|
import type { Currency } from './CurrencySwitcher';
|
|
11
11
|
import { PriceSummary, type PriceSummaryLine } from './PriceSummary';
|
|
12
|
+
import styles from './AdminChangeReceiptComparison.module.css';
|
|
12
13
|
import { collapseAdminPromoLines } from './admin-change-receipt-lines';
|
|
13
14
|
import type { AdminCustomReceiptLine } from './useAdminCustomReceiptLines';
|
|
14
15
|
import {
|
|
@@ -74,6 +75,8 @@ export interface AdminChangeReceiptComparisonProps {
|
|
|
74
75
|
}) => void;
|
|
75
76
|
preserveExistingReceipt?: boolean;
|
|
76
77
|
onPreserveExistingReceiptChange?: (preserve: boolean) => void;
|
|
78
|
+
/** Use denser, wrapping receipt typography in the public mobile change flow. */
|
|
79
|
+
compactMobile?: boolean;
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
function normalizeLocale(locale: string): Locale {
|
|
@@ -132,6 +135,7 @@ export function AdminChangeReceiptComparison({
|
|
|
132
135
|
onSetReceiptLineOverride,
|
|
133
136
|
preserveExistingReceipt = false,
|
|
134
137
|
onPreserveExistingReceiptChange,
|
|
138
|
+
compactMobile = false,
|
|
135
139
|
}: AdminChangeReceiptComparisonProps) {
|
|
136
140
|
const [overrideDrafts, setOverrideDrafts] = useState<Record<string, string>>({});
|
|
137
141
|
useEffect(() => {
|
|
@@ -344,7 +348,9 @@ export function AdminChangeReceiptComparison({
|
|
|
344
348
|
<div className="grid gap-4 md:grid-cols-2">
|
|
345
349
|
<section className="min-w-0 overflow-visible rounded-lg border border-stone-200 bg-stone-50/70 p-3">
|
|
346
350
|
<div className="mb-3 flex items-center justify-between gap-3">
|
|
347
|
-
<h3 className=
|
|
351
|
+
<h3 className={`${compactMobile ? styles.receiptTitle : ''} text-sm font-semibold text-stone-900`}>
|
|
352
|
+
Existing purchase
|
|
353
|
+
</h3>
|
|
348
354
|
<span className="shrink-0 text-xs font-medium text-stone-500">
|
|
349
355
|
{formatCurrencyAmount(originalReceipt.total, originalCurrency, displayLocale)}
|
|
350
356
|
</span>
|
|
@@ -358,6 +364,7 @@ export function AdminChangeReceiptComparison({
|
|
|
358
364
|
taxAmount={hasTaxLine(existingLines) ? 0 : originalReceipt.tax}
|
|
359
365
|
taxRate={taxRate}
|
|
360
366
|
size="sm"
|
|
367
|
+
compactMobileTotal={compactMobile}
|
|
361
368
|
t={t}
|
|
362
369
|
/>
|
|
363
370
|
</section>
|
|
@@ -365,7 +372,7 @@ export function AdminChangeReceiptComparison({
|
|
|
365
372
|
<section className="min-w-0 overflow-visible rounded-lg border border-sky-200 bg-sky-50/50 p-3">
|
|
366
373
|
<div className="mb-3">
|
|
367
374
|
<div>
|
|
368
|
-
<h3 className=
|
|
375
|
+
<h3 className={`${compactMobile ? styles.receiptTitle : ''} text-sm font-semibold text-stone-900`}>
|
|
369
376
|
{isProductFamilyConversion ? 'New purchase' : 'Current change'}
|
|
370
377
|
</h3>
|
|
371
378
|
{isProductFamilyConversion ? (
|
|
@@ -436,6 +443,7 @@ export function AdminChangeReceiptComparison({
|
|
|
436
443
|
locale={displayLocale}
|
|
437
444
|
subtotal={summarySubtotal}
|
|
438
445
|
size="sm"
|
|
446
|
+
compactMobileTotal={compactMobile}
|
|
439
447
|
t={t}
|
|
440
448
|
lineAmountInputs={preserveExistingReceipt ? undefined : { ...quotedOverrideAmountInputs, ...overrideDrafts }}
|
|
441
449
|
onLineAmountInputChange={preserveExistingReceipt ? undefined : handleOverrideChange}
|
|
@@ -538,8 +546,10 @@ export function AdminChangeReceiptComparison({
|
|
|
538
546
|
) : null}
|
|
539
547
|
|
|
540
548
|
<div className="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-stone-200 bg-stone-50 px-3 py-2">
|
|
541
|
-
<span className=
|
|
542
|
-
|
|
549
|
+
<span className={`${compactMobile ? styles.amountDueLabel : ''} text-sm font-semibold text-stone-900`}>
|
|
550
|
+
{amountDueLabel}
|
|
551
|
+
</span>
|
|
552
|
+
<span className={`${compactMobile ? styles.amountDueValue : ''} text-lg font-semibold tabular-nums ${amountDueClass}`}>
|
|
543
553
|
{!selectionChanged || displayedSettlementAmount == null
|
|
544
554
|
? '—'
|
|
545
555
|
: formatCurrencyAmount(displayedSettlementAmount, currency, displayLocale)}
|
|
@@ -109,6 +109,49 @@
|
|
|
109
109
|
padding: 1.25rem;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
.bookingCompareSticky {
|
|
113
|
+
position: sticky;
|
|
114
|
+
top: 0;
|
|
115
|
+
z-index: 10;
|
|
116
|
+
margin-bottom: 0.75rem;
|
|
117
|
+
width: 100%;
|
|
118
|
+
border-radius: 1rem;
|
|
119
|
+
padding: 0.5rem 0;
|
|
120
|
+
background: var(--light-orange-background);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.mobileCompareDigestContainer {
|
|
124
|
+
display: none;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@media (max-width: 767px) {
|
|
128
|
+
.bookingCompareSticky {
|
|
129
|
+
z-index: 20;
|
|
130
|
+
max-height: calc(90vh - 4.5rem);
|
|
131
|
+
max-height: calc(90dvh - 4.5rem);
|
|
132
|
+
overflow: hidden;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.mobileCompareDigestContainer {
|
|
136
|
+
display: block;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.bookingCompareDetails {
|
|
140
|
+
max-height: calc(90vh - 11rem);
|
|
141
|
+
max-height: calc(90dvh - 11rem);
|
|
142
|
+
overflow-y: auto;
|
|
143
|
+
overscroll-behavior: contain;
|
|
144
|
+
border-radius: 0 0 1rem 1rem;
|
|
145
|
+
padding: 0 0.25rem 0.25rem;
|
|
146
|
+
-webkit-overflow-scrolling: touch;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.bookingCompareDetails :global(.booking-change-summary-column) {
|
|
150
|
+
border-radius: 0 0 0.75rem 0.75rem;
|
|
151
|
+
background: transparent;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
112
155
|
@media (min-width: 768px) {
|
|
113
156
|
.dialog {
|
|
114
157
|
max-width: 960px;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
5
5
|
import { BackgroundVideo } from '../BackgroundVideo';
|
|
6
6
|
import type { VideoSources } from '../../constants/products';
|
|
7
|
+
import type { BackgroundVideoLoading } from '../BackgroundVideo';
|
|
7
8
|
import { resolvePublicAssetUrl, useBookingHost } from '../../runtime';
|
|
8
9
|
import { useTranslations } from '../../lib/booking/i18n';
|
|
9
10
|
import styles from './BookingFlowCollage.module.css';
|
|
@@ -17,6 +18,17 @@ export interface BookingFlowCollageProps {
|
|
|
17
18
|
imageIds: string[];
|
|
18
19
|
/** Alt text prefix for images (e.g. product name) */
|
|
19
20
|
altPrefix?: string;
|
|
21
|
+
/** Descriptive alt text matching imageIds. Falls back to numbered labels. */
|
|
22
|
+
imageAlts?: string[];
|
|
23
|
+
/** Responsive slot widths for the one- and two-column gallery cells. */
|
|
24
|
+
imageSizes?: {
|
|
25
|
+
narrow: string;
|
|
26
|
+
wide: string;
|
|
27
|
+
};
|
|
28
|
+
/** Defer loading when the collage appears below the fold. */
|
|
29
|
+
videoLoading?: BackgroundVideoLoading;
|
|
30
|
+
/** Browser preload hint once the video is eligible to load. */
|
|
31
|
+
videoPreload?: 'auto' | 'metadata' | 'none';
|
|
20
32
|
}
|
|
21
33
|
|
|
22
34
|
const DEFAULT_VIDEO: VideoSources = {
|
|
@@ -24,25 +36,36 @@ const DEFAULT_VIDEO: VideoSources = {
|
|
|
24
36
|
webm: '/videos/via-via-moraine-lake-tour-video.webm',
|
|
25
37
|
};
|
|
26
38
|
|
|
39
|
+
// The first and fourth cells occupy one grid column; the middle cells occupy
|
|
40
|
+
// two. Accurate slot hints prevent small thumbnails from selecting full-width
|
|
41
|
+
// gallery assets while retaining enough resolution for high-density displays.
|
|
42
|
+
const NARROW_IMAGE_SIZES = '(max-width: 640px) 29vw, 300px';
|
|
43
|
+
const WIDE_IMAGE_SIZES = '(max-width: 640px) 59vw, 560px';
|
|
44
|
+
const DEFAULT_IMAGE_SIZES = {
|
|
45
|
+
narrow: NARROW_IMAGE_SIZES,
|
|
46
|
+
wide: WIDE_IMAGE_SIZES,
|
|
47
|
+
};
|
|
48
|
+
|
|
27
49
|
export function BookingFlowCollage({
|
|
28
50
|
video,
|
|
29
51
|
videoPosterImageId,
|
|
30
52
|
imageIds,
|
|
31
53
|
altPrefix = 'Tour',
|
|
54
|
+
imageAlts,
|
|
55
|
+
imageSizes = DEFAULT_IMAGE_SIZES,
|
|
56
|
+
videoLoading = 'eager',
|
|
57
|
+
videoPreload = 'auto',
|
|
32
58
|
}: BookingFlowCollageProps) {
|
|
33
59
|
const { catalog, slots, env } = useBookingHost();
|
|
34
60
|
const ViaViaImage = slots.Image;
|
|
35
61
|
const ImageModal = slots.ImageModal;
|
|
36
62
|
const videoSrc = video ?? DEFAULT_VIDEO;
|
|
37
|
-
// Use
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
src: resolvePublicAssetUrl(env.STATIC_ASSET_ORIGIN, videoForCollageRaw.src),
|
|
44
|
-
webm: resolvePublicAssetUrl(env.STATIC_ASSET_ORIGIN, videoForCollageRaw.webm),
|
|
45
|
-
};
|
|
63
|
+
// Use the MP4 here because these videos include an audio track, while the
|
|
64
|
+
// WebM variants are silent background-video encodes.
|
|
65
|
+
const videoForCollageSrc = resolvePublicAssetUrl(
|
|
66
|
+
env.STATIC_ASSET_ORIGIN,
|
|
67
|
+
videoSrc.longSrc ?? videoSrc.src,
|
|
68
|
+
);
|
|
46
69
|
const posterUrl =
|
|
47
70
|
videoPosterImageId && catalog.getImageUrl
|
|
48
71
|
? String(catalog.getImageUrl(videoPosterImageId))
|
|
@@ -91,10 +114,13 @@ export function BookingFlowCollage({
|
|
|
91
114
|
const handleToggleMute = useCallback(() => {
|
|
92
115
|
const el = videoRef.current;
|
|
93
116
|
if (!el) return;
|
|
94
|
-
|
|
117
|
+
const nextMuted = !el.muted;
|
|
118
|
+
el.muted = nextMuted;
|
|
119
|
+
setIsMuted(nextMuted);
|
|
95
120
|
}, []);
|
|
96
121
|
|
|
97
|
-
const
|
|
122
|
+
const getAlt = (i: number) => imageAlts?.[i] ?? `${altPrefix} - ${i + 1}`;
|
|
123
|
+
const imageItems = gridImages.map((id, i) => ({ id, alt: getAlt(i) }));
|
|
98
124
|
|
|
99
125
|
return (
|
|
100
126
|
<div className={`${styles.collage} ${!hasGridImages ? styles.videoOnly : ''}`}>
|
|
@@ -102,11 +128,11 @@ export function BookingFlowCollage({
|
|
|
102
128
|
<div className={styles.videoWrapper}>
|
|
103
129
|
<BackgroundVideo
|
|
104
130
|
ref={videoRef}
|
|
105
|
-
{
|
|
106
|
-
loading=
|
|
107
|
-
preload=
|
|
131
|
+
src={videoForCollageSrc}
|
|
132
|
+
loading={videoLoading}
|
|
133
|
+
preload={videoPreload}
|
|
108
134
|
autoPlay
|
|
109
|
-
muted
|
|
135
|
+
muted={isMuted}
|
|
110
136
|
loop
|
|
111
137
|
playsInline
|
|
112
138
|
poster={posterUrl}
|
|
@@ -163,8 +189,9 @@ export function BookingFlowCollage({
|
|
|
163
189
|
>
|
|
164
190
|
<ViaViaImage
|
|
165
191
|
imageId={id}
|
|
166
|
-
alt={
|
|
192
|
+
alt={getAlt(i)}
|
|
167
193
|
context="GALLERY"
|
|
194
|
+
sizes={i === 0 || i === 3 ? imageSizes.narrow : imageSizes.wide}
|
|
168
195
|
/>
|
|
169
196
|
</button>
|
|
170
197
|
))}
|
|
@@ -375,7 +375,12 @@
|
|
|
375
375
|
transform: translateX(-50%);
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
-
|
|
378
|
+
/*
|
|
379
|
+
* Keep the frame more specific than the scoped booking preflight
|
|
380
|
+
* (`.booking-flow-preflight *`), which otherwise wins when its stylesheet is
|
|
381
|
+
* emitted later and resets the border width to zero on some mobile builds.
|
|
382
|
+
*/
|
|
383
|
+
.calendar .calendarGrid {
|
|
379
384
|
border: 1px solid var(--cal-outer-border);
|
|
380
385
|
border-radius: 0.5rem;
|
|
381
386
|
overflow: hidden;
|
|
@@ -395,7 +400,7 @@
|
|
|
395
400
|
border-bottom: 1px solid var(--cal-header-border);
|
|
396
401
|
}
|
|
397
402
|
|
|
398
|
-
.calendarHeaderCell {
|
|
403
|
+
.calendar .calendarHeaderCell {
|
|
399
404
|
padding: 0.25rem 0;
|
|
400
405
|
text-align: center;
|
|
401
406
|
font-size: 0.75rem;
|
|
@@ -405,7 +410,7 @@
|
|
|
405
410
|
letter-spacing: 0.05em;
|
|
406
411
|
border-right: 1px solid var(--cal-header-border);
|
|
407
412
|
}
|
|
408
|
-
.calendarHeaderCell:last-child {
|
|
413
|
+
.calendar .calendarHeaderCell:last-child {
|
|
409
414
|
border-right: none;
|
|
410
415
|
}
|
|
411
416
|
|
|
@@ -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}
|