@ticketboothapp/booking 1.2.25 → 1.2.27
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 +11 -29
- package/src/components/booking/AddOnsSection.tsx +2 -2
- package/src/components/booking/AdminPaymentChoiceModal.tsx +1 -1
- package/src/components/booking/BookingDialog.tsx +31 -13
- package/src/components/booking/BookingFlow.tsx +32 -27
- package/src/components/booking/BookingFlowCollage.tsx +10 -6
- package/src/components/booking/BookingFlowPlaceholder.tsx +1 -1
- package/src/components/booking/BookingFlowPreview.tsx +18 -9
- package/src/components/booking/BookingProductGrid.tsx +55 -19
- package/src/components/booking/Calendar.module.css +19 -4
- package/src/components/booking/Calendar.tsx +13 -8
- package/src/components/booking/CancellationPolicySelector.tsx +2 -2
- package/src/components/booking/ChangeBookingDialog.tsx +22 -12
- package/src/components/booking/CheckoutForm.module.css +10 -0
- package/src/components/booking/CheckoutForm.tsx +10 -2
- package/src/components/booking/CheckoutModal.tsx +16 -14
- package/src/components/booking/DapFlowCollage.tsx +5 -2
- package/src/components/booking/DapTourDescription.tsx +4 -4
- package/src/components/booking/DependentAddOnBookingDialog.tsx +23 -16
- package/src/components/booking/DependentAddOnPaymentForm.tsx +10 -7
- package/src/components/booking/ItineraryBox.tsx +6 -6
- package/src/components/booking/ItineraryBuilder.tsx +1 -1
- package/src/components/booking/MealDrinkAddOnSelector.tsx +3 -3
- package/src/components/booking/PickupLocationSelector.tsx +20 -18
- package/src/components/booking/PickupTimeSelector.tsx +3 -3
- package/src/components/booking/PriceBreakdown.tsx +5 -5
- package/src/components/booking/PriceSummary.module.css +7 -0
- package/src/components/booking/PriceSummary.tsx +8 -7
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +28 -19
- package/src/components/booking/PromoCodeInput.module.css +31 -25
- package/src/components/booking/PromoCodeInput.tsx +36 -24
- package/src/components/booking/ReturnTimeSelector.tsx +3 -3
- package/src/components/booking/TermsAcceptance.tsx +7 -2
- package/src/components/booking/TicketSelector.tsx +1 -1
- package/src/components/booking/TourDescription.tsx +11 -6
- package/src/components/booking/booking-flow.css +65 -4
- package/src/hooks/useBookingSourceMetadataFromLocation.ts +1 -1
- package/src/hooks/useIsBookingLaunchLive.ts +1 -1
- package/src/index.ts +26 -64
- package/src/providers/booking-dialog-provider.tsx +62 -53
- package/src/runtime/BookingHostContext.tsx +39 -0
- package/src/runtime/index.ts +13 -0
- package/src/runtime/types.ts +86 -0
- package/tsconfig.json +3 -5
- package/src/assets/icons/minus.svg +0 -7
- package/src/assets/icons/partner-logos/getyourguide.svg +0 -8
- package/src/assets/icons/plus.svg +0 -3
- package/src/colours.css +0 -23
- package/src/components/BookingDetails.module.css +0 -1591
- package/src/components/BookingDetails.tsx +0 -2264
- package/src/components/BookingWidget.tsx +0 -305
- package/src/components/ManageBookingView.tsx +0 -437
- package/src/components/PhoneInputWithCountry.module.css +0 -131
- package/src/components/PhoneInputWithCountry.tsx +0 -44
- package/src/components/PickupLocationDialog.module.css +0 -360
- package/src/components/PickupLocationDialog.tsx +0 -357
- package/src/components/PostBookingDependentAddOnUpsell.module.css +0 -174
- package/src/components/PostBookingDependentAddOnUpsell.tsx +0 -407
- package/src/components/button.css +0 -245
- package/src/components/button.tsx +0 -152
- package/src/components/colorable-svg.tsx +0 -29
- package/src/components/image.css +0 -29
- package/src/components/image.tsx +0 -113
- package/src/components/partner/PartnerBookingPage.module.css +0 -130
- package/src/components/partner/PartnerBookingPage.tsx +0 -390
- package/src/components/partner/PartnerBookingPageWithBrowserMetadata.tsx +0 -45
- package/src/components/product-tag.module.css +0 -30
- package/src/components/product-tag.tsx +0 -34
- package/src/components/product-theme-pages/image-modal.tsx +0 -248
- package/src/components/product-theme-pages/photo-gallery.module.css +0 -200
- package/src/components/terms/TermsContent.tsx +0 -178
- package/src/components/value-pill.module.css +0 -59
- package/src/components/value-pill.tsx +0 -46
- package/src/constants/images.ts +0 -556
- package/src/constants/pill-values.ts +0 -210
- package/src/constants/products.ts +0 -155
- package/src/contexts/AvailabilitiesCacheContext.tsx +0 -125
- package/src/contexts/CompanyContext.tsx +0 -70
- package/src/data/dap-descriptions/session-couples-families-friends.en.json +0 -61
- package/src/data/dap-descriptions/session-elopements.en.json +0 -60
- package/src/data/dap-descriptions/session-proposals.en.json +0 -60
- package/src/data/product-descriptions/afternoon-delight.en.json +0 -35
- package/src/data/product-descriptions/emerald-lake-escape.en.json +0 -68
- package/src/data/product-descriptions/lake-louise-adventure.en.json +0 -74
- package/src/data/product-descriptions/moraine-lake-adventure.en.json +0 -78
- package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +0 -65
- package/src/data/product-descriptions/moraine-lake-sunrise.en.json +0 -64
- package/src/data/product-descriptions/private-tour.en.json +0 -80
- package/src/data/product-descriptions/two-lakes-combo.en.json +0 -65
- package/src/data/products-config.json +0 -101
- package/src/lib/analytics.ts +0 -197
- package/src/lib/booking/booking-source.ts +0 -51
- package/src/lib/booking/checkout-breakdown.ts +0 -69
- package/src/lib/booking/correlation-id.ts +0 -46
- package/src/lib/booking/i18n/config.ts +0 -21
- package/src/lib/booking/i18n/index.tsx +0 -144
- package/src/lib/booking/i18n/messages/en.json +0 -236
- package/src/lib/booking/i18n/messages/fr.json +0 -236
- package/src/lib/booking/itinerary-display.ts +0 -36
- package/src/lib/booking/itinerary-labels.ts +0 -70
- package/src/lib/booking/location-calculations.ts +0 -43
- package/src/lib/booking/location-utils.ts +0 -165
- package/src/lib/booking/map-utils.ts +0 -153
- package/src/lib/booking/marker-icons.ts +0 -113
- package/src/lib/booking/normalize-booking-product-id.ts +0 -21
- package/src/lib/booking/pickup-location-types.ts +0 -25
- package/src/lib/booking/places-api.ts +0 -154
- package/src/lib/booking/pricing.ts +0 -466
- package/src/lib/booking/product-option-id.ts +0 -35
- package/src/lib/booking/source-metadata.ts +0 -226
- package/src/lib/booking/sunday-week.ts +0 -14
- package/src/lib/booking/theme.ts +0 -83
- package/src/lib/booking/trace-context.ts +0 -62
- package/src/lib/booking/utils.ts +0 -9
- package/src/lib/booking-api.ts +0 -1793
- package/src/lib/booking-constants.ts +0 -23
- package/src/lib/booking-ref.ts +0 -13
- package/src/lib/booking-types.ts +0 -36
- package/src/lib/currency.ts +0 -81
- package/src/lib/dap-descriptions.ts +0 -50
- package/src/lib/dap-itinerary-preview.ts +0 -315
- package/src/lib/dependent-add-on-api.ts +0 -434
- package/src/lib/env.ts +0 -96
- package/src/lib/firebase.ts +0 -20
- package/src/lib/job-application-api.ts +0 -83
- package/src/lib/manage-booking-embed-print.ts +0 -16
- package/src/lib/manage-booking-post-checkout.ts +0 -68
- package/src/lib/photo-dap-config.ts +0 -228
- package/src/lib/photo-packages.ts +0 -75
- package/src/lib/pickup/map-utils.ts +0 -56
- package/src/lib/pickup/marker-icons.ts +0 -19
- package/src/lib/product-descriptions.ts +0 -66
- package/src/lib/products-config.ts +0 -73
- package/src/providers/dependent-add-on-dialog-provider.tsx +0 -105
- package/src/radius.css +0 -5
- package/src/spacing.css +0 -7
- package/src/strings/en.json +0 -1774
- package/src/strings/es.json +0 -1573
- package/src/strings/fr.json +0 -1573
- package/src/strings/index.js +0 -23
- package/src/text-style.css +0 -56
- package/src/utils/currency-converter.ts +0 -101
|
@@ -2,17 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { formatCurrencyAmount, type Currency } from '@/lib/currency';
|
|
5
|
+
import { getProduct, type Product } from '../../../../../src/lib/booking-api';
|
|
6
|
+
import { useBookingHost } from '../../runtime';
|
|
7
|
+
import { formatCurrencyAmount, type Currency } from '../../../../../src/lib/currency';
|
|
9
8
|
import { BookingFlow, type ChangeFlowSelectionPreview } from './BookingFlow';
|
|
10
|
-
import { useBookingSourceMetadataFromLocation } from '
|
|
9
|
+
import { useBookingSourceMetadataFromLocation } from '../../hooks/useBookingSourceMetadataFromLocation';
|
|
11
10
|
import styles from './BookingDialog.module.css';
|
|
12
11
|
import './booking-flow.css';
|
|
13
|
-
import type { BookingData } from '
|
|
14
|
-
import { getItineraryStepLabel } from '
|
|
15
|
-
import { effectiveProductOptionIdForChangeFlow } from '
|
|
12
|
+
import type { BookingData } from '../../../../../src/components/BookingDetails';
|
|
13
|
+
import { getItineraryStepLabel } from '../../../../../src/lib/booking/itinerary-display';
|
|
14
|
+
import { effectiveProductOptionIdForChangeFlow } from '../../../../../src/lib/booking/product-option-id';
|
|
16
15
|
|
|
17
16
|
interface ChangeBookingDialogProps {
|
|
18
17
|
isOpen: boolean;
|
|
@@ -240,6 +239,7 @@ export default function ChangeBookingDialog({
|
|
|
240
239
|
onClose,
|
|
241
240
|
onChangeCompleted,
|
|
242
241
|
}: ChangeBookingDialogProps) {
|
|
242
|
+
const { env, catalog } = useBookingHost();
|
|
243
243
|
const bookingSourceAttribution = useBookingSourceMetadataFromLocation();
|
|
244
244
|
const [product, setProduct] = useState<Product | null>(null);
|
|
245
245
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -250,12 +250,22 @@ export default function ChangeBookingDialog({
|
|
|
250
250
|
|
|
251
251
|
const bookingProductId = booking.productId;
|
|
252
252
|
// Memoize so the load effect does not re-run every render (buildMinimalProductFromConfig returns a new object each call).
|
|
253
|
-
const config = useMemo(
|
|
253
|
+
const config = useMemo(
|
|
254
|
+
() =>
|
|
255
|
+
catalog.getProductByIdOrSlug(bookingProductId) as {
|
|
256
|
+
display?: { slug?: string; shortName?: string };
|
|
257
|
+
productId?: string;
|
|
258
|
+
} | null,
|
|
259
|
+
[bookingProductId, catalog]
|
|
260
|
+
);
|
|
254
261
|
const productSlug = config?.display?.slug ?? bookingProductId;
|
|
255
262
|
const apiProductId = config?.productId ?? bookingProductId;
|
|
256
263
|
const minimalProduct = useMemo(
|
|
257
|
-
() =>
|
|
258
|
-
|
|
264
|
+
() =>
|
|
265
|
+
config && catalog.buildMinimalProductFromConfig
|
|
266
|
+
? (catalog.buildMinimalProductFromConfig(config, env.COMPANY_ID) as Product)
|
|
267
|
+
: null,
|
|
268
|
+
[config, catalog, env.COMPANY_ID]
|
|
259
269
|
);
|
|
260
270
|
|
|
261
271
|
useEffect(() => {
|
|
@@ -263,7 +273,7 @@ export default function ChangeBookingDialog({
|
|
|
263
273
|
let cancelled = false;
|
|
264
274
|
setError(null);
|
|
265
275
|
if (minimalProduct) setProduct(minimalProduct);
|
|
266
|
-
getProduct(apiProductId,
|
|
276
|
+
getProduct(apiProductId, env.COMPANY_ID)
|
|
267
277
|
.then((p) => {
|
|
268
278
|
if (!cancelled && p) setProduct(p);
|
|
269
279
|
})
|
|
@@ -77,6 +77,16 @@
|
|
|
77
77
|
margin-top: 1.5rem;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/* Motion wrappers — no nested “card” chrome (flush with modal / form background) */
|
|
81
|
+
.pickupExpandedWrapper,
|
|
82
|
+
.pickupCollapsedWrapper {
|
|
83
|
+
margin: 0;
|
|
84
|
+
padding: 0;
|
|
85
|
+
background: transparent;
|
|
86
|
+
border: none;
|
|
87
|
+
box-shadow: none;
|
|
88
|
+
}
|
|
89
|
+
|
|
80
90
|
.pickupHeader {
|
|
81
91
|
display: flex;
|
|
82
92
|
align-items: center;
|
|
@@ -4,9 +4,9 @@ import { PriceSummary, type PriceSummaryLine } from './PriceSummary';
|
|
|
4
4
|
import { TermsAcceptance } from './TermsAcceptance';
|
|
5
5
|
import { PickupLocationSelector } from './PickupLocationSelector';
|
|
6
6
|
import type { Currency } from './CurrencySwitcher';
|
|
7
|
-
import type { PickupLocation, Destination } from '
|
|
7
|
+
import type { PickupLocation, Destination } from '../../../../../src/lib/booking-api';
|
|
8
8
|
import styles from './CheckoutForm.module.css';
|
|
9
|
-
import
|
|
9
|
+
import { useBookingHost } from '../../runtime';
|
|
10
10
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
11
11
|
|
|
12
12
|
type TranslationFn = (key: string, params?: Record<string, string>) => string;
|
|
@@ -113,6 +113,14 @@ export function CheckoutForm({
|
|
|
113
113
|
attributionConfirmed = false,
|
|
114
114
|
onAttributionConfirmedChange,
|
|
115
115
|
}: CheckoutFormProps) {
|
|
116
|
+
const { slots } = useBookingHost();
|
|
117
|
+
const Button = slots.Button;
|
|
118
|
+
const ButtonHoverColor = slots.ButtonHoverColor as {
|
|
119
|
+
Turquoise: string;
|
|
120
|
+
White: string;
|
|
121
|
+
Orange: string;
|
|
122
|
+
};
|
|
123
|
+
|
|
116
124
|
return (
|
|
117
125
|
<div className={styles.section}>
|
|
118
126
|
<div className={styles.summaryWrapper}>
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useRef, type ReactNode } from 'react';
|
|
3
|
+
import { useState, useEffect, useRef, useMemo, type ReactNode } from 'react';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
5
|
import { loadStripe } from '@stripe/stripe-js';
|
|
6
6
|
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { formatCurrencyAmount } from '@/lib/currency';
|
|
7
|
+
import { useBookingHost } from '../../runtime';
|
|
8
|
+
import { formatCurrencyAmount } from '../../../../../src/lib/currency';
|
|
10
9
|
import { PriceSummary, type PriceSummaryLine } from './PriceSummary';
|
|
11
10
|
import type { Currency } from './CurrencySwitcher';
|
|
12
|
-
import type { Locale } from '
|
|
13
|
-
import type { OrderSummaryTicketLine, OrderSummaryFeeLine, PriceBreakdown as PriceBreakdownType } from '
|
|
11
|
+
import type { Locale } from '../../../../../src/lib/booking/i18n/config';
|
|
12
|
+
import type { OrderSummaryTicketLine, OrderSummaryFeeLine, PriceBreakdown as PriceBreakdownType } from '../../../../../src/lib/booking/pricing';
|
|
14
13
|
|
|
15
14
|
export interface CheckoutModalLineItem {
|
|
16
15
|
line: OrderSummaryTicketLine;
|
|
@@ -61,10 +60,6 @@ export interface CheckoutModalProps {
|
|
|
61
60
|
};
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
const stripePromise = ENV.STRIPE_PUBLISHABLE_KEY
|
|
65
|
-
? loadStripe(ENV.STRIPE_PUBLISHABLE_KEY)
|
|
66
|
-
: null;
|
|
67
|
-
|
|
68
63
|
function CheckoutForm({
|
|
69
64
|
successUrl,
|
|
70
65
|
onClose,
|
|
@@ -84,6 +79,7 @@ function CheckoutForm({
|
|
|
84
79
|
currency: Currency;
|
|
85
80
|
locale: Locale;
|
|
86
81
|
}) {
|
|
82
|
+
const { analytics } = useBookingHost();
|
|
87
83
|
const stripe = useStripe();
|
|
88
84
|
const elements = useElements();
|
|
89
85
|
const [loading, setLoading] = useState(false);
|
|
@@ -103,7 +99,7 @@ function CheckoutForm({
|
|
|
103
99
|
}
|
|
104
100
|
onPaymentSubmitStart?.();
|
|
105
101
|
// Store before redirect so success page can fire purchase event
|
|
106
|
-
storePendingPurchase(total, currency);
|
|
102
|
+
analytics.storePendingPurchase(total, currency);
|
|
107
103
|
const { error: confirmError } = await stripe.confirmPayment({
|
|
108
104
|
elements,
|
|
109
105
|
confirmParams: {
|
|
@@ -176,6 +172,12 @@ export function CheckoutModal({
|
|
|
176
172
|
prePaymentPanel,
|
|
177
173
|
changeTotals,
|
|
178
174
|
}: CheckoutModalProps) {
|
|
175
|
+
const { env, analytics } = useBookingHost();
|
|
176
|
+
const stripePromise = useMemo(
|
|
177
|
+
() =>
|
|
178
|
+
env.STRIPE_PUBLISHABLE_KEY ? loadStripe(env.STRIPE_PUBLISHABLE_KEY) : null,
|
|
179
|
+
[env.STRIPE_PUBLISHABLE_KEY]
|
|
180
|
+
);
|
|
179
181
|
const baseUrl = typeof window !== 'undefined' ? window.location.origin : '';
|
|
180
182
|
const manageParams = new URLSearchParams({ reservationRef: reservationReference });
|
|
181
183
|
if (customerLastName?.trim()) manageParams.set('lastName', customerLastName.trim());
|
|
@@ -195,10 +197,10 @@ export function CheckoutModal({
|
|
|
195
197
|
qty: line.qty,
|
|
196
198
|
price: line.itemTotal,
|
|
197
199
|
}));
|
|
198
|
-
trackBeginCheckout(total, currency, items);
|
|
200
|
+
analytics.trackBeginCheckout(total, currency, items);
|
|
199
201
|
}
|
|
200
202
|
if (!open) hasFiredBeginCheckout.current = false;
|
|
201
|
-
}, [open, total, currency, ticketLines]);
|
|
203
|
+
}, [open, total, currency, ticketLines, analytics]);
|
|
202
204
|
|
|
203
205
|
useEffect(() => {
|
|
204
206
|
if (!open || !reservationExpiration) return;
|
|
@@ -233,7 +235,7 @@ export function CheckoutModal({
|
|
|
233
235
|
|
|
234
236
|
if (!open) return null;
|
|
235
237
|
|
|
236
|
-
if (!
|
|
238
|
+
if (!env.STRIPE_PUBLISHABLE_KEY) {
|
|
237
239
|
const noStripe = (
|
|
238
240
|
<div
|
|
239
241
|
className="booking-flow-root booking-flow-preflight fixed inset-0 z-[10050] flex items-center justify-center p-4 bg-black/50"
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useMemo, useState } from 'react';
|
|
4
|
-
import
|
|
5
|
-
import ImageModal from '@/components/product-theme-pages/image-modal';
|
|
4
|
+
import { useBookingHost } from '../../runtime';
|
|
6
5
|
import styles from './BookingFlowCollage.module.css';
|
|
7
6
|
|
|
8
7
|
export interface DapFlowCollageProps {
|
|
@@ -13,6 +12,10 @@ export interface DapFlowCollageProps {
|
|
|
13
12
|
|
|
14
13
|
/** Hero (left) + four-tile grid (right), same layout as BookingFlowCollage without video */
|
|
15
14
|
export function DapFlowCollage({ imageIds, altPrefix = 'Experience' }: DapFlowCollageProps) {
|
|
15
|
+
const { slots: hostSlots } = useBookingHost();
|
|
16
|
+
const ViaViaImage = hostSlots.Image;
|
|
17
|
+
const ImageModal = hostSlots.ImageModal;
|
|
18
|
+
|
|
16
19
|
const slots = useMemo(() => {
|
|
17
20
|
const raw = imageIds.map((id) => id.trim()).filter(Boolean);
|
|
18
21
|
if (raw.length === 0) return [];
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useMemo } from 'react';
|
|
4
|
-
import { TourDescription } from '
|
|
5
|
-
import { useLocale, useTranslations } from '
|
|
6
|
-
import { getDapDescription } from '
|
|
7
|
-
import type { PhotoDapSlug } from '
|
|
4
|
+
import { TourDescription } from './TourDescription';
|
|
5
|
+
import { useLocale, useTranslations } from '../../../../../src/lib/booking/i18n';
|
|
6
|
+
import { getDapDescription } from '../../../../../src/lib/dap-descriptions';
|
|
7
|
+
import type { PhotoDapSlug } from '../../../../../src/lib/photo-dap-config';
|
|
8
8
|
|
|
9
9
|
export function DapTourDescription({
|
|
10
10
|
slug,
|
|
@@ -11,8 +11,8 @@ import {
|
|
|
11
11
|
import { format, parseISO } from 'date-fns';
|
|
12
12
|
import {
|
|
13
13
|
useDependentAddOnDialog,
|
|
14
|
-
} from '
|
|
15
|
-
import {
|
|
14
|
+
} from '../../../../../src/providers/dependent-add-on-dialog-provider';
|
|
15
|
+
import { useBookingHost } from '../../runtime';
|
|
16
16
|
import {
|
|
17
17
|
getDependentAddOnAvailability,
|
|
18
18
|
createDependentAddOnPaymentIntent,
|
|
@@ -20,20 +20,19 @@ import {
|
|
|
20
20
|
type DependentAddOnCheckoutQuestion,
|
|
21
21
|
type DependentAddOnSlot,
|
|
22
22
|
type DapItineraryStep,
|
|
23
|
-
} from '
|
|
24
|
-
import { dapItineraryStepsToDisplay } from '
|
|
25
|
-
import { DEFAULT_PHOTO_DAP_CANCELLATION_DAYS_BEFORE_SESSION } from '
|
|
26
|
-
import { CheckoutModal, type CheckoutModalLineItem } from '
|
|
27
|
-
import { DapFlowCollage } from '
|
|
28
|
-
import { DapTourDescription } from '
|
|
23
|
+
} from '../../../../../src/lib/dependent-add-on-api';
|
|
24
|
+
import { dapItineraryStepsToDisplay } from '../../../../../src/lib/dap-itinerary-preview';
|
|
25
|
+
import { DEFAULT_PHOTO_DAP_CANCELLATION_DAYS_BEFORE_SESSION } from '../../../../../src/lib/photo-dap-config';
|
|
26
|
+
import { CheckoutModal, type CheckoutModalLineItem } from './CheckoutModal';
|
|
27
|
+
import { DapFlowCollage } from './DapFlowCollage';
|
|
28
|
+
import { DapTourDescription } from './DapTourDescription';
|
|
29
29
|
import {
|
|
30
30
|
ItineraryReadOnlySummary,
|
|
31
31
|
type ItineraryReadOnlyPhotoPreview,
|
|
32
|
-
} from '
|
|
33
|
-
import { formatBookingRefForDisplay } from '
|
|
34
|
-
import type { Currency } from '
|
|
35
|
-
import { useLocale, useTranslations } from '
|
|
36
|
-
import Button, { ButtonHoverColor } from '@/components/button';
|
|
32
|
+
} from './ItineraryBox';
|
|
33
|
+
import { formatBookingRefForDisplay } from '../../../../../src/lib/booking-ref';
|
|
34
|
+
import type { Currency } from '../../../../../src/lib/currency';
|
|
35
|
+
import { useLocale, useTranslations } from '../../../../../src/lib/booking/i18n';
|
|
37
36
|
import checkoutFormStyles from './CheckoutForm.module.css';
|
|
38
37
|
import returnTimeStyles from './ReturnTimeSelector.module.css';
|
|
39
38
|
import bookingStyles from './BookingDialog.module.css';
|
|
@@ -200,6 +199,14 @@ function dapSlotsPanelKey(ref: string, productOptionId: string): string {
|
|
|
200
199
|
}
|
|
201
200
|
|
|
202
201
|
export default function DependentAddOnBookingDialog() {
|
|
202
|
+
const { env, slots: hostSlots } = useBookingHost();
|
|
203
|
+
const Button = hostSlots.Button;
|
|
204
|
+
const ButtonHoverColor = hostSlots.ButtonHoverColor as {
|
|
205
|
+
Turquoise: string;
|
|
206
|
+
White: string;
|
|
207
|
+
Orange: string;
|
|
208
|
+
};
|
|
209
|
+
|
|
203
210
|
const { t } = useTranslations();
|
|
204
211
|
const { locale } = useLocale();
|
|
205
212
|
const { isOpen, payload, close } = useDependentAddOnDialog();
|
|
@@ -391,7 +398,7 @@ export default function DependentAddOnBookingDialog() {
|
|
|
391
398
|
void (async () => {
|
|
392
399
|
try {
|
|
393
400
|
const preview = await getDependentAddOnAvailability({
|
|
394
|
-
companyId:
|
|
401
|
+
companyId: env.COMPANY_ID,
|
|
395
402
|
primaryBookingReference: refTrim,
|
|
396
403
|
lastName: lastNameTrim,
|
|
397
404
|
dependentAddOnProductId: payload.dependentAddOnProductId,
|
|
@@ -494,7 +501,7 @@ export default function DependentAddOnBookingDialog() {
|
|
|
494
501
|
const optKey = (productOptionId ?? '').trim();
|
|
495
502
|
try {
|
|
496
503
|
const avail = await getDependentAddOnAvailability({
|
|
497
|
-
companyId:
|
|
504
|
+
companyId: env.COMPANY_ID,
|
|
498
505
|
primaryBookingReference: ref,
|
|
499
506
|
lastName,
|
|
500
507
|
dependentAddOnProductId: payload.dependentAddOnProductId,
|
|
@@ -640,7 +647,7 @@ export default function DependentAddOnBookingDialog() {
|
|
|
640
647
|
setPreparingPayment(true);
|
|
641
648
|
try {
|
|
642
649
|
const pi = await createDependentAddOnPaymentIntent({
|
|
643
|
-
companyId:
|
|
650
|
+
companyId: env.COMPANY_ID,
|
|
644
651
|
primaryBookingReference: ref,
|
|
645
652
|
lastName: primaryBookingLastName.trim(),
|
|
646
653
|
dependentAddOnProductId: payload.dependentAddOnProductId,
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react';
|
|
3
|
+
import { useState, useMemo } from 'react';
|
|
4
4
|
import { loadStripe } from '@stripe/stripe-js';
|
|
5
5
|
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
|
|
6
|
-
import {
|
|
7
|
-
import { formatCurrencyAmount, type Currency } from '
|
|
6
|
+
import { useBookingHost } from '../../runtime';
|
|
7
|
+
import { formatCurrencyAmount, type Currency } from '../../../../../src/lib/currency';
|
|
8
8
|
import bookingStyles from './BookingDialog.module.css';
|
|
9
9
|
|
|
10
|
-
const stripePromise = ENV.STRIPE_PUBLISHABLE_KEY
|
|
11
|
-
? loadStripe(ENV.STRIPE_PUBLISHABLE_KEY)
|
|
12
|
-
: null;
|
|
13
|
-
|
|
14
10
|
function toCurrency(code: string | undefined): Currency {
|
|
15
11
|
const c = (code ?? 'CAD').toUpperCase();
|
|
16
12
|
if (c === 'USD' || c === 'EUR' || c === 'GBP' || c === 'AUD' || c === 'CAD') {
|
|
@@ -95,6 +91,13 @@ export function DependentAddOnPaymentForm({
|
|
|
95
91
|
onSuccess: () => void;
|
|
96
92
|
onError: (message: string) => void;
|
|
97
93
|
}) {
|
|
94
|
+
const { env } = useBookingHost();
|
|
95
|
+
const stripePromise = useMemo(
|
|
96
|
+
() =>
|
|
97
|
+
env.STRIPE_PUBLISHABLE_KEY ? loadStripe(env.STRIPE_PUBLISHABLE_KEY) : null,
|
|
98
|
+
[env.STRIPE_PUBLISHABLE_KEY]
|
|
99
|
+
);
|
|
100
|
+
|
|
98
101
|
if (!stripePromise) {
|
|
99
102
|
return (
|
|
100
103
|
<p className={bookingStyles.dapStripeNotice}>
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import { Fragment, useRef, useEffect, useState } from 'react';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
|
-
import type { ItineraryDisplayStep } from '
|
|
6
|
-
import { ItineraryStepType as StepType } from '
|
|
7
|
-
import { getStepLabel } from '
|
|
8
|
-
import type { PickupLocation } from '
|
|
9
|
-
import type { DapItineraryStep } from '
|
|
10
|
-
import { getPhotoSessionInsertPosition } from '
|
|
5
|
+
import type { ItineraryDisplayStep } from '../../../../../src/lib/booking-api';
|
|
6
|
+
import { ItineraryStepType as StepType } from '../../../../../src/lib/booking-api';
|
|
7
|
+
import { getStepLabel } from '../../../../../src/lib/booking/itinerary-labels';
|
|
8
|
+
import type { PickupLocation } from '../../../../../src/lib/booking-api';
|
|
9
|
+
import type { DapItineraryStep } from '../../../../../src/lib/dap-itinerary-preview';
|
|
10
|
+
import { getPhotoSessionInsertPosition } from '../../../../../src/lib/dap-itinerary-preview';
|
|
11
11
|
import styles from './ItineraryBox.module.css';
|
|
12
12
|
|
|
13
13
|
type TranslationFn = (key: string, params?: Record<string, string>) => string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import type { ItineraryBuilderDestination } from '
|
|
3
|
+
import type { ItineraryBuilderDestination } from '../../../../../src/lib/booking-api';
|
|
4
4
|
|
|
5
5
|
interface ItineraryBuilderProps {
|
|
6
6
|
/** Shared destinations from product.itineraryBuilder */
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useMemo, useCallback } from 'react';
|
|
4
|
-
import { formatCurrencyAmount } from '
|
|
5
|
-
import type { AddOn, AddOnVariant } from '
|
|
4
|
+
import { formatCurrencyAmount } from '../../../../../src/lib/currency';
|
|
5
|
+
import type { AddOn, AddOnVariant } from '../../../../../src/lib/booking-api';
|
|
6
6
|
import type { Currency } from './CurrencySwitcher';
|
|
7
|
-
import type { Locale } from '
|
|
7
|
+
import type { Locale } from '../../../../../src/lib/booking/i18n/config';
|
|
8
8
|
|
|
9
9
|
export interface AddOnSelection {
|
|
10
10
|
addOnId: string;
|
|
@@ -2,38 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
|
4
4
|
import { GoogleMap, Marker, InfoWindow, useJsApiLoader } from '@react-google-maps/api';
|
|
5
|
-
import type { PickupLocation, Destination } from '
|
|
5
|
+
import type { PickupLocation, Destination } from '../../../../../src/lib/booking-api';
|
|
6
6
|
import {
|
|
7
7
|
formatDistance,
|
|
8
8
|
formatTime,
|
|
9
9
|
geocodeAddress,
|
|
10
10
|
isWithinPrivateShuttleServiceArea,
|
|
11
|
-
} from '
|
|
11
|
+
} from '../../../../../src/lib/booking/location-utils';
|
|
12
12
|
import {
|
|
13
13
|
calculateNearbyLocations,
|
|
14
14
|
isExactMatch,
|
|
15
|
-
} from '
|
|
15
|
+
} from '../../../../../src/lib/booking/location-calculations';
|
|
16
16
|
import {
|
|
17
17
|
getAutocompleteSuggestions,
|
|
18
18
|
getPlaceDetails,
|
|
19
19
|
type AutocompleteSuggestion,
|
|
20
|
-
} from '
|
|
20
|
+
} from '../../../../../src/lib/booking/places-api';
|
|
21
21
|
import {
|
|
22
22
|
createDistanceMarkerIcon,
|
|
23
23
|
createPinMarkerIcon,
|
|
24
24
|
createSearchedLocationPinIcon,
|
|
25
25
|
createDestinationMarkerIcon,
|
|
26
|
-
} from '
|
|
27
|
-
import {
|
|
26
|
+
} from '../../../../../src/lib/booking/marker-icons';
|
|
27
|
+
import { useBookingHost } from '../../runtime';
|
|
28
28
|
import {
|
|
29
29
|
calculateMapCenter,
|
|
30
30
|
calculateMapBounds,
|
|
31
31
|
getMapOptions,
|
|
32
32
|
panToLocationIfNeeded,
|
|
33
|
-
} from '
|
|
34
|
-
import type { SearchedLocation, NearbyLocation } from '
|
|
35
|
-
import { useTranslations } from '
|
|
36
|
-
import { useBookingApp } from '
|
|
33
|
+
} from '../../../../../src/lib/booking/map-utils';
|
|
34
|
+
import type { SearchedLocation, NearbyLocation } from '../../../../../src/lib/booking/pickup-location-types';
|
|
35
|
+
import { useTranslations } from '../../../../../src/lib/booking/i18n';
|
|
36
|
+
import { useBookingApp } from '../../contexts/BookingAppContext';
|
|
37
37
|
import styles from './PickupLocationSelector.module.css';
|
|
38
38
|
|
|
39
39
|
export interface CustomPickupLocation {
|
|
@@ -274,9 +274,10 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
|
|
|
274
274
|
const searchInputRef = useRef<HTMLInputElement | null>(null);
|
|
275
275
|
const focusTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
276
276
|
|
|
277
|
+
const { env } = useBookingHost();
|
|
277
278
|
const { googleMapsApiKey: keyFromContext } = useBookingApp();
|
|
278
279
|
const keyFromWindow = typeof window !== 'undefined' ? (window as unknown as { __TICKETBOOTH_GOOGLE_MAPS_API_KEY__?: string }).__TICKETBOOTH_GOOGLE_MAPS_API_KEY__ : undefined;
|
|
279
|
-
const googleMapsApiKey = keyFromContext ?? keyFromWindow ??
|
|
280
|
+
const googleMapsApiKey = keyFromContext ?? keyFromWindow ?? env.GOOGLE_MAPS_API_KEY;
|
|
280
281
|
|
|
281
282
|
// ============ Google Maps API Loading ============
|
|
282
283
|
const { isLoaded, loadError } = useJsApiLoader({
|
|
@@ -1119,7 +1120,7 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
|
|
|
1119
1120
|
{/* When "I don't know" filter is active, show only that option (unless hidden) */}
|
|
1120
1121
|
{!hideSkipOption && dontKnowFilterActive ? (
|
|
1121
1122
|
<label
|
|
1122
|
-
className="flex items-start gap-2 cursor-pointer p-2.5 rounded-lg hover:bg-stone-50
|
|
1123
|
+
className="flex items-start gap-2 cursor-pointer p-2.5 rounded-lg hover:bg-stone-50"
|
|
1123
1124
|
onClick={(e) => {
|
|
1124
1125
|
e.preventDefault();
|
|
1125
1126
|
handleSkip();
|
|
@@ -1146,7 +1147,7 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
|
|
|
1146
1147
|
<>
|
|
1147
1148
|
{/* Custom address option - show at TOP when user searched (Private Shuttle allows custom), and within service area if restricted */}
|
|
1148
1149
|
{allowCustomLocation && searchedLocation && !exactMatchLocationId && !cityFilter && isCustomLocationInServiceArea && (
|
|
1149
|
-
<label className="flex items-start gap-2 cursor-pointer p-3 rounded-lg hover:bg-emerald-50/
|
|
1150
|
+
<label className="flex items-start gap-2 cursor-pointer p-3 rounded-lg hover:bg-emerald-50/60 bg-emerald-50/50 shadow-sm" onMouseEnter={() => { setHoveredMarker('searched'); mapRef.current && searchedLocation.coordinates && panToLocationIfNeeded(mapRef.current, searchedLocation.coordinates); }} onMouseLeave={() => setHoveredMarker(null)}>
|
|
1150
1151
|
<input type="radio" name="pickup-location" checked={selectedCustomAddress === searchedLocation.address} onChange={() => handleCustomLocationSelect(searchedLocation.address, searchedLocation.coordinates ?? undefined)} className="mt-1 w-4 h-4 text-emerald-600 focus:ring-emerald-500 shrink-0" />
|
|
1151
1152
|
<div className="flex-1 min-w-0">
|
|
1152
1153
|
<p className="font-semibold text-emerald-800 text-sm">{t('pickup.useThisAddress')}</p>
|
|
@@ -1159,10 +1160,10 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
|
|
|
1159
1160
|
const isNearby = nearbyLocations.length > 0 && nearbyLocations.some(n => n.id === location.id);
|
|
1160
1161
|
const isPartnerHighlighted = highlightedPickupLocationIdSet.has(location.id);
|
|
1161
1162
|
return (
|
|
1162
|
-
<label key={location.id} className={`flex items-start gap-2 cursor-pointer p-2.5 rounded-lg
|
|
1163
|
+
<label key={location.id} className={`flex items-start gap-2 cursor-pointer p-2.5 rounded-lg ${
|
|
1163
1164
|
isPartnerHighlighted
|
|
1164
|
-
? '
|
|
1165
|
-
: '
|
|
1165
|
+
? 'bg-emerald-50/40 hover:bg-emerald-50'
|
|
1166
|
+
: 'hover:bg-stone-50'
|
|
1166
1167
|
}`} onMouseEnter={() => { setHoveredMarker(location.id); mapRef.current && location.coordinates && panToLocationIfNeeded(mapRef.current, location.coordinates); }} onMouseLeave={() => setHoveredMarker(null)}>
|
|
1167
1168
|
<input type="radio" name="pickup-location" checked={selectedLocationId === location.id} onChange={() => handleLocationSelect(location.id, location.name)} className="mt-1 w-4 h-4 text-emerald-600 focus:ring-emerald-500 shrink-0" />
|
|
1168
1169
|
<div className="flex-1 min-w-0">
|
|
@@ -1202,7 +1203,7 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
|
|
|
1202
1203
|
{/* I don't know - only at end of list when no filters are selected (unless hidden) */}
|
|
1203
1204
|
{!hideSkipOption && selectedFilters.size === 0 && (
|
|
1204
1205
|
<label
|
|
1205
|
-
className="flex items-start gap-2 cursor-pointer p-2.5 rounded-lg hover:bg-stone-50
|
|
1206
|
+
className="flex items-start gap-2 cursor-pointer p-2.5 rounded-lg hover:bg-stone-50"
|
|
1206
1207
|
onClick={(e) => {
|
|
1207
1208
|
e.preventDefault();
|
|
1208
1209
|
handleSkip();
|
|
@@ -1527,10 +1528,11 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
|
|
|
1527
1528
|
* don't hit ApiProjectMapError.
|
|
1528
1529
|
*/
|
|
1529
1530
|
export function PickupLocationSelector(props: PickupLocationSelectorProps) {
|
|
1531
|
+
const { env } = useBookingHost();
|
|
1530
1532
|
const { t } = useTranslations();
|
|
1531
1533
|
const { googleMapsApiKey: keyFromContext } = useBookingApp();
|
|
1532
1534
|
const keyFromWindow = typeof window !== 'undefined' ? (window as unknown as { __TICKETBOOTH_GOOGLE_MAPS_API_KEY__?: string }).__TICKETBOOTH_GOOGLE_MAPS_API_KEY__ : undefined;
|
|
1533
|
-
const mapsKey = keyFromContext ?? keyFromWindow ??
|
|
1535
|
+
const mapsKey = keyFromContext ?? keyFromWindow ?? env.GOOGLE_MAPS_API_KEY;
|
|
1534
1536
|
const hasMapsKey = Boolean(mapsKey?.trim());
|
|
1535
1537
|
const showMapsDebug =
|
|
1536
1538
|
typeof window !== 'undefined' && new URLSearchParams(window.location.search).get('maps_debug') === '1';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import type { Availability } from '
|
|
4
|
-
import type { ProductOption } from '
|
|
3
|
+
import type { Availability } from '../../../../../src/lib/booking-api';
|
|
4
|
+
import type { ProductOption } from '../../../../../src/lib/booking-api';
|
|
5
5
|
import styles from './PickupTimeSelector.module.css';
|
|
6
6
|
|
|
7
7
|
type TranslationFn = (key: string, params?: Record<string, string | number>) => string;
|
|
@@ -98,7 +98,7 @@ export function PickupTimeSelector({
|
|
|
98
98
|
{t('booking.soldOut')}
|
|
99
99
|
</div>
|
|
100
100
|
)}
|
|
101
|
-
{isInsufficientForParty && !isAdmin && (
|
|
101
|
+
{isInsufficientForParty && !isSoldOut && !isAdmin && (
|
|
102
102
|
<div className={styles.soldOut}>
|
|
103
103
|
{`Only ${timeInfo.vacancies} spot${timeInfo.vacancies === 1 ? '' : 's'} left, decrease your ticket count below to select this time`}
|
|
104
104
|
</div>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
|
-
import { formatCurrencyAmount } from '
|
|
5
|
-
import type { PriceBreakdown as PriceBreakdownType } from '
|
|
6
|
-
import { useTranslations } from '
|
|
7
|
-
import { useBookingApp } from '
|
|
4
|
+
import { formatCurrencyAmount } from '../../../../../src/lib/currency';
|
|
5
|
+
import type { PriceBreakdown as PriceBreakdownType } from '../../../../../src/lib/booking/pricing';
|
|
6
|
+
import { useTranslations } from '../../../../../src/lib/booking/i18n';
|
|
7
|
+
import { useBookingApp } from '../../contexts/BookingAppContext';
|
|
8
8
|
import type { Currency } from './CurrencySwitcher';
|
|
9
|
-
import type { Locale } from '
|
|
9
|
+
import type { Locale } from '../../../../../src/lib/booking/i18n/config';
|
|
10
10
|
|
|
11
11
|
export interface PriceBreakdownProps {
|
|
12
12
|
category: string;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Receipt rules only where the design always had a line (subtotal row, total row).
|
|
3
|
+
* Tax/discount stay plain rows + spacing — lines above promo come from PromoCodeInput.promoRow.
|
|
4
|
+
*/
|
|
5
|
+
.ruleAbove {
|
|
6
|
+
border-top: 1px solid var(--booking-stone-200, #e7e5e4);
|
|
7
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { formatCurrencyAmount } from '
|
|
4
|
-
import type { PriceBreakdown as PriceBreakdownType } from '
|
|
3
|
+
import { formatCurrencyAmount } from '../../../../../src/lib/currency';
|
|
4
|
+
import type { PriceBreakdown as PriceBreakdownType } from '../../../../../src/lib/booking/pricing';
|
|
5
5
|
import { PriceBreakdown } from './PriceBreakdown';
|
|
6
6
|
import { InfoTooltip } from './InfoTooltip';
|
|
7
7
|
import type { Currency } from './CurrencySwitcher';
|
|
8
|
-
import type { Locale } from '
|
|
8
|
+
import type { Locale } from '../../../../../src/lib/booking/i18n/config';
|
|
9
|
+
import styles from './PriceSummary.module.css';
|
|
9
10
|
|
|
10
11
|
/** One row in the price summary: either a ticket line (with optional breakdown tooltip) or a simple line. */
|
|
11
12
|
export type PriceSummaryLine =
|
|
@@ -112,8 +113,8 @@ export function PriceSummary({
|
|
|
112
113
|
const textSize = size === 'sm' ? 'text-sm' : 'text-base';
|
|
113
114
|
const totalSize = size === 'sm' ? 'text-xl' : 'text-2xl';
|
|
114
115
|
const subtotalRowClass = subtotalSpacing === 'relaxed'
|
|
115
|
-
?
|
|
116
|
-
:
|
|
116
|
+
? `pt-3 pb-3 mt-2 ${styles.ruleAbove}`
|
|
117
|
+
: `mt-2 pt-1.5 ${styles.ruleAbove}`;
|
|
117
118
|
|
|
118
119
|
let subtotalShown = false;
|
|
119
120
|
|
|
@@ -194,7 +195,7 @@ export function PriceSummary({
|
|
|
194
195
|
<div className="space-y-0">
|
|
195
196
|
{depositMode ? (
|
|
196
197
|
<>
|
|
197
|
-
<div className={`flex justify-between gap-3 pt-2
|
|
198
|
+
<div className={`flex justify-between gap-3 pt-2 min-w-0 ${totalSize} ${styles.ruleAbove}`}>
|
|
198
199
|
<span className="font-semibold text-stone-900 min-w-0 truncate">
|
|
199
200
|
{t('common.total') && t('common.total') !== 'common.total' ? t('common.total') : 'Total'}
|
|
200
201
|
</span>
|
|
@@ -218,7 +219,7 @@ export function PriceSummary({
|
|
|
218
219
|
)}
|
|
219
220
|
</>
|
|
220
221
|
) : (
|
|
221
|
-
<div className={`flex justify-between gap-3 pt-2
|
|
222
|
+
<div className={`flex justify-between gap-3 pt-2 min-w-0 ${totalSize} ${styles.ruleAbove}`}>
|
|
222
223
|
<span className="font-semibold text-stone-900 min-w-0 truncate">
|
|
223
224
|
{totalLabel ??
|
|
224
225
|
(t('common.total') && t('common.total') !== 'common.total' ? t('common.total') : 'Total')}
|