@ticketboothapp/booking 1.2.189 → 1.2.190
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 +1 -1
- package/src/components/booking/AddOnsSection.module.css +124 -0
- package/src/components/booking/AddOnsSection.tsx +17 -21
- package/src/components/booking/AdminChangeBookingContent.tsx +1 -0
- package/src/components/booking/AdminChangeBookingFlow.tsx +1 -0
- package/src/components/booking/Calendar.module.css +124 -10
- 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/ChangeBookingFlow.tsx +1 -0
- package/src/components/booking/ChangeBookingItineraryPanel.tsx +3 -0
- package/src/components/booking/CollapsibleAddOnItem.module.css +1 -1
- package/src/components/booking/ItineraryBox.module.css +14 -0
- package/src/components/booking/ItineraryBox.tsx +75 -7
- package/src/components/booking/MealDrinkAddOnSelector.tsx +10 -12
- package/src/components/booking/NewBookingFlow.tsx +1 -0
- 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/hooks/useIsBookingLaunchLive.ts +4 -1
- package/src/lib/booking/i18n/messages/en.json +2 -0
- package/src/lib/booking/i18n/messages/fr.json +2 -0
- package/src/lib/booking-constants.ts +1 -1
- package/CHANGE_BOOKING_BE_HANDOFF.md +0 -160
|
@@ -81,26 +81,26 @@ export function CancellationPolicySelector({
|
|
|
81
81
|
<span className={`${styles.radio} ${styles.radioSelected}`}>
|
|
82
82
|
<Check className="h-3 w-3 text-white" />
|
|
83
83
|
</span>
|
|
84
|
-
<
|
|
85
|
-
<span className={styles.policyLabel}>{forcedPolicy.label}</span>
|
|
86
|
-
<span
|
|
87
|
-
className={styles.refundTiersText}
|
|
88
|
-
dangerouslySetInnerHTML={{
|
|
89
|
-
__html:
|
|
90
|
-
formatRefundTiersSummary(forcedPolicy.refundTiers ?? [], t) ||
|
|
91
|
-
formatNoRefundsMessage(forcedPolicy.changeWindowHoursBefore, t),
|
|
92
|
-
}}
|
|
93
|
-
/>
|
|
94
|
-
{rowSubtitle && (
|
|
95
|
-
<span className={`${styles.refundTiersText} mt-1 block`}>
|
|
96
|
-
{rowSubtitle}
|
|
97
|
-
</span>
|
|
98
|
-
)}
|
|
99
|
-
</div>
|
|
84
|
+
<span className={styles.policyLabel}>{forcedPolicy.label}</span>
|
|
100
85
|
</div>
|
|
101
|
-
|
|
86
|
+
<span className={`${styles.fee} ${styles.feeFree}`}>
|
|
102
87
|
{t('booking.included') ?? 'Included'}
|
|
103
88
|
</span>
|
|
89
|
+
<div className={styles.policyDetails}>
|
|
90
|
+
<span
|
|
91
|
+
className={styles.refundTiersText}
|
|
92
|
+
dangerouslySetInnerHTML={{
|
|
93
|
+
__html:
|
|
94
|
+
formatRefundTiersSummary(forcedPolicy.refundTiers ?? [], t) ||
|
|
95
|
+
formatNoRefundsMessage(forcedPolicy.changeWindowHoursBefore, t),
|
|
96
|
+
}}
|
|
97
|
+
/>
|
|
98
|
+
{rowSubtitle && (
|
|
99
|
+
<span className={`${styles.refundTiersText} ${styles.rowSubtitle}`}>
|
|
100
|
+
{rowSubtitle}
|
|
101
|
+
</span>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
104
|
</div>
|
|
105
105
|
)}
|
|
106
106
|
{!forcedPolicy && policies.map((policy) => {
|
|
@@ -121,8 +121,17 @@ export function CancellationPolicySelector({
|
|
|
121
121
|
<span className={`${styles.radio} ${isSelected ? styles.radioSelected : styles.radioDefault}`}>
|
|
122
122
|
{isSelected && <Check className="h-3 w-3 text-white" />}
|
|
123
123
|
</span>
|
|
124
|
-
<
|
|
125
|
-
|
|
124
|
+
<span className={styles.policyLabel}>{policy.label}</span>
|
|
125
|
+
</div>
|
|
126
|
+
<span className={`${styles.fee} ${isFree ? styles.feeFree : styles.feePaid}`}>
|
|
127
|
+
{suppressFees
|
|
128
|
+
? '—'
|
|
129
|
+
: isFree
|
|
130
|
+
? (t('booking.included') ?? 'Included')
|
|
131
|
+
: `+${formatCurrencyAmount(fee, currency, locale as 'en' | 'fr')}`}
|
|
132
|
+
</span>
|
|
133
|
+
{(refundSummary || rowSubtitle) && (
|
|
134
|
+
<div className={styles.policyDetails}>
|
|
126
135
|
{refundSummary ? (
|
|
127
136
|
<span
|
|
128
137
|
className={styles.refundTiersText}
|
|
@@ -130,19 +139,12 @@ export function CancellationPolicySelector({
|
|
|
130
139
|
/>
|
|
131
140
|
) : null}
|
|
132
141
|
{rowSubtitle && (
|
|
133
|
-
<span className={`${styles.refundTiersText}
|
|
142
|
+
<span className={`${styles.refundTiersText} ${styles.rowSubtitle}`}>
|
|
134
143
|
{rowSubtitle}
|
|
135
144
|
</span>
|
|
136
145
|
)}
|
|
137
146
|
</div>
|
|
138
|
-
|
|
139
|
-
<span className={`${styles.fee} ${isFree ? styles.feeFree : styles.feePaid}`}>
|
|
140
|
-
{suppressFees
|
|
141
|
-
? '—'
|
|
142
|
-
: isFree
|
|
143
|
-
? (t('booking.included') ?? 'Included')
|
|
144
|
-
: `+${formatCurrencyAmount(fee, currency, locale as 'en' | 'fr')}`}
|
|
145
|
-
</span>
|
|
147
|
+
)}
|
|
146
148
|
</button>
|
|
147
149
|
);
|
|
148
150
|
})}
|
|
@@ -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}
|
|
@@ -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={() => {
|
|
@@ -307,6 +300,11 @@ export function MealDrinkAddOnSelector({
|
|
|
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
|
})}
|
|
@@ -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}
|
|
@@ -39,7 +39,13 @@ export function PrivateShuttlePassengerSection({
|
|
|
39
39
|
Number of Passengers
|
|
40
40
|
</label>
|
|
41
41
|
<div className={styles.passengerBox}>
|
|
42
|
-
<div
|
|
42
|
+
<div
|
|
43
|
+
className={
|
|
44
|
+
isSpecialRequestMode
|
|
45
|
+
? 'flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between'
|
|
46
|
+
: 'flex items-center justify-between gap-4'
|
|
47
|
+
}
|
|
48
|
+
>
|
|
43
49
|
<div>
|
|
44
50
|
<div className="font-medium text-stone-900">Passengers</div>
|
|
45
51
|
<div className="text-sm text-stone-600">
|
|
@@ -100,13 +100,17 @@
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
.content {
|
|
103
|
-
display:
|
|
103
|
+
display: grid;
|
|
104
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
105
|
+
grid-template-rows: auto auto;
|
|
104
106
|
align-items: center;
|
|
105
|
-
|
|
107
|
+
column-gap: 1rem;
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
.details {
|
|
109
|
-
|
|
111
|
+
min-width: 0;
|
|
112
|
+
grid-column: 1;
|
|
113
|
+
grid-row: 1;
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
.time {
|
|
@@ -141,6 +145,8 @@
|
|
|
141
145
|
}
|
|
142
146
|
|
|
143
147
|
.staySummary {
|
|
148
|
+
grid-column: 1;
|
|
149
|
+
grid-row: 2;
|
|
144
150
|
font-size: 0.75rem;
|
|
145
151
|
color: var(--booking-stone-500, #78716c);
|
|
146
152
|
margin-top: 0.25rem;
|
|
@@ -155,9 +161,22 @@
|
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
.price {
|
|
164
|
+
grid-column: 2;
|
|
165
|
+
grid-row: 1 / 3;
|
|
166
|
+
align-self: center;
|
|
158
167
|
font-size: 0.875rem;
|
|
159
168
|
font-weight: 600;
|
|
160
|
-
|
|
169
|
+
white-space: nowrap;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@media (max-width: 639px) {
|
|
173
|
+
.staySummary {
|
|
174
|
+
grid-column: 1 / -1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.price {
|
|
178
|
+
grid-row: 1;
|
|
179
|
+
}
|
|
161
180
|
}
|
|
162
181
|
|
|
163
182
|
.priceIncluded {
|
|
@@ -115,11 +115,6 @@ export function ReturnTimeSelector({
|
|
|
115
115
|
{t('calendar.spotsAvailable', { count: returnVacancies })}
|
|
116
116
|
</div>
|
|
117
117
|
))}
|
|
118
|
-
{staySummary && (
|
|
119
|
-
<div className={styles.staySummary}>
|
|
120
|
-
{staySummary}
|
|
121
|
-
</div>
|
|
122
|
-
)}
|
|
123
118
|
{isSoldOut && (
|
|
124
119
|
<div className={styles.soldOut}>
|
|
125
120
|
{t('booking.soldOut')}
|
|
@@ -142,6 +137,11 @@ export function ReturnTimeSelector({
|
|
|
142
137
|
`${formatCurrencyAmount(priceAdjustmentPerPerson, currency, locale as 'en' | 'fr')} per person`
|
|
143
138
|
)}
|
|
144
139
|
</div>
|
|
140
|
+
{staySummary && (
|
|
141
|
+
<div className={styles.staySummary}>
|
|
142
|
+
{staySummary}
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
145
|
</div>
|
|
146
146
|
</button>
|
|
147
147
|
);
|
|
@@ -14,6 +14,7 @@ import { ItineraryPlaceholder } from './ItineraryPlaceholder';
|
|
|
14
14
|
type TranslationFn = (key: string, params?: Record<string, string>) => string;
|
|
15
15
|
|
|
16
16
|
export interface StandardBookingItineraryPanelProps {
|
|
17
|
+
productId: string;
|
|
17
18
|
selectedDate: string;
|
|
18
19
|
hideItineraryBox: boolean;
|
|
19
20
|
activeOptions: ProductOption[];
|
|
@@ -42,6 +43,7 @@ export interface StandardBookingItineraryPanelProps {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
export function StandardBookingItineraryPanel({
|
|
46
|
+
productId,
|
|
45
47
|
selectedDate,
|
|
46
48
|
hideItineraryBox,
|
|
47
49
|
activeOptions,
|
|
@@ -104,6 +106,7 @@ export function StandardBookingItineraryPanel({
|
|
|
104
106
|
|
|
105
107
|
return (
|
|
106
108
|
<ItineraryBox
|
|
109
|
+
productId={productId}
|
|
107
110
|
selectedDate={selectedDate}
|
|
108
111
|
formattedDate={formattedDate}
|
|
109
112
|
itineraryItems={itineraryItems}
|
|
@@ -24,14 +24,22 @@
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
.row {
|
|
27
|
-
display:
|
|
27
|
+
display: grid;
|
|
28
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
29
|
+
grid-template-rows: auto auto;
|
|
28
30
|
align-items: center;
|
|
29
|
-
|
|
31
|
+
column-gap: 1rem;
|
|
30
32
|
padding: 1rem;
|
|
31
33
|
background: var(--booking-stone-50, #fafaf9);
|
|
32
34
|
border-radius: 0.5rem;
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
.summary {
|
|
38
|
+
grid-column: 1;
|
|
39
|
+
grid-row: 1;
|
|
40
|
+
min-width: 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
.category {
|
|
36
44
|
font-weight: 500;
|
|
37
45
|
color: var(--booking-stone-900, #1c1917);
|
|
@@ -50,11 +58,12 @@
|
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
.finePrint {
|
|
61
|
+
grid-column: 1 / -1;
|
|
62
|
+
grid-row: 2;
|
|
53
63
|
margin-top: 0.25rem;
|
|
54
64
|
font-size: 0.7rem;
|
|
55
65
|
line-height: 1.3;
|
|
56
66
|
color: var(--booking-stone-500, #78716c);
|
|
57
|
-
max-width: 16rem;
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
.priceStrikethrough {
|
|
@@ -68,11 +77,29 @@
|
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
.controls {
|
|
80
|
+
grid-column: 2;
|
|
81
|
+
grid-row: 1;
|
|
71
82
|
display: flex;
|
|
72
83
|
align-items: center;
|
|
73
84
|
gap: 0.75rem;
|
|
74
85
|
}
|
|
75
86
|
|
|
87
|
+
@media (max-width: 639px) {
|
|
88
|
+
.row {
|
|
89
|
+
row-gap: 0.75rem;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.finePrint {
|
|
93
|
+
margin-top: 0;
|
|
94
|
+
font-size: 0.75rem;
|
|
95
|
+
line-height: 1.375;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.controls {
|
|
99
|
+
grid-row: 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
76
103
|
.qtyBtn {
|
|
77
104
|
width: 2.5rem;
|
|
78
105
|
height: 2.5rem;
|
|
@@ -142,7 +142,7 @@ export function TicketSelector({
|
|
|
142
142
|
|
|
143
143
|
return (
|
|
144
144
|
<div key={rate.category} className={styles.row}>
|
|
145
|
-
<div>
|
|
145
|
+
<div className={styles.summary}>
|
|
146
146
|
<p className={styles.category}>
|
|
147
147
|
{rate.category}
|
|
148
148
|
{rate.category === 'CHILD' && (
|
|
@@ -169,11 +169,6 @@ export function TicketSelector({
|
|
|
169
169
|
formatCurrencyAmount(effectiveUnitPrice, currency, locale as 'en' | 'fr')
|
|
170
170
|
)}
|
|
171
171
|
</p>
|
|
172
|
-
{ticketFinePrintByCategory?.[rate.category] && (
|
|
173
|
-
<p className={styles.finePrint}>
|
|
174
|
-
{ticketFinePrintByCategory[rate.category]}
|
|
175
|
-
</p>
|
|
176
|
-
)}
|
|
177
172
|
</div>
|
|
178
173
|
<div className={styles.controls}>
|
|
179
174
|
<button
|
|
@@ -195,6 +190,11 @@ export function TicketSelector({
|
|
|
195
190
|
+
|
|
196
191
|
</button>
|
|
197
192
|
</div>
|
|
193
|
+
{ticketFinePrintByCategory?.[rate.category] && (
|
|
194
|
+
<p className={styles.finePrint}>
|
|
195
|
+
{ticketFinePrintByCategory[rate.category]}
|
|
196
|
+
</p>
|
|
197
|
+
)}
|
|
198
198
|
</div>
|
|
199
199
|
);
|
|
200
200
|
})}
|
|
@@ -15,7 +15,10 @@ import { BOOKING_LAUNCH_AT } from '../lib/booking-constants';
|
|
|
15
15
|
* forces the flow for local testing (e.g. localhost:3000/?booking_preview=partial).
|
|
16
16
|
*/
|
|
17
17
|
export function useIsBookingLaunchLive(): boolean {
|
|
18
|
-
|
|
18
|
+
// Match the server-rendered HTML to the current launch state. Starting with
|
|
19
|
+
// `false` caused expired pre-launch messaging to remain in crawlable HTML
|
|
20
|
+
// until JavaScript hydrated the page.
|
|
21
|
+
const [isLive, setIsLive] = useState(() => Date.now() >= BOOKING_LAUNCH_AT.getTime());
|
|
19
22
|
|
|
20
23
|
useEffect(() => {
|
|
21
24
|
// Dev override for local testing
|
|
@@ -199,7 +199,9 @@
|
|
|
199
199
|
"paxBooked": "{count} pax booked",
|
|
200
200
|
"paxProgress": "{booked}/{minimum} pax",
|
|
201
201
|
"minimumReached": "minimum reached!",
|
|
202
|
+
"minimumReachedShort": "minimum met",
|
|
202
203
|
"available": "Available",
|
|
204
|
+
"open": "Open",
|
|
203
205
|
"days": {
|
|
204
206
|
"sun": "SUN",
|
|
205
207
|
"mon": "MON",
|
|
@@ -199,7 +199,9 @@
|
|
|
199
199
|
"paxBooked": "{count} pax inscrits",
|
|
200
200
|
"paxProgress": "{booked}/{minimum} pax",
|
|
201
201
|
"minimumReached": "minimum atteint!",
|
|
202
|
+
"minimumReachedShort": "minimum atteint",
|
|
202
203
|
"available": "Disponible",
|
|
204
|
+
"open": "Ouvert",
|
|
203
205
|
"days": {
|
|
204
206
|
"sun": "DIM",
|
|
205
207
|
"mon": "LUN",
|
|
@@ -19,5 +19,5 @@ export const MINI_CALENDAR_END_DATE = new Date(2026, 9, 12); // October 12, 2026
|
|
|
19
19
|
// Buffer weeks before/after visible range
|
|
20
20
|
export const VISIBLE_RANGE_BUFFER_WEEKS = 2;
|
|
21
21
|
|
|
22
|
-
/** 8am MDT Mar 11,
|
|
22
|
+
/** 8am MDT Mar 11, 2026 – full booking flow becomes available at this moment (UTC). */
|
|
23
23
|
export const BOOKING_LAUNCH_AT = new Date('2026-03-11T14:00:00.000Z');
|