mautourco-components 0.1.2 → 0.2.0
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/Checkbox/Checkbox.js +7 -1
- package/dist/components/atoms/Icon/Icon.d.ts +1 -1
- package/dist/components/atoms/Icon/Icon.js +22 -1
- package/dist/components/atoms/Icon/icons/BuildingIcon.d.ts +8 -0
- package/dist/components/atoms/Icon/icons/BuildingIcon.js +36 -0
- package/dist/components/atoms/Icon/icons/CalendarOutlineIcon.d.ts +8 -0
- package/dist/components/atoms/Icon/icons/CalendarOutlineIcon.js +36 -0
- package/dist/components/atoms/Icon/icons/HomeIcon.d.ts +8 -0
- package/dist/components/atoms/Icon/icons/HomeIcon.js +25 -0
- package/dist/components/atoms/Icon/icons/MinusIcon.d.ts +8 -0
- package/dist/components/atoms/Icon/icons/MinusIcon.js +25 -0
- package/dist/components/atoms/Icon/icons/PlaneIcon.d.ts +8 -0
- package/dist/components/atoms/Icon/icons/PlaneIcon.js +36 -0
- package/dist/components/atoms/Icon/icons/PlusIcon.d.ts +8 -0
- package/dist/components/atoms/Icon/icons/PlusIcon.js +25 -0
- package/dist/components/atoms/Icon/icons/ShipIcon.d.ts +8 -0
- package/dist/components/atoms/Icon/icons/ShipIcon.js +36 -0
- package/dist/components/atoms/Illustration/Illustration.d.ts +14 -0
- package/dist/components/atoms/Illustration/Illustration.js +33 -0
- package/dist/components/atoms/Illustration/illustrations.d.ts +51 -0
- package/dist/components/atoms/Illustration/illustrations.js +97 -0
- package/dist/components/atoms/RatingStar/RatingStar.d.ts +40 -0
- package/dist/components/atoms/RatingStar/RatingStar.js +54 -0
- package/dist/components/atoms/SegmentedButton/SegmentedButton.d.ts +27 -0
- package/dist/components/atoms/SegmentedButton/SegmentedButton.js +49 -0
- package/dist/components/atoms/Slider/Slider.d.ts +52 -0
- package/dist/components/atoms/Slider/Slider.js +30 -0
- package/dist/components/molecules/Calendar/CalendarInput.d.ts +34 -0
- package/dist/components/molecules/Calendar/CalendarInput.js +49 -0
- package/dist/components/molecules/Calendar/DateTime.d.ts +25 -0
- package/dist/components/molecules/Calendar/DateTime.js +106 -0
- package/dist/components/molecules/Calendar/TimePicker.d.ts +16 -0
- package/dist/components/molecules/Calendar/TimePicker.js +91 -0
- package/dist/components/molecules/LocationDropdown/LocationDropdown.d.ts +34 -0
- package/dist/components/molecules/LocationDropdown/LocationDropdown.js +120 -0
- package/dist/components/molecules/LocationDropdown/index.d.ts +2 -0
- package/dist/components/molecules/LocationDropdown/index.js +1 -0
- package/dist/components/molecules/RatingTab/RatingTab.d.ts +39 -0
- package/dist/components/molecules/RatingTab/RatingTab.js +41 -0
- package/dist/components/molecules/TabGroup/TabGroup.d.ts +17 -0
- package/dist/components/molecules/TabGroup/TabGroup.js +30 -0
- package/dist/components/organisms/CardContainer/CardContainer.d.ts +37 -0
- package/dist/components/organisms/CardContainer/CardContainer.js +27 -0
- package/dist/components/organisms/DateTimePicker/DateTimePicker.d.ts +15 -0
- package/dist/components/organisms/DateTimePicker/DateTimePicker.js +66 -0
- package/dist/components/organisms/Dialog/Dialog.d.ts +103 -0
- package/dist/components/organisms/Dialog/Dialog.js +162 -0
- package/dist/components/organisms/PaxSelector/PaxSelector.d.ts +63 -0
- package/dist/components/organisms/PaxSelector/PaxSelector.js +402 -0
- package/dist/components/organisms/RoundTrip/RoundTrip.d.ts +54 -0
- package/dist/components/organisms/RoundTrip/RoundTrip.js +179 -0
- package/dist/components/organisms/RoundTrip/index.d.ts +2 -0
- package/dist/components/organisms/RoundTrip/index.js +1 -0
- package/dist/components/organisms/SearchBarTransfer/SearchBarTransfer.d.ts +35 -0
- package/dist/components/organisms/SearchBarTransfer/SearchBarTransfer.js +192 -0
- package/dist/components/organisms/SearchBarTransfer/index.d.ts +2 -0
- package/dist/components/organisms/SearchBarTransfer/index.js +1 -0
- package/dist/components/organisms/TransferLine/TransferLine.d.ts +53 -0
- package/dist/components/organisms/TransferLine/TransferLine.js +179 -0
- package/dist/components/ui/button.d.ts +10 -0
- package/dist/components/ui/button.js +56 -0
- package/dist/components/ui/calendar.d.ts +8 -0
- package/dist/components/ui/calendar.js +87 -0
- package/dist/components/ui/popover.d.ts +7 -0
- package/dist/components/ui/popover.js +42 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +18 -0
- package/dist/lib/utils.d.ts +7 -0
- package/dist/lib/utils.js +13 -0
- package/package.json +21 -12
- package/src/components/atoms/Button/Button.css +34 -34
- package/src/components/atoms/Checkbox/Checkbox.tsx +83 -69
- package/src/components/atoms/Icon/Icon.tsx +30 -2
- package/src/components/atoms/Icon/icons/BuildingIcon.tsx +50 -0
- package/src/components/atoms/Icon/icons/CalendarOutlineIcon.tsx +50 -0
- package/src/components/atoms/Icon/icons/HomeIcon.tsx +52 -0
- package/src/components/atoms/Icon/icons/MinusIcon.tsx +45 -0
- package/src/components/atoms/Icon/icons/PlaneIcon.tsx +50 -0
- package/src/components/atoms/Icon/icons/PlusIcon.tsx +45 -0
- package/src/components/atoms/Icon/icons/ShipIcon.tsx +50 -0
- package/src/components/atoms/Illustration/Illustration.tsx +28 -0
- package/src/components/atoms/Illustration/illustrations.ts +116 -0
- package/src/components/atoms/RatingStar/RatingStar.tsx +114 -0
- package/src/components/atoms/SegmentedButton/SegmentedButton.tsx +94 -0
- package/src/components/atoms/Slider/Slider.tsx +95 -0
- package/src/components/molecules/Calendar/CalendarInput.tsx +135 -0
- package/src/components/molecules/Calendar/DateTime.tsx +172 -0
- package/src/components/molecules/Calendar/TimePicker.tsx +174 -0
- package/src/components/molecules/LocationDropdown/LocationDropdown.tsx +234 -0
- package/src/components/molecules/LocationDropdown/index.ts +2 -0
- package/src/components/molecules/RatingTab/RatingTab.tsx +96 -0
- package/src/components/molecules/TabGroup/TabGroup.tsx +60 -0
- package/src/components/molecules/UserCard/UserCard.stories.tsx +2 -2
- package/src/components/organisms/CardContainer/CardContainer.tsx +66 -0
- package/src/components/organisms/DateTimePicker/DateTimePicker.tsx +110 -0
- package/src/components/organisms/Dialog/Dialog.tsx +352 -0
- package/src/components/organisms/PaxSelector/PaxSelector.tsx +979 -0
- package/src/components/organisms/RoundTrip/RoundTrip.tsx +335 -0
- package/src/components/organisms/RoundTrip/index.ts +2 -0
- package/src/components/organisms/SearchBarTransfer/SearchBarTransfer.tsx +388 -0
- package/src/components/organisms/SearchBarTransfer/index.ts +2 -0
- package/src/components/organisms/TransferLine/TransferLine.tsx +369 -0
- package/src/components/ui/button.tsx +60 -0
- package/src/components/ui/calendar.tsx +246 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/styles/components/calendar.css +86 -0
- package/src/styles/components/checkbox.css +130 -132
- package/src/styles/components/dropdown.css +40 -40
- package/src/styles/components/forms.css +51 -51
- package/src/styles/components/illustration.css +7 -0
- package/src/styles/components/molecule/calendarInput.css +157 -0
- package/src/styles/components/molecule/dateTime.css +14 -0
- package/src/styles/components/molecule/location-dropdown.css +204 -0
- package/src/styles/components/molecule/timePicker.css +79 -0
- package/src/styles/components/multiselect-dropdown.css +230 -231
- package/src/styles/components/organism/card-container.css +49 -0
- package/src/styles/components/organism/dialog.css +241 -0
- package/src/styles/components/organism/pax-selector.css +702 -0
- package/src/styles/components/organism/round-trip.css +55 -0
- package/src/styles/components/organism/search-bar-transfer.css +128 -0
- package/src/styles/components/organism/transfer-line.css +86 -0
- package/src/styles/components/rating-star.css +39 -0
- package/src/styles/components/rating-tab.css +83 -0
- package/src/styles/components/segmented-button.css +134 -0
- package/src/styles/components/selected-value.css +16 -16
- package/src/styles/components/slider.css +86 -0
- package/src/styles/components/typography.css +36 -36
- package/src/styles/tokens/tokens.css +1093 -1093
- package/dist/components/atoms/Typography/Heading/Heading.d.ts +0 -9
- package/dist/components/atoms/Typography/Heading/Heading.js +0 -25
- package/dist/components/atoms/Typography/Text/Text.d.ts +0 -10
- package/dist/components/atoms/Typography/Text/Text.js +0 -77
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { Calendar } from "../../ui/calendar";
|
|
3
|
+
import { DateRange } from "react-day-picker";
|
|
4
|
+
import { Button } from "../../ui/button";
|
|
5
|
+
import TimePicker from "./TimePicker";
|
|
6
|
+
import { Locale } from "date-fns/locale";
|
|
7
|
+
import Icon from "../../atoms/Icon/Icon";
|
|
8
|
+
|
|
9
|
+
export interface DateTimeProps {
|
|
10
|
+
mode?: "calendar" | "time" | "both";
|
|
11
|
+
selectionMode?: "single" | "range";
|
|
12
|
+
numberOfMonths?: 1 | 2;
|
|
13
|
+
disableBeforeToday?: boolean;
|
|
14
|
+
disableToday?: boolean;
|
|
15
|
+
defaultDateRange?: DateRange;
|
|
16
|
+
defaultTime?: { hour: string; minute: string; meridiem: "AM" | "PM" };
|
|
17
|
+
onChange?: (payload: { dateRange?: DateRange; time?: { hour: string; minute: string; meridiem: "AM" | "PM" } }) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DateTime: React.FC<DateTimeProps> = ({
|
|
21
|
+
mode = "both",
|
|
22
|
+
selectionMode = "range",
|
|
23
|
+
numberOfMonths = 2,
|
|
24
|
+
disableBeforeToday = true,
|
|
25
|
+
disableToday = false,
|
|
26
|
+
defaultDateRange,
|
|
27
|
+
defaultTime,
|
|
28
|
+
onChange,
|
|
29
|
+
}) => {
|
|
30
|
+
const [dateRange, setDateRange] = useState<DateRange | undefined>(defaultDateRange);
|
|
31
|
+
const [hours, setHours] = useState<string>(defaultTime?.hour ?? "12");
|
|
32
|
+
const [minutes, setMinutes] = useState<string>(defaultTime?.minute ?? "00");
|
|
33
|
+
const [amPm, setAmPm] = useState<"AM" | "PM">(defaultTime?.meridiem ?? "AM");
|
|
34
|
+
|
|
35
|
+
// notify parent when date or time changes
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
onChange?.({
|
|
38
|
+
dateRange: dateRange ?? undefined,
|
|
39
|
+
time: mode === "calendar" ? undefined : { hour: hours, minute: minutes, meridiem: amPm },
|
|
40
|
+
});
|
|
41
|
+
}, [dateRange, hours, minutes, amPm, onChange, mode]);
|
|
42
|
+
|
|
43
|
+
const handleSelect = (val: Date | Date[] | DateRange | undefined) => {
|
|
44
|
+
if (!val) {
|
|
45
|
+
setDateRange(undefined);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (val instanceof Date) {
|
|
50
|
+
setDateRange({ from: val, to: val });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (Array.isArray(val)) {
|
|
55
|
+
const sorted = val.slice().sort((a, b) => +a - +b);
|
|
56
|
+
setDateRange({ from: sorted[0], to: sorted[sorted.length - 1] });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
setDateRange(val);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const resetTime = () => {
|
|
63
|
+
setHours("12");
|
|
64
|
+
setMinutes("00");
|
|
65
|
+
setAmPm("AM");
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const clearSelection = () => {
|
|
69
|
+
setDateRange(undefined);
|
|
70
|
+
resetTime();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const selectToday = () => {
|
|
74
|
+
const today = new Date();
|
|
75
|
+
today.setHours(0, 0, 0, 0);
|
|
76
|
+
setDateRange({ from: today, to: today });
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const calendarMode = selectionMode === "single" ? "single" : "range";
|
|
80
|
+
|
|
81
|
+
const buildDisabled = () => {
|
|
82
|
+
const disabled: any[] = [];
|
|
83
|
+
if (disableBeforeToday) {
|
|
84
|
+
const today = new Date();
|
|
85
|
+
today.setHours(0, 0, 0, 0);
|
|
86
|
+
disabled.push({ before: today });
|
|
87
|
+
}
|
|
88
|
+
if (disableToday) {
|
|
89
|
+
const today = new Date();
|
|
90
|
+
today.setHours(0, 0, 0, 0);
|
|
91
|
+
disabled.push(today);
|
|
92
|
+
}
|
|
93
|
+
return disabled.length ? disabled : undefined;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const disabledDays = buildDisabled();
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className="calendar--dropdown">
|
|
100
|
+
{mode !== "time" && (() => {
|
|
101
|
+
const calendarProps: any = {
|
|
102
|
+
mode: calendarMode,
|
|
103
|
+
selected: dateRange,
|
|
104
|
+
onSelect: handleSelect,
|
|
105
|
+
weekStartsOn: 1,
|
|
106
|
+
showOutsideDays: false,
|
|
107
|
+
numberOfMonths: numberOfMonths,
|
|
108
|
+
disabled: disabledDays,
|
|
109
|
+
className:"p-0",
|
|
110
|
+
formatters: {
|
|
111
|
+
formatWeekdayName: (weekday: Date, options?: { locale?: Locale }) => {
|
|
112
|
+
return weekday
|
|
113
|
+
.toLocaleDateString(options?.locale?.code, { weekday: "short" })
|
|
114
|
+
.toUpperCase();
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return <Calendar {...calendarProps}/>;
|
|
120
|
+
})()}
|
|
121
|
+
|
|
122
|
+
{mode !== "calendar" && (
|
|
123
|
+
<div className={`${mode === "both" ? "calendar--time_container_separated" : ""}`}>
|
|
124
|
+
<TimePicker
|
|
125
|
+
label="Time"
|
|
126
|
+
hour={hours}
|
|
127
|
+
minute={minutes}
|
|
128
|
+
meridiem={amPm}
|
|
129
|
+
onChange={(h, m, mm) => {
|
|
130
|
+
const hh = String(
|
|
131
|
+
Math.max(0, Math.min(12, Number(h || 0)))
|
|
132
|
+
).padStart(2, "0");
|
|
133
|
+
const mmn = String(
|
|
134
|
+
Math.max(0, Math.min(59, Number(m || 0)))
|
|
135
|
+
).padStart(2, "0");
|
|
136
|
+
setHours(hh);
|
|
137
|
+
setMinutes(mmn);
|
|
138
|
+
setAmPm(mm);
|
|
139
|
+
}}
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
|
|
144
|
+
{mode !== "time" && (
|
|
145
|
+
<div className="calendar--footer">
|
|
146
|
+
<Button
|
|
147
|
+
variant="ghost"
|
|
148
|
+
size="sm"
|
|
149
|
+
onClick={selectToday}
|
|
150
|
+
type="button"
|
|
151
|
+
className="calendar--footer--button"
|
|
152
|
+
disabled={disableToday}
|
|
153
|
+
>
|
|
154
|
+
<Icon name="calendar-outline" size="sm" />
|
|
155
|
+
Today
|
|
156
|
+
</Button>
|
|
157
|
+
<Button
|
|
158
|
+
variant="ghost"
|
|
159
|
+
size="sm"
|
|
160
|
+
onClick={clearSelection}
|
|
161
|
+
type="button"
|
|
162
|
+
className="calendar--footer--button"
|
|
163
|
+
>
|
|
164
|
+
Clear
|
|
165
|
+
</Button>
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export default DateTime;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
export interface TimePickerProps {
|
|
4
|
+
label?: string;
|
|
5
|
+
hour?: string;
|
|
6
|
+
minute?: string;
|
|
7
|
+
meridiem?: "AM" | "PM";
|
|
8
|
+
state?: "default" | "active" | "typing" | "disabled" | "success" | "error";
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
errorMessage?: string;
|
|
11
|
+
successMessage?: string;
|
|
12
|
+
onChange?: (hour: string, minute: string, meridiem: "AM" | "PM") => void;
|
|
13
|
+
onFocus?: () => void;
|
|
14
|
+
onBlur?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const clampTwoDigits = (val: string) => {
|
|
18
|
+
return val.replace(/\D/g, "").slice(-2);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const TimePicker: React.FC<TimePickerProps> = ({
|
|
22
|
+
label = "Time",
|
|
23
|
+
hour = "",
|
|
24
|
+
minute = "",
|
|
25
|
+
meridiem = "AM",
|
|
26
|
+
state = "default",
|
|
27
|
+
disabled = false,
|
|
28
|
+
onChange,
|
|
29
|
+
onFocus,
|
|
30
|
+
onBlur,
|
|
31
|
+
}) => {
|
|
32
|
+
const [localHour, setLocalHour] = useState<string>(hour);
|
|
33
|
+
const [localMinute, setLocalMinute] = useState<string>(minute);
|
|
34
|
+
const [localMeridiem, setLocalMeridiem] = useState<"AM" | "PM">(meridiem);
|
|
35
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
36
|
+
const [isTyping, setIsTyping] = useState(false);
|
|
37
|
+
|
|
38
|
+
// Keep local state in sync when parent props change (e.g. Clear button)
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (hour !== localHour) setLocalHour(hour);
|
|
41
|
+
}, [hour]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (minute !== localMinute) setLocalMinute(minute);
|
|
45
|
+
}, [minute]);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (meridiem !== localMeridiem) setLocalMeridiem(meridiem);
|
|
49
|
+
}, [meridiem]);
|
|
50
|
+
|
|
51
|
+
const finalState = disabled ? "disabled" : state;
|
|
52
|
+
const displayState =
|
|
53
|
+
isFocused && isTyping ? "typing" : isFocused ? "active" : finalState;
|
|
54
|
+
|
|
55
|
+
const emitChange = (h: string, m: string, mm: "AM" | "PM") => {
|
|
56
|
+
onChange?.(h, m, mm);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleHourChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
60
|
+
const raw = clampTwoDigits(e.target.value);
|
|
61
|
+
if (raw === "") {
|
|
62
|
+
setLocalHour("");
|
|
63
|
+
setIsTyping(true);
|
|
64
|
+
emitChange("", localMinute, localMeridiem);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
let num = Number(raw || 0);
|
|
68
|
+
if (num > 12) num = 12;
|
|
69
|
+
const val = String(num).padStart(2, "0");
|
|
70
|
+
setLocalHour(val);
|
|
71
|
+
setIsTyping(true);
|
|
72
|
+
emitChange(val, localMinute, localMeridiem);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleMinuteChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
76
|
+
const raw = clampTwoDigits(e.target.value);
|
|
77
|
+
if (raw === "") {
|
|
78
|
+
setLocalMinute("");
|
|
79
|
+
setIsTyping(true);
|
|
80
|
+
emitChange(localHour, "", localMeridiem);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
let num = Number(raw || 0);
|
|
84
|
+
if (num > 59) num = 59;
|
|
85
|
+
const val = String(num).padStart(2, "0");
|
|
86
|
+
setLocalMinute(val);
|
|
87
|
+
setIsTyping(true);
|
|
88
|
+
emitChange(localHour, val, localMeridiem);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handleMeridiemClick = (value: "AM" | "PM") => {
|
|
92
|
+
if (disabled) return;
|
|
93
|
+
setLocalMeridiem(value);
|
|
94
|
+
emitChange(localHour, localMinute, value);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const handleFocus = () => {
|
|
98
|
+
setIsFocused(true);
|
|
99
|
+
onFocus?.();
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const handleBlur = () => {
|
|
103
|
+
setIsFocused(false);
|
|
104
|
+
setIsTyping(false);
|
|
105
|
+
onBlur?.();
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className={`time-picker-wrapper`} data-state={displayState}>
|
|
110
|
+
{label && <div className="time-picker__div">{label}</div>}
|
|
111
|
+
|
|
112
|
+
<div className={`time-picker time-picker--${displayState}`}>
|
|
113
|
+
<input
|
|
114
|
+
className="time-picker__input"
|
|
115
|
+
type="text"
|
|
116
|
+
inputMode="numeric"
|
|
117
|
+
pattern="[0-9]*"
|
|
118
|
+
value={localHour}
|
|
119
|
+
placeholder="00"
|
|
120
|
+
onChange={handleHourChange}
|
|
121
|
+
onFocus={handleFocus}
|
|
122
|
+
onBlur={handleBlur}
|
|
123
|
+
disabled={disabled}
|
|
124
|
+
aria-label="Hour"
|
|
125
|
+
data-unit="hour"
|
|
126
|
+
/>
|
|
127
|
+
|
|
128
|
+
<div className="time-picker__colon">:</div>
|
|
129
|
+
|
|
130
|
+
<input
|
|
131
|
+
className="time-picker__input"
|
|
132
|
+
type="text"
|
|
133
|
+
inputMode="numeric"
|
|
134
|
+
pattern="[0-9]*"
|
|
135
|
+
value={localMinute}
|
|
136
|
+
placeholder="00"
|
|
137
|
+
onChange={handleMinuteChange}
|
|
138
|
+
onFocus={handleFocus}
|
|
139
|
+
onBlur={handleBlur}
|
|
140
|
+
disabled={disabled}
|
|
141
|
+
aria-label="Minute"
|
|
142
|
+
data-unit="minute"
|
|
143
|
+
/>
|
|
144
|
+
|
|
145
|
+
<div
|
|
146
|
+
className="time-picker__am-pm"
|
|
147
|
+
role="tablist"
|
|
148
|
+
aria-label="AM/PM selector"
|
|
149
|
+
>
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
className={`time-picker__seg time-picker__seg--left ${localMeridiem === "AM" ? "time-picker__seg--selected" : ""}`}
|
|
153
|
+
onClick={() => handleMeridiemClick("AM")}
|
|
154
|
+
disabled={disabled}
|
|
155
|
+
aria-pressed={localMeridiem === "AM"}
|
|
156
|
+
>
|
|
157
|
+
AM
|
|
158
|
+
</button>
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
className={`time-picker__seg time-picker__seg--right ${localMeridiem === "PM" ? "time-picker__seg--selected" : ""}`}
|
|
162
|
+
onClick={() => handleMeridiemClick("PM")}
|
|
163
|
+
disabled={disabled}
|
|
164
|
+
aria-pressed={localMeridiem === "PM"}
|
|
165
|
+
>
|
|
166
|
+
PM
|
|
167
|
+
</button>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export default TimePicker;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import Icon from '../../atoms/Icon/Icon';
|
|
3
|
+
import { Text } from '../../atoms/Typography/Typography';
|
|
4
|
+
import '../../../styles/components/molecule/location-dropdown.css';
|
|
5
|
+
|
|
6
|
+
export interface LocationOption {
|
|
7
|
+
id: string | number;
|
|
8
|
+
label: string;
|
|
9
|
+
type: 'airport' | 'port' | 'accommodation';
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface LocationGroup {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
options: LocationOption[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LocationData {
|
|
20
|
+
id: string | number;
|
|
21
|
+
locationName: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface LocationDropdownProps {
|
|
25
|
+
options?: LocationOption[];
|
|
26
|
+
groups?: LocationGroup[];
|
|
27
|
+
selectedValue?: string | number | null;
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
label?: string;
|
|
30
|
+
onSelectionChange: (location: LocationData | null) => void;
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
error?: boolean;
|
|
33
|
+
className?: string;
|
|
34
|
+
type?: 'airport-port' | 'accommodation' | 'pickup-dropoff';
|
|
35
|
+
maxHeight?: number;
|
|
36
|
+
direction?: 'pickup' | 'dropoff';
|
|
37
|
+
showGroupTitles?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const LocationDropdown: React.FC<LocationDropdownProps> = ({
|
|
41
|
+
options = [],
|
|
42
|
+
groups = [],
|
|
43
|
+
selectedValue = null,
|
|
44
|
+
placeholder = 'Select a location',
|
|
45
|
+
label,
|
|
46
|
+
onSelectionChange,
|
|
47
|
+
disabled = false,
|
|
48
|
+
error = false,
|
|
49
|
+
className = '',
|
|
50
|
+
type = 'airport-port',
|
|
51
|
+
maxHeight = 240,
|
|
52
|
+
direction = undefined,
|
|
53
|
+
showGroupTitles = true
|
|
54
|
+
}) => {
|
|
55
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
56
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
57
|
+
|
|
58
|
+
// Close dropdown when clicking outside
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
61
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
62
|
+
setIsOpen(false);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
67
|
+
return () => {
|
|
68
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
69
|
+
};
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
const handleToggleDropdown = () => {
|
|
73
|
+
if (!disabled) {
|
|
74
|
+
setIsOpen(!isOpen);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const handleOptionSelect = (option: LocationOption) => {
|
|
79
|
+
if (disabled) return;
|
|
80
|
+
const locationData: LocationData = {
|
|
81
|
+
id: option.id,
|
|
82
|
+
locationName: option.label,
|
|
83
|
+
};
|
|
84
|
+
onSelectionChange(locationData);
|
|
85
|
+
setIsOpen(false);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const getSelectedOption = (): LocationOption | null => {
|
|
89
|
+
if (!selectedValue) return null;
|
|
90
|
+
|
|
91
|
+
// Search in flat options
|
|
92
|
+
const flatOption = options.find(opt => opt.id === selectedValue);
|
|
93
|
+
if (flatOption) return flatOption;
|
|
94
|
+
|
|
95
|
+
// Search in groups
|
|
96
|
+
for (const group of groups) {
|
|
97
|
+
const groupOption = group.options.find(opt => opt.id === selectedValue);
|
|
98
|
+
if (groupOption) return groupOption;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return null;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const getIconForType = (optionType: 'airport' | 'port' | 'accommodation') => {
|
|
105
|
+
switch (optionType) {
|
|
106
|
+
case 'airport':
|
|
107
|
+
return 'plane';
|
|
108
|
+
case 'port':
|
|
109
|
+
return 'ship';
|
|
110
|
+
case 'accommodation':
|
|
111
|
+
return 'map-pin';
|
|
112
|
+
default:
|
|
113
|
+
return 'map-pin';
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const getInputIcon = () => {
|
|
118
|
+
if (selectedOption) {
|
|
119
|
+
// For airports, show arrival/departure based on direction
|
|
120
|
+
console.log({direction});
|
|
121
|
+
|
|
122
|
+
if (selectedOption.type === 'airport' && direction) {
|
|
123
|
+
return direction === 'pickup' ? 'arrival' : 'departure';
|
|
124
|
+
}
|
|
125
|
+
return getIconForType(selectedOption.type);
|
|
126
|
+
}
|
|
127
|
+
// Use arrival/departure icons based on direction for pickup/dropoff types
|
|
128
|
+
if ((type === 'pickup-dropoff' || type === 'airport-port') && direction) {
|
|
129
|
+
return direction === 'pickup' ? 'arrival' : 'departure';
|
|
130
|
+
}
|
|
131
|
+
return 'map-pin';
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const getOptionIcon = (option: LocationOption, isSelected: boolean) => {
|
|
135
|
+
// For selected airports, show arrival/departure based on direction
|
|
136
|
+
if (isSelected && option.type === 'airport' && direction) {
|
|
137
|
+
return direction === 'pickup' ? 'arrival' : 'departure';
|
|
138
|
+
}
|
|
139
|
+
// For other types (port, accommodation), show their specific icon
|
|
140
|
+
return getIconForType(option.type);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const getDropdownState = () => {
|
|
144
|
+
if (disabled) return 'disabled';
|
|
145
|
+
if (error) return 'error';
|
|
146
|
+
if (isOpen) return 'open';
|
|
147
|
+
if (selectedValue) return 'selected';
|
|
148
|
+
return 'default';
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const selectedOption = getSelectedOption();
|
|
152
|
+
const displayText = selectedOption ? selectedOption.label : placeholder;
|
|
153
|
+
|
|
154
|
+
// Prepare all options (flat or grouped)
|
|
155
|
+
const allOptions = groups.length > 0 ? groups : [{ id: 'default', label: '', options }];
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div ref={dropdownRef} className={`location-dropdown location-dropdown--${type} ${className}`}>
|
|
159
|
+
{label && (
|
|
160
|
+
<div className="location-dropdown__label">
|
|
161
|
+
<Text size="sm" variant="medium">{label}</Text>
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
|
|
165
|
+
<div
|
|
166
|
+
className={`location-dropdown__input location-dropdown__input--${getDropdownState()}`}
|
|
167
|
+
onClick={handleToggleDropdown}
|
|
168
|
+
>
|
|
169
|
+
<div className="location-dropdown__input-content">
|
|
170
|
+
<Icon
|
|
171
|
+
name={getInputIcon()}
|
|
172
|
+
size="sm"
|
|
173
|
+
className={`location-dropdown__input-icon ${!selectedOption ? 'location-dropdown__input-icon--placeholder' : ''}`}
|
|
174
|
+
/>
|
|
175
|
+
<span className={`location-dropdown__input-text ${!selectedOption ? 'location-dropdown__input-text--placeholder' : ''}`}>
|
|
176
|
+
{displayText}
|
|
177
|
+
</span>
|
|
178
|
+
</div>
|
|
179
|
+
<Icon
|
|
180
|
+
name="chevron-down"
|
|
181
|
+
size="sm"
|
|
182
|
+
className="location-dropdown__input-chevron"
|
|
183
|
+
/>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
{isOpen && (
|
|
187
|
+
<div className="location-dropdown__panel">
|
|
188
|
+
<div className="location-dropdown__content" style={{ maxHeight: `${maxHeight}px` }}>
|
|
189
|
+
<div className="location-dropdown__options-wrapper">
|
|
190
|
+
{allOptions.map((group, groupIndex) => (
|
|
191
|
+
<div key={group.id} className="location-dropdown__group">
|
|
192
|
+
{showGroupTitles && group.label && groups.length > 0 && (
|
|
193
|
+
<>
|
|
194
|
+
{groupIndex > 0 && <div className="location-dropdown__divider" />}
|
|
195
|
+
<div className="location-dropdown__group-header">
|
|
196
|
+
<Text size="xs" variant="bold">{group.label}</Text>
|
|
197
|
+
</div>
|
|
198
|
+
</>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
<div className="location-dropdown__group-options">
|
|
202
|
+
{group.options.map((option) => {
|
|
203
|
+
const isSelected = selectedValue === option.id;
|
|
204
|
+
const isDisabled = option.disabled || disabled;
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<div
|
|
208
|
+
key={option.id}
|
|
209
|
+
className={`location-dropdown__option ${isSelected ? 'location-dropdown__option--selected' : ''} ${isDisabled ? 'location-dropdown__option--disabled' : ''}`}
|
|
210
|
+
onClick={() => !isDisabled && handleOptionSelect(option)}
|
|
211
|
+
>
|
|
212
|
+
<Icon
|
|
213
|
+
name={getOptionIcon(option, isSelected)}
|
|
214
|
+
size="sm"
|
|
215
|
+
className="location-dropdown__option-icon"
|
|
216
|
+
/>
|
|
217
|
+
<span className="location-dropdown__option-text">
|
|
218
|
+
{option.label}
|
|
219
|
+
</span>
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
})}
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
))}
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
export default LocationDropdown;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Check from '../../atoms/Icon/icons/Check';
|
|
3
|
+
import { Text } from '../../atoms/Typography/Typography';
|
|
4
|
+
import RatingStar from '../../atoms/RatingStar/RatingStar';
|
|
5
|
+
|
|
6
|
+
export interface RatingTabProps {
|
|
7
|
+
/**
|
|
8
|
+
* Label displayed next to the checkbox (e.g. "5 stars").
|
|
9
|
+
*/
|
|
10
|
+
label: string;
|
|
11
|
+
/**
|
|
12
|
+
* Rating value displayed in the star row (0–max, typically 1–5).
|
|
13
|
+
*/
|
|
14
|
+
ratingValue: number;
|
|
15
|
+
/**
|
|
16
|
+
* Maximum number of stars.
|
|
17
|
+
*/
|
|
18
|
+
maxStars?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Whether this tab is selected.
|
|
21
|
+
*/
|
|
22
|
+
checked?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Whether the tab is disabled.
|
|
25
|
+
*/
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Called when the selection state changes.
|
|
29
|
+
*/
|
|
30
|
+
onChange?: (checked: boolean) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Additional CSS classes for the container.
|
|
33
|
+
*/
|
|
34
|
+
className?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* RatingTab molecule
|
|
39
|
+
*
|
|
40
|
+
* Combines a checkbox-like selector, a label and the RatingStar atom.
|
|
41
|
+
* Used for filters like "5 stars", "4 stars", etc.
|
|
42
|
+
*/
|
|
43
|
+
const RatingTab: React.FC<RatingTabProps> = ({
|
|
44
|
+
label,
|
|
45
|
+
ratingValue,
|
|
46
|
+
maxStars = 5,
|
|
47
|
+
checked = false,
|
|
48
|
+
disabled = false,
|
|
49
|
+
onChange,
|
|
50
|
+
className = '',
|
|
51
|
+
}) => {
|
|
52
|
+
const handleToggle = () => {
|
|
53
|
+
if (disabled || !onChange) return;
|
|
54
|
+
onChange(!checked);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
58
|
+
if (disabled || !onChange) return;
|
|
59
|
+
|
|
60
|
+
if (event.key === ' ' || event.key === 'Enter') {
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
onChange(!checked);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const stateClass = disabled ? 'rating-tab--disabled' : checked ? 'rating-tab--active' : 'rating-tab--default';
|
|
67
|
+
|
|
68
|
+
const containerClasses = ['rating-tab', stateClass, className].filter(Boolean).join(' ');
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
className={containerClasses}
|
|
73
|
+
role="checkbox"
|
|
74
|
+
aria-checked={checked}
|
|
75
|
+
aria-disabled={disabled}
|
|
76
|
+
tabIndex={disabled ? -1 : 0}
|
|
77
|
+
onClick={handleToggle}
|
|
78
|
+
onKeyDown={handleKeyDown}
|
|
79
|
+
>
|
|
80
|
+
<div className="rating-tab__left">
|
|
81
|
+
<div className="rating-tab__checkbox">
|
|
82
|
+
{checked && <Check size="sm" className="rating-tab__checkbox-icon" />}
|
|
83
|
+
</div>
|
|
84
|
+
<Text size="sm" className="rating-tab__label">
|
|
85
|
+
{label}
|
|
86
|
+
</Text>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<RatingStar value={ratingValue} max={maxStars} />
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export default RatingTab;
|
|
95
|
+
|
|
96
|
+
|