@ticketboothapp/booking 0.1.19 → 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 (154) 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/InfoTooltip.tsx +2 -13
  8. package/src/components/booking/PickupLocationSelector.tsx +2 -2
  9. package/src/components/booking/PriceBreakdown.tsx +11 -34
  10. package/src/index.ts +3 -1
  11. package/src/app/photo-sessions/photo-packages.ts +0 -75
  12. package/src/assets/icons/minus.svg +0 -7
  13. package/src/assets/icons/partner-logos/getyourguide.svg +0 -8
  14. package/src/assets/icons/plus.svg +0 -3
  15. package/src/colours.css +0 -23
  16. package/src/components/BookingDetails.module.css +0 -1591
  17. package/src/components/BookingDetails.tsx +0 -2264
  18. package/src/components/JobApplicationDialog.module.css +0 -440
  19. package/src/components/JobApplicationDialog.tsx +0 -620
  20. package/src/components/PhoneInputWithCountry.module.css +0 -131
  21. package/src/components/PhoneInputWithCountry.tsx +0 -44
  22. package/src/components/PickupLocationDialog.module.css +0 -360
  23. package/src/components/PickupLocationDialog.tsx +0 -357
  24. package/src/components/PickupLocationMap.tsx +0 -110
  25. package/src/components/PostBookingDependentAddOnUpsell.module.css +0 -174
  26. package/src/components/PostBookingDependentAddOnUpsell.tsx +0 -407
  27. package/src/components/accordion.css +0 -27
  28. package/src/components/accordion.tsx +0 -29
  29. package/src/components/analytics/AnalyticsConsentRestore.tsx +0 -19
  30. package/src/components/analytics/AnalyticsScripts.tsx +0 -106
  31. package/src/components/analytics/CookieConsentBanner.css +0 -86
  32. package/src/components/analytics/CookieConsentBanner.tsx +0 -102
  33. package/src/components/bottom-sheet.module.css +0 -78
  34. package/src/components/bottom-sheet.tsx +0 -60
  35. package/src/components/breadcrumb.module.css +0 -40
  36. package/src/components/breadcrumb.tsx +0 -36
  37. package/src/components/button.css +0 -245
  38. package/src/components/button.tsx +0 -152
  39. package/src/components/client-bottom-sheet.tsx +0 -14
  40. package/src/components/colorable-svg.tsx +0 -29
  41. package/src/components/conditional-footer.tsx +0 -27
  42. package/src/components/contact-us.module.css +0 -147
  43. package/src/components/contact-us.tsx +0 -49
  44. package/src/components/email-signup.css +0 -151
  45. package/src/components/email-signup.tsx +0 -63
  46. package/src/components/faq-wrapper.module.css +0 -47
  47. package/src/components/faq-wrapper.tsx +0 -15
  48. package/src/components/footer.css +0 -187
  49. package/src/components/footer.tsx +0 -143
  50. package/src/components/global-simple-modal.tsx +0 -33
  51. package/src/components/google-review-summary.module.css +0 -77
  52. package/src/components/google-review-summary.tsx +0 -50
  53. package/src/components/hero-image.css +0 -13
  54. package/src/components/hero-image.tsx +0 -44
  55. package/src/components/image.css +0 -29
  56. package/src/components/image.tsx +0 -113
  57. package/src/components/language-aware-link.tsx +0 -72
  58. package/src/components/language-switcher.module.css +0 -124
  59. package/src/components/language-switcher.tsx +0 -75
  60. package/src/components/map-section.css +0 -59
  61. package/src/components/map-section.tsx +0 -63
  62. package/src/components/navbar.module.css +0 -152
  63. package/src/components/navbar.tsx +0 -125
  64. package/src/components/parallax-provider.tsx +0 -11
  65. package/src/components/product-tag.module.css +0 -30
  66. package/src/components/product-tag.tsx +0 -34
  67. package/src/components/product-theme-pages/best-option.module.css +0 -70
  68. package/src/components/product-theme-pages/best-option.tsx +0 -35
  69. package/src/components/product-theme-pages/extended-tour-options.module.css +0 -22
  70. package/src/components/product-theme-pages/extended-tour-options.tsx +0 -11
  71. package/src/components/product-theme-pages/image-modal.tsx +0 -248
  72. package/src/components/product-theme-pages/photo-gallery.module.css +0 -200
  73. package/src/components/product-theme-pages/photo-gallery.tsx +0 -90
  74. package/src/components/product-theme-pages/product-theme-page-layout.module.css +0 -13
  75. package/src/components/product-theme-pages/product-theme-page-layout.tsx +0 -67
  76. package/src/components/product-theme-pages/top-of-fold.module.css +0 -179
  77. package/src/components/product-theme-pages/top-of-fold.tsx +0 -80
  78. package/src/components/product-tile/image-only-product-tile-desktop.module.css +0 -106
  79. package/src/components/product-tile/image-only-product-tile-desktop.tsx +0 -56
  80. package/src/components/product-tile/image-only-product-tile-mobile.module.css +0 -122
  81. package/src/components/product-tile/image-only-product-tile-mobile.tsx +0 -89
  82. package/src/components/product-tile/image-only-product-tile.tsx +0 -44
  83. package/src/components/product-tile/product-tile-card.module.css +0 -84
  84. package/src/components/product-tile/product-tile-card.tsx +0 -61
  85. package/src/components/review-highlights-section.css +0 -85
  86. package/src/components/review-highlights-section.tsx +0 -127
  87. package/src/components/season-closure-overlay.module.css +0 -99
  88. package/src/components/season-closure-overlay.tsx +0 -98
  89. package/src/components/simple-modal.tsx +0 -69
  90. package/src/components/simple-top-of-fold.module.css +0 -76
  91. package/src/components/simple-top-of-fold.tsx +0 -34
  92. package/src/components/spacer.css +0 -41
  93. package/src/components/spacer.tsx +0 -23
  94. package/src/components/star-rating.module.css +0 -74
  95. package/src/components/star-rating.tsx +0 -48
  96. package/src/components/terms/TermsContent.tsx +0 -178
  97. package/src/components/title-subtitle.module.css +0 -10
  98. package/src/components/title-subtitle.tsx +0 -30
  99. package/src/components/translatable-reviews.tsx +0 -75
  100. package/src/components/value-pill.module.css +0 -59
  101. package/src/components/value-pill.tsx +0 -46
  102. package/src/components/value-props.css +0 -185
  103. package/src/components/value-props.tsx +0 -88
  104. package/src/constants/booking-guide-quiz.ts +0 -64
  105. package/src/constants/contact-info.ts +0 -2
  106. package/src/constants/faq.ts +0 -44
  107. package/src/constants/images.ts +0 -556
  108. package/src/constants/json-ld/faq-json-ld.tsx +0 -170
  109. package/src/constants/json-ld/homepage-json-ld.tsx +0 -138
  110. package/src/constants/json-ld/job-posting-json-ld.tsx +0 -92
  111. package/src/constants/json-ld/organization-json-ld.tsx +0 -62
  112. package/src/constants/json-ld/page-json-ld.tsx +0 -6
  113. package/src/constants/json-ld/product-json-ld.tsx +0 -154
  114. package/src/constants/json-ld/review-json-ld.tsx +0 -377
  115. package/src/constants/navigation-links/footer-links.ts +0 -48
  116. package/src/constants/navigation-links/nav-bar-links.ts +0 -41
  117. package/src/constants/navigation-links/navigation-link.ts +0 -6
  118. package/src/constants/pill-values.ts +0 -210
  119. package/src/constants/products.ts +0 -155
  120. package/src/constants/quiz-recommendations.ts +0 -506
  121. package/src/constants/reviews.ts +0 -75
  122. package/src/constants/staff.ts +0 -197
  123. package/src/constants/value-props.ts +0 -58
  124. package/src/data/dap-descriptions/session-couples-families-friends.en.json +0 -61
  125. package/src/data/dap-descriptions/session-elopements.en.json +0 -60
  126. package/src/data/dap-descriptions/session-proposals.en.json +0 -60
  127. package/src/data/product-descriptions/afternoon-delight.en.json +0 -35
  128. package/src/data/product-descriptions/emerald-lake-escape.en.json +0 -68
  129. package/src/data/product-descriptions/lake-louise-adventure.en.json +0 -74
  130. package/src/data/product-descriptions/moraine-lake-adventure.en.json +0 -78
  131. package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +0 -65
  132. package/src/data/product-descriptions/moraine-lake-sunrise.en.json +0 -64
  133. package/src/data/product-descriptions/private-tour.en.json +0 -80
  134. package/src/data/product-descriptions/two-lakes-combo.en.json +0 -65
  135. package/src/data/products-config.json +0 -101
  136. package/src/hooks/use-bottom-sheet.tsx +0 -15
  137. package/src/hooks/use-simple-modal.tsx +0 -27
  138. package/src/hooks/useBookingSourceMetadataFromLocation.ts +0 -21
  139. package/src/hooks/useEmailSubscription.tsx +0 -103
  140. package/src/hooks/useEmbeddedInIframe.ts +0 -16
  141. package/src/hooks/useIsBookingLaunchLive.ts +0 -49
  142. package/src/hooks/useQuiz.tsx +0 -210
  143. package/src/providers/bottom-sheet-provider.tsx +0 -40
  144. package/src/providers/dependent-add-on-dialog-provider.tsx +0 -105
  145. package/src/radius.css +0 -5
  146. package/src/spacing.css +0 -7
  147. package/src/strings/en.json +0 -1774
  148. package/src/strings/es.json +0 -1573
  149. package/src/strings/fr.json +0 -1573
  150. package/src/strings/index.js +0 -23
  151. package/src/text-style.css +0 -97
  152. package/src/types/fareharbor.d.ts +0 -12
  153. package/src/types/quiz.ts +0 -59
  154. 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
- }