@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.
- package/.github/ISSUE_TEMPLATE/v2-component.md +12 -0
- package/.nvmrc +1 -0
- package/.prettierrc +6 -0
- package/.storybook/main.js +17 -0
- package/.storybook/preview.js +3 -0
- package/README.md +1 -0
- package/babel.config.json +13 -0
- package/build/favicon.ico +0 -0
- package/build/style.css +1 -0
- package/build/ui-kit.mjs +12455 -0
- package/build/ui-kit.umd.js +33 -0
- package/lib/index.js +1 -0
- package/package.json +48 -0
- package/src/components/CountrySelect/CountrySelect.js +15 -0
- package/src/components/CountrySelect/index.js +1 -0
- package/src/components/DatePicker/DatePicker.js +80 -0
- package/src/components/DatePicker/DatePicker.module.scss +3 -0
- package/src/components/DatePicker/DatePicker.scss +18 -0
- package/src/components/DatePicker/index.js +3 -0
- package/src/components/FormInput/FormInput.js +23 -0
- package/src/components/FormInput/index.js +3 -0
- package/src/components/MiniStepper/MiniStepper.js +39 -0
- package/src/components/MiniStepper/MiniStepper.module.scss +9 -0
- package/src/components/MiniStepper/index.js +1 -0
- package/src/components/PhoneInput/PhoneInput.js +44 -0
- package/src/components/PhoneInput/index.js +1 -0
- package/src/components/ProductSelector/ProductListItem/ProductListItem.js +41 -0
- package/src/components/ProductSelector/ProductListItem/ProductListItem.module.scss +32 -0
- package/src/components/ProductSelector/ProductSelector.js +71 -0
- package/src/components/ProductSelector/index.js +3 -0
- package/src/components/ScheduleEditor/ScheduleEditor.js +277 -0
- package/src/components/ScheduleEditor/ScheduleEditorRow.js +26 -0
- package/src/components/ScheduleEditor/TimeRangeSelector/TimeRangeSelector.js +94 -0
- package/src/components/ScheduleEditor/TimeRangeSelector/TimeRangeSelector.module.scss +13 -0
- package/src/components/ScheduleEditor/TimeSlotSelector/TimeSlotSelector.js +62 -0
- package/src/components/ScheduleEditor/TimeSlotSelector/TimeSlotSelector.module.scss +21 -0
- package/src/components/ScheduleEditor/WeekSelector/WeekSelector.js +50 -0
- package/src/components/ScheduleEditor/helpers/schedule.js +15 -0
- package/src/components/ScheduleEditor/helpers/scheduleSummary.js +128 -0
- package/src/components/ScheduleEditor/index.js +4 -0
- package/src/components/Stepper/Step.js +31 -0
- package/src/components/Stepper/Step.module.scss +25 -0
- package/src/components/Stepper/Stepper.js +25 -0
- package/src/components/Stepper/Stepper.module.scss +5 -0
- package/src/components/Stepper/index.js +4 -0
- package/src/components/TimePicker/TimePicker.js +51 -0
- package/src/components/TimePicker/TimePicker.module.scss +16 -0
- package/src/components/TimePicker/TimePickerPopover.js +94 -0
- package/src/components/TimePicker/TimePickerPopover.module.scss +48 -0
- package/src/components/TimePicker/index.js +3 -0
- package/src/icons/CheckIcon.js +9 -0
- package/src/icons/TrashIcon.js +17 -0
- package/src/index.js +18 -0
- package/src/stories/Button.stories.js +66 -0
- package/src/stories/CountrySelect.stories.js +18 -0
- package/src/stories/DatePicker.stories.js +96 -0
- package/src/stories/FormInput.stories.js +25 -0
- package/src/stories/Forms.stories.js +43 -0
- package/src/stories/Icons.stories.js +21 -0
- package/src/stories/MiniStepper.stories.js +15 -0
- package/src/stories/PhoneInput.stories.js +26 -0
- package/src/stories/ProductSelector.stories.js +42 -0
- package/src/stories/ScheduleEditor.stories.js +37 -0
- package/src/stories/Stepper.stories.js +36 -0
- package/src/stories/TimePicker.stories.js +14 -0
- package/src/stories/Typography.stories.js +18 -0
- package/src/styles/custom.scss +60 -0
- package/src/styles/index.scss +3 -0
- package/src/styles/variables.scss +43 -0
- package/src/theme.js +359 -0
- package/src/values/countries.js +196 -0
- package/src/values/products.js +86 -0
- package/src/values/sellers.js +5 -0
- package/webpack.config.js +47 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Fragment } from "react";
|
|
3
|
+
import DayPicker, { DateUtils } from "react-day-picker";
|
|
4
|
+
import DayPickerInput from "react-day-picker/DayPickerInput";
|
|
5
|
+
import dateFnsFormat from "date-fns/format";
|
|
6
|
+
import _ from "lodash";
|
|
7
|
+
import classNames from "classnames";
|
|
8
|
+
import "react-day-picker/lib/style.css";
|
|
9
|
+
import "./DatePicker.scss"; // Not using CSS modules here since we need to override styles from react-day-picker.
|
|
10
|
+
import styles from "./DatePicker.module.scss";
|
|
11
|
+
|
|
12
|
+
export const formatDate = (date, format, locale) => {
|
|
13
|
+
return dateFnsFormat(date, format, { locale });
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const DatePicker = ({
|
|
17
|
+
isDatePicker,
|
|
18
|
+
isMultiple = false,
|
|
19
|
+
value,
|
|
20
|
+
minDate,
|
|
21
|
+
maxDate,
|
|
22
|
+
placeholder,
|
|
23
|
+
onChange,
|
|
24
|
+
format = "MM-dd-yyyy",
|
|
25
|
+
}) => {
|
|
26
|
+
const disableDays = {};
|
|
27
|
+
const dayPickerProps = {};
|
|
28
|
+
if (minDate) {
|
|
29
|
+
disableDays["before"] = minDate;
|
|
30
|
+
}
|
|
31
|
+
if (maxDate) {
|
|
32
|
+
disableDays["after"] = maxDate;
|
|
33
|
+
}
|
|
34
|
+
dayPickerProps["disabledDays"] = disableDays;
|
|
35
|
+
|
|
36
|
+
if (_.isArray(value)) {
|
|
37
|
+
isMultiple = true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (isDatePicker) {
|
|
41
|
+
dayPickerProps["selectedDays"] = value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const handleChange = (v) => {
|
|
45
|
+
if (isMultiple) {
|
|
46
|
+
if (value) {
|
|
47
|
+
if (_.some(value, (item) => DateUtils.isSameDay(item, v))) {
|
|
48
|
+
_.remove(value, (item) => DateUtils.isSameDay(item, v));
|
|
49
|
+
} else {
|
|
50
|
+
value.push(v);
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
value = [v];
|
|
54
|
+
}
|
|
55
|
+
onChange(value);
|
|
56
|
+
} else {
|
|
57
|
+
onChange(v);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Fragment>
|
|
63
|
+
{isDatePicker ? (
|
|
64
|
+
<DayPickerInput
|
|
65
|
+
component={(props) => <input className={classNames(styles.input, "form-control")} {...props} />}
|
|
66
|
+
value={value}
|
|
67
|
+
placeholder={placeholder}
|
|
68
|
+
format={format}
|
|
69
|
+
formatDate={formatDate}
|
|
70
|
+
onDayChange={(v) => handleChange(v)}
|
|
71
|
+
dayPickerProps={dayPickerProps}
|
|
72
|
+
/>
|
|
73
|
+
) : (
|
|
74
|
+
<DayPicker selectedDays={value} onDayClick={(v) => handleChange(v)} dayPickerProps={dayPickerProps} />
|
|
75
|
+
)}
|
|
76
|
+
</Fragment>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export default DatePicker;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
@import "../../styles/variables.scss";
|
|
2
|
+
|
|
3
|
+
.DayPicker-Day {
|
|
4
|
+
line-height: normal !important;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.DayPicker-Day--selected {
|
|
8
|
+
background-color: theme-color("primary") !important;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.DayPicker-Caption,
|
|
12
|
+
.DayPicker-Day:not(.DayPicker-Day--disabled) {
|
|
13
|
+
color: $body-color;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.DayPicker-Day--today:not(.DayPicker-Day--selected):not(.DayPicker-Day--disabled) {
|
|
17
|
+
color: theme-color("danger");
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import classNames from "classnames";
|
|
2
|
+
import React, { cloneElement } from "react";
|
|
3
|
+
import { FormGroup, Input, Label } from "reactstrap";
|
|
4
|
+
|
|
5
|
+
const FormInput = ({ label, error, children, ...rest }) => {
|
|
6
|
+
return (
|
|
7
|
+
<FormGroup>
|
|
8
|
+
<Label htmlFor={rest.id} className={classNames({ "text-danger": error })}>
|
|
9
|
+
<span className={classNames({ "font-weight-bold": error })}>{label}</span> {error}
|
|
10
|
+
</Label>
|
|
11
|
+
|
|
12
|
+
{rest.type === "select" || !children ? (
|
|
13
|
+
<Input id={rest.id} invalid={!!error} {...rest}>
|
|
14
|
+
{children}
|
|
15
|
+
</Input>
|
|
16
|
+
) : (
|
|
17
|
+
cloneElement(children, rest)
|
|
18
|
+
)}
|
|
19
|
+
</FormGroup>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default FormInput;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import classNames from "classnames";
|
|
2
|
+
import PropTypes from "prop-types";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import styles from "./MiniStepper.module.scss";
|
|
5
|
+
|
|
6
|
+
export const MiniStepper = ({ count, value, size, ...rest }) => {
|
|
7
|
+
const dots = [];
|
|
8
|
+
|
|
9
|
+
for (let i = 0; i < count; i++) {
|
|
10
|
+
dots.push(
|
|
11
|
+
<span
|
|
12
|
+
style={{ fontSize: size }}
|
|
13
|
+
key={i}
|
|
14
|
+
className={classNames(styles.dot, { [styles.done]: i < value }, "mr-1")}
|
|
15
|
+
>
|
|
16
|
+
•
|
|
17
|
+
</span>,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return <div {...rest}>{dots}</div>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
MiniStepper.propTypes = {
|
|
25
|
+
/**
|
|
26
|
+
* Total number of steps
|
|
27
|
+
*/
|
|
28
|
+
count: PropTypes.number.isRequired,
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Current step index
|
|
32
|
+
*/
|
|
33
|
+
value: PropTypes.number.isRequired,
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Size (font size) of each dot
|
|
37
|
+
*/
|
|
38
|
+
size: PropTypes.number,
|
|
39
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./MiniStepper";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { AsYouType, getCountryCallingCode } from "libphonenumber-js";
|
|
2
|
+
import PropTypes from "prop-types";
|
|
3
|
+
import React, { useRef, useState } from "react";
|
|
4
|
+
import { Input, InputGroup } from "reactstrap";
|
|
5
|
+
import { CountrySelect } from "../CountrySelect";
|
|
6
|
+
|
|
7
|
+
const getCountry = (value) => {
|
|
8
|
+
const asYouType = new AsYouType();
|
|
9
|
+
asYouType.input(value);
|
|
10
|
+
const phoneNumber = asYouType.getNumber();
|
|
11
|
+
return phoneNumber && phoneNumber.country ? phoneNumber.country : null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const PhoneInput = ({ value, onChange, error, defaultCountry = "", ...rest }) => {
|
|
15
|
+
const [country, setCountry] = useState(defaultCountry);
|
|
16
|
+
const inputRef = useRef();
|
|
17
|
+
const invalid = !!error;
|
|
18
|
+
|
|
19
|
+
const handleValueChange = (e) => {
|
|
20
|
+
onChange(e.target.value);
|
|
21
|
+
setCountry(getCountry(value) || country);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const handleCountryChange = (e) => {
|
|
25
|
+
const country = e.target.value;
|
|
26
|
+
setCountry(country);
|
|
27
|
+
const value = "+" + getCountryCallingCode(country);
|
|
28
|
+
onChange(value);
|
|
29
|
+
inputRef.current.focus();
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<InputGroup>
|
|
34
|
+
<CountrySelect invalid={invalid} style={{ maxWidth: 60 }} value={country} onChange={handleCountryChange} />
|
|
35
|
+
<Input invalid={invalid} {...rest} innerRef={inputRef} onChange={handleValueChange} value={value} />
|
|
36
|
+
</InputGroup>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
PhoneInput.propTypes = {
|
|
41
|
+
onChange: PropTypes.func,
|
|
42
|
+
value: PropTypes.any,
|
|
43
|
+
defaultCountry: PropTypes.string,
|
|
44
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./PhoneInput";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import classNames from "classnames";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Col, Row } from "reactstrap";
|
|
4
|
+
import { CheckIcon } from "../../../";
|
|
5
|
+
import styles from "./ProductListItem.module.scss";
|
|
6
|
+
|
|
7
|
+
const ProductListItem = ({ product, isSelected, onProductSelect }) => {
|
|
8
|
+
return (
|
|
9
|
+
<Col
|
|
10
|
+
xs={12}
|
|
11
|
+
md={6}
|
|
12
|
+
className={classNames(styles.item, "cursor-pointer px-0 my-2")}
|
|
13
|
+
onClick={() => onProductSelect(product.id)}
|
|
14
|
+
>
|
|
15
|
+
<Row className={classNames({ [styles.selected]: isSelected }, styles.row, "mx-3 p-3")}>
|
|
16
|
+
<Col xs={3} md={2} className={classNames(styles.thumbnailContainer, "p-0")}>
|
|
17
|
+
<div className={classNames(styles.thumbnail, "w-100 m-0")}>
|
|
18
|
+
{product.photo?.src ? (
|
|
19
|
+
<img className="mw-100" alt="experience" src={product.photo.src} />
|
|
20
|
+
) : null}
|
|
21
|
+
{isSelected ? (
|
|
22
|
+
<div
|
|
23
|
+
className={classNames(
|
|
24
|
+
styles.selected,
|
|
25
|
+
"w-100 h-100 text-white d-flex text-center align-items-center justify-content-center",
|
|
26
|
+
)}
|
|
27
|
+
>
|
|
28
|
+
<CheckIcon style={{ width: 28, height: 28 }} />
|
|
29
|
+
</div>
|
|
30
|
+
) : null}
|
|
31
|
+
</div>
|
|
32
|
+
</Col>
|
|
33
|
+
<Col xs={9} md={10}>
|
|
34
|
+
<span className={styles.title}>{product.name}</span>
|
|
35
|
+
</Col>
|
|
36
|
+
</Row>
|
|
37
|
+
</Col>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default ProductListItem;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
@import "../../../styles/variables.scss";
|
|
2
|
+
|
|
3
|
+
.title {
|
|
4
|
+
font-size: 16px;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.item {
|
|
8
|
+
.row {
|
|
9
|
+
opacity: 0.6;
|
|
10
|
+
}
|
|
11
|
+
.selected {
|
|
12
|
+
opacity: 1;
|
|
13
|
+
}
|
|
14
|
+
&:hover {
|
|
15
|
+
.row {
|
|
16
|
+
background-color: $hover-background;
|
|
17
|
+
opacity: 1;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.thumbnailContainer {
|
|
23
|
+
min-height: 30px;
|
|
24
|
+
height: fit-content;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.thumbnail .selected {
|
|
28
|
+
position: absolute;
|
|
29
|
+
font-size: 24px;
|
|
30
|
+
background-color: rgba($color: theme-color("success"), $alpha: 0.7);
|
|
31
|
+
top: 0;
|
|
32
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
import _ from "lodash";
|
|
4
|
+
import { Container, Row, Col, Label, Input } from "reactstrap";
|
|
5
|
+
import ProductListItem from "./ProductListItem/ProductListItem";
|
|
6
|
+
|
|
7
|
+
const filterProductsBySearch = (searchQuery = "", products = []) => {
|
|
8
|
+
return products.filter((product) => {
|
|
9
|
+
return product.name.toLowerCase().includes(searchQuery.trim().toLowerCase());
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const ProductSelector = ({ products = [], value = [], onChange }) => {
|
|
14
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
15
|
+
const filteredProducts = filterProductsBySearch(searchQuery, products);
|
|
16
|
+
|
|
17
|
+
const isProductSelected = (productId) => {
|
|
18
|
+
return value.some((selectedProduct) => selectedProduct.id === productId);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const onProductSelect = (productId) => {
|
|
22
|
+
if (isProductSelected(productId)) {
|
|
23
|
+
onChange(value.filter((v) => productId !== v.id));
|
|
24
|
+
} else {
|
|
25
|
+
onChange([...value, products.find((p) => p.id === productId)]);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const onSelectAll = (e) => {
|
|
30
|
+
onChange(e.target.checked ? products : []);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Container className={classNames("p-0")}>
|
|
35
|
+
<Row className={classNames("d-flex align-items-center bg-light mb-1 p-3")}>
|
|
36
|
+
<Col md={8}>
|
|
37
|
+
<Label check>
|
|
38
|
+
<Input
|
|
39
|
+
type="checkbox"
|
|
40
|
+
checked={products.length === value.length}
|
|
41
|
+
onChange={(e) => onSelectAll(e)}
|
|
42
|
+
/>{" "}
|
|
43
|
+
{value.length} Listing(s) selected updated
|
|
44
|
+
</Label>
|
|
45
|
+
</Col>
|
|
46
|
+
<Col md={4}>
|
|
47
|
+
<Input
|
|
48
|
+
type="text"
|
|
49
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
50
|
+
placeholder="Search"
|
|
51
|
+
className="float-right"
|
|
52
|
+
/>
|
|
53
|
+
</Col>
|
|
54
|
+
</Row>
|
|
55
|
+
<Row>
|
|
56
|
+
{filteredProducts.map((product) => {
|
|
57
|
+
return (
|
|
58
|
+
<ProductListItem
|
|
59
|
+
key={product.id}
|
|
60
|
+
onProductSelect={onProductSelect}
|
|
61
|
+
isSelected={isProductSelected(product.id)}
|
|
62
|
+
product={product}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
})}
|
|
66
|
+
</Row>
|
|
67
|
+
</Container>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default ProductSelector;
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import React, { Fragment } from "react";
|
|
2
|
+
import { CustomInput, Input, InputGroup, InputGroupAddon, InputGroupText } from "reactstrap";
|
|
3
|
+
import _ from "lodash";
|
|
4
|
+
import classNames from "classnames";
|
|
5
|
+
import ScheduleEditorRow from "./ScheduleEditorRow";
|
|
6
|
+
import { DatePicker } from "../..";
|
|
7
|
+
import WeekSelector from "./WeekSelector/WeekSelector";
|
|
8
|
+
import TimeSlotSelector from "./TimeSlotSelector/TimeSlotSelector";
|
|
9
|
+
import TimeRangeSelector from "./TimeRangeSelector/TimeRangeSelector";
|
|
10
|
+
import { getScheduleSummary } from "./helpers/scheduleSummary";
|
|
11
|
+
import { getScheduleDefaultValues } from "./helpers/schedule";
|
|
12
|
+
|
|
13
|
+
const ScheduleEditor = ({ value = {}, errors = {}, price = 0, isNew = true, onChange }) => {
|
|
14
|
+
value = JSON.parse(JSON.stringify(value));
|
|
15
|
+
if (value.dates?.length > 0) {
|
|
16
|
+
value.dates = value.dates.map((date) => new Date(date));
|
|
17
|
+
}
|
|
18
|
+
if (value.start) {
|
|
19
|
+
value.start = new Date(value.start);
|
|
20
|
+
}
|
|
21
|
+
if (value.end) {
|
|
22
|
+
value.end = new Date(value.end);
|
|
23
|
+
}
|
|
24
|
+
const today = new Date();
|
|
25
|
+
const defaultScheduleEditorValues = getScheduleDefaultValues();
|
|
26
|
+
const schedule = _.extend(defaultScheduleEditorValues, value);
|
|
27
|
+
|
|
28
|
+
const handleChange = (value, key) => {
|
|
29
|
+
let updatedSchedule = { ...schedule };
|
|
30
|
+
updatedSchedule[key] = value;
|
|
31
|
+
onChange({ ...updatedSchedule, summary: getScheduleSummary(updatedSchedule) });
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const handleAllowedPrivaciesChange = (privacy) => {
|
|
35
|
+
let updatedPrivacies = [...schedule.allowedPrivacies];
|
|
36
|
+
if (updatedPrivacies.includes(privacy)) {
|
|
37
|
+
updatedPrivacies = updatedPrivacies.filter((p) => p !== privacy);
|
|
38
|
+
} else {
|
|
39
|
+
updatedPrivacies.push(privacy);
|
|
40
|
+
}
|
|
41
|
+
handleChange(updatedPrivacies, "allowedPrivacies");
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div>
|
|
46
|
+
<ScheduleEditorRow label="Name" htmlFor="scheduleName">
|
|
47
|
+
<Input
|
|
48
|
+
value={schedule.name}
|
|
49
|
+
onChange={(e) => handleChange(e.target.value, "name")}
|
|
50
|
+
className="w-50"
|
|
51
|
+
type="name"
|
|
52
|
+
name="scheduleName"
|
|
53
|
+
id="scheduleName"
|
|
54
|
+
placeholder="Daily"
|
|
55
|
+
/>
|
|
56
|
+
</ScheduleEditorRow>
|
|
57
|
+
<ScheduleEditorRow
|
|
58
|
+
label="Allowed Privacies"
|
|
59
|
+
htmlFor="allowedPrivacies"
|
|
60
|
+
error={errors && errors.allowedPrivacies}
|
|
61
|
+
>
|
|
62
|
+
<div>
|
|
63
|
+
<CustomInput
|
|
64
|
+
inline
|
|
65
|
+
type="checkbox"
|
|
66
|
+
name="allowedPrivacies"
|
|
67
|
+
id="privacy-public"
|
|
68
|
+
label="Public"
|
|
69
|
+
value="public"
|
|
70
|
+
checked={schedule.allowedPrivacies.includes("public")}
|
|
71
|
+
onChange={(e) => handleAllowedPrivaciesChange("public")}
|
|
72
|
+
/>
|
|
73
|
+
<CustomInput
|
|
74
|
+
inline
|
|
75
|
+
type="checkbox"
|
|
76
|
+
name="allowedPrivacies"
|
|
77
|
+
id="privacy-private"
|
|
78
|
+
label="Private"
|
|
79
|
+
value="private"
|
|
80
|
+
checked={schedule.allowedPrivacies.includes("private")}
|
|
81
|
+
onChange={(e) => handleAllowedPrivaciesChange("private")}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
</ScheduleEditorRow>
|
|
85
|
+
<ScheduleEditorRow label="Availability">
|
|
86
|
+
{isNew ? (
|
|
87
|
+
<div>
|
|
88
|
+
<CustomInput
|
|
89
|
+
inline
|
|
90
|
+
type="radio"
|
|
91
|
+
name="type"
|
|
92
|
+
id="type-available"
|
|
93
|
+
label="Open"
|
|
94
|
+
value="available"
|
|
95
|
+
checked={schedule.type === "available"}
|
|
96
|
+
onChange={(e) => handleChange(e.target.value, "type")}
|
|
97
|
+
/>
|
|
98
|
+
<CustomInput
|
|
99
|
+
inline
|
|
100
|
+
type="radio"
|
|
101
|
+
name="type"
|
|
102
|
+
id="type-unavailable"
|
|
103
|
+
label="Blackout"
|
|
104
|
+
value="unavailable"
|
|
105
|
+
checked={schedule.type === "unavailable"}
|
|
106
|
+
onChange={(e) => handleChange(e.target.value, "type")}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
) : (
|
|
110
|
+
<div className="pl-1">{schedule.type === "available" ? "Open" : "Blackout"}</div>
|
|
111
|
+
)}
|
|
112
|
+
</ScheduleEditorRow>
|
|
113
|
+
<ScheduleEditorRow label="Repeats">
|
|
114
|
+
{isNew ? (
|
|
115
|
+
<div>
|
|
116
|
+
<CustomInput
|
|
117
|
+
inline
|
|
118
|
+
type="radio"
|
|
119
|
+
name="repeat"
|
|
120
|
+
id="repeat-weekly"
|
|
121
|
+
label="Weekly"
|
|
122
|
+
value="weekly"
|
|
123
|
+
checked={schedule.repeat === "weekly"}
|
|
124
|
+
onChange={(e) => handleChange(e.target.value, "repeat")}
|
|
125
|
+
/>
|
|
126
|
+
<CustomInput
|
|
127
|
+
inline
|
|
128
|
+
type="radio"
|
|
129
|
+
name="repeat"
|
|
130
|
+
id="repeat-custom"
|
|
131
|
+
label="Custom"
|
|
132
|
+
value="custom"
|
|
133
|
+
checked={schedule.repeat === "custom"}
|
|
134
|
+
onChange={(e) => handleChange(e.target.value, "repeat")}
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
) : (
|
|
138
|
+
<div className="pl-1">{schedule.repeat === "weekly" ? "Weekly" : "Custom"}</div>
|
|
139
|
+
)}
|
|
140
|
+
</ScheduleEditorRow>
|
|
141
|
+
{schedule.repeat === "weekly" && (
|
|
142
|
+
<ScheduleEditorRow error={errors && errors.days} label="Repeat on">
|
|
143
|
+
<WeekSelector name="days" value={schedule.days} onChange={handleChange} />
|
|
144
|
+
</ScheduleEditorRow>
|
|
145
|
+
)}
|
|
146
|
+
{schedule.repeat === "custom" && (
|
|
147
|
+
<ScheduleEditorRow error={errors && errors.dates} label="Select dates">
|
|
148
|
+
<DatePicker
|
|
149
|
+
isDatePicker={false}
|
|
150
|
+
isMultiple={true}
|
|
151
|
+
onChange={(dates) => handleChange(dates, "dates")}
|
|
152
|
+
value={schedule.dates}
|
|
153
|
+
datePickerType="calendar"
|
|
154
|
+
format="YYYY-MM-DD"
|
|
155
|
+
/>
|
|
156
|
+
</ScheduleEditorRow>
|
|
157
|
+
)}
|
|
158
|
+
{schedule.type === "available" && (
|
|
159
|
+
<Fragment>
|
|
160
|
+
<ScheduleEditorRow label="Time Slots">
|
|
161
|
+
{isNew ? (
|
|
162
|
+
<div>
|
|
163
|
+
<CustomInput
|
|
164
|
+
inline
|
|
165
|
+
type="radio"
|
|
166
|
+
name="departure"
|
|
167
|
+
id="departure-fixed"
|
|
168
|
+
label="Fixed Times"
|
|
169
|
+
value="fixed"
|
|
170
|
+
checked={schedule.departure === "fixed"}
|
|
171
|
+
onChange={(e) => handleChange(e.target.value, "departure")}
|
|
172
|
+
/>
|
|
173
|
+
<CustomInput
|
|
174
|
+
inline
|
|
175
|
+
type="radio"
|
|
176
|
+
name="departure"
|
|
177
|
+
id="departure-varies"
|
|
178
|
+
label="No Fixed Times"
|
|
179
|
+
value="varies"
|
|
180
|
+
checked={schedule.departure === "varies"}
|
|
181
|
+
onChange={(e) => handleChange(e.target.value, "departure")}
|
|
182
|
+
/>
|
|
183
|
+
</div>
|
|
184
|
+
) : (
|
|
185
|
+
<div className="pl-1">{schedule.departure === "fixed" ? "Fixed Times" : "No Varies"} </div>
|
|
186
|
+
)}
|
|
187
|
+
</ScheduleEditorRow>
|
|
188
|
+
{schedule.departure === "fixed" && (
|
|
189
|
+
<ScheduleEditorRow error={errors && errors.times} label="">
|
|
190
|
+
<TimeSlotSelector
|
|
191
|
+
name="times"
|
|
192
|
+
value={schedule.times}
|
|
193
|
+
onChange={(v) => handleChange(v, "times")}
|
|
194
|
+
/>
|
|
195
|
+
</ScheduleEditorRow>
|
|
196
|
+
)}
|
|
197
|
+
</Fragment>
|
|
198
|
+
)}
|
|
199
|
+
{schedule.type === "unavailable" && (
|
|
200
|
+
<ScheduleEditorRow label="Time ranges">
|
|
201
|
+
<TimeRangeSelector
|
|
202
|
+
error={errors && errors.timeRanges}
|
|
203
|
+
name="timeRanges"
|
|
204
|
+
value={schedule.timeRanges}
|
|
205
|
+
onChange={(v) => handleChange(v, "timeRanges")}
|
|
206
|
+
/>
|
|
207
|
+
</ScheduleEditorRow>
|
|
208
|
+
)}
|
|
209
|
+
{schedule.repeat === "weekly" && (
|
|
210
|
+
<ScheduleEditorRow label="Valid from">
|
|
211
|
+
<div>
|
|
212
|
+
<DatePicker
|
|
213
|
+
isDatePicker={true}
|
|
214
|
+
onChange={(v) => handleChange(v, "start")}
|
|
215
|
+
minDate={today}
|
|
216
|
+
maxDate={new Date(schedule.end)}
|
|
217
|
+
value={schedule.start ? new Date(schedule.start) : null}
|
|
218
|
+
format="dd MMM, yyyy"
|
|
219
|
+
placeholder="Now"
|
|
220
|
+
clearButtonText="Now"
|
|
221
|
+
/>
|
|
222
|
+
<span className="mx-2"> until </span>
|
|
223
|
+
<DatePicker
|
|
224
|
+
isDatePicker={true}
|
|
225
|
+
onChange={(v) => handleChange(v, "end")}
|
|
226
|
+
minDate={new Date(schedule.start)}
|
|
227
|
+
value={schedule.end ? new Date(schedule.end) : null}
|
|
228
|
+
format="dd MMM, yyyy"
|
|
229
|
+
placeholder="Forever"
|
|
230
|
+
clearButtonText="Forever"
|
|
231
|
+
/>
|
|
232
|
+
</div>
|
|
233
|
+
</ScheduleEditorRow>
|
|
234
|
+
)}
|
|
235
|
+
{schedule.type === "available" && (
|
|
236
|
+
<ScheduleEditorRow label="Price" error={errors && errors.priceDelta}>
|
|
237
|
+
<div className="w-75 form-check-inline">
|
|
238
|
+
<Input
|
|
239
|
+
className="w-50"
|
|
240
|
+
type="select"
|
|
241
|
+
value={schedule.priceDeltaType}
|
|
242
|
+
onChange={(e) => handleChange(e.target.value, "priceDeltaType")}
|
|
243
|
+
name="priceDeltaType"
|
|
244
|
+
>
|
|
245
|
+
<option value="">No change</option>
|
|
246
|
+
<option value="increase">Increase by</option>
|
|
247
|
+
<option value="decrease">Decrease by</option>
|
|
248
|
+
</Input>{" "}
|
|
249
|
+
|
|
250
|
+
<InputGroup
|
|
251
|
+
className={classNames("w-50", {
|
|
252
|
+
invisible: schedule.priceDeltaType === "",
|
|
253
|
+
})}
|
|
254
|
+
>
|
|
255
|
+
<InputGroupAddon addonType="prepend">
|
|
256
|
+
<InputGroupText className="bg-white">$</InputGroupText>
|
|
257
|
+
</InputGroupAddon>
|
|
258
|
+
<Input
|
|
259
|
+
className="border-left-0"
|
|
260
|
+
type="text"
|
|
261
|
+
name="pricedelta"
|
|
262
|
+
value={schedule.priceDelta}
|
|
263
|
+
onChange={(e) => handleChange(e.target.value, "priceDelta")}
|
|
264
|
+
name="priceDelta"
|
|
265
|
+
/>
|
|
266
|
+
</InputGroup>
|
|
267
|
+
</div>
|
|
268
|
+
</ScheduleEditorRow>
|
|
269
|
+
)}
|
|
270
|
+
<ScheduleEditorRow label="Summary">
|
|
271
|
+
<span>{getScheduleSummary(schedule, price)}</span>
|
|
272
|
+
</ScheduleEditorRow>
|
|
273
|
+
</div>
|
|
274
|
+
);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
export default ScheduleEditor;
|