@ticketboothapp/booking 1.2.25-rc.0 → 1.2.27

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 (142) hide show
  1. package/package.json +11 -29
  2. package/src/components/booking/AddOnsSection.tsx +2 -2
  3. package/src/components/booking/AdminPaymentChoiceModal.tsx +1 -1
  4. package/src/components/booking/BookingDialog.tsx +31 -13
  5. package/src/components/booking/BookingFlow.tsx +32 -27
  6. package/src/components/booking/BookingFlowCollage.tsx +10 -6
  7. package/src/components/booking/BookingFlowPlaceholder.tsx +1 -1
  8. package/src/components/booking/BookingFlowPreview.tsx +18 -9
  9. package/src/components/booking/BookingProductGrid.tsx +55 -19
  10. package/src/components/booking/Calendar.module.css +19 -4
  11. package/src/components/booking/Calendar.tsx +13 -8
  12. package/src/components/booking/CancellationPolicySelector.tsx +2 -2
  13. package/src/components/booking/ChangeBookingDialog.tsx +22 -12
  14. package/src/components/booking/CheckoutForm.module.css +10 -0
  15. package/src/components/booking/CheckoutForm.tsx +10 -2
  16. package/src/components/booking/CheckoutModal.tsx +16 -14
  17. package/src/components/booking/DapFlowCollage.tsx +5 -2
  18. package/src/components/booking/DapTourDescription.tsx +4 -4
  19. package/src/components/booking/DependentAddOnBookingDialog.tsx +23 -16
  20. package/src/components/booking/DependentAddOnPaymentForm.tsx +10 -7
  21. package/src/components/booking/ItineraryBox.tsx +6 -6
  22. package/src/components/booking/ItineraryBuilder.tsx +1 -1
  23. package/src/components/booking/MealDrinkAddOnSelector.tsx +3 -3
  24. package/src/components/booking/PickupLocationSelector.tsx +20 -18
  25. package/src/components/booking/PickupTimeSelector.tsx +3 -3
  26. package/src/components/booking/PriceBreakdown.tsx +5 -5
  27. package/src/components/booking/PriceSummary.module.css +7 -0
  28. package/src/components/booking/PriceSummary.tsx +8 -7
  29. package/src/components/booking/PrivateShuttleBookingFlow.tsx +28 -19
  30. package/src/components/booking/PromoCodeInput.module.css +31 -25
  31. package/src/components/booking/PromoCodeInput.tsx +36 -24
  32. package/src/components/booking/ReturnTimeSelector.tsx +3 -3
  33. package/src/components/booking/TermsAcceptance.tsx +7 -2
  34. package/src/components/booking/TicketSelector.tsx +1 -1
  35. package/src/components/booking/TourDescription.tsx +11 -6
  36. package/src/components/booking/booking-flow.css +65 -4
  37. package/src/hooks/useBookingSourceMetadataFromLocation.ts +1 -1
  38. package/src/hooks/useIsBookingLaunchLive.ts +1 -1
  39. package/src/index.ts +26 -64
  40. package/src/providers/booking-dialog-provider.tsx +62 -53
  41. package/src/runtime/BookingHostContext.tsx +39 -0
  42. package/src/runtime/index.ts +13 -0
  43. package/src/runtime/types.ts +86 -0
  44. package/tsconfig.json +3 -5
  45. package/src/assets/icons/minus.svg +0 -7
  46. package/src/assets/icons/partner-logos/getyourguide.svg +0 -8
  47. package/src/assets/icons/plus.svg +0 -3
  48. package/src/colours.css +0 -23
  49. package/src/components/BookingDetails.module.css +0 -1591
  50. package/src/components/BookingDetails.tsx +0 -2264
  51. package/src/components/BookingWidget.tsx +0 -302
  52. package/src/components/ManageBookingView.tsx +0 -437
  53. package/src/components/PhoneInputWithCountry.module.css +0 -131
  54. package/src/components/PhoneInputWithCountry.tsx +0 -44
  55. package/src/components/PickupLocationDialog.module.css +0 -360
  56. package/src/components/PickupLocationDialog.tsx +0 -357
  57. package/src/components/PostBookingDependentAddOnUpsell.module.css +0 -174
  58. package/src/components/PostBookingDependentAddOnUpsell.tsx +0 -407
  59. package/src/components/button.css +0 -245
  60. package/src/components/button.tsx +0 -152
  61. package/src/components/colorable-svg.tsx +0 -29
  62. package/src/components/image.css +0 -29
  63. package/src/components/image.tsx +0 -113
  64. package/src/components/partner/PartnerBookingPage.module.css +0 -130
  65. package/src/components/partner/PartnerBookingPage.tsx +0 -390
  66. package/src/components/partner/PartnerBookingPageWithBrowserMetadata.tsx +0 -45
  67. package/src/components/product-tag.module.css +0 -30
  68. package/src/components/product-tag.tsx +0 -34
  69. package/src/components/product-theme-pages/image-modal.tsx +0 -248
  70. package/src/components/product-theme-pages/photo-gallery.module.css +0 -200
  71. package/src/components/terms/TermsContent.tsx +0 -178
  72. package/src/components/value-pill.module.css +0 -59
  73. package/src/components/value-pill.tsx +0 -46
  74. package/src/constants/images.ts +0 -556
  75. package/src/constants/pill-values.ts +0 -210
  76. package/src/constants/products.ts +0 -155
  77. package/src/contexts/AvailabilitiesCacheContext.tsx +0 -125
  78. package/src/contexts/CompanyContext.tsx +0 -70
  79. package/src/data/dap-descriptions/session-couples-families-friends.en.json +0 -61
  80. package/src/data/dap-descriptions/session-elopements.en.json +0 -60
  81. package/src/data/dap-descriptions/session-proposals.en.json +0 -60
  82. package/src/data/product-descriptions/afternoon-delight.en.json +0 -35
  83. package/src/data/product-descriptions/emerald-lake-escape.en.json +0 -68
  84. package/src/data/product-descriptions/lake-louise-adventure.en.json +0 -74
  85. package/src/data/product-descriptions/moraine-lake-adventure.en.json +0 -78
  86. package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +0 -65
  87. package/src/data/product-descriptions/moraine-lake-sunrise.en.json +0 -64
  88. package/src/data/product-descriptions/private-tour.en.json +0 -80
  89. package/src/data/product-descriptions/two-lakes-combo.en.json +0 -65
  90. package/src/data/products-config.json +0 -101
  91. package/src/lib/analytics.ts +0 -197
  92. package/src/lib/booking/booking-source.ts +0 -51
  93. package/src/lib/booking/checkout-breakdown.ts +0 -69
  94. package/src/lib/booking/correlation-id.ts +0 -46
  95. package/src/lib/booking/i18n/config.ts +0 -21
  96. package/src/lib/booking/i18n/index.tsx +0 -144
  97. package/src/lib/booking/i18n/messages/en.json +0 -236
  98. package/src/lib/booking/i18n/messages/fr.json +0 -236
  99. package/src/lib/booking/itinerary-display.ts +0 -36
  100. package/src/lib/booking/itinerary-labels.ts +0 -70
  101. package/src/lib/booking/location-calculations.ts +0 -43
  102. package/src/lib/booking/location-utils.ts +0 -165
  103. package/src/lib/booking/map-utils.ts +0 -153
  104. package/src/lib/booking/marker-icons.ts +0 -113
  105. package/src/lib/booking/normalize-booking-product-id.ts +0 -21
  106. package/src/lib/booking/pickup-location-types.ts +0 -25
  107. package/src/lib/booking/places-api.ts +0 -154
  108. package/src/lib/booking/pricing.ts +0 -466
  109. package/src/lib/booking/product-option-id.ts +0 -35
  110. package/src/lib/booking/source-metadata.ts +0 -226
  111. package/src/lib/booking/sunday-week.ts +0 -14
  112. package/src/lib/booking/theme.ts +0 -83
  113. package/src/lib/booking/trace-context.ts +0 -62
  114. package/src/lib/booking/utils.ts +0 -9
  115. package/src/lib/booking-api.ts +0 -1793
  116. package/src/lib/booking-constants.ts +0 -23
  117. package/src/lib/booking-ref.ts +0 -13
  118. package/src/lib/booking-types.ts +0 -36
  119. package/src/lib/currency.ts +0 -81
  120. package/src/lib/dap-descriptions.ts +0 -50
  121. package/src/lib/dap-itinerary-preview.ts +0 -315
  122. package/src/lib/dependent-add-on-api.ts +0 -434
  123. package/src/lib/env.ts +0 -96
  124. package/src/lib/firebase.ts +0 -20
  125. package/src/lib/job-application-api.ts +0 -83
  126. package/src/lib/manage-booking-embed-print.ts +0 -16
  127. package/src/lib/manage-booking-post-checkout.ts +0 -68
  128. package/src/lib/photo-dap-config.ts +0 -228
  129. package/src/lib/photo-packages.ts +0 -75
  130. package/src/lib/pickup/map-utils.ts +0 -56
  131. package/src/lib/pickup/marker-icons.ts +0 -19
  132. package/src/lib/product-descriptions.ts +0 -66
  133. package/src/lib/products-config.ts +0 -73
  134. package/src/providers/dependent-add-on-dialog-provider.tsx +0 -105
  135. package/src/radius.css +0 -5
  136. package/src/spacing.css +0 -7
  137. package/src/strings/en.json +0 -1774
  138. package/src/strings/es.json +0 -1573
  139. package/src/strings/fr.json +0 -1573
  140. package/src/strings/index.js +0 -23
  141. package/src/text-style.css +0 -56
  142. package/src/utils/currency-converter.ts +0 -101
@@ -1,174 +0,0 @@
1
- /* Compact photo-session-style cards (aligned with photo-products-section.module.css) */
2
-
3
- .section {
4
- background-color: var(--primary-background);
5
- border-radius: 24px;
6
- padding: var(--spacing-large);
7
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
8
- border: 1px solid rgba(0, 0, 0, 0.06);
9
- }
10
-
11
- .header {
12
- margin-bottom: var(--spacing-medium);
13
- }
14
-
15
- .title {
16
- font-family: 'Poppins', sans-serif;
17
- font-weight: 700;
18
- font-size: 1.25rem;
19
- color: var(--primary-text);
20
- margin: 0 0 0.5rem 0;
21
- line-height: 1.3;
22
- }
23
-
24
- .subtitle {
25
- margin: 0;
26
- font-size: 0.9375rem;
27
- line-height: 1.5;
28
- color: var(--grey-text);
29
- }
30
-
31
- .cardGrid {
32
- display: grid;
33
- grid-template-columns: repeat(auto-fill, minmax(158px, 1fr));
34
- gap: 0.875rem;
35
- }
36
-
37
- @media (min-width: 480px) {
38
- .cardGrid {
39
- grid-template-columns: repeat(3, 1fr);
40
- gap: 1rem;
41
- }
42
- }
43
-
44
- .card {
45
- height: 100%;
46
- width: 100%;
47
- display: block;
48
- position: relative;
49
- cursor: pointer;
50
- border: none;
51
- background: transparent;
52
- padding: 0;
53
- font: inherit;
54
- color: inherit;
55
- text-align: inherit;
56
- border-radius: 10px;
57
- overflow: hidden;
58
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
59
- transition: box-shadow 0.35s ease, transform 0.35s ease;
60
- }
61
-
62
- .card:focus-visible {
63
- outline: 2px solid var(--accent-turquoise, #0d9488);
64
- outline-offset: 2px;
65
- }
66
-
67
- .card:hover {
68
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
69
- transform: translateY(-2px);
70
- }
71
-
72
- .imageWrap {
73
- width: 100%;
74
- height: 200px;
75
- position: relative;
76
- border-radius: 10px;
77
- overflow: hidden;
78
- aspect-ratio: 16 / 9;
79
- }
80
-
81
- .cardImage {
82
- width: 100%;
83
- height: 100%;
84
- position: absolute;
85
- top: 0;
86
- left: 0;
87
- transition: transform 0.5s ease;
88
- will-change: transform;
89
- }
90
-
91
- .cardImage::after {
92
- content: '';
93
- position: absolute;
94
- bottom: 0;
95
- left: 0;
96
- right: 0;
97
- height: 30%;
98
- background: linear-gradient(to top, rgba(0, 0, 0, 0.55), transparent);
99
- z-index: 1;
100
- }
101
-
102
- @media (min-width: 1024px) {
103
- .card:hover .cardImage {
104
- transform: scale(1.08);
105
- }
106
- }
107
-
108
- @media (prefers-reduced-motion: reduce) {
109
- .card {
110
- transition: none;
111
- }
112
-
113
- .card:hover {
114
- transform: none;
115
- }
116
-
117
- .cardImage {
118
- transition: none;
119
- }
120
-
121
- .card:hover .cardImage {
122
- transform: none;
123
- }
124
- }
125
-
126
- .cardTitle {
127
- position: absolute;
128
- top: 0;
129
- left: 0;
130
- right: 0;
131
- padding: 0.5rem 0.45rem;
132
- color: white;
133
- text-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
134
- font-family: 'Poppins', sans-serif;
135
- text-transform: lowercase;
136
- font-size: 0.95rem;
137
- font-weight: 700;
138
- z-index: 2;
139
- line-height: 1.15;
140
- text-align: center;
141
- }
142
-
143
- @media (min-width: 480px) {
144
- .cardTitle {
145
- font-size: 1rem;
146
- }
147
- }
148
-
149
- .cardInfo {
150
- position: absolute;
151
- bottom: 0;
152
- left: 0;
153
- z-index: 2;
154
- padding: 0.45rem 0.5rem;
155
- }
156
-
157
- .infoLine {
158
- color: var(--accent-white, #fff);
159
- font-family: 'Figtree', sans-serif;
160
- font-weight: 700;
161
- font-size: 0.7rem;
162
- line-height: 1.15;
163
- margin: 0 0 0.2rem 0;
164
- }
165
-
166
- .infoLine:last-child {
167
- margin-bottom: 0;
168
- }
169
-
170
- @media (min-width: 480px) {
171
- .infoLine {
172
- font-size: 0.75rem;
173
- }
174
- }
@@ -1,407 +0,0 @@
1
- 'use client';
2
-
3
- import { useCallback, useEffect, useMemo, useState } from 'react';
4
- import { motion, useReducedMotion } from 'framer-motion';
5
- import {
6
- PHOTO_DAP_SLUGS,
7
- getPhotoDapCatalog,
8
- type PhotoDapCatalog,
9
- type PhotoDapSlug,
10
- } from '@/lib/photo-dap-config';
11
- import {
12
- PHOTO_PACKAGE_SECTIONS,
13
- type PhotoPackage,
14
- } from '@/lib/photo-packages';
15
- import { getDependentAddOnBookingUpsellEligibility } from '@/lib/dependent-add-on-api';
16
- import { ENV } from '@/lib/env';
17
- import {
18
- useDependentAddOnDialog,
19
- type DependentAddOnDialogOpenPayload,
20
- } from '@/providers/dependent-add-on-dialog-provider';
21
- import type { BookingData } from '@/components/BookingDetails';
22
- import ViaViaImage from '@/components/image';
23
- import styles from './PostBookingDependentAddOnUpsell.module.css';
24
-
25
- const easeOut = [0.22, 1, 0.36, 1] as const;
26
-
27
- /** Session cache TTL — balances fewer round-trips vs slot inventory freshness. */
28
- const DAP_UPSELL_PROBE_TTL_MS = 5 * 60 * 1000;
29
- const DAP_UPSELL_PROBE_STORAGE_PREFIX = 'vvia:dapUpsellProbe:v2:';
30
-
31
- type DapUpsellEntry = { slug: PhotoDapSlug; optionId?: string };
32
-
33
- function isAbortError(e: unknown): boolean {
34
- if (e instanceof Error && e.name === 'AbortError') return true;
35
- return (
36
- typeof DOMException !== 'undefined' &&
37
- e instanceof DOMException &&
38
- e.name === 'AbortError'
39
- );
40
- }
41
-
42
- function isPhotoDapSlug(s: string): s is PhotoDapSlug {
43
- return (PHOTO_DAP_SLUGS as readonly string[]).includes(s);
44
- }
45
-
46
- function dapUpsellProbeCacheKey(
47
- companyId: string,
48
- ref: string,
49
- ln: string,
50
- bookingVersionStamp: string
51
- ): string {
52
- return `${DAP_UPSELL_PROBE_STORAGE_PREFIX}${companyId}:${encodeURIComponent(ref)}:${encodeURIComponent(ln)}:${encodeURIComponent(bookingVersionStamp)}`;
53
- }
54
-
55
- /** `undefined` = cache miss; `[]` = cached “no packages with slots”. */
56
- function readDapUpsellProbeCache(key: string): DapUpsellEntry[] | undefined {
57
- if (typeof sessionStorage === 'undefined') return undefined;
58
- try {
59
- const raw = sessionStorage.getItem(key);
60
- if (!raw) return undefined;
61
- const parsed = JSON.parse(raw) as {
62
- entries?: unknown;
63
- slugs?: unknown;
64
- savedAt?: unknown;
65
- };
66
- if (typeof parsed.savedAt !== 'number') {
67
- sessionStorage.removeItem(key);
68
- return undefined;
69
- }
70
- if (Date.now() - parsed.savedAt > DAP_UPSELL_PROBE_TTL_MS) {
71
- sessionStorage.removeItem(key);
72
- return undefined;
73
- }
74
- if (Array.isArray(parsed.entries)) {
75
- const out: DapUpsellEntry[] = [];
76
- for (const row of parsed.entries) {
77
- if (!row || typeof row !== 'object') continue;
78
- const o = row as Record<string, unknown>;
79
- const slug = o.slug;
80
- const optionId = o.optionId;
81
- if (typeof slug === 'string' && isPhotoDapSlug(slug)) {
82
- out.push({
83
- slug,
84
- ...(typeof optionId === 'string' && optionId.trim()
85
- ? { optionId: optionId.trim() }
86
- : {}),
87
- });
88
- }
89
- }
90
- return out;
91
- }
92
- if (Array.isArray(parsed.slugs)) {
93
- const out: DapUpsellEntry[] = [];
94
- for (const s of parsed.slugs) {
95
- if (typeof s === 'string' && isPhotoDapSlug(s)) out.push({ slug: s });
96
- }
97
- return out;
98
- }
99
- sessionStorage.removeItem(key);
100
- return undefined;
101
- } catch {
102
- return undefined;
103
- }
104
- }
105
-
106
- function writeDapUpsellProbeCache(key: string, entries: DapUpsellEntry[]): void {
107
- if (typeof sessionStorage === 'undefined') return;
108
- try {
109
- sessionStorage.setItem(key, JSON.stringify({ entries, savedAt: Date.now() }));
110
- } catch {
111
- /* quota / private mode */
112
- }
113
- }
114
-
115
- function findPhotoPackageByDapSlug(slug: PhotoDapSlug): PhotoPackage | undefined {
116
- for (const section of PHOTO_PACKAGE_SECTIONS) {
117
- const p = section.photoPackages.find((pkg) => pkg.dapSlug === slug);
118
- if (p) return p;
119
- }
120
- return undefined;
121
- }
122
-
123
- function buildOpenPayload(
124
- slug: PhotoDapSlug,
125
- photoPackage: PhotoPackage,
126
- catalog: PhotoDapCatalog,
127
- initialPrimaryBookingReference: string,
128
- initialPrimaryBookingLastName?: string,
129
- /** Option TicketBooth used when probing slots — pre-select only; must not set fixed option if user can pick lengths. */
130
- upsellProbedOptionId?: string
131
- ): DependentAddOnDialogOpenPayload {
132
- const fixedFromCatalog = catalog.dependentAddOnProductOptionId?.trim();
133
- const probed = upsellProbedOptionId?.trim();
134
- const multiSessionLengths = (catalog.productOptions?.length ?? 0) > 1;
135
- const probedMatchesOption =
136
- Boolean(probed) &&
137
- catalog.productOptions?.some((o) => o.dependentAddOnProductOptionId === probed);
138
- /** Upsell must not set `dependentAddOnProductOptionId` when multiple lengths exist — that hides the session picker. */
139
- const initialSelectedFromUpsell =
140
- !fixedFromCatalog && multiSessionLengths && probedMatchesOption ? probed : undefined;
141
-
142
- return {
143
- productDisplayTitle: photoPackage.name,
144
- dependentAddOnProductId: catalog.dependentAddOnProductId,
145
- cancellationDaysBeforeSession: catalog.cancellationDaysBeforeSession,
146
- collageImageIds:
147
- catalog.collageImageIds?.length > 0
148
- ? catalog.collageImageIds
149
- : photoPackage.images.map((img) => img.id),
150
- dapDescriptionSlug: slug,
151
- initialPrimaryBookingReference,
152
- initialPrimaryBookingLastName,
153
- ...(fixedFromCatalog ? { dependentAddOnProductOptionId: fixedFromCatalog } : {}),
154
- ...(initialSelectedFromUpsell
155
- ? { initialSelectedProductOptionId: initialSelectedFromUpsell }
156
- : {}),
157
- ...(catalog.productOptions?.length
158
- ? {
159
- productOptions: catalog.productOptions.map((o) => ({
160
- dependentAddOnProductOptionId: o.dependentAddOnProductOptionId,
161
- label: o.label,
162
- photosLabel: o.photosLabel,
163
- startingAtLabel: o.startingAtLabel,
164
- })),
165
- }
166
- : {}),
167
- };
168
- }
169
-
170
- /** Map aggregate API rows to photo slugs in `PHOTO_DAP_SLUGS` order (env-resolved product ids). */
171
- function upsellEntriesFromProductsWithSlots(
172
- productsWithSlots: { dependentAddOnProductId: string; dependentAddOnProductOptionId?: string }[]
173
- ): DapUpsellEntry[] {
174
- const byProductId = new Map(
175
- productsWithSlots.map((p) => [p.dependentAddOnProductId, p] as const)
176
- );
177
- const ordered: DapUpsellEntry[] = [];
178
- for (const slug of PHOTO_DAP_SLUGS) {
179
- const catalog = getPhotoDapCatalog(slug);
180
- if (!catalog) continue;
181
- const row = byProductId.get(catalog.dependentAddOnProductId);
182
- if (!row) continue;
183
- const opt = row.dependentAddOnProductOptionId?.trim();
184
- ordered.push({
185
- slug,
186
- ...(opt ? { optionId: opt } : {}),
187
- });
188
- }
189
- return ordered;
190
- }
191
-
192
- export function PostBookingDependentAddOnUpsell({
193
- booking,
194
- enabled,
195
- }: {
196
- booking: BookingData;
197
- enabled: boolean;
198
- }) {
199
- const { open: openDapDialog } = useDependentAddOnDialog();
200
- const reduceMotion = useReducedMotion();
201
- const [checking, setChecking] = useState(true);
202
- const [availableEntries, setAvailableEntries] = useState<DapUpsellEntry[]>([]);
203
-
204
- const primaryRefForApi = useMemo(
205
- () => booking.bookingReference.trim(),
206
- [booking.bookingReference]
207
- );
208
- const primaryLastNameForApi = useMemo(
209
- () => booking.customer?.lastName?.trim() || '',
210
- [booking.customer?.lastName]
211
- );
212
-
213
- /** Re-run probe when booking payload changes in ways that can affect slot eligibility. */
214
- const bookingProbeStamp = useMemo(
215
- () =>
216
- [
217
- booking.bookingReference.trim(),
218
- booking.customer?.lastName?.trim() ?? '',
219
- String(booking.dependentAddOnBookings?.length ?? 0),
220
- booking.updatedAt ?? '',
221
- booking.dateTime ?? '',
222
- ].join('|'),
223
- [
224
- booking.bookingReference,
225
- booking.customer?.lastName,
226
- booking.dependentAddOnBookings?.length,
227
- booking.updatedAt,
228
- booking.dateTime,
229
- ]
230
- );
231
-
232
- const skip =
233
- !enabled ||
234
- /** Partner portal / embed: never probe slots (no upsell UI); avoids unnecessary API calls. */
235
- (typeof window !== 'undefined' && window.parent !== window.self) ||
236
- (booking.gygBookingReference ?? '').trim().length > 0 ||
237
- ['CANCELLED', 'CANCELED'].includes((booking.status ?? '').toUpperCase()) ||
238
- (booking.dependentAddOnBookings?.length ?? 0) > 0;
239
-
240
- useEffect(() => {
241
- if (skip) {
242
- setChecking(false);
243
- setAvailableEntries([]);
244
- return;
245
- }
246
-
247
- const ac = new AbortController();
248
- const { signal } = ac;
249
- let cancelled = false;
250
-
251
- const cacheKey = dapUpsellProbeCacheKey(
252
- ENV.COMPANY_ID,
253
- primaryRefForApi,
254
- primaryLastNameForApi,
255
- bookingProbeStamp
256
- );
257
-
258
- const run = async () => {
259
- const cached = readDapUpsellProbeCache(cacheKey);
260
- if (cached !== undefined) {
261
- if (!cancelled && !signal.aborted) {
262
- setAvailableEntries(cached);
263
- setChecking(false);
264
- }
265
- return;
266
- }
267
-
268
- if (!cancelled && !signal.aborted) {
269
- setChecking(true);
270
- setAvailableEntries([]);
271
- }
272
-
273
- try {
274
- if (!primaryLastNameForApi) {
275
- if (!cancelled && !signal.aborted) {
276
- writeDapUpsellProbeCache(cacheKey, []);
277
- setAvailableEntries([]);
278
- }
279
- } else {
280
- const { productsWithSlots } = await getDependentAddOnBookingUpsellEligibility({
281
- companyId: ENV.COMPANY_ID,
282
- primaryBookingReference: primaryRefForApi,
283
- lastName: primaryLastNameForApi,
284
- signal,
285
- });
286
- if (cancelled || signal.aborted) return;
287
- const ordered = upsellEntriesFromProductsWithSlots(productsWithSlots);
288
- writeDapUpsellProbeCache(cacheKey, ordered);
289
- setAvailableEntries(ordered);
290
- }
291
- } catch (e) {
292
- if (isAbortError(e) || cancelled || signal.aborted) return;
293
- setAvailableEntries([]);
294
- } finally {
295
- if (!cancelled && !signal.aborted) {
296
- setChecking(false);
297
- }
298
- }
299
- };
300
-
301
- void run();
302
-
303
- return () => {
304
- cancelled = true;
305
- ac.abort();
306
- };
307
- }, [skip, bookingProbeStamp, primaryRefForApi, primaryLastNameForApi]);
308
-
309
- const handleOpen = useCallback(
310
- (slug: PhotoDapSlug, upsellProbedOptionId?: string) => {
311
- const catalog = getPhotoDapCatalog(slug);
312
- const photoPackage = findPhotoPackageByDapSlug(slug);
313
- if (!catalog || !photoPackage) return;
314
- openDapDialog(
315
- buildOpenPayload(
316
- slug,
317
- photoPackage,
318
- catalog,
319
- primaryRefForApi,
320
- primaryLastNameForApi || undefined,
321
- upsellProbedOptionId
322
- )
323
- );
324
- },
325
- [primaryRefForApi, primaryLastNameForApi, openDapDialog]
326
- );
327
-
328
- if (skip || checking || availableEntries.length === 0) return null;
329
-
330
- const instant = reduceMotion
331
- ? { duration: 0.01 }
332
- : { duration: 0.48, ease: easeOut };
333
- const headerMotion = reduceMotion
334
- ? { duration: 0.01 }
335
- : { duration: 0.4, delay: 0.06, ease: easeOut };
336
- const cardMotion = (index: number) =>
337
- reduceMotion
338
- ? { duration: 0.01 }
339
- : { duration: 0.44, delay: 0.12 + index * 0.09, ease: easeOut };
340
-
341
- const shellEnter = reduceMotion ? { opacity: 0, y: 0 } : { opacity: 0, y: 20 };
342
-
343
- const content = (
344
- <>
345
- <motion.div
346
- className={styles.header}
347
- initial={reduceMotion ? false : { opacity: 0, y: 12 }}
348
- animate={{ opacity: 1, y: 0 }}
349
- transition={headerMotion}
350
- >
351
- <h2 id="post-booking-dap-upsell-title" className={styles.title}>
352
- Interested in adding a professional photography session to your tour?
353
- </h2>
354
- <p className={styles.subtitle}>
355
- Photo sessions are available as an add-on for your shuttle date. Tap a package to pick a time and
356
- add to your booking.
357
- </p>
358
- </motion.div>
359
-
360
- <div className={styles.cardGrid}>
361
- {availableEntries.map(({ slug, optionId }, index) => {
362
- const pkg = findPhotoPackageByDapSlug(slug);
363
- if (!pkg) return null;
364
- const hero = pkg.images[0];
365
- return (
366
- <motion.button
367
- key={slug}
368
- type="button"
369
- className={styles.card}
370
- onClick={() => handleOpen(slug, optionId)}
371
- initial={reduceMotion ? false : { opacity: 0, y: 22, scale: 0.96 }}
372
- animate={{ opacity: 1, y: 0, scale: 1 }}
373
- transition={cardMotion(index)}
374
- >
375
- <div className={styles.imageWrap}>
376
- <ViaViaImage
377
- className={styles.cardImage}
378
- imageId={hero.id}
379
- alt={hero.alt}
380
- context="GALLERY"
381
- />
382
- <h3 className={styles.cardTitle}>{pkg.name}</h3>
383
- <div className={styles.cardInfo}>
384
- <p className={styles.infoLine}>{pkg.startingPrice}</p>
385
- <p className={styles.infoLine}>{pkg.duration}</p>
386
- <p className={styles.infoLine}>{pkg.quantity}</p>
387
- </div>
388
- </div>
389
- </motion.button>
390
- );
391
- })}
392
- </div>
393
- </>
394
- );
395
-
396
- return (
397
- <motion.div
398
- className={styles.section}
399
- aria-labelledby="post-booking-dap-upsell-title"
400
- initial={reduceMotion ? false : shellEnter}
401
- animate={{ opacity: 1, y: 0 }}
402
- transition={instant}
403
- >
404
- {content}
405
- </motion.div>
406
- );
407
- }