@ticketboothapp/booking 0.1.3 → 0.1.7

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 (45) hide show
  1. package/package.json +21 -1
  2. package/src/components/BookingDetails.tsx +546 -0
  3. package/src/components/BookingFlow.tsx +2952 -0
  4. package/src/components/BookingWidget.tsx +349 -0
  5. package/src/components/Calendar.tsx +906 -0
  6. package/src/components/CheckoutModal.tsx +294 -0
  7. package/src/components/CurrencySwitcher.tsx +81 -0
  8. package/src/components/ErrorBoundary.tsx +63 -0
  9. package/src/components/ItineraryBuilder.tsx +83 -0
  10. package/src/components/LanguageSwitcher.tsx +30 -0
  11. package/src/components/ManageBookingView.tsx +409 -0
  12. package/src/components/MealDrinkAddOnSelector.tsx +330 -0
  13. package/src/components/PickupLocationSelector.tsx +1541 -0
  14. package/src/components/PriceBreakdown.tsx +154 -0
  15. package/src/components/PriceSummary.tsx +211 -0
  16. package/src/components/PrivateShuttleBookingFlow.tsx +2290 -0
  17. package/src/components/ProductList.tsx +78 -0
  18. package/src/components/TermsAcceptance.tsx +110 -0
  19. package/src/components/WhatsAppPhoneInput.tsx +224 -0
  20. package/src/components/index.ts +31 -0
  21. package/src/contexts/CompanyContext.tsx +8 -20
  22. package/src/index.ts +16 -0
  23. package/src/lib/api.ts +801 -0
  24. package/src/lib/booking-ref.ts +13 -0
  25. package/src/lib/checkout-breakdown.test.ts +70 -0
  26. package/src/lib/checkout-breakdown.ts +69 -0
  27. package/src/lib/constants.ts +17 -0
  28. package/src/lib/currency.ts +88 -0
  29. package/src/lib/env.ts +10 -12
  30. package/src/lib/i18n/config.ts +21 -0
  31. package/src/lib/i18n/index.tsx +144 -0
  32. package/src/lib/i18n/messages/en.json +192 -0
  33. package/src/lib/i18n/messages/fr.json +192 -0
  34. package/src/lib/itinerary-labels.ts +70 -0
  35. package/src/lib/location-calculations.ts +43 -0
  36. package/src/lib/location-utils.ts +139 -0
  37. package/src/lib/map-utils.ts +153 -0
  38. package/src/lib/marker-icons.ts +113 -0
  39. package/src/lib/pickup-location-types.ts +25 -0
  40. package/src/lib/places-api.ts +154 -0
  41. package/src/lib/pricing.ts +466 -0
  42. package/src/lib/theme.ts +83 -0
  43. package/src/lib/utils.ts +9 -0
  44. package/src/types/google-maps.d.ts +2 -0
  45. package/tsconfig.json +8 -2
@@ -0,0 +1,294 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { loadStripe } from '@stripe/stripe-js';
5
+ import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
6
+ import { ENV } from '@/lib/env';
7
+ import { formatCurrencyAmount } from '@/lib/currency';
8
+ import { PriceSummary, type PriceSummaryLine } from './PriceSummary';
9
+ import type { Currency } from './CurrencySwitcher';
10
+ import type { Locale } from '@/lib/i18n/config';
11
+ import type { OrderSummaryTicketLine, OrderSummaryFeeLine, PriceBreakdown as PriceBreakdownType } from '@/lib/pricing';
12
+
13
+ export interface CheckoutModalLineItem {
14
+ line: OrderSummaryTicketLine;
15
+ breakdown: PriceBreakdownType | null;
16
+ }
17
+
18
+ export interface CheckoutModalProps {
19
+ open: boolean;
20
+ onClose: () => void;
21
+ clientSecret: string;
22
+ reservationReference: string;
23
+ /** Last name for Stripe return_url so success page can poll and redirect to /manage */
24
+ customerLastName?: string;
25
+ /** When set (e.g. provider-dashboard), used as Stripe return_url instead of /manage */
26
+ successUrlOverride?: string;
27
+ ticketLines: CheckoutModalLineItem[];
28
+ feeLineItems: OrderSummaryFeeLine[];
29
+ returnPriceAdjustment: number;
30
+ cancellationPolicyFee?: number;
31
+ cancellationPolicyLabel?: string;
32
+ subtotal: number;
33
+ tax: number;
34
+ total: number;
35
+ promoDiscountAmount?: number;
36
+ discountLabel?: string | null;
37
+ totalQuantity: number;
38
+ isTaxIncludedInPrice: boolean;
39
+ taxRate?: number;
40
+ currency: Currency;
41
+ locale: Locale;
42
+ t: (key: string, params?: Record<string, string | number>) => string;
43
+ /** When true, show deposit messaging (balance charged N days before or pay earlier). */
44
+ isDepositPayment?: boolean;
45
+ /** Days before booking to charge balance (e.g. 7). */
46
+ balanceChargeDaysBefore?: number;
47
+ }
48
+
49
+ const stripePromise = ENV.STRIPE_PUBLISHABLE_KEY
50
+ ? loadStripe(ENV.STRIPE_PUBLISHABLE_KEY)
51
+ : null;
52
+
53
+ function CheckoutForm({
54
+ successUrl,
55
+ onClose,
56
+ t,
57
+ total,
58
+ currency,
59
+ locale,
60
+ }: {
61
+ successUrl: string;
62
+ onClose: () => void;
63
+ t: (key: string) => string;
64
+ total: number;
65
+ currency: Currency;
66
+ locale: Locale;
67
+ }) {
68
+ const stripe = useStripe();
69
+ const elements = useElements();
70
+ const [loading, setLoading] = useState(false);
71
+ const [error, setError] = useState<string | null>(null);
72
+
73
+ const handleSubmit = async (e: React.FormEvent) => {
74
+ e.preventDefault();
75
+ if (!stripe || !elements) return;
76
+ setLoading(true);
77
+ setError(null);
78
+ const { error: submitError } = await elements.submit();
79
+ if (submitError) {
80
+ setError(submitError.message ?? 'Validation failed');
81
+ setLoading(false);
82
+ return;
83
+ }
84
+ const { error: confirmError } = await stripe.confirmPayment({
85
+ elements,
86
+ confirmParams: {
87
+ return_url: successUrl,
88
+ },
89
+ });
90
+ if (confirmError) {
91
+ setError(confirmError.message ?? 'Payment failed');
92
+ }
93
+ setLoading(false);
94
+ };
95
+
96
+ return (
97
+ <form onSubmit={handleSubmit} className="space-y-4">
98
+ <PaymentElement />
99
+ {error && (
100
+ <p className="text-sm text-red-600" role="alert">
101
+ {error}
102
+ </p>
103
+ )}
104
+ <div className="flex gap-3 pt-2">
105
+ <button
106
+ type="button"
107
+ onClick={onClose}
108
+ className="flex-1 py-3 px-4 border border-stone-300 text-stone-700 rounded-lg hover:bg-stone-50"
109
+ >
110
+ {t('common.cancel') || 'Cancel'}
111
+ </button>
112
+ <button
113
+ type="submit"
114
+ disabled={!stripe || loading}
115
+ className="flex-1 py-3 px-4 bg-emerald-600 text-white font-semibold rounded-lg hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed"
116
+ >
117
+ {loading ? (t('booking.paying') || 'Paying...') : `${t('booking.payNow') || 'Pay now'} (${formatCurrencyAmount(total, currency, locale)})`}
118
+ </button>
119
+ </div>
120
+ </form>
121
+ );
122
+ }
123
+
124
+ export function CheckoutModal({
125
+ open,
126
+ onClose,
127
+ clientSecret,
128
+ reservationReference,
129
+ customerLastName,
130
+ successUrlOverride,
131
+ ticketLines,
132
+ feeLineItems,
133
+ returnPriceAdjustment,
134
+ cancellationPolicyFee = 0,
135
+ cancellationPolicyLabel,
136
+ subtotal,
137
+ tax,
138
+ total,
139
+ totalQuantity,
140
+ promoDiscountAmount = 0,
141
+ discountLabel,
142
+ isTaxIncludedInPrice,
143
+ taxRate = 0,
144
+ currency,
145
+ locale,
146
+ t,
147
+ isDepositPayment = false,
148
+ balanceChargeDaysBefore = 7,
149
+ }: CheckoutModalProps) {
150
+ const baseUrl = typeof window !== 'undefined' ? window.location.origin : '';
151
+ const manageParams = new URLSearchParams({ reservationRef: reservationReference });
152
+ if (customerLastName?.trim()) manageParams.set('lastName', customerLastName.trim());
153
+ const successUrl = successUrlOverride ?? `${baseUrl}/manage?${manageParams.toString()}`;
154
+
155
+ if (!open) return null;
156
+
157
+ if (!ENV.STRIPE_PUBLISHABLE_KEY) {
158
+ return (
159
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
160
+ <div className="bg-white rounded-xl shadow-xl max-w-md w-full p-6">
161
+ <h3 className="text-lg font-semibold text-stone-900 mb-2">
162
+ {t('booking.checkout') || 'Checkout'}
163
+ </h3>
164
+ <p className="text-stone-600 mb-4">
165
+ {t('booking.paymentNotConfigured') || 'Payment is not configured. Please use the standard checkout.'}
166
+ </p>
167
+ <button
168
+ type="button"
169
+ onClick={onClose}
170
+ className="w-full py-3 bg-stone-200 text-stone-800 font-medium rounded-lg hover:bg-stone-300"
171
+ >
172
+ {t('common.close') || 'Close'}
173
+ </button>
174
+ </div>
175
+ </div>
176
+ );
177
+ }
178
+
179
+ const options = {
180
+ clientSecret,
181
+ appearance: {
182
+ theme: 'stripe' as const,
183
+ variables: {
184
+ colorPrimary: '#059669',
185
+ borderRadius: '8px',
186
+ },
187
+ },
188
+ };
189
+
190
+ return (
191
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
192
+ <div className="bg-white rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-hidden flex flex-col">
193
+ <div className="p-6 border-b border-stone-200 flex-shrink-0">
194
+ <div className="flex justify-between items-start">
195
+ <h3 className="text-lg font-semibold text-stone-900">
196
+ {t('booking.reviewAndPay') || 'Review & pay'}
197
+ </h3>
198
+ <button
199
+ type="button"
200
+ onClick={onClose}
201
+ className="text-stone-400 hover:text-stone-600 p-1"
202
+ aria-label={t('common.close') || 'Close'}
203
+ >
204
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
205
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
206
+ </svg>
207
+ </button>
208
+ </div>
209
+
210
+ {/* Order summary — shared PriceSummary component */}
211
+ <div className="mt-4 min-w-0">
212
+ <PriceSummary
213
+ lines={[
214
+ ...ticketLines.map(({ line, breakdown }): PriceSummaryLine => ({
215
+ kind: 'ticket',
216
+ category: line.category,
217
+ qty: line.qty,
218
+ itemTotal: line.itemTotal,
219
+ breakdown,
220
+ })),
221
+ ...(returnPriceAdjustment !== 0
222
+ ? [
223
+ {
224
+ kind: 'line' as const,
225
+ label: `${t('booking.returnOption')} (${totalQuantity} ${totalQuantity === 1 ? 'person' : 'people'})`,
226
+ amount: returnPriceAdjustment,
227
+ type: 'return',
228
+ },
229
+ ]
230
+ : []),
231
+ ...(cancellationPolicyFee > 0
232
+ ? [
233
+ {
234
+ kind: 'line' as const,
235
+ label: cancellationPolicyLabel ?? t('booking.flexibleCancellation') ?? 'Flexible cancellation',
236
+ amount: cancellationPolicyFee,
237
+ type: 'cancellation',
238
+ },
239
+ ]
240
+ : []),
241
+ ...feeLineItems.map((fee) => ({
242
+ kind: 'line' as const,
243
+ label: `${fee.name} (${totalQuantity} ${totalQuantity === 1 ? 'person' : 'people'})`,
244
+ amount: fee.totalAmount,
245
+ type: 'fee',
246
+ })),
247
+ ]}
248
+ total={total}
249
+ currency={currency}
250
+ locale={locale}
251
+ subtotal={subtotal !== total ? subtotal : undefined}
252
+ discountAmount={promoDiscountAmount ?? 0}
253
+ discountLabel={discountLabel}
254
+ taxAmount={!isTaxIncludedInPrice && tax > 0 ? tax : 0}
255
+ taxRate={taxRate}
256
+ size="sm"
257
+ t={t}
258
+ depositMode={isDepositPayment && subtotal > total ? { totalLabel: t('booking.deposit') || 'Deposit', balanceAmount: subtotal - total } : undefined}
259
+ hideSubtotal={isDepositPayment}
260
+ />
261
+ </div>
262
+
263
+ {/* Deposit payment notice */}
264
+ {isDepositPayment && (
265
+ <div className="mt-4 p-3 bg-amber-50 border border-amber-200 rounded-lg text-sm text-amber-800">
266
+ <p className="font-medium">
267
+ {(t('booking.depositPaymentNotice') !== 'booking.depositPaymentNotice' ? t('booking.depositPaymentNotice') : null) ?? "You're paying the deposit today."}
268
+ </p>
269
+ <p className="mt-1 text-amber-700">
270
+ {balanceChargeDaysBefore > 0
271
+ ? (t('booking.balanceChargeNotice', { days: balanceChargeDaysBefore }) !== 'booking.balanceChargeNotice'
272
+ ? t('booking.balanceChargeNotice', { days: balanceChargeDaysBefore })
273
+ : `The remaining balance will be charged ${balanceChargeDaysBefore} days before your booking. You can also pay it earlier from your Manage Booking page.`)
274
+ : (t('booking.balancePayEarlier') !== 'booking.balancePayEarlier'
275
+ ? t('booking.balancePayEarlier')
276
+ : 'You can pay the remaining balance anytime from your Manage Booking page.')}
277
+ </p>
278
+ </div>
279
+ )}
280
+ </div>
281
+
282
+ <div className="p-6 overflow-y-auto flex-1">
283
+ {stripePromise && clientSecret ? (
284
+ <Elements stripe={stripePromise} options={options}>
285
+ <CheckoutForm successUrl={successUrl} onClose={onClose} t={t} total={total} currency={currency} locale={locale} />
286
+ </Elements>
287
+ ) : (
288
+ <p className="text-stone-500">{t('booking.loadingPayment') || 'Loading payment form...'}</p>
289
+ )}
290
+ </div>
291
+ </div>
292
+ </div>
293
+ );
294
+ }
@@ -0,0 +1,81 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+
5
+ // Available currencies from pricing.json config
6
+ export const CURRENCIES = ['CAD', 'USD', 'EUR', 'GBP', 'AUD'] as const;
7
+ export type Currency = typeof CURRENCIES[number];
8
+
9
+ const CURRENCY_NAMES: Record<Currency, string> = {
10
+ CAD: 'CAD',
11
+ USD: 'USD',
12
+ EUR: 'EUR',
13
+ GBP: 'GBP',
14
+ AUD: 'AUD',
15
+ };
16
+
17
+ const DEFAULT_CURRENCY: Currency = 'CAD';
18
+
19
+ export function useCurrency() {
20
+ const [currency, setCurrencyState] = useState<Currency>(DEFAULT_CURRENCY);
21
+
22
+ // Load from localStorage after hydration to avoid SSR mismatch
23
+ useEffect(() => {
24
+ try {
25
+ const saved = localStorage.getItem('booking-currency') as Currency | null;
26
+ if (saved && CURRENCIES.includes(saved)) {
27
+ setCurrencyState(saved);
28
+ }
29
+ } catch (error) {
30
+ console.warn('Failed to read currency from localStorage:', error);
31
+ }
32
+ }, []);
33
+
34
+ const setCurrency = (newCurrency: Currency) => {
35
+ if (!CURRENCIES.includes(newCurrency)) {
36
+ console.warn(`Invalid currency: ${newCurrency}. Falling back to ${DEFAULT_CURRENCY}`);
37
+ setCurrencyState(DEFAULT_CURRENCY);
38
+ return;
39
+ }
40
+
41
+ setCurrencyState(newCurrency);
42
+ if (typeof window !== 'undefined') {
43
+ try {
44
+ localStorage.setItem('booking-currency', newCurrency);
45
+ } catch (error) {
46
+ console.warn('Failed to save currency to localStorage:', error);
47
+ }
48
+ }
49
+ };
50
+
51
+ return { currency, setCurrency };
52
+ }
53
+
54
+ interface CurrencySwitcherProps {
55
+ currency: Currency;
56
+ onCurrencyChange: (currency: Currency) => void;
57
+ }
58
+
59
+ export function CurrencySwitcher({ currency, onCurrencyChange }: CurrencySwitcherProps) {
60
+ return (
61
+ <div className="flex items-center gap-1.5 sm:gap-2">
62
+ <span className="text-xs sm:text-sm shrink-0" style={{ color: 'var(--booking-text-muted, #a8a29e)' }}>Currency:</span>
63
+ <div className="flex flex-wrap gap-0.5 sm:gap-1 rounded-lg overflow-hidden" style={{ border: '1px solid var(--booking-border)', backgroundColor: 'var(--booking-header-bg, #1c1917)' }}>
64
+ {CURRENCIES.map((curr) => (
65
+ <button
66
+ key={curr}
67
+ onClick={() => onCurrencyChange(curr)}
68
+ className={`px-2 py-1 sm:px-3 sm:py-1.5 text-xs sm:text-sm font-medium transition-colors ${currency !== curr ? 'hover:opacity-80' : ''}`}
69
+ style={currency === curr
70
+ ? { backgroundColor: 'var(--booking-primary)', color: 'white' }
71
+ : { backgroundColor: 'transparent', color: 'var(--booking-text-muted, #a8a29e)' }
72
+ }
73
+ >
74
+ {CURRENCY_NAMES[curr]}
75
+ </button>
76
+ ))}
77
+ </div>
78
+ </div>
79
+ );
80
+ }
81
+
@@ -0,0 +1,63 @@
1
+ 'use client';
2
+
3
+ import React, { Component, ErrorInfo, ReactNode } from 'react';
4
+
5
+ interface Props {
6
+ children: ReactNode;
7
+ fallback?: ReactNode;
8
+ onError?: (error: Error, errorInfo: ErrorInfo) => void;
9
+ }
10
+
11
+ interface State {
12
+ hasError: boolean;
13
+ error: Error | null;
14
+ }
15
+
16
+ /**
17
+ * Error Boundary component to catch and handle React component errors
18
+ * Prevents the entire app from crashing when a component throws an error
19
+ */
20
+ export class ErrorBoundary extends Component<Props, State> {
21
+ constructor(props: Props) {
22
+ super(props);
23
+ this.state = { hasError: false, error: null };
24
+ }
25
+
26
+ static getDerivedStateFromError(error: Error): State {
27
+ return { hasError: true, error };
28
+ }
29
+
30
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
31
+ console.error('ErrorBoundary caught an error:', error, errorInfo);
32
+ if (this.props.onError) {
33
+ this.props.onError(error, errorInfo);
34
+ }
35
+ }
36
+
37
+ render() {
38
+ if (this.state.hasError) {
39
+ if (this.props.fallback) {
40
+ return this.props.fallback;
41
+ }
42
+
43
+ return (
44
+ <div className="p-6 bg-red-50 border border-red-200 rounded-lg">
45
+ <h2 className="text-lg font-semibold text-red-900 mb-2">
46
+ Something went wrong
47
+ </h2>
48
+ <p className="text-red-700 mb-4">
49
+ {this.state.error?.message || 'An unexpected error occurred'}
50
+ </p>
51
+ <button
52
+ onClick={() => this.setState({ hasError: false, error: null })}
53
+ className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors"
54
+ >
55
+ Try again
56
+ </button>
57
+ </div>
58
+ );
59
+ }
60
+
61
+ return this.props.children;
62
+ }
63
+ }
@@ -0,0 +1,83 @@
1
+ 'use client';
2
+
3
+ import type { ItineraryBuilderDestination } from '@/lib/api';
4
+
5
+ interface ItineraryBuilderProps {
6
+ /** Shared destinations from product.itineraryBuilder */
7
+ destinations: ItineraryBuilderDestination[];
8
+ /** IDs blacklisted for this option (e.g. takkakaw_falls for Sunrise) */
9
+ optionBlacklist: string[];
10
+ /** Selected destination IDs (in order) */
11
+ selectedDestinationIds: string[];
12
+ /** Planning notes text */
13
+ planningNotes: string;
14
+ onDestinationsChange: (ids: string[]) => void;
15
+ onPlanningNotesChange: (value: string) => void;
16
+ }
17
+
18
+ export function ItineraryBuilder({
19
+ destinations,
20
+ optionBlacklist,
21
+ selectedDestinationIds,
22
+ planningNotes,
23
+ onDestinationsChange,
24
+ onPlanningNotesChange,
25
+ }: ItineraryBuilderProps) {
26
+ // Filter destinations by option blacklist
27
+ const availableDestinations = destinations.filter((d) => !optionBlacklist.includes(d.id));
28
+
29
+ const toggleDestination = (id: string) => {
30
+ if (selectedDestinationIds.includes(id)) {
31
+ onDestinationsChange(selectedDestinationIds.filter((d) => d !== id));
32
+ } else {
33
+ onDestinationsChange([...selectedDestinationIds, id]);
34
+ }
35
+ };
36
+
37
+ return (
38
+ <div className="space-y-6">
39
+ <div>
40
+ <label className="block text-sm font-medium text-stone-700 mb-2">
41
+ Where would you like to go?
42
+ </label>
43
+ <p className="text-sm text-stone-500 mb-3">
44
+ Select the destinations you&apos;d like to include. Our team will craft your final
45
+ itinerary.
46
+ </p>
47
+ <div className="flex flex-wrap gap-2">
48
+ {availableDestinations.map((dest) => {
49
+ const isSelected = selectedDestinationIds.includes(dest.id);
50
+ return (
51
+ <button
52
+ key={dest.id}
53
+ type="button"
54
+ onClick={() => toggleDestination(dest.id)}
55
+ className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
56
+ isSelected
57
+ ? 'bg-emerald-600 text-white'
58
+ : 'bg-stone-100 text-stone-700 hover:bg-stone-200'
59
+ }`}
60
+ >
61
+ {dest.label}
62
+ </button>
63
+ );
64
+ })}
65
+ </div>
66
+ </div>
67
+
68
+ {/* Planning notes */}
69
+ <div>
70
+ <label className="block text-sm font-medium text-stone-700 mb-2">
71
+ Planning notes (optional)
72
+ </label>
73
+ <textarea
74
+ value={planningNotes}
75
+ onChange={(e) => onPlanningNotesChange(e.target.value)}
76
+ placeholder="Any special requests, timing preferences, or things you'd like our team to know..."
77
+ rows={3}
78
+ className="w-full px-4 py-3 rounded-lg border border-stone-300 focus:outline-none focus:border-stone-500 text-stone-900 placeholder:text-stone-400"
79
+ />
80
+ </div>
81
+ </div>
82
+ );
83
+ }
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+
3
+ import { useLocale } from '@/lib/i18n';
4
+ import { locales, languageNames } from '@/lib/i18n/config';
5
+
6
+ export function LanguageSwitcher() {
7
+ const { locale, setLocale } = useLocale();
8
+
9
+ return (
10
+ <div className="flex items-center gap-1.5 sm:gap-2">
11
+ <span className="text-xs sm:text-sm shrink-0" style={{ color: 'var(--booking-text-muted, #a8a29e)' }}>Language:</span>
12
+ <div className="flex flex-wrap gap-0.5 sm:gap-1 rounded-lg overflow-hidden" style={{ border: '1px solid var(--booking-border)', backgroundColor: 'var(--booking-header-bg, #1c1917)' }}>
13
+ {locales.map((loc) => (
14
+ <button
15
+ key={loc}
16
+ onClick={() => setLocale(loc)}
17
+ className={`px-2 py-1 sm:px-3 sm:py-1.5 text-xs sm:text-sm font-medium transition-colors ${locale !== loc ? 'hover:opacity-80' : ''}`}
18
+ style={locale === loc
19
+ ? { backgroundColor: 'var(--booking-primary)', color: 'white' }
20
+ : { backgroundColor: 'transparent', color: 'var(--booking-text-muted, #a8a29e)' }
21
+ }
22
+ >
23
+ {languageNames[loc]}
24
+ </button>
25
+ ))}
26
+ </div>
27
+ </div>
28
+ );
29
+ }
30
+