mautourco-components 0.2.23 → 0.2.25
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/dist/components/atoms/Icon/icons/LineIcon.d.ts +8 -0
- package/dist/components/atoms/Icon/icons/LineIcon.js +21 -0
- package/dist/components/atoms/Icon/icons/registry.d.ts +1 -0
- package/dist/components/atoms/Icon/icons/registry.js +2 -0
- package/dist/components/molecules/AccomodationDocket/AccomodationDocket.d.ts +7 -0
- package/dist/components/molecules/AccomodationDocket/AccomodationDocket.js +69 -0
- package/dist/components/molecules/AccomodationDocket/index.d.ts +2 -0
- package/dist/components/molecules/AccomodationDocket/index.js +1 -0
- package/dist/components/molecules/BookingResume/ResumeAccom/ResumeAccom.js +1 -1
- package/dist/components/molecules/BookingResume/ResumeExcursion/ResumeExcursion.js +1 -1
- package/dist/components/molecules/BookingResume/ResumeTransfer.js +1 -1
- package/dist/components/molecules/DateDisplay/DateDisplay.css +2100 -0
- package/dist/components/molecules/DateDisplay/DateDisplay.d.ts +13 -6
- package/dist/components/molecules/DateDisplay/DateDisplay.js +22 -8
- package/dist/components/molecules/DocketPrices/DocketPrices.d.ts +19 -0
- package/dist/components/molecules/DocketPrices/DocketPrices.js +31 -0
- package/dist/components/molecules/DocketPrices/index.d.ts +3 -0
- package/dist/components/molecules/DocketPrices/index.js +2 -0
- package/dist/components/molecules/ExcursionDocket/ExcursionDocket.d.ts +8 -0
- package/dist/components/molecules/ExcursionDocket/ExcursionDocket.js +30 -0
- package/dist/components/molecules/ExcursionDocket/index.d.ts +2 -0
- package/dist/components/molecules/ExcursionDocket/index.js +1 -0
- package/dist/components/molecules/LocationDropdown/LocationDropdown.js +8 -11
- package/dist/components/molecules/OtherServiceDocket/OtherServiceDocket.d.ts +8 -0
- package/dist/components/molecules/OtherServiceDocket/OtherServiceDocket.js +29 -0
- package/dist/components/molecules/OtherServiceDocket/index.d.ts +2 -0
- package/dist/components/molecules/OtherServiceDocket/index.js +1 -0
- package/dist/components/molecules/PriceDisplay/PriceDisplay.css +2101 -0
- package/dist/components/molecules/PriceDisplay/PriceDisplay.d.ts +26 -0
- package/dist/components/molecules/PriceDisplay/PriceDisplay.js +132 -0
- package/dist/components/molecules/PriceDisplay/index.d.ts +3 -0
- package/dist/components/molecules/PriceDisplay/index.js +2 -0
- package/dist/components/molecules/TransferDocket/TransferDocket.d.ts +8 -0
- package/dist/components/molecules/TransferDocket/TransferDocket.js +59 -0
- package/dist/components/molecules/TransferDocket/index.d.ts +3 -0
- package/dist/components/molecules/TransferDocket/index.js +2 -0
- package/dist/components/organisms/Docket/Docket.d.ts +126 -0
- package/dist/components/organisms/Docket/Docket.js +125 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/styles/components/molecule/accomodation-docket.css +2222 -0
- package/dist/styles/components/molecule/docket-prices.css +2095 -0
- package/dist/styles/components/molecule/excursion-docket.css +2135 -0
- package/dist/styles/components/molecule/other-service-docket.css +2114 -0
- package/dist/styles/components/molecule/transfer-docket.css +2150 -0
- package/dist/styles/components/organism/docket.css +2448 -0
- package/dist/types/docket/docket.types.d.ts +11 -0
- package/dist/types/docket/docket.types.js +1 -0
- package/dist/types/docket/services.types.d.ts +125 -0
- package/dist/types/docket/services.types.js +1 -0
- package/package.json +3 -2
- package/src/components/atoms/Icon/icons/LineIcon.tsx +31 -0
- package/src/components/atoms/Icon/icons/registry.tsx +2 -0
- package/src/components/molecules/AccomodationDocket/AccomodationDocket.tsx +224 -0
- package/src/components/molecules/AccomodationDocket/index.ts +3 -0
- package/src/components/molecules/BookingResume/ResumeAccom/ResumeAccom.tsx +1 -1
- package/src/components/molecules/BookingResume/ResumeExcursion/ResumeExcursion.tsx +1 -1
- package/src/components/molecules/BookingResume/ResumeTransfer.tsx +1 -1
- package/src/components/molecules/DateDisplay/DateDisplay.css +21 -0
- package/src/components/molecules/DateDisplay/DateDisplay.tsx +52 -24
- package/src/components/molecules/DocketPrices/DocketPrices.tsx +56 -0
- package/src/components/molecules/DocketPrices/index.ts +4 -0
- package/src/components/molecules/ExcursionDocket/ExcursionDocket.tsx +171 -0
- package/src/components/molecules/ExcursionDocket/index.ts +2 -0
- package/src/components/molecules/LocationDropdown/LocationDropdown.tsx +41 -38
- package/src/components/molecules/OtherServiceDocket/OtherServiceDocket.tsx +58 -0
- package/src/components/molecules/OtherServiceDocket/index.ts +2 -0
- package/src/components/molecules/PriceDisplay/PriceDisplay.css +24 -0
- package/src/components/molecules/PriceDisplay/PriceDisplay.tsx +179 -0
- package/src/components/molecules/PriceDisplay/index.ts +4 -0
- package/src/components/molecules/TransferDocket/TransferDocket.tsx +156 -0
- package/src/components/molecules/TransferDocket/index.ts +4 -0
- package/src/components/organisms/CarBookingCard/index.ts +1 -0
- package/src/components/organisms/Docket/Docket.tsx +456 -0
- package/src/components/organisms/SearchBarTransfer/index.ts +2 -0
- package/src/styles/components/molecule/accomodation-docket.css +117 -0
- package/src/styles/components/molecule/docket-prices.css +13 -0
- package/src/styles/components/molecule/excursion-docket.css +47 -0
- package/src/styles/components/molecule/other-service-docket.css +30 -0
- package/src/styles/components/molecule/transfer-docket.css +61 -0
- package/src/styles/components/organism/docket.css +360 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import '../../../styles/components/molecule/excursion-docket.css';
|
|
3
|
+
import { ExcursionDocket as ExcursionDocketType } from '../../../types/docket/services.types';
|
|
4
|
+
import Chip from '../../atoms/Chip/Chip';
|
|
5
|
+
import Icon from '../../atoms/Icon/Icon';
|
|
6
|
+
import { Text } from '../../atoms/Typography/Typography';
|
|
7
|
+
import { DateDisplay } from '../DateDisplay/DateDisplay';
|
|
8
|
+
import TextWithIcon from '../TextWithIcon/TextWithIcon';
|
|
9
|
+
|
|
10
|
+
export interface ExcursionDocketProps {
|
|
11
|
+
data: ExcursionDocketType;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const ExcursionDocket: React.FC<ExcursionDocketProps> = ({ data }) => {
|
|
15
|
+
const {
|
|
16
|
+
ExcursionName,
|
|
17
|
+
ServiceDate,
|
|
18
|
+
SelectedHotel,
|
|
19
|
+
SelectedHotelDetails,
|
|
20
|
+
Currency,
|
|
21
|
+
TotalPrice,
|
|
22
|
+
AdultCount,
|
|
23
|
+
Duration,
|
|
24
|
+
MealIncluded,
|
|
25
|
+
Meal,
|
|
26
|
+
Accessibility,
|
|
27
|
+
VehicleType,
|
|
28
|
+
VehicleTypeName,
|
|
29
|
+
} = data;
|
|
30
|
+
|
|
31
|
+
const formatPrice = () => {
|
|
32
|
+
if (!TotalPrice) return null;
|
|
33
|
+
const price = typeof TotalPrice === 'string' ? TotalPrice : TotalPrice.toString();
|
|
34
|
+
return `${Currency} ${price}`;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="excursion-docket">
|
|
39
|
+
<div className="excursion-docket__header">
|
|
40
|
+
<div className="excursion-docket__title-section">
|
|
41
|
+
<div className="excursion-docket__title-bar" />
|
|
42
|
+
<Icon name="map-pin" size="sm" />
|
|
43
|
+
<Text variant="bold" size="sm" color="accent">
|
|
44
|
+
Excursion
|
|
45
|
+
</Text>
|
|
46
|
+
</div>
|
|
47
|
+
{TotalPrice && Currency && (
|
|
48
|
+
<Chip type="filled" color="accent" size="sm">
|
|
49
|
+
{formatPrice()}
|
|
50
|
+
</Chip>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{ExcursionName && (
|
|
55
|
+
<div className="excursion-docket__excursion-name">
|
|
56
|
+
<Text variant="bold" size="base" color="default">
|
|
57
|
+
{ExcursionName}
|
|
58
|
+
</Text>
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
<div className="excursion-docket__details">
|
|
63
|
+
{ServiceDate && (
|
|
64
|
+
<DateDisplay
|
|
65
|
+
date={ServiceDate}
|
|
66
|
+
calendarSize="sm"
|
|
67
|
+
textSize="sm"
|
|
68
|
+
colorMode="green"
|
|
69
|
+
/>
|
|
70
|
+
)}
|
|
71
|
+
|
|
72
|
+
{SelectedHotel && (
|
|
73
|
+
<div className="excursion-docket__pickup">
|
|
74
|
+
<Text variant="bold" size="sm" color="subtle">
|
|
75
|
+
Pickup:{' '}
|
|
76
|
+
</Text>
|
|
77
|
+
<Text variant="medium" size="sm" color="subtle">
|
|
78
|
+
{SelectedHotel}
|
|
79
|
+
</Text>
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
{AdultCount !== undefined && AdultCount > 0 && (
|
|
84
|
+
<TextWithIcon icon="user" textSize="sm" color="subtle">
|
|
85
|
+
{AdultCount} Adult{AdultCount > 1 ? 's' : ''}
|
|
86
|
+
</TextWithIcon>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
<div className="excursion-docket__details-list">
|
|
90
|
+
{Duration && (
|
|
91
|
+
<div className="excursion-docket__detail-item">
|
|
92
|
+
<div className="excursion-docket__detail-label">
|
|
93
|
+
<TextWithIcon
|
|
94
|
+
icon="clock"
|
|
95
|
+
textSize="sm"
|
|
96
|
+
color="subtle"
|
|
97
|
+
textLeading="none">
|
|
98
|
+
Duration
|
|
99
|
+
</TextWithIcon>
|
|
100
|
+
</div>
|
|
101
|
+
<Text variant="medium" size="sm" color="subtle" leading="none">
|
|
102
|
+
|
|
|
103
|
+
</Text>
|
|
104
|
+
<Text variant="regular" size="sm" color="subtle" leading="none">
|
|
105
|
+
{Duration}
|
|
106
|
+
</Text>
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
|
|
110
|
+
{(MealIncluded !== undefined || Meal) && (
|
|
111
|
+
<div className="excursion-docket__detail-item">
|
|
112
|
+
<div className="excursion-docket__detail-label">
|
|
113
|
+
<TextWithIcon
|
|
114
|
+
icon="utensils"
|
|
115
|
+
textSize="sm"
|
|
116
|
+
color="subtle"
|
|
117
|
+
textLeading="none">
|
|
118
|
+
Meal
|
|
119
|
+
</TextWithIcon>
|
|
120
|
+
</div>
|
|
121
|
+
<Text variant="medium" size="sm" color="subtle" leading="none">
|
|
122
|
+
|
|
|
123
|
+
</Text>
|
|
124
|
+
<Text variant="regular" size="sm" color="subtle" leading="none">
|
|
125
|
+
{Meal || (MealIncluded ? 'Included' : 'Not included')}
|
|
126
|
+
</Text>
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{Accessibility && (
|
|
131
|
+
<div className="excursion-docket__detail-item">
|
|
132
|
+
<div className="excursion-docket__detail-label">
|
|
133
|
+
<TextWithIcon
|
|
134
|
+
icon="stroller"
|
|
135
|
+
textSize="sm"
|
|
136
|
+
color="subtle"
|
|
137
|
+
textLeading="none">
|
|
138
|
+
Accessibility
|
|
139
|
+
</TextWithIcon>
|
|
140
|
+
</div>
|
|
141
|
+
<Text variant="medium" size="sm" color="subtle" leading="none">
|
|
142
|
+
|
|
|
143
|
+
</Text>
|
|
144
|
+
<Text variant="regular" size="sm" color="subtle" leading="none">
|
|
145
|
+
{Accessibility}
|
|
146
|
+
</Text>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{(VehicleType || VehicleTypeName) && (
|
|
151
|
+
<div className="excursion-docket__detail-item">
|
|
152
|
+
<div className="excursion-docket__detail-label">
|
|
153
|
+
<TextWithIcon icon="car" textSize="sm" color="subtle" textLeading="none">
|
|
154
|
+
Vehicle
|
|
155
|
+
</TextWithIcon>
|
|
156
|
+
</div>
|
|
157
|
+
<Text variant="medium" size="sm" color="subtle" leading="none">
|
|
158
|
+
|
|
|
159
|
+
</Text>
|
|
160
|
+
<Text variant="regular" size="sm" color="subtle" leading="none">
|
|
161
|
+
{VehicleTypeName || VehicleType}
|
|
162
|
+
</Text>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export default ExcursionDocket;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import '../../../styles/components/molecule/location-dropdown.css';
|
|
2
3
|
import Icon from '../../atoms/Icon/Icon';
|
|
3
4
|
import { Text } from '../../atoms/Typography/Typography';
|
|
4
|
-
import '../../../styles/components/molecule/location-dropdown.css';
|
|
5
5
|
|
|
6
6
|
export interface LocationOption {
|
|
7
7
|
id: string | number;
|
|
@@ -52,7 +52,7 @@ const LocationDropdown: React.FC<LocationDropdownProps> = ({
|
|
|
52
52
|
type = 'airport-port',
|
|
53
53
|
maxHeight = 240,
|
|
54
54
|
direction = undefined,
|
|
55
|
-
showGroupTitles = true
|
|
55
|
+
showGroupTitles = true,
|
|
56
56
|
}) => {
|
|
57
57
|
const [isOpen, setIsOpen] = useState(false);
|
|
58
58
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
@@ -91,12 +91,12 @@ const LocationDropdown: React.FC<LocationDropdownProps> = ({
|
|
|
91
91
|
if (!selectedValue) return null;
|
|
92
92
|
|
|
93
93
|
// Search in flat options
|
|
94
|
-
const flatOption = options.find(opt => opt.id === selectedValue);
|
|
94
|
+
const flatOption = options.find((opt) => opt.id === selectedValue);
|
|
95
95
|
if (flatOption) return flatOption;
|
|
96
96
|
|
|
97
97
|
// Search in groups
|
|
98
98
|
for (const group of groups) {
|
|
99
|
-
const groupOption = group.options.find(opt => opt.id === selectedValue);
|
|
99
|
+
const groupOption = group.options.find((opt) => opt.id === selectedValue);
|
|
100
100
|
if (groupOption) return groupOption;
|
|
101
101
|
}
|
|
102
102
|
|
|
@@ -118,15 +118,11 @@ const LocationDropdown: React.FC<LocationDropdownProps> = ({
|
|
|
118
118
|
|
|
119
119
|
const getInputIcon = () => {
|
|
120
120
|
if (selectedOption) {
|
|
121
|
-
// For airports, show arrival/departure based on direction
|
|
122
|
-
console.log({direction});
|
|
123
|
-
|
|
124
121
|
if (selectedOption.type === 'airport' && direction) {
|
|
125
122
|
return direction === 'pickup' ? 'arrival' : 'departure';
|
|
126
123
|
}
|
|
127
124
|
return getIconForType(selectedOption.type);
|
|
128
125
|
}
|
|
129
|
-
// Use arrival/departure icons based on direction for pickup/dropoff types
|
|
130
126
|
if ((type === 'pickup-dropoff' || type === 'airport-port') && direction) {
|
|
131
127
|
return direction === 'pickup' ? 'arrival' : 'departure';
|
|
132
128
|
}
|
|
@@ -134,7 +130,6 @@ const LocationDropdown: React.FC<LocationDropdownProps> = ({
|
|
|
134
130
|
};
|
|
135
131
|
|
|
136
132
|
const getOptionIcon = (option: LocationOption, isSelected: boolean) => {
|
|
137
|
-
// For selected airports, show arrival/departure based on direction
|
|
138
133
|
if (isSelected && option.type === 'airport' && direction) {
|
|
139
134
|
return direction === 'pickup' ? 'arrival' : 'departure';
|
|
140
135
|
}
|
|
@@ -182,38 +177,46 @@ const LocationDropdown: React.FC<LocationDropdownProps> = ({
|
|
|
182
177
|
);
|
|
183
178
|
|
|
184
179
|
return (
|
|
185
|
-
<div
|
|
180
|
+
<div
|
|
181
|
+
ref={dropdownRef}
|
|
182
|
+
className={`location-dropdown location-dropdown--${type} ${className}`}>
|
|
186
183
|
{label && (
|
|
187
184
|
<div className="location-dropdown__label">
|
|
188
|
-
<Text size="sm" variant="medium">
|
|
185
|
+
<Text size="sm" variant="medium">
|
|
186
|
+
{label}
|
|
187
|
+
</Text>
|
|
189
188
|
</div>
|
|
190
189
|
)}
|
|
191
|
-
|
|
192
|
-
<div
|
|
190
|
+
|
|
191
|
+
<div
|
|
193
192
|
className={`location-dropdown__input location-dropdown__input--${getDropdownState()}`}
|
|
194
|
-
onClick={handleToggleDropdown}
|
|
195
|
-
>
|
|
193
|
+
onClick={handleToggleDropdown}>
|
|
196
194
|
<div className="location-dropdown__input-content">
|
|
197
|
-
<Icon
|
|
198
|
-
name={getInputIcon()}
|
|
199
|
-
size="sm"
|
|
195
|
+
<Icon
|
|
196
|
+
name={getInputIcon()}
|
|
197
|
+
size="sm"
|
|
200
198
|
className={`location-dropdown__input-icon ${!selectedOption ? 'location-dropdown__input-icon--placeholder' : ''}`}
|
|
201
199
|
/>
|
|
202
|
-
<span
|
|
200
|
+
<span
|
|
201
|
+
className={`location-dropdown__input-text ${!selectedOption ? 'location-dropdown__input-text--placeholder' : ''}`}>
|
|
203
202
|
{displayText}
|
|
204
203
|
</span>
|
|
205
204
|
</div>
|
|
206
|
-
<Icon
|
|
207
|
-
name="chevron-down"
|
|
208
|
-
size="sm"
|
|
209
|
-
className="location-dropdown__input-chevron"
|
|
205
|
+
<Icon
|
|
206
|
+
name="chevron-down"
|
|
207
|
+
size="sm"
|
|
208
|
+
className="location-dropdown__input-chevron"
|
|
210
209
|
/>
|
|
211
210
|
</div>
|
|
212
211
|
|
|
213
|
-
{isOpen &&
|
|
214
|
-
loading ?
|
|
212
|
+
{isOpen &&
|
|
213
|
+
(loading ? (
|
|
214
|
+
renderSkeletonLoading()
|
|
215
|
+
) : (
|
|
215
216
|
<div className="location-dropdown__panel">
|
|
216
|
-
<div
|
|
217
|
+
<div
|
|
218
|
+
className="location-dropdown__content"
|
|
219
|
+
style={{ maxHeight: `${maxHeight}px` }}>
|
|
217
220
|
<div className="location-dropdown__options-wrapper">
|
|
218
221
|
{allOptions.map((group, groupIndex) => (
|
|
219
222
|
<div key={group.id} className="location-dropdown__group">
|
|
@@ -221,26 +224,27 @@ const LocationDropdown: React.FC<LocationDropdownProps> = ({
|
|
|
221
224
|
<>
|
|
222
225
|
{groupIndex > 0 && <div className="location-dropdown__divider" />}
|
|
223
226
|
<div className="location-dropdown__group-header">
|
|
224
|
-
<Text size="xs" variant="bold">
|
|
227
|
+
<Text size="xs" variant="bold">
|
|
228
|
+
{group.label}
|
|
229
|
+
</Text>
|
|
225
230
|
</div>
|
|
226
231
|
</>
|
|
227
232
|
)}
|
|
228
|
-
|
|
233
|
+
|
|
229
234
|
<div className="location-dropdown__group-options">
|
|
230
235
|
{group.options.map((option) => {
|
|
231
236
|
const isSelected = selectedValue === option.id;
|
|
232
237
|
const isDisabled = option.disabled || disabled;
|
|
233
|
-
|
|
238
|
+
|
|
234
239
|
return (
|
|
235
240
|
<div
|
|
236
241
|
key={option.id}
|
|
237
242
|
className={`location-dropdown__option ${isSelected ? 'location-dropdown__option--selected' : ''} ${isDisabled ? 'location-dropdown__option--disabled' : ''}`}
|
|
238
|
-
onClick={() => !isDisabled && handleOptionSelect(option)}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
className="location-dropdown__option-icon"
|
|
243
|
+
onClick={() => !isDisabled && handleOptionSelect(option)}>
|
|
244
|
+
<Icon
|
|
245
|
+
name={getOptionIcon(option, isSelected)}
|
|
246
|
+
size="sm"
|
|
247
|
+
className="location-dropdown__option-icon"
|
|
244
248
|
/>
|
|
245
249
|
<span className="location-dropdown__option-text">
|
|
246
250
|
{option.label}
|
|
@@ -254,8 +258,7 @@ const LocationDropdown: React.FC<LocationDropdownProps> = ({
|
|
|
254
258
|
</div>
|
|
255
259
|
</div>
|
|
256
260
|
</div>
|
|
257
|
-
)
|
|
258
|
-
)}
|
|
261
|
+
))}
|
|
259
262
|
</div>
|
|
260
263
|
);
|
|
261
264
|
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import '../../../styles/components/molecule/other-service-docket.css';
|
|
3
|
+
import { OtherServiceDocket as OtherServiceDocketType } from '../../../types/docket/services.types';
|
|
4
|
+
import Chip from '../../atoms/Chip/Chip';
|
|
5
|
+
import Icon from '../../atoms/Icon/Icon';
|
|
6
|
+
import { Text } from '../../atoms/Typography/Typography';
|
|
7
|
+
import TextWithIcon from '../TextWithIcon/TextWithIcon';
|
|
8
|
+
|
|
9
|
+
export interface OtherServiceDocketProps {
|
|
10
|
+
data: OtherServiceDocketType;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const OtherServiceDocket: React.FC<OtherServiceDocketProps> = ({ data }) => {
|
|
14
|
+
const { service_for, Currency, TotalPrice, AdultCount } = data;
|
|
15
|
+
|
|
16
|
+
const formatPrice = () => {
|
|
17
|
+
if (!TotalPrice) return null;
|
|
18
|
+
const price = typeof TotalPrice === 'string' ? TotalPrice : TotalPrice.toString();
|
|
19
|
+
return `${Currency} ${price}`;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="other-service-docket">
|
|
24
|
+
<div className="other-service-docket__header">
|
|
25
|
+
<div className="other-service-docket__title-section">
|
|
26
|
+
<div className="other-service-docket__title-bar" />
|
|
27
|
+
<Icon name="more" size="sm" />
|
|
28
|
+
<Text variant="bold" size="sm" color="accent">
|
|
29
|
+
Other services
|
|
30
|
+
</Text>
|
|
31
|
+
</div>
|
|
32
|
+
{TotalPrice && Currency && (
|
|
33
|
+
<Chip type="filled" color="accent" size="sm">
|
|
34
|
+
{formatPrice()}
|
|
35
|
+
</Chip>
|
|
36
|
+
)}
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div className="other-service-docket__details">
|
|
40
|
+
<div className="other-service-docket__service-info">
|
|
41
|
+
{service_for && (
|
|
42
|
+
<TextWithIcon icon="wallet" textSize="sm" color="subtle">
|
|
43
|
+
{service_for}
|
|
44
|
+
</TextWithIcon>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
{AdultCount !== undefined && AdultCount > 0 && (
|
|
49
|
+
<TextWithIcon icon="user" textSize="sm" color="subtle">
|
|
50
|
+
{AdultCount} Adult{AdultCount > 1 ? 's' : ''}
|
|
51
|
+
</TextWithIcon>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default OtherServiceDocket;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
.price-display {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
gap: var(--chip-spacing-sm-gap, 4px);
|
|
6
|
+
padding: var(--chip-spacing-sm-padding-y, 4px) var(--chip-spacing-sm-padding-x, 8px);
|
|
7
|
+
border-radius: var(--chip-border-radius-pill, 9999px);
|
|
8
|
+
background-color: var(--chip-color-accent-filled-background, #0f7173);
|
|
9
|
+
color: var(--chip-color-accent-filled-foreground, #ffffff);
|
|
10
|
+
font-family:
|
|
11
|
+
var(--font-font-family-body, 'Satoshi'), 'Satoshi', 'Inter', 'Segoe UI', 'system-ui',
|
|
12
|
+
sans-serif;
|
|
13
|
+
font-weight: var(--font-weight-font-medium, 500);
|
|
14
|
+
font-size: var(--font-size-text-xs, 12px);
|
|
15
|
+
line-height: calc(var(--font-leading-leading-base, 16) * 1px);
|
|
16
|
+
letter-spacing: var(--font-tracking-tracking-normal, 0px);
|
|
17
|
+
white-space: nowrap;
|
|
18
|
+
opacity: var(--opacity-opacity-100, 1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.price-display__text {
|
|
22
|
+
display: inline-block;
|
|
23
|
+
}
|
|
24
|
+
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import './PriceDisplay.css';
|
|
3
|
+
|
|
4
|
+
export interface PriceDisplayProps {
|
|
5
|
+
/** Currency code (e.g., "MUR", "USD", "EUR") */
|
|
6
|
+
currency: string;
|
|
7
|
+
/** Price value as number or string */
|
|
8
|
+
price: number | string;
|
|
9
|
+
/** Thousand separator character (default: ",") */
|
|
10
|
+
thousandSeparator?: string;
|
|
11
|
+
/** Decimal separator character (default: ".") */
|
|
12
|
+
decimalSeparator?: string;
|
|
13
|
+
/** Additional CSS classes */
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Checks if a string price is already properly formatted
|
|
19
|
+
* @param priceString - The price string to check
|
|
20
|
+
* @param thousandSeparator - The expected thousand separator
|
|
21
|
+
* @param decimalSeparator - The expected decimal separator
|
|
22
|
+
* @returns true if the string appears to be already formatted
|
|
23
|
+
*/
|
|
24
|
+
const isPriceAlreadyFormatted = (
|
|
25
|
+
priceString: string,
|
|
26
|
+
thousandSeparator: string,
|
|
27
|
+
decimalSeparator: string
|
|
28
|
+
): boolean => {
|
|
29
|
+
// Remove currency code and whitespace for checking
|
|
30
|
+
const cleaned = priceString.trim();
|
|
31
|
+
|
|
32
|
+
// Check if it contains the expected separators
|
|
33
|
+
const hasThousandSep = thousandSeparator !== '' && cleaned.includes(thousandSeparator);
|
|
34
|
+
const hasDecimalSep = cleaned.includes(decimalSeparator);
|
|
35
|
+
|
|
36
|
+
// If it has both separators or decimal separator, it's likely already formatted
|
|
37
|
+
if (hasDecimalSep || hasThousandSep) {
|
|
38
|
+
// Additional check: ensure it's a valid number format
|
|
39
|
+
const numberPart = cleaned.replace(/[^\d.,-]/g, '').replace(thousandSeparator, '').replace(decimalSeparator, '.');
|
|
40
|
+
return !isNaN(parseFloat(numberPart));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return false;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Formats a number with custom thousand and decimal separators
|
|
48
|
+
* @param value - The numeric value to format
|
|
49
|
+
* @param thousandSeparator - The thousand separator to use
|
|
50
|
+
* @param decimalSeparator - The decimal separator to use
|
|
51
|
+
* @returns Formatted price string
|
|
52
|
+
*/
|
|
53
|
+
const formatPrice = (
|
|
54
|
+
value: number,
|
|
55
|
+
thousandSeparator: string,
|
|
56
|
+
decimalSeparator: string
|
|
57
|
+
): string => {
|
|
58
|
+
// Convert to string with 2 decimal places
|
|
59
|
+
const parts = value.toFixed(2).split('.');
|
|
60
|
+
const integerPart = parts[0];
|
|
61
|
+
const decimalPart = parts[1];
|
|
62
|
+
|
|
63
|
+
// Add thousand separators to integer part
|
|
64
|
+
const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, thousandSeparator);
|
|
65
|
+
|
|
66
|
+
// Combine with decimal separator
|
|
67
|
+
return `${formattedInteger}${decimalSeparator}${decimalPart}`;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parses a string price to a number, handling various formats
|
|
72
|
+
* @param priceString - The price string to parse
|
|
73
|
+
* @param thousandSeparator - The thousand separator used in the string
|
|
74
|
+
* @param decimalSeparator - The decimal separator used in the string
|
|
75
|
+
* @returns The parsed number or NaN if invalid
|
|
76
|
+
*/
|
|
77
|
+
const parsePriceString = (
|
|
78
|
+
priceString: string,
|
|
79
|
+
thousandSeparator: string,
|
|
80
|
+
decimalSeparator: string
|
|
81
|
+
): number => {
|
|
82
|
+
// Remove currency code, whitespace, and other non-numeric characters except separators
|
|
83
|
+
let cleaned = priceString.trim();
|
|
84
|
+
|
|
85
|
+
// Remove currency code if present at the start
|
|
86
|
+
cleaned = cleaned.replace(/^[A-Z]{3}\s*/i, '');
|
|
87
|
+
|
|
88
|
+
// Replace thousand separator with nothing
|
|
89
|
+
if (thousandSeparator) {
|
|
90
|
+
cleaned = cleaned.replace(new RegExp(`\\${thousandSeparator}`, 'g'), '');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Replace decimal separator with dot for parsing
|
|
94
|
+
if (decimalSeparator !== '.') {
|
|
95
|
+
cleaned = cleaned.replace(new RegExp(`\\${decimalSeparator}`, 'g'), '.');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return parseFloat(cleaned);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A component for displaying prices with currency in a chip-like style.
|
|
103
|
+
* Supports both number and string prices, with customizable separators.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* <PriceDisplay currency="MUR" price={3800} />
|
|
107
|
+
* <PriceDisplay currency="USD" price="1,234.56" thousandSeparator="," decimalSeparator="." />
|
|
108
|
+
* @param PriceDisplayProps
|
|
109
|
+
* @returns
|
|
110
|
+
*/
|
|
111
|
+
export const PriceDisplay: React.FC<PriceDisplayProps> = ({
|
|
112
|
+
currency,
|
|
113
|
+
price,
|
|
114
|
+
thousandSeparator = ',',
|
|
115
|
+
decimalSeparator = '.',
|
|
116
|
+
className = '',
|
|
117
|
+
}) => {
|
|
118
|
+
const formattedPrice = useMemo(() => {
|
|
119
|
+
// Handle number type
|
|
120
|
+
if (typeof price === 'number') {
|
|
121
|
+
if (isNaN(price)) {
|
|
122
|
+
return '0.00';
|
|
123
|
+
}
|
|
124
|
+
return formatPrice(price, thousandSeparator, decimalSeparator);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Handle string type
|
|
128
|
+
if (typeof price === 'string') {
|
|
129
|
+
const trimmed = price.trim();
|
|
130
|
+
|
|
131
|
+
// Handle empty string
|
|
132
|
+
if (trimmed === '') {
|
|
133
|
+
return '0.00';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check if already formatted correctly
|
|
137
|
+
if (isPriceAlreadyFormatted(trimmed, thousandSeparator, decimalSeparator)) {
|
|
138
|
+
// Remove currency code if present (3-letter code at start) and return the formatted part
|
|
139
|
+
const cleaned = trimmed.replace(/^[A-Z]{2,4}\s+/i, '').trim();
|
|
140
|
+
// If after removing currency we have nothing, it was just a currency code
|
|
141
|
+
if (cleaned === '') {
|
|
142
|
+
return '0.00';
|
|
143
|
+
}
|
|
144
|
+
return cleaned;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Parse and reformat
|
|
148
|
+
const numericValue = parsePriceString(trimmed, thousandSeparator, decimalSeparator);
|
|
149
|
+
if (!isNaN(numericValue)) {
|
|
150
|
+
return formatPrice(numericValue, thousandSeparator, decimalSeparator);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// If parsing fails, try to extract just the number part
|
|
154
|
+
const numberMatch = trimmed.match(/[\d.,-]+/);
|
|
155
|
+
if (numberMatch) {
|
|
156
|
+
const extractedNumber = parsePriceString(numberMatch[0], thousandSeparator, decimalSeparator);
|
|
157
|
+
if (!isNaN(extractedNumber)) {
|
|
158
|
+
return formatPrice(extractedNumber, thousandSeparator, decimalSeparator);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Last resort: return the original string (fallback)
|
|
163
|
+
return trimmed;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return String(price);
|
|
167
|
+
}, [price, thousandSeparator, decimalSeparator]);
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div className={`price-display ${className}`}>
|
|
171
|
+
<span className="price-display__text">
|
|
172
|
+
{currency} {formattedPrice}
|
|
173
|
+
</span>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export default PriceDisplay;
|
|
179
|
+
|