@ticketboothapp/booking 0.1.18 → 0.1.20

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 (152) hide show
  1. package/package.json +1 -1
  2. package/src/components/BookingWidget.tsx +282 -26
  3. package/src/components/ManageBookingView.tsx +75 -23
  4. package/src/components/booking/BookingProductGrid.tsx +1 -1
  5. package/src/components/booking/Calendar.module.css +3 -3
  6. package/src/components/booking/CheckoutForm.tsx +1 -1
  7. package/src/components/booking/PickupLocationSelector.tsx +1 -1
  8. package/src/index.ts +3 -1
  9. package/src/app/photo-sessions/photo-packages.ts +0 -75
  10. package/src/assets/icons/minus.svg +0 -7
  11. package/src/assets/icons/partner-logos/getyourguide.svg +0 -8
  12. package/src/assets/icons/plus.svg +0 -3
  13. package/src/colours.css +0 -23
  14. package/src/components/BookingDetails.module.css +0 -1591
  15. package/src/components/BookingDetails.tsx +0 -2264
  16. package/src/components/JobApplicationDialog.module.css +0 -440
  17. package/src/components/JobApplicationDialog.tsx +0 -620
  18. package/src/components/PhoneInputWithCountry.module.css +0 -131
  19. package/src/components/PhoneInputWithCountry.tsx +0 -44
  20. package/src/components/PickupLocationDialog.module.css +0 -360
  21. package/src/components/PickupLocationDialog.tsx +0 -357
  22. package/src/components/PickupLocationMap.tsx +0 -110
  23. package/src/components/PostBookingDependentAddOnUpsell.module.css +0 -174
  24. package/src/components/PostBookingDependentAddOnUpsell.tsx +0 -407
  25. package/src/components/accordion.css +0 -27
  26. package/src/components/accordion.tsx +0 -29
  27. package/src/components/analytics/AnalyticsConsentRestore.tsx +0 -19
  28. package/src/components/analytics/AnalyticsScripts.tsx +0 -106
  29. package/src/components/analytics/CookieConsentBanner.css +0 -86
  30. package/src/components/analytics/CookieConsentBanner.tsx +0 -102
  31. package/src/components/bottom-sheet.module.css +0 -78
  32. package/src/components/bottom-sheet.tsx +0 -60
  33. package/src/components/breadcrumb.module.css +0 -40
  34. package/src/components/breadcrumb.tsx +0 -36
  35. package/src/components/button.css +0 -245
  36. package/src/components/button.tsx +0 -152
  37. package/src/components/client-bottom-sheet.tsx +0 -14
  38. package/src/components/colorable-svg.tsx +0 -29
  39. package/src/components/conditional-footer.tsx +0 -27
  40. package/src/components/contact-us.module.css +0 -147
  41. package/src/components/contact-us.tsx +0 -49
  42. package/src/components/email-signup.css +0 -151
  43. package/src/components/email-signup.tsx +0 -63
  44. package/src/components/faq-wrapper.module.css +0 -47
  45. package/src/components/faq-wrapper.tsx +0 -15
  46. package/src/components/footer.css +0 -187
  47. package/src/components/footer.tsx +0 -143
  48. package/src/components/global-simple-modal.tsx +0 -33
  49. package/src/components/google-review-summary.module.css +0 -77
  50. package/src/components/google-review-summary.tsx +0 -50
  51. package/src/components/hero-image.css +0 -13
  52. package/src/components/hero-image.tsx +0 -44
  53. package/src/components/image.css +0 -29
  54. package/src/components/image.tsx +0 -113
  55. package/src/components/language-aware-link.tsx +0 -72
  56. package/src/components/language-switcher.module.css +0 -124
  57. package/src/components/language-switcher.tsx +0 -75
  58. package/src/components/map-section.css +0 -59
  59. package/src/components/map-section.tsx +0 -63
  60. package/src/components/navbar.module.css +0 -152
  61. package/src/components/navbar.tsx +0 -125
  62. package/src/components/parallax-provider.tsx +0 -11
  63. package/src/components/product-tag.module.css +0 -30
  64. package/src/components/product-tag.tsx +0 -34
  65. package/src/components/product-theme-pages/best-option.module.css +0 -70
  66. package/src/components/product-theme-pages/best-option.tsx +0 -35
  67. package/src/components/product-theme-pages/extended-tour-options.module.css +0 -22
  68. package/src/components/product-theme-pages/extended-tour-options.tsx +0 -11
  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/product-theme-pages/photo-gallery.tsx +0 -90
  72. package/src/components/product-theme-pages/product-theme-page-layout.module.css +0 -13
  73. package/src/components/product-theme-pages/product-theme-page-layout.tsx +0 -67
  74. package/src/components/product-theme-pages/top-of-fold.module.css +0 -179
  75. package/src/components/product-theme-pages/top-of-fold.tsx +0 -80
  76. package/src/components/product-tile/image-only-product-tile-desktop.module.css +0 -106
  77. package/src/components/product-tile/image-only-product-tile-desktop.tsx +0 -56
  78. package/src/components/product-tile/image-only-product-tile-mobile.module.css +0 -122
  79. package/src/components/product-tile/image-only-product-tile-mobile.tsx +0 -89
  80. package/src/components/product-tile/image-only-product-tile.tsx +0 -44
  81. package/src/components/product-tile/product-tile-card.module.css +0 -84
  82. package/src/components/product-tile/product-tile-card.tsx +0 -61
  83. package/src/components/review-highlights-section.css +0 -85
  84. package/src/components/review-highlights-section.tsx +0 -127
  85. package/src/components/season-closure-overlay.module.css +0 -99
  86. package/src/components/season-closure-overlay.tsx +0 -98
  87. package/src/components/simple-modal.tsx +0 -69
  88. package/src/components/simple-top-of-fold.module.css +0 -76
  89. package/src/components/simple-top-of-fold.tsx +0 -34
  90. package/src/components/spacer.css +0 -41
  91. package/src/components/spacer.tsx +0 -23
  92. package/src/components/star-rating.module.css +0 -74
  93. package/src/components/star-rating.tsx +0 -48
  94. package/src/components/terms/TermsContent.tsx +0 -178
  95. package/src/components/title-subtitle.module.css +0 -10
  96. package/src/components/title-subtitle.tsx +0 -30
  97. package/src/components/translatable-reviews.tsx +0 -75
  98. package/src/components/value-pill.module.css +0 -59
  99. package/src/components/value-pill.tsx +0 -46
  100. package/src/components/value-props.css +0 -185
  101. package/src/components/value-props.tsx +0 -88
  102. package/src/constants/booking-guide-quiz.ts +0 -64
  103. package/src/constants/contact-info.ts +0 -2
  104. package/src/constants/faq.ts +0 -44
  105. package/src/constants/images.ts +0 -556
  106. package/src/constants/json-ld/faq-json-ld.tsx +0 -170
  107. package/src/constants/json-ld/homepage-json-ld.tsx +0 -138
  108. package/src/constants/json-ld/job-posting-json-ld.tsx +0 -92
  109. package/src/constants/json-ld/organization-json-ld.tsx +0 -62
  110. package/src/constants/json-ld/page-json-ld.tsx +0 -6
  111. package/src/constants/json-ld/product-json-ld.tsx +0 -154
  112. package/src/constants/json-ld/review-json-ld.tsx +0 -377
  113. package/src/constants/navigation-links/footer-links.ts +0 -48
  114. package/src/constants/navigation-links/nav-bar-links.ts +0 -41
  115. package/src/constants/navigation-links/navigation-link.ts +0 -6
  116. package/src/constants/pill-values.ts +0 -210
  117. package/src/constants/products.ts +0 -155
  118. package/src/constants/quiz-recommendations.ts +0 -506
  119. package/src/constants/reviews.ts +0 -75
  120. package/src/constants/staff.ts +0 -197
  121. package/src/constants/value-props.ts +0 -58
  122. package/src/data/dap-descriptions/session-couples-families-friends.en.json +0 -61
  123. package/src/data/dap-descriptions/session-elopements.en.json +0 -60
  124. package/src/data/dap-descriptions/session-proposals.en.json +0 -60
  125. package/src/data/product-descriptions/afternoon-delight.en.json +0 -35
  126. package/src/data/product-descriptions/emerald-lake-escape.en.json +0 -68
  127. package/src/data/product-descriptions/lake-louise-adventure.en.json +0 -74
  128. package/src/data/product-descriptions/moraine-lake-adventure.en.json +0 -78
  129. package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +0 -65
  130. package/src/data/product-descriptions/moraine-lake-sunrise.en.json +0 -64
  131. package/src/data/product-descriptions/private-tour.en.json +0 -80
  132. package/src/data/product-descriptions/two-lakes-combo.en.json +0 -65
  133. package/src/data/products-config.json +0 -101
  134. package/src/hooks/use-bottom-sheet.tsx +0 -15
  135. package/src/hooks/use-simple-modal.tsx +0 -27
  136. package/src/hooks/useBookingSourceMetadataFromLocation.ts +0 -21
  137. package/src/hooks/useEmailSubscription.tsx +0 -103
  138. package/src/hooks/useEmbeddedInIframe.ts +0 -16
  139. package/src/hooks/useIsBookingLaunchLive.ts +0 -49
  140. package/src/hooks/useQuiz.tsx +0 -210
  141. package/src/providers/bottom-sheet-provider.tsx +0 -40
  142. package/src/providers/dependent-add-on-dialog-provider.tsx +0 -105
  143. package/src/radius.css +0 -5
  144. package/src/spacing.css +0 -7
  145. package/src/strings/en.json +0 -1774
  146. package/src/strings/es.json +0 -1573
  147. package/src/strings/fr.json +0 -1573
  148. package/src/strings/index.js +0 -23
  149. package/src/text-style.css +0 -97
  150. package/src/types/fareharbor.d.ts +0 -12
  151. package/src/types/quiz.ts +0 -59
  152. package/src/utils/currency-converter.ts +0 -101
@@ -1,357 +0,0 @@
1
- 'use client';
2
-
3
- import React, { useState, useEffect, useCallback, useMemo } from 'react';
4
- import { useSearchParams } from 'next/navigation';
5
- import { formatBookingRefForDisplay } from '@/lib/booking-ref';
6
- import { fetchProducts, updatePickupLocation, previewPickupLocationChange, type PickupLocation, type Product } from '@/lib/booking-api';
7
- import type { BookingData, ItineraryDisplayStep } from '@/components/BookingDetails';
8
- import { PickupLocationSelector } from '@/components/booking/PickupLocationSelector';
9
- import Button, { ButtonHoverColor } from '@/components/button';
10
- import { useTranslations } from '@/lib/booking/i18n';
11
- import styles from './PickupLocationDialog.module.css';
12
-
13
- function findProductWithPickupLocations(
14
- products: Product[],
15
- bookingProductId: string
16
- ): Product | null {
17
- for (const p of products) {
18
- if (p.productId === bookingProductId) return p;
19
- if (p.pickupLocations?.some((loc) => loc.id === bookingProductId)) return p;
20
- const opt = (p as Product & { options?: { optionId: string }[] })?.options?.find(
21
- (o) => o.optionId === bookingProductId
22
- );
23
- if (opt) return p;
24
- }
25
- return null;
26
- }
27
-
28
- function getPickupTimeFromItinerary(itinerary: ItineraryDisplayStep[] | null | undefined): string {
29
- if (!itinerary?.length) return '—';
30
- const pickupStep = itinerary.find(s => s.stepType === 'pickup');
31
- return pickupStep?.time?.trim() || 'TBD';
32
- }
33
-
34
- function getDropoffTimeFromItinerary(itinerary: ItineraryDisplayStep[] | null | undefined): string {
35
- if (!itinerary?.length) return '—';
36
- const dropOffStep = [...itinerary].reverse().find(s => s.stepType === 'drop_off');
37
- return dropOffStep?.time?.trim() || 'TBD';
38
- }
39
-
40
- function getItineraryStepLabel(step: ItineraryDisplayStep, includeTime = false): string {
41
- if (step.label?.trim()) {
42
- const base = step.label.trim();
43
- return includeTime && step.time?.trim() ? `${base} ${step.time.trim()}` : base;
44
- }
45
- const place = step.place?.trim();
46
- const placeDisplay =
47
- place === 'your_pickup_location'
48
- ? 'your pickup location'
49
- : place === 'the_destination'
50
- ? 'the destination'
51
- : place ?? '';
52
- const timePart = includeTime && step.time?.trim() ? ` ${step.time.trim()}` : '';
53
- switch (step.stepType) {
54
- case 'pickup':
55
- return placeDisplay ? `Pickup at ${placeDisplay}${timePart}` : `Pickup${timePart}`;
56
- case 'drop_off':
57
- return placeDisplay ? `Drop off at ${placeDisplay}${timePart}` : `Drop-off${timePart}`;
58
- case 'arrive':
59
- return placeDisplay ? `Arrive at ${placeDisplay}${timePart}` : `Arrive${timePart}`;
60
- case 'depart':
61
- return placeDisplay ? `Depart ${placeDisplay}${timePart}` : `Depart${timePart}`;
62
- case 'trip_end':
63
- return 'Trip ends';
64
- case 'draft':
65
- return placeDisplay || 'Stop';
66
- default:
67
- return placeDisplay || (step.stepType ?? 'Step');
68
- }
69
- }
70
-
71
- interface PickupLocationDialogProps {
72
- isOpen: boolean;
73
- onClose: () => void;
74
- booking: BookingData;
75
- onSuccess: (updatedBooking: BookingData) => void;
76
- }
77
-
78
- export default function PickupLocationDialog({
79
- isOpen,
80
- onClose,
81
- booking,
82
- onSuccess,
83
- }: PickupLocationDialogProps) {
84
- const { t } = useTranslations();
85
- const [products, setProducts] = useState<Product[]>([]);
86
- const [loading, setLoading] = useState(false);
87
- const [error, setError] = useState<string | null>(null);
88
- const [selectedLocationId, setSelectedLocationId] = useState<string | null>(
89
- booking.pickupLocationId ?? null
90
- );
91
- const [selectedLocationName, setSelectedLocationName] = useState<string | null>(null);
92
- const [selectedCustomAddress, setSelectedCustomAddress] = useState<string | null>(
93
- booking.travelerHotel ?? null
94
- );
95
- const [saving, setSaving] = useState(false);
96
- const [previewItinerary, setPreviewItinerary] = useState<ItineraryDisplayStep[] | null>(null);
97
-
98
- const searchParams = useSearchParams();
99
- const refFromUrl = searchParams.get('ref') || searchParams.get('bookingReference') || '';
100
- const refFromBooking = formatBookingRefForDisplay(booking.bookingReference) || booking.bookingReference || '';
101
- const effectiveBookingRef = (refFromUrl.trim() || refFromBooking).trim();
102
-
103
- const companyId = booking.companyId ?? '';
104
- const lastName = booking.customer?.lastName ?? '';
105
- const isPrivateShuttle = booking.productType === 'PRIVATE_SHUTTLE';
106
-
107
- const product = useMemo(
108
- () => findProductWithPickupLocations(products, booking.productId),
109
- [products, booking.productId]
110
- );
111
- const pickupLocations = product?.pickupLocations ?? [];
112
- const destinations = product?.destinations ?? [];
113
-
114
- const loadProducts = useCallback(async () => {
115
- if (!companyId?.trim()) {
116
- setError('Missing company information');
117
- return;
118
- }
119
- setLoading(true);
120
- setError(null);
121
- try {
122
- const data = await fetchProducts(companyId);
123
- setProducts(data);
124
- } catch (e) {
125
- setError(e instanceof Error ? e.message : 'Failed to load pickup locations');
126
- } finally {
127
- setLoading(false);
128
- }
129
- }, [companyId]);
130
-
131
- useEffect(() => {
132
- if (isOpen && companyId) loadProducts();
133
- }, [isOpen, companyId, loadProducts]);
134
-
135
- useEffect(() => {
136
- if (isOpen) {
137
- setSelectedLocationId(booking.pickupLocationId ?? null);
138
- setSelectedLocationName(null);
139
- setSelectedCustomAddress(booking.travelerHotel ?? null);
140
- setPreviewItinerary(null);
141
- }
142
- }, [isOpen, booking.pickupLocationId, booking.travelerHotel]);
143
-
144
- const selectedPickupName = useMemo(() => {
145
- if (selectedCustomAddress) return selectedCustomAddress;
146
- if (selectedLocationId) {
147
- return selectedLocationName ?? pickupLocations.find((l) => l.id === selectedLocationId)?.name ?? selectedLocationId;
148
- }
149
- return null;
150
- }, [selectedLocationId, selectedLocationName, selectedCustomAddress, pickupLocations]);
151
-
152
- const hasValidSelection = selectedLocationId || selectedCustomAddress;
153
- const hasChanged =
154
- selectedLocationId !== (booking.pickupLocationId ?? null) ||
155
- selectedCustomAddress !== (booking.travelerHotel ?? null);
156
-
157
- // Fetch preview itinerary when user selects a new pickup (to show new times)
158
- useEffect(() => {
159
- if (!isOpen || !hasValidSelection || !hasChanged || !lastName?.trim()) {
160
- setPreviewItinerary(null);
161
- return;
162
- }
163
- let cancelled = false;
164
- const payload = selectedCustomAddress
165
- ? { travelerHotel: selectedCustomAddress }
166
- : selectedLocationId
167
- ? { pickupLocationId: selectedLocationId }
168
- : null;
169
- if (!payload) return;
170
- previewPickupLocationChange(effectiveBookingRef, lastName, payload)
171
- .then((data) => {
172
- if (!cancelled && data?.itineraryDisplay) {
173
- setPreviewItinerary(data.itineraryDisplay as ItineraryDisplayStep[]);
174
- } else {
175
- setPreviewItinerary(null);
176
- }
177
- })
178
- .catch(() => setPreviewItinerary(null));
179
- return () => { cancelled = true; };
180
- }, [isOpen, hasValidSelection, hasChanged, selectedLocationId, selectedCustomAddress, effectiveBookingRef, lastName]);
181
-
182
- const currentPickupName =
183
- booking.travelerHotel ||
184
- (booking.pickupLocationId
185
- ? pickupLocations.find((l) => l.id === booking.pickupLocationId)?.name ?? booking.pickupLocationId
186
- : null);
187
-
188
- const itineraryDisplay = booking.itineraryDisplay ?? [];
189
- const pickupOrDropOffSteps = itineraryDisplay.filter(
190
- (s) => s.stepType === 'pickup' || s.stepType === 'drop_off'
191
- );
192
-
193
- const handleSave = async () => {
194
- if (!lastName?.trim()) {
195
- setError('Last name is required to update pickup');
196
- return;
197
- }
198
- if (!hasValidSelection) {
199
- setError('Please select a pickup location');
200
- return;
201
- }
202
-
203
- setSaving(true);
204
- setError(null);
205
- try {
206
- let payload: { pickupLocationId?: string; travelerHotel?: string };
207
- if (selectedCustomAddress) {
208
- payload = { travelerHotel: selectedCustomAddress };
209
- } else if (selectedLocationId) {
210
- payload = { pickupLocationId: selectedLocationId, travelerHotel: selectedLocationName ?? undefined };
211
- } else {
212
- setError('Please select a pickup location');
213
- setSaving(false);
214
- return;
215
- }
216
-
217
- const updated = await updatePickupLocation(
218
- effectiveBookingRef,
219
- lastName,
220
- payload
221
- );
222
- onSuccess(updated as BookingData);
223
- onClose();
224
- } catch (e) {
225
- setError(e instanceof Error ? e.message : 'Failed to update pickup location');
226
- } finally {
227
- setSaving(false);
228
- }
229
- };
230
-
231
- if (!isOpen) return null;
232
-
233
- return (
234
- <div
235
- className={styles.overlay}
236
- onClick={onClose}
237
- role="dialog"
238
- aria-modal="true"
239
- aria-labelledby="pickup-dialog-title"
240
- >
241
- <div className={`${styles.modal} ${styles.pickupDialogRoot}`} onClick={(e) => e.stopPropagation()}>
242
- <div className={styles.header} style={{ display: 'grid', gridTemplateColumns: 'auto 1fr auto', alignItems: 'center' }}>
243
- <span className={styles.headerSpacer} aria-hidden />
244
- <h2 id="pickup-dialog-title" className={styles.title} style={{ textAlign: 'center', justifySelf: 'center' }}>
245
- {t('pickup.title') || 'Select pickup location'}
246
- </h2>
247
- <button type="button" onClick={onClose} className={styles.closeBtn} aria-label="Close">
248
- <svg width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
249
- <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
250
- </svg>
251
- </button>
252
- </div>
253
-
254
- <div className={styles.content}>
255
- {loading ? (
256
- <div className={styles.loading}>Loading pickup locations…</div>
257
- ) : error && !pickupLocations.length ? (
258
- <div className={styles.error}>{error}</div>
259
- ) : pickupLocations.length === 0 ? (
260
- <div className={styles.empty}>No pickup locations available for this tour.</div>
261
- ) : (
262
- <>
263
- <div className={styles.currentPickupSection}>
264
- <p className={styles.currentPickupLabel}>Current pickup</p>
265
- <p className={styles.currentPickupName}>
266
- {currentPickupName ?? (t('booking.pickupLocationUnknown') || "I don't know")}
267
- </p>
268
- {(booking.pickupLocationId || booking.travelerHotel) && (
269
- <p className={styles.currentPickupTimes}>
270
- Pickup: {getPickupTimeFromItinerary(itineraryDisplay)} · Drop-off: {getDropoffTimeFromItinerary(itineraryDisplay)}
271
- </p>
272
- )}
273
- </div>
274
- <div
275
- className={`${styles.selectorWrapper} ${styles.selectorWrapperConstrained}`}
276
- style={{ fontFamily: "'Figtree', sans-serif" }}
277
- >
278
- <PickupLocationSelector
279
- hideTitle
280
- hideSkipOption
281
- pickupLocations={pickupLocations}
282
- selectedLocationId={selectedLocationId}
283
- selectedCustomAddress={selectedCustomAddress}
284
- allowCustomLocation={isPrivateShuttle}
285
- restrictCustomLocationToServiceArea={isPrivateShuttle}
286
- destinations={destinations}
287
- onLocationSelect={(locationId, customLocation, locationName) => {
288
- setError('');
289
- if (customLocation) {
290
- setSelectedLocationId(null);
291
- setSelectedLocationName(null);
292
- setSelectedCustomAddress(customLocation.address);
293
- } else {
294
- setSelectedLocationId(locationId);
295
- setSelectedLocationName(locationName ?? null);
296
- setSelectedCustomAddress(null);
297
- }
298
- }}
299
- />
300
- </div>
301
-
302
- {hasValidSelection && hasChanged && pickupOrDropOffSteps.length > 0 && (
303
- <div className={styles.itineraryPreview}>
304
- <h3 className={styles.previewTitle}>Itinerary changes</h3>
305
- <p className={styles.previewIntro}>
306
- Your itinerary will be updated with the new pickup location:
307
- </p>
308
- <ul className={styles.previewList}>
309
- {pickupOrDropOffSteps.map((step, i) => {
310
- const previewStep = previewItinerary?.find(s => s.stepType === step.stepType);
311
- const newTime = previewStep?.time?.trim();
312
- const newLabel =
313
- step.stepType === 'pickup'
314
- ? selectedPickupName
315
- ? `Pickup at ${selectedPickupName}${newTime ? ` ${newTime}` : ''}`
316
- : t('booking.pickupLocationUnknown') || "I don't know"
317
- : step.stepType === 'drop_off'
318
- ? selectedPickupName
319
- ? `Drop off at ${selectedPickupName}${newTime ? ` ${newTime}` : ''}`
320
- : t('booking.pickupLocationUnknown') || "I don't know"
321
- : getItineraryStepLabel(previewStep ?? step, true);
322
- return (
323
- <li key={i} className={styles.previewItem}>
324
- <span className={styles.previewOld}>
325
- {getItineraryStepLabel(step, true)}
326
- </span>
327
- <span className={styles.previewArrow}>→</span>
328
- <span className={styles.previewNew}>{newLabel}</span>
329
- </li>
330
- );
331
- })}
332
- </ul>
333
- </div>
334
- )}
335
-
336
- {error && <div className={styles.error}>{error}</div>}
337
-
338
- <div className={styles.footer}>
339
- <Button variant="outline" className={styles.cancelBtn} onClick={onClose}>
340
- Cancel
341
- </Button>
342
- <Button
343
- variant="primary"
344
- hoverColor={ButtonHoverColor.Turquoise}
345
- onClick={handleSave}
346
- disabled={!hasValidSelection || !hasChanged || saving}
347
- >
348
- {saving ? 'Saving…' : 'Save pickup location'}
349
- </Button>
350
- </div>
351
- </>
352
- )}
353
- </div>
354
- </div>
355
- </div>
356
- );
357
- }
@@ -1,110 +0,0 @@
1
- 'use client';
2
-
3
- import React, { useCallback } from 'react';
4
- import { useJsApiLoader, GoogleMap, Marker, InfoWindow } from '@react-google-maps/api';
5
- import { calculateMapCenter, getMapOptions, calculateMapBounds } from '@/lib/pickup/map-utils';
6
- import { createPinMarkerIcon } from '@/lib/pickup/marker-icons';
7
- import { ENV } from '@/lib/env';
8
- import type { PickupLocation } from '@/lib/booking-api';
9
- import styles from './PickupLocationDialog.module.css';
10
-
11
- const libraries: ('places')[] = ['places'];
12
- const MARKER_COLOR = '#dc2626';
13
- const MARKER_HOVER = '#1e3a8a';
14
-
15
- interface PickupLocationMapProps {
16
- pickupLocations: PickupLocation[];
17
- selectedLocationId: string | null;
18
- onSelectLocation: (id: string) => void;
19
- onMarkerClick?: (id: string) => void;
20
- saving: boolean;
21
- }
22
-
23
- export default function PickupLocationMap({
24
- pickupLocations,
25
- selectedLocationId,
26
- onSelectLocation,
27
- onMarkerClick,
28
- saving,
29
- }: PickupLocationMapProps) {
30
- const [hoveredMarker, setHoveredMarker] = React.useState<string | null>(null);
31
- const [selectedMarker, setSelectedMarker] = React.useState<string | null>(null);
32
-
33
- const { isLoaded, loadError } = useJsApiLoader({
34
- id: 'google-map-pickup',
35
- googleMapsApiKey: ENV.GOOGLE_MAPS_API_KEY,
36
- libraries,
37
- });
38
-
39
- const locationsWithCoords = pickupLocations.filter((l) => l.coordinates);
40
- const mapCenter = calculateMapCenter(locationsWithCoords);
41
- const mapOptions = getMapOptions();
42
- const mapBounds = calculateMapBounds(locationsWithCoords);
43
-
44
- const onMapLoad = useCallback(
45
- (map: google.maps.Map) => {
46
- if (mapBounds) {
47
- map.fitBounds(mapBounds, { top: 50, right: 50, bottom: 50, left: 50 });
48
- }
49
- },
50
- [mapBounds]
51
- );
52
-
53
- if (!isLoaded || loadError || locationsWithCoords.length === 0) {
54
- return null;
55
- }
56
-
57
- return (
58
- <div className={styles.mapContainer}>
59
- <GoogleMap
60
- mapContainerClassName={styles.map}
61
- center={mapCenter}
62
- zoom={10}
63
- options={mapOptions}
64
- onLoad={onMapLoad}
65
- >
66
- {locationsWithCoords.map((loc) => {
67
- if (!loc.coordinates) return null;
68
- const isHovered = hoveredMarker === loc.id;
69
- const isSelected = selectedLocationId === loc.id;
70
- return (
71
- <Marker
72
- key={loc.id}
73
- position={loc.coordinates}
74
- title={loc.name}
75
- zIndex={isHovered || isSelected ? 200 : 100}
76
- icon={{
77
- url: createPinMarkerIcon(isHovered || isSelected ? MARKER_HOVER : MARKER_COLOR),
78
- scaledSize: new google.maps.Size(32, 40),
79
- anchor: new google.maps.Point(16, 40),
80
- }}
81
- onMouseOver={() => setHoveredMarker(loc.id)}
82
- onMouseOut={() => setHoveredMarker(null)}
83
- onClick={() => {
84
- setSelectedMarker(loc.id);
85
- onMarkerClick?.(loc.id);
86
- }}
87
- >
88
- {selectedMarker === loc.id && (
89
- <InfoWindow onCloseClick={() => setSelectedMarker(null)}>
90
- <div className={styles.infoWindow}>
91
- <h3 className={styles.infoTitle}>{loc.name}</h3>
92
- <p className={styles.infoAddress}>{loc.address}</p>
93
- <button
94
- type="button"
95
- onClick={() => onSelectLocation(loc.id)}
96
- className={styles.selectBtn}
97
- disabled={saving}
98
- >
99
- {saving ? 'Saving…' : 'Select this location'}
100
- </button>
101
- </div>
102
- </InfoWindow>
103
- )}
104
- </Marker>
105
- );
106
- })}
107
- </GoogleMap>
108
- </div>
109
- );
110
- }
@@ -1,174 +0,0 @@
1
- /* Compact photo-session-style cards (aligned with photo-products-section.module.css) */
2
-
3
- .section {
4
- background-color: var(--primary-background);
5
- border-radius: 24px;
6
- padding: var(--spacing-large);
7
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
8
- border: 1px solid rgba(0, 0, 0, 0.06);
9
- }
10
-
11
- .header {
12
- margin-bottom: var(--spacing-medium);
13
- }
14
-
15
- .title {
16
- font-family: 'Poppins', sans-serif;
17
- font-weight: 700;
18
- font-size: 1.25rem;
19
- color: var(--primary-text);
20
- margin: 0 0 0.5rem 0;
21
- line-height: 1.3;
22
- }
23
-
24
- .subtitle {
25
- margin: 0;
26
- font-size: 0.9375rem;
27
- line-height: 1.5;
28
- color: var(--grey-text);
29
- }
30
-
31
- .cardGrid {
32
- display: grid;
33
- grid-template-columns: repeat(auto-fill, minmax(158px, 1fr));
34
- gap: 0.875rem;
35
- }
36
-
37
- @media (min-width: 480px) {
38
- .cardGrid {
39
- grid-template-columns: repeat(3, 1fr);
40
- gap: 1rem;
41
- }
42
- }
43
-
44
- .card {
45
- height: 100%;
46
- width: 100%;
47
- display: block;
48
- position: relative;
49
- cursor: pointer;
50
- border: none;
51
- background: transparent;
52
- padding: 0;
53
- font: inherit;
54
- color: inherit;
55
- text-align: inherit;
56
- border-radius: 10px;
57
- overflow: hidden;
58
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
59
- transition: box-shadow 0.35s ease, transform 0.35s ease;
60
- }
61
-
62
- .card:focus-visible {
63
- outline: 2px solid var(--accent-turquoise, #0d9488);
64
- outline-offset: 2px;
65
- }
66
-
67
- .card:hover {
68
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
69
- transform: translateY(-2px);
70
- }
71
-
72
- .imageWrap {
73
- width: 100%;
74
- height: 200px;
75
- position: relative;
76
- border-radius: 10px;
77
- overflow: hidden;
78
- aspect-ratio: 16 / 9;
79
- }
80
-
81
- .cardImage {
82
- width: 100%;
83
- height: 100%;
84
- position: absolute;
85
- top: 0;
86
- left: 0;
87
- transition: transform 0.5s ease;
88
- will-change: transform;
89
- }
90
-
91
- .cardImage::after {
92
- content: '';
93
- position: absolute;
94
- bottom: 0;
95
- left: 0;
96
- right: 0;
97
- height: 30%;
98
- background: linear-gradient(to top, rgba(0, 0, 0, 0.55), transparent);
99
- z-index: 1;
100
- }
101
-
102
- @media (min-width: 1024px) {
103
- .card:hover .cardImage {
104
- transform: scale(1.08);
105
- }
106
- }
107
-
108
- @media (prefers-reduced-motion: reduce) {
109
- .card {
110
- transition: none;
111
- }
112
-
113
- .card:hover {
114
- transform: none;
115
- }
116
-
117
- .cardImage {
118
- transition: none;
119
- }
120
-
121
- .card:hover .cardImage {
122
- transform: none;
123
- }
124
- }
125
-
126
- .cardTitle {
127
- position: absolute;
128
- top: 0;
129
- left: 0;
130
- right: 0;
131
- padding: 0.5rem 0.45rem;
132
- color: white;
133
- text-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
134
- font-family: 'Poppins', sans-serif;
135
- text-transform: lowercase;
136
- font-size: 0.95rem;
137
- font-weight: 700;
138
- z-index: 2;
139
- line-height: 1.15;
140
- text-align: center;
141
- }
142
-
143
- @media (min-width: 480px) {
144
- .cardTitle {
145
- font-size: 1rem;
146
- }
147
- }
148
-
149
- .cardInfo {
150
- position: absolute;
151
- bottom: 0;
152
- left: 0;
153
- z-index: 2;
154
- padding: 0.45rem 0.5rem;
155
- }
156
-
157
- .infoLine {
158
- color: var(--accent-white, #fff);
159
- font-family: 'Figtree', sans-serif;
160
- font-weight: 700;
161
- font-size: 0.7rem;
162
- line-height: 1.15;
163
- margin: 0 0 0.2rem 0;
164
- }
165
-
166
- .infoLine:last-child {
167
- margin-bottom: 0;
168
- }
169
-
170
- @media (min-width: 480px) {
171
- .infoLine {
172
- font-size: 0.75rem;
173
- }
174
- }