@ticketboothapp/booking 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/package.json +2 -1
  2. package/src/app/photo-sessions/photo-packages.ts +75 -0
  3. package/src/assets/icons/partner-logos/getyourguide.svg +8 -0
  4. package/src/assets/icons/plus.svg +3 -0
  5. package/src/colours.css +23 -0
  6. package/src/components/BookingDetails.module.css +1591 -0
  7. package/src/components/BookingDetails.tsx +2072 -354
  8. package/src/components/BookingWidget.tsx +28 -248
  9. package/src/components/JobApplicationDialog.module.css +440 -0
  10. package/src/components/JobApplicationDialog.tsx +620 -0
  11. package/src/components/ManageBookingView.tsx +28 -36
  12. package/src/components/PhoneInputWithCountry.module.css +131 -0
  13. package/src/components/PhoneInputWithCountry.tsx +44 -0
  14. package/src/components/PickupLocationDialog.module.css +360 -0
  15. package/src/components/PickupLocationDialog.tsx +357 -0
  16. package/src/components/PickupLocationMap.tsx +110 -0
  17. package/src/components/PostBookingDependentAddOnUpsell.module.css +174 -0
  18. package/src/components/PostBookingDependentAddOnUpsell.tsx +407 -0
  19. package/src/components/accordion.css +27 -0
  20. package/src/components/accordion.tsx +29 -0
  21. package/src/components/analytics/AnalyticsConsentRestore.tsx +19 -0
  22. package/src/components/analytics/AnalyticsScripts.tsx +106 -0
  23. package/src/components/analytics/CookieConsentBanner.css +86 -0
  24. package/src/components/analytics/CookieConsentBanner.tsx +102 -0
  25. package/src/components/booking/AddOnsSection.module.css +10 -0
  26. package/src/components/booking/AddOnsSection.tsx +184 -0
  27. package/src/components/booking/AdminPaymentChoiceModal.tsx +98 -0
  28. package/src/components/booking/BookingDialog.module.css +643 -0
  29. package/src/components/booking/BookingDialog.tsx +356 -0
  30. package/src/components/booking/BookingFlow.tsx +4385 -0
  31. package/src/components/booking/BookingFlowCollage.module.css +148 -0
  32. package/src/components/booking/BookingFlowCollage.tsx +184 -0
  33. package/src/components/booking/BookingFlowPlaceholder.module.css +27 -0
  34. package/src/components/booking/BookingFlowPlaceholder.tsx +25 -0
  35. package/src/components/booking/BookingFlowPreview.tsx +51 -0
  36. package/src/components/booking/BookingProductGrid.module.css +359 -0
  37. package/src/components/booking/BookingProductGrid.tsx +497 -0
  38. package/src/components/booking/Calendar.module.css +616 -0
  39. package/src/components/{Calendar.tsx → booking/Calendar.tsx} +464 -247
  40. package/src/components/booking/CancellationPolicySelector.module.css +124 -0
  41. package/src/components/booking/CancellationPolicySelector.tsx +142 -0
  42. package/src/components/booking/ChangeBookingDialog.tsx +562 -0
  43. package/src/components/booking/CheckoutForm.module.css +244 -0
  44. package/src/components/booking/CheckoutForm.tsx +364 -0
  45. package/src/components/{CheckoutModal.tsx → booking/CheckoutModal.tsx} +176 -19
  46. package/src/components/booking/DapFlowCollage.tsx +88 -0
  47. package/src/components/booking/DapTourDescription.tsx +35 -0
  48. package/src/components/booking/DependentAddOnBookingDialog.tsx +1350 -0
  49. package/src/components/booking/DependentAddOnPaymentForm.tsx +124 -0
  50. package/src/components/booking/InfoTooltip.tsx +108 -0
  51. package/src/components/booking/ItineraryBox.module.css +258 -0
  52. package/src/components/booking/ItineraryBox.tsx +550 -0
  53. package/src/components/{ItineraryBuilder.tsx → booking/ItineraryBuilder.tsx} +1 -2
  54. package/src/components/booking/ItineraryPlaceholder.module.css +45 -0
  55. package/src/components/booking/ItineraryPlaceholder.tsx +26 -0
  56. package/src/components/{MealDrinkAddOnSelector.tsx → booking/MealDrinkAddOnSelector.tsx} +21 -13
  57. package/src/components/booking/PickupLocationSelector.module.css +124 -0
  58. package/src/components/{PickupLocationSelector.tsx → booking/PickupLocationSelector.tsx} +315 -290
  59. package/src/components/booking/PickupTimeSelector.module.css +134 -0
  60. package/src/components/booking/PickupTimeSelector.tsx +112 -0
  61. package/src/components/{PriceBreakdown.tsx → booking/PriceBreakdown.tsx} +3 -3
  62. package/src/components/{PriceSummary.tsx → booking/PriceSummary.tsx} +51 -28
  63. package/src/components/booking/PrivateShuttleBookingFlow.module.css +357 -0
  64. package/src/components/booking/PrivateShuttleBookingFlow.tsx +2662 -0
  65. package/src/components/booking/PromoCodeInput.module.css +166 -0
  66. package/src/components/booking/PromoCodeInput.tsx +99 -0
  67. package/src/components/booking/ReturnTimeSelector.module.css +173 -0
  68. package/src/components/booking/ReturnTimeSelector.tsx +145 -0
  69. package/src/components/{TermsAcceptance.tsx → booking/TermsAcceptance.tsx} +9 -8
  70. package/src/components/booking/TicketSelector.module.css +164 -0
  71. package/src/components/booking/TicketSelector.tsx +199 -0
  72. package/src/components/booking/TourDescription.module.css +304 -0
  73. package/src/components/booking/TourDescription.tsx +273 -0
  74. package/src/components/booking/booking-flow-ui.ts +15 -1
  75. package/src/components/booking/booking-flow.css +944 -0
  76. package/src/components/bottom-sheet.module.css +78 -0
  77. package/src/components/bottom-sheet.tsx +60 -0
  78. package/src/components/breadcrumb.module.css +40 -0
  79. package/src/components/breadcrumb.tsx +36 -0
  80. package/src/components/button.css +245 -0
  81. package/src/components/button.tsx +152 -0
  82. package/src/components/client-bottom-sheet.tsx +14 -0
  83. package/src/components/colorable-svg.tsx +29 -0
  84. package/src/components/conditional-footer.tsx +27 -0
  85. package/src/components/contact-us.module.css +147 -0
  86. package/src/components/contact-us.tsx +49 -0
  87. package/src/components/email-signup.css +151 -0
  88. package/src/components/email-signup.tsx +63 -0
  89. package/src/components/faq-wrapper.module.css +47 -0
  90. package/src/components/faq-wrapper.tsx +15 -0
  91. package/src/components/footer.css +187 -0
  92. package/src/components/footer.tsx +143 -0
  93. package/src/components/global-simple-modal.tsx +33 -0
  94. package/src/components/google-review-summary.module.css +77 -0
  95. package/src/components/google-review-summary.tsx +50 -0
  96. package/src/components/hero-image.css +13 -0
  97. package/src/components/hero-image.tsx +44 -0
  98. package/src/components/image.css +29 -0
  99. package/src/components/image.tsx +113 -0
  100. package/src/components/language-aware-link.tsx +72 -0
  101. package/src/components/language-switcher.module.css +124 -0
  102. package/src/components/language-switcher.tsx +75 -0
  103. package/src/components/map-section.css +59 -0
  104. package/src/components/map-section.tsx +63 -0
  105. package/src/components/navbar.module.css +152 -0
  106. package/src/components/navbar.tsx +125 -0
  107. package/src/components/parallax-provider.tsx +11 -0
  108. package/src/components/partner/PartnerBookingPage.module.css +130 -0
  109. package/src/components/partner/PartnerBookingPage.tsx +390 -0
  110. package/src/components/partner/PartnerBookingPageWithBrowserMetadata.tsx +19 -35
  111. package/src/components/product-tag.module.css +30 -0
  112. package/src/components/product-tag.tsx +34 -0
  113. package/src/components/product-theme-pages/best-option.module.css +70 -0
  114. package/src/components/product-theme-pages/best-option.tsx +35 -0
  115. package/src/components/product-theme-pages/extended-tour-options.module.css +22 -0
  116. package/src/components/product-theme-pages/extended-tour-options.tsx +11 -0
  117. package/src/components/product-theme-pages/image-modal.tsx +248 -0
  118. package/src/components/product-theme-pages/photo-gallery.module.css +200 -0
  119. package/src/components/product-theme-pages/photo-gallery.tsx +90 -0
  120. package/src/components/product-theme-pages/product-theme-page-layout.module.css +13 -0
  121. package/src/components/product-theme-pages/product-theme-page-layout.tsx +67 -0
  122. package/src/components/product-theme-pages/top-of-fold.module.css +179 -0
  123. package/src/components/product-theme-pages/top-of-fold.tsx +80 -0
  124. package/src/components/product-tile/image-only-product-tile-desktop.module.css +106 -0
  125. package/src/components/product-tile/image-only-product-tile-desktop.tsx +56 -0
  126. package/src/components/product-tile/image-only-product-tile-mobile.module.css +122 -0
  127. package/src/components/product-tile/image-only-product-tile-mobile.tsx +89 -0
  128. package/src/components/product-tile/image-only-product-tile.tsx +44 -0
  129. package/src/components/product-tile/product-tile-card.module.css +84 -0
  130. package/src/components/product-tile/product-tile-card.tsx +61 -0
  131. package/src/components/review-highlights-section.css +85 -0
  132. package/src/components/review-highlights-section.tsx +127 -0
  133. package/src/components/season-closure-overlay.module.css +99 -0
  134. package/src/components/season-closure-overlay.tsx +98 -0
  135. package/src/components/simple-modal.tsx +69 -0
  136. package/src/components/simple-top-of-fold.module.css +76 -0
  137. package/src/components/simple-top-of-fold.tsx +34 -0
  138. package/src/components/spacer.css +41 -0
  139. package/src/components/spacer.tsx +23 -0
  140. package/src/components/star-rating.module.css +74 -0
  141. package/src/components/star-rating.tsx +48 -0
  142. package/src/components/terms/TermsContent.tsx +178 -0
  143. package/src/components/title-subtitle.module.css +10 -0
  144. package/src/components/title-subtitle.tsx +30 -0
  145. package/src/components/translatable-reviews.tsx +75 -0
  146. package/src/components/value-pill.module.css +59 -0
  147. package/src/components/value-pill.tsx +46 -0
  148. package/src/components/value-props.css +185 -0
  149. package/src/components/value-props.tsx +88 -0
  150. package/src/constants/booking-guide-quiz.ts +64 -0
  151. package/src/constants/contact-info.ts +2 -0
  152. package/src/constants/faq.ts +44 -0
  153. package/src/constants/images.ts +556 -0
  154. package/src/constants/json-ld/faq-json-ld.tsx +170 -0
  155. package/src/constants/json-ld/homepage-json-ld.tsx +138 -0
  156. package/src/constants/json-ld/job-posting-json-ld.tsx +92 -0
  157. package/src/constants/json-ld/organization-json-ld.tsx +62 -0
  158. package/src/constants/json-ld/page-json-ld.tsx +6 -0
  159. package/src/constants/json-ld/product-json-ld.tsx +154 -0
  160. package/src/constants/json-ld/review-json-ld.tsx +377 -0
  161. package/src/constants/navigation-links/footer-links.ts +48 -0
  162. package/src/constants/navigation-links/nav-bar-links.ts +41 -0
  163. package/src/constants/navigation-links/navigation-link.ts +6 -0
  164. package/src/constants/pill-values.ts +210 -0
  165. package/src/constants/products.ts +155 -0
  166. package/src/constants/quiz-recommendations.ts +506 -0
  167. package/src/constants/reviews.ts +75 -0
  168. package/src/constants/staff.ts +197 -0
  169. package/src/constants/value-props.ts +58 -0
  170. package/src/data/dap-descriptions/session-couples-families-friends.en.json +61 -0
  171. package/src/data/dap-descriptions/session-elopements.en.json +60 -0
  172. package/src/data/dap-descriptions/session-proposals.en.json +60 -0
  173. package/src/data/product-descriptions/afternoon-delight.en.json +35 -0
  174. package/src/data/product-descriptions/emerald-lake-escape.en.json +68 -0
  175. package/src/data/product-descriptions/lake-louise-adventure.en.json +74 -0
  176. package/src/data/product-descriptions/moraine-lake-adventure.en.json +78 -0
  177. package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +65 -0
  178. package/src/data/product-descriptions/moraine-lake-sunrise.en.json +64 -0
  179. package/src/data/product-descriptions/private-tour.en.json +80 -0
  180. package/src/data/product-descriptions/two-lakes-combo.en.json +65 -0
  181. package/src/data/products-config.json +101 -0
  182. package/src/hooks/use-bottom-sheet.tsx +15 -0
  183. package/src/hooks/use-simple-modal.tsx +27 -0
  184. package/src/hooks/useBookingSourceMetadataFromLocation.ts +21 -0
  185. package/src/hooks/useEmailSubscription.tsx +103 -0
  186. package/src/hooks/useEmbeddedInIframe.ts +16 -0
  187. package/src/hooks/useIsBookingLaunchLive.ts +49 -0
  188. package/src/hooks/useQuiz.tsx +210 -0
  189. package/src/index.ts +27 -2
  190. package/src/lib/analytics.ts +197 -0
  191. package/src/lib/booking/booking-source.ts +20 -2
  192. package/src/lib/{checkout-breakdown.ts → booking/checkout-breakdown.ts} +1 -1
  193. package/src/lib/booking/correlation-id.ts +46 -0
  194. package/src/lib/{i18n → booking/i18n}/messages/en.json +48 -4
  195. package/src/lib/{i18n → booking/i18n}/messages/fr.json +48 -4
  196. package/src/lib/booking/itinerary-display.ts +36 -0
  197. package/src/lib/{itinerary-labels.ts → booking/itinerary-labels.ts} +1 -1
  198. package/src/lib/{location-calculations.ts → booking/location-calculations.ts} +4 -4
  199. package/src/lib/{location-utils.ts → booking/location-utils.ts} +26 -0
  200. package/src/lib/{map-utils.ts → booking/map-utils.ts} +3 -3
  201. package/src/lib/booking/normalize-booking-product-id.ts +7 -0
  202. package/src/lib/{pickup-location-types.ts → booking/pickup-location-types.ts} +2 -2
  203. package/src/lib/{pricing.ts → booking/pricing.ts} +2 -2
  204. package/src/lib/booking/product-option-id.ts +35 -0
  205. package/src/lib/booking/source-metadata.ts +72 -7
  206. package/src/lib/booking/sunday-week.ts +14 -0
  207. package/src/lib/booking/trace-context.ts +62 -0
  208. package/src/lib/booking-api.ts +1793 -0
  209. package/src/lib/{constants.ts → booking-constants.ts} +11 -5
  210. package/src/lib/booking-types.ts +36 -0
  211. package/src/lib/currency.ts +38 -45
  212. package/src/lib/dap-descriptions.ts +50 -0
  213. package/src/lib/dap-itinerary-preview.ts +315 -0
  214. package/src/lib/dependent-add-on-api.ts +434 -0
  215. package/src/lib/env.ts +89 -5
  216. package/src/lib/firebase.ts +20 -0
  217. package/src/lib/job-application-api.ts +83 -0
  218. package/src/lib/manage-booking-embed-print.ts +16 -0
  219. package/src/lib/manage-booking-post-checkout.ts +68 -0
  220. package/src/lib/photo-dap-config.ts +228 -0
  221. package/src/lib/pickup/map-utils.ts +56 -0
  222. package/src/lib/pickup/marker-icons.ts +19 -0
  223. package/src/lib/product-descriptions.ts +66 -0
  224. package/src/lib/products-config.ts +73 -0
  225. package/src/providers/booking-dialog-provider.tsx +107 -38
  226. package/src/providers/bottom-sheet-provider.tsx +40 -0
  227. package/src/providers/dependent-add-on-dialog-provider.tsx +105 -0
  228. package/src/radius.css +5 -0
  229. package/src/spacing.css +7 -0
  230. package/src/strings/en.json +1774 -0
  231. package/src/strings/es.json +1573 -0
  232. package/src/strings/fr.json +1573 -0
  233. package/src/strings/index.js +23 -0
  234. package/src/text-style.css +97 -0
  235. package/src/types/fareharbor.d.ts +12 -0
  236. package/src/types/quiz.ts +59 -0
  237. package/src/utils/currency-converter.ts +101 -0
  238. package/src/components/BookingFlow.tsx +0 -2952
  239. package/src/components/LanguageSwitcher.tsx +0 -30
  240. package/src/components/PrivateShuttleBookingFlow.tsx +0 -2290
  241. package/src/components/ProductList.tsx +0 -78
  242. package/src/components/WhatsAppPhoneInput.tsx +0 -224
  243. package/src/components/index.ts +0 -31
  244. package/src/lib/api.ts +0 -801
  245. package/src/lib/booking-api-auth.ts +0 -9
  246. package/src/lib/checkout-breakdown.test.ts +0 -70
  247. package/src/types/google-maps.d.ts +0 -2
  248. /package/src/components/{CurrencySwitcher.tsx → booking/CurrencySwitcher.tsx} +0 -0
  249. /package/src/components/{ErrorBoundary.tsx → booking/ErrorBoundary.tsx} +0 -0
  250. /package/src/lib/{i18n → booking/i18n}/config.ts +0 -0
  251. /package/src/lib/{i18n → booking/i18n}/index.tsx +0 -0
  252. /package/src/lib/{marker-icons.ts → booking/marker-icons.ts} +0 -0
  253. /package/src/lib/{places-api.ts → booking/places-api.ts} +0 -0
  254. /package/src/lib/{theme.ts → booking/theme.ts} +0 -0
  255. /package/src/lib/{utils.ts → booking/utils.ts} +0 -0
@@ -2,37 +2,39 @@
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 '@/lib/api';
5
+ import type { PickupLocation, Destination } from '@/lib/booking-api';
6
6
  import {
7
7
  formatDistance,
8
8
  formatTime,
9
9
  geocodeAddress,
10
- } from '@/lib/location-utils';
10
+ isWithinPrivateShuttleServiceArea,
11
+ } from '@/lib/booking/location-utils';
11
12
  import {
12
13
  calculateNearbyLocations,
13
14
  isExactMatch,
14
- } from '@/lib/location-calculations';
15
+ } from '@/lib/booking/location-calculations';
15
16
  import {
16
17
  getAutocompleteSuggestions,
17
18
  getPlaceDetails,
18
19
  type AutocompleteSuggestion,
19
- } from '@/lib/places-api';
20
+ } from '@/lib/booking/places-api';
20
21
  import {
21
22
  createDistanceMarkerIcon,
22
23
  createPinMarkerIcon,
23
24
  createSearchedLocationPinIcon,
24
25
  createDestinationMarkerIcon,
25
- } from '@/lib/marker-icons';
26
+ } from '@/lib/booking/marker-icons';
26
27
  import { ENV } from '@/lib/env';
27
28
  import {
28
29
  calculateMapCenter,
29
30
  calculateMapBounds,
30
31
  getMapOptions,
31
32
  panToLocationIfNeeded,
32
- } from '@/lib/map-utils';
33
- import type { SearchedLocation, NearbyLocation } from '@/lib/pickup-location-types';
34
- import { useTranslations } from '@/lib/i18n';
33
+ } from '@/lib/booking/map-utils';
34
+ import type { SearchedLocation, NearbyLocation } from '@/lib/booking/pickup-location-types';
35
+ import { useTranslations } from '@/lib/booking/i18n';
35
36
  import { useBookingApp } from '@/contexts/BookingAppContext';
37
+ import styles from './PickupLocationSelector.module.css';
36
38
 
37
39
  export interface CustomPickupLocation {
38
40
  address: string;
@@ -45,10 +47,18 @@ interface PickupLocationSelectorProps {
45
47
  selectedCustomAddress?: string | null;
46
48
  /** When true (default), show "Use this address" for custom locations. Only Private Shuttle uses this. */
47
49
  allowCustomLocation?: boolean;
48
- onLocationSelect: (locationId: string | null, customLocation?: CustomPickupLocation) => void;
50
+ /** When true with allowCustomLocation, restrict custom addresses to Lake Louise–Kananaskis corridor. */
51
+ restrictCustomLocationToServiceArea?: boolean;
52
+ onLocationSelect: (locationId: string | null, customLocation?: CustomPickupLocation, locationName?: string) => void;
49
53
  onSkip?: () => void;
50
54
  isSkipped?: boolean;
51
55
  destinations?: Destination[]; // Destination locations to show on the map
56
+ /** When true, hide the title (e.g. when embedded in a dialog that has its own title) */
57
+ hideTitle?: boolean;
58
+ /** When true, hide the "I don't know" filter pill and option (e.g. when changing pickup on existing booking) */
59
+ hideSkipOption?: boolean;
60
+ /** Location IDs to prioritize at the top and visually highlight (e.g. partner-preferred pickups). */
61
+ highlightedPickupLocationIds?: string[];
52
62
  }
53
63
 
54
64
  // Constants
@@ -78,6 +88,10 @@ const FILTERS = [
78
88
 
79
89
  type FilterId = typeof FILTERS[number]['id'];
80
90
 
91
+ /** Stable fallback — default `= []` in destructuring would allocate a new array every render. */
92
+ const NO_HIGHLIGHTED_PICKUP_LOCATION_IDS: string[] = [];
93
+ const NO_DESTINATIONS: Destination[] = [];
94
+
81
95
  // Helper function to determine which city a location belongs to
82
96
  function getLocationCity(location: PickupLocation): FilterId | null {
83
97
  const addressLower = location.address.toLowerCase();
@@ -157,7 +171,7 @@ function FilterPills({ pickupLocations, selectedFilters, onFilterToggle }: Filte
157
171
  }, [pickupLocations, selectedFilters]);
158
172
 
159
173
  return (
160
- <div className="mb-4 flex flex-wrap gap-2">
174
+ <div className="flex flex-wrap items-center gap-2">
161
175
  {FILTERS.map(filter => {
162
176
  const isSelected = selectedFilters.has(filter.id);
163
177
  const isCityFilter = ['canmore', 'banff', 'lake-louise'].includes(filter.id);
@@ -185,7 +199,7 @@ function FilterPills({ pickupLocations, selectedFilters, onFilterToggle }: Filte
185
199
  key={filter.id}
186
200
  type="button"
187
201
  onClick={() => onFilterToggle(filter.id)}
188
- className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
202
+ className={`px-4 py-3 rounded-full text-[11px] font-medium transition-colors ${
189
203
  isSelected
190
204
  ? 'bg-emerald-600 text-white hover:bg-emerald-700'
191
205
  : 'bg-stone-100 text-stone-700 hover:bg-stone-200'
@@ -209,11 +223,17 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
209
223
  selectedLocationId,
210
224
  selectedCustomAddress = null,
211
225
  allowCustomLocation = false,
226
+ restrictCustomLocationToServiceArea = false,
212
227
  onLocationSelect,
213
228
  onSkip,
214
229
  isSkipped = false,
215
- destinations = [],
230
+ hideTitle = false,
231
+ hideSkipOption = false,
232
+ highlightedPickupLocationIds,
233
+ destinations: destinationsProp,
216
234
  } = props;
235
+ const highlightIds = highlightedPickupLocationIds ?? NO_HIGHLIGHTED_PICKUP_LOCATION_IDS;
236
+ const destinations = destinationsProp ?? NO_DESTINATIONS;
217
237
  const { t } = useTranslations();
218
238
  const [searchInput, setSearchInput] = useState('');
219
239
  const [searchedLocation, setSearchedLocation] = useState<SearchedLocation | null>(null);
@@ -230,7 +250,26 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
230
250
  const [showSuggestions, setShowSuggestions] = useState(false);
231
251
  const [mapZoom, setMapZoom] = useState(DEFAULT_MAP_ZOOM);
232
252
  const [showSkipWarning, setShowSkipWarning] = useState(false);
253
+ const [dontKnowFilterActive, setDontKnowFilterActive] = useState(false);
233
254
  const mapRef = useRef<google.maps.Map | null>(null);
255
+ const highlightedPickupLocationIdSet = useMemo(
256
+ () => new Set(highlightIds),
257
+ [highlightIds]
258
+ );
259
+
260
+ const sortHighlightedFirst = useCallback(
261
+ <T extends PickupLocation,>(locations: T[]): T[] => {
262
+ if (highlightedPickupLocationIdSet.size === 0 || locations.length < 2) return locations;
263
+ return [...locations].sort((a, b) => {
264
+ const aHighlighted = highlightedPickupLocationIdSet.has(a.id);
265
+ const bHighlighted = highlightedPickupLocationIdSet.has(b.id);
266
+ if (aHighlighted === bHighlighted) return 0;
267
+ return aHighlighted ? -1 : 1;
268
+ });
269
+ },
270
+ [highlightedPickupLocationIdSet]
271
+ );
272
+
234
273
  const suggestionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
235
274
  const searchInputRef = useRef<HTMLInputElement | null>(null);
236
275
  const focusTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -250,8 +289,14 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
250
289
 
251
290
  // Apply filter pills to pickup locations
252
291
  const filteredPickupLocations = useMemo(() => {
253
- return filterLocations(pickupLocations, selectedFilters);
254
- }, [pickupLocations, selectedFilters]);
292
+ return sortHighlightedFirst(filterLocations(pickupLocations, selectedFilters));
293
+ }, [pickupLocations, selectedFilters, sortHighlightedFirst]);
294
+
295
+ // When restrictCustomLocationToServiceArea is true, custom address is only allowed if within Lake Louise–Kananaskis corridor
296
+ const isCustomLocationInServiceArea = useMemo(() => {
297
+ if (!restrictCustomLocationToServiceArea || !searchedLocation?.coordinates) return true;
298
+ return isWithinPrivateShuttleServiceArea(searchedLocation.coordinates);
299
+ }, [restrictCustomLocationToServiceArea, searchedLocation?.coordinates]);
255
300
 
256
301
  // Calculate nearby locations when user searches
257
302
  useEffect(() => {
@@ -276,7 +321,7 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
276
321
 
277
322
  // Case 2: City/town filter - show all locations in that city (already filtered by pills)
278
323
  if (cityFilter) {
279
- const cityLocations = filteredPickupLocations
324
+ const cityLocations = sortHighlightedFirst(filteredPickupLocations
280
325
  .filter(loc => {
281
326
  if (!loc.coordinates) return false;
282
327
  const addressLower = loc.address.toLowerCase();
@@ -287,7 +332,7 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
287
332
  distance: 0, // No distance calculation for city filter
288
333
  walkingTime: 0,
289
334
  drivingTime: 0,
290
- }));
335
+ })));
291
336
  setNearbyLocations(cityLocations);
292
337
  setIsValidLocation(null);
293
338
  return;
@@ -306,11 +351,11 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
306
351
  );
307
352
 
308
353
  // Take only the 3 closest locations (already sorted by distance)
309
- const closestLocations = locationsWithDistance.slice(0, 3);
354
+ const closestLocations = sortHighlightedFirst(locationsWithDistance.slice(0, 3));
310
355
 
311
356
  setNearbyLocations(closestLocations);
312
357
  setIsValidLocation(isExactMatch(locationsWithDistance));
313
- }, [searchedLocation, filteredPickupLocations, exactMatchLocationId, cityFilter]);
358
+ }, [searchedLocation, filteredPickupLocations, exactMatchLocationId, cityFilter, sortHighlightedFirst]);
314
359
 
315
360
  // Reset search state when switching to "I don't know" option
316
361
  useEffect(() => {
@@ -328,10 +373,10 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
328
373
  }
329
374
  }, [isSkipped]);
330
375
 
331
- // Clear search state when a location is selected externally (from parent)
376
+ // Clear search state when a location is selected from the list (parent updates selectedLocationId/selectedCustomAddress)
377
+ // Only run when selection changes - NOT when user types (searchInput), otherwise typing gets erased
332
378
  useEffect(() => {
333
- if (selectedLocationId && (searchedLocation || searchInput)) {
334
- // Location was selected externally, clear search state
379
+ if ((selectedLocationId || selectedCustomAddress) && (searchedLocation || searchInput)) {
335
380
  setSearchInput('');
336
381
  setSearchedLocation(null);
337
382
  setIsValidLocation(null);
@@ -339,7 +384,7 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
339
384
  setShowSuggestions(false);
340
385
  setAutocompleteSuggestions([]);
341
386
  }
342
- }, [selectedLocationId, searchedLocation, searchInput]);
387
+ }, [selectedLocationId, selectedCustomAddress]);
343
388
 
344
389
  // Cleanup timeouts on unmount
345
390
  useEffect(() => {
@@ -588,8 +633,8 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
588
633
  }
589
634
  };
590
635
 
591
- const handleLocationSelect = (locationId: string) => {
592
- onLocationSelect(locationId);
636
+ const handleLocationSelect = (locationId: string, locationName?: string) => {
637
+ onLocationSelect(locationId, undefined, locationName);
593
638
  // Clear search state when a location is selected
594
639
  setSearchInput('');
595
640
  setSearchedLocation(null);
@@ -831,49 +876,26 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
831
876
  }
832
877
 
833
878
  return (
834
- <div className="space-y-6">
835
- <h2 className="text-2xl font-bold text-stone-900">
836
- {t('pickup.title')}
837
- </h2>
838
-
839
- {/* Radio buttons for selection */}
840
- <div className="space-y-4">
841
- <label className="flex items-start gap-3 cursor-pointer">
842
- <input
843
- type="radio"
844
- name="pickup-option"
845
- checked={!isSkipped}
846
- onChange={handleYesOptionClick}
847
- className="mt-1 w-5 h-5 text-emerald-600 focus:ring-emerald-500"
848
- aria-label={t('pickup.yesAddNow')}
849
- />
850
- <div className="flex-1">
851
- <span className="font-medium text-stone-900">
852
- {t('pickup.yesAddNow')}
853
- </span>
854
- <p className="text-sm text-stone-600 mt-1">
855
- {t('pickup.yesAddNowSubtext')}
856
- </p>
857
- {/* Show search and map interface when "Yes" option is selected (not skipped) */}
858
- {!isSkipped && (
859
- <div className="mt-2">
860
- <div className="relative">
861
- <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
862
- <svg
863
- className="h-5 w-5 text-stone-400"
864
- fill="none"
865
- stroke="currentColor"
866
- viewBox="0 0 24 24"
867
- >
868
- <path
869
- strokeLinecap="round"
870
- strokeLinejoin="round"
871
- strokeWidth={2}
872
- d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
873
- />
874
- </svg>
875
- </div>
876
- <input
879
+ <div className="space-y-3">
880
+ {!hideTitle && (
881
+ <div className="space-y-0.5">
882
+ <h2 className="text-2xl font-bold text-stone-900">
883
+ {t('pickup.title')}
884
+ </h2>
885
+ <p className="text-sm text-stone-600">
886
+ {t('pickup.yesAddNowSubtext')}
887
+ </p>
888
+ </div>
889
+ )}
890
+
891
+ {/* Input with search icon inside - padding ensures text stays to the right of icon */}
892
+ <div className="relative">
893
+ <div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-stone-400">
894
+ <svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
895
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
896
+ </svg>
897
+ </div>
898
+ <input
877
899
  ref={searchInputRef}
878
900
  type="text"
879
901
  value={searchInput}
@@ -916,7 +938,8 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
916
938
  aria-label={t('pickup.enterAddress')}
917
939
  aria-autocomplete="list"
918
940
  aria-controls="autocomplete-suggestions"
919
- className="w-full pl-10 pr-4 py-3 rounded-lg border border-stone-300 focus:outline-none focus:border-stone-500 text-stone-900"
941
+ data-pickup-search
942
+ className="w-full rounded-lg border border-stone-300 focus:outline-none focus:border-stone-500 text-stone-900 placeholder:text-stone-400"
920
943
  />
921
944
  {/* Autocomplete suggestions dropdown */}
922
945
  {showSuggestions && autocompleteSuggestions.length > 0 && (
@@ -966,42 +989,57 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
966
989
  ))}
967
990
  </div>
968
991
  )}
969
- </div>
970
- {isGeocoding && (
971
- <p className="mt-1 text-sm text-stone-500">
972
- {t('pickup.searchingLocation')}
973
- </p>
974
- )}
975
- {searchedLocation && !searchedLocation.coordinates && !isGeocoding && (
976
- <div className="mt-2 p-3 bg-red-50 border border-red-200 rounded-lg">
977
- <div className="flex items-start gap-2">
978
- <svg
979
- className="w-5 h-5 text-red-600 mt-0.5 flex-shrink-0"
980
- fill="currentColor"
981
- viewBox="0 0 20 20"
982
- >
983
- <path
984
- fillRule="evenodd"
985
- d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
986
- clipRule="evenodd"
987
- />
988
- </svg>
989
- <p className="text-sm text-red-700">
990
- {t('pickup.locationNotFound')}
991
- </p>
992
- </div>
993
- </div>
994
- )}
995
- </div>
996
- )}
992
+ {isGeocoding && (
993
+ <p className="mt-1 text-sm text-stone-500">
994
+ {t('pickup.searchingLocation')}
995
+ </p>
996
+ )}
997
+ {searchedLocation && !searchedLocation.coordinates && !isGeocoding && (
998
+ <div className="mt-2 p-3 bg-red-50 border border-red-200 rounded-lg">
999
+ <div className="flex items-start gap-2">
1000
+ <svg
1001
+ className="w-5 h-5 text-red-600 mt-0.5 flex-shrink-0"
1002
+ fill="currentColor"
1003
+ viewBox="0 0 20 20"
1004
+ >
1005
+ <path
1006
+ fillRule="evenodd"
1007
+ d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
1008
+ clipRule="evenodd"
1009
+ />
1010
+ </svg>
1011
+ <p className="text-sm text-red-700">
1012
+ {t('pickup.locationNotFound')}
1013
+ </p>
1014
+ </div>
997
1015
  </div>
998
- </label>
1016
+ )}
1017
+ {allowCustomLocation && restrictCustomLocationToServiceArea && searchedLocation?.coordinates && !isCustomLocationInServiceArea && (
1018
+ <div className="mt-2 p-3 bg-amber-50 border border-amber-200 rounded-lg">
1019
+ <div className="flex items-start gap-2">
1020
+ <svg
1021
+ className="w-5 h-5 text-amber-600 mt-0.5 flex-shrink-0"
1022
+ fill="currentColor"
1023
+ viewBox="0 0 20 20"
1024
+ >
1025
+ <path
1026
+ fillRule="evenodd"
1027
+ d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
1028
+ clipRule="evenodd"
1029
+ />
1030
+ </svg>
1031
+ <p className="text-sm text-amber-800">
1032
+ {t('pickup.outsideServiceArea')}
1033
+ </p>
1034
+ </div>
1035
+ </div>
1036
+ )}
1037
+ </div>
999
1038
 
1000
- {/* Map - Show when "Yes" option is selected (not skipped) */}
1001
- {!isSkipped && isLoaded && pickupLocations.length > 0 && (
1002
- <div className="ml-8">
1003
- {/* Filter Pills */}
1004
- <FilterPills
1039
+ {/* Filter Pills + I don't know pill - horizontal scroll on mobile */}
1040
+ {isLoaded && pickupLocations.length > 0 && (
1041
+ <div className={styles.filterPillsScroll}>
1042
+ <FilterPills
1005
1043
  pickupLocations={pickupLocations}
1006
1044
  selectedFilters={selectedFilters}
1007
1045
  onFilterToggle={(filterId) => {
@@ -1033,6 +1071,11 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
1033
1071
 
1034
1072
  setSelectedFilters(newFilters);
1035
1073
 
1074
+ // When a location filter (city or free-parking) is selected, exit "I don't know" mode
1075
+ if (newFilters.size > 0) {
1076
+ setDontKnowFilterActive(false);
1077
+ }
1078
+
1036
1079
  // When a city filter is active, also drive the map zoom like a typed city search
1037
1080
  const activeCityFilter = Array.from(newFilters).find(f =>
1038
1081
  cityFilterIds.includes(f as typeof cityFilterIds[number])
@@ -1052,23 +1095,147 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
1052
1095
  }
1053
1096
  }}
1054
1097
  />
1055
-
1056
- {/* No results message */}
1057
- {nearbyLocations.length === 0 &&
1058
- !exactMatchLocationId &&
1059
- !cityFilter &&
1060
- !searchedLocation &&
1061
- filteredPickupLocations.length === 0 &&
1062
- selectedFilters.size > 0 && (
1063
- <div className="mb-4 p-4 bg-stone-50 border border-stone-200 rounded-lg">
1064
- <p className="text-sm text-stone-600 text-center">
1065
- No pickup locations match the selected filters. Try adjusting your filters.
1066
- </p>
1098
+ {/* I don't know pill - only show when no filters are selected and not hidden */}
1099
+ {!hideSkipOption && selectedFilters.size === 0 && (
1100
+ <button
1101
+ type="button"
1102
+ onClick={() => setDontKnowFilterActive(!dontKnowFilterActive)}
1103
+ className={`px-4 py-3 rounded-full text-[11px] font-medium transition-colors ${
1104
+ dontKnowFilterActive ? 'bg-emerald-600 text-white hover:bg-emerald-700' : 'bg-stone-100 text-stone-700 hover:bg-stone-200'
1105
+ }`}
1106
+ >
1107
+ {t('pickup.dontKnow')}
1108
+ </button>
1109
+ )}
1110
+ </div>
1111
+ )}
1112
+
1113
+ {/* Two-column layout: list left, map right - list height matches map */}
1114
+ {isLoaded && pickupLocations.length > 0 && (
1115
+ <div className={styles.twoColLayout}>
1116
+ {/* Left column: scrollable list of pickup locations + I don't know */}
1117
+ <div className={styles.leftColumn}>
1118
+ <div className={styles.locationList}>
1119
+ {/* When "I don't know" filter is active, show only that option (unless hidden) */}
1120
+ {!hideSkipOption && dontKnowFilterActive ? (
1121
+ <label
1122
+ className="flex items-start gap-2 cursor-pointer p-2.5 rounded-lg hover:bg-stone-50 border border-stone-200"
1123
+ onClick={(e) => {
1124
+ e.preventDefault();
1125
+ handleSkip();
1126
+ }}
1127
+ >
1128
+ <input
1129
+ type="radio"
1130
+ name="pickup-location"
1131
+ checked={isSkipped}
1132
+ readOnly
1133
+ onClick={(e) => {
1134
+ e.preventDefault();
1135
+ handleSkip();
1136
+ }}
1137
+ className="mt-1 w-4 h-4 text-emerald-600 focus:ring-emerald-500 shrink-0"
1138
+ aria-label={t('pickup.dontKnow')}
1139
+ />
1140
+ <div className="flex-1 min-w-0">
1141
+ <p className="font-medium text-stone-900 text-sm">{t('pickup.dontKnow')}</p>
1142
+ <p className="text-xs text-stone-600">{t('pickup.dontKnowSubtext')}</p>
1143
+ </div>
1144
+ </label>
1145
+ ) : (
1146
+ <>
1147
+ {/* Custom address option - show at TOP when user searched (Private Shuttle allows custom), and within service area if restricted */}
1148
+ {allowCustomLocation && searchedLocation && !exactMatchLocationId && !cityFilter && isCustomLocationInServiceArea && (
1149
+ <label className="flex items-start gap-2 cursor-pointer p-3 rounded-lg hover:bg-emerald-50/50 border-2 border-emerald-500 bg-emerald-50/50 shadow-sm" onMouseEnter={() => { setHoveredMarker('searched'); mapRef.current && searchedLocation.coordinates && panToLocationIfNeeded(mapRef.current, searchedLocation.coordinates); }} onMouseLeave={() => setHoveredMarker(null)}>
1150
+ <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
+ <div className="flex-1 min-w-0">
1152
+ <p className="font-semibold text-emerald-800 text-sm">{t('pickup.useThisAddress')}</p>
1153
+ <p className="text-xs text-stone-600 truncate">{searchedLocation.address}</p>
1154
+ </div>
1155
+ </label>
1156
+ )}
1157
+ {/* Locations to show: nearbyLocations when we have search/filter results, else filteredPickupLocations */}
1158
+ {(nearbyLocations.length > 0 ? nearbyLocations : filteredPickupLocations).map((location) => {
1159
+ const isNearby = nearbyLocations.length > 0 && nearbyLocations.some(n => n.id === location.id);
1160
+ const isPartnerHighlighted = highlightedPickupLocationIdSet.has(location.id);
1161
+ return (
1162
+ <label key={location.id} className={`flex items-start gap-2 cursor-pointer p-2.5 rounded-lg border ${
1163
+ isPartnerHighlighted
1164
+ ? 'border-emerald-300 bg-emerald-50/40 hover:bg-emerald-50'
1165
+ : 'border-stone-200 hover:bg-stone-50'
1166
+ }`} onMouseEnter={() => { setHoveredMarker(location.id); mapRef.current && location.coordinates && panToLocationIfNeeded(mapRef.current, location.coordinates); }} onMouseLeave={() => setHoveredMarker(null)}>
1167
+ <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
+ <div className="flex-1 min-w-0">
1169
+ <div className="flex items-center gap-1.5">
1170
+ <p className="font-medium text-stone-900 text-sm">{location.name}</p>
1171
+ {isPartnerHighlighted && (
1172
+ <span
1173
+ className="inline-flex items-center justify-center rounded-full w-4 h-4 bg-emerald-100 text-emerald-700"
1174
+ aria-label="Highlighted pickup location"
1175
+ title="Highlighted pickup location"
1176
+ >
1177
+ <svg
1178
+ className="w-2.5 h-2.5"
1179
+ viewBox="0 0 20 20"
1180
+ fill="currentColor"
1181
+ aria-hidden="true"
1182
+ >
1183
+ <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.539 1.118l-2.8-2.034a1 1 0 00-1.176 0l-2.8 2.034c-.783.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81H7.03a1 1 0 00.95-.69l1.07-3.292z" />
1184
+ </svg>
1185
+ </span>
1186
+ )}
1187
+ </div>
1188
+ <p className="text-xs text-stone-600 truncate">{location.address}</p>
1189
+ {isNearby && searchedLocation && !exactMatchLocationId && !cityFilter && (
1190
+ <div className="flex items-center gap-2 mt-1 text-xs text-stone-500">
1191
+ <span>📍 {formatDistance((location as NearbyLocation).distance, useImperial)}</span>
1192
+ <span>🚶 {formatTime((location as NearbyLocation).walkingTime)}</span>
1193
+ </div>
1194
+ )}
1195
+ </div>
1196
+ </label>
1197
+ );})}
1198
+ {/* No results message */}
1199
+ {nearbyLocations.length === 0 && filteredPickupLocations.length === 0 && selectedFilters.size > 0 && (
1200
+ <p className="p-3 text-sm text-stone-500 text-center">No locations match. Try adjusting filters.</p>
1201
+ )}
1202
+ {/* I don't know - only at end of list when no filters are selected (unless hidden) */}
1203
+ {!hideSkipOption && selectedFilters.size === 0 && (
1204
+ <label
1205
+ className="flex items-start gap-2 cursor-pointer p-2.5 rounded-lg hover:bg-stone-50 border border-stone-200"
1206
+ onClick={(e) => {
1207
+ e.preventDefault();
1208
+ handleSkip();
1209
+ }}
1210
+ >
1211
+ <input
1212
+ type="radio"
1213
+ name="pickup-location"
1214
+ checked={isSkipped}
1215
+ readOnly
1216
+ onClick={(e) => {
1217
+ e.preventDefault();
1218
+ handleSkip();
1219
+ }}
1220
+ className="mt-1 w-4 h-4 text-emerald-600 focus:ring-emerald-500 shrink-0"
1221
+ aria-label={t('pickup.dontKnow')}
1222
+ />
1223
+ <div className="flex-1 min-w-0">
1224
+ <p className="font-medium text-stone-900 text-sm">{t('pickup.dontKnow')}</p>
1225
+ <p className="text-xs text-stone-600">{t('pickup.dontKnowSubtext')}</p>
1226
+ </div>
1227
+ </label>
1228
+ )}
1229
+ </>
1230
+ )}
1067
1231
  </div>
1068
- )}
1069
-
1232
+ </div>
1233
+
1234
+ {/* Right column: map */}
1235
+ <div className={styles.rightColumn}>
1236
+ <div className={styles.mapWrapper}>
1070
1237
  <GoogleMap
1071
- mapContainerClassName="w-full h-96 rounded-lg border border-stone-300"
1238
+ mapContainerClassName="w-full h-full rounded-lg overflow-hidden"
1072
1239
  center={mapCenter}
1073
1240
  zoom={mapZoom}
1074
1241
  options={mapOptions}
@@ -1127,7 +1294,7 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
1127
1294
  </div>
1128
1295
  <button
1129
1296
  onClick={() => {
1130
- handleLocationSelect(location.id);
1297
+ handleLocationSelect(location.id, location.name);
1131
1298
  setSelectedMarker(null);
1132
1299
  }}
1133
1300
  className="mt-2 w-full px-3 py-1.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700 transition-colors"
@@ -1178,7 +1345,7 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
1178
1345
  )}
1179
1346
  <button
1180
1347
  onClick={() => {
1181
- handleLocationSelect(location.id);
1348
+ handleLocationSelect(location.id, location.name);
1182
1349
  setSelectedMarker(null);
1183
1350
  }}
1184
1351
  className="mt-2 w-full px-3 py-1.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700 transition-colors"
@@ -1235,7 +1402,7 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
1235
1402
  )}
1236
1403
  <button
1237
1404
  onClick={() => {
1238
- handleLocationSelect(location.id);
1405
+ handleLocationSelect(location.id, location.name);
1239
1406
  setSelectedMarker(null);
1240
1407
  }}
1241
1408
  className="mt-2 w-full px-3 py-1.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700 transition-colors"
@@ -1275,7 +1442,7 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
1275
1442
  <p className="text-sm text-stone-600 mb-2">
1276
1443
  {searchedLocation.address}
1277
1444
  </p>
1278
- {allowCustomLocation && (
1445
+ {allowCustomLocation && isCustomLocationInServiceArea && (
1279
1446
  <button
1280
1447
  onClick={() => handleCustomLocationSelect(searchedLocation.address, searchedLocation.coordinates ?? undefined)}
1281
1448
  className="w-full px-3 py-1.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700 transition-colors mb-2"
@@ -1283,6 +1450,11 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
1283
1450
  {t('pickup.useThisAddress')}
1284
1451
  </button>
1285
1452
  )}
1453
+ {allowCustomLocation && !isCustomLocationInServiceArea && restrictCustomLocationToServiceArea && (
1454
+ <p className="text-xs text-amber-700 mb-2">
1455
+ {t('pickup.outsideServiceArea')}
1456
+ </p>
1457
+ )}
1286
1458
  <p className="text-xs text-stone-500 mt-2">
1287
1459
  {t('pickup.chooseClosest')}
1288
1460
  </p>
@@ -1308,174 +1480,15 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
1308
1480
  />
1309
1481
  ))}
1310
1482
  </GoogleMap>
1311
- </div>
1312
- )}
1313
-
1314
- {/* Filtered locations list */}
1315
- {nearbyLocations.length > 0 && (
1316
- <div className="ml-8 space-y-4">
1317
- <h3 className="font-semibold text-stone-900">
1318
- {exactMatchLocationId
1319
- ? t('pickup.exactMatch')
1320
- : cityFilter
1321
- ? t('pickup.locationsInCity', { city: cityFilter.charAt(0).toUpperCase() + cityFilter.slice(1) })
1322
- : t('pickup.chooseNearby')}
1323
- </h3>
1324
- <div className="space-y-3">
1325
- {/* Custom address option - only when user searched for an address (Case 3) and allowCustomLocation is true */}
1326
- {allowCustomLocation && searchedLocation && !exactMatchLocationId && !cityFilter && (
1327
- <label
1328
- className="flex items-start gap-3 cursor-pointer p-3 rounded-lg hover:bg-stone-50 border border-stone-200 bg-emerald-50/30"
1329
- onMouseEnter={() => {
1330
- setHoveredMarker('searched');
1331
- if (mapRef.current && searchedLocation.coordinates) {
1332
- panToLocationIfNeeded(mapRef.current, searchedLocation.coordinates);
1333
- }
1334
- }}
1335
- onMouseLeave={() => setHoveredMarker(null)}
1336
- >
1337
- <input
1338
- type="radio"
1339
- name="pickup-location"
1340
- checked={selectedCustomAddress === searchedLocation.address}
1341
- onChange={() => handleCustomLocationSelect(searchedLocation.address, searchedLocation.coordinates ?? undefined)}
1342
- className="mt-1 w-5 h-5 text-emerald-600 focus:ring-emerald-500"
1343
- />
1344
- <div className="flex-1">
1345
- <p className="font-medium text-stone-900">
1346
- {t('pickup.useThisAddress')} — {searchedLocation.address}
1347
- </p>
1348
- <p className="text-sm text-stone-600 mt-1">
1349
- {t('pickup.yourLocation')}
1350
- </p>
1351
- </div>
1352
- </label>
1353
- )}
1354
- {nearbyLocations.map((location) => (
1355
- <label
1356
- key={location.id}
1357
- className="flex items-start gap-3 cursor-pointer p-3 rounded-lg hover:bg-stone-50 border border-stone-200"
1358
- onMouseEnter={() => {
1359
- setHoveredMarker(location.id);
1360
- if (mapRef.current && location.coordinates) {
1361
- panToLocationIfNeeded(mapRef.current, location.coordinates);
1362
- }
1363
- }}
1364
- onMouseLeave={() => {
1365
- setHoveredMarker(null);
1366
- }}
1367
- >
1368
- <input
1369
- type="radio"
1370
- name="pickup-location"
1371
- checked={selectedLocationId === location.id}
1372
- onChange={() => handleLocationSelect(location.id)}
1373
- className="mt-1 w-5 h-5 text-emerald-600 focus:ring-emerald-500"
1374
- />
1375
- <div className="flex-1">
1376
- <p className="font-medium text-stone-900">
1377
- {location.name}
1378
- </p>
1379
- <p className="text-sm text-stone-600 mt-1">
1380
- {location.address}
1381
- </p>
1382
- {/* Only show distance/time for address searches (Case 3) */}
1383
- {searchedLocation && !exactMatchLocationId && !cityFilter && (
1384
- <div className="flex items-center gap-4 mt-2 text-sm text-stone-500">
1385
- <span className="flex items-center gap-1">
1386
- <svg
1387
- className="w-4 h-4"
1388
- fill="none"
1389
- stroke="currentColor"
1390
- viewBox="0 0 24 24"
1391
- >
1392
- <path
1393
- strokeLinecap="round"
1394
- strokeLinejoin="round"
1395
- strokeWidth={2}
1396
- d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
1397
- />
1398
- <path
1399
- strokeLinecap="round"
1400
- strokeLinejoin="round"
1401
- strokeWidth={2}
1402
- d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
1403
- />
1404
- </svg>
1405
- {formatDistance(location.distance, useImperial)}
1406
- </span>
1407
- <span className="flex items-center gap-1">
1408
- <svg
1409
- className="w-4 h-4"
1410
- fill="none"
1411
- stroke="currentColor"
1412
- viewBox="0 0 24 24"
1413
- >
1414
- <path
1415
- strokeLinecap="round"
1416
- strokeLinejoin="round"
1417
- strokeWidth={2}
1418
- d="M13 10V3L4 14h7v7l9-11h-7z"
1419
- />
1420
- </svg>
1421
- {formatTime(location.walkingTime)} {t('pickup.walk')}
1422
- </span>
1423
- <span className="flex items-center gap-1">
1424
- <svg
1425
- className="w-4 h-4"
1426
- fill="none"
1427
- stroke="currentColor"
1428
- viewBox="0 0 24 24"
1429
- >
1430
- <path
1431
- strokeLinecap="round"
1432
- strokeLinejoin="round"
1433
- strokeWidth={2}
1434
- d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"
1435
- />
1436
- </svg>
1437
- {formatTime(location.drivingTime)} {t('pickup.drive')}
1438
- </span>
1439
- </div>
1440
- )}
1441
- </div>
1442
- </label>
1443
- ))}
1444
1483
  </div>
1445
- <button
1446
- onClick={() => setUseImperial(!useImperial)}
1447
- className="text-sm text-emerald-600 hover:text-emerald-700 underline"
1448
- >
1449
- {t('pickup.switchUnits', { unit: useImperial ? t('pickup.metric') : t('pickup.imperial') })}
1450
- </button>
1451
- </div>
1452
- )}
1453
-
1454
- {/* Skip option */}
1455
- <label className="flex items-start gap-3 cursor-pointer">
1456
- <input
1457
- type="radio"
1458
- name="pickup-option"
1459
- checked={isSkipped && selectedLocationId === null && searchedLocation === null}
1460
- onChange={handleSkip}
1461
- className="mt-1 w-5 h-5 text-emerald-600 focus:ring-emerald-500"
1462
- aria-label={t('pickup.dontKnow')}
1463
- />
1464
- <div className="flex-1">
1465
- <span className="font-medium text-stone-900">
1466
- {t('pickup.dontKnow')}
1467
- </span>
1468
- <p className="text-sm text-stone-600 mt-1">
1469
- {t('pickup.dontKnowSubtext')}
1470
- </p>
1484
+ </div>
1471
1485
  </div>
1472
- </label>
1473
- </div>
1486
+ )}
1474
1487
 
1475
- {/* Warning Modal - Only closes when "I understand" is clicked */}
1488
+ {/* Warning Modal - Only closes when "I understand" or "Nevermind" is clicked */}
1476
1489
  {showSkipWarning && (
1477
1490
  <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
1478
- <div className="bg-white rounded-lg shadow-xl max-w-md w-full mx-4 p-6">
1491
+ <div className="bg-white rounded-lg shadow-xl max-w-xl w-full mx-4 p-6">
1479
1492
  <h3 className="text-lg font-semibold text-stone-900 mb-4">
1480
1493
  {t('pickup.skipWarningTitle')}
1481
1494
  </h3>
@@ -1483,10 +1496,20 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
1483
1496
  className="text-stone-700 mb-6"
1484
1497
  dangerouslySetInnerHTML={{ __html: t('pickup.skipWarningMessage') }}
1485
1498
  />
1486
- <div className="flex justify-end">
1499
+ <div className="flex justify-center gap-3 flex-nowrap">
1500
+ <button
1501
+ type="button"
1502
+ onClick={() => {
1503
+ setShowSkipWarning(false);
1504
+ setDontKnowFilterActive(false);
1505
+ }}
1506
+ className={styles.skipModalNevermindBtn}
1507
+ >
1508
+ {t('pickup.nevermindSelectLocation')}
1509
+ </button>
1487
1510
  <button
1488
1511
  onClick={handleSkipConfirm}
1489
- className="px-4 py-2 bg-emerald-600 text-white font-medium rounded-lg hover:bg-emerald-700 transition-colors"
1512
+ className={`${styles.skipModalUnderstandBtn} bg-emerald-600 text-white font-medium rounded-lg hover:bg-emerald-700 transition-colors`}
1490
1513
  >
1491
1514
  {t('pickup.iUnderstand')}
1492
1515
  </button>
@@ -1515,9 +1538,11 @@ export function PickupLocationSelector(props: PickupLocationSelectorProps) {
1515
1538
  if (!hasMapsKey) {
1516
1539
  return (
1517
1540
  <div className="space-y-6">
1518
- <h2 className="text-2xl font-bold text-stone-900">
1519
- {props.isSkipped ? null : t('pickup.title')}
1520
- </h2>
1541
+ {!props.hideTitle && (
1542
+ <h2 className="text-2xl font-bold text-stone-900">
1543
+ {props.isSkipped ? null : t('pickup.title')}
1544
+ </h2>
1545
+ )}
1521
1546
  <div className="p-4 bg-stone-50 border border-stone-200 rounded-lg text-stone-600 text-sm">
1522
1547
  Map unavailable (no API key). You can still complete your booking. Add{' '}
1523
1548
  <code className="bg-stone-200 px-1 rounded">NEXT_PUBLIC_GOOGLE_MAPS_API_KEY</code> to enable the map.
@@ -1528,7 +1553,7 @@ export function PickupLocationSelector(props: PickupLocationSelectorProps) {
1528
1553
  </p>
1529
1554
  )}
1530
1555
  </div>
1531
- {!props.isSkipped && (
1556
+ {!props.isSkipped && !props.hideTitle && (
1532
1557
  <p className="text-sm text-stone-500">
1533
1558
  Select a pickup location from the list, or skip this step.
1534
1559
  </p>