forked from M-Labs/web2019
Egor Savkin
abb472f0ea
TODO: * Add all other warnings for cards * Add crate level warnings * Add reminders to the bottom of the crate * Refactor crate mode selection (???) * Add resource consumers, instead of a bunch of nbrOccupiedSLots Signed-off-by: Egor Savkin <es@m-labs.hk>
616 lines
22 KiB
JavaScript
616 lines
22 KiB
JavaScript
import React, {PureComponent} from 'react';
|
|
import PropTypes from "prop-types";
|
|
import {FilterOptions} from "./options/utils";
|
|
import {v4 as uuidv4} from "uuid";
|
|
import {DragDropContext} from "@hello-pangea/dnd";
|
|
|
|
import {copy, itemsUnfoldedList, nbrOccupiedSlotsInCrate, remove, reorder} from "./utils";
|
|
|
|
import {Layout} from "./Layout.jsx";
|
|
import {Backlog} from "./Backlog.jsx";
|
|
import {OrderPanel} from "./OrderPanel.jsx";
|
|
import {CrateMode} from "./CrateMode.jsx";
|
|
import {Crate} from "./Crate.jsx";
|
|
import {Cart} from "./Cart.jsx";
|
|
import {OrderSummary} from "./OrderSummary.jsx";
|
|
import {OrderForm} from "./OrderForm.jsx";
|
|
import {TriggerWarnings} from "./warnings";
|
|
import {FillResources} from "./count_resources";
|
|
|
|
/**
|
|
* Component that render the entire shop
|
|
*/
|
|
|
|
export class Shop extends PureComponent {
|
|
|
|
static get propTypes() {
|
|
return {
|
|
data: PropTypes.object.isRequired,
|
|
};
|
|
}
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = this.props.data;
|
|
this.handleCrateModeChange = this.handleCrateModeChange.bind(this);
|
|
this.handleOnDragEnd = this.handleOnDragEnd.bind(this);
|
|
this.handleDeleteItem = this.handleDeleteItem.bind(this);
|
|
this.handleDeleteAllItems = this.handleDeleteAllItems.bind(this);
|
|
this.handleMouseEnterItem = this.handleMouseEnterItem.bind(this);
|
|
this.handleMouseLeaveItem = this.handleMouseLeaveItem.bind(this);
|
|
this.handleClickAddItem = this.handleClickAddItem.bind(this);
|
|
this.checkAlerts = this.checkAlerts.bind(this);
|
|
this.handleClickSelectItem = this.handleClickSelectItem.bind(this);
|
|
this.handleClickSubmit = this.handleClickSubmit.bind(this);
|
|
this.handleToggleOverlayRemove = this.handleToggleOverlayRemove.bind(this);
|
|
this.handleShowOverlayRemove = this.handleShowOverlayRemove.bind(this);
|
|
this.handleClickToggleMobileSideMenu = this.handleClickToggleMobileSideMenu.bind(this);
|
|
this.handleClickCloseRFQFeedback = this.handleClickCloseRFQFeedback.bind(this);
|
|
this.handleClickShowOrder = this.handleClickShowOrder.bind(this);
|
|
this.handleClickOpenImport = this.handleClickOpenImport.bind(this);
|
|
this.handleLoadCustomConf = this.handleLoadCustomConf.bind(this);
|
|
this.handleCardsUpdated = this.handleCardsUpdated.bind(this);
|
|
|
|
this.timer = null;
|
|
this.timer_remove = null;
|
|
}
|
|
|
|
componentDidMount() {
|
|
const source = {
|
|
droppableId: 'backlog',
|
|
indexes: [
|
|
itemsUnfoldedList.findIndex(element => element === "eem_pwr_mod"),
|
|
itemsUnfoldedList.findIndex(element => element === "kasli")
|
|
],
|
|
};
|
|
const destination = {
|
|
droppableId: 'cart',
|
|
index: 0,
|
|
};
|
|
|
|
this.handleOnDragEnd({
|
|
source,
|
|
destination
|
|
});
|
|
}
|
|
|
|
componentDidUpdate(prevProps, prevState) {
|
|
/**
|
|
* We check alerts (reminder + warning) only when items inside crate or
|
|
* crate mode change.
|
|
*
|
|
* In the function checkAlerts, we DO NOT want to change items as we will
|
|
* trigger again this function (componentDidUpdate) and thus,
|
|
* making an infinite loop.
|
|
*/
|
|
if (
|
|
(prevState.columns.cart.items !== this.state.columns.cart.items) ||
|
|
(prevState.currentMode !== this.state.currentMode)
|
|
) {
|
|
this.checkAlerts(this.state.columns.cart.items);
|
|
}
|
|
|
|
if (this.state.newCardJustAdded) {
|
|
this.timer = setTimeout(() => {
|
|
this.setState({
|
|
newCardJustAdded: false,
|
|
});
|
|
}, 2000);
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
clearTimeout(this.timer);
|
|
}
|
|
|
|
handleCardsUpdated() {
|
|
this.checkAlerts(this.state.columns.cart.items);
|
|
}
|
|
|
|
handleCrateModeChange(mode) {
|
|
this.setState({
|
|
currentMode: mode,
|
|
});
|
|
}
|
|
|
|
handleDeleteItem(index) {
|
|
let cloned = Array.from(this.state.columns.cart.items);
|
|
let cloned_data = Array.from(this.state.columns.cart.itemsData);
|
|
cloned.splice(index, 1);
|
|
cloned_data.splice(index, 1);
|
|
|
|
this.setState({
|
|
...this.state,
|
|
columns: {
|
|
...this.state.columns,
|
|
cart: {
|
|
...this.state.columns.cart,
|
|
items: cloned,
|
|
itemsData: cloned_data,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
handleDeleteAllItems() {
|
|
this.setState({
|
|
...this.state,
|
|
columns: {
|
|
...this.state.columns,
|
|
cart: {
|
|
...this.state.columns.cart,
|
|
items: [],
|
|
itemsData: []
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
handleMouseEnterItem(id) {
|
|
this.setState({
|
|
...this.state,
|
|
currentItemHovered: id,
|
|
});
|
|
}
|
|
|
|
handleMouseLeaveItem() {
|
|
this.setState({
|
|
...this.state,
|
|
currentItemHovered: null,
|
|
});
|
|
}
|
|
|
|
handleClickAddItem(index, tap) {
|
|
const source = {
|
|
droppableId: 'backlog',
|
|
index: index,
|
|
};
|
|
const destination = {
|
|
droppableId: 'cart',
|
|
index: this.state.columns.cart.items.length,
|
|
};
|
|
|
|
this.handleOnDragEnd({
|
|
source,
|
|
destination
|
|
}, tap);
|
|
}
|
|
|
|
handleClickSelectItem(index) {
|
|
const itemsCloned = Array.from(this.state.columns.cart.items);
|
|
|
|
this.setState({
|
|
...this.state,
|
|
columns: {
|
|
...this.state.columns,
|
|
cart: {
|
|
...this.state.columns.cart,
|
|
items: itemsCloned.map((item, id) => {
|
|
return {...item, selected: id === index ? true : false};
|
|
}),
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
handleToggleOverlayRemove(index, show) {
|
|
const itemsCloned = Array.from(this.state.columns.cart.items);
|
|
|
|
this.setState({
|
|
...this.state,
|
|
columns: {
|
|
...this.state.columns,
|
|
cart: {
|
|
...this.state.columns.cart,
|
|
items: itemsCloned.map((item, id) => {
|
|
return {
|
|
...item,
|
|
showOverlayRemove: id === index ? show : false
|
|
};
|
|
}),
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
handleShowOverlayRemove(index) {
|
|
if (this.timer_remove)
|
|
clearTimeout(this.timer_remove);
|
|
|
|
this.handleToggleOverlayRemove(index, true);
|
|
|
|
this.timer_remove = setTimeout(() => {
|
|
this.handleToggleOverlayRemove(index, false);
|
|
}, 2000);
|
|
}
|
|
|
|
handleClickShowOrder() {
|
|
const crate = {
|
|
items: [],
|
|
type: this.state.currentMode,
|
|
};
|
|
const clonedCart = Array.from(this.state.columns.cart.items);
|
|
const clonedCartData = Array.from(this.state.columns.cart.itemsData);
|
|
for (const i in clonedCart) {
|
|
const item = clonedCart[i];
|
|
const item_data = clonedCartData[i];
|
|
crate.items.push({
|
|
'pn': item.name_number,
|
|
'options': (item_data.options_data && item_data.options) ? FilterOptions(item_data.options, item_data.options_data) : null,
|
|
});
|
|
}
|
|
|
|
this.setState({
|
|
isProcessing: false,
|
|
shouldShowRFQFeedback: true,
|
|
RFQBodyType: 'show',
|
|
RFQBodyOrder: JSON.stringify(crate, null, 2),
|
|
});
|
|
}
|
|
|
|
handleClickOpenImport() {
|
|
this.setState({
|
|
isProcessing: false,
|
|
shouldShowRFQFeedback: true,
|
|
RFQBodyType: 'import',
|
|
});
|
|
}
|
|
|
|
handleLoadCustomConf(customconf) {
|
|
if (!customconf) {return; }
|
|
|
|
const items = this.props.data.items;
|
|
|
|
let new_items = [];
|
|
let new_items_data = [];
|
|
|
|
|
|
this.setState({
|
|
...this.state,
|
|
columns: {
|
|
...this.state.columns,
|
|
cart: {
|
|
...this.state.columns.cart,
|
|
items: [],
|
|
},
|
|
},
|
|
}, function () {
|
|
|
|
customconf.items.map(function (item) {
|
|
Object.keys(items).map(key => {
|
|
if (item.pn && item.pn === items[key].name_number) {
|
|
new_items.push(Object.assign({
|
|
...items[key],
|
|
}, {
|
|
id: uuidv4(),
|
|
options_data: item.options ? item.options : null,
|
|
}));
|
|
new_items_data.push({options_data: item.options ? item.options : null});
|
|
}
|
|
});
|
|
|
|
return item;
|
|
});
|
|
|
|
this.setState({
|
|
...this.state,
|
|
columns: {
|
|
...this.state.columns,
|
|
cart: {
|
|
...this.state.columns.cart,
|
|
items: new_items,
|
|
itemsData: new_items_data,
|
|
},
|
|
},
|
|
currentMode: customconf.type,
|
|
});
|
|
});
|
|
}
|
|
|
|
handleClickSubmit(note, email) {
|
|
const crate = {
|
|
items: [],
|
|
type: this.state.currentMode,
|
|
};
|
|
const clonedCart = Array.from(this.state.columns.cart.items);
|
|
const clonedCartData = Array.from(this.state.columns.cart.itemsData);
|
|
for (const i in clonedCart) {
|
|
const item = clonedCart[i];
|
|
const item_data = clonedCartData[i];
|
|
crate.items.push({
|
|
'pn': item.name_number,
|
|
'options': (item_data.options_data && item_data.options) ? FilterOptions(item_data.options, item_data.options_data) : null,
|
|
});
|
|
}
|
|
|
|
const {data} = this.props;
|
|
|
|
this.setState({isProcessing: true});
|
|
|
|
fetch(data.API_RFQ, {
|
|
method: "POST",
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({
|
|
email,
|
|
note,
|
|
configuration: JSON.stringify(crate)
|
|
})
|
|
}).then(response => {
|
|
if (response.status !== 200) {
|
|
throw Error("Response status is not OK: " + response.status + ".\n" + response);
|
|
}
|
|
this.setState({
|
|
isProcessing: false,
|
|
shouldShowRFQFeedback: true,
|
|
RFQBodyType: 'email',
|
|
isProcessingComplete: true,
|
|
});
|
|
}).catch(err => {
|
|
console.error("Request failed, reason:", err)
|
|
this.setState({isProcessing: false}, () => {
|
|
alert("We cannot receive your request. Try using the export by coping the configuration and send it to us at sales@m-labs.hk");
|
|
});
|
|
})
|
|
}
|
|
|
|
handleOnDragEnd(result, newAdded) {
|
|
const {
|
|
source,
|
|
destination,
|
|
} = result;
|
|
let dragged_items = [];
|
|
if (source.indexes) {
|
|
source.indexes.forEach((card_index, _) => {
|
|
dragged_items.push(itemsUnfoldedList[card_index]);
|
|
})
|
|
} else if (source.index >= 0) {
|
|
dragged_items.push(itemsUnfoldedList[source.index]);
|
|
}
|
|
|
|
|
|
if (!destination) {
|
|
if (source.droppableId === 'cart') {
|
|
this.setState({
|
|
...this.state,
|
|
newCardJustAdded: false,
|
|
columns: {
|
|
...this.state.columns,
|
|
[source.droppableId]: {
|
|
...this.state.columns[source.droppableId],
|
|
items: remove(
|
|
this.state.columns[source.droppableId].items,
|
|
source.index,
|
|
),
|
|
itemsData: remove(
|
|
this.state.columns[source.droppableId].itemsData,
|
|
source.index,
|
|
)
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
switch(source.droppableId) {
|
|
|
|
case 'backlog':
|
|
if (source.droppableId !== destination.droppableId) {
|
|
this.setState({
|
|
...this.state,
|
|
newCardJustAdded: newAdded ? true : false,
|
|
columns: {
|
|
...this.state.columns,
|
|
[destination.droppableId]: {
|
|
...this.state.columns[destination.droppableId],
|
|
items: copy(
|
|
this.state.items,
|
|
this.state.columns[source.droppableId],
|
|
this.state.columns[destination.droppableId],
|
|
dragged_items,
|
|
destination,
|
|
),
|
|
},
|
|
},
|
|
});
|
|
}
|
|
break;
|
|
|
|
case destination.droppableId:
|
|
this.setState({
|
|
...this.state,
|
|
newCardJustAdded: false,
|
|
columns: {
|
|
...this.state.columns,
|
|
[destination.droppableId]: {
|
|
...this.state.columns[destination.droppableId],
|
|
items: reorder(
|
|
this.state.columns[destination.droppableId].items,
|
|
source.index,
|
|
destination.index,
|
|
),
|
|
itemsData: reorder(
|
|
this.state.columns[destination.droppableId].itemsData,
|
|
source.index,
|
|
destination.index,
|
|
),
|
|
},
|
|
},
|
|
});
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
handleClickToggleMobileSideMenu() {
|
|
this.setState({
|
|
...this.state,
|
|
mobileSideMenuShouldOpen: !this.state.mobileSideMenuShouldOpen,
|
|
});
|
|
}
|
|
|
|
handleClickCloseRFQFeedback() {
|
|
this.setState({
|
|
shouldShowRFQFeedback: false,
|
|
});
|
|
}
|
|
|
|
checkAlerts(newItems) {
|
|
console.log('--- START CHECKING CRATE WARNING ---');
|
|
|
|
const {
|
|
currentMode,
|
|
crateModeSlots,
|
|
crateRules,
|
|
} = this.state;
|
|
|
|
let itemsCloned = Array.from(newItems);
|
|
const rules = {};
|
|
|
|
itemsCloned.forEach((elem, idx) => {
|
|
if (!(idx in itemsCloned)) itemsCloned[idx] = elem;
|
|
if (idx in this.state.columns.cart.itemsData && this.state.columns.cart.itemsData[idx].options_data) {
|
|
itemsCloned[idx].options_data = this.state.columns.cart.itemsData[idx].options_data;
|
|
}
|
|
});
|
|
|
|
itemsCloned = FillResources(itemsCloned);
|
|
itemsCloned = TriggerWarnings(itemsCloned);
|
|
|
|
// check number of slot in crate
|
|
const nbrOccupied = nbrOccupiedSlotsInCrate(newItems);
|
|
if (nbrOccupied > crateModeSlots[currentMode]) {
|
|
rules[crateRules.maxSlot.type] = {...crateRules.maxSlot};
|
|
} else if (crateModeSlots[currentMode] === 21 && nbrOccupied <= 10) {
|
|
rules[crateRules.compactSlot.type] = {...crateRules.compactSlot};
|
|
}
|
|
|
|
// update state with rules
|
|
this.setState({
|
|
...this.state,
|
|
columns: {
|
|
...this.state.columns,
|
|
cart: {
|
|
...this.state.columns.cart,
|
|
itemsData: itemsCloned,
|
|
}
|
|
},
|
|
rules: {
|
|
...rules,
|
|
},
|
|
});
|
|
}
|
|
|
|
render() {
|
|
|
|
const {
|
|
currency,
|
|
currentItemHovered,
|
|
currentMode,
|
|
crateModeSlots,
|
|
crateModeItems,
|
|
items,
|
|
columns,
|
|
rules,
|
|
mobileSideMenuShouldOpen,
|
|
newCardJustAdded,
|
|
isProcessing,
|
|
shouldShowRFQFeedback,
|
|
RFQBodyType,
|
|
RFQBodyOrder,
|
|
isProcessingComplete,
|
|
} = this.state;
|
|
|
|
const isMobile = window.deviceIsMobile();
|
|
const isTouch = window.isTouchEnabled();
|
|
|
|
return (
|
|
<DragDropContext onDragEnd={this.handleOnDragEnd}>
|
|
|
|
<Layout
|
|
showRFQFeedback={shouldShowRFQFeedback}
|
|
RFQBodyType={RFQBodyType}
|
|
RFQBodyOrder={RFQBodyOrder}
|
|
className="shop"
|
|
mobileSideMenuShouldOpen={mobileSideMenuShouldOpen}
|
|
isMobile={isMobile}
|
|
newCardJustAdded={newCardJustAdded}
|
|
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
|
|
onClickCloseRFQFeedback={this.handleClickCloseRFQFeedback}
|
|
onClickLoadCustomConf={this.handleLoadCustomConf}
|
|
items={items}
|
|
aside={
|
|
<Backlog
|
|
currency={currency}
|
|
items={items}
|
|
data={columns['backlog']}
|
|
onClickAddItem={this.handleClickAddItem}
|
|
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
|
|
isMobile={isMobile}>
|
|
</Backlog>
|
|
}
|
|
main={(
|
|
<OrderPanel
|
|
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
|
|
onClickOpenImport={this.handleClickOpenImport}
|
|
isMobile={isMobile}
|
|
title="Order hardware"
|
|
description={(<p className="description">Drag and drop the cards you want into the crate below to see how the combination would look like. Setup card's configuration by tapping at the top of the card, most of the options can be modified after shipment. If you have any issues with this ordering system, or if you need other configurations, email us directly anytime at <a href="mailto:sales@m-labs.hk">sales@m-labs.hk</a>. The price is estimated and must be confirmed by a quote.</p>)}
|
|
crateMode={
|
|
<CrateMode
|
|
items={crateModeItems}
|
|
mode={currentMode}
|
|
onClickMode={this.handleCrateModeChange}>
|
|
</CrateMode>}
|
|
crate={
|
|
<Crate
|
|
cart={
|
|
<Cart
|
|
nbrSlots={crateModeSlots[currentMode]}
|
|
data={columns['cart']}
|
|
isMobile={isMobile}
|
|
isTouch={isTouch}
|
|
itemHovered={currentItemHovered}
|
|
onToggleOverlayRemove={this.handleToggleOverlayRemove}
|
|
onClickRemoveItem={this.handleDeleteItem}
|
|
onClickItem={this.handleShowOverlayRemove}
|
|
onCardUpdate={this.handleCardsUpdated}>
|
|
</Cart>
|
|
}
|
|
rules={Object.values(rules).filter(rule => rule)}>
|
|
</Crate>
|
|
}
|
|
summaryPrice={
|
|
<OrderSummary
|
|
currency={currency}
|
|
currentMode={currentMode}
|
|
modes={crateModeItems}
|
|
summary={columns['cart'].items}
|
|
itemsData={columns.cart.itemsData}
|
|
onMouseEnterItem={this.handleMouseEnterItem}
|
|
onMouseLeaveItem={this.handleMouseLeaveItem}
|
|
onDeleteItem={this.handleDeleteItem}
|
|
onDeleteAllItems={this.handleDeleteAllItems}
|
|
onClickSelectItem={this.handleClickSelectItem}>
|
|
</OrderSummary>
|
|
}
|
|
form={
|
|
<OrderForm
|
|
isProcessingComplete={isProcessingComplete}
|
|
processingComplete={this.handleProcessingComplete}
|
|
isProcessing={isProcessing}
|
|
onClickSubmit={this.handleClickSubmit}
|
|
onClickShow={this.handleClickShowOrder}>
|
|
</OrderForm>
|
|
}>
|
|
</OrderPanel>
|
|
)}>
|
|
</Layout>
|
|
|
|
</DragDropContext>
|
|
);
|
|
}
|
|
} |