@ticketboothapp/booking 0.1.4 → 0.1.8
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.
- package/package.json +21 -1
- package/src/components/BookingDetails.tsx +546 -0
- package/src/components/BookingFlow.tsx +2952 -0
- package/src/components/BookingWidget.tsx +7 -5
- package/src/components/Calendar.tsx +906 -0
- package/src/components/CheckoutModal.tsx +294 -0
- package/src/components/CurrencySwitcher.tsx +81 -0
- package/src/components/ErrorBoundary.tsx +63 -0
- package/src/components/ItineraryBuilder.tsx +83 -0
- package/src/components/LanguageSwitcher.tsx +30 -0
- package/src/components/ManageBookingView.tsx +4 -2
- package/src/components/MealDrinkAddOnSelector.tsx +330 -0
- package/src/components/PickupLocationSelector.tsx +1541 -0
- package/src/components/PriceBreakdown.tsx +154 -0
- package/src/components/PriceSummary.tsx +211 -0
- package/src/components/PrivateShuttleBookingFlow.tsx +2290 -0
- package/src/components/ProductList.tsx +78 -0
- package/src/components/TermsAcceptance.tsx +110 -0
- package/src/components/WhatsAppPhoneInput.tsx +224 -0
- package/src/components/index.ts +31 -0
- package/src/contexts/CompanyContext.tsx +8 -20
- package/src/index.ts +5 -0
- package/src/lib/api.ts +801 -0
- package/src/lib/booking-ref.ts +13 -0
- package/src/lib/checkout-breakdown.test.ts +70 -0
- package/src/lib/checkout-breakdown.ts +69 -0
- package/src/lib/constants.ts +17 -0
- package/src/lib/currency.ts +88 -0
- package/src/lib/env.ts +10 -12
- package/src/lib/i18n/config.ts +21 -0
- package/src/lib/i18n/index.tsx +144 -0
- package/src/lib/i18n/messages/en.json +192 -0
- package/src/lib/i18n/messages/fr.json +192 -0
- package/src/lib/itinerary-labels.ts +70 -0
- package/src/lib/location-calculations.ts +43 -0
- package/src/lib/location-utils.ts +139 -0
- package/src/lib/map-utils.ts +153 -0
- package/src/lib/marker-icons.ts +113 -0
- package/src/lib/pickup-location-types.ts +25 -0
- package/src/lib/places-api.ts +154 -0
- package/src/lib/pricing.ts +466 -0
- package/src/lib/theme.ts +83 -0
- package/src/lib/utils.ts +9 -0
- package/src/types/google-maps.d.ts +2 -0
- package/tsconfig.json +8 -2
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marker icon generation utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface MarkerIconOptions {
|
|
6
|
+
bgColor: string;
|
|
7
|
+
distanceStr?: string;
|
|
8
|
+
walkingTimeStr?: string;
|
|
9
|
+
label?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Escape text for safe use in SVG
|
|
14
|
+
*/
|
|
15
|
+
function escapeSvgText(text: string): string {
|
|
16
|
+
return text
|
|
17
|
+
.replace(/&/g, '&')
|
|
18
|
+
.replace(/</g, '<')
|
|
19
|
+
.replace(/>/g, '>')
|
|
20
|
+
.replace(/"/g, '"')
|
|
21
|
+
.replace(/'/g, ''');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validate and sanitize color value (hex color)
|
|
26
|
+
*/
|
|
27
|
+
function validateColor(color: string): string {
|
|
28
|
+
// Only allow hex colors (# followed by 3 or 6 hex digits)
|
|
29
|
+
const hexColorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
|
|
30
|
+
if (hexColorRegex.test(color)) {
|
|
31
|
+
return color;
|
|
32
|
+
}
|
|
33
|
+
// Default to a safe color if invalid
|
|
34
|
+
console.warn(`Invalid color provided: ${color}, using default`);
|
|
35
|
+
return '#dc2626';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Truncate text to fit within marker width
|
|
40
|
+
*/
|
|
41
|
+
function truncateText(text: string, maxLength: number = 20): string {
|
|
42
|
+
if (text.length <= maxLength) return text;
|
|
43
|
+
return text.slice(0, maxLength - 3) + '...';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate SVG icon for pickup location marker with distance/time
|
|
48
|
+
*/
|
|
49
|
+
export function createDistanceMarkerIcon(options: MarkerIconOptions): string {
|
|
50
|
+
const { bgColor, distanceStr, walkingTimeStr } = options;
|
|
51
|
+
const safeColor = validateColor(bgColor);
|
|
52
|
+
const text = distanceStr && walkingTimeStr
|
|
53
|
+
? truncateText(`${distanceStr} • ${walkingTimeStr}`, 20)
|
|
54
|
+
: '';
|
|
55
|
+
const escapedText = escapeSvgText(text);
|
|
56
|
+
|
|
57
|
+
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(`
|
|
58
|
+
<svg width="120" height="32" viewBox="0 0 120 32" xmlns="http://www.w3.org/2000/svg">
|
|
59
|
+
<rect x="0" y="0" width="120" height="24" rx="4" fill="${safeColor}"/>
|
|
60
|
+
<polygon points="55,24 60,32 65,24" fill="${safeColor}"/>
|
|
61
|
+
<text x="60" y="16" fill="white" font-size="10" font-weight="600" text-anchor="middle" font-family="Arial">${escapedText}</text>
|
|
62
|
+
</svg>
|
|
63
|
+
`)}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Generate SVG icon for simple pickup location pin
|
|
68
|
+
*/
|
|
69
|
+
export function createPinMarkerIcon(color: string): string {
|
|
70
|
+
const safeColor = validateColor(color);
|
|
71
|
+
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(`
|
|
72
|
+
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
73
|
+
<path d="M16 0C7.163 0 0 7.163 0 16C0 24.837 16 40 16 40C16 40 32 24.837 32 16C32 7.163 24.837 0 16 0Z" fill="#ffffff"/>
|
|
74
|
+
<path d="M16 2C8.268 2 2 8.268 2 16C2 22.177 16 38 16 38C16 38 30 22.177 30 16C30 8.268 23.732 2 16 2Z" fill="${safeColor}"/>
|
|
75
|
+
<circle cx="16" cy="16" r="6" fill="#ffffff"/>
|
|
76
|
+
</svg>
|
|
77
|
+
`)}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Generate SVG icon for user's searched location pin
|
|
82
|
+
*/
|
|
83
|
+
export function createSearchedLocationPinIcon(): string {
|
|
84
|
+
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(`
|
|
85
|
+
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
86
|
+
<path d="M16 0C7.163 0 0 7.163 0 16C0 24.837 16 40 16 40C16 40 32 24.837 32 16C32 7.163 24.837 0 16 0Z" fill="#ffffff"/>
|
|
87
|
+
<path d="M16 2C8.268 2 2 8.268 2 16C2 22.177 16 38 16 38C16 38 30 22.177 30 16C30 8.268 23.732 2 16 2Z" fill="#3b82f6"/>
|
|
88
|
+
<circle cx="16" cy="16" r="6" fill="#ffffff"/>
|
|
89
|
+
</svg>
|
|
90
|
+
`)}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Generate SVG icon for destination marker pin with text below (yellow)
|
|
95
|
+
*/
|
|
96
|
+
export function createDestinationMarkerIcon(name: string, color: string = '#facc15'): string {
|
|
97
|
+
const safeColor = validateColor(color);
|
|
98
|
+
const escapedName = escapeSvgText(truncateText(name, 20));
|
|
99
|
+
// SVG includes pin (40px height) + text below (20px height) = 60px total
|
|
100
|
+
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(`
|
|
101
|
+
<svg width="80" height="60" viewBox="0 0 80 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
102
|
+
<!-- Pin marker (centered horizontally, at top) -->
|
|
103
|
+
<g transform="translate(24, 0)">
|
|
104
|
+
<path d="M16 0C7.163 0 0 7.163 0 16C0 24.837 16 40 16 40C16 40 32 24.837 32 16C32 7.163 24.837 0 16 0Z" fill="#ffffff"/>
|
|
105
|
+
<path d="M16 2C8.268 2 2 8.268 2 16C2 22.177 16 38 16 38C16 38 30 22.177 30 16C30 8.268 23.732 2 16 2Z" fill="${safeColor}"/>
|
|
106
|
+
<circle cx="16" cy="16" r="6" fill="#ffffff"/>
|
|
107
|
+
</g>
|
|
108
|
+
<!-- Text below pin -->
|
|
109
|
+
<text x="40" y="52" fill="#1f2937" font-size="12" font-weight="600" text-anchor="middle" font-family="Arial, sans-serif">${escapedName}</text>
|
|
110
|
+
</svg>
|
|
111
|
+
`)}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for pickup location selector
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { PickupLocation } from './api';
|
|
6
|
+
import type { Coordinates } from './location-utils';
|
|
7
|
+
|
|
8
|
+
export interface NearbyLocation extends PickupLocation {
|
|
9
|
+
distance: number;
|
|
10
|
+
walkingTime: number;
|
|
11
|
+
drivingTime: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SearchedLocation {
|
|
15
|
+
address: string;
|
|
16
|
+
coordinates: Coordinates | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AutocompleteSuggestion {
|
|
20
|
+
placeId: string;
|
|
21
|
+
mainText: string;
|
|
22
|
+
secondaryText: string;
|
|
23
|
+
description: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Places API (New) utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface AutocompleteSuggestion {
|
|
6
|
+
placeId: string;
|
|
7
|
+
mainText: string;
|
|
8
|
+
secondaryText: string;
|
|
9
|
+
description: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Internal API response types
|
|
13
|
+
interface PlacePrediction {
|
|
14
|
+
placeId: string;
|
|
15
|
+
text?: { text: string };
|
|
16
|
+
structuredFormat?: {
|
|
17
|
+
mainText?: { text: string };
|
|
18
|
+
secondaryText?: { text: string };
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface AutocompleteSuggestionResponse {
|
|
23
|
+
placePrediction?: PlacePrediction;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const GOOGLE_PLACES_API_BASE = 'https://places.googleapis.com/v1';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get autocomplete suggestions using Places API (New)
|
|
30
|
+
*/
|
|
31
|
+
export async function getAutocompleteSuggestions(
|
|
32
|
+
input: string,
|
|
33
|
+
apiKey: string,
|
|
34
|
+
locationBias?: { latitude: number; longitude: number; radius: number }
|
|
35
|
+
): Promise<AutocompleteSuggestion[]> {
|
|
36
|
+
if (!input.trim() || !apiKey) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch(`${GOOGLE_PLACES_API_BASE}/places:autocomplete`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: {
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
'X-Goog-Api-Key': apiKey,
|
|
46
|
+
'X-Goog-FieldMask':
|
|
47
|
+
'suggestions.placePrediction.placeId,suggestions.placePrediction.text,suggestions.placePrediction.structuredFormat',
|
|
48
|
+
},
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
input,
|
|
51
|
+
includedRegionCodes: ['ca'], // Restrict to Canada
|
|
52
|
+
...(locationBias && {
|
|
53
|
+
locationBias: {
|
|
54
|
+
circle: {
|
|
55
|
+
center: {
|
|
56
|
+
latitude: locationBias.latitude,
|
|
57
|
+
longitude: locationBias.longitude,
|
|
58
|
+
},
|
|
59
|
+
radius: locationBias.radius,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const errorText = await response.text();
|
|
68
|
+
console.error('Autocomplete API error:', response.status, errorText);
|
|
69
|
+
throw new Error(`Autocomplete request failed: ${response.status} ${errorText}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const data = await response.json() as {
|
|
73
|
+
suggestions?: AutocompleteSuggestionResponse[];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (data.suggestions && Array.isArray(data.suggestions)) {
|
|
77
|
+
return data.suggestions
|
|
78
|
+
.filter((s): s is AutocompleteSuggestionResponse & { placePrediction: PlacePrediction } =>
|
|
79
|
+
!!s.placePrediction
|
|
80
|
+
)
|
|
81
|
+
.map((s) => {
|
|
82
|
+
const prediction = s.placePrediction;
|
|
83
|
+
const text = prediction.text?.text || '';
|
|
84
|
+
const structuredFormat = prediction.structuredFormat || {};
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
placeId: prediction.placeId,
|
|
88
|
+
mainText: structuredFormat.mainText?.text || text,
|
|
89
|
+
secondaryText: structuredFormat.secondaryText?.text || '',
|
|
90
|
+
description: text,
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return [];
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('Autocomplete error:', error);
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get place coordinates and types using Place Details API (New)
|
|
104
|
+
*/
|
|
105
|
+
export async function getPlaceDetails(
|
|
106
|
+
placeId: string,
|
|
107
|
+
apiKey: string
|
|
108
|
+
): Promise<{ lat: number; lng: number; types: string[] } | null> {
|
|
109
|
+
try {
|
|
110
|
+
const response = await fetch(`${GOOGLE_PLACES_API_BASE}/places/${placeId}`, {
|
|
111
|
+
method: 'GET',
|
|
112
|
+
headers: {
|
|
113
|
+
'Content-Type': 'application/json',
|
|
114
|
+
'X-Goog-Api-Key': apiKey,
|
|
115
|
+
'X-Goog-FieldMask': 'location,types',
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
throw new Error('Place details request failed');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const data = await response.json();
|
|
124
|
+
|
|
125
|
+
if (data.location) {
|
|
126
|
+
return {
|
|
127
|
+
lat: data.location.latitude,
|
|
128
|
+
lng: data.location.longitude,
|
|
129
|
+
types: data.types || [],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return null;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error('Error getting place details:', error);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get place coordinates using Place Details API (New)
|
|
142
|
+
* @deprecated Use getPlaceDetails instead to also get types
|
|
143
|
+
*/
|
|
144
|
+
export async function getPlaceCoordinates(
|
|
145
|
+
placeId: string,
|
|
146
|
+
apiKey: string
|
|
147
|
+
): Promise<{ lat: number; lng: number } | null> {
|
|
148
|
+
const details = await getPlaceDetails(placeId, apiKey);
|
|
149
|
+
if (details) {
|
|
150
|
+
return { lat: details.lat, lng: details.lng };
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|