@ticketboothapp/booking 0.1.23 → 1.2.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/package.json +2 -29
  2. package/src/index.ts +0 -79
  3. package/tsconfig.json +2 -8
  4. package/src/assets/icons/minus.svg +0 -7
  5. package/src/assets/icons/partner-logos/getyourguide.svg +0 -8
  6. package/src/assets/icons/plus.svg +0 -3
  7. package/src/colours.css +0 -23
  8. package/src/components/BookingDetails.module.css +0 -1591
  9. package/src/components/BookingDetails.tsx +0 -2264
  10. package/src/components/BookingWidget.tsx +0 -302
  11. package/src/components/ManageBookingView.tsx +0 -437
  12. package/src/components/PhoneInputWithCountry.module.css +0 -131
  13. package/src/components/PhoneInputWithCountry.tsx +0 -44
  14. package/src/components/PickupLocationDialog.module.css +0 -360
  15. package/src/components/PickupLocationDialog.tsx +0 -357
  16. package/src/components/PostBookingDependentAddOnUpsell.module.css +0 -174
  17. package/src/components/PostBookingDependentAddOnUpsell.tsx +0 -407
  18. package/src/components/booking/AddOnsSection.module.css +0 -10
  19. package/src/components/booking/AddOnsSection.tsx +0 -184
  20. package/src/components/booking/AdminPaymentChoiceModal.tsx +0 -98
  21. package/src/components/booking/BookingDialog.module.css +0 -643
  22. package/src/components/booking/BookingDialog.tsx +0 -356
  23. package/src/components/booking/BookingFlow.tsx +0 -4385
  24. package/src/components/booking/BookingFlowCollage.module.css +0 -148
  25. package/src/components/booking/BookingFlowCollage.tsx +0 -184
  26. package/src/components/booking/BookingFlowPlaceholder.module.css +0 -27
  27. package/src/components/booking/BookingFlowPlaceholder.tsx +0 -25
  28. package/src/components/booking/BookingFlowPreview.tsx +0 -51
  29. package/src/components/booking/BookingProductGrid.module.css +0 -359
  30. package/src/components/booking/BookingProductGrid.tsx +0 -497
  31. package/src/components/booking/Calendar.module.css +0 -616
  32. package/src/components/booking/Calendar.tsx +0 -1123
  33. package/src/components/booking/CancellationPolicySelector.module.css +0 -124
  34. package/src/components/booking/CancellationPolicySelector.tsx +0 -142
  35. package/src/components/booking/ChangeBookingDialog.tsx +0 -562
  36. package/src/components/booking/CheckoutForm.module.css +0 -244
  37. package/src/components/booking/CheckoutForm.tsx +0 -364
  38. package/src/components/booking/CheckoutModal.tsx +0 -451
  39. package/src/components/booking/CurrencySwitcher.tsx +0 -81
  40. package/src/components/booking/DapFlowCollage.tsx +0 -88
  41. package/src/components/booking/DapTourDescription.tsx +0 -35
  42. package/src/components/booking/DependentAddOnBookingDialog.tsx +0 -1350
  43. package/src/components/booking/DependentAddOnPaymentForm.tsx +0 -124
  44. package/src/components/booking/ErrorBoundary.tsx +0 -63
  45. package/src/components/booking/InfoTooltip.tsx +0 -108
  46. package/src/components/booking/ItineraryBox.module.css +0 -258
  47. package/src/components/booking/ItineraryBox.tsx +0 -550
  48. package/src/components/booking/ItineraryBuilder.tsx +0 -82
  49. package/src/components/booking/ItineraryPlaceholder.module.css +0 -45
  50. package/src/components/booking/ItineraryPlaceholder.tsx +0 -26
  51. package/src/components/booking/MealDrinkAddOnSelector.tsx +0 -338
  52. package/src/components/booking/PickupLocationSelector.module.css +0 -124
  53. package/src/components/booking/PickupLocationSelector.tsx +0 -1566
  54. package/src/components/booking/PickupTimeSelector.module.css +0 -134
  55. package/src/components/booking/PickupTimeSelector.tsx +0 -112
  56. package/src/components/booking/PriceBreakdown.tsx +0 -154
  57. package/src/components/booking/PriceSummary.tsx +0 -234
  58. package/src/components/booking/PrivateShuttleBookingFlow.module.css +0 -357
  59. package/src/components/booking/PrivateShuttleBookingFlow.tsx +0 -2662
  60. package/src/components/booking/PromoCodeInput.module.css +0 -166
  61. package/src/components/booking/PromoCodeInput.tsx +0 -99
  62. package/src/components/booking/ReturnTimeSelector.module.css +0 -173
  63. package/src/components/booking/ReturnTimeSelector.tsx +0 -145
  64. package/src/components/booking/TermsAcceptance.tsx +0 -111
  65. package/src/components/booking/TicketSelector.module.css +0 -164
  66. package/src/components/booking/TicketSelector.tsx +0 -199
  67. package/src/components/booking/TourDescription.module.css +0 -304
  68. package/src/components/booking/TourDescription.tsx +0 -273
  69. package/src/components/booking/booking-flow-ui.ts +0 -38
  70. package/src/components/booking/booking-flow.css +0 -944
  71. package/src/components/button.css +0 -245
  72. package/src/components/button.tsx +0 -152
  73. package/src/components/colorable-svg.tsx +0 -29
  74. package/src/components/image.css +0 -29
  75. package/src/components/image.tsx +0 -113
  76. package/src/components/partner/PartnerBookingPage.module.css +0 -130
  77. package/src/components/partner/PartnerBookingPage.tsx +0 -390
  78. package/src/components/partner/PartnerBookingPageWithBrowserMetadata.tsx +0 -45
  79. package/src/components/product-tag.module.css +0 -30
  80. package/src/components/product-tag.tsx +0 -34
  81. package/src/components/product-theme-pages/image-modal.tsx +0 -248
  82. package/src/components/product-theme-pages/photo-gallery.module.css +0 -200
  83. package/src/components/terms/TermsContent.tsx +0 -178
  84. package/src/components/value-pill.module.css +0 -59
  85. package/src/components/value-pill.tsx +0 -46
  86. package/src/constants/images.ts +0 -556
  87. package/src/constants/pill-values.ts +0 -210
  88. package/src/constants/products.ts +0 -155
  89. package/src/contexts/AvailabilitiesCacheContext.tsx +0 -125
  90. package/src/contexts/BookingAppContext.tsx +0 -134
  91. package/src/contexts/CompanyContext.tsx +0 -70
  92. package/src/data/dap-descriptions/session-couples-families-friends.en.json +0 -61
  93. package/src/data/dap-descriptions/session-elopements.en.json +0 -60
  94. package/src/data/dap-descriptions/session-proposals.en.json +0 -60
  95. package/src/data/product-descriptions/afternoon-delight.en.json +0 -35
  96. package/src/data/product-descriptions/emerald-lake-escape.en.json +0 -68
  97. package/src/data/product-descriptions/lake-louise-adventure.en.json +0 -74
  98. package/src/data/product-descriptions/moraine-lake-adventure.en.json +0 -78
  99. package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +0 -65
  100. package/src/data/product-descriptions/moraine-lake-sunrise.en.json +0 -64
  101. package/src/data/product-descriptions/private-tour.en.json +0 -80
  102. package/src/data/product-descriptions/two-lakes-combo.en.json +0 -65
  103. package/src/data/products-config.json +0 -101
  104. package/src/hooks/useBookingSourceMetadataFromLocation.ts +0 -21
  105. package/src/hooks/useIsBookingLaunchLive.ts +0 -49
  106. package/src/lib/analytics.ts +0 -197
  107. package/src/lib/booking/booking-source.ts +0 -51
  108. package/src/lib/booking/checkout-breakdown.ts +0 -69
  109. package/src/lib/booking/correlation-id.ts +0 -46
  110. package/src/lib/booking/i18n/config.ts +0 -21
  111. package/src/lib/booking/i18n/index.tsx +0 -144
  112. package/src/lib/booking/i18n/messages/en.json +0 -236
  113. package/src/lib/booking/i18n/messages/fr.json +0 -236
  114. package/src/lib/booking/itinerary-display.ts +0 -36
  115. package/src/lib/booking/itinerary-labels.ts +0 -70
  116. package/src/lib/booking/location-calculations.ts +0 -43
  117. package/src/lib/booking/location-utils.ts +0 -165
  118. package/src/lib/booking/map-utils.ts +0 -153
  119. package/src/lib/booking/marker-icons.ts +0 -113
  120. package/src/lib/booking/normalize-booking-product-id.ts +0 -21
  121. package/src/lib/booking/pickup-location-types.ts +0 -25
  122. package/src/lib/booking/places-api.ts +0 -154
  123. package/src/lib/booking/pricing.ts +0 -466
  124. package/src/lib/booking/product-option-id.ts +0 -35
  125. package/src/lib/booking/source-metadata.ts +0 -226
  126. package/src/lib/booking/sunday-week.ts +0 -14
  127. package/src/lib/booking/theme.ts +0 -83
  128. package/src/lib/booking/trace-context.ts +0 -62
  129. package/src/lib/booking/utils.ts +0 -9
  130. package/src/lib/booking-api.ts +0 -1793
  131. package/src/lib/booking-constants.ts +0 -23
  132. package/src/lib/booking-ref.ts +0 -13
  133. package/src/lib/booking-types.ts +0 -36
  134. package/src/lib/currency.ts +0 -81
  135. package/src/lib/dap-descriptions.ts +0 -50
  136. package/src/lib/dap-itinerary-preview.ts +0 -315
  137. package/src/lib/dependent-add-on-api.ts +0 -434
  138. package/src/lib/env.ts +0 -96
  139. package/src/lib/firebase.ts +0 -20
  140. package/src/lib/job-application-api.ts +0 -83
  141. package/src/lib/manage-booking-embed-print.ts +0 -16
  142. package/src/lib/manage-booking-post-checkout.ts +0 -68
  143. package/src/lib/photo-dap-config.ts +0 -228
  144. package/src/lib/photo-packages.ts +0 -75
  145. package/src/lib/pickup/map-utils.ts +0 -56
  146. package/src/lib/pickup/marker-icons.ts +0 -19
  147. package/src/lib/product-descriptions.ts +0 -66
  148. package/src/lib/products-config.ts +0 -73
  149. package/src/providers/booking-dialog-provider.tsx +0 -282
  150. package/src/providers/dependent-add-on-dialog-provider.tsx +0 -105
  151. package/src/radius.css +0 -5
  152. package/src/spacing.css +0 -7
  153. package/src/strings/en.json +0 -1774
  154. package/src/strings/es.json +0 -1573
  155. package/src/strings/fr.json +0 -1573
  156. package/src/strings/index.js +0 -23
  157. package/src/text-style.css +0 -56
  158. package/src/utils/currency-converter.ts +0 -101
@@ -1,134 +0,0 @@
1
- /**
2
- * Pickup time selector - grid of time slot buttons
3
- */
4
-
5
- .label {
6
- display: block;
7
- font-size: 0.875rem;
8
- font-weight: 500;
9
- color: var(--booking-stone-700, #44403c);
10
- margin-bottom: 0.5rem;
11
- }
12
-
13
- .grid {
14
- display: grid;
15
- grid-template-columns: repeat(3, minmax(0, 1fr));
16
- gap: 0.5rem;
17
- }
18
-
19
- @media (min-width: 640px) {
20
- .grid {
21
- grid-template-columns: repeat(6, minmax(0, 1fr));
22
- }
23
- }
24
-
25
- .btn {
26
- padding: 0.875rem 1.25rem;
27
- border-radius: 0.5rem;
28
- font-size: 0.875rem;
29
- font-weight: 500;
30
- transition: all 0.2s;
31
- position: relative;
32
- }
33
-
34
- .btnDefault {
35
- padding-top: 0.75rem;
36
- }
37
-
38
- .btnWithBadge {
39
- padding-top: 1.25rem;
40
- }
41
-
42
- @media (min-width: 640px) {
43
- .btnWithBadge {
44
- padding-top: 1rem;
45
- }
46
- }
47
-
48
- .btnSelected {
49
- background: var(--booking-emerald-600, #059669);
50
- color: #fff;
51
- }
52
-
53
- .btnAvailable {
54
- background: var(--light-orange-background-dark, #f7e4dc);
55
- color: var(--booking-stone-700, #44403c);
56
- }
57
-
58
- .btnAvailable:hover {
59
- background: var(--light-orange-background, #fff1eb);
60
- }
61
-
62
- .btnDisabled {
63
- background: var(--booking-stone-100, #f5f5f4);
64
- color: var(--booking-stone-400, #a8a29e);
65
- cursor: not-allowed;
66
- }
67
-
68
- .btnSoldOutAdmin {
69
- background: #fee2e2;
70
- color: #b91c1c;
71
- border: 1px solid #fca5a5;
72
- }
73
-
74
- .btnSoldOutAdmin:hover {
75
- background: #fee2e2;
76
- }
77
-
78
- .btnSoldOutLocked {
79
- background: #fee2e2;
80
- color: #b91c1c;
81
- border: 1px solid #fca5a5;
82
- cursor: not-allowed;
83
- }
84
-
85
- .btnSoldOutLocked:hover {
86
- background: #fee2e2;
87
- }
88
-
89
- .capacity {
90
- font-size: 0.75rem;
91
- margin-top: 0.125rem;
92
- font-variant-numeric: tabular-nums;
93
- }
94
-
95
- /* Fitted to green selected tile — not pure white (readability) */
96
- .capacityOnSelected {
97
- color: rgba(255, 255, 255, 0.9);
98
- }
99
-
100
- .capacityDefault {
101
- color: var(--booking-stone-500, #78716c);
102
- }
103
-
104
- .capacityProjected {
105
- font-size: 0.7rem;
106
- line-height: 1.2;
107
- margin-top: 0.25rem;
108
- font-weight: 600;
109
- text-align: center;
110
- color: #b91c1c;
111
- font-variant-numeric: tabular-nums;
112
- }
113
-
114
- .badge {
115
- position: absolute;
116
- top: -0.75rem;
117
- left: 50%;
118
- transform: translateX(-50%);
119
- font-family: 'Poppins', sans-serif;
120
- font-size: 0.75rem;
121
- font-weight: 600;
122
- text-transform: lowercase;
123
- color: #fff;
124
- padding: 0.25rem 0.625rem;
125
- border-radius: 9999px;
126
- white-space: nowrap;
127
- background-color: #ff4d00;
128
- }
129
-
130
- .soldOut {
131
- font-size: 0.75rem;
132
- font-weight: 500;
133
- color: #b91c1c;
134
- }
@@ -1,112 +0,0 @@
1
- 'use client';
2
-
3
- import type { Availability } from '@/lib/booking-api';
4
- import type { ProductOption } from '@/lib/booking-api';
5
- import styles from './PickupTimeSelector.module.css';
6
-
7
- type TranslationFn = (key: string, params?: Record<string, string | number>) => string;
8
-
9
- export interface PickupTimeInfo extends Availability {
10
- pickupTime: string;
11
- displayTime: string;
12
- originalTime: string;
13
- displayTimeRange?: string;
14
- }
15
-
16
- interface PickupTimeSelectorProps {
17
- pickupTimes: PickupTimeInfo[];
18
- selectedDateTime: string | null;
19
- selectedTicketCount: number;
20
- optionsMap: Map<string, ProductOption>;
21
- hasAnyMostPopular: boolean;
22
- isAdmin: boolean;
23
- pickupLocationSkipped: boolean;
24
- t: TranslationFn;
25
- onTimeSelect: (availability: Availability) => void;
26
- }
27
-
28
- export function PickupTimeSelector({
29
- pickupTimes,
30
- selectedDateTime,
31
- selectedTicketCount,
32
- optionsMap,
33
- hasAnyMostPopular,
34
- isAdmin,
35
- pickupLocationSkipped,
36
- t,
37
- onTimeSelect,
38
- }: PickupTimeSelectorProps) {
39
- return (
40
- <div>
41
- <label className={styles.label}>
42
- {t('booking.selectPickupTime')}
43
- </label>
44
- <div className={styles.grid}>
45
- {pickupTimes.map((timeInfo) => {
46
- const isSelected = selectedDateTime === timeInfo.dateTime;
47
- const isSoldOut = timeInfo.vacancies === 0;
48
- const isInsufficientForParty =
49
- selectedTicketCount > 0 && (timeInfo.vacancies ?? 0) < selectedTicketCount;
50
- const canSelect = isAdmin || (!isSoldOut && !isInsufficientForParty);
51
- const option = timeInfo.productOptionId
52
- ? optionsMap.get(timeInfo.productOptionId)
53
- : undefined;
54
- const isMostPopular = pickupTimes.length > 1 && option?.mostPopular;
55
- const totalCap = timeInfo.totalCapacity ?? 0;
56
- const booked = timeInfo.bookedCapacity ?? (totalCap - (timeInfo.vacancies ?? 0));
57
- const showCapacity = isAdmin && totalCap > 0;
58
- const slotVacancies = timeInfo.vacancies;
59
- const projectedSeats = booked + selectedTicketCount;
60
- const showAdminProjectedLoad =
61
- isSelected && showCapacity && selectedTicketCount > slotVacancies;
62
- // Selected tile stays green; unselected + admin + tight party uses public-style tile, not "sold out admin" pink
63
- const btnClass = (() => {
64
- if (isSelected) return styles.btnSelected;
65
- if (!isAdmin) {
66
- return (isSoldOut || isInsufficientForParty) ? styles.btnSoldOutLocked : styles.btnAvailable;
67
- }
68
- if (isSoldOut && slotVacancies === 0) return styles.btnSoldOutAdmin;
69
- return styles.btnAvailable;
70
- })();
71
- return (
72
- <button
73
- key={timeInfo.dateTime}
74
- onClick={() => canSelect && onTimeSelect(timeInfo)}
75
- disabled={!canSelect}
76
- className={`${styles.btn} ${hasAnyMostPopular ? styles.btnWithBadge : styles.btnDefault} ${btnClass}`}
77
- >
78
- <div>{pickupLocationSkipped && timeInfo.displayTimeRange ? timeInfo.displayTimeRange : timeInfo.displayTime}</div>
79
- {showCapacity && (showAdminProjectedLoad ? (
80
- <div className={styles.capacityProjected} aria-live="polite">
81
- <div>{t('booking.adminBookingLoadLine1', { projected: projectedSeats })}</div>
82
- <div>{t('booking.adminBookingLoadLine2', { total: totalCap })}</div>
83
- </div>
84
- ) : (
85
- <div
86
- className={`${styles.capacity} ${isSelected ? styles.capacityOnSelected : styles.capacityDefault}`}
87
- >
88
- {t('calendar.spotsAvailable', { count: slotVacancies })}
89
- </div>
90
- ))}
91
- {isMostPopular && (
92
- <div className={styles.badge}>
93
- {t('booking.mostPopular')}
94
- </div>
95
- )}
96
- {isSoldOut && (
97
- <div className={styles.soldOut}>
98
- {t('booking.soldOut')}
99
- </div>
100
- )}
101
- {isInsufficientForParty && !isAdmin && (
102
- <div className={styles.soldOut}>
103
- {`Only ${timeInfo.vacancies} spot${timeInfo.vacancies === 1 ? '' : 's'} left, decrease your ticket count below to select this time`}
104
- </div>
105
- )}
106
- </button>
107
- );
108
- })}
109
- </div>
110
- </div>
111
- );
112
- }
@@ -1,154 +0,0 @@
1
- 'use client';
2
-
3
- import { useState } from 'react';
4
- import { formatCurrencyAmount } from '@/lib/currency';
5
- import type { PriceBreakdown as PriceBreakdownType } from '@/lib/booking/pricing';
6
- import { useTranslations } from '@/lib/booking/i18n';
7
- import { useBookingApp } from '@/contexts/BookingAppContext';
8
- import type { Currency } from './CurrencySwitcher';
9
- import type { Locale } from '@/lib/booking/i18n/config';
10
-
11
- export interface PriceBreakdownProps {
12
- category: string;
13
- qty: number;
14
- itemTotal: number;
15
- breakdown: PriceBreakdownType | null;
16
- currency: Currency;
17
- locale: Locale;
18
- }
19
-
20
- /**
21
- * Format adjustment details for display (e.g., "+10%", "-30%")
22
- * Uses the actual price change sign to determine +/- display
23
- */
24
- function formatAdjustmentDetail(
25
- adjustmentType?: string, // 'percentage' or 'fixed'
26
- adjustmentValue?: number,
27
- isNegativeChange?: boolean // true if changeByCurrency is negative
28
- ): string {
29
- if (!adjustmentType || adjustmentValue === undefined) return '';
30
-
31
- const sign = isNegativeChange ? '-' : '+';
32
- const absValue = Math.abs(adjustmentValue);
33
-
34
- if (adjustmentType === 'percentage') {
35
- return ` (${sign}${absValue}%)`;
36
- } else if (adjustmentType === 'fixed') {
37
- return ` (${sign}C$${absValue.toFixed(2)})`;
38
- }
39
- return '';
40
- }
41
-
42
- /**
43
- * Renders a single price line (e.g. "Adult × 2 — $X"). When the host app grants
44
- * permission, shows a hover tooltip with the full price breakdown (base, adjustments,
45
- * fee, tax). Used in the booking itinerary summary.
46
- */
47
- export function PriceBreakdown({
48
- category,
49
- qty,
50
- itemTotal,
51
- breakdown,
52
- currency,
53
- locale,
54
- }: PriceBreakdownProps) {
55
- const [showTooltip, setShowTooltip] = useState(false);
56
- const { t } = useTranslations();
57
- const { permissions } = useBookingApp();
58
- const canShowBreakdown = breakdown != null && permissions.canViewPriceBreakdown;
59
-
60
- // Calculate base price for discount detection
61
- // In public mode, the base price in breakdown already has dynamic increases rolled in
62
- const basePricePerPerson = breakdown?.lineItems?.find((l) => l.type === 'base')?.amountInDisplayCurrency;
63
- const baseTotal = basePricePerPerson != null ? basePricePerPerson * qty : 0;
64
- const hasDiscount = baseTotal > 0 && itemTotal < baseTotal; // only when final < original
65
-
66
- if (!breakdown) {
67
- return (
68
- <div className="flex items-center justify-between">
69
- <span className="text-sm text-stone-600">
70
- {category} {qty > 1 ? `× ${qty}` : ''}
71
- </span>
72
- <span className="text-sm font-medium text-stone-700">
73
- {formatCurrencyAmount(itemTotal, currency, locale)}
74
- </span>
75
- </div>
76
- );
77
- }
78
-
79
- return (
80
- <div className="flex items-center justify-between gap-3 min-w-0">
81
- <span className="text-sm text-stone-600 min-w-0 truncate">
82
- {category} {qty > 1 ? `× ${qty}` : ''}
83
- </span>
84
- <div className="relative flex-shrink-0 whitespace-nowrap">
85
- <span
86
- className={
87
- canShowBreakdown
88
- ? 'text-sm font-medium text-stone-700 cursor-help underline decoration-dotted'
89
- : 'text-sm font-medium text-stone-700'
90
- }
91
- onMouseEnter={canShowBreakdown ? () => setShowTooltip(true) : undefined}
92
- onMouseLeave={canShowBreakdown ? () => setShowTooltip(false) : undefined}
93
- >
94
- {hasDiscount ? (
95
- <>
96
- <span className="line-through text-stone-400">
97
- {formatCurrencyAmount(baseTotal, currency, locale)}
98
- </span>
99
- {' '}
100
- <span className="text-emerald-600">
101
- {formatCurrencyAmount(itemTotal, currency, locale)}
102
- </span>
103
- </>
104
- ) : (
105
- formatCurrencyAmount(itemTotal, currency, locale)
106
- )}
107
- </span>
108
- {canShowBreakdown && showTooltip && (
109
- <div className="absolute right-0 top-full mt-2 w-72 p-3 bg-stone-900 text-white text-xs rounded-lg shadow-xl z-50">
110
- <div className="font-semibold mb-2 pb-2 border-b border-stone-700">
111
- Price Breakdown ({category})
112
- </div>
113
- <div className="space-y-1.5">
114
- {breakdown.lineItems.map((line) => (
115
- <div key={line.id ?? line.name} className="flex justify-between">
116
- <span className="text-stone-400">
117
- {line.name}{line.type === 'base' ? ` (${currency})` : ''}{formatAdjustmentDetail(line.adjustmentType, line.adjustmentValue, line.amountInDisplayCurrency < 0)}
118
- </span>
119
- <span className={line.type === 'adjustment' && line.amountInDisplayCurrency < 0 ? 'text-red-300' : ''}>
120
- {line.type === 'adjustment' && line.amountInDisplayCurrency >= 0 ? '+' : ''}
121
- {formatCurrencyAmount(line.amountInDisplayCurrency, currency, locale)}
122
- </span>
123
- </div>
124
- ))}
125
- {/* Only show fees and tax in breakdown when rolled up (tax-inclusive); for CAD/USD they're already separate line items. */}
126
- {breakdown.isTaxIncluded && (
127
- <>
128
- {breakdown.feeLines.map((fee) => (
129
- <div key={fee.name} className="flex justify-between">
130
- <span className="text-stone-400" title={fee.description}>
131
- {fee.name}:
132
- </span>
133
- <span>{formatCurrencyAmount(fee.feeAmount, currency, locale)}</span>
134
- </div>
135
- ))}
136
- <div className="flex justify-between">
137
- <span className="text-stone-400">
138
- {`${t('booking.tax') !== 'booking.tax' ? t('booking.tax') : 'Taxes and fees'} (included):`}
139
- </span>
140
- <span>{formatCurrencyAmount(breakdown.taxAmount, currency, locale)}</span>
141
- </div>
142
- </>
143
- )}
144
- <div className="flex justify-between pt-2 mt-2 border-t border-stone-700 font-semibold">
145
- <span>Final price ({currency}):</span>
146
- <span>{formatCurrencyAmount(breakdown.finalPrice, currency, locale)}</span>
147
- </div>
148
- </div>
149
- </div>
150
- )}
151
- </div>
152
- </div>
153
- );
154
- }
@@ -1,234 +0,0 @@
1
- 'use client';
2
-
3
- import { formatCurrencyAmount } from '@/lib/currency';
4
- import type { PriceBreakdown as PriceBreakdownType } from '@/lib/booking/pricing';
5
- import { PriceBreakdown } from './PriceBreakdown';
6
- import { InfoTooltip } from './InfoTooltip';
7
- import type { Currency } from './CurrencySwitcher';
8
- import type { Locale } from '@/lib/booking/i18n/config';
9
-
10
- /** One row in the price summary: either a ticket line (with optional breakdown tooltip) or a simple line. */
11
- export type PriceSummaryLine =
12
- | {
13
- kind: 'ticket';
14
- category: string;
15
- qty: number;
16
- itemTotal: number;
17
- breakdown?: PriceBreakdownType | null;
18
- }
19
- | {
20
- kind: 'line';
21
- label: string;
22
- amount: number;
23
- /** Drives styling: discount (red, -), return add-on (green, +), default (stone). Receipt types: TICKET, FEE, RETURN_OPTION, PROMO_CODE, CANCELLATION_UPGRADE, TAX, etc. */
24
- type?: string;
25
- quantity?: number | null;
26
- /** Optional tooltip text - when set, shows info icon next to label (e.g. for Moraine Lake Road Access Fee) */
27
- tooltip?: string;
28
- };
29
-
30
- export interface PriceSummaryProps {
31
- /** Lines to show (tickets with optional breakdown, then fees/return/discount/etc.) */
32
- lines: PriceSummaryLine[];
33
- /** Total amount (required) */
34
- total: number;
35
- /** Currency and locale for formatting */
36
- currency: Currency;
37
- locale: Locale;
38
- /** Charges before tax. Shown as "Subtotal" row before tax (receipt: inserted before TAX line; checkout: after lines). */
39
- subtotal?: number;
40
- /** Checkout mode only: Tax amount when not in lines */
41
- taxAmount?: number;
42
- taxRate?: number;
43
- /** Checkout mode only: Discount amount when not in lines */
44
- discountAmount?: number;
45
- /** Checkout mode only: Label for discount row */
46
- discountLabel?: string | null;
47
- /** Size: 'sm' for modal/sidebar (text-sm), 'base' for manage page */
48
- size?: 'sm' | 'base';
49
- /** Optional i18n; falls back to default strings */
50
- t?: (key: string) => string;
51
- /** Optional: extra class on the container */
52
- className?: string;
53
- /** Optional: render between tax row and total row (e.g. promo code input in BookingFlow) */
54
- extraBetweenTaxAndTotal?: React.ReactNode;
55
- /** Subtotal row spacing: 'compact' for booking flow/Stripe modal, 'relaxed' for /manage (equal top/bottom) */
56
- subtotalSpacing?: 'compact' | 'relaxed';
57
- /** Deposit mode: show Total (full), Deposit (amount due today), Remaining Balance */
58
- depositMode?: { totalLabel: string; balanceAmount: number; fullTotalAmount: number };
59
- /** When true (e.g. deposit flow with single line), hide redundant Subtotal row */
60
- hideSubtotal?: boolean;
61
- /** Overrides the final "Total" row label (e.g. change booking: amount due for the change) */
62
- totalLabel?: string;
63
- }
64
-
65
- function getLineAmountClass(type: string | undefined, amount: number): string {
66
- const isDiscount = amount < 0 || type === 'PROMO_CODE' || type === 'DISCOUNT' || type === 'GIFT_CARD';
67
- const isAddOn = (type === 'RETURN_OPTION' || type === 'return') && amount > 0;
68
- if (isDiscount) return 'font-medium text-red-600';
69
- if (isAddOn) return 'font-medium text-emerald-600';
70
- return 'text-stone-700';
71
- }
72
-
73
- function formatLineAmount(
74
- kind: 'line',
75
- amount: number,
76
- type: string | undefined,
77
- currency: Currency,
78
- locale: Locale
79
- ): string {
80
- const formatted = formatCurrencyAmount(amount, currency, locale);
81
- const isAddOn = (type === 'RETURN_OPTION' || type === 'return') && amount > 0;
82
- if (isAddOn) return `+${formatted}`;
83
- return formatted;
84
- }
85
-
86
- /**
87
- * Reusable price breakdown/summary used in:
88
- * - BookingFlow sidebar (order summary)
89
- * - CheckoutModal ("Review & pay")
90
- * - BookingDetails /manage (Payment Summary from receipt)
91
- * - PrivateShuttleBookingFlow sidebar
92
- */
93
- export function PriceSummary({
94
- lines,
95
- total,
96
- currency,
97
- locale,
98
- subtotal,
99
- taxAmount = 0,
100
- taxRate: _taxRate,
101
- discountAmount = 0,
102
- discountLabel,
103
- size = 'sm',
104
- t = (k) => k,
105
- className = '',
106
- extraBetweenTaxAndTotal,
107
- subtotalSpacing = 'compact',
108
- depositMode,
109
- hideSubtotal = false,
110
- totalLabel,
111
- }: PriceSummaryProps) {
112
- const textSize = size === 'sm' ? 'text-sm' : 'text-base';
113
- const totalSize = size === 'sm' ? 'text-xl' : 'text-2xl';
114
- const subtotalRowClass = subtotalSpacing === 'relaxed'
115
- ? 'pt-3 pb-3 mt-2 border-t border-stone-200'
116
- : 'mt-2 pt-1.5 border-t border-stone-200';
117
-
118
- let subtotalShown = false;
119
-
120
- return (
121
- <div className={`space-y-2 min-w-0 ${className}`}>
122
- {lines.map((row, index) => {
123
- if (row.kind === 'ticket') {
124
- return (
125
- <PriceBreakdown
126
- key={`${row.category}-${index}`}
127
- category={row.category}
128
- qty={row.qty}
129
- itemTotal={row.itemTotal}
130
- breakdown={row.breakdown ?? null}
131
- currency={currency}
132
- locale={locale}
133
- />
134
- );
135
- }
136
- const { label, amount, type, quantity, tooltip } = row;
137
- // Receipt mode: insert Subtotal row before first TAX line
138
- const isTaxLine = type === 'TAX';
139
- const showSubtotalBeforeTax = isTaxLine && subtotal != null && subtotal > 0 && !subtotalShown;
140
- if (showSubtotalBeforeTax) subtotalShown = true;
141
- return (
142
- <div key={`${label}-${index}`}>
143
- {showSubtotalBeforeTax && (
144
- <div className={`flex justify-between gap-3 min-w-0 ${textSize} ${subtotalRowClass}`}>
145
- <span className="text-stone-600 min-w-0 truncate">{t('booking.subtotal') || 'Subtotal'}</span>
146
- <span className="flex-shrink-0 whitespace-nowrap font-medium text-stone-700">{formatCurrencyAmount(subtotal!, currency, locale)}</span>
147
- </div>
148
- )}
149
- <div className={`flex justify-between gap-3 min-w-0 ${textSize}`}>
150
- <span className="text-stone-600 min-w-0 flex items-center gap-1">
151
- <span className="min-w-0 truncate">
152
- {label}
153
- {type === 'TICKET' && quantity != null && quantity > 1 ? ` (x${quantity})` : ''}
154
- </span>
155
- {tooltip && <InfoTooltip text={tooltip} />}
156
- </span>
157
- <span className={`flex-shrink-0 whitespace-nowrap font-medium ${getLineAmountClass(type, amount)}`}>
158
- {formatLineAmount('line', amount, type, currency, locale)}
159
- </span>
160
- </div>
161
- </div>
162
- );
163
- })}
164
-
165
- {/* Checkout mode: subtotal/tax/discount not in lines (e.g. Stripe Review & pay modal) */}
166
- {subtotal != null && !subtotalShown && !hideSubtotal && (subtotal !== total || discountAmount > 0) && (
167
- <div className={`flex justify-between gap-3 min-w-0 ${textSize} ${subtotalRowClass}`}>
168
- <span className="text-stone-600 min-w-0 truncate">{t('booking.subtotal') || 'Subtotal'}</span>
169
- <span className="flex-shrink-0 whitespace-nowrap font-medium text-stone-700">{formatCurrencyAmount(subtotal, currency, locale)}</span>
170
- </div>
171
- )}
172
- {taxAmount > 0 && (
173
- <div className={`flex justify-between gap-3 min-w-0 ${textSize}`}>
174
- <span className="text-stone-600 min-w-0 truncate">
175
- {t('booking.tax') && t('booking.tax') !== 'booking.tax' ? t('booking.tax') : 'Taxes and fees'}
176
- </span>
177
- <span className="flex-shrink-0 whitespace-nowrap font-medium text-stone-700">
178
- {formatCurrencyAmount(taxAmount, currency, locale)}
179
- </span>
180
- </div>
181
- )}
182
-
183
- {discountAmount > 0 && (
184
- <div className={`flex justify-between gap-3 min-w-0 ${textSize}`}>
185
- <span className="text-stone-600 min-w-0 truncate">{discountLabel ?? t('booking.discount') ?? 'Discount'}</span>
186
- <span className="flex-shrink-0 whitespace-nowrap font-medium text-red-600">
187
- -{formatCurrencyAmount(discountAmount, currency, locale)}
188
- </span>
189
- </div>
190
- )}
191
-
192
- {extraBetweenTaxAndTotal}
193
-
194
- <div className="space-y-0">
195
- {depositMode ? (
196
- <>
197
- <div className={`flex justify-between gap-3 pt-2 border-t border-stone-200 min-w-0 ${totalSize}`}>
198
- <span className="font-semibold text-stone-900 min-w-0 truncate">
199
- {t('common.total') && t('common.total') !== 'common.total' ? t('common.total') : 'Total'}
200
- </span>
201
- <span className="flex-shrink-0 whitespace-nowrap font-semibold text-stone-900">
202
- {formatCurrencyAmount(depositMode.fullTotalAmount, currency, locale)}
203
- </span>
204
- </div>
205
- <div className={`flex justify-between gap-3 min-w-0 pt-1 ${textSize}`}>
206
- <span className="text-stone-600 min-w-0 truncate">{depositMode.totalLabel}</span>
207
- <span className="flex-shrink-0 whitespace-nowrap font-medium text-stone-700">
208
- {formatCurrencyAmount(total, currency, locale)}
209
- </span>
210
- </div>
211
- {depositMode.balanceAmount > 0 && (
212
- <div className={`flex justify-between gap-3 min-w-0 pt-1 ${textSize}`}>
213
- <span className="text-stone-600 min-w-0 truncate">{t('booking.remainingBalance') || 'Remaining Balance'}</span>
214
- <span className="flex-shrink-0 whitespace-nowrap font-medium text-stone-700">
215
- {formatCurrencyAmount(depositMode.balanceAmount, currency, locale)}
216
- </span>
217
- </div>
218
- )}
219
- </>
220
- ) : (
221
- <div className={`flex justify-between gap-3 pt-2 border-t border-stone-200 min-w-0 ${totalSize}`}>
222
- <span className="font-semibold text-stone-900 min-w-0 truncate">
223
- {totalLabel ??
224
- (t('common.total') && t('common.total') !== 'common.total' ? t('common.total') : 'Total')}
225
- </span>
226
- <span className="flex-shrink-0 whitespace-nowrap font-semibold text-stone-900">
227
- {formatCurrencyAmount(total, currency, locale)}
228
- </span>
229
- </div>
230
- )}
231
- </div>
232
- </div>
233
- );
234
- }