@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,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, '&lt;')
19
+ .replace(/>/g, '&gt;')
20
+ .replace(/"/g, '&quot;')
21
+ .replace(/'/g, '&#39;');
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
+