@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,390 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
4
|
+
import defaultStrings from '@/strings';
|
|
5
|
+
import BookingProductGrid, { type FilterId } from '@/components/booking/BookingProductGrid';
|
|
6
|
+
import { BookingFlow } from '@/components/booking/BookingFlow';
|
|
7
|
+
import { PrivateShuttleBookingFlow } from '@/components/booking/PrivateShuttleBookingFlow';
|
|
8
|
+
import { getProductByIdOrSlug, buildMinimalProductFromConfig } from '@/lib/products-config';
|
|
9
|
+
import { getProduct, type Product } from '@/lib/booking-api';
|
|
10
|
+
import { ENV } from '@/lib/env';
|
|
11
|
+
import type { BookingFlowUiOptions } from '@/components/booking/booking-flow-ui';
|
|
12
|
+
import type { BookingSourceMetadata } from '@/lib/booking/source-metadata';
|
|
13
|
+
import dialogStyles from '@/components/booking/BookingDialog.module.css';
|
|
14
|
+
import styles from './PartnerBookingPage.module.css';
|
|
15
|
+
import { BOOKING_FLOW_ABANDON_EVENT } from '@/providers/booking-dialog-provider';
|
|
16
|
+
|
|
17
|
+
type PartnerScreen =
|
|
18
|
+
| { type: 'product-grid'; filterId?: string }
|
|
19
|
+
| { type: 'book-flow'; productId: string };
|
|
20
|
+
|
|
21
|
+
function PartnerBookFlowScreen({
|
|
22
|
+
productId,
|
|
23
|
+
onBack,
|
|
24
|
+
autoAppliedPromoCode,
|
|
25
|
+
calendarDiscountPercent,
|
|
26
|
+
highlightedPickupLocationIds,
|
|
27
|
+
flowUi,
|
|
28
|
+
onBookingFlowSuccess,
|
|
29
|
+
bookingSourceAttribution,
|
|
30
|
+
partnerPortalBooking = false,
|
|
31
|
+
availabilityPricingProfileId,
|
|
32
|
+
availabilityCancellationPolicyProfileId,
|
|
33
|
+
}: {
|
|
34
|
+
productId: string;
|
|
35
|
+
onBack: () => void;
|
|
36
|
+
autoAppliedPromoCode?: string;
|
|
37
|
+
calendarDiscountPercent?: number;
|
|
38
|
+
highlightedPickupLocationIds?: string[];
|
|
39
|
+
flowUi?: BookingFlowUiOptions;
|
|
40
|
+
/** Partner portal: after deferred-invoice confirmation, refetch bookings / switch tab. */
|
|
41
|
+
onBookingFlowSuccess?: (data: { reservationReference: string; bookingReference?: string }) => void;
|
|
42
|
+
bookingSourceAttribution: Partial<BookingSourceMetadata>;
|
|
43
|
+
partnerPortalBooking?: boolean;
|
|
44
|
+
availabilityPricingProfileId?: string | null;
|
|
45
|
+
availabilityCancellationPolicyProfileId?: string | null;
|
|
46
|
+
}) {
|
|
47
|
+
const [product, setProduct] = useState<Product | null>(null);
|
|
48
|
+
const [error, setError] = useState<string | null>(null);
|
|
49
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
50
|
+
|
|
51
|
+
const config = getProductByIdOrSlug(productId);
|
|
52
|
+
const apiProductId = config?.productId ?? productId;
|
|
53
|
+
|
|
54
|
+
const minimalProduct = config
|
|
55
|
+
? buildMinimalProductFromConfig(config, ENV.COMPANY_ID)
|
|
56
|
+
: null;
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
let cancelled = false;
|
|
60
|
+
setError(null);
|
|
61
|
+
getProduct(apiProductId, ENV.COMPANY_ID)
|
|
62
|
+
.then((p) => {
|
|
63
|
+
if (!cancelled && p) setProduct(p);
|
|
64
|
+
else if (!cancelled && !p && !config) setError('Product not found');
|
|
65
|
+
})
|
|
66
|
+
.catch((err) => {
|
|
67
|
+
if (!cancelled) {
|
|
68
|
+
setError(
|
|
69
|
+
err instanceof Error ? err.message : 'Failed to load product',
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return () => {
|
|
74
|
+
cancelled = true;
|
|
75
|
+
};
|
|
76
|
+
}, [apiProductId, config]);
|
|
77
|
+
|
|
78
|
+
if (!config) {
|
|
79
|
+
if (error) {
|
|
80
|
+
return (
|
|
81
|
+
<div className={`${styles.screen} booking-flow-preflight`}>
|
|
82
|
+
<div className="flex flex-col items-center justify-center py-16 gap-4">
|
|
83
|
+
<p className="text-red-600">{error}</p>
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
onClick={onBack}
|
|
87
|
+
className="text-emerald-600 hover:underline"
|
|
88
|
+
>
|
|
89
|
+
Go back
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return (
|
|
96
|
+
<div className={`${styles.screen} booking-flow-preflight`}>
|
|
97
|
+
<div className="flex items-center justify-center py-16">
|
|
98
|
+
<div className="text-stone-600">
|
|
99
|
+
{defaultStrings.common.chooseYourExperience} — Loading...
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const displayProduct = product ?? minimalProduct!;
|
|
107
|
+
|
|
108
|
+
if (error && !product) {
|
|
109
|
+
return (
|
|
110
|
+
<div className={`${styles.screen} booking-flow-preflight`}>
|
|
111
|
+
<div className="flex flex-col items-center justify-center py-16 gap-4">
|
|
112
|
+
<p className="text-red-600">{error}</p>
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
onClick={onBack}
|
|
116
|
+
className="text-emerald-600 hover:underline"
|
|
117
|
+
>
|
|
118
|
+
Go back
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div className={`${dialogStyles.screen} booking-flow-preflight`}>
|
|
127
|
+
{displayProduct.productType === 'PRIVATE_SHUTTLE' ? (
|
|
128
|
+
<PrivateShuttleBookingFlow
|
|
129
|
+
product={displayProduct}
|
|
130
|
+
productId={productId}
|
|
131
|
+
onBack={onBack}
|
|
132
|
+
currency="CAD"
|
|
133
|
+
contentRef={contentRef}
|
|
134
|
+
highlightedPickupLocationIds={highlightedPickupLocationIds}
|
|
135
|
+
flowUi={flowUi}
|
|
136
|
+
bookingSourceAttribution={bookingSourceAttribution}
|
|
137
|
+
partnerPortalBooking={partnerPortalBooking}
|
|
138
|
+
availabilityPricingProfileId={availabilityPricingProfileId}
|
|
139
|
+
availabilityCancellationPolicyProfileId={availabilityCancellationPolicyProfileId}
|
|
140
|
+
onSuccess={(data) => {
|
|
141
|
+
onBookingFlowSuccess?.(data);
|
|
142
|
+
if (typeof window !== 'undefined') {
|
|
143
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
144
|
+
}
|
|
145
|
+
}}
|
|
146
|
+
/>
|
|
147
|
+
) : (
|
|
148
|
+
<BookingFlow
|
|
149
|
+
product={displayProduct}
|
|
150
|
+
productId={productId}
|
|
151
|
+
onBack={onBack}
|
|
152
|
+
currency="CAD"
|
|
153
|
+
contentRef={contentRef}
|
|
154
|
+
useWindowScroll
|
|
155
|
+
autoAppliedPromoCode={autoAppliedPromoCode}
|
|
156
|
+
calendarDiscountPercent={calendarDiscountPercent}
|
|
157
|
+
highlightedPickupLocationIds={highlightedPickupLocationIds}
|
|
158
|
+
flowUi={flowUi}
|
|
159
|
+
bookingSourceAttribution={bookingSourceAttribution}
|
|
160
|
+
partnerPortalBooking={partnerPortalBooking}
|
|
161
|
+
availabilityPricingProfileId={availabilityPricingProfileId}
|
|
162
|
+
availabilityCancellationPolicyProfileId={availabilityCancellationPolicyProfileId}
|
|
163
|
+
onSuccess={(data) => {
|
|
164
|
+
onBookingFlowSuccess?.(data);
|
|
165
|
+
if (typeof window !== 'undefined') {
|
|
166
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
167
|
+
}
|
|
168
|
+
}}
|
|
169
|
+
/>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface PartnerBookingPageProps {
|
|
176
|
+
/** Optional initial filter for the product grid (e.g. 'moraine-lake', 'all'). */
|
|
177
|
+
initialFilterId?: FilterId;
|
|
178
|
+
/** Optional custom title for the grid view header. */
|
|
179
|
+
headerTitle?: string;
|
|
180
|
+
/** Partner logo URL (e.g. from public folder or CDN). */
|
|
181
|
+
partnerLogoUrl?: string;
|
|
182
|
+
/** Accessible alt text for the partner logo. */
|
|
183
|
+
partnerLogoAlt?: string;
|
|
184
|
+
/** Via Via logo URL (fallback could be your standard logo). */
|
|
185
|
+
viaViaLogoUrl?: string;
|
|
186
|
+
/** Accessible alt text for the Via Via logo. */
|
|
187
|
+
viaViaLogoAlt?: string;
|
|
188
|
+
/** Promo code to auto-apply on this partner page. */
|
|
189
|
+
autoAppliedPromoCode?: string;
|
|
190
|
+
/** Extra discount percent shown in calendar date badges for this partner page. */
|
|
191
|
+
calendarDiscountPercent?: number;
|
|
192
|
+
/** Pickup location IDs to prioritize and highlight as partner-preferred. */
|
|
193
|
+
highlightedPickupLocationIds?: string[];
|
|
194
|
+
/**
|
|
195
|
+
* When true, render only the booking header + content (no outer page shell or card).
|
|
196
|
+
* Use inside a parent that already provides layout (e.g. partner portal).
|
|
197
|
+
*/
|
|
198
|
+
embedded?: boolean;
|
|
199
|
+
/** Hide the top title row on the product grid only (filters stay visible). */
|
|
200
|
+
hideProductGridHeader?: boolean;
|
|
201
|
+
/** Product tile opens book flow directly (requires stable `onBookProduct` / embedded routing). */
|
|
202
|
+
bookOnTileClick?: boolean;
|
|
203
|
+
flowUi?: BookingFlowUiOptions;
|
|
204
|
+
/** Embedded partner portal: called after booking completes (e.g. deferred invoice). */
|
|
205
|
+
onBookingFlowSuccess?: (data: { reservationReference: string; bookingReference?: string }) => void;
|
|
206
|
+
/** Required explicit attribution base (URL + optional merge); prefer `PartnerBookingPageWithBrowserMetadata` when deriving from the browser. */
|
|
207
|
+
bookingSourceAttribution: Partial<BookingSourceMetadata>;
|
|
208
|
+
/** Set on the dedicated partner portal app only — reserves use PARTNER_PORTAL source. */
|
|
209
|
+
partnerPortalBooking?: boolean;
|
|
210
|
+
/** Partner pricing profile for get-availabilities (when API supports it). */
|
|
211
|
+
availabilityPricingProfileId?: string | null;
|
|
212
|
+
/** Partner cancellation-policy profile for get-availabilities policy filtering. */
|
|
213
|
+
availabilityCancellationPolicyProfileId?: string | null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function PartnerBookingPage({
|
|
217
|
+
initialFilterId = 'all',
|
|
218
|
+
headerTitle,
|
|
219
|
+
partnerLogoUrl,
|
|
220
|
+
partnerLogoAlt,
|
|
221
|
+
viaViaLogoUrl = 'https://viaviamorainelake.b-cdn.net/logo512.png',
|
|
222
|
+
viaViaLogoAlt = 'Via Via Moraine Lake Shuttle logo',
|
|
223
|
+
autoAppliedPromoCode,
|
|
224
|
+
calendarDiscountPercent,
|
|
225
|
+
highlightedPickupLocationIds,
|
|
226
|
+
embedded = false,
|
|
227
|
+
hideProductGridHeader = false,
|
|
228
|
+
bookOnTileClick = false,
|
|
229
|
+
flowUi,
|
|
230
|
+
onBookingFlowSuccess,
|
|
231
|
+
bookingSourceAttribution,
|
|
232
|
+
partnerPortalBooking = false,
|
|
233
|
+
availabilityPricingProfileId,
|
|
234
|
+
availabilityCancellationPolicyProfileId,
|
|
235
|
+
}: PartnerBookingPageProps) {
|
|
236
|
+
const [screen, setScreen] = useState<PartnerScreen>({ type: 'product-grid' });
|
|
237
|
+
const cardRef = useRef<HTMLDivElement>(null);
|
|
238
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
239
|
+
const handleBackToProductGrid = useCallback(() => {
|
|
240
|
+
if (typeof window !== 'undefined') {
|
|
241
|
+
window.dispatchEvent(new Event(BOOKING_FLOW_ABANDON_EVENT));
|
|
242
|
+
}
|
|
243
|
+
setScreen({ type: 'product-grid' });
|
|
244
|
+
}, []);
|
|
245
|
+
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
if (screen.type === 'book-flow' && typeof window !== 'undefined') {
|
|
248
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
249
|
+
}
|
|
250
|
+
}, [screen.type, screen.type === 'book-flow' ? screen.productId : null]);
|
|
251
|
+
|
|
252
|
+
const bookFlowProductDisplayName = useMemo(() => {
|
|
253
|
+
if (screen.type !== 'book-flow') return '';
|
|
254
|
+
const cfg = getProductByIdOrSlug(screen.productId);
|
|
255
|
+
const short = cfg?.display.shortName?.trim();
|
|
256
|
+
if (short) return short;
|
|
257
|
+
const slug = cfg?.display.slug?.trim();
|
|
258
|
+
if (slug) {
|
|
259
|
+
return slug
|
|
260
|
+
.split('-')
|
|
261
|
+
.filter(Boolean)
|
|
262
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
263
|
+
.join(' ');
|
|
264
|
+
}
|
|
265
|
+
return screen.productId;
|
|
266
|
+
}, [screen]);
|
|
267
|
+
|
|
268
|
+
const currentTitle =
|
|
269
|
+
screen.type === 'product-grid'
|
|
270
|
+
? headerTitle ?? defaultStrings.common.chooseYourExperience
|
|
271
|
+
: null;
|
|
272
|
+
|
|
273
|
+
const showTopHeader =
|
|
274
|
+
screen.type === 'book-flow' || !hideProductGridHeader;
|
|
275
|
+
|
|
276
|
+
const bookSection = (
|
|
277
|
+
<>
|
|
278
|
+
{showTopHeader && (
|
|
279
|
+
<header className={styles.header}>
|
|
280
|
+
<div className={styles.headerLeft}>
|
|
281
|
+
{screen.type === 'book-flow' ? (
|
|
282
|
+
<button
|
|
283
|
+
type="button"
|
|
284
|
+
className={styles.backButton}
|
|
285
|
+
onClick={handleBackToProductGrid}
|
|
286
|
+
aria-label="Go back"
|
|
287
|
+
>
|
|
288
|
+
<svg
|
|
289
|
+
width="24"
|
|
290
|
+
height="24"
|
|
291
|
+
viewBox="0 0 24 24"
|
|
292
|
+
fill="none"
|
|
293
|
+
stroke="currentColor"
|
|
294
|
+
strokeWidth="2"
|
|
295
|
+
strokeLinecap="round"
|
|
296
|
+
strokeLinejoin="round"
|
|
297
|
+
>
|
|
298
|
+
<path d="M19 12H5M12 19l-7-7 7-7" />
|
|
299
|
+
</svg>
|
|
300
|
+
</button>
|
|
301
|
+
) : (
|
|
302
|
+
<span className={styles.headerSpacer} aria-hidden />
|
|
303
|
+
)}
|
|
304
|
+
</div>
|
|
305
|
+
{screen.type === 'product-grid' ? (
|
|
306
|
+
<h1 className={styles.titleProductGrid}>{currentTitle}</h1>
|
|
307
|
+
) : (
|
|
308
|
+
<h1 className={styles.titleBookFlow}>
|
|
309
|
+
<span className={styles.titleBookPrefix}>{defaultStrings.common.book}</span>
|
|
310
|
+
<span className={styles.titleBookSeparator}> - </span>
|
|
311
|
+
<span className={styles.titleBookProduct}>{bookFlowProductDisplayName}</span>
|
|
312
|
+
</h1>
|
|
313
|
+
)}
|
|
314
|
+
<div className={styles.headerRight}>
|
|
315
|
+
{/* No close button on dedicated partner page */}
|
|
316
|
+
<span className={styles.headerSpacer} aria-hidden />
|
|
317
|
+
</div>
|
|
318
|
+
</header>
|
|
319
|
+
)}
|
|
320
|
+
|
|
321
|
+
<div
|
|
322
|
+
ref={contentRef}
|
|
323
|
+
className={embedded ? styles.contentEmbedded : styles.content}
|
|
324
|
+
>
|
|
325
|
+
{screen.type === 'product-grid' && (
|
|
326
|
+
<div className={dialogStyles.screen}>
|
|
327
|
+
<BookingProductGrid
|
|
328
|
+
contentRef={contentRef}
|
|
329
|
+
initialFilterId={initialFilterId}
|
|
330
|
+
onBookProduct={(productId: string) =>
|
|
331
|
+
setScreen({ type: 'book-flow', productId })
|
|
332
|
+
}
|
|
333
|
+
bookOnTileClick={bookOnTileClick}
|
|
334
|
+
/>
|
|
335
|
+
</div>
|
|
336
|
+
)}
|
|
337
|
+
{screen.type === 'book-flow' && (
|
|
338
|
+
<PartnerBookFlowScreen
|
|
339
|
+
productId={screen.productId}
|
|
340
|
+
onBack={handleBackToProductGrid}
|
|
341
|
+
autoAppliedPromoCode={autoAppliedPromoCode}
|
|
342
|
+
highlightedPickupLocationIds={highlightedPickupLocationIds}
|
|
343
|
+
calendarDiscountPercent={calendarDiscountPercent}
|
|
344
|
+
flowUi={flowUi}
|
|
345
|
+
onBookingFlowSuccess={onBookingFlowSuccess}
|
|
346
|
+
bookingSourceAttribution={bookingSourceAttribution}
|
|
347
|
+
partnerPortalBooking={partnerPortalBooking}
|
|
348
|
+
availabilityPricingProfileId={availabilityPricingProfileId}
|
|
349
|
+
availabilityCancellationPolicyProfileId={availabilityCancellationPolicyProfileId}
|
|
350
|
+
/>
|
|
351
|
+
)}
|
|
352
|
+
</div>
|
|
353
|
+
</>
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
if (embedded) {
|
|
357
|
+
return bookSection;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<div>
|
|
362
|
+
<main className="booking-flow-root">
|
|
363
|
+
<div className={styles.root}>
|
|
364
|
+
{partnerLogoUrl && viaViaLogoUrl && (
|
|
365
|
+
<div className={styles.logoRow}>
|
|
366
|
+
<img
|
|
367
|
+
src={partnerLogoUrl}
|
|
368
|
+
alt={partnerLogoAlt || 'Partner logo'}
|
|
369
|
+
className={styles.partnerLogo}
|
|
370
|
+
/>
|
|
371
|
+
<span className={styles.logoDivider}>x</span>
|
|
372
|
+
<img
|
|
373
|
+
src={viaViaLogoUrl}
|
|
374
|
+
alt={viaViaLogoAlt}
|
|
375
|
+
className={styles.viaViaLogo}
|
|
376
|
+
/>
|
|
377
|
+
</div>
|
|
378
|
+
)}
|
|
379
|
+
|
|
380
|
+
<div ref={cardRef} className={styles.card}>
|
|
381
|
+
{bookSection}
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
</main>
|
|
385
|
+
</div>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export default PartnerBookingPage;
|
|
390
|
+
|
|
@@ -1,61 +1,45 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useMemo
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
4
|
import { usePathname } from 'next/navigation';
|
|
5
|
-
import
|
|
5
|
+
import PartnerBookingPage, { type PartnerBookingPageProps } from '@/components/partner/PartnerBookingPage';
|
|
6
6
|
import {
|
|
7
7
|
buildBookingSourceMetadataFromLocation,
|
|
8
8
|
mergeBookingSourceMetadata,
|
|
9
9
|
type BookingSourceMetadata,
|
|
10
|
-
} from '
|
|
10
|
+
} from '@/lib/booking/source-metadata';
|
|
11
11
|
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export interface PartnerBookingPageWithBrowserMetadataProps {
|
|
12
|
+
export type PartnerBookingPageWithBrowserMetadataProps = Omit<PartnerBookingPageProps, 'bookingSourceAttribution'> & {
|
|
13
|
+
/**
|
|
14
|
+
* Route-level identity (e.g. `partnerSlug`) so attribution works on localhost and when the URL
|
|
15
|
+
* does not carry `/partner/:slug`. Merged after browser URL fields; portal/session layer wins last.
|
|
16
|
+
*/
|
|
19
17
|
canonicalAttribution: Partial<BookingSourceMetadata>;
|
|
18
|
+
/** Partner portal: org `par_…` + agent merged on top of URL + canonical. */
|
|
20
19
|
bookingSourceAttributionMerge?: Partial<BookingSourceMetadata> | null;
|
|
21
|
-
|
|
22
|
-
onBookingFlowSuccess?: (data: { reservationReference: string; bookingReference?: string }) => void;
|
|
23
|
-
render: (props: PartnerBookingPageRenderProps) => ReactNode;
|
|
24
|
-
}
|
|
20
|
+
};
|
|
25
21
|
|
|
26
22
|
/**
|
|
27
|
-
*
|
|
28
|
-
* It computes attribution from browser URL + canonical route + runtime merge.
|
|
23
|
+
* Partner surfaces: **canonical** route metadata + optional browser URL supplements + optional portal merge.
|
|
29
24
|
*/
|
|
30
25
|
export default function PartnerBookingPageWithBrowserMetadata({
|
|
31
26
|
canonicalAttribution,
|
|
32
27
|
bookingSourceAttributionMerge,
|
|
33
|
-
|
|
34
|
-
onBookingFlowSuccess,
|
|
35
|
-
render,
|
|
28
|
+
...rest
|
|
36
29
|
}: PartnerBookingPageWithBrowserMetadataProps) {
|
|
37
30
|
const pathname = usePathname() ?? '';
|
|
31
|
+
const mergeKey = JSON.stringify(bookingSourceAttributionMerge ?? {});
|
|
32
|
+
const canonicalKey = JSON.stringify(canonicalAttribution ?? {});
|
|
38
33
|
|
|
39
34
|
const bookingSourceAttribution = useMemo(
|
|
40
|
-
() =>
|
|
41
|
-
|
|
42
|
-
void pathname;
|
|
43
|
-
return mergeBookingSourceMetadata(
|
|
35
|
+
() =>
|
|
36
|
+
mergeBookingSourceMetadata(
|
|
44
37
|
buildBookingSourceMetadataFromLocation(),
|
|
45
38
|
canonicalAttribution,
|
|
46
39
|
bookingSourceAttributionMerge,
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
[pathname, canonicalAttribution, bookingSourceAttributionMerge],
|
|
40
|
+
),
|
|
41
|
+
[pathname, mergeKey, canonicalKey],
|
|
50
42
|
);
|
|
51
43
|
|
|
52
|
-
return
|
|
53
|
-
<>
|
|
54
|
-
{render({
|
|
55
|
-
bookingSourceAttribution,
|
|
56
|
-
flowUi,
|
|
57
|
-
onBookingFlowSuccess,
|
|
58
|
-
})}
|
|
59
|
-
</>
|
|
60
|
-
);
|
|
44
|
+
return <PartnerBookingPage {...rest} bookingSourceAttribution={bookingSourceAttribution} />;
|
|
61
45
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
.tag {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
padding: 0.4rem 0.8rem;
|
|
6
|
+
font-size: 0.75rem;
|
|
7
|
+
font-weight: 700;
|
|
8
|
+
text-transform: uppercase;
|
|
9
|
+
letter-spacing: 0.5px;
|
|
10
|
+
z-index: 4;
|
|
11
|
+
text-align: center;
|
|
12
|
+
line-height: 1.2;
|
|
13
|
+
border-radius: 4px;
|
|
14
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
15
|
+
margin-bottom: 8px;
|
|
16
|
+
white-space: nowrap;
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
text-overflow: ellipsis;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* Style variants */
|
|
22
|
+
.most_popular {
|
|
23
|
+
background-color: var(--accent-orange);
|
|
24
|
+
color: white;
|
|
25
|
+
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.custom {
|
|
29
|
+
/* Custom styles will be applied via inline styles */
|
|
30
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import styles from './product-tag.module.css';
|
|
2
|
+
|
|
3
|
+
export enum ProductTagStyle {
|
|
4
|
+
MOST_POPULAR = 'MOST_POPULAR',
|
|
5
|
+
CUSTOM = 'CUSTOM',
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface ProductTagProps {
|
|
9
|
+
text: string;
|
|
10
|
+
style: ProductTagStyle;
|
|
11
|
+
backgroundColor?: string;
|
|
12
|
+
textColor?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function ProductTag({
|
|
16
|
+
text,
|
|
17
|
+
style,
|
|
18
|
+
backgroundColor,
|
|
19
|
+
textColor
|
|
20
|
+
}: ProductTagProps) {
|
|
21
|
+
const customStyles = style === ProductTagStyle.CUSTOM ? {
|
|
22
|
+
backgroundColor: backgroundColor || '#000000',
|
|
23
|
+
color: textColor || '#ffffff',
|
|
24
|
+
} : {};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
className={`${styles.tag} ${styles[style.toLowerCase()]}`}
|
|
29
|
+
style={customStyles}
|
|
30
|
+
>
|
|
31
|
+
{text}
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
.bestOptionContainer {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
align-items: center;
|
|
5
|
+
padding-top: var(--spacing-large);
|
|
6
|
+
padding-bottom: var(--spacing-large);
|
|
7
|
+
padding-left: var(--spacing-small);
|
|
8
|
+
padding-right: var(--spacing-small);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.otherOptionsRow {
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: row;
|
|
14
|
+
gap: 1rem;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.bestOptionProductsRowWrapper {
|
|
19
|
+
width: 100%;
|
|
20
|
+
max-width: 1200px;
|
|
21
|
+
margin: 0 auto;
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
gap: 1rem;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.bestOptionProductsRow {
|
|
28
|
+
display: grid;
|
|
29
|
+
grid-template-columns: 1fr;
|
|
30
|
+
gap: 1rem;
|
|
31
|
+
width: 100%;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Tablet breakpoint - 2 tiles per row */
|
|
35
|
+
@media (min-width: 768px) {
|
|
36
|
+
.bestOptionProductsRow {
|
|
37
|
+
grid-template-columns: repeat(2, 1fr);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Desktop breakpoint - 3 tiles per row */
|
|
42
|
+
@media (min-width: 1024px) {
|
|
43
|
+
.bestOptionProductsRow {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-wrap: wrap;
|
|
46
|
+
justify-content: flex-start;
|
|
47
|
+
gap: 1rem;
|
|
48
|
+
max-width: 1200px;
|
|
49
|
+
margin: 0 auto;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.bestOptionProductsRow > * {
|
|
53
|
+
flex: 0 0 calc(33.333% - 0.67rem);
|
|
54
|
+
max-width: calc(33.333% - 0.67rem);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.bestOptionProductsRow > a {
|
|
59
|
+
text-decoration: none;
|
|
60
|
+
color: inherit;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.bestOptionBottomContainer {
|
|
64
|
+
padding-top: var(--spacing-large);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.bestOptionBottomTitle {
|
|
68
|
+
color: var(--primary-text);
|
|
69
|
+
font-size: 1.2rem;
|
|
70
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import defaultStrings from "@/strings";
|
|
4
|
+
import styles from "./best-option.module.css";
|
|
5
|
+
import { Product } from "@/constants/products";
|
|
6
|
+
import Link from "next/link";
|
|
7
|
+
import ProductTileCard from "../product-tile/product-tile-card";
|
|
8
|
+
|
|
9
|
+
export default function BestOption({ bestOptionProducts, otherProductThemePages, strings = defaultStrings }: { bestOptionProducts: Product[], otherProductThemePages: { title: string, path: string }[], strings?: any }) {
|
|
10
|
+
return (
|
|
11
|
+
<div className={styles.bestOptionContainer}>
|
|
12
|
+
{bestOptionProducts.length > 0 ? (
|
|
13
|
+
<>
|
|
14
|
+
<h2>{strings.productThemePages.bestOption.title}</h2>
|
|
15
|
+
<div className={styles.bestOptionProductsRowWrapper}>
|
|
16
|
+
<div className={styles.bestOptionProductsRow}>
|
|
17
|
+
{bestOptionProducts.map((product) => (
|
|
18
|
+
<ProductTileCard key={product.id} product={product} strings={strings} />
|
|
19
|
+
))}
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</>
|
|
23
|
+
) : null}
|
|
24
|
+
|
|
25
|
+
<div className={styles.bestOptionBottomContainer}>
|
|
26
|
+
<h3 className={styles.bestOptionBottomTitle}>{strings.productThemePages.bestOption.bottomTitle}</h3>
|
|
27
|
+
<div className={styles.otherOptionsRow}>
|
|
28
|
+
{otherProductThemePages.map((page) => (
|
|
29
|
+
<Link href={page.path} key={page.title}>{page.title}</Link>
|
|
30
|
+
))}
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
border: 4px dashed var(--accent-turquoise-60);
|
|
3
|
+
padding: 1rem;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.description {
|
|
7
|
+
text-align: center;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@media (max-width: 1023px) {
|
|
11
|
+
.container {
|
|
12
|
+
margin: 0 auto;
|
|
13
|
+
width: 70%;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@media (min-width: 1024px) {
|
|
18
|
+
.container {
|
|
19
|
+
margin: 2rem auto;
|
|
20
|
+
width: 50%;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import styles from "./extended-tour-options.module.css";
|
|
2
|
+
import strings from "@/strings";
|
|
3
|
+
|
|
4
|
+
export default function ExtendedTourOptions() {
|
|
5
|
+
return (
|
|
6
|
+
<div className={styles.container}>
|
|
7
|
+
<h4>{strings.productThemePages.extendedTourOptions.title}</h4>
|
|
8
|
+
<p dangerouslySetInnerHTML={{ __html: strings.productThemePages.extendedTourOptions.description }} className={styles.description} />
|
|
9
|
+
</div>
|
|
10
|
+
)
|
|
11
|
+
}
|