@ticketboothapp/booking 0.1.18 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/BookingWidget.tsx +282 -26
- package/src/components/ManageBookingView.tsx +75 -23
- package/src/components/booking/BookingProductGrid.tsx +1 -1
- package/src/components/booking/Calendar.module.css +3 -3
- package/src/components/booking/CheckoutForm.tsx +1 -1
- package/src/components/booking/PickupLocationSelector.tsx +1 -1
- package/src/index.ts +3 -1
- package/src/app/photo-sessions/photo-packages.ts +0 -75
- 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/JobApplicationDialog.module.css +0 -440
- package/src/components/JobApplicationDialog.tsx +0 -620
- 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/PickupLocationMap.tsx +0 -110
- package/src/components/PostBookingDependentAddOnUpsell.module.css +0 -174
- package/src/components/PostBookingDependentAddOnUpsell.tsx +0 -407
- package/src/components/accordion.css +0 -27
- package/src/components/accordion.tsx +0 -29
- package/src/components/analytics/AnalyticsConsentRestore.tsx +0 -19
- package/src/components/analytics/AnalyticsScripts.tsx +0 -106
- package/src/components/analytics/CookieConsentBanner.css +0 -86
- package/src/components/analytics/CookieConsentBanner.tsx +0 -102
- package/src/components/bottom-sheet.module.css +0 -78
- package/src/components/bottom-sheet.tsx +0 -60
- package/src/components/breadcrumb.module.css +0 -40
- package/src/components/breadcrumb.tsx +0 -36
- package/src/components/button.css +0 -245
- package/src/components/button.tsx +0 -152
- package/src/components/client-bottom-sheet.tsx +0 -14
- package/src/components/colorable-svg.tsx +0 -29
- package/src/components/conditional-footer.tsx +0 -27
- package/src/components/contact-us.module.css +0 -147
- package/src/components/contact-us.tsx +0 -49
- package/src/components/email-signup.css +0 -151
- package/src/components/email-signup.tsx +0 -63
- package/src/components/faq-wrapper.module.css +0 -47
- package/src/components/faq-wrapper.tsx +0 -15
- package/src/components/footer.css +0 -187
- package/src/components/footer.tsx +0 -143
- package/src/components/global-simple-modal.tsx +0 -33
- package/src/components/google-review-summary.module.css +0 -77
- package/src/components/google-review-summary.tsx +0 -50
- package/src/components/hero-image.css +0 -13
- package/src/components/hero-image.tsx +0 -44
- package/src/components/image.css +0 -29
- package/src/components/image.tsx +0 -113
- package/src/components/language-aware-link.tsx +0 -72
- package/src/components/language-switcher.module.css +0 -124
- package/src/components/language-switcher.tsx +0 -75
- package/src/components/map-section.css +0 -59
- package/src/components/map-section.tsx +0 -63
- package/src/components/navbar.module.css +0 -152
- package/src/components/navbar.tsx +0 -125
- package/src/components/parallax-provider.tsx +0 -11
- package/src/components/product-tag.module.css +0 -30
- package/src/components/product-tag.tsx +0 -34
- package/src/components/product-theme-pages/best-option.module.css +0 -70
- package/src/components/product-theme-pages/best-option.tsx +0 -35
- package/src/components/product-theme-pages/extended-tour-options.module.css +0 -22
- package/src/components/product-theme-pages/extended-tour-options.tsx +0 -11
- 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/product-theme-pages/photo-gallery.tsx +0 -90
- package/src/components/product-theme-pages/product-theme-page-layout.module.css +0 -13
- package/src/components/product-theme-pages/product-theme-page-layout.tsx +0 -67
- package/src/components/product-theme-pages/top-of-fold.module.css +0 -179
- package/src/components/product-theme-pages/top-of-fold.tsx +0 -80
- package/src/components/product-tile/image-only-product-tile-desktop.module.css +0 -106
- package/src/components/product-tile/image-only-product-tile-desktop.tsx +0 -56
- package/src/components/product-tile/image-only-product-tile-mobile.module.css +0 -122
- package/src/components/product-tile/image-only-product-tile-mobile.tsx +0 -89
- package/src/components/product-tile/image-only-product-tile.tsx +0 -44
- package/src/components/product-tile/product-tile-card.module.css +0 -84
- package/src/components/product-tile/product-tile-card.tsx +0 -61
- package/src/components/review-highlights-section.css +0 -85
- package/src/components/review-highlights-section.tsx +0 -127
- package/src/components/season-closure-overlay.module.css +0 -99
- package/src/components/season-closure-overlay.tsx +0 -98
- package/src/components/simple-modal.tsx +0 -69
- package/src/components/simple-top-of-fold.module.css +0 -76
- package/src/components/simple-top-of-fold.tsx +0 -34
- package/src/components/spacer.css +0 -41
- package/src/components/spacer.tsx +0 -23
- package/src/components/star-rating.module.css +0 -74
- package/src/components/star-rating.tsx +0 -48
- package/src/components/terms/TermsContent.tsx +0 -178
- package/src/components/title-subtitle.module.css +0 -10
- package/src/components/title-subtitle.tsx +0 -30
- package/src/components/translatable-reviews.tsx +0 -75
- package/src/components/value-pill.module.css +0 -59
- package/src/components/value-pill.tsx +0 -46
- package/src/components/value-props.css +0 -185
- package/src/components/value-props.tsx +0 -88
- package/src/constants/booking-guide-quiz.ts +0 -64
- package/src/constants/contact-info.ts +0 -2
- package/src/constants/faq.ts +0 -44
- package/src/constants/images.ts +0 -556
- package/src/constants/json-ld/faq-json-ld.tsx +0 -170
- package/src/constants/json-ld/homepage-json-ld.tsx +0 -138
- package/src/constants/json-ld/job-posting-json-ld.tsx +0 -92
- package/src/constants/json-ld/organization-json-ld.tsx +0 -62
- package/src/constants/json-ld/page-json-ld.tsx +0 -6
- package/src/constants/json-ld/product-json-ld.tsx +0 -154
- package/src/constants/json-ld/review-json-ld.tsx +0 -377
- package/src/constants/navigation-links/footer-links.ts +0 -48
- package/src/constants/navigation-links/nav-bar-links.ts +0 -41
- package/src/constants/navigation-links/navigation-link.ts +0 -6
- package/src/constants/pill-values.ts +0 -210
- package/src/constants/products.ts +0 -155
- package/src/constants/quiz-recommendations.ts +0 -506
- package/src/constants/reviews.ts +0 -75
- package/src/constants/staff.ts +0 -197
- package/src/constants/value-props.ts +0 -58
- 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/hooks/use-bottom-sheet.tsx +0 -15
- package/src/hooks/use-simple-modal.tsx +0 -27
- package/src/hooks/useBookingSourceMetadataFromLocation.ts +0 -21
- package/src/hooks/useEmailSubscription.tsx +0 -103
- package/src/hooks/useEmbeddedInIframe.ts +0 -16
- package/src/hooks/useIsBookingLaunchLive.ts +0 -49
- package/src/hooks/useQuiz.tsx +0 -210
- package/src/providers/bottom-sheet-provider.tsx +0 -40
- 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 -97
- package/src/types/fareharbor.d.ts +0 -12
- package/src/types/quiz.ts +0 -59
- package/src/utils/currency-converter.ts +0 -101
|
@@ -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 '@/app/photo-sessions/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,27 +0,0 @@
|
|
|
1
|
-
.via-via-accordion-base {
|
|
2
|
-
background-color: var(--light-orange-background-dark);
|
|
3
|
-
box-shadow: none;
|
|
4
|
-
width: 100%;
|
|
5
|
-
box-sizing: border-box;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
.via-via-accordion-base button[data-slot="trigger"] {
|
|
9
|
-
cursor: pointer;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
.via-via-accordion-content {
|
|
13
|
-
width: 100%;
|
|
14
|
-
box-sizing: border-box;
|
|
15
|
-
cursor: pointer;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
.via-via-accordion-title {
|
|
19
|
-
color: var(--primary-text);
|
|
20
|
-
font-family: 'Figtree', sans-serif;
|
|
21
|
-
font-weight: 400;
|
|
22
|
-
font-size: 1.25rem;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.accordion-icon {
|
|
26
|
-
color: var(--accent-orange);
|
|
27
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import {Accordion, AccordionItem} from "@heroui/accordion";
|
|
4
|
-
import "./accordion.css";
|
|
5
|
-
import PlusIcon from "@/assets/icons/plus.svg";
|
|
6
|
-
import MinusIcon from "@/assets/icons/minus.svg";
|
|
7
|
-
import { faqItem } from "@/constants/faq";
|
|
8
|
-
|
|
9
|
-
export default function AccordionComponent({items, className, selectionMode}: {items: faqItem[], className: string, selectionMode: "single" | "multiple"}) {
|
|
10
|
-
return (
|
|
11
|
-
<Accordion
|
|
12
|
-
selectionMode={selectionMode}
|
|
13
|
-
variant="splitted"
|
|
14
|
-
className={className}
|
|
15
|
-
itemClasses={{
|
|
16
|
-
base: "via-via-accordion-base",
|
|
17
|
-
title: "via-via-accordion-title",
|
|
18
|
-
trigger: "bg-transparent hover:bg-transparent border-none outline-none",
|
|
19
|
-
content: "via-via-accordion-content",
|
|
20
|
-
}}
|
|
21
|
-
>
|
|
22
|
-
{items.map((item) => (
|
|
23
|
-
<AccordionItem key={item.question} aria-label={item.question} title={item.question} indicator={({isOpen}) => (isOpen ? <MinusIcon className="accordion-icon" /> : <PlusIcon className="accordion-icon" />)}>
|
|
24
|
-
<div dangerouslySetInnerHTML={{__html: item.answer}} />
|
|
25
|
-
</AccordionItem>
|
|
26
|
-
))}
|
|
27
|
-
</Accordion>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useEffect } from 'react';
|
|
4
|
-
import { updateAnalyticsConsent } from './AnalyticsScripts';
|
|
5
|
-
import { hasAnalyticsConsent } from '@/lib/analytics';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Restores analytics consent on load if user previously accepted.
|
|
9
|
-
* Must run after AnalyticsScripts have loaded gtag/fbq.
|
|
10
|
-
*/
|
|
11
|
-
export function AnalyticsConsentRestore() {
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
if (hasAnalyticsConsent()) {
|
|
14
|
-
updateAnalyticsConsent(true);
|
|
15
|
-
}
|
|
16
|
-
}, []);
|
|
17
|
-
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect } from 'react';
|
|
4
|
-
import Script from 'next/script';
|
|
5
|
-
import { ENV, isLocalhost, isProduction } from '@/lib/env';
|
|
6
|
-
import { setAnalyticsConsentGranted } from '@/lib/analytics';
|
|
7
|
-
|
|
8
|
-
const gaId = ENV.GA4_MEASUREMENT_ID;
|
|
9
|
-
const pixelId = ENV.META_PIXEL_ID;
|
|
10
|
-
|
|
11
|
-
/** Update GA4 and Meta consent. Call when user accepts cookies or on load if previously granted. */
|
|
12
|
-
export function updateAnalyticsConsent(granted: boolean): void {
|
|
13
|
-
if (granted) {
|
|
14
|
-
setAnalyticsConsentGranted();
|
|
15
|
-
if (typeof window !== 'undefined') {
|
|
16
|
-
if (window.gtag) {
|
|
17
|
-
window.gtag('consent', 'update', {
|
|
18
|
-
ad_user_data: 'granted',
|
|
19
|
-
ad_personalization: 'granted',
|
|
20
|
-
ad_storage: 'granted',
|
|
21
|
-
analytics_storage: 'granted',
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
if (window.fbq) {
|
|
25
|
-
window.fbq('consent', 'grant');
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function AnalyticsScripts() {
|
|
32
|
-
// Don't load on localhost (prod build testing) or when not production env
|
|
33
|
-
const [shouldLoad, setShouldLoad] = useState(false);
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
setShouldLoad(!isLocalhost() && isProduction() && (!!gaId || !!pixelId));
|
|
36
|
-
}, []);
|
|
37
|
-
|
|
38
|
-
if (!shouldLoad) return null;
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<>
|
|
42
|
-
{/* GA4 with Consent Mode v2 (default denied) */}
|
|
43
|
-
{gaId && (
|
|
44
|
-
<>
|
|
45
|
-
<Script
|
|
46
|
-
id="gtag-consent"
|
|
47
|
-
strategy="beforeInteractive"
|
|
48
|
-
dangerouslySetInnerHTML={{
|
|
49
|
-
__html: `
|
|
50
|
-
window.dataLayer = window.dataLayer || [];
|
|
51
|
-
function gtag(){dataLayer.push(arguments);}
|
|
52
|
-
gtag('js', new Date());
|
|
53
|
-
gtag('consent', 'default', {
|
|
54
|
-
ad_user_data: 'denied',
|
|
55
|
-
ad_personalization: 'denied',
|
|
56
|
-
ad_storage: 'denied',
|
|
57
|
-
analytics_storage: 'denied',
|
|
58
|
-
wait_for_update: 500
|
|
59
|
-
});
|
|
60
|
-
`,
|
|
61
|
-
}}
|
|
62
|
-
/>
|
|
63
|
-
<Script
|
|
64
|
-
src={`https://www.googletagmanager.com/gtag/js?id=${gaId}`}
|
|
65
|
-
strategy="afterInteractive"
|
|
66
|
-
/>
|
|
67
|
-
<Script
|
|
68
|
-
id="gtag-config"
|
|
69
|
-
strategy="afterInteractive"
|
|
70
|
-
dangerouslySetInnerHTML={{
|
|
71
|
-
__html: `
|
|
72
|
-
window.dataLayer = window.dataLayer || [];
|
|
73
|
-
function gtag(){dataLayer.push(arguments);}
|
|
74
|
-
gtag('config', '${gaId}', { anonymize_ip: true });
|
|
75
|
-
`,
|
|
76
|
-
}}
|
|
77
|
-
/>
|
|
78
|
-
</>
|
|
79
|
-
)}
|
|
80
|
-
|
|
81
|
-
{/* Meta Pixel with consent revoked by default */}
|
|
82
|
-
{pixelId && (
|
|
83
|
-
<Script
|
|
84
|
-
id="meta-pixel"
|
|
85
|
-
strategy="afterInteractive"
|
|
86
|
-
dangerouslySetInnerHTML={{
|
|
87
|
-
__html: `
|
|
88
|
-
!function(f,b,e,v,n,t,s)
|
|
89
|
-
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
|
90
|
-
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
|
|
91
|
-
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
|
|
92
|
-
n.queue=[];t=b.createElement(e);t.async=!0;
|
|
93
|
-
t.src=v;s=b.getElementsByTagName(e)[0];
|
|
94
|
-
s.parentNode.insertBefore(t,s)}(window, document,'script',
|
|
95
|
-
'https://connect.facebook.net/en_US/fbevents.js');
|
|
96
|
-
fbq('consent', 'revoke');
|
|
97
|
-
fbq('init', '${pixelId}');
|
|
98
|
-
fbq('track', 'PageView');
|
|
99
|
-
`,
|
|
100
|
-
}}
|
|
101
|
-
/>
|
|
102
|
-
)}
|
|
103
|
-
|
|
104
|
-
</>
|
|
105
|
-
);
|
|
106
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/* Cookie consent banner – customize appearance here */
|
|
2
|
-
|
|
3
|
-
.cookieConsentBanner {
|
|
4
|
-
position: fixed;
|
|
5
|
-
bottom: 0;
|
|
6
|
-
left: 0;
|
|
7
|
-
right: 0;
|
|
8
|
-
z-index: 50;
|
|
9
|
-
background-color: rgba(28, 25, 23, 0.9);
|
|
10
|
-
color: #ffffff;
|
|
11
|
-
padding: 1rem;
|
|
12
|
-
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
.cookieConsentBannerInner {
|
|
16
|
-
max-width: 850px;
|
|
17
|
-
margin-left: auto;
|
|
18
|
-
margin-right: auto;
|
|
19
|
-
display: flex;
|
|
20
|
-
flex-direction: column;
|
|
21
|
-
gap: var(--spacing-small);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
@media (min-width: 1023px) {
|
|
25
|
-
.cookieConsentBannerInner {
|
|
26
|
-
flex-direction: row;
|
|
27
|
-
align-items: center;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
.cookieConsentBannerText {
|
|
32
|
-
flex: 1 1 0%;
|
|
33
|
-
font-size: 0.875rem;
|
|
34
|
-
line-height: 1.25rem;
|
|
35
|
-
color: #ffffff;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
.cookieConsentBannerLink {
|
|
39
|
-
text-decoration: underline;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
.cookieConsentBannerLink:hover {
|
|
43
|
-
color: #34d399;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.cookieConsentBannerButtons {
|
|
47
|
-
display: flex;
|
|
48
|
-
gap: 0.75rem;
|
|
49
|
-
width: 100%;
|
|
50
|
-
justify-content: center;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
@media (min-width: 640px) {
|
|
54
|
-
.cookieConsentBannerButtons {
|
|
55
|
-
width: auto;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
.cookieConsentBannerDecline {
|
|
60
|
-
padding: 0.5rem 1rem;
|
|
61
|
-
font-size: 0.875rem;
|
|
62
|
-
border: 1px solid #57534e;
|
|
63
|
-
border-radius: 0.5rem;
|
|
64
|
-
background: transparent;
|
|
65
|
-
color: inherit;
|
|
66
|
-
cursor: pointer;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
.cookieConsentBannerDecline:hover {
|
|
70
|
-
background-color: #292524;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
.cookieConsentBannerAccept {
|
|
74
|
-
padding: 0.5rem 1rem;
|
|
75
|
-
font-size: 0.875rem;
|
|
76
|
-
font-weight: 500;
|
|
77
|
-
background-color: #059669;
|
|
78
|
-
color: white;
|
|
79
|
-
border: none;
|
|
80
|
-
border-radius: 0.5rem;
|
|
81
|
-
cursor: pointer;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.cookieConsentBannerAccept:hover {
|
|
85
|
-
background-color: #047857;
|
|
86
|
-
}
|