UNPKG

@shopgate/engage

Version:
317 lines (305 loc) • 9.59 kB
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);