@ticketboothapp/booking 0.1.20 → 0.1.23
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 +2 -1
- package/src/assets/icons/minus.svg +7 -0
- package/src/assets/icons/partner-logos/getyourguide.svg +8 -0
- package/src/assets/icons/plus.svg +3 -0
- package/src/colours.css +23 -0
- package/src/components/BookingDetails.module.css +1591 -0
- package/src/components/BookingDetails.tsx +2264 -0
- package/src/components/PhoneInputWithCountry.module.css +131 -0
- package/src/components/PhoneInputWithCountry.tsx +44 -0
- package/src/components/PickupLocationDialog.module.css +360 -0
- package/src/components/PickupLocationDialog.tsx +357 -0
- package/src/components/PostBookingDependentAddOnUpsell.module.css +174 -0
- package/src/components/PostBookingDependentAddOnUpsell.tsx +407 -0
- package/src/components/button.css +245 -0
- package/src/components/button.tsx +152 -0
- package/src/components/colorable-svg.tsx +29 -0
- package/src/components/image.css +29 -0
- package/src/components/image.tsx +113 -0
- package/src/components/product-tag.module.css +30 -0
- package/src/components/product-tag.tsx +34 -0
- package/src/components/product-theme-pages/image-modal.tsx +248 -0
- package/src/components/product-theme-pages/photo-gallery.module.css +200 -0
- package/src/components/terms/TermsContent.tsx +178 -0
- package/src/components/value-pill.module.css +59 -0
- package/src/components/value-pill.tsx +46 -0
- package/src/constants/images.ts +556 -0
- package/src/constants/pill-values.ts +210 -0
- package/src/constants/products.ts +155 -0
- package/src/data/dap-descriptions/session-couples-families-friends.en.json +61 -0
- package/src/data/dap-descriptions/session-elopements.en.json +60 -0
- package/src/data/dap-descriptions/session-proposals.en.json +60 -0
- package/src/data/product-descriptions/afternoon-delight.en.json +35 -0
- package/src/data/product-descriptions/emerald-lake-escape.en.json +68 -0
- package/src/data/product-descriptions/lake-louise-adventure.en.json +74 -0
- package/src/data/product-descriptions/moraine-lake-adventure.en.json +78 -0
- package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +65 -0
- package/src/data/product-descriptions/moraine-lake-sunrise.en.json +64 -0
- package/src/data/product-descriptions/private-tour.en.json +80 -0
- package/src/data/product-descriptions/two-lakes-combo.en.json +65 -0
- package/src/data/products-config.json +101 -0
- package/src/hooks/useBookingSourceMetadataFromLocation.ts +21 -0
- package/src/hooks/useIsBookingLaunchLive.ts +49 -0
- package/src/lib/photo-packages.ts +75 -0
- package/src/providers/dependent-add-on-dialog-provider.tsx +105 -0
- package/src/radius.css +5 -0
- package/src/spacing.css +7 -0
- package/src/strings/en.json +1774 -0
- package/src/strings/es.json +1573 -0
- package/src/strings/fr.json +1573 -0
- package/src/strings/index.js +23 -0
- package/src/text-style.css +56 -0
- package/src/utils/currency-converter.ts +101 -0
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,407 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
.button {
|
|
2
|
+
padding: 8px 16px;
|
|
3
|
+
border-radius: 24px;
|
|
4
|
+
border: none;
|
|
5
|
+
font-size: 1rem;
|
|
6
|
+
cursor: pointer;
|
|
7
|
+
transition: all 0.2s ease-in-out;
|
|
8
|
+
font-weight: 500;
|
|
9
|
+
position: relative;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
z-index: 1;
|
|
12
|
+
font-family: 'Poppins', sans-serif;
|
|
13
|
+
font-weight: 700;
|
|
14
|
+
text-transform: lowercase;
|
|
15
|
+
display: inline-block;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.button-primary {
|
|
19
|
+
background-color: var(--accent-orange);
|
|
20
|
+
color: var(--accent-white);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.button-secondary {
|
|
24
|
+
background-color: var(--accent-white);
|
|
25
|
+
color: var(--accent-orange);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.button-disabled {
|
|
29
|
+
background-color: var(--grey-text);
|
|
30
|
+
color: var(--accent-white);
|
|
31
|
+
cursor: not-allowed;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.button-disabled:hover {
|
|
35
|
+
background-color: var(--grey-text);
|
|
36
|
+
color: var(--accent-white) !important;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.button-outline {
|
|
40
|
+
background-color: transparent;
|
|
41
|
+
color: var(--accent-white);
|
|
42
|
+
border: 2px solid var(--accent-white);
|
|
43
|
+
box-sizing: border-box;
|
|
44
|
+
padding: 6px 14px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.button-large {
|
|
48
|
+
font-size: 1.2rem;
|
|
49
|
+
padding: 1rem 2rem;
|
|
50
|
+
border-radius: 40px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Desktop hover */
|
|
54
|
+
@media (min-width: 1024px) {
|
|
55
|
+
.button::before {
|
|
56
|
+
content: '';
|
|
57
|
+
position: absolute;
|
|
58
|
+
top: 0;
|
|
59
|
+
left: -100%;
|
|
60
|
+
width: 100%;
|
|
61
|
+
height: 100%;
|
|
62
|
+
border-radius: 24px;
|
|
63
|
+
transition: all 0.3s ease-in-out;
|
|
64
|
+
z-index: -1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.button-primary:hover::before {
|
|
68
|
+
left: 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.button-primary.hover-white::before {
|
|
72
|
+
background-color: var(--accent-white);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.button-primary.hover-turquoise::before {
|
|
76
|
+
background-color: var(--accent-turquoise);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.button-primary.hover-orange::before {
|
|
80
|
+
background-color: var(--accent-orange);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.button-primary.hover-orange:hover {
|
|
84
|
+
color: var(--accent-white);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.button-primary.hover-turquoise:hover {
|
|
88
|
+
color: var(--accent-white);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.button-secondary:hover::before {
|
|
92
|
+
left: 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.button-secondary::before {
|
|
96
|
+
background-color: var(--accent-turquoise);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.button-secondary.hover-orange::before {
|
|
100
|
+
background-color: var(--accent-orange);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.button-secondary.hover-orange:hover {
|
|
104
|
+
color: var(--accent-white) !important;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.button-secondary:hover {
|
|
108
|
+
color: var(--accent-white) !important;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.button-outline::before {
|
|
112
|
+
background-color: var(--accent-white);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.button-outline:hover::before {
|
|
116
|
+
left: 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.button:hover {
|
|
120
|
+
color: var(--accent-orange);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Focus state for accessibility */
|
|
125
|
+
.button:focus {
|
|
126
|
+
outline: 2px solid var(--accent-orange);
|
|
127
|
+
outline-offset: 2px;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.button-menu {
|
|
131
|
+
background-color: var(--accent-orange);
|
|
132
|
+
border-radius: 50px;
|
|
133
|
+
padding: 8px 20px;
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
gap: 8px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.button-menu .menu-text {
|
|
140
|
+
color: white;
|
|
141
|
+
font-weight: 400;
|
|
142
|
+
text-transform: lowercase;
|
|
143
|
+
font-size: 0.8rem;
|
|
144
|
+
padding: 0.2rem;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.button-menu .menu-icon {
|
|
148
|
+
display: flex;
|
|
149
|
+
flex-direction: column;
|
|
150
|
+
gap: 3px;
|
|
151
|
+
position: relative;
|
|
152
|
+
width: 12px;
|
|
153
|
+
height: 12px;
|
|
154
|
+
transition: transform 0.3s ease;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.button-menu .menu-icon span {
|
|
158
|
+
display: block;
|
|
159
|
+
width: 12px;
|
|
160
|
+
height: 1.5px;
|
|
161
|
+
background-color: white;
|
|
162
|
+
border-radius: 2px;
|
|
163
|
+
position: absolute;
|
|
164
|
+
transition: transform 0.3s ease, opacity 0.3s ease;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Position the lines */
|
|
168
|
+
.button-menu .menu-icon span:nth-child(1) {
|
|
169
|
+
top: 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.button-menu .menu-icon span:nth-child(2) {
|
|
173
|
+
top: 50%;
|
|
174
|
+
transform: translateY(-50%);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.button-menu .menu-icon span:nth-child(3) {
|
|
178
|
+
bottom: 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* X animation when open with full rotation */
|
|
182
|
+
.button-menu.open .menu-icon {
|
|
183
|
+
transform: rotate(360deg);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.button-menu.open .menu-icon span:nth-child(1) {
|
|
187
|
+
top: 50%;
|
|
188
|
+
transform: translateY(-50%) rotate(45deg);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.button-menu.open .menu-icon span:nth-child(2) {
|
|
192
|
+
opacity: 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.button-menu.open .menu-icon span:nth-child(3) {
|
|
196
|
+
top: 50%;
|
|
197
|
+
transform: translateY(-50%) rotate(-45deg);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Override booking-flow-preflight so Button component styles apply inside the booking flow */
|
|
201
|
+
.booking-flow-preflight button.button {
|
|
202
|
+
padding: 8px 16px;
|
|
203
|
+
border-radius: 24px;
|
|
204
|
+
border: none;
|
|
205
|
+
font-size: 1rem;
|
|
206
|
+
cursor: pointer;
|
|
207
|
+
transition: all 0.2s ease-in-out;
|
|
208
|
+
font-weight: 700;
|
|
209
|
+
position: relative;
|
|
210
|
+
overflow: hidden;
|
|
211
|
+
z-index: 1;
|
|
212
|
+
font-family: 'Poppins', sans-serif;
|
|
213
|
+
text-transform: lowercase;
|
|
214
|
+
display: inline-block;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.booking-flow-preflight button.button-primary {
|
|
218
|
+
background-color: var(--accent-orange);
|
|
219
|
+
color: var(--accent-white);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.booking-flow-preflight button.button-secondary {
|
|
223
|
+
background-color: var(--accent-white);
|
|
224
|
+
color: var(--accent-orange);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.booking-flow-preflight button.button-disabled {
|
|
228
|
+
background-color: var(--grey-text);
|
|
229
|
+
color: var(--accent-white);
|
|
230
|
+
cursor: not-allowed;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.booking-flow-preflight button.button-outline {
|
|
234
|
+
background-color: transparent;
|
|
235
|
+
color: var(--accent-white);
|
|
236
|
+
border: 2px solid var(--accent-white);
|
|
237
|
+
box-sizing: border-box;
|
|
238
|
+
padding: 6px 14px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.booking-flow-preflight button.button-large {
|
|
242
|
+
font-size: 1.2rem;
|
|
243
|
+
padding: 1rem 2rem;
|
|
244
|
+
border-radius: 40px;
|
|
245
|
+
}
|