@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.
Files changed (142) hide show
  1. package/package.json +11 -29
  2. package/src/components/booking/AddOnsSection.tsx +2 -2
  3. package/src/components/booking/AdminPaymentChoiceModal.tsx +1 -1
  4. package/src/components/booking/BookingDialog.tsx +31 -13
  5. package/src/components/booking/BookingFlow.tsx +32 -27
  6. package/src/components/booking/BookingFlowCollage.tsx +10 -6
  7. package/src/components/booking/BookingFlowPlaceholder.tsx +1 -1
  8. package/src/components/booking/BookingFlowPreview.tsx +18 -9
  9. package/src/components/booking/BookingProductGrid.tsx +55 -19
  10. package/src/components/booking/Calendar.module.css +19 -4
  11. package/src/components/booking/Calendar.tsx +13 -8
  12. package/src/components/booking/CancellationPolicySelector.tsx +2 -2
  13. package/src/components/booking/ChangeBookingDialog.tsx +22 -12
  14. package/src/components/booking/CheckoutForm.module.css +10 -0
  15. package/src/components/booking/CheckoutForm.tsx +10 -2
  16. package/src/components/booking/CheckoutModal.tsx +16 -14
  17. package/src/components/booking/DapFlowCollage.tsx +5 -2
  18. package/src/components/booking/DapTourDescription.tsx +4 -4
  19. package/src/components/booking/DependentAddOnBookingDialog.tsx +23 -16
  20. package/src/components/booking/DependentAddOnPaymentForm.tsx +10 -7
  21. package/src/components/booking/ItineraryBox.tsx +6 -6
  22. package/src/components/booking/ItineraryBuilder.tsx +1 -1
  23. package/src/components/booking/MealDrinkAddOnSelector.tsx +3 -3
  24. package/src/components/booking/PickupLocationSelector.tsx +20 -18
  25. package/src/components/booking/PickupTimeSelector.tsx +3 -3
  26. package/src/components/booking/PriceBreakdown.tsx +5 -5
  27. package/src/components/booking/PriceSummary.module.css +7 -0
  28. package/src/components/booking/PriceSummary.tsx +8 -7
  29. package/src/components/booking/PrivateShuttleBookingFlow.tsx +28 -19
  30. package/src/components/booking/PromoCodeInput.module.css +31 -25
  31. package/src/components/booking/PromoCodeInput.tsx +36 -24
  32. package/src/components/booking/ReturnTimeSelector.tsx +3 -3
  33. package/src/components/booking/TermsAcceptance.tsx +7 -2
  34. package/src/components/booking/TicketSelector.tsx +1 -1
  35. package/src/components/booking/TourDescription.tsx +11 -6
  36. package/src/components/booking/booking-flow.css +65 -4
  37. package/src/hooks/useBookingSourceMetadataFromLocation.ts +1 -1
  38. package/src/hooks/useIsBookingLaunchLive.ts +1 -1
  39. package/src/index.ts +26 -64
  40. package/src/providers/booking-dialog-provider.tsx +62 -53
  41. package/src/runtime/BookingHostContext.tsx +39 -0
  42. package/src/runtime/index.ts +13 -0
  43. package/src/runtime/types.ts +86 -0
  44. package/tsconfig.json +3 -5
  45. package/src/assets/icons/minus.svg +0 -7
  46. package/src/assets/icons/partner-logos/getyourguide.svg +0 -8
  47. package/src/assets/icons/plus.svg +0 -3
  48. package/src/colours.css +0 -23
  49. package/src/components/BookingDetails.module.css +0 -1591
  50. package/src/components/BookingDetails.tsx +0 -2264
  51. package/src/components/BookingWidget.tsx +0 -305
  52. package/src/components/ManageBookingView.tsx +0 -437
  53. package/src/components/PhoneInputWithCountry.module.css +0 -131
  54. package/src/components/PhoneInputWithCountry.tsx +0 -44
  55. package/src/components/PickupLocationDialog.module.css +0 -360
  56. package/src/components/PickupLocationDialog.tsx +0 -357
  57. package/src/components/PostBookingDependentAddOnUpsell.module.css +0 -174
  58. package/src/components/PostBookingDependentAddOnUpsell.tsx +0 -407
  59. package/src/components/button.css +0 -245
  60. package/src/components/button.tsx +0 -152
  61. package/src/components/colorable-svg.tsx +0 -29
  62. package/src/components/image.css +0 -29
  63. package/src/components/image.tsx +0 -113
  64. package/src/components/partner/PartnerBookingPage.module.css +0 -130
  65. package/src/components/partner/PartnerBookingPage.tsx +0 -390
  66. package/src/components/partner/PartnerBookingPageWithBrowserMetadata.tsx +0 -45
  67. package/src/components/product-tag.module.css +0 -30
  68. package/src/components/product-tag.tsx +0 -34
  69. package/src/components/product-theme-pages/image-modal.tsx +0 -248
  70. package/src/components/product-theme-pages/photo-gallery.module.css +0 -200
  71. package/src/components/terms/TermsContent.tsx +0 -178
  72. package/src/components/value-pill.module.css +0 -59
  73. package/src/components/value-pill.tsx +0 -46
  74. package/src/constants/images.ts +0 -556
  75. package/src/constants/pill-values.ts +0 -210
  76. package/src/constants/products.ts +0 -155
  77. package/src/contexts/AvailabilitiesCacheContext.tsx +0 -125
  78. package/src/contexts/CompanyContext.tsx +0 -70
  79. package/src/data/dap-descriptions/session-couples-families-friends.en.json +0 -61
  80. package/src/data/dap-descriptions/session-elopements.en.json +0 -60
  81. package/src/data/dap-descriptions/session-proposals.en.json +0 -60
  82. package/src/data/product-descriptions/afternoon-delight.en.json +0 -35
  83. package/src/data/product-descriptions/emerald-lake-escape.en.json +0 -68
  84. package/src/data/product-descriptions/lake-louise-adventure.en.json +0 -74
  85. package/src/data/product-descriptions/moraine-lake-adventure.en.json +0 -78
  86. package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +0 -65
  87. package/src/data/product-descriptions/moraine-lake-sunrise.en.json +0 -64
  88. package/src/data/product-descriptions/private-tour.en.json +0 -80
  89. package/src/data/product-descriptions/two-lakes-combo.en.json +0 -65
  90. package/src/data/products-config.json +0 -101
  91. package/src/lib/analytics.ts +0 -197
  92. package/src/lib/booking/booking-source.ts +0 -51
  93. package/src/lib/booking/checkout-breakdown.ts +0 -69
  94. package/src/lib/booking/correlation-id.ts +0 -46
  95. package/src/lib/booking/i18n/config.ts +0 -21
  96. package/src/lib/booking/i18n/index.tsx +0 -144
  97. package/src/lib/booking/i18n/messages/en.json +0 -236
  98. package/src/lib/booking/i18n/messages/fr.json +0 -236
  99. package/src/lib/booking/itinerary-display.ts +0 -36
  100. package/src/lib/booking/itinerary-labels.ts +0 -70
  101. package/src/lib/booking/location-calculations.ts +0 -43
  102. package/src/lib/booking/location-utils.ts +0 -165
  103. package/src/lib/booking/map-utils.ts +0 -153
  104. package/src/lib/booking/marker-icons.ts +0 -113
  105. package/src/lib/booking/normalize-booking-product-id.ts +0 -21
  106. package/src/lib/booking/pickup-location-types.ts +0 -25
  107. package/src/lib/booking/places-api.ts +0 -154
  108. package/src/lib/booking/pricing.ts +0 -466
  109. package/src/lib/booking/product-option-id.ts +0 -35
  110. package/src/lib/booking/source-metadata.ts +0 -226
  111. package/src/lib/booking/sunday-week.ts +0 -14
  112. package/src/lib/booking/theme.ts +0 -83
  113. package/src/lib/booking/trace-context.ts +0 -62
  114. package/src/lib/booking/utils.ts +0 -9
  115. package/src/lib/booking-api.ts +0 -1793
  116. package/src/lib/booking-constants.ts +0 -23
  117. package/src/lib/booking-ref.ts +0 -13
  118. package/src/lib/booking-types.ts +0 -36
  119. package/src/lib/currency.ts +0 -81
  120. package/src/lib/dap-descriptions.ts +0 -50
  121. package/src/lib/dap-itinerary-preview.ts +0 -315
  122. package/src/lib/dependent-add-on-api.ts +0 -434
  123. package/src/lib/env.ts +0 -96
  124. package/src/lib/firebase.ts +0 -20
  125. package/src/lib/job-application-api.ts +0 -83
  126. package/src/lib/manage-booking-embed-print.ts +0 -16
  127. package/src/lib/manage-booking-post-checkout.ts +0 -68
  128. package/src/lib/photo-dap-config.ts +0 -228
  129. package/src/lib/photo-packages.ts +0 -75
  130. package/src/lib/pickup/map-utils.ts +0 -56
  131. package/src/lib/pickup/marker-icons.ts +0 -19
  132. package/src/lib/product-descriptions.ts +0 -66
  133. package/src/lib/products-config.ts +0 -73
  134. package/src/providers/dependent-add-on-dialog-provider.tsx +0 -105
  135. package/src/radius.css +0 -5
  136. package/src/spacing.css +0 -7
  137. package/src/strings/en.json +0 -1774
  138. package/src/strings/es.json +0 -1573
  139. package/src/strings/fr.json +0 -1573
  140. package/src/strings/index.js +0 -23
  141. package/src/text-style.css +0 -56
  142. package/src/utils/currency-converter.ts +0 -101
@@ -1,70 +0,0 @@
1
- import type { ItineraryDisplayStep } from '@/lib/booking-api';
2
-
3
- export type TranslationFn = (key: string, params?: Record<string, string>) => string;
4
-
5
- /** Step shape accepted for label building (stepType may be string when from JSON). */
6
- type StepForLabel = Pick<ItineraryDisplayStep, 'time' | 'place'> & { stepType: string };
7
-
8
- const EN_PLACE: Record<string, string> = {
9
- your_pickup_location: 'your pickup location',
10
- the_destination: 'the destination',
11
- };
12
-
13
- /**
14
- * English-only label (for provider dashboard, email, or when t() is not available).
15
- */
16
- export function getStepLabelEn(step: StepForLabel): string {
17
- const place = step.place;
18
- const placeDisplay = place ? (EN_PLACE[place] ?? place) : '';
19
-
20
- switch (step.stepType) {
21
- case 'pickup':
22
- return `Pickup at ${placeDisplay}`;
23
- case 'drop_off':
24
- return `Drop off at ${placeDisplay}`;
25
- case 'arrive':
26
- return `Arrive at ${place ?? ''}`;
27
- case 'depart':
28
- return `Depart ${place ?? ''}`;
29
- case 'trip_end':
30
- return 'Trip ends';
31
- case 'draft':
32
- return `Stop at ${place ?? ''} (time TBD)`;
33
- default:
34
- return place ? `${place}` : step.stepType;
35
- }
36
- }
37
-
38
- /**
39
- * Builds the display label for an itinerary step from stepType + place.
40
- * All labels are built on the FE so the user's language choice is respected.
41
- */
42
- export function getStepLabel(
43
- step: StepForLabel,
44
- t: TranslationFn
45
- ): string {
46
- const place = step.place;
47
- const placeDisplay =
48
- place === 'your_pickup_location'
49
- ? t('booking.yourPickupLocation')
50
- : place === 'the_destination'
51
- ? t('booking.placeTheDestination')
52
- : place ?? '';
53
-
54
- switch (step.stepType) {
55
- case 'pickup':
56
- return t('booking.pickupAtLocation', { location: placeDisplay });
57
- case 'drop_off':
58
- return t('booking.dropOffAt', { location: placeDisplay });
59
- case 'arrive':
60
- return t('booking.arriveAtPlace', { place: place ?? '' });
61
- case 'depart':
62
- return t('booking.departFromPlace', { place: place ?? '' });
63
- case 'trip_end':
64
- return t('booking.tripEnd');
65
- case 'draft':
66
- return t('booking.stopAtPlaceTbd', { place: place ?? '' });
67
- default:
68
- return place ? `${place}` : step.stepType;
69
- }
70
- }
@@ -1,43 +0,0 @@
1
- /**
2
- * Location calculation utilities
3
- */
4
-
5
- import type { PickupLocation } from '@/lib/booking-api';
6
- import type { Coordinates } from '@/lib/booking/location-utils';
7
- import type { NearbyLocation } from '@/lib/booking/pickup-location-types';
8
- import {
9
- calculateDistance,
10
- estimateWalkingTime,
11
- estimateDrivingTime,
12
- } from '@/lib/booking/location-utils';
13
-
14
- const EXACT_MATCH_THRESHOLD_KM = 0.05; // 50 meters - tighter threshold for exact matches
15
-
16
- /**
17
- * Calculate nearby locations with distances and travel times
18
- */
19
- export function calculateNearbyLocations(
20
- searchCoordinates: Coordinates,
21
- pickupLocations: PickupLocation[]
22
- ): NearbyLocation[] {
23
- return pickupLocations
24
- .filter((loc) => loc.coordinates)
25
- .map((loc) => {
26
- const distance = calculateDistance(searchCoordinates, loc.coordinates!);
27
- return {
28
- ...loc,
29
- distance,
30
- walkingTime: estimateWalkingTime(distance),
31
- drivingTime: estimateDrivingTime(distance),
32
- };
33
- })
34
- .sort((a, b) => a.distance - b.distance);
35
- }
36
-
37
- /**
38
- * Check if searched location is very close to a pickup location (exact match)
39
- */
40
- export function isExactMatch(nearbyLocations: NearbyLocation[]): boolean {
41
- return nearbyLocations.some((loc) => loc.distance < EXACT_MATCH_THRESHOLD_KM);
42
- }
43
-
@@ -1,165 +0,0 @@
1
- // Location utilities for distance calculation and travel time estimation
2
-
3
- export interface Coordinates {
4
- lat: number;
5
- lng: number;
6
- }
7
-
8
- /**
9
- * Calculate the distance between two coordinates using the Haversine formula
10
- * Returns distance in kilometers
11
- */
12
- export function calculateDistance(
13
- coord1: Coordinates,
14
- coord2: Coordinates
15
- ): number {
16
- const R = 6371; // Earth's radius in kilometers
17
- const dLat = toRadians(coord2.lat - coord1.lat);
18
- const dLon = toRadians(coord2.lng - coord1.lng);
19
-
20
- const a =
21
- Math.sin(dLat / 2) * Math.sin(dLat / 2) +
22
- Math.cos(toRadians(coord1.lat)) *
23
- Math.cos(toRadians(coord2.lat)) *
24
- Math.sin(dLon / 2) *
25
- Math.sin(dLon / 2);
26
-
27
- const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
28
- return R * c;
29
- }
30
-
31
- function toRadians(degrees: number): number {
32
- return degrees * (Math.PI / 180);
33
- }
34
-
35
- /**
36
- * Convert kilometers to miles
37
- */
38
- export function kmToMiles(km: number): number {
39
- return km * 0.621371;
40
- }
41
-
42
- /**
43
- * Convert miles to kilometers
44
- */
45
- export function milesToKm(miles: number): number {
46
- return miles * 1.60934;
47
- }
48
-
49
- /**
50
- * Estimate walking time in minutes
51
- * Assumes average walking speed of 5 km/h
52
- */
53
- export function estimateWalkingTime(distanceKm: number): number {
54
- const walkingSpeedKmh = 5;
55
- return Math.round((distanceKm / walkingSpeedKmh) * 60);
56
- }
57
-
58
- /**
59
- * Estimate driving time in minutes
60
- * Assumes average driving speed of 50 km/h (urban/suburban)
61
- */
62
- export function estimateDrivingTime(distanceKm: number): number {
63
- const drivingSpeedKmh = 50;
64
- return Math.round((distanceKm / drivingSpeedKmh) * 60);
65
- }
66
-
67
- /**
68
- * Format distance with appropriate unit
69
- */
70
- export function formatDistance(
71
- distanceKm: number,
72
- useImperial: boolean = false
73
- ): string {
74
- if (useImperial) {
75
- const miles = kmToMiles(distanceKm);
76
- if (miles < 0.1) {
77
- return `${Math.round(miles * 5280)} ft`;
78
- } else if (miles < 1) {
79
- return `${miles.toFixed(1)} mi`;
80
- } else {
81
- return `${miles.toFixed(1)} mi`;
82
- }
83
- } else {
84
- if (distanceKm < 0.1) {
85
- return `${Math.round(distanceKm * 1000)} m`;
86
- } else if (distanceKm < 1) {
87
- return `${Math.round(distanceKm * 1000)} m`;
88
- } else {
89
- return `${distanceKm.toFixed(1)} km`;
90
- }
91
- }
92
- }
93
-
94
- /**
95
- * Format time duration
96
- */
97
- export function formatTime(minutes: number): string {
98
- if (minutes < 60) {
99
- return `${minutes} min`;
100
- } else {
101
- const hours = Math.floor(minutes / 60);
102
- const mins = minutes % 60;
103
- return mins > 0 ? `${hours} h ${mins} min` : `${hours} h`;
104
- }
105
- }
106
-
107
- /**
108
- * Service area for Private Shuttle custom pickups: Lake Louise to Kananaskis casino corridor.
109
- * Bounding box encompasses: Lake Louise, Banff, Canmore, Kananaskis.
110
- * Excludes Calgary and areas east.
111
- */
112
- export const PRIVATE_SHUTTLE_SERVICE_BOUNDS = {
113
- latMin: 51.0,
114
- latMax: 51.5,
115
- lngMin: -116.5,
116
- lngMax: -114.8,
117
- } as const;
118
-
119
- /**
120
- * Check if coordinates are within the Private Shuttle service area
121
- * (corridor from Lake Louise to Kananaskis casino).
122
- */
123
- export function isWithinPrivateShuttleServiceArea(coords: Coordinates): boolean {
124
- const { latMin, latMax, lngMin, lngMax } = PRIVATE_SHUTTLE_SERVICE_BOUNDS;
125
- return (
126
- coords.lat >= latMin &&
127
- coords.lat <= latMax &&
128
- coords.lng >= lngMin &&
129
- coords.lng <= lngMax
130
- );
131
- }
132
-
133
- /**
134
- * Geocode an address using Google Maps Geocoding API
135
- * Note: This requires a Google Maps API key
136
- */
137
- export async function geocodeAddress(
138
- address: string,
139
- apiKey?: string
140
- ): Promise<Coordinates | null> {
141
- if (!apiKey) {
142
- // Fallback: try to use browser geolocation or return null
143
- console.warn('Google Maps API key not provided');
144
- return null;
145
- }
146
-
147
- try {
148
- const response = await fetch(
149
- `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(
150
- address
151
- )}&key=${apiKey}`
152
- );
153
- const data = await response.json();
154
-
155
- if (data.status === 'OK' && data.results.length > 0) {
156
- const location = data.results[0].geometry.location;
157
- return { lat: location.lat, lng: location.lng };
158
- }
159
- return null;
160
- } catch (error) {
161
- console.error('Geocoding error:', error);
162
- return null;
163
- }
164
- }
165
-
@@ -1,153 +0,0 @@
1
- /**
2
- * Google Maps utilities
3
- */
4
-
5
- import type { Coordinates } from '@/lib/booking/location-utils';
6
- import type { PickupLocation } from '@/lib/booking-api';
7
- import type { NearbyLocation } from '@/lib/booking/pickup-location-types';
8
-
9
- export interface MapCenter {
10
- lat: number;
11
- lng: number;
12
- }
13
-
14
- const DEFAULT_CENTER: MapCenter = { lat: 51.1784, lng: -115.5708 };
15
-
16
- /**
17
- * Calculate map center based on available locations
18
- */
19
- export function calculateMapCenter(
20
- searchedLocation: { coordinates: Coordinates | null } | null,
21
- nearbyLocations: NearbyLocation[],
22
- pickupLocations: PickupLocation[]
23
- ): MapCenter {
24
- // Prioritize searched location - center on it when found
25
- if (searchedLocation?.coordinates) {
26
- return searchedLocation.coordinates;
27
- }
28
-
29
- if (nearbyLocations.length > 0 && nearbyLocations[0].coordinates) {
30
- return {
31
- lat: nearbyLocations[0].coordinates.lat,
32
- lng: nearbyLocations[0].coordinates.lng,
33
- };
34
- }
35
-
36
- // Default to first pickup location if available
37
- if (pickupLocations.length > 0 && pickupLocations[0].coordinates) {
38
- return {
39
- lat: pickupLocations[0].coordinates.lat,
40
- lng: pickupLocations[0].coordinates.lng,
41
- };
42
- }
43
-
44
- return DEFAULT_CENTER;
45
- }
46
-
47
- /**
48
- * Get map options configuration
49
- * Note: Returns a plain object that will be typed by Google Maps API
50
- */
51
- export function getMapOptions(): google.maps.MapOptions {
52
- return {
53
- disableDefaultUI: false,
54
- clickableIcons: false,
55
- scrollwheel: true,
56
- zoomControl: true,
57
- streetViewControl: false,
58
- mapTypeControl: false,
59
- fullscreenControl: true,
60
- gestureHandling: 'cooperative',
61
- panControl: false,
62
- } as google.maps.MapOptions;
63
- }
64
-
65
- /**
66
- * Calculate bounds for all locations to fit on map
67
- * Note: Requires Google Maps API to be loaded
68
- */
69
- export function calculateMapBounds(
70
- nearbyLocations: NearbyLocation[],
71
- searchedLocation: { coordinates: Coordinates | null } | null,
72
- isValidLocation: boolean | null,
73
- pickupLocations: PickupLocation[]
74
- ): google.maps.LatLngBounds | null {
75
- if (typeof google === 'undefined' || !google.maps) {
76
- console.warn('Google Maps API not loaded');
77
- return null;
78
- }
79
-
80
- try {
81
- const bounds = new google.maps.LatLngBounds();
82
- let hasLocations = false;
83
-
84
- // Add nearby locations to bounds (if user searched)
85
- nearbyLocations.forEach((location) => {
86
- if (location.coordinates) {
87
- bounds.extend(location.coordinates);
88
- hasLocations = true;
89
- }
90
- });
91
-
92
- // Add searched location to bounds if invalid
93
- if (searchedLocation?.coordinates && !isValidLocation) {
94
- bounds.extend(searchedLocation.coordinates);
95
- hasLocations = true;
96
- }
97
-
98
- // If no search results, show all pickup locations
99
- if (!hasLocations) {
100
- pickupLocations.forEach((location) => {
101
- if (location.coordinates) {
102
- bounds.extend(location.coordinates);
103
- hasLocations = true;
104
- }
105
- });
106
- }
107
-
108
- return hasLocations ? bounds : null;
109
- } catch (error) {
110
- console.error('Error calculating map bounds:', error);
111
- return null;
112
- }
113
- }
114
-
115
- /**
116
- * Pan map to location if it's outside viewport
117
- * Note: Requires Google Maps API to be loaded
118
- */
119
- export function panToLocationIfNeeded(
120
- map: google.maps.Map,
121
- coordinates: Coordinates
122
- ): void {
123
- if (typeof google === 'undefined' || !google.maps) {
124
- console.warn('Google Maps API not loaded');
125
- return;
126
- }
127
-
128
- try {
129
- const latLng = new google.maps.LatLng(coordinates.lat, coordinates.lng);
130
- const bounds = map.getBounds();
131
-
132
- if (bounds && !bounds.contains(latLng)) {
133
- const currentCenter = map.getCenter();
134
- if (currentCenter) {
135
- const panBounds = new google.maps.LatLngBounds();
136
- panBounds.extend(currentCenter);
137
- panBounds.extend(latLng);
138
- // Add padding to make the pan more visible
139
- map.fitBounds(panBounds, {
140
- top: 50,
141
- right: 50,
142
- bottom: 50,
143
- left: 50,
144
- });
145
- } else {
146
- map.panTo(latLng);
147
- }
148
- }
149
- } catch (error) {
150
- console.error('Error panning map:', error);
151
- }
152
- }
153
-
@@ -1,113 +0,0 @@
1
- /**
2
- * Marker icon generation utilities
3
- */
4
-
5
- export interface MarkerIconOptions {
6
- bgColor: string;
7
- distanceStr?: string;
8
- walkingTimeStr?: string;
9
- label?: string;
10
- }
11
-
12
- /**
13
- * Escape text for safe use in SVG
14
- */
15
- function escapeSvgText(text: string): string {
16
- return text
17
- .replace(/&/g, '&amp;')
18
- .replace(/</g, '&lt;')
19
- .replace(/>/g, '&gt;')
20
- .replace(/"/g, '&quot;')
21
- .replace(/'/g, '&#39;');
22
- }
23
-
24
- /**
25
- * Validate and sanitize color value (hex color)
26
- */
27
- function validateColor(color: string): string {
28
- // Only allow hex colors (# followed by 3 or 6 hex digits)
29
- const hexColorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
30
- if (hexColorRegex.test(color)) {
31
- return color;
32
- }
33
- // Default to a safe color if invalid
34
- console.warn(`Invalid color provided: ${color}, using default`);
35
- return '#dc2626';
36
- }
37
-
38
- /**
39
- * Truncate text to fit within marker width
40
- */
41
- function truncateText(text: string, maxLength: number = 20): string {
42
- if (text.length <= maxLength) return text;
43
- return text.slice(0, maxLength - 3) + '...';
44
- }
45
-
46
- /**
47
- * Generate SVG icon for pickup location marker with distance/time
48
- */
49
- export function createDistanceMarkerIcon(options: MarkerIconOptions): string {
50
- const { bgColor, distanceStr, walkingTimeStr } = options;
51
- const safeColor = validateColor(bgColor);
52
- const text = distanceStr && walkingTimeStr
53
- ? truncateText(`${distanceStr} • ${walkingTimeStr}`, 20)
54
- : '';
55
- const escapedText = escapeSvgText(text);
56
-
57
- return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(`
58
- <svg width="120" height="32" viewBox="0 0 120 32" xmlns="http://www.w3.org/2000/svg">
59
- <rect x="0" y="0" width="120" height="24" rx="4" fill="${safeColor}"/>
60
- <polygon points="55,24 60,32 65,24" fill="${safeColor}"/>
61
- <text x="60" y="16" fill="white" font-size="10" font-weight="600" text-anchor="middle" font-family="Arial">${escapedText}</text>
62
- </svg>
63
- `)}`;
64
- }
65
-
66
- /**
67
- * Generate SVG icon for simple pickup location pin
68
- */
69
- export function createPinMarkerIcon(color: string): string {
70
- const safeColor = validateColor(color);
71
- return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(`
72
- <svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
73
- <path d="M16 0C7.163 0 0 7.163 0 16C0 24.837 16 40 16 40C16 40 32 24.837 32 16C32 7.163 24.837 0 16 0Z" fill="#ffffff"/>
74
- <path d="M16 2C8.268 2 2 8.268 2 16C2 22.177 16 38 16 38C16 38 30 22.177 30 16C30 8.268 23.732 2 16 2Z" fill="${safeColor}"/>
75
- <circle cx="16" cy="16" r="6" fill="#ffffff"/>
76
- </svg>
77
- `)}`;
78
- }
79
-
80
- /**
81
- * Generate SVG icon for user's searched location pin
82
- */
83
- export function createSearchedLocationPinIcon(): string {
84
- return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(`
85
- <svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
86
- <path d="M16 0C7.163 0 0 7.163 0 16C0 24.837 16 40 16 40C16 40 32 24.837 32 16C32 7.163 24.837 0 16 0Z" fill="#ffffff"/>
87
- <path d="M16 2C8.268 2 2 8.268 2 16C2 22.177 16 38 16 38C16 38 30 22.177 30 16C30 8.268 23.732 2 16 2Z" fill="#3b82f6"/>
88
- <circle cx="16" cy="16" r="6" fill="#ffffff"/>
89
- </svg>
90
- `)}`;
91
- }
92
-
93
- /**
94
- * Generate SVG icon for destination marker pin with text below (yellow)
95
- */
96
- export function createDestinationMarkerIcon(name: string, color: string = '#facc15'): string {
97
- const safeColor = validateColor(color);
98
- const escapedName = escapeSvgText(truncateText(name, 20));
99
- // SVG includes pin (40px height) + text below (20px height) = 60px total
100
- return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(`
101
- <svg width="80" height="60" viewBox="0 0 80 60" fill="none" xmlns="http://www.w3.org/2000/svg">
102
- <!-- Pin marker (centered horizontally, at top) -->
103
- <g transform="translate(24, 0)">
104
- <path d="M16 0C7.163 0 0 7.163 0 16C0 24.837 16 40 16 40C16 40 32 24.837 32 16C32 7.163 24.837 0 16 0Z" fill="#ffffff"/>
105
- <path d="M16 2C8.268 2 2 8.268 2 16C2 22.177 16 38 16 38C16 38 30 22.177 30 16C30 8.268 23.732 2 16 2Z" fill="${safeColor}"/>
106
- <circle cx="16" cy="16" r="6" fill="#ffffff"/>
107
- </g>
108
- <!-- Text below pin -->
109
- <text x="40" y="52" fill="#1f2937" font-size="12" font-weight="600" text-anchor="middle" font-family="Arial, sans-serif">${escapedName}</text>
110
- </svg>
111
- `)}`;
112
- }
113
-
@@ -1,21 +0,0 @@
1
- /**
2
- * Normalize IDs used in TicketBooth query params (product id, product option id).
3
- * Strips hash fragments, accidental query strings, or concatenated `&…` tails — aligned with
4
- * defensive parsing in GetAvailabilitiesHandler.
5
- */
6
- export function normalizeBookingProductId(rawId: string): string {
7
- const trimmed = rawId.trim();
8
- if (!trimmed) return '';
9
- const withoutHash = trimmed.split('#', 1)[0] ?? '';
10
- const withoutQuery = withoutHash.split('?', 1)[0] ?? '';
11
- const normalized = withoutQuery.split('&', 1)[0] ?? '';
12
- return normalized.trim();
13
- }
14
-
15
- /** True when the raw value clearly contains URL/query junk and should be normalized before use. */
16
- export function isSuspiciousBookingProductId(value: string): boolean {
17
- return /[?&=]/.test(value);
18
- }
19
-
20
- /** Alias: same normalization for get-availabilities lookup id (product or option). */
21
- export const normalizeAvailabilityLookupId = normalizeBookingProductId;
@@ -1,25 +0,0 @@
1
- /**
2
- * Types for pickup location selector
3
- */
4
-
5
- import type { PickupLocation } from '@/lib/booking-api';
6
- import type { Coordinates } from '@/lib/booking/location-utils';
7
-
8
- export interface NearbyLocation extends PickupLocation {
9
- distance: number;
10
- walkingTime: number;
11
- drivingTime: number;
12
- }
13
-
14
- export interface SearchedLocation {
15
- address: string;
16
- coordinates: Coordinates | null;
17
- }
18
-
19
- export interface AutocompleteSuggestion {
20
- placeId: string;
21
- mainText: string;
22
- secondaryText: string;
23
- description: string;
24
- }
25
-