@ticketboothapp/booking 0.1.10 → 0.1.12

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 (252) hide show
  1. package/package.json +1 -1
  2. package/src/colours.css +23 -0
  3. package/src/components/BookingDetails.module.css +1591 -0
  4. package/src/components/BookingDetails.tsx +2072 -354
  5. package/src/components/BookingWidget.tsx +28 -248
  6. package/src/components/JobApplicationDialog.module.css +440 -0
  7. package/src/components/JobApplicationDialog.tsx +620 -0
  8. package/src/components/ManageBookingView.tsx +344 -34
  9. package/src/components/PhoneInputWithCountry.module.css +131 -0
  10. package/src/components/PhoneInputWithCountry.tsx +44 -0
  11. package/src/components/PickupLocationDialog.module.css +360 -0
  12. package/src/components/PickupLocationDialog.tsx +357 -0
  13. package/src/components/PickupLocationMap.tsx +110 -0
  14. package/src/components/PostBookingDependentAddOnUpsell.module.css +174 -0
  15. package/src/components/PostBookingDependentAddOnUpsell.tsx +407 -0
  16. package/src/components/accordion.css +27 -0
  17. package/src/components/accordion.tsx +29 -0
  18. package/src/components/analytics/AnalyticsConsentRestore.tsx +19 -0
  19. package/src/components/analytics/AnalyticsScripts.tsx +106 -0
  20. package/src/components/analytics/CookieConsentBanner.css +86 -0
  21. package/src/components/analytics/CookieConsentBanner.tsx +102 -0
  22. package/src/components/booking/AddOnsSection.module.css +10 -0
  23. package/src/components/booking/AddOnsSection.tsx +184 -0
  24. package/src/components/booking/AdminPaymentChoiceModal.tsx +98 -0
  25. package/src/components/booking/BookingDialog.module.css +643 -0
  26. package/src/components/booking/BookingDialog.tsx +356 -0
  27. package/src/components/booking/BookingFlow.tsx +4385 -0
  28. package/src/components/booking/BookingFlowCollage.module.css +148 -0
  29. package/src/components/booking/BookingFlowCollage.tsx +184 -0
  30. package/src/components/booking/BookingFlowPlaceholder.module.css +27 -0
  31. package/src/components/booking/BookingFlowPlaceholder.tsx +25 -0
  32. package/src/components/booking/BookingFlowPreview.tsx +51 -0
  33. package/src/components/booking/BookingProductGrid.module.css +359 -0
  34. package/src/components/booking/BookingProductGrid.tsx +497 -0
  35. package/src/components/booking/Calendar.module.css +616 -0
  36. package/src/components/{Calendar.tsx → booking/Calendar.tsx} +464 -247
  37. package/src/components/booking/CancellationPolicySelector.module.css +124 -0
  38. package/src/components/booking/CancellationPolicySelector.tsx +142 -0
  39. package/src/components/booking/ChangeBookingDialog.tsx +562 -0
  40. package/src/components/booking/CheckoutForm.module.css +244 -0
  41. package/src/components/booking/CheckoutForm.tsx +364 -0
  42. package/src/components/{CheckoutModal.tsx → booking/CheckoutModal.tsx} +176 -19
  43. package/src/components/booking/DapFlowCollage.tsx +88 -0
  44. package/src/components/booking/DapTourDescription.tsx +35 -0
  45. package/src/components/booking/DependentAddOnBookingDialog.tsx +1350 -0
  46. package/src/components/booking/DependentAddOnPaymentForm.tsx +124 -0
  47. package/src/components/booking/InfoTooltip.tsx +108 -0
  48. package/src/components/booking/ItineraryBox.module.css +258 -0
  49. package/src/components/booking/ItineraryBox.tsx +550 -0
  50. package/src/components/{ItineraryBuilder.tsx → booking/ItineraryBuilder.tsx} +1 -2
  51. package/src/components/booking/ItineraryPlaceholder.module.css +45 -0
  52. package/src/components/booking/ItineraryPlaceholder.tsx +26 -0
  53. package/src/components/{MealDrinkAddOnSelector.tsx → booking/MealDrinkAddOnSelector.tsx} +21 -13
  54. package/src/components/booking/PickupLocationSelector.module.css +124 -0
  55. package/src/components/{PickupLocationSelector.tsx → booking/PickupLocationSelector.tsx} +315 -290
  56. package/src/components/booking/PickupTimeSelector.module.css +134 -0
  57. package/src/components/booking/PickupTimeSelector.tsx +112 -0
  58. package/src/components/{PriceBreakdown.tsx → booking/PriceBreakdown.tsx} +3 -3
  59. package/src/components/{PriceSummary.tsx → booking/PriceSummary.tsx} +51 -28
  60. package/src/components/booking/PrivateShuttleBookingFlow.module.css +357 -0
  61. package/src/components/booking/PrivateShuttleBookingFlow.tsx +2662 -0
  62. package/src/components/booking/PromoCodeInput.module.css +166 -0
  63. package/src/components/booking/PromoCodeInput.tsx +99 -0
  64. package/src/components/booking/ReturnTimeSelector.module.css +173 -0
  65. package/src/components/booking/ReturnTimeSelector.tsx +145 -0
  66. package/src/components/{TermsAcceptance.tsx → booking/TermsAcceptance.tsx} +9 -8
  67. package/src/components/booking/TicketSelector.module.css +164 -0
  68. package/src/components/booking/TicketSelector.tsx +199 -0
  69. package/src/components/booking/TourDescription.module.css +304 -0
  70. package/src/components/booking/TourDescription.tsx +273 -0
  71. package/src/components/booking/booking-flow-ui.ts +15 -1
  72. package/src/components/booking/booking-flow.css +944 -0
  73. package/src/components/bottom-sheet.module.css +78 -0
  74. package/src/components/bottom-sheet.tsx +60 -0
  75. package/src/components/breadcrumb.module.css +40 -0
  76. package/src/components/breadcrumb.tsx +36 -0
  77. package/src/components/button.css +245 -0
  78. package/src/components/button.tsx +152 -0
  79. package/src/components/client-bottom-sheet.tsx +14 -0
  80. package/src/components/colorable-svg.tsx +29 -0
  81. package/src/components/conditional-footer.tsx +27 -0
  82. package/src/components/contact-us.module.css +147 -0
  83. package/src/components/contact-us.tsx +49 -0
  84. package/src/components/email-signup.css +151 -0
  85. package/src/components/email-signup.tsx +63 -0
  86. package/src/components/faq-wrapper.module.css +47 -0
  87. package/src/components/faq-wrapper.tsx +15 -0
  88. package/src/components/footer.css +187 -0
  89. package/src/components/footer.tsx +143 -0
  90. package/src/components/global-simple-modal.tsx +33 -0
  91. package/src/components/google-review-summary.module.css +77 -0
  92. package/src/components/google-review-summary.tsx +50 -0
  93. package/src/components/hero-image.css +13 -0
  94. package/src/components/hero-image.tsx +44 -0
  95. package/src/components/image.css +29 -0
  96. package/src/components/image.tsx +113 -0
  97. package/src/components/language-aware-link.tsx +72 -0
  98. package/src/components/language-switcher.module.css +124 -0
  99. package/src/components/language-switcher.tsx +75 -0
  100. package/src/components/map-section.css +59 -0
  101. package/src/components/map-section.tsx +63 -0
  102. package/src/components/navbar.module.css +152 -0
  103. package/src/components/navbar.tsx +125 -0
  104. package/src/components/parallax-provider.tsx +11 -0
  105. package/src/components/partner/PartnerBookingPage.module.css +130 -0
  106. package/src/components/partner/PartnerBookingPage.tsx +390 -0
  107. package/src/components/partner/PartnerBookingPageWithBrowserMetadata.tsx +19 -35
  108. package/src/components/product-tag.module.css +30 -0
  109. package/src/components/product-tag.tsx +34 -0
  110. package/src/components/product-theme-pages/best-option.module.css +70 -0
  111. package/src/components/product-theme-pages/best-option.tsx +35 -0
  112. package/src/components/product-theme-pages/extended-tour-options.module.css +22 -0
  113. package/src/components/product-theme-pages/extended-tour-options.tsx +11 -0
  114. package/src/components/product-theme-pages/image-modal.tsx +248 -0
  115. package/src/components/product-theme-pages/photo-gallery.module.css +200 -0
  116. package/src/components/product-theme-pages/photo-gallery.tsx +90 -0
  117. package/src/components/product-theme-pages/product-theme-page-layout.module.css +13 -0
  118. package/src/components/product-theme-pages/product-theme-page-layout.tsx +67 -0
  119. package/src/components/product-theme-pages/top-of-fold.module.css +179 -0
  120. package/src/components/product-theme-pages/top-of-fold.tsx +80 -0
  121. package/src/components/product-tile/image-only-product-tile-desktop.module.css +106 -0
  122. package/src/components/product-tile/image-only-product-tile-desktop.tsx +56 -0
  123. package/src/components/product-tile/image-only-product-tile-mobile.module.css +122 -0
  124. package/src/components/product-tile/image-only-product-tile-mobile.tsx +89 -0
  125. package/src/components/product-tile/image-only-product-tile.tsx +44 -0
  126. package/src/components/product-tile/product-tile-card.module.css +84 -0
  127. package/src/components/product-tile/product-tile-card.tsx +61 -0
  128. package/src/components/review-highlights-section.css +85 -0
  129. package/src/components/review-highlights-section.tsx +127 -0
  130. package/src/components/season-closure-overlay.module.css +99 -0
  131. package/src/components/season-closure-overlay.tsx +98 -0
  132. package/src/components/simple-modal.tsx +69 -0
  133. package/src/components/simple-top-of-fold.module.css +76 -0
  134. package/src/components/simple-top-of-fold.tsx +34 -0
  135. package/src/components/spacer.css +41 -0
  136. package/src/components/spacer.tsx +23 -0
  137. package/src/components/star-rating.module.css +74 -0
  138. package/src/components/star-rating.tsx +48 -0
  139. package/src/components/terms/TermsContent.tsx +178 -0
  140. package/src/components/title-subtitle.module.css +10 -0
  141. package/src/components/title-subtitle.tsx +30 -0
  142. package/src/components/translatable-reviews.tsx +75 -0
  143. package/src/components/value-pill.module.css +59 -0
  144. package/src/components/value-pill.tsx +46 -0
  145. package/src/components/value-props.css +185 -0
  146. package/src/components/value-props.tsx +88 -0
  147. package/src/constants/booking-guide-quiz.ts +64 -0
  148. package/src/constants/contact-info.ts +2 -0
  149. package/src/constants/faq.ts +44 -0
  150. package/src/constants/images.ts +556 -0
  151. package/src/constants/json-ld/faq-json-ld.tsx +170 -0
  152. package/src/constants/json-ld/homepage-json-ld.tsx +138 -0
  153. package/src/constants/json-ld/job-posting-json-ld.tsx +92 -0
  154. package/src/constants/json-ld/organization-json-ld.tsx +62 -0
  155. package/src/constants/json-ld/page-json-ld.tsx +6 -0
  156. package/src/constants/json-ld/product-json-ld.tsx +154 -0
  157. package/src/constants/json-ld/review-json-ld.tsx +377 -0
  158. package/src/constants/navigation-links/footer-links.ts +48 -0
  159. package/src/constants/navigation-links/nav-bar-links.ts +41 -0
  160. package/src/constants/navigation-links/navigation-link.ts +6 -0
  161. package/src/constants/pill-values.ts +210 -0
  162. package/src/constants/products.ts +155 -0
  163. package/src/constants/quiz-recommendations.ts +506 -0
  164. package/src/constants/reviews.ts +75 -0
  165. package/src/constants/staff.ts +197 -0
  166. package/src/constants/value-props.ts +58 -0
  167. package/src/data/dap-descriptions/session-couples-families-friends.en.json +61 -0
  168. package/src/data/dap-descriptions/session-elopements.en.json +60 -0
  169. package/src/data/dap-descriptions/session-proposals.en.json +60 -0
  170. package/src/data/product-descriptions/afternoon-delight.en.json +35 -0
  171. package/src/data/product-descriptions/emerald-lake-escape.en.json +68 -0
  172. package/src/data/product-descriptions/lake-louise-adventure.en.json +74 -0
  173. package/src/data/product-descriptions/moraine-lake-adventure.en.json +78 -0
  174. package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +65 -0
  175. package/src/data/product-descriptions/moraine-lake-sunrise.en.json +64 -0
  176. package/src/data/product-descriptions/private-tour.en.json +80 -0
  177. package/src/data/product-descriptions/two-lakes-combo.en.json +65 -0
  178. package/src/data/products-config.json +101 -0
  179. package/src/hooks/use-bottom-sheet.tsx +15 -0
  180. package/src/hooks/use-simple-modal.tsx +27 -0
  181. package/src/hooks/useBookingSourceMetadataFromLocation.ts +21 -0
  182. package/src/hooks/useEmailSubscription.tsx +103 -0
  183. package/src/hooks/useEmbeddedInIframe.ts +16 -0
  184. package/src/hooks/useIsBookingLaunchLive.ts +49 -0
  185. package/src/hooks/useQuiz.tsx +210 -0
  186. package/src/index.ts +27 -2
  187. package/src/lib/analytics.ts +197 -0
  188. package/src/lib/booking/booking-source.ts +20 -2
  189. package/src/lib/{checkout-breakdown.ts → booking/checkout-breakdown.ts} +1 -1
  190. package/src/lib/booking/correlation-id.ts +46 -0
  191. package/src/lib/{i18n → booking/i18n}/messages/en.json +48 -4
  192. package/src/lib/{i18n → booking/i18n}/messages/fr.json +48 -4
  193. package/src/lib/booking/itinerary-display.ts +36 -0
  194. package/src/lib/{itinerary-labels.ts → booking/itinerary-labels.ts} +1 -1
  195. package/src/lib/{location-calculations.ts → booking/location-calculations.ts} +4 -4
  196. package/src/lib/{location-utils.ts → booking/location-utils.ts} +26 -0
  197. package/src/lib/{map-utils.ts → booking/map-utils.ts} +3 -3
  198. package/src/lib/booking/normalize-booking-product-id.ts +7 -0
  199. package/src/lib/{pickup-location-types.ts → booking/pickup-location-types.ts} +2 -2
  200. package/src/lib/{pricing.ts → booking/pricing.ts} +2 -2
  201. package/src/lib/booking/product-option-id.ts +35 -0
  202. package/src/lib/booking/source-metadata.ts +72 -7
  203. package/src/lib/booking/sunday-week.ts +14 -0
  204. package/src/lib/booking/trace-context.ts +62 -0
  205. package/src/lib/booking-api.ts +1793 -0
  206. package/src/lib/{constants.ts → booking-constants.ts} +11 -5
  207. package/src/lib/booking-types.ts +36 -0
  208. package/src/lib/currency.ts +38 -45
  209. package/src/lib/dap-descriptions.ts +50 -0
  210. package/src/lib/dap-itinerary-preview.ts +315 -0
  211. package/src/lib/dependent-add-on-api.ts +434 -0
  212. package/src/lib/env.ts +89 -5
  213. package/src/lib/firebase.ts +20 -0
  214. package/src/lib/job-application-api.ts +83 -0
  215. package/src/lib/manage-booking-embed-print.ts +16 -0
  216. package/src/lib/manage-booking-post-checkout.ts +68 -0
  217. package/src/lib/photo-dap-config.ts +228 -0
  218. package/src/lib/pickup/map-utils.ts +56 -0
  219. package/src/lib/pickup/marker-icons.ts +19 -0
  220. package/src/lib/product-descriptions.ts +66 -0
  221. package/src/lib/products-config.ts +73 -0
  222. package/src/providers/booking-dialog-provider.tsx +107 -38
  223. package/src/providers/bottom-sheet-provider.tsx +40 -0
  224. package/src/providers/dependent-add-on-dialog-provider.tsx +105 -0
  225. package/src/radius.css +5 -0
  226. package/src/spacing.css +7 -0
  227. package/src/strings/en.json +1774 -0
  228. package/src/strings/es.json +1573 -0
  229. package/src/strings/fr.json +1573 -0
  230. package/src/strings/index.js +23 -0
  231. package/src/text-style.css +97 -0
  232. package/src/types/fareharbor.d.ts +12 -0
  233. package/src/types/quiz.ts +59 -0
  234. package/src/utils/currency-converter.ts +101 -0
  235. package/src/components/BookingFlow.tsx +0 -2952
  236. package/src/components/LanguageSwitcher.tsx +0 -30
  237. package/src/components/PrivateShuttleBookingFlow.tsx +0 -2290
  238. package/src/components/ProductList.tsx +0 -78
  239. package/src/components/WhatsAppPhoneInput.tsx +0 -224
  240. package/src/components/index.ts +0 -31
  241. package/src/lib/api.ts +0 -801
  242. package/src/lib/booking-api-auth.ts +0 -9
  243. package/src/lib/checkout-breakdown.test.ts +0 -70
  244. package/src/types/google-maps.d.ts +0 -2
  245. /package/src/components/{CurrencySwitcher.tsx → booking/CurrencySwitcher.tsx} +0 -0
  246. /package/src/components/{ErrorBoundary.tsx → booking/ErrorBoundary.tsx} +0 -0
  247. /package/src/lib/{i18n → booking/i18n}/config.ts +0 -0
  248. /package/src/lib/{i18n → booking/i18n}/index.tsx +0 -0
  249. /package/src/lib/{marker-icons.ts → booking/marker-icons.ts} +0 -0
  250. /package/src/lib/{places-api.ts → booking/places-api.ts} +0 -0
  251. /package/src/lib/{theme.ts → booking/theme.ts} +0 -0
  252. /package/src/lib/{utils.ts → booking/utils.ts} +0 -0
@@ -0,0 +1,29 @@
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
+ }
@@ -0,0 +1,19 @@
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
+ }
@@ -0,0 +1,106 @@
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
+ }
@@ -0,0 +1,86 @@
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
+ }
@@ -0,0 +1,102 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { usePathname } from 'next/navigation';
5
+ import { useEmbeddedInIframe } from '@/hooks/useEmbeddedInIframe';
6
+ import { updateAnalyticsConsent } from './AnalyticsScripts';
7
+ import { ENV, isLocalhost, isProduction, isStaging } from '@/lib/env';
8
+ import Link from 'next/link';
9
+ import './CookieConsentBanner.css';
10
+
11
+ const CONSENT_KEY = 'cookie-consent';
12
+
13
+ /** Show banner when we load GA4/Meta (production), or on localhost/staging for testing. */
14
+ function shouldShowBanner(): boolean {
15
+ const hasAnalyticsIds = !!ENV.GA4_MEASUREMENT_ID || !!ENV.META_PIXEL_ID;
16
+ if (isProduction() && !isLocalhost() && hasAnalyticsIds) return true;
17
+ if (isLocalhost() || isStaging()) return true; // show for testing
18
+ return false;
19
+ }
20
+
21
+ export function CookieConsentBanner() {
22
+ const pathname = usePathname();
23
+ const embeddedInIframe = useEmbeddedInIframe();
24
+ const [mounted, setMounted] = useState(false);
25
+ const [showBanner, setShowBanner] = useState(false);
26
+
27
+ useEffect(() => {
28
+ setMounted(true);
29
+ }, []);
30
+
31
+ useEffect(() => {
32
+ if (!mounted || typeof window === 'undefined') return;
33
+ if (!shouldShowBanner()) return;
34
+ try {
35
+ // On localhost/staging: always show for testing. On production: only when user hasn't accepted.
36
+ const alwaysShowForTesting = isLocalhost() || isStaging();
37
+ const stored = localStorage.getItem(CONSENT_KEY);
38
+ setShowBanner(alwaysShowForTesting || stored !== 'granted');
39
+ } catch {
40
+ setShowBanner(true);
41
+ }
42
+ }, [mounted]);
43
+
44
+ // Signal to other UI (e.g. floating book button) that banner is visible, so they can position above it
45
+ useEffect(() => {
46
+ if (typeof document === 'undefined') return;
47
+ if (showBanner && !embeddedInIframe && pathname !== '/live-pickups') {
48
+ document.body.dataset.cookieBannerVisible = 'true';
49
+ } else {
50
+ delete document.body.dataset.cookieBannerVisible;
51
+ }
52
+ return () => {
53
+ delete document.body.dataset.cookieBannerVisible;
54
+ };
55
+ }, [showBanner, embeddedInIframe, pathname]);
56
+
57
+ const handleAccept = () => {
58
+ updateAnalyticsConsent(true);
59
+ setShowBanner(false);
60
+ };
61
+
62
+ const handleDecline = () => {
63
+ try {
64
+ localStorage.setItem(CONSENT_KEY, 'denied');
65
+ } catch {
66
+ /* ignore */
67
+ }
68
+ setShowBanner(false);
69
+ };
70
+
71
+ if (embeddedInIframe || pathname === '/live-pickups' || !showBanner || !mounted) return null;
72
+
73
+ return (
74
+ <div
75
+ className="cookieConsentBanner"
76
+ role="dialog"
77
+ aria-label="Cookie consent"
78
+ >
79
+ <div className="cookieConsentBannerInner">
80
+ <p className="cookieConsentBannerText">
81
+ Accept to approve the use of cookies for analytics and advertising. See our <Link href="/privacy-policy" className="cookieConsentBannerLink">privacy policy</Link> for details.
82
+ </p>
83
+ <div className="cookieConsentBannerButtons">
84
+ <button
85
+ type="button"
86
+ onClick={handleDecline}
87
+ className="cookieConsentBannerDecline"
88
+ >
89
+ Decline
90
+ </button>
91
+ <button
92
+ type="button"
93
+ onClick={handleAccept}
94
+ className="cookieConsentBannerAccept"
95
+ >
96
+ Accept
97
+ </button>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ );
102
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Add-ons section - label styled like tickets, cancellation policy (via booking-flow.css preflight)
3
+ */
4
+
5
+ .label {
6
+ display: block;
7
+ font-size: 0.875rem;
8
+ font-weight: 500;
9
+ color: var(--booking-stone-700, #44403c);
10
+ }
@@ -0,0 +1,184 @@
1
+ 'use client';
2
+
3
+ import { formatCurrencyAmount } from '@/lib/currency';
4
+ import type { AddOn } from '@/lib/booking-api';
5
+ import { MealDrinkAddOnSelector, canUseMealDrinkSelector } from './MealDrinkAddOnSelector';
6
+ import type { Currency } from './CurrencySwitcher';
7
+ import styles from './AddOnsSection.module.css';
8
+
9
+ export type AddOnSelection = { addOnId: string; variantId?: string; quantity?: number };
10
+
11
+ interface AddOnsSectionProps {
12
+ addOns: AddOn[];
13
+ addOnSelections: AddOnSelection[];
14
+ currency: Currency;
15
+ locale: string;
16
+ onSelectionsChange: (selections: AddOnSelection[] | ((prev: AddOnSelection[]) => AddOnSelection[])) => void;
17
+ minimumTotalByAddOnId?: Map<string, number>;
18
+ }
19
+
20
+ export function AddOnsSection({
21
+ addOns,
22
+ addOnSelections,
23
+ currency,
24
+ locale,
25
+ onSelectionsChange,
26
+ minimumTotalByAddOnId,
27
+ }: AddOnsSectionProps) {
28
+ return (
29
+ <div className="border-t border-stone-200 pt-6 space-y-4">
30
+ <label className={styles.label}>
31
+ Add-ons
32
+ </label>
33
+ {addOns.map((addOn) => {
34
+ const minTotalForAddOn = minimumTotalByAddOnId?.get(addOn.addOnId) ?? 0;
35
+ if (addOn.variantType === 'none') {
36
+ const isSelected = addOnSelections.some((s) => s.addOnId === addOn.addOnId);
37
+ const canToggleOff = minTotalForAddOn <= 0;
38
+ return (
39
+ <div key={addOn.addOnId}>
40
+ <label className="block text-sm font-medium text-stone-700 mb-2">{addOn.name}</label>
41
+ {addOn.description && (
42
+ <p className="text-sm text-stone-500 mb-2">{addOn.description}</p>
43
+ )}
44
+ <button
45
+ type="button"
46
+ onClick={() => {
47
+ onSelectionsChange((prev) => {
48
+ if (isSelected) {
49
+ if (!canToggleOff) return prev;
50
+ return prev.filter((s) => s.addOnId !== addOn.addOnId);
51
+ }
52
+ return [...prev, { addOnId: addOn.addOnId, quantity: 1 }];
53
+ });
54
+ }}
55
+ className={`flex items-center justify-between w-full p-4 rounded-lg border-2 text-left transition-colors ${
56
+ isSelected ? 'border-emerald-500 bg-emerald-50' : 'border-stone-200 bg-white hover:border-stone-300'
57
+ }`}
58
+ >
59
+ <span className="font-medium text-stone-900">
60
+ {isSelected ? `Yes, add ${addOn.name}` : `No ${addOn.name}`}
61
+ </span>
62
+ <span className="text-sm font-semibold text-stone-700">
63
+ +{formatCurrencyAmount(addOn.price ?? 0, currency, locale as 'en' | 'fr')}
64
+ </span>
65
+ </button>
66
+ </div>
67
+ );
68
+ }
69
+ if (addOn.variantType === 'multi_quantity' && addOn.variants?.length) {
70
+ if (canUseMealDrinkSelector(addOn)) {
71
+ return (
72
+ <MealDrinkAddOnSelector
73
+ key={addOn.addOnId}
74
+ addOn={addOn}
75
+ selections={addOnSelections}
76
+ onSelectionsChange={onSelectionsChange}
77
+ currency={currency}
78
+ locale={locale as 'en' | 'fr'}
79
+ minimumTotal={minTotalForAddOn}
80
+ />
81
+ );
82
+ }
83
+ return (
84
+ <div key={addOn.addOnId}>
85
+ <label className="block text-sm font-medium text-stone-700 mb-2">{addOn.name}</label>
86
+ {addOn.description && (
87
+ <p className="text-sm text-stone-500 mb-2">{addOn.description}</p>
88
+ )}
89
+ <div className="space-y-2">
90
+ {addOn.variants.map((variant) => {
91
+ const sel = addOnSelections.find((s) => s.addOnId === addOn.addOnId && s.variantId === variant.id);
92
+ const qty = sel?.quantity ?? 0;
93
+ const currentTotalForAddOn = addOnSelections
94
+ .filter((s) => s.addOnId === addOn.addOnId)
95
+ .reduce((sum, s) => sum + Math.max(1, Number(s.quantity) || 1), 0);
96
+ const canDecrement = qty > 0 && currentTotalForAddOn > minTotalForAddOn;
97
+ const price = (addOn.price ?? 0) + (variant.priceAdjustment ?? 0);
98
+ return (
99
+ <div key={variant.id} className="flex items-center gap-3 p-3 bg-stone-50 rounded-lg">
100
+ <span className="text-sm text-stone-800">{variant.label}</span>
101
+ <div className="flex items-center gap-2 shrink-0">
102
+ <button
103
+ type="button"
104
+ onClick={() => {
105
+ onSelectionsChange((prev) => {
106
+ const next = Math.max(0, qty - 1);
107
+ const rest = prev.filter((s) => !(s.addOnId === addOn.addOnId && s.variantId === variant.id));
108
+ if (next > 0) return [...rest, { addOnId: addOn.addOnId, variantId: variant.id, quantity: next }];
109
+ return rest;
110
+ });
111
+ }}
112
+ disabled={!canDecrement}
113
+ className="h-8 w-8 rounded-full border border-stone-300 bg-white text-stone-600 hover:bg-stone-50 disabled:opacity-50 disabled:cursor-not-allowed text-sm"
114
+ >
115
+
116
+ </button>
117
+ <span className="w-6 text-center font-medium tabular-nums text-sm">{qty}</span>
118
+ <button
119
+ type="button"
120
+ onClick={() => {
121
+ onSelectionsChange((prev) => {
122
+ const rest = prev.filter((s) => !(s.addOnId === addOn.addOnId && s.variantId === variant.id));
123
+ return [...rest, { addOnId: addOn.addOnId, variantId: variant.id, quantity: qty + 1 }];
124
+ });
125
+ }}
126
+ className="h-8 w-8 rounded-full border border-stone-300 bg-white text-stone-600 hover:bg-stone-50 text-sm"
127
+ >
128
+ +
129
+ </button>
130
+ <span className="text-sm font-medium text-stone-700 w-16 text-right">
131
+ {formatCurrencyAmount(price, currency, locale as 'en' | 'fr')} ea
132
+ </span>
133
+ </div>
134
+ </div>
135
+ );
136
+ })}
137
+ </div>
138
+ </div>
139
+ );
140
+ }
141
+ if (addOn.variantType === 'single_choice' && addOn.variants?.length) {
142
+ const sel = addOnSelections.find((s) => s.addOnId === addOn.addOnId);
143
+ const hasLockedInitialSelection = minTotalForAddOn > 0;
144
+ return (
145
+ <div key={addOn.addOnId}>
146
+ <label className="block text-sm font-medium text-stone-700 mb-2">{addOn.name}</label>
147
+ {addOn.description && (
148
+ <p className="text-sm text-stone-500 mb-2">{addOn.description}</p>
149
+ )}
150
+ <div className="space-y-2">
151
+ {addOn.variants.map((variant) => {
152
+ const isSelected = sel?.variantId === variant.id;
153
+ const price = (addOn.price ?? 0) + (variant.priceAdjustment ?? 0);
154
+ return (
155
+ <button
156
+ key={variant.id}
157
+ type="button"
158
+ onClick={() => {
159
+ onSelectionsChange((prev) => {
160
+ const rest = prev.filter((s) => s.addOnId !== addOn.addOnId);
161
+ if (isSelected) return hasLockedInitialSelection ? prev : rest;
162
+ return [...rest, { addOnId: addOn.addOnId, variantId: variant.id, quantity: 1 }];
163
+ });
164
+ }}
165
+ className={`flex items-center justify-between w-full p-3 rounded-lg border-2 text-left transition-colors ${
166
+ isSelected ? 'border-emerald-500 bg-emerald-50' : 'border-stone-200 bg-white hover:border-stone-300'
167
+ }`}
168
+ >
169
+ <span className="text-sm font-medium text-stone-800">{variant.label}</span>
170
+ <span className="text-sm font-semibold text-stone-700">
171
+ +{formatCurrencyAmount(price, currency, locale as 'en' | 'fr')}
172
+ </span>
173
+ </button>
174
+ );
175
+ })}
176
+ </div>
177
+ </div>
178
+ );
179
+ }
180
+ return null;
181
+ })}
182
+ </div>
183
+ );
184
+ }
@@ -0,0 +1,98 @@
1
+ 'use client';
2
+
3
+ import { createPortal } from 'react-dom';
4
+ import { formatCurrencyAmount } from '@/lib/currency';
5
+ import type { Currency } from './CurrencySwitcher';
6
+
7
+ interface AdminPaymentChoiceModalProps {
8
+ open: boolean;
9
+ totalAmount: number;
10
+ currency: Currency;
11
+ loading: boolean;
12
+ error: string;
13
+ onPayNow: () => void;
14
+ onConfirmWithoutPayment: () => void;
15
+ onCancel: () => void;
16
+ }
17
+
18
+ /**
19
+ * Provider / staff: pay now vs confirm without payment.
20
+ * Uses the same overlay + card + Tailwind button pattern as {@link CheckoutModal}
21
+ * so `.booking-flow-preflight button { background: transparent }` does not strip primary styles.
22
+ */
23
+ export function AdminPaymentChoiceModal({
24
+ open,
25
+ totalAmount,
26
+ currency,
27
+ loading,
28
+ error,
29
+ onPayNow,
30
+ onConfirmWithoutPayment,
31
+ onCancel,
32
+ }: AdminPaymentChoiceModalProps) {
33
+ if (!open) return null;
34
+
35
+ const modal = (
36
+ <div
37
+ className="booking-flow-root booking-flow-preflight fixed inset-0 z-[10050] flex items-center justify-center p-4 bg-black/50"
38
+ style={{ zIndex: 100_000 }}
39
+ >
40
+ <div className="bg-white rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-hidden flex flex-col">
41
+ <div className="p-6 border-b border-stone-200 flex-shrink-0">
42
+ <div className="flex justify-between items-start gap-3">
43
+ <h3 className="text-lg font-semibold text-stone-900 pr-2">
44
+ Complete booking
45
+ </h3>
46
+ <button
47
+ type="button"
48
+ onClick={onCancel}
49
+ className="text-stone-400 hover:text-stone-600 p-1 shrink-0 rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500/40"
50
+ aria-label="Close"
51
+ >
52
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
53
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
54
+ </svg>
55
+ </button>
56
+ </div>
57
+ <p className="mt-3 text-sm text-stone-600 leading-relaxed">
58
+ Pay now, or confirm without payment. The customer can pay the full balance from the Manage Booking page.
59
+ </p>
60
+ </div>
61
+
62
+ <div className="p-6 flex flex-col gap-3 flex-1 min-h-0">
63
+ {error ? (
64
+ <p className="text-sm text-red-600" role="alert">
65
+ {error}
66
+ </p>
67
+ ) : null}
68
+
69
+ <button
70
+ type="button"
71
+ onClick={onPayNow}
72
+ disabled={loading}
73
+ className="w-full py-3 px-4 bg-emerald-600 text-white font-semibold rounded-lg hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed"
74
+ >
75
+ {loading ? 'Loading...' : `Pay now (${formatCurrencyAmount(totalAmount, currency)})`}
76
+ </button>
77
+ <button
78
+ type="button"
79
+ onClick={onConfirmWithoutPayment}
80
+ disabled={loading}
81
+ className="w-full py-3 px-4 border border-stone-300 text-stone-700 rounded-lg hover:bg-stone-50 font-medium disabled:opacity-50 disabled:cursor-not-allowed"
82
+ >
83
+ Confirm without payment
84
+ </button>
85
+ <button
86
+ type="button"
87
+ onClick={onCancel}
88
+ disabled={loading}
89
+ className="w-full py-2 text-sm text-stone-500 hover:text-stone-700 font-medium disabled:opacity-50"
90
+ >
91
+ Cancel
92
+ </button>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ );
97
+ return typeof document !== 'undefined' ? createPortal(modal, document.body) : null;
98
+ }