@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.
Files changed (27) hide show
  1. package/package.json +1 -1
  2. package/src/components/booking/AddOnsSection.module.css +124 -0
  3. package/src/components/booking/AddOnsSection.tsx +17 -21
  4. package/src/components/booking/AdminChangeBookingContent.tsx +1 -0
  5. package/src/components/booking/AdminChangeBookingFlow.tsx +1 -0
  6. package/src/components/booking/Calendar.module.css +124 -10
  7. package/src/components/booking/Calendar.tsx +46 -20
  8. package/src/components/booking/CancellationPolicySelector.module.css +33 -4
  9. package/src/components/booking/CancellationPolicySelector.tsx +30 -28
  10. package/src/components/booking/ChangeBookingFlow.tsx +1 -0
  11. package/src/components/booking/ChangeBookingItineraryPanel.tsx +3 -0
  12. package/src/components/booking/CollapsibleAddOnItem.module.css +1 -1
  13. package/src/components/booking/ItineraryBox.module.css +14 -0
  14. package/src/components/booking/ItineraryBox.tsx +75 -7
  15. package/src/components/booking/MealDrinkAddOnSelector.tsx +10 -12
  16. package/src/components/booking/NewBookingFlow.tsx +1 -0
  17. package/src/components/booking/PrivateShuttlePassengerSection.tsx +7 -1
  18. package/src/components/booking/ReturnTimeSelector.module.css +23 -4
  19. package/src/components/booking/ReturnTimeSelector.tsx +5 -5
  20. package/src/components/booking/StandardBookingItineraryPanel.tsx +3 -0
  21. package/src/components/booking/TicketSelector.module.css +30 -3
  22. package/src/components/booking/TicketSelector.tsx +6 -6
  23. package/src/hooks/useIsBookingLaunchLive.ts +4 -1
  24. package/src/lib/booking/i18n/messages/en.json +2 -0
  25. package/src/lib/booking/i18n/messages/fr.json +2 -0
  26. package/src/lib/booking-constants.ts +1 -1
  27. 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
- <div className={styles.policyContent}>
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
- <span className={`${styles.fee} ${styles.feeFree}`}>
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
- <div className={styles.policyContent}>
125
- <span className={styles.policyLabel}>{policy.label}</span>
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} mt-1 block`}>
142
+ <span className={`${styles.refundTiersText} ${styles.rowSubtitle}`}>
134
143
  {rowSubtitle}
135
144
  </span>
136
145
  )}
137
146
  </div>
138
- </div>
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
  })}
@@ -893,6 +893,7 @@ export function ChangeBookingFlow({
893
893
  >
894
894
  <div className="mt-6 space-y-6">
895
895
  <ChangeBookingItineraryPanel
896
+ productId={product.productId}
896
897
  selectedDate={selectedDate}
897
898
  hideItineraryBox={hideItineraryBox}
898
899
  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}
@@ -34,7 +34,7 @@
34
34
  flex: 1;
35
35
  font-family: 'Figtree', var(--booking-font-sans, ui-sans-serif), sans-serif !important;
36
36
  font-size: 1rem;
37
- font-weight: 700;
37
+ font-weight: 500;
38
38
  line-height: 1.35;
39
39
  color: var(--booking-stone-900, #1c1917) !important;
40
40
  }
@@ -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={() => {
@@ -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 className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
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: flex;
103
+ display: grid;
104
+ grid-template-columns: minmax(0, 1fr) auto;
105
+ grid-template-rows: auto auto;
104
106
  align-items: center;
105
- justify-content: space-between;
107
+ column-gap: 1rem;
106
108
  }
107
109
 
108
110
  .details {
109
- flex: 1;
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
- margin-left: 1rem;
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: flex;
27
+ display: grid;
28
+ grid-template-columns: minmax(0, 1fr) auto;
29
+ grid-template-rows: auto auto;
28
30
  align-items: center;
29
- justify-content: space-between;
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
- const [isLive, setIsLive] = useState(false);
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, 2025 – full booking flow becomes available at this moment (UTC). */
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');