mautourco-components 0.2.77 → 0.2.79
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/Chip/Chip.css +32 -1
- package/dist/components/molecules/VehicleSupplement/VehicleSupplement.css +85 -0
- package/dist/components/molecules/VehicleSupplement/VehicleSupplement.d.ts +31 -0
- package/dist/components/molecules/VehicleSupplement/VehicleSupplement.js +101 -0
- package/dist/components/molecules/VehicleSupplement/index.d.ts +2 -0
- package/dist/components/molecules/VehicleSupplement/index.js +1 -0
- package/dist/components/organisms/CarBookingCard/CarBookingCard.css +118 -0
- package/dist/components/organisms/CarBookingCard/CarBookingCard.d.ts +9 -5
- package/dist/components/organisms/CarBookingCard/CarBookingCard.js +89 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
- package/src/components/atoms/Chip/Chip.css +32 -1
- package/src/components/molecules/VehicleSupplement/VehicleSupplement.css +81 -0
- package/src/components/molecules/VehicleSupplement/VehicleSupplement.tsx +206 -0
- package/src/components/molecules/VehicleSupplement/index.ts +2 -0
- package/src/components/organisms/CarBookingCard/CarBookingCard.css +117 -0
- package/src/components/organisms/CarBookingCard/CarBookingCard.tsx +154 -34
|
@@ -162,11 +162,42 @@
|
|
|
162
162
|
opacity: 0.6;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
/* Focus styles - specific to each color */
|
|
166
|
+
.chip--clickable.chip--brand:focus {
|
|
166
167
|
outline: 2px solid var(--chip-color-brand-outline-foreground, #ed4c09);
|
|
167
168
|
outline-offset: 2px;
|
|
168
169
|
}
|
|
169
170
|
|
|
171
|
+
.chip--clickable.chip--accent:focus {
|
|
172
|
+
outline: 2px solid var(--chip-color-accent-outline-foreground, #0f7173);
|
|
173
|
+
outline-offset: 2px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.chip--clickable.chip--blue:focus {
|
|
177
|
+
outline: 2px solid var(--chip-color-blue-outline-foreground, #2e4780);
|
|
178
|
+
outline-offset: 2px;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.chip--clickable.chip--green:focus {
|
|
182
|
+
outline: 2px solid var(--chip-color-green-outline-foreground, #4a6045);
|
|
183
|
+
outline-offset: 2px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.chip--clickable.chip--yellow:focus {
|
|
187
|
+
outline: 2px solid var(--chip-color-yellow-outline-foreground, #eab308);
|
|
188
|
+
outline-offset: 2px;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.chip--clickable.chip--red:focus {
|
|
192
|
+
outline: 2px solid var(--chip-color-red-outline-foreground, #991b1b);
|
|
193
|
+
outline-offset: 2px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.chip--clickable.chip--neutral:focus {
|
|
197
|
+
outline: 2px solid var(--chip-color-neutral-outline-foreground, #9ca3af);
|
|
198
|
+
outline-offset: 2px;
|
|
199
|
+
}
|
|
200
|
+
|
|
170
201
|
.chip--outline.chip--black-text {
|
|
171
202
|
color: var(--chip-color-neutral-outline-text, #262626);
|
|
172
203
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Do not edit directly, this file was auto-generated.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
.vehicle-supplement {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
gap: var(--spacing-gap-gap-6, 24px);
|
|
9
|
+
padding: var(--spacing-padding-px-0, 0px);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.vehicle-supplement__content {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
gap: var(--spacing-gap-gap-4, 16px);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.vehicle-supplement__transfer-section {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
gap: var(--spacing-gap-gap-4, 16px);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.vehicle-supplement__transfer-header {
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: space-between;
|
|
28
|
+
height: 44px;
|
|
29
|
+
padding: var(--spacing-gap-gap-0, 0px);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.vehicle-supplement__transfer-title {
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
gap: var(--spacing-gap-gap-2, 8px);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.vehicle-supplement__transfer-label {
|
|
39
|
+
font-family: var(--font-font-family-body, "Satoshi"), "Inter", "Segoe UI", "system-ui", sans-serif;
|
|
40
|
+
font-weight: var(--font-weight-font-bold, 700);
|
|
41
|
+
font-size: var(--font-size-text-base, 16px);
|
|
42
|
+
line-height: calc(var(--font-leading-leading-md, 24) * 1px);
|
|
43
|
+
color: var(--checkbox-color-label-default, #303642);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.vehicle-supplement__supplements-list {
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
gap: var(--spacing-gap-gap-3, 12px);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.vehicle-supplement__supplement-row {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
justify-content: space-between;
|
|
56
|
+
height: 32px;
|
|
57
|
+
padding: var(--spacing-padding-px-0, 0px);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.vehicle-supplement__supplement-name {
|
|
61
|
+
font-family: var(--font-font-family-body, "Satoshi"), "Inter", "Segoe UI", "system-ui", sans-serif;
|
|
62
|
+
font-weight: var(--font-weight-font-medium, 500);
|
|
63
|
+
font-size: var(--font-size-text-base, 16px);
|
|
64
|
+
line-height: calc(var(--font-leading-leading-md, 24) * 1px);
|
|
65
|
+
color: var(--checkbox-color-label-default, #303642);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.vehicle-supplement__divider {
|
|
69
|
+
width: 100%;
|
|
70
|
+
height: 0;
|
|
71
|
+
border-top: 1px solid var(--color-border-default, #d9d9d9);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.vehicle-supplement__footer {
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
justify-content: flex-end;
|
|
78
|
+
gap: var(--spacing-gap-gap-4, 16px);
|
|
79
|
+
height: 36px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.vehicle-supplement__cancel-button,
|
|
83
|
+
.vehicle-supplement__done-button {
|
|
84
|
+
min-width: 114px;
|
|
85
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { TransferDocket } from '../../../types/docket/services.types';
|
|
3
|
+
import './VehicleSupplement.css';
|
|
4
|
+
export interface Supplement {
|
|
5
|
+
supplement_name: string;
|
|
6
|
+
max?: number;
|
|
7
|
+
min?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface SupplementValue {
|
|
10
|
+
transferId: number;
|
|
11
|
+
supplementName: string;
|
|
12
|
+
value: number;
|
|
13
|
+
}
|
|
14
|
+
export interface VehicleSupplementProps {
|
|
15
|
+
/** Array of available supplements */
|
|
16
|
+
supplements: Supplement[];
|
|
17
|
+
/** Array of transfers (Arrival, Inter-Hotel, Departure) */
|
|
18
|
+
transfer: TransferDocket[];
|
|
19
|
+
/** Handler called when supplement values change */
|
|
20
|
+
onChange: (values: SupplementValue[]) => void;
|
|
21
|
+
/** Handler called when Done button is clicked */
|
|
22
|
+
onDone: (values: SupplementValue[]) => void;
|
|
23
|
+
/** Handler called when Clear/Cancel button is clicked */
|
|
24
|
+
onClear: () => void;
|
|
25
|
+
/** Initial values for the supplements (optional) */
|
|
26
|
+
initialValues?: SupplementValue[];
|
|
27
|
+
/** Additional CSS classes */
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
declare const VehicleSupplement: React.FC<VehicleSupplementProps>;
|
|
31
|
+
export default VehicleSupplement;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
var __assign = (this && this.__assign) || function () {
|
|
2
|
+
__assign = Object.assign || function(t) {
|
|
3
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
4
|
+
s = arguments[i];
|
|
5
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
6
|
+
t[p] = s[p];
|
|
7
|
+
}
|
|
8
|
+
return t;
|
|
9
|
+
};
|
|
10
|
+
return __assign.apply(this, arguments);
|
|
11
|
+
};
|
|
12
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
|
+
import { useState } from 'react';
|
|
14
|
+
import Button from '../../atoms/Button/Button';
|
|
15
|
+
import Chip from '../../atoms/Chip/Chip';
|
|
16
|
+
import Icon from '../../atoms/Icon/Icon';
|
|
17
|
+
import Stepper from '../Stepper/Stepper';
|
|
18
|
+
import './VehicleSupplement.css';
|
|
19
|
+
var VehicleSupplement = function (_a) {
|
|
20
|
+
var supplements = _a.supplements, transfer = _a.transfer, onChange = _a.onChange, onDone = _a.onDone, onClear = _a.onClear, _b = _a.initialValues, initialValues = _b === void 0 ? [] : _b, _c = _a.className, className = _c === void 0 ? '' : _c;
|
|
21
|
+
// Initialize state for all supplements across all transfers
|
|
22
|
+
var _d = useState(function () {
|
|
23
|
+
// If initial values are provided, use them
|
|
24
|
+
if (initialValues.length > 0) {
|
|
25
|
+
return initialValues;
|
|
26
|
+
}
|
|
27
|
+
// Otherwise, create default values with 0
|
|
28
|
+
var defaultValues = [];
|
|
29
|
+
transfer.forEach(function (t) {
|
|
30
|
+
supplements.forEach(function (s) {
|
|
31
|
+
defaultValues.push({
|
|
32
|
+
transferId: t.IdTransfer,
|
|
33
|
+
supplementName: s.supplement_name,
|
|
34
|
+
value: 0,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
return defaultValues;
|
|
39
|
+
}), supplementValues = _d[0], setSupplementValues = _d[1];
|
|
40
|
+
var getValue = function (transferId, supplementName) {
|
|
41
|
+
var item = supplementValues.find(function (v) { return v.transferId === transferId && v.supplementName === supplementName; });
|
|
42
|
+
return (item === null || item === void 0 ? void 0 : item.value) || 0;
|
|
43
|
+
};
|
|
44
|
+
var handleSupplementChange = function (transferId, supplementName, newValue) {
|
|
45
|
+
var updatedValues = supplementValues.map(function (v) {
|
|
46
|
+
return v.transferId === transferId && v.supplementName === supplementName
|
|
47
|
+
? __assign(__assign({}, v), { value: newValue }) : v;
|
|
48
|
+
});
|
|
49
|
+
setSupplementValues(updatedValues);
|
|
50
|
+
onChange(updatedValues);
|
|
51
|
+
};
|
|
52
|
+
var getTransferTypeIcon = function (transferType) {
|
|
53
|
+
var type = transferType.toLowerCase();
|
|
54
|
+
if (type.includes('arrival') || type.includes('airport') || type.includes('inbound')) {
|
|
55
|
+
return 'plane-landing-outline';
|
|
56
|
+
}
|
|
57
|
+
if (type.includes('departure') || type.includes('outbound')) {
|
|
58
|
+
return 'plane-takeoff-outline';
|
|
59
|
+
}
|
|
60
|
+
return 'building-2-outline';
|
|
61
|
+
};
|
|
62
|
+
var getTransferTypeLabel = function (transferType) {
|
|
63
|
+
var type = transferType.toLowerCase();
|
|
64
|
+
if (type.includes('arrival') || type.includes('airport') || type.includes('inbound')) {
|
|
65
|
+
return 'Arrival';
|
|
66
|
+
}
|
|
67
|
+
if (type.includes('departure') || type.includes('outbound')) {
|
|
68
|
+
return 'Departure';
|
|
69
|
+
}
|
|
70
|
+
return 'Inter-Hotel';
|
|
71
|
+
};
|
|
72
|
+
var formatDate = function (dateStr) {
|
|
73
|
+
try {
|
|
74
|
+
var date = new Date(dateStr);
|
|
75
|
+
var day = String(date.getDate()).padStart(2, '0');
|
|
76
|
+
var month = String(date.getMonth() + 1).padStart(2, '0');
|
|
77
|
+
var year = date.getFullYear();
|
|
78
|
+
return "".concat(day, "/").concat(month, "/").concat(year);
|
|
79
|
+
}
|
|
80
|
+
catch (_a) {
|
|
81
|
+
return dateStr;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
var handleDone = function () {
|
|
85
|
+
onDone(supplementValues);
|
|
86
|
+
};
|
|
87
|
+
var baseClass = 'vehicle-supplement';
|
|
88
|
+
var classes = [baseClass, className].filter(Boolean).join(' ');
|
|
89
|
+
return (_jsxs("div", { className: classes, children: [_jsx("div", { className: "".concat(baseClass, "__content"), children: transfer.map(function (t, index) {
|
|
90
|
+
var isLast = index === transfer.length - 1;
|
|
91
|
+
var typeLabel = getTransferTypeLabel(t.TransferType);
|
|
92
|
+
var iconName = getTransferTypeIcon(t.TransferType);
|
|
93
|
+
return (_jsxs("div", { className: "".concat(baseClass, "__transfer-section"), children: [_jsxs("div", { className: "".concat(baseClass, "__transfer-header"), children: [_jsxs("div", { className: "".concat(baseClass, "__transfer-title"), children: [_jsx(Icon, { name: iconName, size: "sm" }), _jsx("span", { className: "".concat(baseClass, "__transfer-label"), children: typeLabel })] }), _jsx(Chip, { leadingIcon: "calendar", label: formatDate(t.TransferDate), size: "sm", color: "neutral", isBlackText: true })] }), _jsx("div", { className: "".concat(baseClass, "__supplements-list"), children: supplements.map(function (supplement) {
|
|
94
|
+
var currentValue = getValue(t.IdTransfer, supplement.supplement_name);
|
|
95
|
+
return (_jsxs("div", { className: "".concat(baseClass, "__supplement-row"), children: [_jsx("span", { className: "".concat(baseClass, "__supplement-name"), children: supplement.supplement_name }), _jsx(Stepper, { value: currentValue, min: supplement.min || 0, max: supplement.max || 99, onChange: function (value) {
|
|
96
|
+
return handleSupplementChange(t.IdTransfer, supplement.supplement_name, value);
|
|
97
|
+
} })] }, supplement.supplement_name));
|
|
98
|
+
}) }), !isLast && _jsx("div", { className: "".concat(baseClass, "__divider") })] }, t.IdTransfer));
|
|
99
|
+
}) }), _jsxs("div", { className: "".concat(baseClass, "__footer"), children: [_jsx(Button, { variant: "outline-secondary", size: "sm", onClick: onClear, className: "".concat(baseClass, "__cancel-button"), children: "Cancel" }), _jsx(Button, { variant: "secondary", size: "sm", onClick: handleDone, className: "".concat(baseClass, "__done-button"), children: "Done" })] })] }));
|
|
100
|
+
};
|
|
101
|
+
export default VehicleSupplement;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './VehicleSupplement';
|
|
@@ -250,4 +250,122 @@
|
|
|
250
250
|
|
|
251
251
|
.car-booking-card__cta {
|
|
252
252
|
white-space: nowrap;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* Supplement dropdown styles */
|
|
256
|
+
.car-booking-card__supplement-dropdown {
|
|
257
|
+
position: relative;
|
|
258
|
+
width: 100%;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.car-booking-card__supplement-trigger {
|
|
262
|
+
width: 100%;
|
|
263
|
+
display: flex;
|
|
264
|
+
align-items: center;
|
|
265
|
+
justify-content: space-between;
|
|
266
|
+
gap: var(--spacing-gap-gap-2, 8px);
|
|
267
|
+
padding: var(--spacing-padding-py-3, 12px) var(--spacing-padding-px-4, 16px);
|
|
268
|
+
background: var(--color-elevation-level-1, #ffffff);
|
|
269
|
+
border: var(--input-border-width-default, 1px) solid var(--color-border-default, #d4d4d4);
|
|
270
|
+
border-radius: var(--border-radius-rounded-xl, 12px);
|
|
271
|
+
font-family: var(--font-font-family-body, "Satoshi"), "Inter", "Segoe UI", "system-ui", sans-serif;
|
|
272
|
+
font-size: var(--font-size-text-base, 16px);
|
|
273
|
+
line-height: calc(var(--font-leading-leading-md, 24) * 1px);
|
|
274
|
+
color: var(--color-text-default, #262626);
|
|
275
|
+
cursor: pointer;
|
|
276
|
+
transition: border-color 0.2s ease, background-color 0.2s ease;
|
|
277
|
+
min-height: 48px;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.car-booking-card__supplement-trigger:hover:not(:disabled) {
|
|
281
|
+
border-color: var(--color-border-active-default, #0f7173);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.car-booking-card__supplement-trigger:focus {
|
|
285
|
+
outline: var(--border-width-focus, 2px) solid var(--color-border-active-default, #0f7173);
|
|
286
|
+
outline-offset: var(--spacing-base-0-5, 2px);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.car-booking-card__supplement-trigger--disabled {
|
|
290
|
+
background: var(--color-elevation-level-2, #f5f5f5);
|
|
291
|
+
cursor: not-allowed;
|
|
292
|
+
opacity: 0.6;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.car-booking-card__supplement-placeholder {
|
|
296
|
+
color: var(--color-text-subtle, #737373);
|
|
297
|
+
font-weight: var(--font-weight-font-regular, 400);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.car-booking-card__supplement-trigger--has-value .car-booking-card__supplement-placeholder {
|
|
301
|
+
color: var(--color-text-default, #262626);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.car-booking-card__supplement-chips {
|
|
305
|
+
display: flex;
|
|
306
|
+
flex-wrap: wrap;
|
|
307
|
+
gap: var(--spacing-gap-gap-2, 8px);
|
|
308
|
+
flex: 1;
|
|
309
|
+
align-items: center;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* Ensure chips are properly interactive within the button */
|
|
313
|
+
.car-booking-card__supplement-chips .chip {
|
|
314
|
+
cursor: pointer;
|
|
315
|
+
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
316
|
+
-webkit-user-select: none;
|
|
317
|
+
user-select: none;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* Hover state for chips - subtle scale and opacity change */
|
|
321
|
+
.car-booking-card__supplement-chips .chip:hover {
|
|
322
|
+
opacity: 0.85;
|
|
323
|
+
transform: translateY(-1px);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* Active/pressed state for chips */
|
|
327
|
+
.car-booking-card__supplement-chips .chip:active {
|
|
328
|
+
opacity: 0.7;
|
|
329
|
+
transform: translateY(0);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* Make close icon more prominent on chip hover */
|
|
333
|
+
.car-booking-card__supplement-chips .chip:hover .chip__icon--trailing {
|
|
334
|
+
opacity: 1;
|
|
335
|
+
transform: scale(1.1);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* Transition for chip icons */
|
|
339
|
+
.car-booking-card__supplement-chips .chip .chip__icon--trailing {
|
|
340
|
+
transition: transform 0.15s ease, opacity 0.15s ease;
|
|
341
|
+
opacity: 0.8;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.car-booking-card__supplement-icon {
|
|
345
|
+
transition: transform 0.2s ease;
|
|
346
|
+
flex-shrink: 0;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.car-booking-card__supplement-icon--open {
|
|
350
|
+
transform: rotate(180deg);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.car-booking-card__supplement-panel {
|
|
354
|
+
position: absolute;
|
|
355
|
+
top: calc(100% + var(--spacing-gap-gap-2, 8px));
|
|
356
|
+
left: 0;
|
|
357
|
+
right: 0;
|
|
358
|
+
z-index: 10;
|
|
359
|
+
background: var(--color-elevation-level-1, #ffffff);
|
|
360
|
+
border-radius: var(--border-radius-rounded-xl, 12px);
|
|
361
|
+
box-shadow:
|
|
362
|
+
var(--spacing-base-0, 0px) var(--spacing-base-1, 4px)
|
|
363
|
+
var(--backdrop-blur-backdrop-blur, 8px) var(--spacing-base-0, 0px)
|
|
364
|
+
rgba(48, 54, 66, 0.1),
|
|
365
|
+
var(--spacing-base-0, 0px) var(--spacing-base-px, 1px)
|
|
366
|
+
var(--backdrop-blur-backdrop-blur, 8px) var(--spacing-base-0, 0px)
|
|
367
|
+
rgba(48, 54, 66, 0.11);
|
|
368
|
+
padding: var(--spacing-padding-py-6, 24px);
|
|
369
|
+
max-height: 600px;
|
|
370
|
+
overflow-y: auto;
|
|
253
371
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { TransferDocket } from '../../../types/docket/services.types';
|
|
2
3
|
import { ButtonProps } from '../../atoms/Button/Button';
|
|
3
4
|
import { FeatureRowProps } from '../../molecules/FeatureRow/FeatureRow';
|
|
5
|
+
import { Supplement, SupplementValue } from '../../molecules/VehicleSupplement/VehicleSupplement';
|
|
4
6
|
import './CarBookingCard.css';
|
|
5
7
|
export type CarBookingCardSize = 'small' | 'large';
|
|
6
8
|
export type CarBookingCardState = 'default' | 'selected' | 'hover';
|
|
@@ -36,17 +38,19 @@ export interface CarBookingCardProps {
|
|
|
36
38
|
supplementMessageState?: 'default' | 'error';
|
|
37
39
|
supplementLabel?: string;
|
|
38
40
|
supplementPlaceholder?: string;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
/** Array of available supplements */
|
|
42
|
+
supplements?: Supplement[];
|
|
43
|
+
/** Array of transfers for supplement selection */
|
|
44
|
+
transfers?: TransferDocket[];
|
|
45
|
+
/** Handler when supplements are selected */
|
|
46
|
+
onSupplementChange?: (values: SupplementValue[]) => void;
|
|
43
47
|
/** Footer price */
|
|
44
48
|
totalPrice: string;
|
|
45
49
|
totalPriceLabel?: string;
|
|
46
50
|
/** Footer CTA */
|
|
47
51
|
ctaLabel: string;
|
|
48
52
|
ctaButtonProps?: Omit<ButtonProps, 'children'>;
|
|
49
|
-
onCtaClick?:
|
|
53
|
+
onCtaClick?: (event: React.MouseEvent<HTMLButtonElement>, supplements?: SupplementValue[]) => void;
|
|
50
54
|
/** Readonly mode - disables interactions and shows values as text */
|
|
51
55
|
readonly?: boolean;
|
|
52
56
|
className?: string;
|
|
@@ -10,24 +10,99 @@ var __assign = (this && this.__assign) || function () {
|
|
|
10
10
|
return __assign.apply(this, arguments);
|
|
11
11
|
};
|
|
12
12
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
|
-
import { useState } from 'react';
|
|
13
|
+
import { useEffect, useRef, useState } from 'react';
|
|
14
14
|
import Button from '../../atoms/Button/Button';
|
|
15
|
+
import Chip from '../../atoms/Chip/Chip';
|
|
15
16
|
import Divider from '../../atoms/Divider/Divider';
|
|
16
|
-
import
|
|
17
|
+
import Icon from '../../atoms/Icon/Icon';
|
|
17
18
|
import { Heading, Text } from '../../atoms/Typography/Typography';
|
|
18
19
|
import FeatureRow from '../../molecules/FeatureRow/FeatureRow';
|
|
20
|
+
import VehicleSupplement from '../../molecules/VehicleSupplement/VehicleSupplement';
|
|
19
21
|
import './CarBookingCard.css';
|
|
20
22
|
var CarBookingCard = function (_a) {
|
|
21
23
|
var _b;
|
|
22
|
-
var imageSrc = _a.imageSrc, title = _a.title, _c = _a.size, size = _c === void 0 ? 'large' : _c, _d = _a.state, state = _d === void 0 ? 'default' : _d, _e = _a.type, type = _e === void 0 ? 'default' : _e, features = _a.features, infoText = _a.infoText, _f = _a.priceTitle, priceTitle = _f === void 0 ? 'Price breakdown' : _f, _g = _a.priceRows, priceRows = _g === void 0 ? [] : _g, showSupplement = _a.showSupplement, supplementMessage = _a.supplementMessage, _h = _a.supplementMessageState, supplementMessageState = _h === void 0 ? 'error' : _h, _j = _a.supplementLabel, supplementLabel = _j === void 0 ? 'Supplement' : _j, _k = _a.supplementPlaceholder, supplementPlaceholder = _k === void 0 ? 'Select a supplement' : _k,
|
|
24
|
+
var imageSrc = _a.imageSrc, title = _a.title, _c = _a.size, size = _c === void 0 ? 'large' : _c, _d = _a.state, state = _d === void 0 ? 'default' : _d, _e = _a.type, type = _e === void 0 ? 'default' : _e, features = _a.features, infoText = _a.infoText, _f = _a.priceTitle, priceTitle = _f === void 0 ? 'Price breakdown' : _f, _g = _a.priceRows, priceRows = _g === void 0 ? [] : _g, showSupplement = _a.showSupplement, supplementMessage = _a.supplementMessage, _h = _a.supplementMessageState, supplementMessageState = _h === void 0 ? 'error' : _h, _j = _a.supplementLabel, supplementLabel = _j === void 0 ? 'Supplement' : _j, _k = _a.supplementPlaceholder, supplementPlaceholder = _k === void 0 ? 'Select a supplement' : _k, _l = _a.supplements, supplements = _l === void 0 ? [] : _l, _m = _a.transfers, transfers = _m === void 0 ? [] : _m, onSupplementChange = _a.onSupplementChange, totalPrice = _a.totalPrice, _o = _a.totalPriceLabel, totalPriceLabel = _o === void 0 ? 'Total price' : _o, ctaLabel = _a.ctaLabel, ctaButtonProps = _a.ctaButtonProps, onCtaClick = _a.onCtaClick, _p = _a.readonly, readonly = _p === void 0 ? false : _p, _q = _a.className, className = _q === void 0 ? '' : _q;
|
|
23
25
|
var _r = useState(state === 'selected'), isSelected = _r[0], setIsSelected = _r[1];
|
|
24
26
|
var _s = useState(false), isHovered = _s[0], setIsHovered = _s[1];
|
|
25
|
-
var
|
|
26
|
-
var
|
|
27
|
-
|
|
27
|
+
var _t = useState(false), isSupplementOpen = _t[0], setIsSupplementOpen = _t[1];
|
|
28
|
+
var _u = useState([]), selectedSupplements = _u[0], setSelectedSupplements = _u[1];
|
|
29
|
+
var dropdownRef = useRef(null);
|
|
30
|
+
var panelRef = useRef(null);
|
|
31
|
+
// Synchronize internal isSelected state with the state prop when it changes from parent
|
|
32
|
+
useEffect(function () {
|
|
33
|
+
setIsSelected(state === 'selected');
|
|
34
|
+
}, [state]);
|
|
35
|
+
var resolvedShowSupplement = showSupplement !== null && showSupplement !== void 0 ? showSupplement : Boolean(supplements.length > 0 && transfers.length > 0);
|
|
36
|
+
var hasSupplementsSelected = selectedSupplements.some(function (s) { return s.value > 0; });
|
|
37
|
+
// Close dropdown when clicking outside
|
|
38
|
+
useEffect(function () {
|
|
39
|
+
var handleClickOutside = function (event) {
|
|
40
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
|
41
|
+
setIsSupplementOpen(false);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
if (isSupplementOpen) {
|
|
45
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
46
|
+
}
|
|
47
|
+
return function () {
|
|
48
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
49
|
+
};
|
|
50
|
+
}, [isSupplementOpen]);
|
|
51
|
+
// Auto-scroll to panel when opened
|
|
52
|
+
useEffect(function () {
|
|
53
|
+
if (isSupplementOpen && panelRef.current) {
|
|
54
|
+
// Small delay to ensure the panel is rendered
|
|
55
|
+
setTimeout(function () {
|
|
56
|
+
var _a;
|
|
57
|
+
(_a = panelRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({
|
|
58
|
+
behavior: 'smooth',
|
|
59
|
+
block: 'nearest',
|
|
60
|
+
});
|
|
61
|
+
}, 100);
|
|
62
|
+
}
|
|
63
|
+
}, [isSupplementOpen]);
|
|
64
|
+
var getSupplementSummary = function () {
|
|
65
|
+
var summary = [];
|
|
66
|
+
var grouped = selectedSupplements.reduce(function (acc, item) {
|
|
67
|
+
if (item.value > 0) {
|
|
68
|
+
if (!acc[item.supplementName]) {
|
|
69
|
+
acc[item.supplementName] = 0;
|
|
70
|
+
}
|
|
71
|
+
acc[item.supplementName] += item.value;
|
|
72
|
+
}
|
|
73
|
+
return acc;
|
|
74
|
+
}, {});
|
|
75
|
+
Object.entries(grouped).forEach(function (_a) {
|
|
76
|
+
var name = _a[0], count = _a[1];
|
|
77
|
+
summary.push("".concat(name, " x").concat(count));
|
|
78
|
+
});
|
|
79
|
+
return summary;
|
|
80
|
+
};
|
|
81
|
+
var handleSupplementDone = function (values) {
|
|
82
|
+
setSelectedSupplements(values);
|
|
83
|
+
setIsSupplementOpen(false);
|
|
84
|
+
onSupplementChange === null || onSupplementChange === void 0 ? void 0 : onSupplementChange(values);
|
|
85
|
+
};
|
|
86
|
+
var handleSupplementClear = function () {
|
|
87
|
+
setIsSupplementOpen(false);
|
|
88
|
+
};
|
|
89
|
+
var handleSupplementToggle = function () {
|
|
90
|
+
if (!supplementMessage) {
|
|
91
|
+
setIsSupplementOpen(!isSupplementOpen);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
var handleRemoveSupplement = function (supplementName) {
|
|
95
|
+
var updatedValues = selectedSupplements.map(function (s) {
|
|
96
|
+
return s.supplementName === supplementName ? __assign(__assign({}, s), { value: 0 }) : s;
|
|
97
|
+
});
|
|
98
|
+
setSelectedSupplements(updatedValues);
|
|
99
|
+
onSupplementChange === null || onSupplementChange === void 0 ? void 0 : onSupplementChange(updatedValues);
|
|
100
|
+
};
|
|
101
|
+
// Handle CTA click: toggle between "Add to quote" and "Selected" and pass supplement data
|
|
28
102
|
var handleCtaClick = function (e) {
|
|
29
103
|
setIsSelected(!isSelected);
|
|
30
|
-
|
|
104
|
+
// Pass the selected supplements to the parent component
|
|
105
|
+
onCtaClick === null || onCtaClick === void 0 ? void 0 : onCtaClick(e, selectedSupplements.length > 0 ? selectedSupplements : undefined);
|
|
31
106
|
};
|
|
32
107
|
// Determine button state based on selection and hover
|
|
33
108
|
var shouldShowRemove = isSelected && isHovered;
|
|
@@ -71,6 +146,12 @@ var CarBookingCard = function (_a) {
|
|
|
71
146
|
return (_jsxs("article", { className: classes, children: [_jsx("div", { className: "car-booking-card__image-wrap", children: _jsx("img", { className: "car-booking-card__image", src: imageSrc, alt: "" }) }), _jsx("div", { className: "car-booking-card__active-divider" }), _jsxs("div", { className: "car-booking-card__body", children: [_jsxs("section", { className: "car-booking-card__section car-booking-card__section--content", children: [_jsxs("div", { className: "car-booking-card__title", children: [_jsx("span", { className: "car-booking-card__title-marker", "aria-hidden": "true" }), _jsx(Heading, { level: 4, variant: "bold", color: "accent", className: "car-booking-card__title-text", children: title })] }), _jsx("div", { className: "car-booking-card__features", children: features.map(function (feature, idx) {
|
|
72
147
|
var _a;
|
|
73
148
|
return (_jsx(FeatureRow, __assign({}, feature, { className: "car-booking-card__feature-row ".concat((_a = feature.className) !== null && _a !== void 0 ? _a : '').trim() }), "".concat(feature.label, "-").concat(idx)));
|
|
74
|
-
}) }), infoText && !readonly && (_jsxs("div", { className: "car-booking-card__info", children: [_jsx("span", { className: "car-booking-card__info-icon", "aria-hidden": "true", children: "i" }), _jsx(Text, { size: "sm", leading: "5", variant: "regular", color: "default", className: "car-booking-card__info-text", children: infoText })] }))] }), resolvedShowSupplement && !readonly && (_jsxs("section", { className: "car-booking-card__section car-booking-card__section--supplement", children: [_jsx(Divider, { variant: "dashed", className: "car-booking-card__dashed-divider" }), _jsxs("div", { className: "car-booking-card__supplement-header", children: [_jsx(Text, { as: "h3", size: "md", variant: "bold", leading: "none", color: "default", className: "car-booking-card__section-title", children: supplementLabel }), supplementMessage && (_jsx(Text, { as: "p", size: "sm", leading: "5", variant: "regular", className: "car-booking-card__supplement-message ".concat(supplementMessageState === 'error' ? 'car-booking-card__supplement-message--error' : '').trim(), children: supplementMessage }))] }),
|
|
149
|
+
}) }), infoText && !readonly && (_jsxs("div", { className: "car-booking-card__info", children: [_jsx("span", { className: "car-booking-card__info-icon", "aria-hidden": "true", children: "i" }), _jsx(Text, { size: "sm", leading: "5", variant: "regular", color: "default", className: "car-booking-card__info-text", children: infoText })] }))] }), resolvedShowSupplement && !readonly && (_jsxs("section", { className: "car-booking-card__section car-booking-card__section--supplement", children: [_jsx(Divider, { variant: "dashed", className: "car-booking-card__dashed-divider" }), _jsxs("div", { className: "car-booking-card__supplement-header", children: [_jsx(Text, { as: "h3", size: "md", variant: "bold", leading: "none", color: "default", className: "car-booking-card__section-title", children: supplementLabel }), supplementMessage && (_jsx(Text, { as: "p", size: "sm", leading: "5", variant: "regular", className: "car-booking-card__supplement-message ".concat(supplementMessageState === 'error' ? 'car-booking-card__supplement-message--error' : '').trim(), children: supplementMessage }))] }), _jsxs("div", { className: "car-booking-card__supplement-dropdown", ref: dropdownRef, children: [_jsxs("button", { type: "button", className: "car-booking-card__supplement-trigger ".concat(hasSupplementsSelected ? 'car-booking-card__supplement-trigger--has-value' : '', " ").concat(supplementMessage ? 'car-booking-card__supplement-trigger--disabled' : ''), onClick: handleSupplementToggle, disabled: Boolean(supplementMessage), children: [hasSupplementsSelected ? (_jsx("div", { className: "car-booking-card__supplement-chips", children: getSupplementSummary().map(function (summary, idx) {
|
|
150
|
+
var name = summary.split(' x')[0];
|
|
151
|
+
return (_jsx(Chip, { label: summary, size: "sm", color: "accent", trailingIcon: "close", onClick: function (e) {
|
|
152
|
+
e.stopPropagation();
|
|
153
|
+
handleRemoveSupplement(name);
|
|
154
|
+
} }, idx));
|
|
155
|
+
}) })) : (_jsx("span", { className: "car-booking-card__supplement-placeholder", children: supplementPlaceholder })), _jsx(Icon, { name: "chevron-down", size: "sm", className: "car-booking-card__supplement-icon ".concat(isSupplementOpen ? 'car-booking-card__supplement-icon--open' : '') })] }), isSupplementOpen && (_jsx("div", { className: "car-booking-card__supplement-panel", ref: panelRef, children: _jsx(VehicleSupplement, { supplements: supplements, transfer: transfers, initialValues: selectedSupplements, onChange: function () { }, onDone: handleSupplementDone, onClear: handleSupplementClear }) }))] })] })), priceRows.length > 0 && !readonly && (_jsxs("section", { className: "car-booking-card__section car-booking-card__section--price", children: [_jsx(Divider, { variant: "dashed", className: "car-booking-card__dashed-divider" }), _jsxs("div", { className: "car-booking-card__price-content", children: [_jsx(Text, { as: "h3", size: "md", variant: "bold", leading: "none", color: "default", className: "car-booking-card__section-title", children: priceTitle }), _jsx("div", { className: "car-booking-card__price-rows", children: priceRows.map(function (row, idx) { return (_jsxs("div", { className: "car-booking-card__price-row", children: [_jsx(Text, { size: "sm", leading: "5", variant: "regular", color: "subtle", className: "car-booking-card__price-label", children: row.label }), _jsx(Text, { size: "sm", leading: "5", variant: "bold", color: "subtle", className: "car-booking-card__price-value", children: row.value })] }, "".concat(row.label, "-").concat(idx))); }) })] }), _jsx(Divider, { variant: "dashed", className: "car-booking-card__dashed-divider" })] })), !readonly && _jsxs("footer", { className: "car-booking-card__footer", children: [_jsxs("div", { className: "car-booking-card__total", children: [_jsx(Text, { size: "base", variant: "bold", color: "accent", className: "car-booking-card__total-price", children: totalPrice }), _jsx(Text, { size: "sm", variant: "regular", color: "subtle", className: "car-booking-card__total-label", children: totalPriceLabel })] }), !readonly && (_jsx("div", { onMouseEnter: function () { return setIsHovered(true); }, onMouseLeave: function () { return setIsHovered(false); }, className: "car-booking-card__cta-wrapper", children: _jsx(Button, __assign({}, resolvedCtaButtonProps, { onClick: handleCtaClick, className: "car-booking-card__cta ".concat((_b = resolvedCtaButtonProps === null || resolvedCtaButtonProps === void 0 ? void 0 : resolvedCtaButtonProps.className) !== null && _b !== void 0 ? _b : '').trim(), children: resolvedCtaLabel })) }))] })] })] }));
|
|
75
156
|
};
|
|
76
157
|
export default CarBookingCard;
|
package/dist/index.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export { ServiceLanguages } from './components/molecules/ServiceLanguages/Servic
|
|
|
39
39
|
export { default as ServiceSelector } from './components/molecules/ServiceSelector/ServiceSelector';
|
|
40
40
|
export { default as Stepper } from './components/molecules/Stepper/Stepper';
|
|
41
41
|
export { default as TextWithIcon } from './components/molecules/TextWithIcon/TextWithIcon';
|
|
42
|
+
export { default as VehicleSupplement } from './components/molecules/VehicleSupplement/VehicleSupplement';
|
|
43
|
+
export type { VehicleSupplementProps, Supplement, SupplementValue } from './components/molecules/VehicleSupplement';
|
|
42
44
|
export { default as TimelineItem } from './components/molecules/TimelineItem/TimelineItem';
|
|
43
45
|
export { default as Toast } from './components/molecules/Toast/Toast';
|
|
44
46
|
export * from './components/molecules/TooltipDisplay/TooltipDisplay';
|
package/dist/index.js
CHANGED
|
@@ -41,6 +41,7 @@ export { ServiceLanguages } from './components/molecules/ServiceLanguages/Servic
|
|
|
41
41
|
export { default as ServiceSelector } from './components/molecules/ServiceSelector/ServiceSelector';
|
|
42
42
|
export { default as Stepper } from './components/molecules/Stepper/Stepper';
|
|
43
43
|
export { default as TextWithIcon } from './components/molecules/TextWithIcon/TextWithIcon';
|
|
44
|
+
export { default as VehicleSupplement } from './components/molecules/VehicleSupplement/VehicleSupplement';
|
|
44
45
|
export { default as TimelineItem } from './components/molecules/TimelineItem/TimelineItem';
|
|
45
46
|
export { default as Toast } from './components/molecules/Toast/Toast';
|
|
46
47
|
export * from './components/molecules/TooltipDisplay/TooltipDisplay';
|
package/package.json
CHANGED
|
@@ -156,11 +156,42 @@
|
|
|
156
156
|
opacity: 0.6;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
|
|
159
|
+
/* Focus styles - specific to each color */
|
|
160
|
+
.chip--clickable.chip--brand:focus {
|
|
160
161
|
outline: 2px solid var(--chip-color-brand-outline-foreground, #ed4c09);
|
|
161
162
|
outline-offset: 2px;
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
.chip--clickable.chip--accent:focus {
|
|
166
|
+
outline: 2px solid var(--chip-color-accent-outline-foreground, #0f7173);
|
|
167
|
+
outline-offset: 2px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.chip--clickable.chip--blue:focus {
|
|
171
|
+
outline: 2px solid var(--chip-color-blue-outline-foreground, #2e4780);
|
|
172
|
+
outline-offset: 2px;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.chip--clickable.chip--green:focus {
|
|
176
|
+
outline: 2px solid var(--chip-color-green-outline-foreground, #4a6045);
|
|
177
|
+
outline-offset: 2px;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.chip--clickable.chip--yellow:focus {
|
|
181
|
+
outline: 2px solid var(--chip-color-yellow-outline-foreground, #eab308);
|
|
182
|
+
outline-offset: 2px;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.chip--clickable.chip--red:focus {
|
|
186
|
+
outline: 2px solid var(--chip-color-red-outline-foreground, #991b1b);
|
|
187
|
+
outline-offset: 2px;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.chip--clickable.chip--neutral:focus {
|
|
191
|
+
outline: 2px solid var(--chip-color-neutral-outline-foreground, #9ca3af);
|
|
192
|
+
outline-offset: 2px;
|
|
193
|
+
}
|
|
194
|
+
|
|
164
195
|
.chip--outline.chip--black-text {
|
|
165
196
|
color: var(--chip-color-neutral-outline-text, #262626);
|
|
166
197
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
.vehicle-supplement {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--spacing-gap-gap-6, 24px);
|
|
5
|
+
padding: var(--spacing-padding-px-0, 0px);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.vehicle-supplement__content {
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
gap: var(--spacing-gap-gap-4, 16px);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.vehicle-supplement__transfer-section {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
gap: var(--spacing-gap-gap-4, 16px);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.vehicle-supplement__transfer-header {
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
justify-content: space-between;
|
|
24
|
+
height: 44px;
|
|
25
|
+
padding: var(--spacing-gap-gap-0, 0px);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.vehicle-supplement__transfer-title {
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: var(--spacing-gap-gap-2, 8px);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.vehicle-supplement__transfer-label {
|
|
35
|
+
font-family: var(--font-font-family-body, "Satoshi"), "Inter", "Segoe UI", "system-ui", sans-serif;
|
|
36
|
+
font-weight: var(--font-weight-font-bold, 700);
|
|
37
|
+
font-size: var(--font-size-text-base, 16px);
|
|
38
|
+
line-height: calc(var(--font-leading-leading-md, 24) * 1px);
|
|
39
|
+
color: var(--checkbox-color-label-default, #303642);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.vehicle-supplement__supplements-list {
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
gap: var(--spacing-gap-gap-3, 12px);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.vehicle-supplement__supplement-row {
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: space-between;
|
|
52
|
+
height: 32px;
|
|
53
|
+
padding: var(--spacing-padding-px-0, 0px);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.vehicle-supplement__supplement-name {
|
|
57
|
+
font-family: var(--font-font-family-body, "Satoshi"), "Inter", "Segoe UI", "system-ui", sans-serif;
|
|
58
|
+
font-weight: var(--font-weight-font-medium, 500);
|
|
59
|
+
font-size: var(--font-size-text-base, 16px);
|
|
60
|
+
line-height: calc(var(--font-leading-leading-md, 24) * 1px);
|
|
61
|
+
color: var(--checkbox-color-label-default, #303642);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.vehicle-supplement__divider {
|
|
65
|
+
width: 100%;
|
|
66
|
+
height: 0;
|
|
67
|
+
border-top: 1px solid var(--color-border-default, #d9d9d9);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.vehicle-supplement__footer {
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: flex-end;
|
|
74
|
+
gap: var(--spacing-gap-gap-4, 16px);
|
|
75
|
+
height: 36px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.vehicle-supplement__cancel-button,
|
|
79
|
+
.vehicle-supplement__done-button {
|
|
80
|
+
min-width: 114px;
|
|
81
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { TransferDocket } from '../../../types/docket/services.types';
|
|
3
|
+
import Button from '../../atoms/Button/Button';
|
|
4
|
+
import Chip from '../../atoms/Chip/Chip';
|
|
5
|
+
import Icon from '../../atoms/Icon/Icon';
|
|
6
|
+
import Stepper from '../Stepper/Stepper';
|
|
7
|
+
import './VehicleSupplement.css';
|
|
8
|
+
|
|
9
|
+
export interface Supplement {
|
|
10
|
+
supplement_name: string;
|
|
11
|
+
max?: number;
|
|
12
|
+
min?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SupplementValue {
|
|
16
|
+
transferId: number;
|
|
17
|
+
supplementName: string;
|
|
18
|
+
value: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface VehicleSupplementProps {
|
|
22
|
+
/** Array of available supplements */
|
|
23
|
+
supplements: Supplement[];
|
|
24
|
+
/** Array of transfers (Arrival, Inter-Hotel, Departure) */
|
|
25
|
+
transfer: TransferDocket[];
|
|
26
|
+
/** Handler called when supplement values change */
|
|
27
|
+
onChange: (values: SupplementValue[]) => void;
|
|
28
|
+
/** Handler called when Done button is clicked */
|
|
29
|
+
onDone: (values: SupplementValue[]) => void;
|
|
30
|
+
/** Handler called when Clear/Cancel button is clicked */
|
|
31
|
+
onClear: () => void;
|
|
32
|
+
/** Initial values for the supplements (optional) */
|
|
33
|
+
initialValues?: SupplementValue[];
|
|
34
|
+
/** Additional CSS classes */
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const VehicleSupplement: React.FC<VehicleSupplementProps> = ({
|
|
39
|
+
supplements,
|
|
40
|
+
transfer,
|
|
41
|
+
onChange,
|
|
42
|
+
onDone,
|
|
43
|
+
onClear,
|
|
44
|
+
initialValues = [],
|
|
45
|
+
className = '',
|
|
46
|
+
}) => {
|
|
47
|
+
// Initialize state for all supplements across all transfers
|
|
48
|
+
const [supplementValues, setSupplementValues] = useState<SupplementValue[]>(() => {
|
|
49
|
+
// If initial values are provided, use them
|
|
50
|
+
if (initialValues.length > 0) {
|
|
51
|
+
return initialValues;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Otherwise, create default values with 0
|
|
55
|
+
const defaultValues: SupplementValue[] = [];
|
|
56
|
+
transfer.forEach((t) => {
|
|
57
|
+
supplements.forEach((s) => {
|
|
58
|
+
defaultValues.push({
|
|
59
|
+
transferId: t.IdTransfer,
|
|
60
|
+
supplementName: s.supplement_name,
|
|
61
|
+
value: 0,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
return defaultValues;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const getValue = (transferId: number, supplementName: string): number => {
|
|
69
|
+
const item = supplementValues.find(
|
|
70
|
+
(v) => v.transferId === transferId && v.supplementName === supplementName
|
|
71
|
+
);
|
|
72
|
+
return item?.value || 0;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleSupplementChange = (
|
|
76
|
+
transferId: number,
|
|
77
|
+
supplementName: string,
|
|
78
|
+
newValue: number
|
|
79
|
+
) => {
|
|
80
|
+
const updatedValues = supplementValues.map((v) =>
|
|
81
|
+
v.transferId === transferId && v.supplementName === supplementName
|
|
82
|
+
? { ...v, value: newValue }
|
|
83
|
+
: v
|
|
84
|
+
);
|
|
85
|
+
setSupplementValues(updatedValues);
|
|
86
|
+
onChange(updatedValues);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const getTransferTypeIcon = (transferType: string) => {
|
|
90
|
+
const type = transferType.toLowerCase();
|
|
91
|
+
if (type.includes('arrival') || type.includes('airport') || type.includes('inbound')) {
|
|
92
|
+
return 'plane-landing-outline';
|
|
93
|
+
}
|
|
94
|
+
if (type.includes('departure') || type.includes('outbound')) {
|
|
95
|
+
return 'plane-takeoff-outline';
|
|
96
|
+
}
|
|
97
|
+
return 'building-2-outline';
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const getTransferTypeLabel = (transferType: string): string => {
|
|
101
|
+
const type = transferType.toLowerCase();
|
|
102
|
+
if (type.includes('arrival') || type.includes('airport') || type.includes('inbound')) {
|
|
103
|
+
return 'Arrival';
|
|
104
|
+
}
|
|
105
|
+
if (type.includes('departure') || type.includes('outbound')) {
|
|
106
|
+
return 'Departure';
|
|
107
|
+
}
|
|
108
|
+
return 'Inter-Hotel';
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const formatDate = (dateStr: string): string => {
|
|
112
|
+
try {
|
|
113
|
+
const date = new Date(dateStr);
|
|
114
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
115
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
116
|
+
const year = date.getFullYear();
|
|
117
|
+
return `${day}/${month}/${year}`;
|
|
118
|
+
} catch {
|
|
119
|
+
return dateStr;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const handleDone = () => {
|
|
124
|
+
onDone(supplementValues);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const baseClass = 'vehicle-supplement';
|
|
128
|
+
const classes = [baseClass, className].filter(Boolean).join(' ');
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div className={classes}>
|
|
132
|
+
<div className={`${baseClass}__content`}>
|
|
133
|
+
{transfer.map((t, index) => {
|
|
134
|
+
const isLast = index === transfer.length - 1;
|
|
135
|
+
const typeLabel = getTransferTypeLabel(t.TransferType);
|
|
136
|
+
const iconName = getTransferTypeIcon(t.TransferType);
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div key={t.IdTransfer} className={`${baseClass}__transfer-section`}>
|
|
140
|
+
<div className={`${baseClass}__transfer-header`}>
|
|
141
|
+
<div className={`${baseClass}__transfer-title`}>
|
|
142
|
+
<Icon name={iconName as any} size="sm" />
|
|
143
|
+
<span className={`${baseClass}__transfer-label`}>{typeLabel}</span>
|
|
144
|
+
</div>
|
|
145
|
+
<Chip
|
|
146
|
+
leadingIcon="calendar"
|
|
147
|
+
label={formatDate(t.TransferDate)}
|
|
148
|
+
size="sm"
|
|
149
|
+
color="neutral"
|
|
150
|
+
isBlackText
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div className={`${baseClass}__supplements-list`}>
|
|
155
|
+
{supplements.map((supplement) => {
|
|
156
|
+
const currentValue = getValue(t.IdTransfer, supplement.supplement_name);
|
|
157
|
+
return (
|
|
158
|
+
<div
|
|
159
|
+
key={supplement.supplement_name}
|
|
160
|
+
className={`${baseClass}__supplement-row`}
|
|
161
|
+
>
|
|
162
|
+
<span className={`${baseClass}__supplement-name`}>
|
|
163
|
+
{supplement.supplement_name}
|
|
164
|
+
</span>
|
|
165
|
+
<Stepper
|
|
166
|
+
value={currentValue}
|
|
167
|
+
min={supplement.min || 0}
|
|
168
|
+
max={supplement.max || 99}
|
|
169
|
+
onChange={(value) =>
|
|
170
|
+
handleSupplementChange(t.IdTransfer, supplement.supplement_name, value)
|
|
171
|
+
}
|
|
172
|
+
/>
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
})}
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
{!isLast && <div className={`${baseClass}__divider`} />}
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
})}
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<div className={`${baseClass}__footer`}>
|
|
185
|
+
<Button
|
|
186
|
+
variant="outline-secondary"
|
|
187
|
+
size="sm"
|
|
188
|
+
onClick={onClear}
|
|
189
|
+
className={`${baseClass}__cancel-button`}
|
|
190
|
+
>
|
|
191
|
+
Cancel
|
|
192
|
+
</Button>
|
|
193
|
+
<Button
|
|
194
|
+
variant="secondary"
|
|
195
|
+
size="sm"
|
|
196
|
+
onClick={handleDone}
|
|
197
|
+
className={`${baseClass}__done-button`}
|
|
198
|
+
>
|
|
199
|
+
Done
|
|
200
|
+
</Button>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export default VehicleSupplement;
|
|
@@ -248,4 +248,121 @@
|
|
|
248
248
|
white-space: nowrap;
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
/* Supplement dropdown styles */
|
|
252
|
+
.car-booking-card__supplement-dropdown {
|
|
253
|
+
position: relative;
|
|
254
|
+
width: 100%;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.car-booking-card__supplement-trigger {
|
|
258
|
+
width: 100%;
|
|
259
|
+
display: flex;
|
|
260
|
+
align-items: center;
|
|
261
|
+
justify-content: space-between;
|
|
262
|
+
gap: var(--spacing-gap-gap-2, 8px);
|
|
263
|
+
padding: var(--spacing-padding-py-3, 12px) var(--spacing-padding-px-4, 16px);
|
|
264
|
+
background: var(--color-elevation-level-1, #ffffff);
|
|
265
|
+
border: var(--input-border-width-default, 1px) solid var(--color-border-default, #d4d4d4);
|
|
266
|
+
border-radius: var(--border-radius-rounded-xl, 12px);
|
|
267
|
+
font-family: var(--font-font-family-body, "Satoshi"), "Inter", "Segoe UI", "system-ui", sans-serif;
|
|
268
|
+
font-size: var(--font-size-text-base, 16px);
|
|
269
|
+
line-height: calc(var(--font-leading-leading-md, 24) * 1px);
|
|
270
|
+
color: var(--color-text-default, #262626);
|
|
271
|
+
cursor: pointer;
|
|
272
|
+
transition: border-color 0.2s ease, background-color 0.2s ease;
|
|
273
|
+
min-height: 48px;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.car-booking-card__supplement-trigger:hover:not(:disabled) {
|
|
277
|
+
border-color: var(--color-border-active-default, #0f7173);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.car-booking-card__supplement-trigger:focus {
|
|
281
|
+
outline: var(--border-width-focus, 2px) solid var(--color-border-active-default, #0f7173);
|
|
282
|
+
outline-offset: var(--spacing-base-0-5, 2px);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.car-booking-card__supplement-trigger--disabled {
|
|
286
|
+
background: var(--color-elevation-level-2, #f5f5f5);
|
|
287
|
+
cursor: not-allowed;
|
|
288
|
+
opacity: 0.6;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.car-booking-card__supplement-placeholder {
|
|
292
|
+
color: var(--color-text-subtle, #737373);
|
|
293
|
+
font-weight: var(--font-weight-font-regular, 400);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.car-booking-card__supplement-trigger--has-value .car-booking-card__supplement-placeholder {
|
|
297
|
+
color: var(--color-text-default, #262626);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.car-booking-card__supplement-chips {
|
|
301
|
+
display: flex;
|
|
302
|
+
flex-wrap: wrap;
|
|
303
|
+
gap: var(--spacing-gap-gap-2, 8px);
|
|
304
|
+
flex: 1;
|
|
305
|
+
align-items: center;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* Ensure chips are properly interactive within the button */
|
|
309
|
+
.car-booking-card__supplement-chips .chip {
|
|
310
|
+
cursor: pointer;
|
|
311
|
+
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
312
|
+
user-select: none;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* Hover state for chips - subtle scale and opacity change */
|
|
316
|
+
.car-booking-card__supplement-chips .chip:hover {
|
|
317
|
+
opacity: 0.85;
|
|
318
|
+
transform: translateY(-1px);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/* Active/pressed state for chips */
|
|
322
|
+
.car-booking-card__supplement-chips .chip:active {
|
|
323
|
+
opacity: 0.7;
|
|
324
|
+
transform: translateY(0);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* Make close icon more prominent on chip hover */
|
|
328
|
+
.car-booking-card__supplement-chips .chip:hover .chip__icon--trailing {
|
|
329
|
+
opacity: 1;
|
|
330
|
+
transform: scale(1.1);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* Transition for chip icons */
|
|
334
|
+
.car-booking-card__supplement-chips .chip .chip__icon--trailing {
|
|
335
|
+
transition: transform 0.15s ease, opacity 0.15s ease;
|
|
336
|
+
opacity: 0.8;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.car-booking-card__supplement-icon {
|
|
340
|
+
transition: transform 0.2s ease;
|
|
341
|
+
flex-shrink: 0;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.car-booking-card__supplement-icon--open {
|
|
345
|
+
transform: rotate(180deg);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.car-booking-card__supplement-panel {
|
|
349
|
+
position: absolute;
|
|
350
|
+
top: calc(100% + var(--spacing-gap-gap-2, 8px));
|
|
351
|
+
left: 0;
|
|
352
|
+
right: 0;
|
|
353
|
+
z-index: 10;
|
|
354
|
+
background: var(--color-elevation-level-1, #ffffff);
|
|
355
|
+
border-radius: var(--border-radius-rounded-xl, 12px);
|
|
356
|
+
box-shadow:
|
|
357
|
+
var(--spacing-base-0, 0px) var(--spacing-base-1, 4px)
|
|
358
|
+
var(--backdrop-blur-backdrop-blur, 8px) var(--spacing-base-0, 0px)
|
|
359
|
+
rgba(48, 54, 66, 0.1),
|
|
360
|
+
var(--spacing-base-0, 0px) var(--spacing-base-px, 1px)
|
|
361
|
+
var(--backdrop-blur-backdrop-blur, 8px) var(--spacing-base-0, 0px)
|
|
362
|
+
rgba(48, 54, 66, 0.11);
|
|
363
|
+
padding: var(--spacing-padding-py-6, 24px);
|
|
364
|
+
max-height: 600px;
|
|
365
|
+
overflow-y: auto;
|
|
366
|
+
}
|
|
367
|
+
|
|
251
368
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { TransferDocket } from '../../../types/docket/services.types';
|
|
2
3
|
import Button, { ButtonProps } from '../../atoms/Button/Button';
|
|
4
|
+
import Chip from '../../atoms/Chip/Chip';
|
|
3
5
|
import Divider from '../../atoms/Divider/Divider';
|
|
4
|
-
import
|
|
6
|
+
import Icon from '../../atoms/Icon/Icon';
|
|
5
7
|
import { Heading, Text } from '../../atoms/Typography/Typography';
|
|
6
8
|
import FeatureRow, { FeatureRowProps } from '../../molecules/FeatureRow/FeatureRow';
|
|
9
|
+
import VehicleSupplement, { Supplement, SupplementValue } from '../../molecules/VehicleSupplement/VehicleSupplement';
|
|
7
10
|
import './CarBookingCard.css';
|
|
8
11
|
|
|
9
12
|
export type CarBookingCardSize = 'small' | 'large';
|
|
@@ -46,10 +49,12 @@ export interface CarBookingCardProps {
|
|
|
46
49
|
supplementMessageState?: 'default' | 'error';
|
|
47
50
|
supplementLabel?: string;
|
|
48
51
|
supplementPlaceholder?: string;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
/** Array of available supplements */
|
|
53
|
+
supplements?: Supplement[];
|
|
54
|
+
/** Array of transfers for supplement selection */
|
|
55
|
+
transfers?: TransferDocket[];
|
|
56
|
+
/** Handler when supplements are selected */
|
|
57
|
+
onSupplementChange?: (values: SupplementValue[]) => void;
|
|
53
58
|
|
|
54
59
|
/** Footer price */
|
|
55
60
|
totalPrice: string;
|
|
@@ -58,7 +63,7 @@ export interface CarBookingCardProps {
|
|
|
58
63
|
/** Footer CTA */
|
|
59
64
|
ctaLabel: string;
|
|
60
65
|
ctaButtonProps?: Omit<ButtonProps, 'children'>;
|
|
61
|
-
onCtaClick?:
|
|
66
|
+
onCtaClick?: (event: React.MouseEvent<HTMLButtonElement>, supplements?: SupplementValue[]) => void;
|
|
62
67
|
|
|
63
68
|
/** Readonly mode - disables interactions and shows values as text */
|
|
64
69
|
readonly?: boolean;
|
|
@@ -81,10 +86,9 @@ const CarBookingCard: React.FC<CarBookingCardProps> = ({
|
|
|
81
86
|
supplementMessageState = 'error',
|
|
82
87
|
supplementLabel = 'Supplement',
|
|
83
88
|
supplementPlaceholder = 'Select a supplement',
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
onSupplementSelect,
|
|
89
|
+
supplements = [],
|
|
90
|
+
transfers = [],
|
|
91
|
+
onSupplementChange,
|
|
88
92
|
totalPrice,
|
|
89
93
|
totalPriceLabel = 'Total price',
|
|
90
94
|
ctaLabel,
|
|
@@ -95,16 +99,99 @@ const CarBookingCard: React.FC<CarBookingCardProps> = ({
|
|
|
95
99
|
}) => {
|
|
96
100
|
const [isSelected, setIsSelected] = useState(state === 'selected');
|
|
97
101
|
const [isHovered, setIsHovered] = useState(false);
|
|
102
|
+
const [isSupplementOpen, setIsSupplementOpen] = useState(false);
|
|
103
|
+
const [selectedSupplements, setSelectedSupplements] = useState<SupplementValue[]>([]);
|
|
104
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
105
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
106
|
+
|
|
107
|
+
// Synchronize internal isSelected state with the state prop when it changes from parent
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
setIsSelected(state === 'selected');
|
|
110
|
+
}, [state]);
|
|
98
111
|
|
|
99
112
|
const resolvedShowSupplement =
|
|
100
|
-
showSupplement ?? Boolean(
|
|
101
|
-
|
|
102
|
-
|
|
113
|
+
showSupplement ?? Boolean(supplements.length > 0 && transfers.length > 0);
|
|
114
|
+
|
|
115
|
+
const hasSupplementsSelected = selectedSupplements.some((s) => s.value > 0);
|
|
116
|
+
|
|
117
|
+
// Close dropdown when clicking outside
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
120
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
121
|
+
setIsSupplementOpen(false);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
if (isSupplementOpen) {
|
|
126
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return () => {
|
|
130
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
131
|
+
};
|
|
132
|
+
}, [isSupplementOpen]);
|
|
133
|
+
|
|
134
|
+
// Auto-scroll to panel when opened
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (isSupplementOpen && panelRef.current) {
|
|
137
|
+
// Small delay to ensure the panel is rendered
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
panelRef.current?.scrollIntoView({
|
|
140
|
+
behavior: 'smooth',
|
|
141
|
+
block: 'nearest',
|
|
142
|
+
});
|
|
143
|
+
}, 100);
|
|
144
|
+
}
|
|
145
|
+
}, [isSupplementOpen]);
|
|
146
|
+
|
|
147
|
+
const getSupplementSummary = (): string[] => {
|
|
148
|
+
const summary: string[] = [];
|
|
149
|
+
const grouped = selectedSupplements.reduce((acc, item) => {
|
|
150
|
+
if (item.value > 0) {
|
|
151
|
+
if (!acc[item.supplementName]) {
|
|
152
|
+
acc[item.supplementName] = 0;
|
|
153
|
+
}
|
|
154
|
+
acc[item.supplementName] += item.value;
|
|
155
|
+
}
|
|
156
|
+
return acc;
|
|
157
|
+
}, {} as Record<string, number>);
|
|
158
|
+
|
|
159
|
+
Object.entries(grouped).forEach(([name, count]) => {
|
|
160
|
+
summary.push(`${name} x${count}`);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return summary;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const handleSupplementDone = (values: SupplementValue[]) => {
|
|
167
|
+
setSelectedSupplements(values);
|
|
168
|
+
setIsSupplementOpen(false);
|
|
169
|
+
onSupplementChange?.(values);
|
|
170
|
+
};
|
|
103
171
|
|
|
104
|
-
|
|
172
|
+
const handleSupplementClear = () => {
|
|
173
|
+
setIsSupplementOpen(false);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const handleSupplementToggle = () => {
|
|
177
|
+
if (!supplementMessage) {
|
|
178
|
+
setIsSupplementOpen(!isSupplementOpen);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const handleRemoveSupplement = (supplementName: string) => {
|
|
183
|
+
const updatedValues = selectedSupplements.map((s) =>
|
|
184
|
+
s.supplementName === supplementName ? { ...s, value: 0 } : s
|
|
185
|
+
);
|
|
186
|
+
setSelectedSupplements(updatedValues);
|
|
187
|
+
onSupplementChange?.(updatedValues);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Handle CTA click: toggle between "Add to quote" and "Selected" and pass supplement data
|
|
105
191
|
const handleCtaClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
106
192
|
setIsSelected(!isSelected);
|
|
107
|
-
|
|
193
|
+
// Pass the selected supplements to the parent component
|
|
194
|
+
onCtaClick?.(e, selectedSupplements.length > 0 ? selectedSupplements : undefined);
|
|
108
195
|
};
|
|
109
196
|
|
|
110
197
|
// Determine button state based on selection and hover
|
|
@@ -215,25 +302,58 @@ const CarBookingCard: React.FC<CarBookingCardProps> = ({
|
|
|
215
302
|
</Text>
|
|
216
303
|
)}
|
|
217
304
|
</div>
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
305
|
+
|
|
306
|
+
<div className="car-booking-card__supplement-dropdown" ref={dropdownRef}>
|
|
307
|
+
<button
|
|
308
|
+
type="button"
|
|
309
|
+
className={`car-booking-card__supplement-trigger ${hasSupplementsSelected ? 'car-booking-card__supplement-trigger--has-value' : ''} ${supplementMessage ? 'car-booking-card__supplement-trigger--disabled' : ''}`}
|
|
310
|
+
onClick={handleSupplementToggle}
|
|
311
|
+
disabled={Boolean(supplementMessage)}
|
|
225
312
|
>
|
|
226
|
-
{
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
313
|
+
{hasSupplementsSelected ? (
|
|
314
|
+
<div className="car-booking-card__supplement-chips">
|
|
315
|
+
{getSupplementSummary().map((summary, idx) => {
|
|
316
|
+
const [name] = summary.split(' x');
|
|
317
|
+
return (
|
|
318
|
+
<Chip
|
|
319
|
+
key={idx}
|
|
320
|
+
label={summary}
|
|
321
|
+
size="sm"
|
|
322
|
+
color="accent"
|
|
323
|
+
trailingIcon="close"
|
|
324
|
+
onClick={(e) => {
|
|
325
|
+
e.stopPropagation();
|
|
326
|
+
handleRemoveSupplement(name);
|
|
327
|
+
}}
|
|
328
|
+
/>
|
|
329
|
+
);
|
|
330
|
+
})}
|
|
331
|
+
</div>
|
|
332
|
+
) : (
|
|
333
|
+
<span className="car-booking-card__supplement-placeholder">
|
|
334
|
+
{supplementPlaceholder}
|
|
335
|
+
</span>
|
|
336
|
+
)}
|
|
337
|
+
<Icon
|
|
338
|
+
name="chevron-down"
|
|
339
|
+
size="sm"
|
|
340
|
+
className={`car-booking-card__supplement-icon ${isSupplementOpen ? 'car-booking-card__supplement-icon--open' : ''}`}
|
|
341
|
+
/>
|
|
342
|
+
</button>
|
|
343
|
+
|
|
344
|
+
{isSupplementOpen && (
|
|
345
|
+
<div className="car-booking-card__supplement-panel" ref={panelRef}>
|
|
346
|
+
<VehicleSupplement
|
|
347
|
+
supplements={supplements}
|
|
348
|
+
transfer={transfers}
|
|
349
|
+
initialValues={selectedSupplements}
|
|
350
|
+
onChange={() => {}}
|
|
351
|
+
onDone={handleSupplementDone}
|
|
352
|
+
onClear={handleSupplementClear}
|
|
353
|
+
/>
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
356
|
+
</div>
|
|
237
357
|
</section>
|
|
238
358
|
)}
|
|
239
359
|
|