@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.
- package/package.json +11 -29
- package/src/components/booking/AddOnsSection.tsx +2 -2
- package/src/components/booking/AdminPaymentChoiceModal.tsx +1 -1
- package/src/components/booking/BookingDialog.tsx +31 -13
- package/src/components/booking/BookingFlow.tsx +32 -27
- package/src/components/booking/BookingFlowCollage.tsx +10 -6
- package/src/components/booking/BookingFlowPlaceholder.tsx +1 -1
- package/src/components/booking/BookingFlowPreview.tsx +18 -9
- package/src/components/booking/BookingProductGrid.tsx +55 -19
- package/src/components/booking/Calendar.module.css +19 -4
- package/src/components/booking/Calendar.tsx +13 -8
- package/src/components/booking/CancellationPolicySelector.tsx +2 -2
- package/src/components/booking/ChangeBookingDialog.tsx +22 -12
- package/src/components/booking/CheckoutForm.module.css +10 -0
- package/src/components/booking/CheckoutForm.tsx +10 -2
- package/src/components/booking/CheckoutModal.tsx +16 -14
- package/src/components/booking/DapFlowCollage.tsx +5 -2
- package/src/components/booking/DapTourDescription.tsx +4 -4
- package/src/components/booking/DependentAddOnBookingDialog.tsx +23 -16
- package/src/components/booking/DependentAddOnPaymentForm.tsx +10 -7
- package/src/components/booking/ItineraryBox.tsx +6 -6
- package/src/components/booking/ItineraryBuilder.tsx +1 -1
- package/src/components/booking/MealDrinkAddOnSelector.tsx +3 -3
- package/src/components/booking/PickupLocationSelector.tsx +20 -18
- package/src/components/booking/PickupTimeSelector.tsx +3 -3
- package/src/components/booking/PriceBreakdown.tsx +5 -5
- package/src/components/booking/PriceSummary.module.css +7 -0
- package/src/components/booking/PriceSummary.tsx +8 -7
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +28 -19
- package/src/components/booking/PromoCodeInput.module.css +31 -25
- package/src/components/booking/PromoCodeInput.tsx +36 -24
- package/src/components/booking/ReturnTimeSelector.tsx +3 -3
- package/src/components/booking/TermsAcceptance.tsx +7 -2
- package/src/components/booking/TicketSelector.tsx +1 -1
- package/src/components/booking/TourDescription.tsx +11 -6
- package/src/components/booking/booking-flow.css +65 -4
- package/src/hooks/useBookingSourceMetadataFromLocation.ts +1 -1
- package/src/hooks/useIsBookingLaunchLive.ts +1 -1
- package/src/index.ts +26 -64
- package/src/providers/booking-dialog-provider.tsx +62 -53
- package/src/runtime/BookingHostContext.tsx +39 -0
- package/src/runtime/index.ts +13 -0
- package/src/runtime/types.ts +86 -0
- package/tsconfig.json +3 -5
- package/src/assets/icons/minus.svg +0 -7
- package/src/assets/icons/partner-logos/getyourguide.svg +0 -8
- package/src/assets/icons/plus.svg +0 -3
- package/src/colours.css +0 -23
- package/src/components/BookingDetails.module.css +0 -1591
- package/src/components/BookingDetails.tsx +0 -2264
- package/src/components/BookingWidget.tsx +0 -302
- package/src/components/ManageBookingView.tsx +0 -437
- package/src/components/PhoneInputWithCountry.module.css +0 -131
- package/src/components/PhoneInputWithCountry.tsx +0 -44
- package/src/components/PickupLocationDialog.module.css +0 -360
- package/src/components/PickupLocationDialog.tsx +0 -357
- package/src/components/PostBookingDependentAddOnUpsell.module.css +0 -174
- package/src/components/PostBookingDependentAddOnUpsell.tsx +0 -407
- package/src/components/button.css +0 -245
- package/src/components/button.tsx +0 -152
- package/src/components/colorable-svg.tsx +0 -29
- package/src/components/image.css +0 -29
- package/src/components/image.tsx +0 -113
- package/src/components/partner/PartnerBookingPage.module.css +0 -130
- package/src/components/partner/PartnerBookingPage.tsx +0 -390
- package/src/components/partner/PartnerBookingPageWithBrowserMetadata.tsx +0 -45
- package/src/components/product-tag.module.css +0 -30
- package/src/components/product-tag.tsx +0 -34
- package/src/components/product-theme-pages/image-modal.tsx +0 -248
- package/src/components/product-theme-pages/photo-gallery.module.css +0 -200
- package/src/components/terms/TermsContent.tsx +0 -178
- package/src/components/value-pill.module.css +0 -59
- package/src/components/value-pill.tsx +0 -46
- package/src/constants/images.ts +0 -556
- package/src/constants/pill-values.ts +0 -210
- package/src/constants/products.ts +0 -155
- package/src/contexts/AvailabilitiesCacheContext.tsx +0 -125
- package/src/contexts/CompanyContext.tsx +0 -70
- package/src/data/dap-descriptions/session-couples-families-friends.en.json +0 -61
- package/src/data/dap-descriptions/session-elopements.en.json +0 -60
- package/src/data/dap-descriptions/session-proposals.en.json +0 -60
- package/src/data/product-descriptions/afternoon-delight.en.json +0 -35
- package/src/data/product-descriptions/emerald-lake-escape.en.json +0 -68
- package/src/data/product-descriptions/lake-louise-adventure.en.json +0 -74
- package/src/data/product-descriptions/moraine-lake-adventure.en.json +0 -78
- package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +0 -65
- package/src/data/product-descriptions/moraine-lake-sunrise.en.json +0 -64
- package/src/data/product-descriptions/private-tour.en.json +0 -80
- package/src/data/product-descriptions/two-lakes-combo.en.json +0 -65
- package/src/data/products-config.json +0 -101
- package/src/lib/analytics.ts +0 -197
- package/src/lib/booking/booking-source.ts +0 -51
- package/src/lib/booking/checkout-breakdown.ts +0 -69
- package/src/lib/booking/correlation-id.ts +0 -46
- package/src/lib/booking/i18n/config.ts +0 -21
- package/src/lib/booking/i18n/index.tsx +0 -144
- package/src/lib/booking/i18n/messages/en.json +0 -236
- package/src/lib/booking/i18n/messages/fr.json +0 -236
- package/src/lib/booking/itinerary-display.ts +0 -36
- package/src/lib/booking/itinerary-labels.ts +0 -70
- package/src/lib/booking/location-calculations.ts +0 -43
- package/src/lib/booking/location-utils.ts +0 -165
- package/src/lib/booking/map-utils.ts +0 -153
- package/src/lib/booking/marker-icons.ts +0 -113
- package/src/lib/booking/normalize-booking-product-id.ts +0 -21
- package/src/lib/booking/pickup-location-types.ts +0 -25
- package/src/lib/booking/places-api.ts +0 -154
- package/src/lib/booking/pricing.ts +0 -466
- package/src/lib/booking/product-option-id.ts +0 -35
- package/src/lib/booking/source-metadata.ts +0 -226
- package/src/lib/booking/sunday-week.ts +0 -14
- package/src/lib/booking/theme.ts +0 -83
- package/src/lib/booking/trace-context.ts +0 -62
- package/src/lib/booking/utils.ts +0 -9
- package/src/lib/booking-api.ts +0 -1793
- package/src/lib/booking-constants.ts +0 -23
- package/src/lib/booking-ref.ts +0 -13
- package/src/lib/booking-types.ts +0 -36
- package/src/lib/currency.ts +0 -81
- package/src/lib/dap-descriptions.ts +0 -50
- package/src/lib/dap-itinerary-preview.ts +0 -315
- package/src/lib/dependent-add-on-api.ts +0 -434
- package/src/lib/env.ts +0 -96
- package/src/lib/firebase.ts +0 -20
- package/src/lib/job-application-api.ts +0 -83
- package/src/lib/manage-booking-embed-print.ts +0 -16
- package/src/lib/manage-booking-post-checkout.ts +0 -68
- package/src/lib/photo-dap-config.ts +0 -228
- package/src/lib/photo-packages.ts +0 -75
- package/src/lib/pickup/map-utils.ts +0 -56
- package/src/lib/pickup/marker-icons.ts +0 -19
- package/src/lib/product-descriptions.ts +0 -66
- package/src/lib/products-config.ts +0 -73
- package/src/providers/dependent-add-on-dialog-provider.tsx +0 -105
- package/src/radius.css +0 -5
- package/src/spacing.css +0 -7
- package/src/strings/en.json +0 -1774
- package/src/strings/es.json +0 -1573
- package/src/strings/fr.json +0 -1573
- package/src/strings/index.js +0 -23
- package/src/text-style.css +0 -56
- package/src/utils/currency-converter.ts +0 -101
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Manage-booking post-checkout helpers.
|
|
3
|
-
*
|
|
4
|
-
* Next.js `router.replace` remounts the page; React state for a success banner is lost.
|
|
5
|
-
* We stash a one-time marker in sessionStorage immediately before cleaning the URL
|
|
6
|
-
* (only paid *change booking* Stripe returns). New bookings use `reservationRef` → `ref`
|
|
7
|
-
* and never set this flag.
|
|
8
|
-
*
|
|
9
|
-
* We intentionally do not poll the booking API here: one landing fetch + optional remount
|
|
10
|
-
* fetch after `replace` is enough; avoid timers/retry loops that feel like “random” refreshes.
|
|
11
|
-
*/
|
|
12
|
-
const STORAGE_KEY = 'viavia:manage-booking:flash:v1';
|
|
13
|
-
|
|
14
|
-
export const MANAGE_BOOKING_QUERY_FROM = 'from';
|
|
15
|
-
/** Only paid *change booking* Stripe returns add this; new bookings must not use it. */
|
|
16
|
-
export const MANAGE_BOOKING_FROM_CHANGE_PAYMENT = 'change_payment';
|
|
17
|
-
|
|
18
|
-
/** Success copy for any self-serve booking change (no-pay, paid-after-Stripe, etc.). */
|
|
19
|
-
export function manageBookingChangeSuccessMessage(
|
|
20
|
-
preview: {
|
|
21
|
-
dateChanged?: boolean;
|
|
22
|
-
ticketsChanged?: boolean;
|
|
23
|
-
} | null,
|
|
24
|
-
): string {
|
|
25
|
-
const updates: string[] = [];
|
|
26
|
-
if (preview?.dateChanged) updates.push('date/time');
|
|
27
|
-
if (preview?.ticketsChanged) updates.push('ticket selection');
|
|
28
|
-
const suffix =
|
|
29
|
-
updates.length > 0 ? ` Updated ${updates.join(' and ')}.` : '';
|
|
30
|
-
return `Your booking was updated successfully.${suffix}`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export type ManageBookingFlashPayload = {
|
|
34
|
-
v: 1;
|
|
35
|
-
kind: 'change_payment_success';
|
|
36
|
-
t: number;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export function setManageBookingFlashChangePaymentSuccess(): void {
|
|
40
|
-
if (typeof window === 'undefined') return;
|
|
41
|
-
try {
|
|
42
|
-
const payload: ManageBookingFlashPayload = {
|
|
43
|
-
v: 1,
|
|
44
|
-
kind: 'change_payment_success',
|
|
45
|
-
t: Date.now(),
|
|
46
|
-
};
|
|
47
|
-
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
|
|
48
|
-
} catch {
|
|
49
|
-
/* private mode / quota */
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Read and clear. Returns user-facing message or null. */
|
|
54
|
-
export function consumeManageBookingFlash(): string | null {
|
|
55
|
-
if (typeof window === 'undefined') return null;
|
|
56
|
-
try {
|
|
57
|
-
const raw = sessionStorage.getItem(STORAGE_KEY);
|
|
58
|
-
if (!raw) return null;
|
|
59
|
-
sessionStorage.removeItem(STORAGE_KEY);
|
|
60
|
-
const j = JSON.parse(raw) as ManageBookingFlashPayload;
|
|
61
|
-
if (j?.v === 1 && j.kind === 'change_payment_success') {
|
|
62
|
-
return manageBookingChangeSuccessMessage(null);
|
|
63
|
-
}
|
|
64
|
-
} catch {
|
|
65
|
-
/* ignore */
|
|
66
|
-
}
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Maps photo-sessions packages to TicketBooth dependent add-on product (and optional option) IDs.
|
|
3
|
-
* Set NEXT_PUBLIC_DAP_* in .env.local to override seed defaults (e.g. other envs or 60/90 min options).
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { IMAGES } from '@/constants/images';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Default full-refund cancellation window (days before the scheduled photo session).
|
|
10
|
-
* Override per catalog entry in `getPhotoDapCatalog` when a DAP product differs.
|
|
11
|
-
*/
|
|
12
|
-
export const DEFAULT_PHOTO_DAP_CANCELLATION_DAYS_BEFORE_SESSION = 30;
|
|
13
|
-
|
|
14
|
-
/** Seeded test DAP: Friends, Family, & Couples (`dependent-add-on-products.json`) */
|
|
15
|
-
const COUPLES_SESSION_DEFAULT_PRODUCT_ID = 'dap_sM8pSzbFfU7P';
|
|
16
|
-
const ELOPEMENTS_SESSION_DEFAULT_PRODUCT_ID = 'dap_6tITweGzVyg8';
|
|
17
|
-
const PROPOSALS_SESSION_DEFAULT_PRODUCT_ID = 'dap_Xy3OCPIKQ7vr';
|
|
18
|
-
|
|
19
|
-
/** Session lengths for couples / families card — matches `dependent-add-on-products.json` options */
|
|
20
|
-
const COUPLES_SESSION_PRODUCT_OPTIONS = [
|
|
21
|
-
{
|
|
22
|
-
dependentAddOnProductOptionId: 'dapo_Ey0v4O7BjFaD',
|
|
23
|
-
label: '30 minutes',
|
|
24
|
-
photosLabel: '25 photos',
|
|
25
|
-
startingAtLabel: 'Starting at $499',
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
dependentAddOnProductOptionId: 'dapo_caJh25HsTwKx',
|
|
29
|
-
label: '60 minutes',
|
|
30
|
-
photosLabel: '50 photos',
|
|
31
|
-
startingAtLabel: 'Starting at $799',
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
dependentAddOnProductOptionId: 'dapo_6Km5njalQBp1',
|
|
35
|
-
label: '90 minutes',
|
|
36
|
-
photosLabel: '75 photos',
|
|
37
|
-
startingAtLabel: 'Starting at $799',
|
|
38
|
-
},
|
|
39
|
-
] as const;
|
|
40
|
-
|
|
41
|
-
const ELOPEMENTS_SESSION_PRODUCT_OPTIONS = [
|
|
42
|
-
{
|
|
43
|
-
dependentAddOnProductOptionId: 'dapo_XTeJt09NyNfX',
|
|
44
|
-
label: '30 minutes',
|
|
45
|
-
photosLabel: '25 photos',
|
|
46
|
-
startingAtLabel: 'Starting at $799',
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
dependentAddOnProductOptionId: 'dapo_Y8AxYt6Tjaam',
|
|
50
|
-
label: '60 minutes',
|
|
51
|
-
photosLabel: '50 photos',
|
|
52
|
-
startingAtLabel: 'Starting at $999',
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
dependentAddOnProductOptionId: 'dapo_YdmxIiQPxEJg',
|
|
56
|
-
label: '90 minutes',
|
|
57
|
-
photosLabel: '75 photos',
|
|
58
|
-
startingAtLabel: 'Starting at $1199',
|
|
59
|
-
},
|
|
60
|
-
] as const;
|
|
61
|
-
|
|
62
|
-
const PROPOSALS_SESSION_PRODUCT_OPTIONS = [
|
|
63
|
-
{
|
|
64
|
-
dependentAddOnProductOptionId: 'dapo_SMRxjfDpIvwU',
|
|
65
|
-
label: '30 minutes',
|
|
66
|
-
photosLabel: '25 photos',
|
|
67
|
-
startingAtLabel: 'Starting at $799',
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
dependentAddOnProductOptionId: 'dapo_FyMbFgrBU4L8',
|
|
71
|
-
label: '60 minutes',
|
|
72
|
-
photosLabel: '50 photos',
|
|
73
|
-
startingAtLabel: 'Starting at $999',
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
dependentAddOnProductOptionId: 'dapo_EOohfF0g2i1D',
|
|
77
|
-
label: '90 minutes',
|
|
78
|
-
photosLabel: '75 photos',
|
|
79
|
-
startingAtLabel: 'Starting at $1199',
|
|
80
|
-
},
|
|
81
|
-
] as const;
|
|
82
|
-
|
|
83
|
-
export const PHOTO_DAP_SLUGS = [
|
|
84
|
-
'session-couples-families-friends',
|
|
85
|
-
'session-elopements',
|
|
86
|
-
'session-proposals',
|
|
87
|
-
] as const;
|
|
88
|
-
|
|
89
|
-
export type PhotoDapSlug = (typeof PHOTO_DAP_SLUGS)[number];
|
|
90
|
-
|
|
91
|
-
export type PhotoDapProductOption = {
|
|
92
|
-
dependentAddOnProductOptionId: string;
|
|
93
|
-
/** First line on the tile (e.g. duration). */
|
|
94
|
-
label: string;
|
|
95
|
-
/** Second line (e.g. edited photo count). */
|
|
96
|
-
photosLabel?: string;
|
|
97
|
-
/** Third line (e.g. "Starting at $399"). */
|
|
98
|
-
startingAtLabel?: string;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
export type PhotoDapCatalog = {
|
|
102
|
-
dependentAddOnProductId: string;
|
|
103
|
-
/** When set, that option is fixed (no picker). Env `NEXT_PUBLIC_DAP_PHOTO_SESSION_COUPLES_OPTION_ID` forces this for couples. */
|
|
104
|
-
dependentAddOnProductOptionId?: string;
|
|
105
|
-
/** When multiple entries and no fixed option id, the dialog shows a session-length control */
|
|
106
|
-
productOptions?: PhotoDapProductOption[];
|
|
107
|
-
/** Bunny CDN IDs for DapFlowCollage (hero + four-tile grid) */
|
|
108
|
-
collageImageIds: string[];
|
|
109
|
-
/**
|
|
110
|
-
* Days before the scheduled photo session that a full refund still applies for this DAP product.
|
|
111
|
-
* Should match TicketBooth dependent add-on product config; availability API may override when present.
|
|
112
|
-
*/
|
|
113
|
-
cancellationDaysBeforeSession: number;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
/** Image sets for the dependent add-on dialog collage (photos only; no video). */
|
|
117
|
-
export function photoDapCollageImageIds(slug: PhotoDapSlug): string[] {
|
|
118
|
-
switch (slug) {
|
|
119
|
-
case 'session-couples-families-friends':
|
|
120
|
-
return [
|
|
121
|
-
IMAGES.FAMILY_MORIANE_LAKE_ROCKPILE.id,
|
|
122
|
-
IMAGES.PRIVATE_TOUR_FAMILY_MORAINE_LAKE.id,
|
|
123
|
-
IMAGES.GIRLS_DAY_RAINY_LAKE_LOUISE.id,
|
|
124
|
-
IMAGES.MORAINE_LAKE_COUPLE_AT_WATER.id,
|
|
125
|
-
IMAGES.MORAINE_LAKE_SUNRISE_GIRL_FRIENDS.id,
|
|
126
|
-
];
|
|
127
|
-
case 'session-elopements':
|
|
128
|
-
return [
|
|
129
|
-
IMAGES.MORAINE_LAKE_SUNRISE_ELOPEMENT.id,
|
|
130
|
-
IMAGES.SENTINEL_PASS_ELOPEMENT_SNOW.id,
|
|
131
|
-
IMAGES.MORAINE_LAKE_ELOPEMENT.id,
|
|
132
|
-
IMAGES.SENTINEL_PASS_ELOPEMENT_SNOW_HORIZONTAL.id,
|
|
133
|
-
IMAGES.MORAINE_LAKE_ELOPEMENT_LAKESHORE.id,
|
|
134
|
-
];
|
|
135
|
-
case 'session-proposals':
|
|
136
|
-
return [
|
|
137
|
-
IMAGES.LAKE_LOUISE_KISS.id,
|
|
138
|
-
IMAGES.MORAINE_LAKE_PROPOSE_CRY.id,
|
|
139
|
-
IMAGES.MORAINE_LAKE_OVERCAST_PROPOSE.id,
|
|
140
|
-
IMAGES.MORAINE_LAKE_PROPOSAL.id,
|
|
141
|
-
IMAGES.MORAINE_LAKE_PROPOSE.id,
|
|
142
|
-
];
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function trimEnv(value: string | undefined): string {
|
|
147
|
-
return (value ?? '').trim();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Returns catalog when product id is known (env or built-in default for couples session).
|
|
152
|
-
* Otherwise null (card falls back to #book-now).
|
|
153
|
-
*/
|
|
154
|
-
export function getPhotoDapCatalog(slug: PhotoDapSlug): PhotoDapCatalog | null {
|
|
155
|
-
if (slug === 'session-couples-families-friends') {
|
|
156
|
-
const productId =
|
|
157
|
-
trimEnv(process.env.NEXT_PUBLIC_DAP_PHOTO_SESSION_COUPLES_PRODUCT_ID) ||
|
|
158
|
-
COUPLES_SESSION_DEFAULT_PRODUCT_ID;
|
|
159
|
-
const forcedOptionId = trimEnv(
|
|
160
|
-
process.env.NEXT_PUBLIC_DAP_PHOTO_SESSION_COUPLES_OPTION_ID
|
|
161
|
-
);
|
|
162
|
-
const collageImageIds = photoDapCollageImageIds('session-couples-families-friends');
|
|
163
|
-
if (forcedOptionId) {
|
|
164
|
-
return {
|
|
165
|
-
dependentAddOnProductId: productId,
|
|
166
|
-
dependentAddOnProductOptionId: forcedOptionId,
|
|
167
|
-
collageImageIds,
|
|
168
|
-
cancellationDaysBeforeSession: DEFAULT_PHOTO_DAP_CANCELLATION_DAYS_BEFORE_SESSION,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
return {
|
|
172
|
-
dependentAddOnProductId: productId,
|
|
173
|
-
productOptions: [...COUPLES_SESSION_PRODUCT_OPTIONS],
|
|
174
|
-
collageImageIds,
|
|
175
|
-
cancellationDaysBeforeSession: DEFAULT_PHOTO_DAP_CANCELLATION_DAYS_BEFORE_SESSION,
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (slug === 'session-elopements') {
|
|
180
|
-
const productId =
|
|
181
|
-
trimEnv(process.env.NEXT_PUBLIC_DAP_PHOTO_SESSION_ELOPEMENTS_PRODUCT_ID) ||
|
|
182
|
-
ELOPEMENTS_SESSION_DEFAULT_PRODUCT_ID;
|
|
183
|
-
const forcedOptionId = trimEnv(
|
|
184
|
-
process.env.NEXT_PUBLIC_DAP_PHOTO_SESSION_ELOPEMENTS_OPTION_ID
|
|
185
|
-
);
|
|
186
|
-
const collageImageIds = photoDapCollageImageIds('session-elopements');
|
|
187
|
-
if (forcedOptionId) {
|
|
188
|
-
return {
|
|
189
|
-
dependentAddOnProductId: productId,
|
|
190
|
-
dependentAddOnProductOptionId: forcedOptionId,
|
|
191
|
-
collageImageIds,
|
|
192
|
-
cancellationDaysBeforeSession: DEFAULT_PHOTO_DAP_CANCELLATION_DAYS_BEFORE_SESSION,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
return {
|
|
196
|
-
dependentAddOnProductId: productId,
|
|
197
|
-
productOptions: [...ELOPEMENTS_SESSION_PRODUCT_OPTIONS],
|
|
198
|
-
collageImageIds,
|
|
199
|
-
cancellationDaysBeforeSession: DEFAULT_PHOTO_DAP_CANCELLATION_DAYS_BEFORE_SESSION,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (slug === 'session-proposals') {
|
|
204
|
-
const productId =
|
|
205
|
-
trimEnv(process.env.NEXT_PUBLIC_DAP_PHOTO_SESSION_PROPOSALS_PRODUCT_ID) ||
|
|
206
|
-
PROPOSALS_SESSION_DEFAULT_PRODUCT_ID;
|
|
207
|
-
const forcedOptionId = trimEnv(
|
|
208
|
-
process.env.NEXT_PUBLIC_DAP_PHOTO_SESSION_PROPOSALS_OPTION_ID
|
|
209
|
-
);
|
|
210
|
-
const collageImageIds = photoDapCollageImageIds('session-proposals');
|
|
211
|
-
if (forcedOptionId) {
|
|
212
|
-
return {
|
|
213
|
-
dependentAddOnProductId: productId,
|
|
214
|
-
dependentAddOnProductOptionId: forcedOptionId,
|
|
215
|
-
collageImageIds,
|
|
216
|
-
cancellationDaysBeforeSession: DEFAULT_PHOTO_DAP_CANCELLATION_DAYS_BEFORE_SESSION,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
return {
|
|
220
|
-
dependentAddOnProductId: productId,
|
|
221
|
-
productOptions: [...PROPOSALS_SESSION_PRODUCT_OPTIONS],
|
|
222
|
-
collageImageIds,
|
|
223
|
-
cancellationDaysBeforeSession: DEFAULT_PHOTO_DAP_CANCELLATION_DAYS_BEFORE_SESSION,
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return null;
|
|
228
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { ImageData, IMAGES } from "@/constants/images";
|
|
2
|
-
import type { PhotoDapSlug } from "@/lib/photo-dap-config";
|
|
3
|
-
|
|
4
|
-
export interface VideoSources {
|
|
5
|
-
src: string;
|
|
6
|
-
webm: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface PhotoPackage {
|
|
10
|
-
name: string;
|
|
11
|
-
bookingLink: string;
|
|
12
|
-
/** When set and env IDs are configured, opens dependent add-on booking for this catalog entry */
|
|
13
|
-
dapSlug?: PhotoDapSlug;
|
|
14
|
-
images: ImageData[];
|
|
15
|
-
startingPrice: string;
|
|
16
|
-
duration: string;
|
|
17
|
-
quantity: string;
|
|
18
|
-
videoUrl?: VideoSources;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface PhotoPackageSection {
|
|
22
|
-
name: string;
|
|
23
|
-
subTitle: string;
|
|
24
|
-
description: string;
|
|
25
|
-
photoPackages: PhotoPackage[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export const PHOTO_PACKAGE_SECTIONS: PhotoPackageSection[] = [
|
|
29
|
-
{
|
|
30
|
-
name: 'Photo Sessions',
|
|
31
|
-
subTitle: 'Make it last forever',
|
|
32
|
-
description: "Capture the magic of Moraine Lake with a mini photo shoot; perfect for couples, families, or friends looking to take home more than just memories. Choose from 30, 60, or 90-minute sessions, with professionally edited photos included. Longer sessions can also be arranged with our private shuttle experiences for those wanting more time and flexibility.<br><br>Planning to propose or elope in one of the most breathtaking places on earth? Let us help you make it truly unforgettable. Our experienced local photographers know how to blend into the moment while capturing it beautifully - from the quiet emotion of a surprise proposal to the joy of an intimate celebration. Reach out to our team after booking your shoot & shuttle package reach to help you plan the perfect setting, timing, and details to make your moment picture-perfect and entirely your own.",
|
|
33
|
-
photoPackages: [
|
|
34
|
-
{
|
|
35
|
-
name: 'Couples, Families, & Friends',
|
|
36
|
-
bookingLink: '#book-now',
|
|
37
|
-
dapSlug: 'session-couples-families-friends',
|
|
38
|
-
images: [IMAGES.FAMILY_MORIANE_LAKE_ROCKPILE],
|
|
39
|
-
startingPrice: 'Starting at $499',
|
|
40
|
-
duration: '30 / 60 / 90 minutes',
|
|
41
|
-
quantity: '25 / 50 / 75 photos',
|
|
42
|
-
videoUrl: {
|
|
43
|
-
src: '/videos/sunrise-rockpile-tourist-vibe-group-shoot.mp4',
|
|
44
|
-
webm: '/videos/sunrise-rockpile-tourist-vibe-group-shoot.webm'
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
name: 'Elopements',
|
|
49
|
-
bookingLink: '#book-now',
|
|
50
|
-
dapSlug: 'session-elopements',
|
|
51
|
-
images: [IMAGES.MORAINE_LAKE_SUNRISE_ELOPEMENT],
|
|
52
|
-
startingPrice: 'Starting at $799',
|
|
53
|
-
duration: '30 / 60 / 90 minutes',
|
|
54
|
-
quantity: '25 / 50 / 75 photos',
|
|
55
|
-
videoUrl: {
|
|
56
|
-
src: '/videos/dancing-couple-elopement-shoot.mp4',
|
|
57
|
-
webm: '/videos/dancing-couple-elopement-shoot.webm'
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
name: 'Proposals',
|
|
62
|
-
bookingLink: '#book-now',
|
|
63
|
-
dapSlug: 'session-proposals',
|
|
64
|
-
images: [IMAGES.LAKE_LOUISE_KISS],
|
|
65
|
-
startingPrice: 'Starting at $799',
|
|
66
|
-
duration: '30 / 60 / 90 minutes',
|
|
67
|
-
quantity: '25 / 50 / 75 photos',
|
|
68
|
-
videoUrl: {
|
|
69
|
-
src: '/videos/proposal-moraine-lake-shoot.mp4',
|
|
70
|
-
webm: '/videos/proposal-moraine-lake-shoot.webm'
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
]
|
|
74
|
-
}
|
|
75
|
-
]
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Google Maps utilities for pickup location picker
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export interface MapCenter {
|
|
6
|
-
lat: number;
|
|
7
|
-
lng: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface PickupLocationWithCoords {
|
|
11
|
-
id: string;
|
|
12
|
-
name: string;
|
|
13
|
-
address: string;
|
|
14
|
-
coordinates?: { lat: number; lng: number };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const DEFAULT_CENTER: MapCenter = { lat: 51.1784, lng: -115.5708 };
|
|
18
|
-
|
|
19
|
-
export function calculateMapCenter(pickupLocations: PickupLocationWithCoords[]): MapCenter {
|
|
20
|
-
const withCoords = pickupLocations.filter((l) => l.coordinates);
|
|
21
|
-
if (withCoords.length > 0 && withCoords[0].coordinates) {
|
|
22
|
-
return withCoords[0].coordinates;
|
|
23
|
-
}
|
|
24
|
-
return DEFAULT_CENTER;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function getMapOptions(): google.maps.MapOptions {
|
|
28
|
-
return {
|
|
29
|
-
disableDefaultUI: false,
|
|
30
|
-
clickableIcons: false,
|
|
31
|
-
scrollwheel: true,
|
|
32
|
-
zoomControl: true,
|
|
33
|
-
streetViewControl: false,
|
|
34
|
-
mapTypeControl: false,
|
|
35
|
-
fullscreenControl: true,
|
|
36
|
-
gestureHandling: 'cooperative',
|
|
37
|
-
} as google.maps.MapOptions;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function calculateMapBounds(
|
|
41
|
-
pickupLocations: PickupLocationWithCoords[]
|
|
42
|
-
): google.maps.LatLngBounds | null {
|
|
43
|
-
if (typeof google === 'undefined' || !google.maps) return null;
|
|
44
|
-
try {
|
|
45
|
-
const bounds = new google.maps.LatLngBounds();
|
|
46
|
-
pickupLocations.forEach((loc) => {
|
|
47
|
-
if (loc.coordinates) bounds.extend(loc.coordinates);
|
|
48
|
-
});
|
|
49
|
-
return bounds.getNorthEast().lat() !== bounds.getSouthWest().lat() ||
|
|
50
|
-
bounds.getNorthEast().lng() !== bounds.getSouthWest().lng()
|
|
51
|
-
? bounds
|
|
52
|
-
: null;
|
|
53
|
-
} catch {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Marker icon generation for pickup location pins
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
function validateColor(color: string): string {
|
|
6
|
-
const hexColorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
|
|
7
|
-
return hexColorRegex.test(color) ? color : '#dc2626';
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function createPinMarkerIcon(color: string): string {
|
|
11
|
-
const safeColor = validateColor(color);
|
|
12
|
-
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(`
|
|
13
|
-
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
14
|
-
<path d="M16 0C7.163 0 0 7.163 0 16C0 24.837 16 40 16 40C16 40 32 24.837 32 16C32 7.163 24.837 0 16 0Z" fill="#ffffff"/>
|
|
15
|
-
<path d="M16 2C8.268 2 2 8.268 2 16C2 22.177 16 38 16 38C16 38 30 22.177 30 16C30 8.268 23.732 2 16 2Z" fill="${safeColor}"/>
|
|
16
|
-
<circle cx="16" cy="16" r="6" fill="#ffffff"/>
|
|
17
|
-
</svg>
|
|
18
|
-
`)}`;
|
|
19
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Product description content loader.
|
|
3
|
-
* Loads full tour descriptions from src/data/product-descriptions/{slug}.{locale}.json
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export interface ProductDescriptionReview {
|
|
7
|
-
text: string;
|
|
8
|
-
name: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** Section content: string for HTML, string[] for bullet list OR joined HTML (array of lines for readability) */
|
|
12
|
-
export type SectionContent = string | string[];
|
|
13
|
-
|
|
14
|
-
export interface ProductDescriptionResult {
|
|
15
|
-
shortDescription?: string;
|
|
16
|
-
paragraphs: string[];
|
|
17
|
-
review?: ProductDescriptionReview;
|
|
18
|
-
sections: { title: string; content: SectionContent }[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface ProductDescriptionData {
|
|
22
|
-
shortDescription?: string;
|
|
23
|
-
paragraphs: string[];
|
|
24
|
-
review?: ProductDescriptionReview;
|
|
25
|
-
sections: { title: string; content: SectionContent }[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Static imports - add new locales here when available
|
|
29
|
-
import moraineLakeSunriseLakeLouiseGoldenHourEn from '@/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json';
|
|
30
|
-
import moraineLakeSunriseEn from '@/data/product-descriptions/moraine-lake-sunrise.en.json';
|
|
31
|
-
import twoLakesComboEn from '@/data/product-descriptions/two-lakes-combo.en.json';
|
|
32
|
-
import moraineLakeAdventureEn from '@/data/product-descriptions/moraine-lake-adventure.en.json';
|
|
33
|
-
import lakeLouiseAdventureEn from '@/data/product-descriptions/lake-louise-adventure.en.json';
|
|
34
|
-
import emeraldLakeEscapeEn from '@/data/product-descriptions/emerald-lake-escape.en.json';
|
|
35
|
-
import afternoonDelightEn from '@/data/product-descriptions/afternoon-delight.en.json';
|
|
36
|
-
import privateTourEn from '@/data/product-descriptions/private-tour.en.json';
|
|
37
|
-
|
|
38
|
-
const DESCRIPTIONS: Record<string, Record<string, ProductDescriptionData>> = {
|
|
39
|
-
en: {
|
|
40
|
-
'moraine-lake-sunrise-lake-louise-golden-hour': moraineLakeSunriseLakeLouiseGoldenHourEn as ProductDescriptionData,
|
|
41
|
-
'moraine-lake-sunrise': moraineLakeSunriseEn as ProductDescriptionData,
|
|
42
|
-
'two-lakes-combo': twoLakesComboEn as ProductDescriptionData,
|
|
43
|
-
'moraine-lake-adventure': moraineLakeAdventureEn as ProductDescriptionData,
|
|
44
|
-
'lake-louise-adventure': lakeLouiseAdventureEn as ProductDescriptionData,
|
|
45
|
-
'emerald-lake-escape': emeraldLakeEscapeEn as ProductDescriptionData,
|
|
46
|
-
'afternoon-delight': afternoonDelightEn as ProductDescriptionData,
|
|
47
|
-
'private-tour': privateTourEn as ProductDescriptionData,
|
|
48
|
-
},
|
|
49
|
-
// Add fr, es etc. when files exist:
|
|
50
|
-
// fr: { ... },
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export function getProductDescription(
|
|
54
|
-
slug: string,
|
|
55
|
-
locale: string
|
|
56
|
-
): ProductDescriptionResult | null {
|
|
57
|
-
const localeData = DESCRIPTIONS[locale] ?? DESCRIPTIONS.en;
|
|
58
|
-
const data = localeData?.[slug];
|
|
59
|
-
if (!data) return null;
|
|
60
|
-
return {
|
|
61
|
-
shortDescription: data.shortDescription,
|
|
62
|
-
paragraphs: data.paragraphs ?? [],
|
|
63
|
-
review: data.review,
|
|
64
|
-
sections: data.sections ?? [],
|
|
65
|
-
};
|
|
66
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Load products config from JSON (build-time / static).
|
|
3
|
-
* Later: can be swapped for API fetch or generated by TicketBooth script.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { ProductsConfig, ProductConfig } from './booking-types';
|
|
7
|
-
import type { Product, ProductOption } from '@/lib/booking-api';
|
|
8
|
-
import { getImageUrl } from '@/constants/images';
|
|
9
|
-
|
|
10
|
-
// Import JSON - Next.js supports this; ensure tsconfig has "resolveJsonModule": true
|
|
11
|
-
import productsConfigJson from '@/data/products-config.json';
|
|
12
|
-
|
|
13
|
-
const config = productsConfigJson as ProductsConfig;
|
|
14
|
-
|
|
15
|
-
export function getProductsConfig(): ProductsConfig {
|
|
16
|
-
return config;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function getProductsForBooking(): ProductConfig[] {
|
|
20
|
-
return config.products;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function getProductBySlug(slug: string): ProductConfig | null {
|
|
24
|
-
return config.products.find((p) => p.display.slug === slug) ?? null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function getProductByOptionId(optionId: string): ProductConfig | null {
|
|
28
|
-
return (
|
|
29
|
-
config.products.find((p) => p.optionIds.includes(optionId)) ?? null
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Find product by productId, optionId, or slug */
|
|
34
|
-
export function getProductByIdOrSlug(idOrSlug: string): ProductConfig | null {
|
|
35
|
-
const bySlug = getProductBySlug(idOrSlug);
|
|
36
|
-
if (bySlug) return bySlug;
|
|
37
|
-
const byOption = getProductByOptionId(idOrSlug);
|
|
38
|
-
if (byOption) return byOption;
|
|
39
|
-
return config.products.find((p) => p.productId === idOrSlug) ?? null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Build a minimal Product from config for immediate availability fetch.
|
|
44
|
-
* Use when API product is still loading - allows get-availabilities to run in parallel with /products.
|
|
45
|
-
*/
|
|
46
|
-
export function buildMinimalProductFromConfig(
|
|
47
|
-
config: ProductConfig,
|
|
48
|
-
companyId: string
|
|
49
|
-
): Product {
|
|
50
|
-
const options: ProductOption[] = config.optionIds.map((optionId) => ({
|
|
51
|
-
optionId,
|
|
52
|
-
name: '',
|
|
53
|
-
description: null,
|
|
54
|
-
pricing: {},
|
|
55
|
-
status: 'ACTIVE',
|
|
56
|
-
}));
|
|
57
|
-
return {
|
|
58
|
-
productId: config.productId,
|
|
59
|
-
companyId,
|
|
60
|
-
name: config.display.shortName,
|
|
61
|
-
description: null,
|
|
62
|
-
status: 'ACTIVE',
|
|
63
|
-
productType: config.productType ?? 'STANDARD',
|
|
64
|
-
options,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Get image URL for a product (first image from config) */
|
|
69
|
-
export function getProductImageUrl(product: ProductConfig): string {
|
|
70
|
-
const imageId = product.display.imageIds[0];
|
|
71
|
-
if (!imageId) return '';
|
|
72
|
-
return getImageUrl(imageId);
|
|
73
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
createContext,
|
|
5
|
-
useCallback,
|
|
6
|
-
useContext,
|
|
7
|
-
useMemo,
|
|
8
|
-
useRef,
|
|
9
|
-
useState,
|
|
10
|
-
type ReactNode,
|
|
11
|
-
} from 'react';
|
|
12
|
-
import type { PhotoDapSlug } from '@/lib/photo-dap-config';
|
|
13
|
-
|
|
14
|
-
export type DependentAddOnProductOptionChoice = {
|
|
15
|
-
dependentAddOnProductOptionId: string;
|
|
16
|
-
label: string;
|
|
17
|
-
photosLabel?: string;
|
|
18
|
-
startingAtLabel?: string;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export type DependentAddOnDialogOpenPayload = {
|
|
22
|
-
/** Card title shown in dialog header */
|
|
23
|
-
productDisplayTitle: string;
|
|
24
|
-
dependentAddOnProductId: string;
|
|
25
|
-
/** Fixed catalog option (no picker) */
|
|
26
|
-
dependentAddOnProductOptionId?: string;
|
|
27
|
-
/** When provided without a fixed option id, user picks one (e.g. 30 / 60 / 90 min) */
|
|
28
|
-
productOptions?: DependentAddOnProductOptionChoice[];
|
|
29
|
-
/**
|
|
30
|
-
* Default session-length id when multiple `productOptions` exist (e.g. manage-booking upsell probed 30 min).
|
|
31
|
-
* Unlike `dependentAddOnProductOptionId`, this does not hide the picker — the user can switch length.
|
|
32
|
-
*/
|
|
33
|
-
initialSelectedProductOptionId?: string;
|
|
34
|
-
/** Hero + grid images (DapFlowCollage); Bunny CDN IDs */
|
|
35
|
-
collageImageIds?: string[];
|
|
36
|
-
/** Loads expandable copy from dap-descriptions */
|
|
37
|
-
dapDescriptionSlug?: PhotoDapSlug;
|
|
38
|
-
/**
|
|
39
|
-
* From DAP catalog / TicketBooth product — days before the photo session for full-refund cancellation.
|
|
40
|
-
* Availability API may override when it returns the same field.
|
|
41
|
-
*/
|
|
42
|
-
cancellationDaysBeforeSession: number;
|
|
43
|
-
/**
|
|
44
|
-
* Pre-fill primary booking reference (e.g. manage-booking upsell after shuttle checkout).
|
|
45
|
-
* Accepts short or bookRef_ form; dialog normalizes for display.
|
|
46
|
-
*/
|
|
47
|
-
initialPrimaryBookingReference?: string;
|
|
48
|
-
/** Optional pre-fill for booking-owner verification on DAP availability checks. */
|
|
49
|
-
initialPrimaryBookingLastName?: string;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
interface DependentAddOnDialogContextValue {
|
|
53
|
-
isOpen: boolean;
|
|
54
|
-
payload: DependentAddOnDialogOpenPayload | null;
|
|
55
|
-
open: (p: DependentAddOnDialogOpenPayload) => void;
|
|
56
|
-
close: () => void;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const DependentAddOnDialogContext =
|
|
60
|
-
createContext<DependentAddOnDialogContextValue | null>(null);
|
|
61
|
-
|
|
62
|
-
export function useDependentAddOnDialog() {
|
|
63
|
-
const ctx = useContext(DependentAddOnDialogContext);
|
|
64
|
-
if (!ctx) {
|
|
65
|
-
throw new Error(
|
|
66
|
-
'useDependentAddOnDialog must be used within DependentAddOnDialogProvider'
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
return ctx;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function DependentAddOnDialogProvider({ children }: { children: ReactNode }) {
|
|
73
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
74
|
-
const [payload, setPayload] = useState<DependentAddOnDialogOpenPayload | null>(null);
|
|
75
|
-
const previouslyFocusedRef = useRef<HTMLElement | null>(null);
|
|
76
|
-
|
|
77
|
-
const open = useCallback((p: DependentAddOnDialogOpenPayload) => {
|
|
78
|
-
previouslyFocusedRef.current =
|
|
79
|
-
document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
80
|
-
setPayload(p);
|
|
81
|
-
setIsOpen(true);
|
|
82
|
-
}, []);
|
|
83
|
-
|
|
84
|
-
const close = useCallback(() => {
|
|
85
|
-
setIsOpen(false);
|
|
86
|
-
setPayload(null);
|
|
87
|
-
const prev = previouslyFocusedRef.current;
|
|
88
|
-
requestAnimationFrame(() => {
|
|
89
|
-
if (prev && typeof prev.focus === 'function') {
|
|
90
|
-
prev.focus();
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
}, []);
|
|
94
|
-
|
|
95
|
-
const value = useMemo(
|
|
96
|
-
() => ({ isOpen, payload, open, close }),
|
|
97
|
-
[isOpen, payload, open, close]
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
return (
|
|
101
|
-
<DependentAddOnDialogContext.Provider value={value}>
|
|
102
|
-
{children}
|
|
103
|
-
</DependentAddOnDialogContext.Provider>
|
|
104
|
-
);
|
|
105
|
-
}
|