@shopgate/engage
Version:
Shopgate's ENGAGE library.
317 lines (305 loc) • 9.59 kB
JavaScript
import "core-js/modules/es.string.replace.js";
import React, { Fragment, useState, useMemo, useEffect, useCallback } from 'react';
import classnames from 'classnames';
import moment from 'moment';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { css } from 'glamor';
import groupBy from 'lodash/groupBy';
import { SheetDrawer, Button } from '@shopgate/engage/components';
import { i18n } from '@shopgate/engage/core';
import { getActiveFulfillmentSlot } from '@shopgate/engage/cart/cart.selectors';
import { makeGetFulfillmentSlotsForLocation, getPreferredLocation } from "../../selectors";
import fetchFulfillmentSlots from "../../actions/fetchFulfillmentSlots";
/**
* Map state to props.
* @returns {Function}
*/
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const makeMapStateToProps = () => {
const getFulfillmentSlotsForLocation = makeGetFulfillmentSlotsForLocation(state => getPreferredLocation(state)?.code || null);
return state => ({
fulfillmentSlot: getActiveFulfillmentSlot(state),
locationCode: getPreferredLocation(state)?.code,
fulfillmentSlots: getFulfillmentSlotsForLocation(state)
});
};
/**
* Map dispatch to props.
* @param {Function} dispatch Dispatch
* @returns {Object}
*/
const mapDispatchToProps = dispatch => ({
fetch: locationCode => dispatch(fetchFulfillmentSlots(locationCode))
});
const styles = {
root: css({
position: 'relative',
display: 'flex',
flexDirection: 'column',
padding: 16,
paddingBottom: 0
}).toString(),
title: css({
fontSize: 24,
fontWeight: '500',
marginBottom: 8
}).toString(),
subtitle: css({
fontSize: 20,
fontWeight: '500',
marginBottom: 8,
marginTop: 16
}).toString(),
row: css({
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
marginLeft: -8,
marginRight: -8
}).toString(),
button: css({
position: 'relative',
cursor: 'pointer',
display: 'flex',
margin: 8,
justifyContent: 'center',
alignItems: 'center',
border: '1px solid var(--color-secondary)',
borderRadius: 4,
background: '#fff',
transition: 'background, color 500ms',
outline: 'none'
}).toString(),
buttonActive: css({
color: '#fff',
background: 'var(--color-secondary)'
}).toString(),
buttonDate: css({
width: 124,
height: 70,
lineHeight: 1.3
}).toString(),
buttonLabel: css({
fontSize: 20,
textAlign: 'center'
}).toString(),
buttonLabelSlot: css({
padding: 2,
fontSize: 16,
textAlign: 'center'
}).toString(),
buttonDisabled: css({
cursor: 'blocked',
pointerEvents: 'none',
border: '1px solid #444'
}).toString(),
buttonStrikethrough: css({
position: 'absolute',
background: '#444',
left: 0,
right: 0,
height: 2,
transform: 'rotate(-5deg)'
}).toString(),
buttonScheduleContainer: css({
position: 'sticky',
bottom: 'calc(-1 * env(safe-area-inset-bottom))',
margin: -16,
marginTop: 8,
background: '#fff',
padding: 16,
paddingBottom: 24
}).toString(),
buttonSchedule: css({
'&&': {
width: '100%'
}
}).toString()
};
/**
* Get Month day time.
* @param {string} time Time
* @return {string}
*/
const getMonthDay = time => {
const momentTime = moment(time, 'YYYY-MM-DD');
return momentTime.format('L').replace(new RegExp(`[^.]?${momentTime.format('YYYY')}.?`), '');
};
/**
* Get Range for a time.
* @param {string} from From
* @param {string} to To
* @return {string}
*/
const getRange = (from, to) => {
const [fromHours, fromMinutes] = from.split(':').map(x => parseInt(x, 10));
const fromMoment = moment().set({
hours: fromHours,
minutes: fromMinutes
});
const [toHours, toMinutes] = to.split(':').map(x => parseInt(x, 10));
const toMoment = moment().set({
hours: toHours,
minutes: toMinutes
});
return `${fromMoment.format('LT')} - ${toMoment.format('LT')}`;
};
/** Ranges for different times */
const MORNING_RANGE = [0, 11];
const AFTERNOON_RANGE = [12, 17];
const EVENING_RANGE = [18, 23];
const RANGES = {
morning: MORNING_RANGE,
afternoon: AFTERNOON_RANGE,
evening: EVENING_RANGE
};
/**
* @param {Object} props Props.
* @returns {JSX}
*/
const FulfillmentSlotSheet = ({
isOpen,
onClose,
onChange,
fulfillmentSlots,
locationCode,
fetch,
allowClose,
fulfillmentSlot
}) => {
// Group by date.
const groupedSlots = useMemo(() => groupBy(fulfillmentSlots, 'date'), [fulfillmentSlots]);
// Load slots when opening.
useEffect(() => {
if (!isOpen) {
return;
}
fetch({
locationCode
});
}, [fetch, isOpen, locationCode]);
// Handle active selected date.
const [selectedDate, setSelectedDate] = useState(null);
useEffect(() => {
if (!Object.keys(groupedSlots).length) {
return;
}
// Don't change if selected date is still available
if (groupedSlots[selectedDate]) {
return;
}
setSelectedDate(Object.keys(groupedSlots)[0]);
/* eslint-disable react-hooks/exhaustive-deps */
}, [groupedSlots]);
/* eslint-enable react-hooks/exhaustive-deps */
// Handle groups of time slots
const [selectedSlot, setSelectedSlot] = useState(null);
const [slotGroups, setSlotGroups] = useState(null);
useEffect(() => {
if (!groupedSlots || !groupedSlots[selectedDate]) {
setSlotGroups(null);
return;
}
const slotGroupsNew = Object.keys(RANGES).map(rangeName => {
const [start, end] = RANGES[rangeName];
return {
name: rangeName,
slots: groupedSlots[selectedDate].filter(slot => {
const [startHour] = slot.from.split(':').map(x => parseInt(x, 10));
return startHour >= start && startHour <= end;
})
};
}).filter(group => group.slots.length > 0);
setSlotGroups(slotGroupsNew);
}, [selectedDate, groupedSlots]);
useEffect(() => {
if (!selectedDate || !groupedSlots || !groupedSlots[selectedDate]) {
setSelectedSlot(null);
return;
}
// Only switch if not found.
if (groupedSlots[selectedDate].find(slot => slot.id === selectedSlot)) {
return;
}
// Select first active slot.
setSelectedSlot(groupedSlots[selectedDate]?.find(slot => slot.status === 'active')?.id || null);
/* eslint-disable react-hooks/exhaustive-deps */
}, [selectedDate]);
/* eslint-enable react-hooks/exhaustive-deps */
// When opening the selected values are set to active selected content.
useEffect(() => {
if (!isOpen || !fulfillmentSlot || !fulfillmentSlot.date) {
return;
}
setSelectedDate(fulfillmentSlot.date);
setSelectedSlot(fulfillmentSlot.id);
}, [fulfillmentSlot, isOpen]);
const handleChange = useCallback(() => {
onChange(fulfillmentSlots.find(slot => slot.id === selectedSlot));
}, [fulfillmentSlots, onChange, selectedSlot]);
return /*#__PURE__*/_jsx(SheetDrawer, {
isOpen: isOpen,
title: i18n.text('locations.your_current_timeslot.dialog.title'),
onDidClose: onClose,
allowClose: allowClose,
children: /*#__PURE__*/_jsxs("div", {
className: styles.root,
children: [/*#__PURE__*/_jsx("span", {
className: styles.title,
children: i18n.text('locations.your_current_timeslot.dialog.date')
}), /*#__PURE__*/_jsx("div", {
className: styles.row,
children: Object.keys(groupedSlots).map(date => /*#__PURE__*/_jsx("button", {
type: "button",
className: classnames(styles.button, styles.buttonDate, {
[styles.buttonActive]: selectedDate === date
}),
onClick: () => setSelectedDate(date),
children: /*#__PURE__*/_jsxs("span", {
className: styles.buttonLabel,
children: [moment(date, 'YYYY-MM-DD').format('dddd'), ' ', getMonthDay(date)]
})
}, date))
}), slotGroups && slotGroups.map(group => /*#__PURE__*/_jsxs(Fragment, {
children: [/*#__PURE__*/_jsx("span", {
className: styles.subtitle,
children: i18n.text(`locations.your_current_timeslot.dialog.${group.name}`)
}), /*#__PURE__*/_jsx("div", {
className: styles.row,
children: group.slots.map(slot => /*#__PURE__*/_jsxs("button", {
type: "button",
onClick: () => setSelectedSlot(slot.id),
className: classnames(styles.button, {
[styles.buttonDisabled]: slot.status !== 'active',
[styles.buttonActive]: slot.id === selectedSlot
}),
children: [/*#__PURE__*/_jsx("span", {
className: styles.buttonLabelSlot,
children: getRange(slot.from, slot.to)
}), slot.status !== 'active' ? /*#__PURE__*/_jsx("div", {
className: styles.buttonStrikethrough
}) : null]
}, `${slot.from}-${slot.to}`))
})]
}, group.name)), /*#__PURE__*/_jsx("div", {
className: styles.buttonScheduleContainer,
children: /*#__PURE__*/_jsx(Button, {
className: styles.buttonSchedule,
type: "secondary",
onClick: handleChange,
disabled: !selectedDate || !selectedSlot,
children: i18n.text('locations.your_current_timeslot.dialog.schedule')
})
})]
})
});
};
FulfillmentSlotSheet.defaultProps = {
isOpen: false,
locationCode: null,
fulfillmentSlots: [],
fulfillmentSlot: null,
allowClose: true
};
export default connect(makeMapStateToProps, mapDispatchToProps)(FulfillmentSlotSheet);