@ticketboothapp/booking 0.1.19 → 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.
Files changed (154) hide show
  1. package/package.json +1 -1
  2. package/src/components/BookingWidget.tsx +282 -26
  3. package/src/components/ManageBookingView.tsx +75 -23
  4. package/src/components/booking/BookingProductGrid.tsx +1 -1
  5. package/src/components/booking/Calendar.module.css +3 -3
  6. package/src/components/booking/CheckoutForm.tsx +1 -1
  7. package/src/components/booking/InfoTooltip.tsx +2 -13
  8. package/src/components/booking/PickupLocationSelector.tsx +2 -2
  9. package/src/components/booking/PriceBreakdown.tsx +11 -34
  10. package/src/index.ts +3 -1
  11. package/src/app/photo-sessions/photo-packages.ts +0 -75
  12. package/src/assets/icons/minus.svg +0 -7
  13. package/src/assets/icons/partner-logos/getyourguide.svg +0 -8
  14. package/src/assets/icons/plus.svg +0 -3
  15. package/src/colours.css +0 -23
  16. package/src/components/BookingDetails.module.css +0 -1591
  17. package/src/components/BookingDetails.tsx +0 -2264
  18. package/src/components/JobApplicationDialog.module.css +0 -440
  19. package/src/components/JobApplicationDialog.tsx +0 -620
  20. package/src/components/PhoneInputWithCountry.module.css +0 -131
  21. package/src/components/PhoneInputWithCountry.tsx +0 -44
  22. package/src/components/PickupLocationDialog.module.css +0 -360
  23. package/src/components/PickupLocationDialog.tsx +0 -357
  24. package/src/components/PickupLocationMap.tsx +0 -110
  25. package/src/components/PostBookingDependentAddOnUpsell.module.css +0 -174
  26. package/src/components/PostBookingDependentAddOnUpsell.tsx +0 -407
  27. package/src/components/accordion.css +0 -27
  28. package/src/components/accordion.tsx +0 -29
  29. package/src/components/analytics/AnalyticsConsentRestore.tsx +0 -19
  30. package/src/components/analytics/AnalyticsScripts.tsx +0 -106
  31. package/src/components/analytics/CookieConsentBanner.css +0 -86
  32. package/src/components/analytics/CookieConsentBanner.tsx +0 -102
  33. package/src/components/bottom-sheet.module.css +0 -78
  34. package/src/components/bottom-sheet.tsx +0 -60
  35. package/src/components/breadcrumb.module.css +0 -40
  36. package/src/components/breadcrumb.tsx +0 -36
  37. package/src/components/button.css +0 -245
  38. package/src/components/button.tsx +0 -152
  39. package/src/components/client-bottom-sheet.tsx +0 -14
  40. package/src/components/colorable-svg.tsx +0 -29
  41. package/src/components/conditional-footer.tsx +0 -27
  42. package/src/components/contact-us.module.css +0 -147
  43. package/src/components/contact-us.tsx +0 -49
  44. package/src/components/email-signup.css +0 -151
  45. package/src/components/email-signup.tsx +0 -63
  46. package/src/components/faq-wrapper.module.css +0 -47
  47. package/src/components/faq-wrapper.tsx +0 -15
  48. package/src/components/footer.css +0 -187
  49. package/src/components/footer.tsx +0 -143
  50. package/src/components/global-simple-modal.tsx +0 -33
  51. package/src/components/google-review-summary.module.css +0 -77
  52. package/src/components/google-review-summary.tsx +0 -50
  53. package/src/components/hero-image.css +0 -13
  54. package/src/components/hero-image.tsx +0 -44
  55. package/src/components/image.css +0 -29
  56. package/src/components/image.tsx +0 -113
  57. package/src/components/language-aware-link.tsx +0 -72
  58. package/src/components/language-switcher.module.css +0 -124
  59. package/src/components/language-switcher.tsx +0 -75
  60. package/src/components/map-section.css +0 -59
  61. package/src/components/map-section.tsx +0 -63
  62. package/src/components/navbar.module.css +0 -152
  63. package/src/components/navbar.tsx +0 -125
  64. package/src/components/parallax-provider.tsx +0 -11
  65. package/src/components/product-tag.module.css +0 -30
  66. package/src/components/product-tag.tsx +0 -34
  67. package/src/components/product-theme-pages/best-option.module.css +0 -70
  68. package/src/components/product-theme-pages/best-option.tsx +0 -35
  69. package/src/components/product-theme-pages/extended-tour-options.module.css +0 -22
  70. package/src/components/product-theme-pages/extended-tour-options.tsx +0 -11
  71. package/src/components/product-theme-pages/image-modal.tsx +0 -248
  72. package/src/components/product-theme-pages/photo-gallery.module.css +0 -200
  73. package/src/components/product-theme-pages/photo-gallery.tsx +0 -90
  74. package/src/components/product-theme-pages/product-theme-page-layout.module.css +0 -13
  75. package/src/components/product-theme-pages/product-theme-page-layout.tsx +0 -67
  76. package/src/components/product-theme-pages/top-of-fold.module.css +0 -179
  77. package/src/components/product-theme-pages/top-of-fold.tsx +0 -80
  78. package/src/components/product-tile/image-only-product-tile-desktop.module.css +0 -106
  79. package/src/components/product-tile/image-only-product-tile-desktop.tsx +0 -56
  80. package/src/components/product-tile/image-only-product-tile-mobile.module.css +0 -122
  81. package/src/components/product-tile/image-only-product-tile-mobile.tsx +0 -89
  82. package/src/components/product-tile/image-only-product-tile.tsx +0 -44
  83. package/src/components/product-tile/product-tile-card.module.css +0 -84
  84. package/src/components/product-tile/product-tile-card.tsx +0 -61
  85. package/src/components/review-highlights-section.css +0 -85
  86. package/src/components/review-highlights-section.tsx +0 -127
  87. package/src/components/season-closure-overlay.module.css +0 -99
  88. package/src/components/season-closure-overlay.tsx +0 -98
  89. package/src/components/simple-modal.tsx +0 -69
  90. package/src/components/simple-top-of-fold.module.css +0 -76
  91. package/src/components/simple-top-of-fold.tsx +0 -34
  92. package/src/components/spacer.css +0 -41
  93. package/src/components/spacer.tsx +0 -23
  94. package/src/components/star-rating.module.css +0 -74
  95. package/src/components/star-rating.tsx +0 -48
  96. package/src/components/terms/TermsContent.tsx +0 -178
  97. package/src/components/title-subtitle.module.css +0 -10
  98. package/src/components/title-subtitle.tsx +0 -30
  99. package/src/components/translatable-reviews.tsx +0 -75
  100. package/src/components/value-pill.module.css +0 -59
  101. package/src/components/value-pill.tsx +0 -46
  102. package/src/components/value-props.css +0 -185
  103. package/src/components/value-props.tsx +0 -88
  104. package/src/constants/booking-guide-quiz.ts +0 -64
  105. package/src/constants/contact-info.ts +0 -2
  106. package/src/constants/faq.ts +0 -44
  107. package/src/constants/images.ts +0 -556
  108. package/src/constants/json-ld/faq-json-ld.tsx +0 -170
  109. package/src/constants/json-ld/homepage-json-ld.tsx +0 -138
  110. package/src/constants/json-ld/job-posting-json-ld.tsx +0 -92
  111. package/src/constants/json-ld/organization-json-ld.tsx +0 -62
  112. package/src/constants/json-ld/page-json-ld.tsx +0 -6
  113. package/src/constants/json-ld/product-json-ld.tsx +0 -154
  114. package/src/constants/json-ld/review-json-ld.tsx +0 -377
  115. package/src/constants/navigation-links/footer-links.ts +0 -48
  116. package/src/constants/navigation-links/nav-bar-links.ts +0 -41
  117. package/src/constants/navigation-links/navigation-link.ts +0 -6
  118. package/src/constants/pill-values.ts +0 -210
  119. package/src/constants/products.ts +0 -155
  120. package/src/constants/quiz-recommendations.ts +0 -506
  121. package/src/constants/reviews.ts +0 -75
  122. package/src/constants/staff.ts +0 -197
  123. package/src/constants/value-props.ts +0 -58
  124. package/src/data/dap-descriptions/session-couples-families-friends.en.json +0 -61
  125. package/src/data/dap-descriptions/session-elopements.en.json +0 -60
  126. package/src/data/dap-descriptions/session-proposals.en.json +0 -60
  127. package/src/data/product-descriptions/afternoon-delight.en.json +0 -35
  128. package/src/data/product-descriptions/emerald-lake-escape.en.json +0 -68
  129. package/src/data/product-descriptions/lake-louise-adventure.en.json +0 -74
  130. package/src/data/product-descriptions/moraine-lake-adventure.en.json +0 -78
  131. package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +0 -65
  132. package/src/data/product-descriptions/moraine-lake-sunrise.en.json +0 -64
  133. package/src/data/product-descriptions/private-tour.en.json +0 -80
  134. package/src/data/product-descriptions/two-lakes-combo.en.json +0 -65
  135. package/src/data/products-config.json +0 -101
  136. package/src/hooks/use-bottom-sheet.tsx +0 -15
  137. package/src/hooks/use-simple-modal.tsx +0 -27
  138. package/src/hooks/useBookingSourceMetadataFromLocation.ts +0 -21
  139. package/src/hooks/useEmailSubscription.tsx +0 -103
  140. package/src/hooks/useEmbeddedInIframe.ts +0 -16
  141. package/src/hooks/useIsBookingLaunchLive.ts +0 -49
  142. package/src/hooks/useQuiz.tsx +0 -210
  143. package/src/providers/bottom-sheet-provider.tsx +0 -40
  144. package/src/providers/dependent-add-on-dialog-provider.tsx +0 -105
  145. package/src/radius.css +0 -5
  146. package/src/spacing.css +0 -7
  147. package/src/strings/en.json +0 -1774
  148. package/src/strings/es.json +0 -1573
  149. package/src/strings/fr.json +0 -1573
  150. package/src/strings/index.js +0 -23
  151. package/src/text-style.css +0 -97
  152. package/src/types/fareharbor.d.ts +0 -12
  153. package/src/types/quiz.ts +0 -59
  154. 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
- }