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.
Files changed (81) hide show
  1. package/dist/components/atoms/Icon/icons/LineIcon.d.ts +8 -0
  2. package/dist/components/atoms/Icon/icons/LineIcon.js +21 -0
  3. package/dist/components/atoms/Icon/icons/registry.d.ts +1 -0
  4. package/dist/components/atoms/Icon/icons/registry.js +2 -0
  5. package/dist/components/molecules/AccomodationDocket/AccomodationDocket.d.ts +7 -0
  6. package/dist/components/molecules/AccomodationDocket/AccomodationDocket.js +69 -0
  7. package/dist/components/molecules/AccomodationDocket/index.d.ts +2 -0
  8. package/dist/components/molecules/AccomodationDocket/index.js +1 -0
  9. package/dist/components/molecules/BookingResume/ResumeAccom/ResumeAccom.js +1 -1
  10. package/dist/components/molecules/BookingResume/ResumeExcursion/ResumeExcursion.js +1 -1
  11. package/dist/components/molecules/BookingResume/ResumeTransfer.js +1 -1
  12. package/dist/components/molecules/DateDisplay/DateDisplay.css +2100 -0
  13. package/dist/components/molecules/DateDisplay/DateDisplay.d.ts +13 -6
  14. package/dist/components/molecules/DateDisplay/DateDisplay.js +22 -8
  15. package/dist/components/molecules/DocketPrices/DocketPrices.d.ts +19 -0
  16. package/dist/components/molecules/DocketPrices/DocketPrices.js +31 -0
  17. package/dist/components/molecules/DocketPrices/index.d.ts +3 -0
  18. package/dist/components/molecules/DocketPrices/index.js +2 -0
  19. package/dist/components/molecules/ExcursionDocket/ExcursionDocket.d.ts +8 -0
  20. package/dist/components/molecules/ExcursionDocket/ExcursionDocket.js +30 -0
  21. package/dist/components/molecules/ExcursionDocket/index.d.ts +2 -0
  22. package/dist/components/molecules/ExcursionDocket/index.js +1 -0
  23. package/dist/components/molecules/LocationDropdown/LocationDropdown.js +8 -11
  24. package/dist/components/molecules/OtherServiceDocket/OtherServiceDocket.d.ts +8 -0
  25. package/dist/components/molecules/OtherServiceDocket/OtherServiceDocket.js +29 -0
  26. package/dist/components/molecules/OtherServiceDocket/index.d.ts +2 -0
  27. package/dist/components/molecules/OtherServiceDocket/index.js +1 -0
  28. package/dist/components/molecules/PriceDisplay/PriceDisplay.css +2101 -0
  29. package/dist/components/molecules/PriceDisplay/PriceDisplay.d.ts +26 -0
  30. package/dist/components/molecules/PriceDisplay/PriceDisplay.js +132 -0
  31. package/dist/components/molecules/PriceDisplay/index.d.ts +3 -0
  32. package/dist/components/molecules/PriceDisplay/index.js +2 -0
  33. package/dist/components/molecules/TransferDocket/TransferDocket.d.ts +8 -0
  34. package/dist/components/molecules/TransferDocket/TransferDocket.js +59 -0
  35. package/dist/components/molecules/TransferDocket/index.d.ts +3 -0
  36. package/dist/components/molecules/TransferDocket/index.js +2 -0
  37. package/dist/components/organisms/Docket/Docket.d.ts +126 -0
  38. package/dist/components/organisms/Docket/Docket.js +125 -0
  39. package/dist/index.d.ts +6 -0
  40. package/dist/index.js +3 -0
  41. package/dist/styles/components/molecule/accomodation-docket.css +2222 -0
  42. package/dist/styles/components/molecule/docket-prices.css +2095 -0
  43. package/dist/styles/components/molecule/excursion-docket.css +2135 -0
  44. package/dist/styles/components/molecule/other-service-docket.css +2114 -0
  45. package/dist/styles/components/molecule/transfer-docket.css +2150 -0
  46. package/dist/styles/components/organism/docket.css +2448 -0
  47. package/dist/types/docket/docket.types.d.ts +11 -0
  48. package/dist/types/docket/docket.types.js +1 -0
  49. package/dist/types/docket/services.types.d.ts +125 -0
  50. package/dist/types/docket/services.types.js +1 -0
  51. package/package.json +3 -2
  52. package/src/components/atoms/Icon/icons/LineIcon.tsx +31 -0
  53. package/src/components/atoms/Icon/icons/registry.tsx +2 -0
  54. package/src/components/molecules/AccomodationDocket/AccomodationDocket.tsx +224 -0
  55. package/src/components/molecules/AccomodationDocket/index.ts +3 -0
  56. package/src/components/molecules/BookingResume/ResumeAccom/ResumeAccom.tsx +1 -1
  57. package/src/components/molecules/BookingResume/ResumeExcursion/ResumeExcursion.tsx +1 -1
  58. package/src/components/molecules/BookingResume/ResumeTransfer.tsx +1 -1
  59. package/src/components/molecules/DateDisplay/DateDisplay.css +21 -0
  60. package/src/components/molecules/DateDisplay/DateDisplay.tsx +52 -24
  61. package/src/components/molecules/DocketPrices/DocketPrices.tsx +56 -0
  62. package/src/components/molecules/DocketPrices/index.ts +4 -0
  63. package/src/components/molecules/ExcursionDocket/ExcursionDocket.tsx +171 -0
  64. package/src/components/molecules/ExcursionDocket/index.ts +2 -0
  65. package/src/components/molecules/LocationDropdown/LocationDropdown.tsx +41 -38
  66. package/src/components/molecules/OtherServiceDocket/OtherServiceDocket.tsx +58 -0
  67. package/src/components/molecules/OtherServiceDocket/index.ts +2 -0
  68. package/src/components/molecules/PriceDisplay/PriceDisplay.css +24 -0
  69. package/src/components/molecules/PriceDisplay/PriceDisplay.tsx +179 -0
  70. package/src/components/molecules/PriceDisplay/index.ts +4 -0
  71. package/src/components/molecules/TransferDocket/TransferDocket.tsx +156 -0
  72. package/src/components/molecules/TransferDocket/index.ts +4 -0
  73. package/src/components/organisms/CarBookingCard/index.ts +1 -0
  74. package/src/components/organisms/Docket/Docket.tsx +456 -0
  75. package/src/components/organisms/SearchBarTransfer/index.ts +2 -0
  76. package/src/styles/components/molecule/accomodation-docket.css +117 -0
  77. package/src/styles/components/molecule/docket-prices.css +13 -0
  78. package/src/styles/components/molecule/excursion-docket.css +47 -0
  79. package/src/styles/components/molecule/other-service-docket.css +30 -0
  80. package/src/styles/components/molecule/transfer-docket.css +61 -0
  81. package/src/styles/components/organism/docket.css +360 -0
@@ -0,0 +1,4 @@
1
+ export { DocketPrices } from './DocketPrices';
2
+ export type { DocketPricesProps } from './DocketPrices';
3
+ export { default } from './DocketPrices';
4
+
@@ -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;
@@ -0,0 +1,2 @@
1
+ export { ExcursionDocket, default } from './ExcursionDocket';
2
+ export type { ExcursionDocketProps } from './ExcursionDocket';
@@ -1,7 +1,7 @@
1
- import React, { useState, useRef, useEffect } from '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 ref={dropdownRef} className={`location-dropdown location-dropdown--${type} ${className}`}>
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">{label}</Text>
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 className={`location-dropdown__input-text ${!selectedOption ? 'location-dropdown__input-text--placeholder' : ''}`}>
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 ? renderSkeletonLoading() : (
212
+ {isOpen &&
213
+ (loading ? (
214
+ renderSkeletonLoading()
215
+ ) : (
215
216
  <div className="location-dropdown__panel">
216
- <div className="location-dropdown__content" style={{ maxHeight: `${maxHeight}px` }}>
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">{group.label}</Text>
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
- <Icon
241
- name={getOptionIcon(option, isSelected)}
242
- size="sm"
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,2 @@
1
+ export { default, OtherServiceDocket } from './OtherServiceDocket';
2
+ export type { OtherServiceDocketProps } from './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
+
@@ -0,0 +1,4 @@
1
+ export { PriceDisplay } from './PriceDisplay';
2
+ export type { PriceDisplayProps } from './PriceDisplay';
3
+ export { default } from './PriceDisplay';
4
+