@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,550 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Fragment, useRef, useEffect, useState } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import type { ItineraryDisplayStep } from '@/lib/booking-api';
|
|
6
|
+
import { ItineraryStepType as StepType } from '@/lib/booking-api';
|
|
7
|
+
import { getStepLabel } from '@/lib/booking/itinerary-labels';
|
|
8
|
+
import type { PickupLocation } from '@/lib/booking-api';
|
|
9
|
+
import type { DapItineraryStep } from '@/lib/dap-itinerary-preview';
|
|
10
|
+
import { getPhotoSessionInsertPosition } from '@/lib/dap-itinerary-preview';
|
|
11
|
+
import styles from './ItineraryBox.module.css';
|
|
12
|
+
|
|
13
|
+
type TranslationFn = (key: string, params?: Record<string, string>) => string;
|
|
14
|
+
|
|
15
|
+
const TOOLTIP_TEXT = 'Approximate time - will be finalized when you select a pickup location';
|
|
16
|
+
|
|
17
|
+
function TooltipAnchor({
|
|
18
|
+
showTooltip,
|
|
19
|
+
isMobile,
|
|
20
|
+
onTooltipToggle,
|
|
21
|
+
onTooltipShow,
|
|
22
|
+
}: {
|
|
23
|
+
showTooltip: boolean;
|
|
24
|
+
isMobile: boolean;
|
|
25
|
+
onTooltipToggle: () => void;
|
|
26
|
+
onTooltipShow: (show: boolean) => void;
|
|
27
|
+
}) {
|
|
28
|
+
const anchorRef = useRef<HTMLSpanElement>(null);
|
|
29
|
+
const [tooltipStyle, setTooltipStyle] = useState<React.CSSProperties>({});
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!showTooltip || !anchorRef.current || typeof document === 'undefined') return;
|
|
33
|
+
const rect = anchorRef.current.getBoundingClientRect();
|
|
34
|
+
const vw = typeof window !== 'undefined' ? window.innerWidth : 375;
|
|
35
|
+
const vh = typeof window !== 'undefined' ? window.innerHeight : 667;
|
|
36
|
+
const tooltipWidth = 280;
|
|
37
|
+
const padding = 16;
|
|
38
|
+
|
|
39
|
+
if (isMobile) {
|
|
40
|
+
const effectiveWidth = Math.min(tooltipWidth, vw - padding * 2);
|
|
41
|
+
const leftClamped = Math.max(padding, Math.min(rect.left, vw - effectiveWidth - padding));
|
|
42
|
+
const spaceBelow = vh - rect.bottom - padding;
|
|
43
|
+
const preferAbove = spaceBelow < 100;
|
|
44
|
+
|
|
45
|
+
setTooltipStyle({
|
|
46
|
+
position: 'fixed',
|
|
47
|
+
left: leftClamped,
|
|
48
|
+
...(preferAbove
|
|
49
|
+
? { bottom: vh - rect.top + 4, maxHeight: rect.top - padding }
|
|
50
|
+
: { top: rect.bottom + 4, maxHeight: Math.min(200, spaceBelow - 8) }),
|
|
51
|
+
width: effectiveWidth,
|
|
52
|
+
overflowY: 'auto',
|
|
53
|
+
zIndex: 99999,
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
const maxW = Math.min(320, vw - padding * 2);
|
|
57
|
+
const centerX = rect.left + rect.width / 2;
|
|
58
|
+
const left = Math.max(padding + maxW / 2, Math.min(centerX, vw - padding - maxW / 2));
|
|
59
|
+
|
|
60
|
+
setTooltipStyle({
|
|
61
|
+
position: 'fixed',
|
|
62
|
+
left,
|
|
63
|
+
top: rect.top - 4,
|
|
64
|
+
transform: 'translate(-50%, -100%)',
|
|
65
|
+
maxWidth: maxW,
|
|
66
|
+
zIndex: 99999,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}, [showTooltip, isMobile]);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<>
|
|
73
|
+
<span className="relative inline-block" data-tooltip-icon ref={anchorRef}>
|
|
74
|
+
<svg
|
|
75
|
+
className="w-4 h-4 text-stone-400 cursor-pointer shrink-0"
|
|
76
|
+
fill="none"
|
|
77
|
+
stroke="currentColor"
|
|
78
|
+
viewBox="0 0 24 24"
|
|
79
|
+
onClick={(e) => {
|
|
80
|
+
e.stopPropagation();
|
|
81
|
+
onTooltipToggle();
|
|
82
|
+
}}
|
|
83
|
+
onMouseEnter={() => !isMobile && onTooltipShow(true)}
|
|
84
|
+
onMouseLeave={() => !isMobile && onTooltipShow(false)}
|
|
85
|
+
>
|
|
86
|
+
<path
|
|
87
|
+
strokeLinecap="round"
|
|
88
|
+
strokeLinejoin="round"
|
|
89
|
+
strokeWidth={2}
|
|
90
|
+
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
91
|
+
/>
|
|
92
|
+
</svg>
|
|
93
|
+
</span>
|
|
94
|
+
{showTooltip &&
|
|
95
|
+
typeof document !== 'undefined' &&
|
|
96
|
+
createPortal(
|
|
97
|
+
<span
|
|
98
|
+
className="whitespace-normal text-xs bg-stone-800 text-white px-3 py-2 rounded shadow-lg pointer-events-none"
|
|
99
|
+
style={tooltipStyle}
|
|
100
|
+
>
|
|
101
|
+
{TOOLTIP_TEXT}
|
|
102
|
+
</span>,
|
|
103
|
+
document.body
|
|
104
|
+
)}
|
|
105
|
+
</>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface ItineraryBoxProps {
|
|
110
|
+
selectedDate: string;
|
|
111
|
+
formattedDate: string;
|
|
112
|
+
itineraryItems: ItineraryDisplayStep[] | null;
|
|
113
|
+
isBookingComplete: boolean;
|
|
114
|
+
isItinerarySticky: boolean;
|
|
115
|
+
/** When set, CSS `position: sticky` uses this `top` (px) so the box clears host sticky headers. */
|
|
116
|
+
stickyTopPx?: number;
|
|
117
|
+
isMobile: boolean;
|
|
118
|
+
useWindowScroll?: boolean;
|
|
119
|
+
showTooltip: boolean;
|
|
120
|
+
selectedPickupLocation: PickupLocation | null | undefined;
|
|
121
|
+
pickupLocationSkipped: boolean;
|
|
122
|
+
pickupLocationsCount: number;
|
|
123
|
+
itineraryRef: React.RefObject<HTMLDivElement | null>;
|
|
124
|
+
t: TranslationFn;
|
|
125
|
+
onTooltipToggle: () => void;
|
|
126
|
+
onTooltipShow: (show: boolean) => void;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function ItineraryBox({
|
|
130
|
+
selectedDate,
|
|
131
|
+
formattedDate,
|
|
132
|
+
itineraryItems,
|
|
133
|
+
isBookingComplete,
|
|
134
|
+
isItinerarySticky,
|
|
135
|
+
stickyTopPx = 0,
|
|
136
|
+
isMobile,
|
|
137
|
+
useWindowScroll = false,
|
|
138
|
+
showTooltip,
|
|
139
|
+
selectedPickupLocation,
|
|
140
|
+
pickupLocationSkipped,
|
|
141
|
+
pickupLocationsCount,
|
|
142
|
+
itineraryRef,
|
|
143
|
+
t,
|
|
144
|
+
onTooltipToggle,
|
|
145
|
+
onTooltipShow,
|
|
146
|
+
}: ItineraryBoxProps) {
|
|
147
|
+
if (!selectedDate) return null;
|
|
148
|
+
if (!itineraryItems || itineraryItems.length === 0) return null;
|
|
149
|
+
|
|
150
|
+
const abbreviateDestination = (text: string): string => {
|
|
151
|
+
const abbreviations: Record<string, string> = {
|
|
152
|
+
'Moraine Lake': 'ML',
|
|
153
|
+
'Lake Louise': 'LL',
|
|
154
|
+
'Banff': 'B',
|
|
155
|
+
'Jasper': 'J',
|
|
156
|
+
'Canmore': 'C',
|
|
157
|
+
};
|
|
158
|
+
for (const [full, abbrev] of Object.entries(abbreviations)) {
|
|
159
|
+
if (text.includes(full)) {
|
|
160
|
+
return text.replace(full, abbrev);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return text;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const handlePickupLocationClick = () => {
|
|
167
|
+
const pickupSection = document.getElementById('pickup-location-section');
|
|
168
|
+
if (!pickupSection) return;
|
|
169
|
+
|
|
170
|
+
const findScrollParent = (node: HTMLElement): HTMLElement | null => {
|
|
171
|
+
let parent = node.parentElement;
|
|
172
|
+
while (parent) {
|
|
173
|
+
const { overflowY } = getComputedStyle(parent);
|
|
174
|
+
if (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') return parent;
|
|
175
|
+
parent = parent.parentElement;
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const scrollParent = useWindowScroll ? null : findScrollParent(pickupSection);
|
|
181
|
+
const itineraryBox = document.querySelector('[class*="sticky top-"]');
|
|
182
|
+
const itineraryHeight = itineraryBox?.getBoundingClientRect().height ?? 0;
|
|
183
|
+
// Extra offset so sticky header doesn't block the pickup section (mobile needs more clearance)
|
|
184
|
+
const STICKY_HEADER_CLEARANCE = isMobile ? 280 : 270;
|
|
185
|
+
const targetOffsetFromTop = itineraryHeight + 16 + STICKY_HEADER_CLEARANCE;
|
|
186
|
+
|
|
187
|
+
if (scrollParent) {
|
|
188
|
+
const elementPosition = pickupSection.getBoundingClientRect().top;
|
|
189
|
+
const scrollDelta = elementPosition - targetOffsetFromTop;
|
|
190
|
+
scrollParent.scrollTo({
|
|
191
|
+
top: Math.max(0, scrollParent.scrollTop + scrollDelta),
|
|
192
|
+
behavior: 'smooth',
|
|
193
|
+
});
|
|
194
|
+
} else if (typeof window !== 'undefined') {
|
|
195
|
+
const elementPosition = pickupSection.getBoundingClientRect().top + window.scrollY;
|
|
196
|
+
const scrollTop = Math.max(0, elementPosition - targetOffsetFromTop);
|
|
197
|
+
window.scrollTo({ top: scrollTop, behavior: 'smooth' });
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const getDisplayLabel = (label: string): string => {
|
|
202
|
+
if (isItinerarySticky && isMobile) {
|
|
203
|
+
return abbreviateDestination(label);
|
|
204
|
+
}
|
|
205
|
+
return label;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const stickyTop = Math.max(0, stickyTopPx ?? 0);
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<div
|
|
212
|
+
ref={itineraryRef as React.RefObject<HTMLDivElement>}
|
|
213
|
+
className={`${styles.box} ${isItinerarySticky ? styles.boxSticky : styles.boxExpanded}`}
|
|
214
|
+
style={stickyTop > 0 ? { top: stickyTop } : undefined}
|
|
215
|
+
>
|
|
216
|
+
<h3 className={`${styles.title} ${isItinerarySticky ? styles.titleSticky : styles.titleExpanded}`}>
|
|
217
|
+
{!isBookingComplete && (
|
|
218
|
+
<svg className={`${styles.icon} ${isItinerarySticky ? styles.iconSticky : styles.iconExpanded}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
219
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
220
|
+
</svg>
|
|
221
|
+
)}
|
|
222
|
+
{t('booking.buildYourItinerary')}
|
|
223
|
+
{formattedDate && (
|
|
224
|
+
<span className={styles.dateSubtitle}> · {formattedDate}</span>
|
|
225
|
+
)}
|
|
226
|
+
</h3>
|
|
227
|
+
{itineraryItems.length > 0 && (
|
|
228
|
+
<div className={isItinerarySticky ? styles.itemsSticky : styles.itemsExpanded}>
|
|
229
|
+
{itineraryItems.map((item, index) => {
|
|
230
|
+
const itemLabel = getStepLabel(item, t);
|
|
231
|
+
const isUncertain =
|
|
232
|
+
!item.time ||
|
|
233
|
+
(!selectedPickupLocation &&
|
|
234
|
+
!pickupLocationSkipped &&
|
|
235
|
+
(item.stepType === StepType.pickup || item.stepType === StepType.drop_off));
|
|
236
|
+
const hasTime = !!item.time;
|
|
237
|
+
const isPlaceholder = item.stepType === StepType.trip_end && !item.time;
|
|
238
|
+
const isPickupTime = index === 0 && item.stepType === StepType.pickup;
|
|
239
|
+
const isApproximatePickupTime = isPickupTime && !selectedPickupLocation && (pickupLocationsCount > 0 || pickupLocationSkipped);
|
|
240
|
+
|
|
241
|
+
let formattedTime = '';
|
|
242
|
+
if (item.time) {
|
|
243
|
+
formattedTime = item.time.replace(/^at /i, '');
|
|
244
|
+
formattedTime = formattedTime.replace(/:00(?=\s*(AM|PM))/i, '');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const isUncertainPickupOrDropOff =
|
|
248
|
+
isUncertain && (item.stepType === StepType.pickup || item.stepType === StepType.drop_off);
|
|
249
|
+
|
|
250
|
+
const locationOnlyDisplay =
|
|
251
|
+
item.place === 'your_pickup_location' ? t('booking.yourPickupLocation') : (item.place ?? '');
|
|
252
|
+
const prefixKey =
|
|
253
|
+
item.stepType === StepType.pickup ? 'booking.pickupAtPrefix' : 'booking.dropOffAtPrefix';
|
|
254
|
+
|
|
255
|
+
const content = (
|
|
256
|
+
<>
|
|
257
|
+
{hasTime ? (
|
|
258
|
+
<span className="inline-flex items-center gap-1.5 flex-wrap">
|
|
259
|
+
<span className={`inline-flex items-center gap-1.5 shrink-0 ${(formattedTime === 'TBD' && isUncertain) || isApproximatePickupTime ? 'text-stone-400' : 'font-bold text-stone-900'}`}>
|
|
260
|
+
{(formattedTime === 'TBD' && isUncertain) && (
|
|
261
|
+
<svg className="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden>
|
|
262
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
263
|
+
</svg>
|
|
264
|
+
)}
|
|
265
|
+
{isApproximatePickupTime && formattedTime !== 'TBD' && (
|
|
266
|
+
<TooltipAnchor
|
|
267
|
+
showTooltip={showTooltip}
|
|
268
|
+
isMobile={isMobile}
|
|
269
|
+
onTooltipToggle={onTooltipToggle}
|
|
270
|
+
onTooltipShow={onTooltipShow}
|
|
271
|
+
/>
|
|
272
|
+
)}
|
|
273
|
+
<span className="shrink-0 font-bold">{formattedTime}</span>
|
|
274
|
+
</span>
|
|
275
|
+
<span className="text-stone-400 shrink-0">-</span>
|
|
276
|
+
<span className="text-stone-600">
|
|
277
|
+
{isUncertainPickupOrDropOff ? (
|
|
278
|
+
<>
|
|
279
|
+
{t(prefixKey)}
|
|
280
|
+
<button
|
|
281
|
+
type="button"
|
|
282
|
+
onClick={handlePickupLocationClick}
|
|
283
|
+
className="text-stone-400 hover:text-stone-600 underline cursor-pointer"
|
|
284
|
+
>
|
|
285
|
+
{getDisplayLabel(locationOnlyDisplay)}
|
|
286
|
+
</button>
|
|
287
|
+
</>
|
|
288
|
+
) : isUncertain ? (
|
|
289
|
+
<span className="text-stone-400 underline">{getDisplayLabel(itemLabel)}</span>
|
|
290
|
+
) : (
|
|
291
|
+
getDisplayLabel(itemLabel)
|
|
292
|
+
)}
|
|
293
|
+
</span>
|
|
294
|
+
</span>
|
|
295
|
+
) : isPlaceholder ? (
|
|
296
|
+
<span className="text-stone-400">{itemLabel}</span>
|
|
297
|
+
) : (
|
|
298
|
+
<span className="inline-flex items-center gap-1.5 flex-wrap">
|
|
299
|
+
<span className={`inline-flex items-center gap-1.5 shrink-0 ${isUncertain ? 'text-stone-400' : 'font-bold text-stone-900'}`}>
|
|
300
|
+
{isUncertain && (
|
|
301
|
+
<svg className="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden>
|
|
302
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
303
|
+
</svg>
|
|
304
|
+
)}
|
|
305
|
+
<span className="shrink-0 font-bold">TBD</span>
|
|
306
|
+
</span>
|
|
307
|
+
<span className="text-stone-400 shrink-0">-</span>
|
|
308
|
+
<span className="text-stone-600">
|
|
309
|
+
{isUncertainPickupOrDropOff ? (
|
|
310
|
+
<>
|
|
311
|
+
{t(prefixKey)}
|
|
312
|
+
<button
|
|
313
|
+
type="button"
|
|
314
|
+
onClick={handlePickupLocationClick}
|
|
315
|
+
className="text-stone-400 hover:text-stone-600 underline cursor-pointer"
|
|
316
|
+
>
|
|
317
|
+
{getDisplayLabel(locationOnlyDisplay)}
|
|
318
|
+
</button>
|
|
319
|
+
</>
|
|
320
|
+
) : (
|
|
321
|
+
<span className="text-stone-400 underline">{getDisplayLabel(itemLabel)}</span>
|
|
322
|
+
)}
|
|
323
|
+
</span>
|
|
324
|
+
</span>
|
|
325
|
+
)}
|
|
326
|
+
</>
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
if (isItinerarySticky) {
|
|
330
|
+
return (
|
|
331
|
+
<span key={index} className={`${styles.item} ${styles.itemSticky}`}>
|
|
332
|
+
{content}
|
|
333
|
+
{index < itineraryItems.length - 1 && (
|
|
334
|
+
<span className={styles.separator}>
|
|
335
|
+
<svg className="w-3 h-3 sm:w-4 sm:h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
336
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
|
337
|
+
</svg>
|
|
338
|
+
</span>
|
|
339
|
+
)}
|
|
340
|
+
</span>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<div key={index} className={`${styles.item}`}>
|
|
346
|
+
{content}
|
|
347
|
+
</div>
|
|
348
|
+
);
|
|
349
|
+
})}
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
352
|
+
</div>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export type ItineraryReadOnlySummaryStatus = 'loading' | 'error' | 'ready';
|
|
357
|
+
|
|
358
|
+
export type ItineraryReadOnlyPhotoPreview = {
|
|
359
|
+
sessionTimeRangeLabel: string;
|
|
360
|
+
photographerLabel: string;
|
|
361
|
+
sessionLengthLabel: string | null;
|
|
362
|
+
primaryItinerarySteps: DapItineraryStep[];
|
|
363
|
+
slotStartIso: string;
|
|
364
|
+
slotEndIso: string;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
function formatReadOnlyItineraryTime(time: string): string {
|
|
368
|
+
let formattedTime = time;
|
|
369
|
+
if (formattedTime) {
|
|
370
|
+
formattedTime = formattedTime.replace(/^at /i, '');
|
|
371
|
+
formattedTime = formattedTime.replace(/:00(?=\s*(AM|PM))/i, '');
|
|
372
|
+
}
|
|
373
|
+
return formattedTime;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function ReadOnlyPhotoSessionItineraryRow({
|
|
377
|
+
sessionTimeRangeLabel,
|
|
378
|
+
photographerLabel,
|
|
379
|
+
sessionLengthLabel,
|
|
380
|
+
photoAddOnTitle,
|
|
381
|
+
}: {
|
|
382
|
+
sessionTimeRangeLabel: string;
|
|
383
|
+
photographerLabel: string;
|
|
384
|
+
sessionLengthLabel: string | null;
|
|
385
|
+
photoAddOnTitle: string;
|
|
386
|
+
}) {
|
|
387
|
+
const sub = ['Photographer: ' + photographerLabel, sessionLengthLabel].filter(Boolean).join(' · ');
|
|
388
|
+
return (
|
|
389
|
+
<div className={styles.item}>
|
|
390
|
+
<span className="inline-flex items-center gap-1.5 flex-wrap">
|
|
391
|
+
<span className={`inline-flex shrink-0 ${styles.readOnlyPhotoTime}`}>
|
|
392
|
+
{sessionTimeRangeLabel}
|
|
393
|
+
</span>
|
|
394
|
+
<span className={`shrink-0 ${styles.readOnlyPhotoDash}`}>-</span>
|
|
395
|
+
<span className={styles.readOnlyPhotoLabel}>
|
|
396
|
+
<span className="font-bold">{photoAddOnTitle}</span>
|
|
397
|
+
{sub ? <span>{` · ${sub}`}</span> : null}
|
|
398
|
+
</span>
|
|
399
|
+
</span>
|
|
400
|
+
</div>
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Same chrome and row layout as expanded [ItineraryBox] (time — label), without pickup-location
|
|
406
|
+
* editing or tooltips. Used for DAP “your current itinerary” under the booking reference field.
|
|
407
|
+
*/
|
|
408
|
+
export function ItineraryReadOnlySummary({
|
|
409
|
+
title,
|
|
410
|
+
dateSubtitle,
|
|
411
|
+
itineraryItems,
|
|
412
|
+
status,
|
|
413
|
+
loadingMessage,
|
|
414
|
+
errorMessage,
|
|
415
|
+
emptyMessage,
|
|
416
|
+
t,
|
|
417
|
+
className,
|
|
418
|
+
titleClassName,
|
|
419
|
+
sticky,
|
|
420
|
+
photoSessionPreview,
|
|
421
|
+
}: {
|
|
422
|
+
title: string;
|
|
423
|
+
/** Inline after the title, e.g. `MMM d` like main [BookingFlow] itinerary. */
|
|
424
|
+
dateSubtitle?: string;
|
|
425
|
+
itineraryItems: ItineraryDisplayStep[] | null;
|
|
426
|
+
status: ItineraryReadOnlySummaryStatus;
|
|
427
|
+
loadingMessage: string;
|
|
428
|
+
errorMessage: string;
|
|
429
|
+
emptyMessage: string;
|
|
430
|
+
t: TranslationFn;
|
|
431
|
+
className?: string;
|
|
432
|
+
titleClassName?: string;
|
|
433
|
+
/** Sticks to top of the dialog content scroll area (e.g. DAP). */
|
|
434
|
+
sticky?: boolean;
|
|
435
|
+
/** When set and status is ready, inserts one green photo row into the same list layout. */
|
|
436
|
+
photoSessionPreview?: ItineraryReadOnlyPhotoPreview | null;
|
|
437
|
+
}) {
|
|
438
|
+
const showPhotoPreview = status === 'ready' && photoSessionPreview != null;
|
|
439
|
+
const stepsForInsert = showPhotoPreview ? photoSessionPreview.primaryItinerarySteps : null;
|
|
440
|
+
const insertAt =
|
|
441
|
+
stepsForInsert && stepsForInsert.length > 0 && photoSessionPreview
|
|
442
|
+
? getPhotoSessionInsertPosition(stepsForInsert, {
|
|
443
|
+
slotStartIso: photoSessionPreview.slotStartIso,
|
|
444
|
+
slotEndIso: photoSessionPreview.slotEndIso,
|
|
445
|
+
})
|
|
446
|
+
: 0;
|
|
447
|
+
|
|
448
|
+
const hasItineraryRows = Boolean(itineraryItems && itineraryItems.length > 0);
|
|
449
|
+
const photoAddOnTitle = t('booking.dapPhotoSessionAddOn');
|
|
450
|
+
|
|
451
|
+
const boxLayoutClass = sticky ? styles.boxStickyInDialog : styles.boxEmbedded;
|
|
452
|
+
|
|
453
|
+
return (
|
|
454
|
+
<div
|
|
455
|
+
className={`${styles.box} ${styles.boxExpanded} ${boxLayoutClass} ${className ?? ''}`}
|
|
456
|
+
aria-busy={status === 'loading'}
|
|
457
|
+
>
|
|
458
|
+
<h3 className={`${styles.title} ${styles.titleExpanded} ${titleClassName ?? ''}`}>
|
|
459
|
+
{title}
|
|
460
|
+
{dateSubtitle ? (
|
|
461
|
+
<span className={styles.dateSubtitle}> · {dateSubtitle}</span>
|
|
462
|
+
) : null}
|
|
463
|
+
</h3>
|
|
464
|
+
{status === 'loading' ? (
|
|
465
|
+
<p className={styles.readOnlySummaryMessage} aria-live="polite">
|
|
466
|
+
{loadingMessage}
|
|
467
|
+
</p>
|
|
468
|
+
) : null}
|
|
469
|
+
{status === 'error' ? (
|
|
470
|
+
<p className={styles.readOnlySummaryError} role="alert">
|
|
471
|
+
{errorMessage}
|
|
472
|
+
</p>
|
|
473
|
+
) : null}
|
|
474
|
+
{showPhotoPreview && hasItineraryRows && photoSessionPreview && itineraryItems ? (
|
|
475
|
+
<div className={styles.itemsExpanded}>
|
|
476
|
+
{itineraryItems.map((item, index) => {
|
|
477
|
+
const itemLabel = getStepLabel(item, t);
|
|
478
|
+
const formattedTime = formatReadOnlyItineraryTime(item.time ?? '');
|
|
479
|
+
return (
|
|
480
|
+
<Fragment key={`dap-itin-${index}-${item.time ?? ''}-${item.place ?? ''}`}>
|
|
481
|
+
{insertAt === index ? (
|
|
482
|
+
<ReadOnlyPhotoSessionItineraryRow
|
|
483
|
+
sessionTimeRangeLabel={photoSessionPreview.sessionTimeRangeLabel}
|
|
484
|
+
photographerLabel={photoSessionPreview.photographerLabel}
|
|
485
|
+
sessionLengthLabel={photoSessionPreview.sessionLengthLabel}
|
|
486
|
+
photoAddOnTitle={photoAddOnTitle}
|
|
487
|
+
/>
|
|
488
|
+
) : null}
|
|
489
|
+
<div className={styles.item}>
|
|
490
|
+
<span className="inline-flex items-center gap-1.5 flex-wrap">
|
|
491
|
+
<span className="inline-flex shrink-0 font-bold text-stone-900">
|
|
492
|
+
{formattedTime || '—'}
|
|
493
|
+
</span>
|
|
494
|
+
<span className="text-stone-400 shrink-0">-</span>
|
|
495
|
+
<span className="text-stone-600">{itemLabel}</span>
|
|
496
|
+
</span>
|
|
497
|
+
</div>
|
|
498
|
+
</Fragment>
|
|
499
|
+
);
|
|
500
|
+
})}
|
|
501
|
+
{insertAt === itineraryItems.length ? (
|
|
502
|
+
<ReadOnlyPhotoSessionItineraryRow
|
|
503
|
+
sessionTimeRangeLabel={photoSessionPreview.sessionTimeRangeLabel}
|
|
504
|
+
photographerLabel={photoSessionPreview.photographerLabel}
|
|
505
|
+
sessionLengthLabel={photoSessionPreview.sessionLengthLabel}
|
|
506
|
+
photoAddOnTitle={photoAddOnTitle}
|
|
507
|
+
/>
|
|
508
|
+
) : null}
|
|
509
|
+
</div>
|
|
510
|
+
) : null}
|
|
511
|
+
{showPhotoPreview && !hasItineraryRows && photoSessionPreview ? (
|
|
512
|
+
<>
|
|
513
|
+
<ReadOnlyPhotoSessionItineraryRow
|
|
514
|
+
sessionTimeRangeLabel={photoSessionPreview.sessionTimeRangeLabel}
|
|
515
|
+
photographerLabel={photoSessionPreview.photographerLabel}
|
|
516
|
+
sessionLengthLabel={photoSessionPreview.sessionLengthLabel}
|
|
517
|
+
photoAddOnTitle={photoAddOnTitle}
|
|
518
|
+
/>
|
|
519
|
+
<p className={styles.readOnlyPhotoFallbackFoot}>
|
|
520
|
+
{t('booking.dapItineraryFallbackFoot')}
|
|
521
|
+
</p>
|
|
522
|
+
</>
|
|
523
|
+
) : null}
|
|
524
|
+
{status === 'ready' && !showPhotoPreview && itineraryItems && itineraryItems.length > 0 ? (
|
|
525
|
+
<div className={styles.itemsExpanded}>
|
|
526
|
+
{itineraryItems.map((item, index) => {
|
|
527
|
+
const itemLabel = getStepLabel(item, t);
|
|
528
|
+
const formattedTime = formatReadOnlyItineraryTime(item.time ?? '');
|
|
529
|
+
return (
|
|
530
|
+
<div key={index} className={styles.item}>
|
|
531
|
+
<span className="inline-flex items-center gap-1.5 flex-wrap">
|
|
532
|
+
<span className="inline-flex shrink-0 font-bold text-stone-900">
|
|
533
|
+
{formattedTime || '—'}
|
|
534
|
+
</span>
|
|
535
|
+
<span className="text-stone-400 shrink-0">-</span>
|
|
536
|
+
<span className="text-stone-600">{itemLabel}</span>
|
|
537
|
+
</span>
|
|
538
|
+
</div>
|
|
539
|
+
);
|
|
540
|
+
})}
|
|
541
|
+
</div>
|
|
542
|
+
) : null}
|
|
543
|
+
{status === 'ready' &&
|
|
544
|
+
!showPhotoPreview &&
|
|
545
|
+
(!itineraryItems || itineraryItems.length === 0) ? (
|
|
546
|
+
<p className={styles.readOnlySummaryMessage}>{emptyMessage}</p>
|
|
547
|
+
) : null}
|
|
548
|
+
</div>
|
|
549
|
+
);
|
|
550
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import type { ItineraryBuilderDestination } from '@/lib/api';
|
|
3
|
+
import type { ItineraryBuilderDestination } from '@/lib/booking-api';
|
|
4
4
|
|
|
5
5
|
interface ItineraryBuilderProps {
|
|
6
6
|
/** Shared destinations from product.itineraryBuilder */
|
|
@@ -23,7 +23,6 @@ export function ItineraryBuilder({
|
|
|
23
23
|
onDestinationsChange,
|
|
24
24
|
onPlanningNotesChange,
|
|
25
25
|
}: ItineraryBuilderProps) {
|
|
26
|
-
// Filter destinations by option blacklist
|
|
27
26
|
const availableDestinations = destinations.filter((d) => !optionBlacklist.includes(d.id));
|
|
28
27
|
|
|
29
28
|
const toggleDestination = (id: string) => {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Itinerary placeholder - shown when date selected but no time yet
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
.box {
|
|
6
|
+
position: sticky;
|
|
7
|
+
top: 0;
|
|
8
|
+
z-index: 10;
|
|
9
|
+
margin-top: 1rem;
|
|
10
|
+
margin-bottom: 1rem;
|
|
11
|
+
padding: 0.75rem;
|
|
12
|
+
background: var(--light-orange-background-dark);
|
|
13
|
+
border-radius: 0.5rem;
|
|
14
|
+
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
15
|
+
border: 1px solid var(--booking-stone-200, #e7e5e4);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.title {
|
|
19
|
+
font-family: 'Poppins', sans-serif;
|
|
20
|
+
font-size: 1.125rem;
|
|
21
|
+
font-weight: 700;
|
|
22
|
+
text-transform: lowercase;
|
|
23
|
+
color: var(--accent-orange);
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
margin-bottom: 0.5rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@media (min-width: 768px) {
|
|
30
|
+
.title {
|
|
31
|
+
font-size: 1.375rem;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.dateSubtitle {
|
|
36
|
+
font-weight: 400;
|
|
37
|
+
color: var(--accent-orange);
|
|
38
|
+
font-size: inherit;
|
|
39
|
+
opacity: 0.9;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.hint {
|
|
43
|
+
font-size: 0.875rem;
|
|
44
|
+
color: var(--booking-stone-600, #57534e);
|
|
45
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import styles from './ItineraryPlaceholder.module.css';
|
|
4
|
+
|
|
5
|
+
type TranslationFn = (key: string, params?: Record<string, string>) => string;
|
|
6
|
+
|
|
7
|
+
interface ItineraryPlaceholderProps {
|
|
8
|
+
formattedDate: string;
|
|
9
|
+
t: TranslationFn;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ItineraryPlaceholder({ formattedDate, t }: ItineraryPlaceholderProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div className={styles.box}>
|
|
15
|
+
<h3 className={styles.title}>
|
|
16
|
+
{t('booking.buildYourItinerary')}
|
|
17
|
+
{formattedDate && (
|
|
18
|
+
<span className={styles.dateSubtitle}> · {formattedDate}</span>
|
|
19
|
+
)}
|
|
20
|
+
</h3>
|
|
21
|
+
<p className={styles.hint}>
|
|
22
|
+
{t('booking.selectPickupTimeToSeeItinerary') || 'Select a pickup time below to see your schedule.'}
|
|
23
|
+
</p>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|