@ticketboothapp/booking 0.1.11 → 0.1.13
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/app/photo-sessions/photo-packages.ts +75 -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 +2072 -354
- package/src/components/BookingWidget.tsx +28 -248
- package/src/components/JobApplicationDialog.module.css +440 -0
- package/src/components/JobApplicationDialog.tsx +620 -0
- package/src/components/ManageBookingView.tsx +28 -36
- 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/PickupLocationMap.tsx +110 -0
- package/src/components/PostBookingDependentAddOnUpsell.module.css +174 -0
- package/src/components/PostBookingDependentAddOnUpsell.tsx +407 -0
- package/src/components/accordion.css +27 -0
- package/src/components/accordion.tsx +29 -0
- package/src/components/analytics/AnalyticsConsentRestore.tsx +19 -0
- package/src/components/analytics/AnalyticsScripts.tsx +106 -0
- package/src/components/analytics/CookieConsentBanner.css +86 -0
- package/src/components/analytics/CookieConsentBanner.tsx +102 -0
- package/src/components/booking/AddOnsSection.module.css +10 -0
- package/src/components/booking/AddOnsSection.tsx +184 -0
- package/src/components/booking/AdminPaymentChoiceModal.tsx +98 -0
- package/src/components/booking/BookingDialog.module.css +643 -0
- package/src/components/booking/BookingDialog.tsx +356 -0
- package/src/components/booking/BookingFlow.tsx +4385 -0
- package/src/components/booking/BookingFlowCollage.module.css +148 -0
- package/src/components/booking/BookingFlowCollage.tsx +184 -0
- package/src/components/booking/BookingFlowPlaceholder.module.css +27 -0
- package/src/components/booking/BookingFlowPlaceholder.tsx +25 -0
- package/src/components/booking/BookingFlowPreview.tsx +51 -0
- package/src/components/booking/BookingProductGrid.module.css +359 -0
- package/src/components/booking/BookingProductGrid.tsx +497 -0
- package/src/components/booking/Calendar.module.css +616 -0
- package/src/components/{Calendar.tsx → booking/Calendar.tsx} +464 -247
- package/src/components/booking/CancellationPolicySelector.module.css +124 -0
- package/src/components/booking/CancellationPolicySelector.tsx +142 -0
- package/src/components/booking/ChangeBookingDialog.tsx +562 -0
- package/src/components/booking/CheckoutForm.module.css +244 -0
- package/src/components/booking/CheckoutForm.tsx +364 -0
- package/src/components/{CheckoutModal.tsx → booking/CheckoutModal.tsx} +176 -19
- package/src/components/booking/DapFlowCollage.tsx +88 -0
- package/src/components/booking/DapTourDescription.tsx +35 -0
- package/src/components/booking/DependentAddOnBookingDialog.tsx +1350 -0
- package/src/components/booking/DependentAddOnPaymentForm.tsx +124 -0
- package/src/components/booking/InfoTooltip.tsx +108 -0
- package/src/components/booking/ItineraryBox.module.css +258 -0
- package/src/components/booking/ItineraryBox.tsx +550 -0
- package/src/components/{ItineraryBuilder.tsx → booking/ItineraryBuilder.tsx} +1 -2
- package/src/components/booking/ItineraryPlaceholder.module.css +45 -0
- package/src/components/booking/ItineraryPlaceholder.tsx +26 -0
- package/src/components/{MealDrinkAddOnSelector.tsx → booking/MealDrinkAddOnSelector.tsx} +21 -13
- package/src/components/booking/PickupLocationSelector.module.css +124 -0
- package/src/components/{PickupLocationSelector.tsx → booking/PickupLocationSelector.tsx} +315 -290
- package/src/components/booking/PickupTimeSelector.module.css +134 -0
- package/src/components/booking/PickupTimeSelector.tsx +112 -0
- package/src/components/{PriceBreakdown.tsx → booking/PriceBreakdown.tsx} +3 -3
- package/src/components/{PriceSummary.tsx → booking/PriceSummary.tsx} +51 -28
- package/src/components/booking/PrivateShuttleBookingFlow.module.css +357 -0
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +2662 -0
- package/src/components/booking/PromoCodeInput.module.css +166 -0
- package/src/components/booking/PromoCodeInput.tsx +99 -0
- package/src/components/booking/ReturnTimeSelector.module.css +173 -0
- package/src/components/booking/ReturnTimeSelector.tsx +145 -0
- package/src/components/{TermsAcceptance.tsx → booking/TermsAcceptance.tsx} +9 -8
- package/src/components/booking/TicketSelector.module.css +164 -0
- package/src/components/booking/TicketSelector.tsx +199 -0
- package/src/components/booking/TourDescription.module.css +304 -0
- package/src/components/booking/TourDescription.tsx +273 -0
- package/src/components/booking/booking-flow-ui.ts +15 -1
- package/src/components/booking/booking-flow.css +944 -0
- package/src/components/bottom-sheet.module.css +78 -0
- package/src/components/bottom-sheet.tsx +60 -0
- package/src/components/breadcrumb.module.css +40 -0
- package/src/components/breadcrumb.tsx +36 -0
- package/src/components/button.css +245 -0
- package/src/components/button.tsx +152 -0
- package/src/components/client-bottom-sheet.tsx +14 -0
- package/src/components/colorable-svg.tsx +29 -0
- package/src/components/conditional-footer.tsx +27 -0
- package/src/components/contact-us.module.css +147 -0
- package/src/components/contact-us.tsx +49 -0
- package/src/components/email-signup.css +151 -0
- package/src/components/email-signup.tsx +63 -0
- package/src/components/faq-wrapper.module.css +47 -0
- package/src/components/faq-wrapper.tsx +15 -0
- package/src/components/footer.css +187 -0
- package/src/components/footer.tsx +143 -0
- package/src/components/global-simple-modal.tsx +33 -0
- package/src/components/google-review-summary.module.css +77 -0
- package/src/components/google-review-summary.tsx +50 -0
- package/src/components/hero-image.css +13 -0
- package/src/components/hero-image.tsx +44 -0
- package/src/components/image.css +29 -0
- package/src/components/image.tsx +113 -0
- package/src/components/language-aware-link.tsx +72 -0
- package/src/components/language-switcher.module.css +124 -0
- package/src/components/language-switcher.tsx +75 -0
- package/src/components/map-section.css +59 -0
- package/src/components/map-section.tsx +63 -0
- package/src/components/navbar.module.css +152 -0
- package/src/components/navbar.tsx +125 -0
- package/src/components/parallax-provider.tsx +11 -0
- package/src/components/partner/PartnerBookingPage.module.css +130 -0
- package/src/components/partner/PartnerBookingPage.tsx +390 -0
- package/src/components/partner/PartnerBookingPageWithBrowserMetadata.tsx +19 -35
- package/src/components/product-tag.module.css +30 -0
- package/src/components/product-tag.tsx +34 -0
- package/src/components/product-theme-pages/best-option.module.css +70 -0
- package/src/components/product-theme-pages/best-option.tsx +35 -0
- package/src/components/product-theme-pages/extended-tour-options.module.css +22 -0
- package/src/components/product-theme-pages/extended-tour-options.tsx +11 -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/product-theme-pages/photo-gallery.tsx +90 -0
- package/src/components/product-theme-pages/product-theme-page-layout.module.css +13 -0
- package/src/components/product-theme-pages/product-theme-page-layout.tsx +67 -0
- package/src/components/product-theme-pages/top-of-fold.module.css +179 -0
- package/src/components/product-theme-pages/top-of-fold.tsx +80 -0
- package/src/components/product-tile/image-only-product-tile-desktop.module.css +106 -0
- package/src/components/product-tile/image-only-product-tile-desktop.tsx +56 -0
- package/src/components/product-tile/image-only-product-tile-mobile.module.css +122 -0
- package/src/components/product-tile/image-only-product-tile-mobile.tsx +89 -0
- package/src/components/product-tile/image-only-product-tile.tsx +44 -0
- package/src/components/product-tile/product-tile-card.module.css +84 -0
- package/src/components/product-tile/product-tile-card.tsx +61 -0
- package/src/components/review-highlights-section.css +85 -0
- package/src/components/review-highlights-section.tsx +127 -0
- package/src/components/season-closure-overlay.module.css +99 -0
- package/src/components/season-closure-overlay.tsx +98 -0
- package/src/components/simple-modal.tsx +69 -0
- package/src/components/simple-top-of-fold.module.css +76 -0
- package/src/components/simple-top-of-fold.tsx +34 -0
- package/src/components/spacer.css +41 -0
- package/src/components/spacer.tsx +23 -0
- package/src/components/star-rating.module.css +74 -0
- package/src/components/star-rating.tsx +48 -0
- package/src/components/terms/TermsContent.tsx +178 -0
- package/src/components/title-subtitle.module.css +10 -0
- package/src/components/title-subtitle.tsx +30 -0
- package/src/components/translatable-reviews.tsx +75 -0
- package/src/components/value-pill.module.css +59 -0
- package/src/components/value-pill.tsx +46 -0
- package/src/components/value-props.css +185 -0
- package/src/components/value-props.tsx +88 -0
- package/src/constants/booking-guide-quiz.ts +64 -0
- package/src/constants/contact-info.ts +2 -0
- package/src/constants/faq.ts +44 -0
- package/src/constants/images.ts +556 -0
- package/src/constants/json-ld/faq-json-ld.tsx +170 -0
- package/src/constants/json-ld/homepage-json-ld.tsx +138 -0
- package/src/constants/json-ld/job-posting-json-ld.tsx +92 -0
- package/src/constants/json-ld/organization-json-ld.tsx +62 -0
- package/src/constants/json-ld/page-json-ld.tsx +6 -0
- package/src/constants/json-ld/product-json-ld.tsx +154 -0
- package/src/constants/json-ld/review-json-ld.tsx +377 -0
- package/src/constants/navigation-links/footer-links.ts +48 -0
- package/src/constants/navigation-links/nav-bar-links.ts +41 -0
- package/src/constants/navigation-links/navigation-link.ts +6 -0
- package/src/constants/pill-values.ts +210 -0
- package/src/constants/products.ts +155 -0
- package/src/constants/quiz-recommendations.ts +506 -0
- package/src/constants/reviews.ts +75 -0
- package/src/constants/staff.ts +197 -0
- package/src/constants/value-props.ts +58 -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/use-bottom-sheet.tsx +15 -0
- package/src/hooks/use-simple-modal.tsx +27 -0
- package/src/hooks/useBookingSourceMetadataFromLocation.ts +21 -0
- package/src/hooks/useEmailSubscription.tsx +103 -0
- package/src/hooks/useEmbeddedInIframe.ts +16 -0
- package/src/hooks/useIsBookingLaunchLive.ts +49 -0
- package/src/hooks/useQuiz.tsx +210 -0
- package/src/index.ts +27 -2
- package/src/lib/analytics.ts +197 -0
- package/src/lib/booking/booking-source.ts +20 -2
- package/src/lib/{checkout-breakdown.ts → booking/checkout-breakdown.ts} +1 -1
- package/src/lib/booking/correlation-id.ts +46 -0
- package/src/lib/{i18n → booking/i18n}/messages/en.json +48 -4
- package/src/lib/{i18n → booking/i18n}/messages/fr.json +48 -4
- package/src/lib/booking/itinerary-display.ts +36 -0
- package/src/lib/{itinerary-labels.ts → booking/itinerary-labels.ts} +1 -1
- package/src/lib/{location-calculations.ts → booking/location-calculations.ts} +4 -4
- package/src/lib/{location-utils.ts → booking/location-utils.ts} +26 -0
- package/src/lib/{map-utils.ts → booking/map-utils.ts} +3 -3
- package/src/lib/booking/normalize-booking-product-id.ts +7 -0
- package/src/lib/{pickup-location-types.ts → booking/pickup-location-types.ts} +2 -2
- package/src/lib/{pricing.ts → booking/pricing.ts} +2 -2
- package/src/lib/booking/product-option-id.ts +35 -0
- package/src/lib/booking/source-metadata.ts +72 -7
- package/src/lib/booking/sunday-week.ts +14 -0
- package/src/lib/booking/trace-context.ts +62 -0
- package/src/lib/booking-api.ts +1793 -0
- package/src/lib/{constants.ts → booking-constants.ts} +11 -5
- package/src/lib/booking-types.ts +36 -0
- package/src/lib/currency.ts +38 -45
- package/src/lib/dap-descriptions.ts +50 -0
- package/src/lib/dap-itinerary-preview.ts +315 -0
- package/src/lib/dependent-add-on-api.ts +434 -0
- package/src/lib/env.ts +89 -5
- package/src/lib/firebase.ts +20 -0
- package/src/lib/job-application-api.ts +83 -0
- package/src/lib/manage-booking-embed-print.ts +16 -0
- package/src/lib/manage-booking-post-checkout.ts +68 -0
- package/src/lib/photo-dap-config.ts +228 -0
- package/src/lib/pickup/map-utils.ts +56 -0
- package/src/lib/pickup/marker-icons.ts +19 -0
- package/src/lib/product-descriptions.ts +66 -0
- package/src/lib/products-config.ts +73 -0
- package/src/providers/booking-dialog-provider.tsx +107 -38
- package/src/providers/bottom-sheet-provider.tsx +40 -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 +97 -0
- package/src/types/fareharbor.d.ts +12 -0
- package/src/types/quiz.ts +59 -0
- package/src/utils/currency-converter.ts +101 -0
- package/src/components/BookingFlow.tsx +0 -2952
- package/src/components/LanguageSwitcher.tsx +0 -30
- package/src/components/PrivateShuttleBookingFlow.tsx +0 -2290
- package/src/components/ProductList.tsx +0 -78
- package/src/components/WhatsAppPhoneInput.tsx +0 -224
- package/src/components/index.ts +0 -31
- package/src/lib/api.ts +0 -801
- package/src/lib/booking-api-auth.ts +0 -9
- package/src/lib/checkout-breakdown.test.ts +0 -70
- package/src/types/google-maps.d.ts +0 -2
- /package/src/components/{CurrencySwitcher.tsx → booking/CurrencySwitcher.tsx} +0 -0
- /package/src/components/{ErrorBoundary.tsx → booking/ErrorBoundary.tsx} +0 -0
- /package/src/lib/{i18n → booking/i18n}/config.ts +0 -0
- /package/src/lib/{i18n → booking/i18n}/index.tsx +0 -0
- /package/src/lib/{marker-icons.ts → booking/marker-icons.ts} +0 -0
- /package/src/lib/{places-api.ts → booking/places-api.ts} +0 -0
- /package/src/lib/{theme.ts → booking/theme.ts} +0 -0
- /package/src/lib/{utils.ts → booking/utils.ts} +0 -0
|
@@ -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,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
|
+
}
|