@ticketboothapp/booking 0.1.23 → 1.2.24

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 (158) hide show
  1. package/package.json +2 -29
  2. package/src/index.ts +0 -79
  3. package/tsconfig.json +2 -8
  4. package/src/assets/icons/minus.svg +0 -7
  5. package/src/assets/icons/partner-logos/getyourguide.svg +0 -8
  6. package/src/assets/icons/plus.svg +0 -3
  7. package/src/colours.css +0 -23
  8. package/src/components/BookingDetails.module.css +0 -1591
  9. package/src/components/BookingDetails.tsx +0 -2264
  10. package/src/components/BookingWidget.tsx +0 -302
  11. package/src/components/ManageBookingView.tsx +0 -437
  12. package/src/components/PhoneInputWithCountry.module.css +0 -131
  13. package/src/components/PhoneInputWithCountry.tsx +0 -44
  14. package/src/components/PickupLocationDialog.module.css +0 -360
  15. package/src/components/PickupLocationDialog.tsx +0 -357
  16. package/src/components/PostBookingDependentAddOnUpsell.module.css +0 -174
  17. package/src/components/PostBookingDependentAddOnUpsell.tsx +0 -407
  18. package/src/components/booking/AddOnsSection.module.css +0 -10
  19. package/src/components/booking/AddOnsSection.tsx +0 -184
  20. package/src/components/booking/AdminPaymentChoiceModal.tsx +0 -98
  21. package/src/components/booking/BookingDialog.module.css +0 -643
  22. package/src/components/booking/BookingDialog.tsx +0 -356
  23. package/src/components/booking/BookingFlow.tsx +0 -4385
  24. package/src/components/booking/BookingFlowCollage.module.css +0 -148
  25. package/src/components/booking/BookingFlowCollage.tsx +0 -184
  26. package/src/components/booking/BookingFlowPlaceholder.module.css +0 -27
  27. package/src/components/booking/BookingFlowPlaceholder.tsx +0 -25
  28. package/src/components/booking/BookingFlowPreview.tsx +0 -51
  29. package/src/components/booking/BookingProductGrid.module.css +0 -359
  30. package/src/components/booking/BookingProductGrid.tsx +0 -497
  31. package/src/components/booking/Calendar.module.css +0 -616
  32. package/src/components/booking/Calendar.tsx +0 -1123
  33. package/src/components/booking/CancellationPolicySelector.module.css +0 -124
  34. package/src/components/booking/CancellationPolicySelector.tsx +0 -142
  35. package/src/components/booking/ChangeBookingDialog.tsx +0 -562
  36. package/src/components/booking/CheckoutForm.module.css +0 -244
  37. package/src/components/booking/CheckoutForm.tsx +0 -364
  38. package/src/components/booking/CheckoutModal.tsx +0 -451
  39. package/src/components/booking/CurrencySwitcher.tsx +0 -81
  40. package/src/components/booking/DapFlowCollage.tsx +0 -88
  41. package/src/components/booking/DapTourDescription.tsx +0 -35
  42. package/src/components/booking/DependentAddOnBookingDialog.tsx +0 -1350
  43. package/src/components/booking/DependentAddOnPaymentForm.tsx +0 -124
  44. package/src/components/booking/ErrorBoundary.tsx +0 -63
  45. package/src/components/booking/InfoTooltip.tsx +0 -108
  46. package/src/components/booking/ItineraryBox.module.css +0 -258
  47. package/src/components/booking/ItineraryBox.tsx +0 -550
  48. package/src/components/booking/ItineraryBuilder.tsx +0 -82
  49. package/src/components/booking/ItineraryPlaceholder.module.css +0 -45
  50. package/src/components/booking/ItineraryPlaceholder.tsx +0 -26
  51. package/src/components/booking/MealDrinkAddOnSelector.tsx +0 -338
  52. package/src/components/booking/PickupLocationSelector.module.css +0 -124
  53. package/src/components/booking/PickupLocationSelector.tsx +0 -1566
  54. package/src/components/booking/PickupTimeSelector.module.css +0 -134
  55. package/src/components/booking/PickupTimeSelector.tsx +0 -112
  56. package/src/components/booking/PriceBreakdown.tsx +0 -154
  57. package/src/components/booking/PriceSummary.tsx +0 -234
  58. package/src/components/booking/PrivateShuttleBookingFlow.module.css +0 -357
  59. package/src/components/booking/PrivateShuttleBookingFlow.tsx +0 -2662
  60. package/src/components/booking/PromoCodeInput.module.css +0 -166
  61. package/src/components/booking/PromoCodeInput.tsx +0 -99
  62. package/src/components/booking/ReturnTimeSelector.module.css +0 -173
  63. package/src/components/booking/ReturnTimeSelector.tsx +0 -145
  64. package/src/components/booking/TermsAcceptance.tsx +0 -111
  65. package/src/components/booking/TicketSelector.module.css +0 -164
  66. package/src/components/booking/TicketSelector.tsx +0 -199
  67. package/src/components/booking/TourDescription.module.css +0 -304
  68. package/src/components/booking/TourDescription.tsx +0 -273
  69. package/src/components/booking/booking-flow-ui.ts +0 -38
  70. package/src/components/booking/booking-flow.css +0 -944
  71. package/src/components/button.css +0 -245
  72. package/src/components/button.tsx +0 -152
  73. package/src/components/colorable-svg.tsx +0 -29
  74. package/src/components/image.css +0 -29
  75. package/src/components/image.tsx +0 -113
  76. package/src/components/partner/PartnerBookingPage.module.css +0 -130
  77. package/src/components/partner/PartnerBookingPage.tsx +0 -390
  78. package/src/components/partner/PartnerBookingPageWithBrowserMetadata.tsx +0 -45
  79. package/src/components/product-tag.module.css +0 -30
  80. package/src/components/product-tag.tsx +0 -34
  81. package/src/components/product-theme-pages/image-modal.tsx +0 -248
  82. package/src/components/product-theme-pages/photo-gallery.module.css +0 -200
  83. package/src/components/terms/TermsContent.tsx +0 -178
  84. package/src/components/value-pill.module.css +0 -59
  85. package/src/components/value-pill.tsx +0 -46
  86. package/src/constants/images.ts +0 -556
  87. package/src/constants/pill-values.ts +0 -210
  88. package/src/constants/products.ts +0 -155
  89. package/src/contexts/AvailabilitiesCacheContext.tsx +0 -125
  90. package/src/contexts/BookingAppContext.tsx +0 -134
  91. package/src/contexts/CompanyContext.tsx +0 -70
  92. package/src/data/dap-descriptions/session-couples-families-friends.en.json +0 -61
  93. package/src/data/dap-descriptions/session-elopements.en.json +0 -60
  94. package/src/data/dap-descriptions/session-proposals.en.json +0 -60
  95. package/src/data/product-descriptions/afternoon-delight.en.json +0 -35
  96. package/src/data/product-descriptions/emerald-lake-escape.en.json +0 -68
  97. package/src/data/product-descriptions/lake-louise-adventure.en.json +0 -74
  98. package/src/data/product-descriptions/moraine-lake-adventure.en.json +0 -78
  99. package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +0 -65
  100. package/src/data/product-descriptions/moraine-lake-sunrise.en.json +0 -64
  101. package/src/data/product-descriptions/private-tour.en.json +0 -80
  102. package/src/data/product-descriptions/two-lakes-combo.en.json +0 -65
  103. package/src/data/products-config.json +0 -101
  104. package/src/hooks/useBookingSourceMetadataFromLocation.ts +0 -21
  105. package/src/hooks/useIsBookingLaunchLive.ts +0 -49
  106. package/src/lib/analytics.ts +0 -197
  107. package/src/lib/booking/booking-source.ts +0 -51
  108. package/src/lib/booking/checkout-breakdown.ts +0 -69
  109. package/src/lib/booking/correlation-id.ts +0 -46
  110. package/src/lib/booking/i18n/config.ts +0 -21
  111. package/src/lib/booking/i18n/index.tsx +0 -144
  112. package/src/lib/booking/i18n/messages/en.json +0 -236
  113. package/src/lib/booking/i18n/messages/fr.json +0 -236
  114. package/src/lib/booking/itinerary-display.ts +0 -36
  115. package/src/lib/booking/itinerary-labels.ts +0 -70
  116. package/src/lib/booking/location-calculations.ts +0 -43
  117. package/src/lib/booking/location-utils.ts +0 -165
  118. package/src/lib/booking/map-utils.ts +0 -153
  119. package/src/lib/booking/marker-icons.ts +0 -113
  120. package/src/lib/booking/normalize-booking-product-id.ts +0 -21
  121. package/src/lib/booking/pickup-location-types.ts +0 -25
  122. package/src/lib/booking/places-api.ts +0 -154
  123. package/src/lib/booking/pricing.ts +0 -466
  124. package/src/lib/booking/product-option-id.ts +0 -35
  125. package/src/lib/booking/source-metadata.ts +0 -226
  126. package/src/lib/booking/sunday-week.ts +0 -14
  127. package/src/lib/booking/theme.ts +0 -83
  128. package/src/lib/booking/trace-context.ts +0 -62
  129. package/src/lib/booking/utils.ts +0 -9
  130. package/src/lib/booking-api.ts +0 -1793
  131. package/src/lib/booking-constants.ts +0 -23
  132. package/src/lib/booking-ref.ts +0 -13
  133. package/src/lib/booking-types.ts +0 -36
  134. package/src/lib/currency.ts +0 -81
  135. package/src/lib/dap-descriptions.ts +0 -50
  136. package/src/lib/dap-itinerary-preview.ts +0 -315
  137. package/src/lib/dependent-add-on-api.ts +0 -434
  138. package/src/lib/env.ts +0 -96
  139. package/src/lib/firebase.ts +0 -20
  140. package/src/lib/job-application-api.ts +0 -83
  141. package/src/lib/manage-booking-embed-print.ts +0 -16
  142. package/src/lib/manage-booking-post-checkout.ts +0 -68
  143. package/src/lib/photo-dap-config.ts +0 -228
  144. package/src/lib/photo-packages.ts +0 -75
  145. package/src/lib/pickup/map-utils.ts +0 -56
  146. package/src/lib/pickup/marker-icons.ts +0 -19
  147. package/src/lib/product-descriptions.ts +0 -66
  148. package/src/lib/products-config.ts +0 -73
  149. package/src/providers/booking-dialog-provider.tsx +0 -282
  150. package/src/providers/dependent-add-on-dialog-provider.tsx +0 -105
  151. package/src/radius.css +0 -5
  152. package/src/spacing.css +0 -7
  153. package/src/strings/en.json +0 -1774
  154. package/src/strings/es.json +0 -1573
  155. package/src/strings/fr.json +0 -1573
  156. package/src/strings/index.js +0 -23
  157. package/src/text-style.css +0 -56
  158. 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
- }
@@ -1,10 +0,0 @@
1
- /**
2
- * Add-ons section - label styled like tickets, cancellation policy (via booking-flow.css preflight)
3
- */
4
-
5
- .label {
6
- display: block;
7
- font-size: 0.875rem;
8
- font-weight: 500;
9
- color: var(--booking-stone-700, #44403c);
10
- }