@xola/ui-kit 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/.github/ISSUE_TEMPLATE/v2-component.md +12 -0
  2. package/.nvmrc +1 -0
  3. package/.prettierrc +6 -0
  4. package/.storybook/main.js +17 -0
  5. package/.storybook/preview.js +3 -0
  6. package/README.md +1 -0
  7. package/babel.config.json +13 -0
  8. package/build/favicon.ico +0 -0
  9. package/build/style.css +1 -0
  10. package/build/ui-kit.mjs +12455 -0
  11. package/build/ui-kit.umd.js +33 -0
  12. package/lib/index.js +1 -0
  13. package/package.json +48 -0
  14. package/src/components/CountrySelect/CountrySelect.js +15 -0
  15. package/src/components/CountrySelect/index.js +1 -0
  16. package/src/components/DatePicker/DatePicker.js +80 -0
  17. package/src/components/DatePicker/DatePicker.module.scss +3 -0
  18. package/src/components/DatePicker/DatePicker.scss +18 -0
  19. package/src/components/DatePicker/index.js +3 -0
  20. package/src/components/FormInput/FormInput.js +23 -0
  21. package/src/components/FormInput/index.js +3 -0
  22. package/src/components/MiniStepper/MiniStepper.js +39 -0
  23. package/src/components/MiniStepper/MiniStepper.module.scss +9 -0
  24. package/src/components/MiniStepper/index.js +1 -0
  25. package/src/components/PhoneInput/PhoneInput.js +44 -0
  26. package/src/components/PhoneInput/index.js +1 -0
  27. package/src/components/ProductSelector/ProductListItem/ProductListItem.js +41 -0
  28. package/src/components/ProductSelector/ProductListItem/ProductListItem.module.scss +32 -0
  29. package/src/components/ProductSelector/ProductSelector.js +71 -0
  30. package/src/components/ProductSelector/index.js +3 -0
  31. package/src/components/ScheduleEditor/ScheduleEditor.js +277 -0
  32. package/src/components/ScheduleEditor/ScheduleEditorRow.js +26 -0
  33. package/src/components/ScheduleEditor/TimeRangeSelector/TimeRangeSelector.js +94 -0
  34. package/src/components/ScheduleEditor/TimeRangeSelector/TimeRangeSelector.module.scss +13 -0
  35. package/src/components/ScheduleEditor/TimeSlotSelector/TimeSlotSelector.js +62 -0
  36. package/src/components/ScheduleEditor/TimeSlotSelector/TimeSlotSelector.module.scss +21 -0
  37. package/src/components/ScheduleEditor/WeekSelector/WeekSelector.js +50 -0
  38. package/src/components/ScheduleEditor/helpers/schedule.js +15 -0
  39. package/src/components/ScheduleEditor/helpers/scheduleSummary.js +128 -0
  40. package/src/components/ScheduleEditor/index.js +4 -0
  41. package/src/components/Stepper/Step.js +31 -0
  42. package/src/components/Stepper/Step.module.scss +25 -0
  43. package/src/components/Stepper/Stepper.js +25 -0
  44. package/src/components/Stepper/Stepper.module.scss +5 -0
  45. package/src/components/Stepper/index.js +4 -0
  46. package/src/components/TimePicker/TimePicker.js +51 -0
  47. package/src/components/TimePicker/TimePicker.module.scss +16 -0
  48. package/src/components/TimePicker/TimePickerPopover.js +94 -0
  49. package/src/components/TimePicker/TimePickerPopover.module.scss +48 -0
  50. package/src/components/TimePicker/index.js +3 -0
  51. package/src/icons/CheckIcon.js +9 -0
  52. package/src/icons/TrashIcon.js +17 -0
  53. package/src/index.js +18 -0
  54. package/src/stories/Button.stories.js +66 -0
  55. package/src/stories/CountrySelect.stories.js +18 -0
  56. package/src/stories/DatePicker.stories.js +96 -0
  57. package/src/stories/FormInput.stories.js +25 -0
  58. package/src/stories/Forms.stories.js +43 -0
  59. package/src/stories/Icons.stories.js +21 -0
  60. package/src/stories/MiniStepper.stories.js +15 -0
  61. package/src/stories/PhoneInput.stories.js +26 -0
  62. package/src/stories/ProductSelector.stories.js +42 -0
  63. package/src/stories/ScheduleEditor.stories.js +37 -0
  64. package/src/stories/Stepper.stories.js +36 -0
  65. package/src/stories/TimePicker.stories.js +14 -0
  66. package/src/stories/Typography.stories.js +18 -0
  67. package/src/styles/custom.scss +60 -0
  68. package/src/styles/index.scss +3 -0
  69. package/src/styles/variables.scss +43 -0
  70. package/src/theme.js +359 -0
  71. package/src/values/countries.js +196 -0
  72. package/src/values/products.js +86 -0
  73. package/src/values/sellers.js +5 -0
  74. package/webpack.config.js +47 -0
@@ -0,0 +1,26 @@
1
+ import React from "react";
2
+ import { Col, FormFeedback, FormGroup, Input, Label } from "reactstrap";
3
+ import classNames from "classnames";
4
+
5
+ const ScheduleEditorRow = ({ label, error, htmlFor, children }) => {
6
+ return (
7
+ <FormGroup row>
8
+ <Col className={classNames("d-flex py-1 align-items-center")} sm={2}>
9
+ <Label className="font-14 m-0" for={htmlFor}>
10
+ {label}
11
+ </Label>
12
+ </Col>
13
+ <Col className={classNames("font-14 d-flex py-1 align-items-center")} sm={10}>
14
+ {children}
15
+ </Col>
16
+ {error && (
17
+ <Col sm={{ size: 12, offset: 2 }}>
18
+ <Input className="d-none" invalid />
19
+ <FormFeedback valid={false}> {error}</FormFeedback>
20
+ </Col>
21
+ )}
22
+ </FormGroup>
23
+ );
24
+ };
25
+
26
+ export default ScheduleEditorRow;
@@ -0,0 +1,94 @@
1
+ import classNames from "classnames";
2
+ import React, { Fragment } from "react";
3
+ import { TimePicker, TrashIcon } from "../../../";
4
+ import styles from "./TimeRangeSelector.module.scss";
5
+
6
+ const TimeRangeSelector = ({ value = [{}], name, onChange, error }) => {
7
+ const handleAddNewRow = () => {
8
+ onChange([...value, { startTime: null, endTime: null }], name);
9
+ };
10
+
11
+ const handleDeleteRow = (event, index) => {
12
+ let timeRanges = [...value];
13
+ timeRanges.splice(index, 1);
14
+ onChange(timeRanges, name);
15
+ event.preventDefault();
16
+ };
17
+
18
+ const handleChange = (v, index, key) => {
19
+ let timeRanges = [...value];
20
+ timeRanges[index][key] = v;
21
+ onChange(timeRanges, name);
22
+ };
23
+
24
+ return (
25
+ <Fragment>
26
+ <div>
27
+ <div className="d-block">
28
+ {value.map((timeRange, index) => (
29
+ <div key={index} className={classNames("mb-2", styles.row)}>
30
+ <div className={classNames("d-inline-block")}>
31
+ <div className="d-flex align-items-center">
32
+ <span className="mr-2">Start Time</span>
33
+ <span className="mr-4">
34
+ <TimePicker
35
+ value={timeRange.startTime}
36
+ onChange={(v) => handleChange(v, index, "startTime")}
37
+ />
38
+ </span>
39
+ </div>
40
+ {error && error[index] && (
41
+ <Fragment>
42
+ <div className="invalid-feedback d-block">
43
+ {error[index].startTime ? (
44
+ <span>{error[index].startTime}</span>
45
+ ) : (
46
+ <span>&nbsp;</span>
47
+ )}
48
+ </div>
49
+ </Fragment>
50
+ )}
51
+ </div>
52
+ <div className={classNames("d-inline-block")}>
53
+ <div className="d-flex align-items-center">
54
+ <span className="mr-2">End Time</span>
55
+ <span className="mr-4">
56
+ <TimePicker
57
+ value={timeRange.endTime}
58
+ onChange={(v) => handleChange(v, index, "endTime")}
59
+ />
60
+ </span>
61
+ <span
62
+ onClick={(e) => handleDeleteRow(e, index)}
63
+ className={classNames(
64
+ styles.delete,
65
+ "cursor-pointer ml-2 text-center p-1 rounded-circle bg-danger text-white",
66
+ )}
67
+ >
68
+ <TrashIcon />
69
+ </span>
70
+ </div>
71
+ {error && error[index] && (
72
+ <Fragment>
73
+ <div className="invalid-feedback d-block">
74
+ {error[index].endTime ? (
75
+ <span>{error[index].endTime}</span>
76
+ ) : (
77
+ <span>&nbsp;</span>
78
+ )}
79
+ </div>
80
+ </Fragment>
81
+ )}
82
+ </div>
83
+ </div>
84
+ ))}
85
+ </div>
86
+ <a onClick={handleAddNewRow} className={classNames("cursor-pointer d-block")}>
87
+ + add time range
88
+ </a>
89
+ </div>
90
+ </Fragment>
91
+ );
92
+ };
93
+
94
+ export default TimeRangeSelector;
@@ -0,0 +1,13 @@
1
+ .row {
2
+ align-items: center;
3
+ .delete {
4
+ width: 28px;
5
+ height: 28px;
6
+ display: none;
7
+ }
8
+ &:hover {
9
+ .delete {
10
+ display: block;
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,62 @@
1
+ import React from "react";
2
+ import _ from "lodash";
3
+ import classNames from "classnames";
4
+ import { TimePicker } from "../../../";
5
+ import styles from "./TimeSlotSelector.module.scss";
6
+
7
+ const TimeSlotSelector = ({ name, value, onChange }) => {
8
+ const initialDisplayCount = 4;
9
+ let selectedValues;
10
+
11
+ const handleAddEmptySlot = () => {
12
+ let undefinedCount = [...value].filter((v) => _.isNull(v)).length;
13
+ if (undefinedCount === 0) {
14
+ selectedValues.push(null);
15
+ }
16
+ };
17
+
18
+ const handleChange = (value, index) => {
19
+ let currentValues = [...selectedValues];
20
+ currentValues[index] = value;
21
+ onChange(currentValues, name);
22
+ handleAddEmptySlot();
23
+ };
24
+
25
+ const handleDeleteTimeSlot = (index) => {
26
+ let currentValues = [...selectedValues].filter((v, i) => i !== index);
27
+ onChange(currentValues, name);
28
+ };
29
+
30
+ if (value && _.isArray(value) && value.length > 0) {
31
+ selectedValues = value;
32
+ handleAddEmptySlot();
33
+ } else {
34
+ selectedValues = new Array(initialDisplayCount).fill(null);
35
+ }
36
+
37
+ return (
38
+ <div>
39
+ {selectedValues.map((selectedValue, index) => (
40
+ <div
41
+ className={classNames(styles.slot, "position-relative d-inline-block mr-2")}
42
+ key={index}
43
+ id={`timeslot-${index}`}
44
+ >
45
+ <span
46
+ onClick={() => handleDeleteTimeSlot(index)}
47
+ className={classNames(
48
+ styles.clearButton,
49
+ "position-absolute cursor-pointer text-white rounded-circle bg-secondary text-center",
50
+ )}
51
+ >
52
+ &times;
53
+ </span>
54
+
55
+ <TimePicker value={selectedValue} onChange={(v) => handleChange(v, index)} />
56
+ </div>
57
+ ))}
58
+ </div>
59
+ );
60
+ };
61
+
62
+ export default TimeSlotSelector;
@@ -0,0 +1,21 @@
1
+ @import "../../../styles/variables.scss";
2
+
3
+ .slot {
4
+ .clearButton {
5
+ $size: 15px;
6
+ display: none;
7
+ top: -$size / 2;
8
+ right: -$size / 2;
9
+ width: $size;
10
+ height: $size;
11
+ font-size: $size;
12
+ line-height: $size;
13
+ z-index: 10;
14
+ }
15
+
16
+ &:hover {
17
+ .clearButton {
18
+ display: block;
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,50 @@
1
+ import React from "react";
2
+ import { CustomInput } from "reactstrap";
3
+
4
+ const WeekSelector = ({ value, name, onChange }) => {
5
+ const weekConfig = [
6
+ { label: "S", value: 0 },
7
+ { label: "M", value: 1 },
8
+ { label: "T", value: 2 },
9
+ { label: "W", value: 3 },
10
+ { label: "T", value: 4 },
11
+ { label: "F", value: 5 },
12
+ { label: "S", value: 6 },
13
+ ];
14
+
15
+ let values = value ? value : [];
16
+
17
+ const handleChange = (value) => {
18
+ let selectedValues = [...values];
19
+ let index = selectedValues.indexOf(value);
20
+ if (index >= 0) {
21
+ selectedValues.splice(index, 1);
22
+ } else {
23
+ selectedValues.push(value);
24
+ }
25
+ selectedValues.sort((a, b) => a - b);
26
+ onChange(selectedValues, name);
27
+ };
28
+
29
+ return (
30
+ <div className="d-inline-block float-left w-100 position-relative">
31
+ {weekConfig.map((week, index) => {
32
+ return (
33
+ <CustomInput
34
+ inline
35
+ type="checkbox"
36
+ name={name}
37
+ key={index}
38
+ id={`day-${index}`}
39
+ label={week.label}
40
+ value={week.value}
41
+ checked={values.indexOf(week.value) >= 0}
42
+ onChange={(e) => handleChange(week.value)}
43
+ />
44
+ );
45
+ })}
46
+ </div>
47
+ );
48
+ };
49
+
50
+ export default WeekSelector;
@@ -0,0 +1,15 @@
1
+ export const getScheduleDefaultValues = () => {
2
+ return {
3
+ name: "",
4
+ type: "available",
5
+ repeat: "weekly",
6
+ days: [0, 1, 2, 3, 4, 5, 6],
7
+ dates: [],
8
+ departure: "fixed",
9
+ priceDelta: "",
10
+ priceDeltaType: "",
11
+ times: [],
12
+ timeRanges: [],
13
+ allowedPrivacies: ["public", "private"],
14
+ };
15
+ };
@@ -0,0 +1,128 @@
1
+ import _ from "lodash";
2
+ import { formatDate } from "../../../";
3
+
4
+ function getDateRangeDescription(schedule) {
5
+ let dateRangeDescription = "";
6
+ if (!schedule.start && !schedule.end) {
7
+ return dateRangeDescription;
8
+ }
9
+ if (schedule.start) {
10
+ dateRangeDescription += ` Starting on ${formatDate(new Date(schedule.start), "MM/dd/yyyy")}`;
11
+ }
12
+ if (schedule.end) {
13
+ dateRangeDescription += ` untill ${formatDate(new Date(schedule.end), "MM/dd/yyyy")}`;
14
+ }
15
+ return dateRangeDescription;
16
+ }
17
+
18
+ function getWeeklyDescription(selectedWeekNumbers) {
19
+ let weekDescription = "";
20
+ let weekNameArr = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
21
+ selectedWeekNumbers.forEach((weekNumber, index) => {
22
+ weekDescription += weekNameArr[weekNumber];
23
+ if (index < selectedWeekNumbers.length - 1) {
24
+ weekDescription += ", ";
25
+ }
26
+ });
27
+ return weekDescription;
28
+ }
29
+
30
+ function getCustomDateDescription(selectedDates) {
31
+ let displayDates = selectedDates.slice(0, 4);
32
+ let customDateDescription = "";
33
+ displayDates.forEach((displayDate, index) => {
34
+ customDateDescription += new Date(displayDate).toLocaleDateString("en-US");
35
+ if (index < displayDates.length - 1) {
36
+ customDateDescription += ", ";
37
+ }
38
+ });
39
+ customDateDescription += selectedDates.length - 4 > 0 ? ` and ${selectedDates.length - 4} more dates` : "";
40
+ return customDateDescription;
41
+ }
42
+
43
+ function getPriceDescription(schedule, basePrice) {
44
+ if (schedule.type === "unavailable" || !basePrice) {
45
+ return "";
46
+ }
47
+ const { priceDelta, priceDeltaType } = schedule;
48
+ if (priceDeltaType === "increase") {
49
+ basePrice += parseFloat(priceDelta);
50
+ } else if (priceDeltaType === "decrease") {
51
+ basePrice -= parseFloat(priceDelta);
52
+ }
53
+ return ` for $ ${basePrice}`;
54
+ }
55
+
56
+ function formatNumberToTime(time) {
57
+ const minute = time % 100;
58
+ let meridian = "AM";
59
+ let hour = parseInt(time / 100);
60
+ if (hour === 0) {
61
+ hour = 12;
62
+ } else if (hour === 12) {
63
+ meridian = "PM";
64
+ } else if (hour > 12) {
65
+ hour -= 12;
66
+ meridian = "PM";
67
+ }
68
+ return `${hour}:${minute < 10 ? "0" + minute : minute} ${meridian}`;
69
+ }
70
+
71
+ function getTimesDescription(schedule) {
72
+ let times = _.sortBy(_.filter(schedule.times, (time) => time !== null));
73
+ if (schedule.type === "unavailable" || schedule.departure !== "fixed" || !times || times.length === 0) {
74
+ return "";
75
+ }
76
+ let timesDescription = " at ";
77
+ let timesArray = _.map(times, (time) => formatNumberToTime(time));
78
+ return timesDescription + timesArray.join(", ");
79
+ }
80
+
81
+ function getTimeRangeDescription(schedule) {
82
+ if (schedule.type === "available" || !schedule.timeRanges || schedule.timeRanges.length === 0) {
83
+ return "";
84
+ }
85
+ let timeRangeArray = [];
86
+ schedule.timeRanges.forEach((timeRange, index) => {
87
+ if (timeRange.startTime && timeRange.endTime) {
88
+ timeRangeArray.push(
89
+ `${index === 0 ? "between" : ""} ${formatNumberToTime(timeRange.startTime)} - ${formatNumberToTime(
90
+ timeRange.endTime,
91
+ )}`,
92
+ );
93
+ } else if (timeRange.startTime) {
94
+ timeRangeArray.push(` after ${formatNumberToTime(timeRange.startTime)}`);
95
+ } else if (timeRange.endTime) {
96
+ timeRangeArray.push(` before ${formatNumberToTime(timeRange.endTime)}`);
97
+ }
98
+ });
99
+ return ` for trips ${timeRangeArray.join(", ")}`;
100
+ }
101
+
102
+ function getPrefixDescription(schedule) {
103
+ let prefixDescription = "";
104
+ let availableDescription = "";
105
+ if (schedule.repeat === "weekly") {
106
+ prefixDescription = schedule.days.length === 7 ? `Daily` : `Every ${getWeeklyDescription(schedule.days)}`;
107
+ availableDescription = schedule.type === "available" ? "" : "Blackout ";
108
+ } else {
109
+ if (schedule.dates.length === 0) {
110
+ prefixDescription = "Custom dates (no dates selected)";
111
+ availableDescription = schedule.type === "available" ? "" : "Blackout ";
112
+ } else {
113
+ prefixDescription = ` ${getCustomDateDescription(schedule.dates)}`;
114
+ availableDescription = schedule.type === "available" ? "Occurs on" : "Blackout for trips departing on";
115
+ }
116
+ }
117
+ return availableDescription + prefixDescription;
118
+ }
119
+
120
+ export function getScheduleSummary(schedule, price = 0) {
121
+ return (
122
+ getPrefixDescription(schedule) +
123
+ getTimesDescription(schedule) +
124
+ getPriceDescription(schedule, price) +
125
+ getDateRangeDescription(schedule) +
126
+ getTimeRangeDescription(schedule)
127
+ );
128
+ }
@@ -0,0 +1,4 @@
1
+ import ScheduleEditor from "./ScheduleEditor";
2
+ import { getScheduleSummary } from "./helpers/scheduleSummary";
3
+
4
+ export { ScheduleEditor, getScheduleSummary };
@@ -0,0 +1,31 @@
1
+ import classNames from "classnames";
2
+ import React, { Fragment } from "react";
3
+ import styles from "./Step.module.scss";
4
+ import PropTypes from "prop-types";
5
+
6
+ const Step = ({ children, last, label, current, done, size }) => {
7
+ return (
8
+ <Fragment>
9
+ <div
10
+ style={{ width: size, height: size, maxWidth: size, maxHeight: size }}
11
+ className={classNames(
12
+ styles.circle,
13
+ "d-flex align-items-center justify-content-center rounded-circle border mr-2",
14
+ { "border-primary bg-primary text-white": current, "border-success bg-success text-white": done },
15
+ )}
16
+ >
17
+ {children}
18
+ </div>
19
+
20
+ <div className={classNames(styles.label, { [styles.current]: current, "text-dark": current })}>{label}</div>
21
+ {last ? null : <div className={classNames(styles.line, "mx-4", { "bg-primary": done })} />}
22
+ </Fragment>
23
+ );
24
+ };
25
+
26
+ Step.propTypes = {
27
+ done: PropTypes.bool,
28
+ last: PropTypes.bool,
29
+ };
30
+
31
+ export default Step;
@@ -0,0 +1,25 @@
1
+ @import "../../styles/variables.scss";
2
+
3
+ .circle {
4
+ font-size: 12px;
5
+ font-weight: bold;
6
+ flex-shrink: 0;
7
+ }
8
+
9
+ .label {
10
+ text-transform: uppercase;
11
+ font-size: 11px;
12
+ font-weight: bold;
13
+ }
14
+
15
+ @include media-breakpoint-down(xs) {
16
+ .label:not(.current) {
17
+ display: none;
18
+ }
19
+ }
20
+
21
+ .line {
22
+ background-color: $gray-300;
23
+ height: 1px;
24
+ flex: 1;
25
+ }
@@ -0,0 +1,25 @@
1
+ import classNames from "classnames";
2
+ import PropTypes from "prop-types";
3
+ import React, { Children, cloneElement } from "react";
4
+ import styles from "./Stepper.module.scss";
5
+
6
+ const Stepper = ({ children, size = 26, className, ...rest }) => {
7
+ return (
8
+ <div className={classNames(className, "mx-auto")} {...rest}>
9
+ <div className={classNames(styles.text, "d-flex align-items-center justify-content-between")}>
10
+ {Children.map(children, (child, index) =>
11
+ cloneElement(child, { size, last: Children.count(children) === index + 1 }),
12
+ )}
13
+ </div>
14
+ </div>
15
+ );
16
+ };
17
+
18
+ Stepper.propTypes = {
19
+ /**
20
+ * Size of the circle.
21
+ */
22
+ size: PropTypes.number,
23
+ };
24
+
25
+ export default Stepper;
@@ -0,0 +1,5 @@
1
+ @import "../../styles/variables.scss";
2
+
3
+ .text {
4
+ color: $gray-500;
5
+ }
@@ -0,0 +1,4 @@
1
+ import Stepper from "./Stepper";
2
+ import Step from "./Step";
3
+
4
+ export { Stepper, Step };
@@ -0,0 +1,51 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import classNames from "classnames";
3
+ import { Dropdown, DropdownToggle, DropdownMenu } from "reactstrap";
4
+ import TimeSlotPopOver from "./TimePickerPopover";
5
+ import styles from "./TimePicker.module.scss";
6
+
7
+ const formatValue = (value) => {
8
+ if (_.isNil(value)) {
9
+ return null;
10
+ }
11
+
12
+ return parseInt(value / 100) + ":" + (value % 100 < 10 ? "0" + (value % 100) : value % 100);
13
+ };
14
+
15
+ const TimePicker = ({ value, onChange }) => {
16
+ const [isOpen, setIsOpen] = useState(false);
17
+ const [selectedValue, setSelectedValue] = useState(value);
18
+
19
+ useEffect(() => {
20
+ setSelectedValue(value);
21
+ }, [value]);
22
+
23
+ const handleChange = (updatedValue) => {
24
+ setSelectedValue(updatedValue);
25
+ if (onChange) {
26
+ onChange(updatedValue);
27
+ }
28
+ };
29
+
30
+ const handleToggle = () => {
31
+ setIsOpen(!isOpen);
32
+ };
33
+
34
+ return (
35
+ <Dropdown isOpen={isOpen} toggle={handleToggle}>
36
+ <DropdownToggle
37
+ color="primary"
38
+ className={classNames(styles.button, "p-0 bg-white rounded text-center")}
39
+ outline
40
+ >
41
+ {formatValue(selectedValue)}
42
+ </DropdownToggle>
43
+
44
+ <DropdownMenu className={classNames(styles.dropdownMenu, "p-4")}>
45
+ <TimeSlotPopOver value={selectedValue} onChange={handleChange} onClose={() => setIsOpen(false)} />
46
+ </DropdownMenu>
47
+ </Dropdown>
48
+ );
49
+ };
50
+
51
+ export default TimePicker;
@@ -0,0 +1,16 @@
1
+ @import "../../styles/variables.scss";
2
+
3
+ .button {
4
+ width: 50px;
5
+ height: 35px; // Same height as form inputs.
6
+ border: solid 1px $input-border;
7
+
8
+ &,
9
+ &:hover {
10
+ color: $input-color;
11
+ }
12
+ }
13
+
14
+ .dropdownMenu {
15
+ width: 500px;
16
+ }
@@ -0,0 +1,94 @@
1
+ import React, { useState } from "react";
2
+ import classNames from "classnames";
3
+ import { Row, Button } from "reactstrap";
4
+ import styles from "./TimePickerPopover.module.scss";
5
+
6
+ const TimePickerPopover = ({ value, onChange, onClose }) => {
7
+ const hourArray = Array.from(Array(24).keys());
8
+ const minuteArray = Array.from([...Array(12).keys()].map((m) => m * 5));
9
+ let selectedValue = {};
10
+
11
+ if (value && !isNaN(value)) {
12
+ const inputValue = parseInt(value);
13
+ selectedValue = {
14
+ minute: inputValue % 100,
15
+ hour: parseInt(inputValue / 100),
16
+ };
17
+ }
18
+
19
+ const initialActions = {
20
+ minute: false,
21
+ hour: false,
22
+ };
23
+
24
+ const [actions, setActions] = useState(initialActions);
25
+
26
+ const handleClick = (value, key) => {
27
+ selectedValue[key] = value;
28
+ if (!selectedValue.minute) {
29
+ selectedValue.minute = 0;
30
+ }
31
+ if (!selectedValue.hour) {
32
+ selectedValue.hour = 0;
33
+ }
34
+ actions[key] = true;
35
+ setActions(actions);
36
+ onChange(selectedValue.hour * 100 + selectedValue.minute);
37
+ if (actions.hour && actions.minute) {
38
+ onClose();
39
+ }
40
+ };
41
+
42
+ return (
43
+ <Row>
44
+ <div className={styles.hourContainer}>
45
+ <div className="p-1 text-center">HOUR</div>
46
+ <div className={classNames(styles.content, "d-inline-block")}>
47
+ <div className={classNames(styles.meridian, "float-left text-center")}>
48
+ <p>AM</p>
49
+ <p>PM</p>
50
+ </div>
51
+ <div className={classNames(styles.hours, "float-left")}>
52
+ {hourArray.map((hour) => (
53
+ <Button
54
+ key={hour}
55
+ onClick={(e) => handleClick(hour, "hour")}
56
+ className={classNames(
57
+ styles.slotButton,
58
+ "d-inline-flex align-items-center justify-content-center",
59
+ { [styles.selected]: hour === selectedValue.hour },
60
+ )}
61
+ >
62
+ {hour}
63
+ </Button>
64
+ ))}
65
+ </div>
66
+ </div>
67
+ </div>
68
+ <div className={styles.minuteContainer}>
69
+ <div className="p-1 text-center">MINUTE</div>
70
+ <div className={classNames(styles.content, "d-inline-block")}>
71
+ <div className={classNames(styles.minutes, "float-left")}>
72
+ {minuteArray.map((minute) => (
73
+ <Button
74
+ className={classNames(
75
+ styles.slotButton,
76
+ "rounded border-secondary d-inline-flex align-items-center justify-content-center",
77
+ {
78
+ [styles.selected]: minute === selectedValue.minute,
79
+ },
80
+ )}
81
+ key={minute}
82
+ onClick={(e) => handleClick(minute, "minute")}
83
+ >
84
+ {minute < 10 ? "0" + minute : minute}
85
+ </Button>
86
+ ))}
87
+ </div>
88
+ </div>
89
+ </div>
90
+ </Row>
91
+ );
92
+ };
93
+
94
+ export default TimePickerPopover;