forked from M-Labs/web2019
2463 lines
67 KiB
JavaScript
2463 lines
67 KiB
JavaScript
'use strict';
|
|
|
|
import React from "react";
|
|
import axios from "axios";
|
|
import { createRoot } from "react-dom/client";
|
|
import PropTypes from "prop-types";
|
|
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import { OptionsDialogPopup, OptionsSummaryPopup } from "./shop_components.jsx";import { OverlayTrigger } from "react-bootstrap";
|
|
|
|
const data = window.shop_data;
|
|
const itemsUnfoldedList = Array.from(data.columns.backlog.categories.map(groupId => groupId.itemIds).flat());
|
|
|
|
const productStyle = (style, snapshot, removeAnim, hovered, selected, cart=false) => {
|
|
const custom = {
|
|
opacity: snapshot.isDragging ? .7 : 1,
|
|
backgroundColor: (hovered || selected) ? '#eae7f7' : 'initial',
|
|
};
|
|
|
|
if (!cart && snapshot.draggingOver == null && // hack for backlog
|
|
((!snapshot.isDragging) // prevent next elements from animation
|
|
|| (snapshot.isDragging && snapshot.isDropAnimating))) { // prevent dragged element from weird animation
|
|
style.transform = "none";
|
|
}
|
|
|
|
if (!snapshot.isDropAnimating) {
|
|
return { ...style, ...custom};
|
|
}
|
|
|
|
if (removeAnim) {
|
|
// cannot be 0, but make it super tiny
|
|
custom.transitionDuration = '0.001s';
|
|
}
|
|
|
|
return {
|
|
...style,
|
|
...custom,
|
|
};
|
|
}
|
|
|
|
const cartStyle = (style, snapshot) => {
|
|
const isDraggingOver = snapshot.isDraggingOver;
|
|
return {
|
|
...style,
|
|
...{
|
|
backgroundColor: isDraggingOver ? '#f2f2f2' : '#f9f9f9',
|
|
border: isDraggingOver ? '1px dashed #ccc' : '0',
|
|
},
|
|
};
|
|
}
|
|
|
|
const nbrConnectorsStyle = (data) => {
|
|
if (!data || !data.nbrCurrentSlot) {
|
|
return {};
|
|
}
|
|
|
|
let p = data.nbrCurrentSlot * 100 / data.nbrSlotMax;
|
|
if (p > 100) {
|
|
p = 100;
|
|
}
|
|
return {
|
|
width: `${p}%`,
|
|
}
|
|
};
|
|
|
|
const nbrClocksStyle = (data) => {
|
|
if (!data || !data.nbrCurrentClock) {
|
|
return {};
|
|
}
|
|
|
|
let p = data.nbrCurrentClock * 100 / data.nbrClockMax;
|
|
if (p > 100) {
|
|
p = 100;
|
|
}
|
|
return {
|
|
width: `${p}%`,
|
|
}
|
|
};
|
|
|
|
const copy = (
|
|
model,
|
|
source,
|
|
destination,
|
|
draggableSource,
|
|
droppableDestination
|
|
) => {
|
|
const destClone = Array.from(destination.items);
|
|
|
|
destClone.splice(droppableDestination.index, 0, ...draggableSource.map((dragged_item, _) => {
|
|
return {
|
|
...model[dragged_item],
|
|
id: uuidv4(),
|
|
}
|
|
}));
|
|
|
|
return destClone;
|
|
};
|
|
|
|
const reorder = (list, startIndex, endIndex) => {
|
|
const result = Array.from(list);
|
|
const [removed] = result.splice(startIndex, 1);
|
|
result.splice(endIndex, 0, removed);
|
|
|
|
return result;
|
|
};
|
|
|
|
const remove = (list, startIndex) => {
|
|
const result = Array.from(list);
|
|
result.splice(startIndex, 1);
|
|
return result;
|
|
};
|
|
|
|
const nbrOccupiedSlotsInCrate = (items) => {
|
|
return items.reduce((prev, next) => {
|
|
return prev + (next.hp === 8 ? 2 : 1);
|
|
}, 0);
|
|
};
|
|
|
|
function formatMoney(amount, decimalCount = 2, decimal = ".", thousands = ",") {
|
|
// https://stackoverflow.com/questions/149055/how-can-i-format-numbers-as-currency-string-in-javascript
|
|
// changes: return amount if error in order to avoid empty value
|
|
try {
|
|
decimalCount = Math.abs(decimalCount);
|
|
decimalCount = isNaN(decimalCount) ? 2 : decimalCount;
|
|
|
|
const negativeSign = amount < 0 ? "-" : "";
|
|
|
|
let i = parseInt(amount = Math.abs(Number(amount) || 0).toFixed(decimalCount)).toString();
|
|
let j = (i.length > 3) ? i.length % 3 : 0;
|
|
|
|
return negativeSign + (j ? i.substr(0, j) + thousands : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + thousands) + (decimalCount ? decimal + Math.abs(amount - i).toFixed(decimalCount).slice(2) : "");
|
|
} catch (e) {
|
|
return amount;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Component that provides a base layout (aside/main) for the page.
|
|
*/
|
|
class Layout extends React.PureComponent {
|
|
|
|
static get propTypes() {
|
|
return {
|
|
aside: PropTypes.any,
|
|
main: PropTypes.any,
|
|
mobileSideMenuShouldOpen: PropTypes.bool,
|
|
isMobile: PropTypes.bool,
|
|
newCardJustAdded: PropTypes.bool,
|
|
onClickToggleMobileSideMenu: PropTypes.func,
|
|
onClickCloseRFQFeedback: PropTypes.func,
|
|
RFQBodyType: PropTypes.string,
|
|
RFQBodyOrder: PropTypes.string,
|
|
onClickLoadCustomConf: PropTypes.func,
|
|
items: PropTypes.object,
|
|
};
|
|
}
|
|
|
|
static get defaultProps() {
|
|
return {
|
|
mobileSideMenuShouldOpen: false,
|
|
};
|
|
}
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
customconf: '',
|
|
error: null,
|
|
};
|
|
|
|
this.handleCustomConfig = this.handleCustomConfig.bind(this);
|
|
this.handleClickLoad = this.handleClickLoad.bind(this);
|
|
this.checkValidation = this.checkValidation.bind(this);
|
|
|
|
// retrieve list of available pn
|
|
const items_keys = Object.keys(props.items);
|
|
this.list_pn = items_keys.map(function (key) {
|
|
return props.items[key].name_number;
|
|
});
|
|
}
|
|
|
|
handleCustomConfig(e) {
|
|
const value = e.target.value;
|
|
|
|
this.checkValidation(value);
|
|
}
|
|
|
|
checkValidation(conf) {
|
|
let conf_obj;
|
|
|
|
try {
|
|
conf_obj = JSON.parse(conf);
|
|
} catch (e) {
|
|
return this.setState({
|
|
...this.state,
|
|
customconf: conf,
|
|
customconf_ready: null,
|
|
error: 'invalid format',
|
|
});
|
|
}
|
|
|
|
if (!conf_obj) {
|
|
return this.setState({
|
|
...this.state,
|
|
customconf: conf,
|
|
customconf_ready: null,
|
|
error: 'invalid format',
|
|
});
|
|
}
|
|
|
|
if ((!conf_obj.items || !conf_obj.type) &&
|
|
(Object.prototype.toString.call(conf_obj.items) !== '[object Array]' ||
|
|
Object.prototype.toString.call(conf_obj.type) !== '[object String]')) {
|
|
return this.setState({
|
|
...this.state,
|
|
customconf: conf,
|
|
customconf_ready: null,
|
|
error: 'invalid format',
|
|
});
|
|
}
|
|
|
|
if (conf_obj.type !== "desktop" && conf_obj.type !== "rack") {
|
|
return this.setState({
|
|
...this.state,
|
|
customconf: conf,
|
|
customconf_ready: null,
|
|
error: 'invalid format',
|
|
});
|
|
}
|
|
|
|
conf_obj.items.map(function (item) {
|
|
try {
|
|
return JSON.parse(item);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
});
|
|
|
|
conf_obj.items = conf_obj.items.filter(function (item) {
|
|
return item;
|
|
});
|
|
|
|
if (conf_obj.items.filter(function (item) {
|
|
return Object.prototype.toString.call(item) !== '[object Object]' || !item.pn || Object.prototype.toString.call(item.pn) !== '[object String]';
|
|
}).length > 0) {
|
|
return this.setState({
|
|
...this.state,
|
|
customconf: conf,
|
|
customconf_ready: null,
|
|
error: 'invalid format',
|
|
});
|
|
}
|
|
|
|
conf_obj.items = conf_obj.items.map(function (item) {
|
|
return {
|
|
pn: item.pn,
|
|
options: item.options ? item.options : null,
|
|
};
|
|
});
|
|
|
|
const self = this;
|
|
const unknow_pn = conf_obj.items.filter(function (item_pn) {
|
|
return self.list_pn.includes(item_pn.pn) === false;
|
|
}).map(function (item_pn) {
|
|
return item_pn.pn;
|
|
});
|
|
|
|
if (unknow_pn.length > 0) {
|
|
return this.setState({
|
|
...this.state,
|
|
customconf: conf,
|
|
customconf_ready: null,
|
|
error: `${unknow_pn.join(', ')} unknown${unknow_pn.length > 1 ? 's':''} pn number`,
|
|
});
|
|
}
|
|
|
|
this.setState({
|
|
...this.state,
|
|
customconf: conf,
|
|
error: null,
|
|
customconf_ready: conf_obj,
|
|
});
|
|
}
|
|
|
|
handleClickLoad() {
|
|
this.checkValidation(this.state.customconf);
|
|
|
|
if (this.props.onClickLoadCustomConf) {
|
|
this.props.onClickLoadCustomConf(this.state.customconf_ready);
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
aside,
|
|
main,
|
|
mobileSideMenuShouldOpen,
|
|
isMobile,
|
|
newCardJustAdded,
|
|
onClickToggleMobileSideMenu,
|
|
onClickCloseRFQFeedback,
|
|
showRFQFeedback,
|
|
RFQBodyType,
|
|
RFQBodyOrder,
|
|
} = this.props;
|
|
|
|
return (
|
|
<div className="layout">
|
|
|
|
<aside className={'aside ' + (mobileSideMenuShouldOpen ? 'menu-opened' : '')}>{aside}</aside>
|
|
|
|
{mobileSideMenuShouldOpen ? (
|
|
<section className="main" onClick={onClickToggleMobileSideMenu}>{main}</section>
|
|
) : (
|
|
<section className="main">{main}</section>
|
|
)}
|
|
|
|
{isMobile && newCardJustAdded ? (
|
|
<div className="feedback-add-success">
|
|
✓ added
|
|
</div>
|
|
) : null}
|
|
|
|
|
|
<div className={`modal fade ${ showRFQFeedback ? 'show': ''}`} style={{'display': showRFQFeedback ? 'block':'none'}} id="exampleModal" tabIndex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
|
<div className="modal-dialog" role="document">
|
|
<div className="modal-content">
|
|
<div className="modal-body rfqFeedback">
|
|
|
|
<div className="d-flex w-100">
|
|
|
|
{RFQBodyType === 'email' ? (
|
|
<div className="d-flex">
|
|
|
|
<div>
|
|
<img width="30px" src="/images/shop/icon-done.svg" alt="close" />
|
|
</div>
|
|
|
|
<div style={{'padding': '0 .5em'}}>
|
|
We've received your request and will be in contact soon.
|
|
</div>
|
|
|
|
</div>
|
|
) : null }
|
|
|
|
{RFQBodyType === 'show' ? (
|
|
<p>
|
|
{RFQBodyOrder}
|
|
</p>
|
|
) : null}
|
|
|
|
{RFQBodyType === 'import' ? (
|
|
<div className="w-100">
|
|
|
|
<form className="w-100">
|
|
<div className="mb-3">
|
|
<p className="small">
|
|
Input the JSON description below. Should be something like:
|
|
<br />
|
|
{JSON.stringify({"items":[{"pn":"1124"},{"pn":"2118"},{"pn":"2118"},{"pn":"2128"}],"type":"desktop"})}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="mb-3 w-100">
|
|
<textarea
|
|
onChange={this.handleCustomConfig}
|
|
value={this.state.customconf}
|
|
className="form-control w-100"
|
|
rows="5"
|
|
placeholder="Input JSON description here." />
|
|
</div>
|
|
{this.state.error ? (
|
|
<div className="mb-3">
|
|
<p className="text-danger">{this.state.error}</p>
|
|
</div>
|
|
) : null}
|
|
</form>
|
|
|
|
<div className="d-flex flex-column flex-sm-row justify-content-end">
|
|
<a type="button" onClick={onClickCloseRFQFeedback} className="btn btn-sm btn-outline-primary m-0 mb-2 mb-sm-0 me-sm-2">Close</a>
|
|
<a type="button" onClick={this.handleClickLoad} className={`btn btn-sm btn-primary m-0 ms-sm-2 ${this.state.error ? 'disabled':''}`}>Load configuration</a>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
<div>
|
|
<button onClick={onClickCloseRFQFeedback}>
|
|
<img src="/images/shop/icon-close.svg" alt="close" />
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div onClick={onClickCloseRFQFeedback} className={`modal-backdrop fade ${ showRFQFeedback ? 'show': ''}`} style={{'display': showRFQFeedback ? 'initial':'none'}}></div>
|
|
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Component that renders a product.
|
|
* Used in the aside (e.g backlog of product)
|
|
*/
|
|
class ProductItem extends React.PureComponent {
|
|
|
|
static get propTypes() {
|
|
return {
|
|
id: PropTypes.string.isRequired,
|
|
index: PropTypes.number.isRequired,
|
|
name: PropTypes.string.isRequired,
|
|
name_codename: PropTypes.string,
|
|
price: PropTypes.number.isRequired,
|
|
currency: PropTypes.string.isRequired,
|
|
image: PropTypes.string.isRequired,
|
|
specs: PropTypes.array,
|
|
datasheet_file: PropTypes.string,
|
|
datasheet_name: PropTypes.string,
|
|
onClickAddItem: PropTypes.func,
|
|
};
|
|
}
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
this.handleOnClickAddItem = this.handleOnClickAddItem.bind(this);
|
|
}
|
|
|
|
handleOnClickAddItem(id, tap, e) {
|
|
if (this.props.onClickAddItem) {
|
|
this.props.onClickAddItem(id, tap);
|
|
}
|
|
e.preventDefault();
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
id,
|
|
index,
|
|
name,
|
|
name_codename,
|
|
price,
|
|
currency,
|
|
image,
|
|
specs,
|
|
datasheet_file,
|
|
datasheet_name,
|
|
} = this.props;
|
|
|
|
const render_specs = (specs && specs.length > 0 && (
|
|
<ul>
|
|
{specs.map((spec, index) =>
|
|
<li key={index}>{spec}</li>
|
|
)}
|
|
</ul>
|
|
));
|
|
|
|
const render_datasheet_link = (datasheet_file && datasheet_name && (
|
|
<div className="ds">
|
|
<span className='doc-icon'></span>
|
|
<a href={datasheet_file} target="_blank" rel="noopener noreferrer">
|
|
{datasheet_name}
|
|
</a>
|
|
</div>
|
|
));
|
|
|
|
return (
|
|
<section className="productItem">
|
|
|
|
<div className="content">
|
|
<h3 style={{ 'marginBottom': name_codename ? '5px' : '20px'}}>{name}</h3>
|
|
{name_codename ? (
|
|
<p>{name_codename}</p>
|
|
) : null }
|
|
|
|
<div className="price">{`${currency} ${formatMoney(price)}`}</div>
|
|
|
|
{render_specs}
|
|
|
|
{render_datasheet_link}
|
|
</div>
|
|
|
|
<div className="content">
|
|
|
|
<button onClick={this.handleOnClickAddItem.bind(this, index, true)}>
|
|
<img src="/images/shop/icon-add.svg" alt="add" />
|
|
</button>
|
|
|
|
<Draggable draggableId={id} index={index}>
|
|
{(provided, snapshot) => (
|
|
<React.Fragment>
|
|
<img
|
|
ref={provided.innerRef}
|
|
{...provided.draggableProps}
|
|
{...provided.dragHandleProps}
|
|
style={productStyle(
|
|
provided.draggableProps.style,
|
|
snapshot,
|
|
true, // hack: remove weird animation after a drop
|
|
)}
|
|
src={image} />
|
|
|
|
{/* Allows to simulate a clone */}
|
|
{snapshot.isDragging && (
|
|
<img className="simclone" src={image} />
|
|
)}
|
|
</React.Fragment>
|
|
)}
|
|
</Draggable>
|
|
|
|
</div>
|
|
|
|
</section>
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Component that renders a product.
|
|
* Used in the crate
|
|
*/
|
|
class ProductCartItem extends React.PureComponent {
|
|
|
|
static get propTypes() {
|
|
return {
|
|
isMobile: PropTypes.bool,
|
|
isTouch: PropTypes.bool,
|
|
hovered: PropTypes.bool,
|
|
index: PropTypes.number.isRequired,
|
|
model: PropTypes.object.isRequired,
|
|
data: PropTypes.object,
|
|
onToggleOverlayRemove: PropTypes.func,
|
|
onClickRemoveItem: PropTypes.func,
|
|
onClickItem: PropTypes.func,
|
|
};
|
|
}
|
|
|
|
static get defaultProps() {
|
|
return {
|
|
hovered: false,
|
|
};
|
|
}
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
this.handleOnMouseEnterRemoveItem = this.handleOnMouseEnterRemoveItem.bind(this);
|
|
this.handleOnMouseLeaveRemoveItem = this.handleOnMouseLeaveRemoveItem.bind(this);
|
|
this.handleOnClickRemoveItem = this.handleOnClickRemoveItem.bind(this);
|
|
this.handleOnClickItem = this.handleOnClickItem.bind(this);
|
|
}
|
|
|
|
handleOnMouseEnterRemoveItem(index, e) {
|
|
if (this.props.onToggleOverlayRemove && !this.props.isMobile) {
|
|
this.props.onToggleOverlayRemove(index, true);
|
|
}
|
|
e.preventDefault();
|
|
}
|
|
|
|
handleOnMouseLeaveRemoveItem(index, e) {
|
|
if (this.props.onToggleOverlayRemove && !this.props.isMobile) {
|
|
this.props.onToggleOverlayRemove(index, false);
|
|
}
|
|
e.preventDefault();
|
|
}
|
|
|
|
handleOnClickItem(index, e) {
|
|
if (this.props.onClickItem && this.props.isTouch) {
|
|
this.props.onClickItem(index);
|
|
}
|
|
e.preventDefault();
|
|
}
|
|
|
|
handleOnClickRemoveItem(index, e) {
|
|
if (this.props.onClickRemoveItem) {
|
|
this.props.onClickRemoveItem(index);
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
hovered,
|
|
model,
|
|
data,
|
|
index,
|
|
} = this.props;
|
|
|
|
let warning, options, options_data;
|
|
if (data && data.warnings) {
|
|
const warningsKeys = Object.keys(data.warnings);
|
|
if (warningsKeys && warningsKeys.length > 0) {
|
|
// we display only the first warning
|
|
warning = data.warnings[warningsKeys[0]];
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (data && data.options) {
|
|
options = data.options;
|
|
if (!data.options_data) data.options_data = {};
|
|
options_data = data.options_data;
|
|
}
|
|
|
|
let render_progress;
|
|
if (data) {
|
|
switch(model.type) {
|
|
case 'kasli':
|
|
render_progress = (
|
|
<div className="k-popup-connectors">
|
|
<p>{`${data.nbrCurrentSlot}/${model.nbrSlotMax} EEM connectors used`}</p>
|
|
<p>{`${data.nbrCurrentClock}/${model.nbrClockMax} Clock connectors used`}</p>
|
|
</div>
|
|
);
|
|
break;
|
|
|
|
case 'vhdcicarrier':
|
|
render_progress = (
|
|
<div className="k-popup-connectors">
|
|
<p>{`${data.nbrCurrentSlot}/${model.nbrSlotMax} EEM connectors used`}</p>
|
|
</div>
|
|
);
|
|
break;
|
|
|
|
case 'zotino':
|
|
case 'hd68':
|
|
render_progress = (
|
|
<div className="k-popup-connectors">
|
|
<p>{`${data.nbrCurrentSlot}/${model.nbrSlotMax} connectors used`}</p>
|
|
</div>
|
|
);
|
|
break;
|
|
|
|
case 'clocker':
|
|
render_progress = (
|
|
<div className="k-popup-connectors">
|
|
<p>{`${data.nbrCurrentClock}/${model.nbrClockMax} Clock connectors used`}</p>
|
|
</div>
|
|
);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Draggable draggableId={model.id} index={index}>
|
|
|
|
{(provided, snapshot) => (
|
|
<div
|
|
ref={provided.innerRef}
|
|
{...provided.draggableProps}
|
|
{...provided.dragHandleProps}
|
|
style={{ ...productStyle(
|
|
provided.draggableProps.style,
|
|
snapshot,
|
|
true,
|
|
hovered ? true : false,
|
|
model.selected ? true : false,
|
|
true
|
|
)}}
|
|
onMouseEnter={this.handleOnMouseEnterRemoveItem.bind(this, index)}
|
|
onMouseLeave={this.handleOnMouseLeaveRemoveItem.bind(this, index)}
|
|
>
|
|
|
|
{/* warning container */}
|
|
<OverlayTrigger
|
|
placement="bottom"
|
|
trigger={warning ? ['click', 'hover', 'focus'] : []}
|
|
overlay={
|
|
warning ? (<div className="k-popup-warning">
|
|
<p className="rule warning">
|
|
<i>{warning.message}</i>
|
|
</p>
|
|
</div>) : null
|
|
}
|
|
rootClose
|
|
>
|
|
<div className="progress-container warning">
|
|
{warning && (<img className="alert-warning" src={warning ? `/images${warning.icon}` : null}/>)}
|
|
</div>
|
|
</OverlayTrigger>
|
|
|
|
<div
|
|
className="progress-container warning"
|
|
onMouseEnter={this.handleOnMouseEnterWarningItem.bind(this, index, warning)}
|
|
onMouseLeave={this.handleOnMouseLeaveWarningItem.bind(this, index, warning)}>
|
|
|
|
{warning ? (
|
|
<img className="alert-warning" src={`/images${warning.icon}`} />
|
|
) : (options ? (<OptionsDialogPopup
|
|
options={options}
|
|
data={options_data}
|
|
key={"popover" + index}
|
|
id={"popover" + index}
|
|
big={data.size == "big"}
|
|
target={{
|
|
construct: ((outvar, value) => {
|
|
//console.log("construct", outvar, value, options_data);
|
|
options_data[outvar] = value;
|
|
this.setState(options_data);
|
|
}),
|
|
update: ((outvar, value) => {
|
|
//console.log("update", outvar, value, options_data);
|
|
if (outvar in options_data) options_data[outvar] = value;
|
|
this.setState(options_data);
|
|
}),
|
|
unmount: ((outvar) => {
|
|
//console.log("delete", outvar);
|
|
delete options_data[outvar];
|
|
})
|
|
}}
|
|
/>) : null) }
|
|
|
|
|
|
|
|
{warning && model.showWarning && (
|
|
<div className={`k-popup-warning ${shouldTooltipWarningClassInverted ? 'inverted': ''}`}>
|
|
<p className="rule warning">
|
|
<i>{warning.message}</i>
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<h6>{model.name_number}</h6>
|
|
|
|
<div
|
|
onMouseEnter={this.handleOnMouseEnterRemoveItem.bind(this, index)}
|
|
onClick={this.handleOnClickItem.bind(this, index)}
|
|
>
|
|
|
|
<img
|
|
className='item-cart'
|
|
src={`/images${model.image}`} />
|
|
</div>
|
|
|
|
{/* remove container */}
|
|
<div
|
|
style={{'display': model.showOverlayRemove ? 'flex':'none'}}
|
|
className="overlayRemove"
|
|
onClick={this.handleOnClickRemoveItem.bind(this, index)}>
|
|
|
|
<img src="/images/shop/icon-remove.svg" alt="rm"/>
|
|
|
|
<p>Remove</p>
|
|
</div>
|
|
|
|
{/* progression container */}
|
|
{render_progress && (
|
|
<OverlayTrigger
|
|
placement="top"
|
|
trigger={['click', 'hover', 'focus']}
|
|
overlay={render_progress}
|
|
rootClose
|
|
>
|
|
<div className="progress-container">
|
|
|
|
{model.nbrSlotMax > 0 && (
|
|
<div className="nbr-connectors">
|
|
<div style={{ ...nbrConnectorsStyle(data)}}></div>
|
|
</div>
|
|
)}
|
|
|
|
{model.nbrClockMax > 0 && (
|
|
<div className="nbr-clocks">
|
|
<div style={{ ...nbrClocksStyle(data)}}></div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</OverlayTrigger>
|
|
)}
|
|
|
|
|
|
</div>
|
|
)}
|
|
|
|
</Draggable>
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Component that displays a placeholder inside crate.
|
|
* Allows to display how it remains space for the current crate.
|
|
*/
|
|
class FakePlaceholder extends React.PureComponent {
|
|
|
|
static get propTypes() {
|
|
return {
|
|
isDraggingOver: PropTypes.bool,
|
|
nbrSlots: PropTypes.number.isRequired,
|
|
items: PropTypes.array.isRequired,
|
|
};
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
isDraggingOver,
|
|
nbrSlots,
|
|
items,
|
|
} = this.props;
|
|
|
|
const fakePlaceholder = [];
|
|
const nbrOccupied = nbrOccupiedSlotsInCrate(items);
|
|
|
|
for (var i = (nbrSlots - nbrOccupied); i > 0; i--) {
|
|
fakePlaceholder.push(
|
|
<div key={i} style={{
|
|
display: isDraggingOver ? 'none' : 'block',
|
|
border: '1px dashed #ccc',
|
|
width: '45px',
|
|
marginBottom: '5px',
|
|
}}></div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<React.Fragment>
|
|
{fakePlaceholder}
|
|
</React.Fragment>
|
|
);
|
|
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Component that displays a list of <ProductCartItem>
|
|
*/
|
|
class Cart extends React.PureComponent {
|
|
|
|
static get propTypes() {
|
|
return {
|
|
isMobile: PropTypes.bool,
|
|
isTouch: PropTypes.bool,
|
|
nbrSlots: PropTypes.number,
|
|
itemHovered: PropTypes.string,
|
|
data: PropTypes.object.isRequired,
|
|
onToggleOverlayRemove: PropTypes.func,
|
|
onClickRemoveItem: PropTypes.func,
|
|
onClickItem: PropTypes.func,
|
|
};
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
isMobile,
|
|
isTouch,
|
|
nbrSlots,
|
|
itemHovered,
|
|
data,
|
|
onToggleOverlayRemove,
|
|
onClickRemoveItem,
|
|
onClickItem,
|
|
} = this.props;
|
|
|
|
const products = data.items.map((item, index) => {
|
|
let itemData;
|
|
if (data.itemsData && index in data.itemsData) {
|
|
itemData = data.itemsData[index];
|
|
}
|
|
|
|
return (
|
|
<ProductCartItem
|
|
isMobile={isMobile}
|
|
isTouch={isTouch}
|
|
hovered={item.id === itemHovered}
|
|
key={item.id}
|
|
id={item.id}
|
|
index={index}
|
|
data={itemData}
|
|
onToggleOverlayRemove={onToggleOverlayRemove}
|
|
onClickRemoveItem={onClickRemoveItem}
|
|
onClickItem={onClickItem}
|
|
model={item}>
|
|
</ProductCartItem>
|
|
);
|
|
});
|
|
|
|
return (
|
|
<Droppable droppableId={data.id} direction="horizontal">
|
|
|
|
{(provided, snapshot) => (
|
|
<div
|
|
ref={provided.innerRef}
|
|
{...provided.droppableProps}
|
|
style={cartStyle(
|
|
provided.droppableProps.style,
|
|
snapshot,
|
|
)}
|
|
className="items-cart-list">
|
|
|
|
{products}
|
|
|
|
{provided.placeholder && (
|
|
<div style={{ display: 'none' }}>
|
|
{provided.placeholder}
|
|
</div>
|
|
)}
|
|
|
|
<FakePlaceholder
|
|
nbrSlots={nbrSlots}
|
|
items={data.items}
|
|
isDraggingOver={snapshot.isDraggingOver} />
|
|
</div>
|
|
)}
|
|
|
|
</Droppable>
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Component that displays crate modes
|
|
*/
|
|
class CrateMode extends React.PureComponent {
|
|
|
|
static get propTypes() {
|
|
return {
|
|
items: PropTypes.array.isRequired,
|
|
mode: PropTypes.string.isRequired,
|
|
onClickMode: PropTypes.func,
|
|
};
|
|
}
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
this.handleOnClickMode = this.handleOnClickMode.bind(this);
|
|
}
|
|
|
|
handleOnClickMode(mode, e) {
|
|
if (this.props.onClickMode) {
|
|
this.props.onClickMode(mode);
|
|
}
|
|
e.preventDefault();
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
mode,
|
|
items,
|
|
} = this.props;
|
|
|
|
return (
|
|
<div className="crate-mode">
|
|
{items.map(item => (
|
|
<a
|
|
key={item.id}
|
|
className={mode == item.id ? 'active' : ''}
|
|
onClick={this.handleOnClickMode.bind(this, item.id)}
|
|
href="#"
|
|
role="button">{item.name}</a>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Component that displays the main crate with reminder rules.
|
|
* It includes <Cart> and rules
|
|
*/
|
|
class Crate extends React.PureComponent {
|
|
|
|
static get propTypes() {
|
|
return {
|
|
rules: PropTypes.array,
|
|
cart: PropTypes.element,
|
|
};
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
rules,
|
|
cart,
|
|
} = this.props;
|
|
|
|
return (
|
|
<div className="crate">
|
|
|
|
<div className="crate-products">
|
|
|
|
{cart}
|
|
|
|
{rules && rules.length > 0 && (
|
|
<div className="crate-info">
|
|
{rules.map((rule, index) => (
|
|
<p key={index} className="rule" style={{'color': rule.color ? rule.color : 'inherit'}}>
|
|
<img src={`/images${rule.icon}`} /> <i><strong>{rule.name}:</strong> {rule.message}</i>
|
|
</p>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Component that renders all things for order.
|
|
* It acts like-a layout, this component do nothing more.
|
|
*/
|
|
class OrderPanel extends React.PureComponent {
|
|
|
|
static get propTypes() {
|
|
return {
|
|
title: PropTypes.string,
|
|
description: PropTypes.string,
|
|
crateMode: PropTypes.element,
|
|
crate: PropTypes.element,
|
|
summaryPrice: PropTypes.element,
|
|
form: PropTypes.element,
|
|
isMobile: PropTypes.bool,
|
|
onClickToggleMobileSideMenu: PropTypes.func,
|
|
onClickOpenImport: PropTypes.func,
|
|
};
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
title,
|
|
description,
|
|
crateMode,
|
|
crate,
|
|
summaryPrice,
|
|
form,
|
|
isMobile,
|
|
onClickToggleMobileSideMenu,
|
|
onClickOpenImport,
|
|
} = this.props;
|
|
|
|
return (
|
|
<section className="panel">
|
|
|
|
<h2>{title}</h2>
|
|
|
|
<div className="control">
|
|
<p className="description">{description}</p>
|
|
|
|
{crateMode}
|
|
</div>
|
|
|
|
<div>
|
|
<button
|
|
className="btn btn-sm btn-outline-primary m-0 mb-2"
|
|
style={{'cursor': 'pointer'}}
|
|
onClick={onClickOpenImport}>Import JSON</button>
|
|
</div>
|
|
|
|
{isMobile ? (
|
|
<div className="mobileBtnDisplaySideMenu">
|
|
<button onClick={onClickToggleMobileSideMenu}>
|
|
<img src="/images/shop/icon-add.svg" alt="add" />
|
|
</button>
|
|
</div>
|
|
) : null}
|
|
|
|
{crate}
|
|
|
|
<section className="summary">
|
|
{summaryPrice}
|
|
|
|
{form}
|
|
</section>
|
|
|
|
</section>
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Components that renders the form to request quote.
|
|
*/
|
|
class OrderForm extends React.PureComponent {
|
|
|
|
static get propTypes() {
|
|
return {
|
|
isProcessing: PropTypes.bool,
|
|
isProcessingComplete: PropTypes.bool,
|
|
onClickSubmit: PropTypes.func,
|
|
};
|
|
}
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
note: '',
|
|
email: '',
|
|
error: {
|
|
note: null,
|
|
email: null,
|
|
},
|
|
empty: {
|
|
note: null,
|
|
email: null,
|
|
},
|
|
};
|
|
|
|
this.handleEmail = this.handleEmail.bind(this);
|
|
this.handleNote = this.handleNote.bind(this);
|
|
this.handleSubmit = this.handleSubmit.bind(this);
|
|
this.resetEmptyError = this.resetEmptyError.bind(this);
|
|
this.checkValidation = this.checkValidation.bind(this);
|
|
}
|
|
|
|
checkValidation() {
|
|
let isValid = true;
|
|
let validationFields = {...this.state};
|
|
|
|
const {
|
|
isEmpty: isEmailEmpty,
|
|
isError: isEmailError
|
|
} = this.validateEmail(this.state.email);
|
|
|
|
validationFields = {
|
|
...validationFields,
|
|
error: {
|
|
...this.state.error,
|
|
email: isEmailError,
|
|
},
|
|
empty: {
|
|
...this.state.empty,
|
|
email: isEmailEmpty,
|
|
}
|
|
}
|
|
|
|
this.setState(validationFields);
|
|
|
|
isValid =
|
|
!isEmailEmpty &&
|
|
!isEmailError
|
|
|
|
return isValid;
|
|
}
|
|
|
|
validateEmail(value) {
|
|
let isEmpty = null;
|
|
let isError = null;
|
|
|
|
const { t } = this.props;
|
|
|
|
if (!value || value.trim() === '') {
|
|
isEmpty = true;
|
|
} else if (value && !value.match(/^\w+([\+\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/)) {
|
|
isError = {
|
|
message: 'Your email is incomplete',
|
|
};
|
|
}
|
|
|
|
return { isEmpty, isError };
|
|
}
|
|
|
|
validateNote(value) {
|
|
let isEmpty = null;
|
|
|
|
if (!value || value.trim() === '') {
|
|
isEmpty = true;
|
|
}
|
|
|
|
return { isEmpty };
|
|
}
|
|
|
|
resetEmptyError(key) {
|
|
this.setState({
|
|
...this.state,
|
|
error: {
|
|
...this.state.error,
|
|
[key]: null,
|
|
},
|
|
empty: {
|
|
...this.state.empty,
|
|
[key]: null,
|
|
},
|
|
});
|
|
}
|
|
|
|
handleEmail(e) {
|
|
const value = e.target.value;
|
|
const { isEmpty, isError } = this.validateEmail(value);
|
|
|
|
this.setState({
|
|
...this.state,
|
|
email: value,
|
|
error: {
|
|
...this.state.error,
|
|
email: isError,
|
|
},
|
|
empty: {
|
|
...this.state.empty,
|
|
email: isEmpty,
|
|
}
|
|
});
|
|
}
|
|
|
|
handleNote(e) {
|
|
const value = e.target.value;
|
|
|
|
this.setState({
|
|
...this.state,
|
|
note: value,
|
|
});
|
|
}
|
|
|
|
handleSubmit(event) {
|
|
event.preventDefault();
|
|
|
|
if (this.props.onClickSubmit) {
|
|
// check validation input fields
|
|
const isValidated = this.checkValidation();
|
|
if (!isValidated) {
|
|
return false;
|
|
}
|
|
|
|
this.props.onClickSubmit(this.state.note, this.state.email);
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
handleEmail,
|
|
handleNote,
|
|
resetEmptyError,
|
|
handleSubmit,
|
|
} = this;
|
|
|
|
const {
|
|
onClickShow,
|
|
} = this.props;
|
|
|
|
const {
|
|
email,
|
|
note,
|
|
error,
|
|
empty
|
|
} = this.state;
|
|
|
|
const { isProcessing, isProcessingComplete } = this.props;
|
|
|
|
return (
|
|
<div className="summary-form">
|
|
|
|
<form onSubmit={handleSubmit} noValidate>
|
|
|
|
<input
|
|
className={`${error && error.email ? 'errorField':''}`}
|
|
type="email"
|
|
placeholder="Email"
|
|
onFocus={() => resetEmptyError('email')}
|
|
onChange={handleEmail}
|
|
onBlur={handleEmail}
|
|
value={email} />
|
|
|
|
{ empty && empty.email ? (
|
|
<div className="error">
|
|
<small>Required</small>
|
|
</div>
|
|
) : null}
|
|
|
|
{ error && error.email ? (
|
|
<div className="error">
|
|
<small>Your email is incomplete</small>
|
|
</div>
|
|
) : null}
|
|
|
|
<textarea
|
|
onChange={handleNote}
|
|
defaultValue={note}
|
|
rows="5"
|
|
placeholder="Additional notes" />
|
|
|
|
<div className="d-flex flex-column flex-sm-row justify-content-between">
|
|
<input
|
|
className="btn btn-outline-primary w-100 m-0 mb-2 mb-sm-0 me-sm-2"
|
|
style={{'cursor': 'pointer', 'fontWeight': '700'}}
|
|
defaultValue="Show JSON"
|
|
onClick={onClickShow}
|
|
readOnly={true} />
|
|
|
|
<input className="btn btn-primary w-100 m-0 ms-sm-2" type="submit" value={`${isProcessing ? 'Processing ...' : 'Request quote'}`} />
|
|
</div>
|
|
{/*This will open an email window. Send the email to make your request.*/}
|
|
</form>
|
|
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Components that displays the list of card that are used in the crate.
|
|
* It is a summary of purchase
|
|
*/
|
|
class OrderSumary extends React.PureComponent {
|
|
|
|
static get propTypes() {
|
|
return {
|
|
currency: PropTypes.string,
|
|
modes: PropTypes.array,
|
|
currentMode: PropTypes.string,
|
|
summary: PropTypes.array,
|
|
itemsData: PropTypes.array,
|
|
onDeleteItem: PropTypes.func,
|
|
onDeleteAllItems: PropTypes.func,
|
|
onMouseEnterItem: PropTypes.func,
|
|
onMouseLeaveItem: PropTypes.func,
|
|
onClickSelectItem: PropTypes.func,
|
|
};
|
|
}
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
this.handleOnDeleteItem = this.handleOnDeleteItem.bind(this);
|
|
this.handleOnDeleteAllItems = this.handleOnDeleteAllItems.bind(this);
|
|
this.handleOnMouseEnterItem = this.handleOnMouseEnterItem.bind(this);
|
|
this.handleOnMouseLeaveItem = this.handleOnMouseLeaveItem.bind(this);
|
|
this.handleOnClickSelectItem = this.handleOnClickSelectItem.bind(this);
|
|
}
|
|
|
|
handleOnDeleteItem(index, e) {
|
|
if (this.props.onDeleteItem) {
|
|
this.props.onDeleteItem(index);
|
|
}
|
|
e.preventDefault();
|
|
}
|
|
|
|
handleOnDeleteAllItems(e) {
|
|
if (this.props.onDeleteAllItems) {
|
|
this.props.onDeleteAllItems();
|
|
}
|
|
e.preventDefault();
|
|
}
|
|
|
|
handleOnMouseEnterItem(id, e) {
|
|
if (this.props.onMouseEnterItem) {
|
|
this.props.onMouseEnterItem(id);
|
|
}
|
|
e.preventDefault();
|
|
}
|
|
|
|
handleOnMouseLeaveItem(e) {
|
|
if (this.props.onMouseLeaveItem) {
|
|
this.props.onMouseLeaveItem();
|
|
}
|
|
e.preventDefault();
|
|
}
|
|
|
|
handleOnClickSelectItem(index, e) {
|
|
if (e.target.tagName !== 'IMG') {
|
|
if (this.props.onClickSelectItem) {
|
|
this.props.onClickSelectItem(index);
|
|
}
|
|
}
|
|
return e.preventDefault();
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
currency,
|
|
modes,
|
|
currentMode,
|
|
summary,
|
|
itemsData,
|
|
} = this.props;
|
|
|
|
const mode = modes.find(elem => elem.id === currentMode);
|
|
|
|
return (
|
|
<div className="summary-price">
|
|
|
|
<table>
|
|
|
|
<thead>
|
|
<tr>
|
|
<td colSpan="2" className="summary-remove-all">
|
|
<span className="item-card-name">Remove all cards</span>
|
|
|
|
<button onClick={this.handleOnDeleteAllItems}>
|
|
<img src="/images/shop/icon-remove.svg" />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
|
|
{mode && (
|
|
<tr>
|
|
<td className="item-card-name">{mode.name}</td>
|
|
<td className="price">
|
|
<div>
|
|
{`${currency} ${formatMoney(mode.price)}`}
|
|
|
|
<button style={{'opacity': '0', 'cursor': 'initial'}}>
|
|
<img src="/images/shop/icon-remove.svg" />
|
|
</button>
|
|
</div>
|
|
|
|
<span style={{
|
|
'display': 'inline-block',
|
|
'width': '30px',
|
|
}}> </span>
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</thead>
|
|
|
|
<tbody>
|
|
{summary.map((item, index) => {
|
|
let alert, warning, options, options_data;
|
|
|
|
if (itemsData[index] && itemsData[index].warnings) {
|
|
alert = itemsData[index];
|
|
const warningsKeys = Object.keys(alert.warnings);
|
|
if (warningsKeys && warningsKeys.length > 0) {
|
|
warning = alert.warnings[warningsKeys[0]];
|
|
}
|
|
}
|
|
options = itemsData[index] && itemsData[index].options;
|
|
options_data = itemsData[index] && itemsData[index].options_data;
|
|
|
|
return (
|
|
<tr key={item.id}
|
|
className={`hoverable ${item.selected ? 'selected' : ''}`}
|
|
onClick={this.handleOnClickSelectItem.bind(this, index)}
|
|
onMouseEnter={this.handleOnMouseEnterItem.bind(this, item.id)}
|
|
onMouseLeave={this.handleOnMouseLeaveItem}>
|
|
|
|
<td className="item-card-name">
|
|
<div>{`${item.name_number} ${item.name} ${item.name_codename}`}</div>
|
|
</td>
|
|
|
|
<td className="price">
|
|
<div>
|
|
{`${currency} ${formatMoney(item.price)}`}
|
|
|
|
<button onClick={this.handleOnDeleteItem.bind(this, index)}>
|
|
<img src="/images/shop/icon-remove.svg" />
|
|
</button>
|
|
</div>
|
|
|
|
{warning ? (
|
|
<img
|
|
style={{'marginLeft': '10px'}}
|
|
className="alert-warning"
|
|
src={`/images/${warning.icon}`}
|
|
/>
|
|
) : ( (options && options_data) ?
|
|
( <OptionsSummaryPopup id={item.id + "options"} data={options_data} /> ) : null
|
|
)}
|
|
|
|
{(!warning && !options) && (
|
|
<span style={{
|
|
'display': 'inline-block',
|
|
'width': '30px',
|
|
}}> </span>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
|
|
<tfoot>
|
|
<tr>
|
|
<td className="item-card-name">Price estimate</td>
|
|
<td className="price">
|
|
<div>
|
|
{summary.length ? (
|
|
`${currency} ${formatMoney(summary.reduce(
|
|
(prev, next) => {
|
|
return prev + next.price;
|
|
}, 0
|
|
) + mode.price)}`
|
|
) : (
|
|
`${currency} ${formatMoney(mode.price)}`
|
|
)}
|
|
|
|
<button style={{'opacity': '0', 'cursor': 'initial'}}>
|
|
<img src="/images/shop/icon-remove.svg" alt="icon remove"/>
|
|
</button>
|
|
</div>
|
|
|
|
<span style={{
|
|
'display': 'inline-block',
|
|
'width': '30px',
|
|
}}> </span>
|
|
</td>
|
|
</tr>
|
|
</tfoot>
|
|
|
|
</table>
|
|
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Component that renders the backlog in the aside
|
|
*/
|
|
class Backlog extends React.PureComponent {
|
|
|
|
static get propTypes() {
|
|
return {
|
|
currency: PropTypes.string,
|
|
data: PropTypes.object.isRequired,
|
|
items: PropTypes.object,
|
|
isMobile: PropTypes.bool,
|
|
onClickAddItem: PropTypes.func,
|
|
onClickToggleMobileSideMenu: PropTypes.func,
|
|
};
|
|
}
|
|
|
|
static get defaultProps() {
|
|
return {
|
|
items: {},
|
|
};
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
currency,
|
|
data,
|
|
items,
|
|
onClickAddItem,
|
|
onClickToggleMobileSideMenu,
|
|
isMobile,
|
|
} = this.props;
|
|
|
|
|
|
const ordered_groups = data.categories.map(groupItem => ({ name: groupItem.name,
|
|
items: groupItem.itemIds.map(itemId => items[itemId])
|
|
}));
|
|
let item_index = -1;
|
|
const groups = ordered_groups.map((group, g_index) => {
|
|
return (
|
|
<div className="accordion-item" key={`${group.name}`}>
|
|
<h2 className="accordion-header">
|
|
<button className="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
|
data-bs-target={`#collapse${g_index}`} aria-expanded="true" aria-controls={`collapse${g_index}`}>
|
|
{group.name}
|
|
</button>
|
|
</h2>
|
|
<div id={`collapse${g_index}`} className="accordion-collapse collapse" aria-labelledby="headingOne"
|
|
data-bs-parent="#accordion_categories">
|
|
<div className="accordion-body">
|
|
{group.items.map(item => {
|
|
item_index++;
|
|
return (
|
|
<ProductItem
|
|
key={item.id}
|
|
id={uuidv4()}
|
|
index={item_index}
|
|
name={`${item.name_number} ${item.name}`}
|
|
name_codename={item.name_codename}
|
|
price={item.price}
|
|
currency={currency}
|
|
image={`/images/${item.image}`}
|
|
specs={item.specs}
|
|
datasheet_file={item.datasheet_file}
|
|
datasheet_name={item.datasheet_name}
|
|
onClickAddItem={onClickAddItem}
|
|
></ProductItem>
|
|
)})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
|
|
return (
|
|
<Droppable
|
|
droppableId={data.id}
|
|
isDropDisabled={true}>
|
|
|
|
{(provided) => (
|
|
<div
|
|
className="backlog-container"
|
|
ref={provided.innerRef}
|
|
{...provided.droppableProps}>
|
|
|
|
{isMobile ? (
|
|
<div className="mobileCloseMenu">
|
|
<button onClick={onClickToggleMobileSideMenu}>
|
|
<img src="/images/shop/icon-close-white.svg" alt="add" />
|
|
</button>
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="accordion accordion-flush" id="accordion_categories">
|
|
{groups}
|
|
</div>
|
|
|
|
{provided.placeholder && (
|
|
<div style={{ display: 'none' }}>
|
|
{provided.placeholder}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
</Droppable>
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Component that render the entire shop
|
|
*/
|
|
class Shop extends React.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.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(
|
|
prevState.columns.cart.items,
|
|
this.state.columns.cart.items);
|
|
}
|
|
|
|
if (this.state.newCardJustAdded) {
|
|
this.timer = setTimeout(() => {
|
|
this.setState({
|
|
newCardJustAdded: false,
|
|
});
|
|
}, 2000);
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
clearTimeout(this.timer);
|
|
}
|
|
|
|
handleCrateModeChange(mode) {
|
|
this.setState({
|
|
currentMode: mode,
|
|
});
|
|
}
|
|
|
|
handleDeleteItem(index) {
|
|
const cloned = Array.from(this.state.columns.cart.items);
|
|
cloned.splice(index, 1);
|
|
this.setState({
|
|
...this.state,
|
|
columns: {
|
|
...this.state.columns,
|
|
cart: {
|
|
...this.state.columns.cart,
|
|
items: cloned,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
handleDeleteAllItems() {
|
|
this.setState({
|
|
...this.state,
|
|
columns: {
|
|
...this.state.columns,
|
|
cart: {
|
|
...this.state.columns.cart,
|
|
items: [],
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
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_data : null,
|
|
});
|
|
}
|
|
|
|
this.setState({
|
|
isProcessing: false,
|
|
shouldShowRFQFeedback: true,
|
|
RFQBodyType: 'show',
|
|
RFQBodyOrder: JSON.stringify(crate),
|
|
});
|
|
}
|
|
|
|
handleClickOpenImport() {
|
|
this.setState({
|
|
isProcessing: false,
|
|
shouldShowRFQFeedback: true,
|
|
RFQBodyType: 'import',
|
|
});
|
|
}
|
|
|
|
handleLoadCustomConf(customconf) {
|
|
if (!customconf) {return; }
|
|
|
|
const items = this.props.data.items;
|
|
console.trace(customconf);
|
|
const self = this;
|
|
|
|
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_data : null,
|
|
});
|
|
}
|
|
|
|
const {data} = this.props;
|
|
|
|
this.setState({isProcessing: true});
|
|
|
|
axios.post(data.API_RFQ, {
|
|
email,
|
|
note,
|
|
configuration: JSON.stringify(crate)
|
|
}).then(response => {
|
|
this.setState({
|
|
isProcessing: false,
|
|
shouldShowRFQFeedback: true,
|
|
RFQBodyType: 'email',
|
|
isProcessingComplete: true,
|
|
});
|
|
}).catch(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[at]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,
|
|
),
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
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,
|
|
),
|
|
},
|
|
},
|
|
});
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
handleClickToggleMobileSideMenu() {
|
|
this.setState({
|
|
...this.state,
|
|
mobileSideMenuShouldOpen: !this.state.mobileSideMenuShouldOpen,
|
|
});
|
|
}
|
|
|
|
handleClickCloseRFQFeedback() {
|
|
this.setState({
|
|
shouldShowRFQFeedback: false,
|
|
});
|
|
}
|
|
|
|
checkAlerts(prevItems, newItems) {
|
|
console.log('--- START CHECKING CRATE WARNING ---');
|
|
|
|
const {
|
|
currentMode,
|
|
crateModeSlots,
|
|
crateRules,
|
|
} = this.state;
|
|
|
|
const itemsCloned = Array.from(newItems);
|
|
const itemsData = [];
|
|
const rules = {};
|
|
|
|
|
|
// 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};
|
|
}
|
|
|
|
|
|
// check the number of EEM connectors available for all Kasli
|
|
const idxK = itemsCloned.reduce((prev, next, i) => {
|
|
if (next.type === 'kasli' || next.type === 'vhdcicarrier') {
|
|
prev.push(i);
|
|
}
|
|
return prev;
|
|
}, []);
|
|
for (let i = 0; i <= idxK.length - 1; i++) {
|
|
let slots;
|
|
let nbUsedSlot = 0;
|
|
let nbrCurrentClock = 0;
|
|
let idx = idxK[i];
|
|
|
|
if (i !== idxK.length - 1) {
|
|
slots = itemsCloned.slice(idx + 1, idxK[i + 1]);
|
|
} else {
|
|
slots = itemsCloned.slice(idx + 1);
|
|
}
|
|
|
|
if (i == 0) {
|
|
const slots_need_resource = itemsCloned.slice(0, idx);
|
|
const idx_need = slots_need_resource.findIndex(e => (e.rules && e.rules.resources));
|
|
|
|
if (idx_need != -1) {
|
|
if (idx_need in itemsData) {
|
|
if ('warnings' in itemsData[idx_need]) {
|
|
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
|
|
} else {
|
|
itemsData[idx_need].warnings = {};
|
|
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
|
|
}
|
|
} else {
|
|
itemsData[idx_need] = {...itemsCloned[idx_need]};
|
|
itemsData[idx_need].warnings = {};
|
|
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
|
|
}
|
|
}
|
|
}
|
|
|
|
nbUsedSlot = slots
|
|
.filter(item => item.type !== 'idc-bnc')
|
|
.reduce((prev, next) => {
|
|
return prev + next.slotOccupied;
|
|
}, 0);
|
|
nbrCurrentClock = slots
|
|
.reduce((prev, next) => {
|
|
return next.type === 'clocker' ? prev + next.clockOccupied : prev;
|
|
}, 0);
|
|
|
|
if (idx in itemsData) {
|
|
itemsData[idx].nbrCurrentSlot = nbUsedSlot;
|
|
itemsData[idx].nbrCurrentClock = nbrCurrentClock;
|
|
if (!('warnings' in itemsData[idx])) {
|
|
itemsData[idx].warnings = {};
|
|
}
|
|
} else {
|
|
itemsData[idx] = {...itemsCloned[idx]};
|
|
itemsData[idx].nbrCurrentSlot = nbUsedSlot;
|
|
itemsData[idx].nbrCurrentClock = nbrCurrentClock;
|
|
itemsData[idx].warnings = {};
|
|
}
|
|
|
|
if (nbUsedSlot > itemsCloned[idx].nbrSlotMax) {
|
|
if (itemsCloned[idx].rules.maxSlot.message) {
|
|
rules[itemsCloned[idx].rules.maxSlot.type] = {...itemsCloned[idx].rules.maxSlot};
|
|
}
|
|
itemsData[idx].warnings.maxSlotWarning = {...itemsCloned[idx].rules.maxSlotWarning};
|
|
}
|
|
|
|
if (nbrCurrentClock > itemsCloned[idx].nbrClockMax) {
|
|
rules[itemsCloned[idx].rules.maxClock.type] = {...itemsCloned[idx].rules.maxClock};
|
|
itemsData[idx].warnings.maxClockWarning = {...itemsCloned[idx].rules.maxClockWarning};
|
|
}
|
|
|
|
if (itemsCloned.length > (idx + 1)) {
|
|
const ddkali = itemsCloned[idx + 1];
|
|
if (ddkali.type === 'kasli' || ddkali.type === 'vhdcicarrier') {
|
|
rules[ddkali.rules.follow.type] = {...ddkali.rules.follow};
|
|
}
|
|
}
|
|
}
|
|
|
|
if (idxK.length === 0) {
|
|
const slots_need_resource = itemsCloned.slice(0);
|
|
const idx_need = slots_need_resource.findIndex(e => (e.rules && e.rules.resources));
|
|
|
|
if (idx_need != -1) {
|
|
if (idx_need in itemsData) {
|
|
if ('warnings' in itemsData[idx_need]) {
|
|
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
|
|
} else {
|
|
itemsData[idx_need].warnings = {};
|
|
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
|
|
}
|
|
} else {
|
|
itemsData[idx_need] = {...itemsCloned[idx_need]};
|
|
itemsData[idx_need].warnings = {};
|
|
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// check number of clock connector available
|
|
const idxC = itemsCloned.reduce((prev, next, i) => {
|
|
if (next.type === 'kasli' || next.type === 'clocker') {
|
|
prev.push(i);
|
|
}
|
|
return prev;
|
|
}, []);
|
|
for (let i = 0; i <= idxC.length - 1; i++) {
|
|
let slots;
|
|
let nbrCurrentClock = 0;
|
|
let idx = idxC[i];
|
|
|
|
if (i !== idxC.length - 1) {
|
|
slots = itemsCloned.slice(idx + 1, idxC[i + 1]);
|
|
} else {
|
|
slots = itemsCloned.slice(idx + 1);
|
|
}
|
|
|
|
nbrCurrentClock = slots.reduce((prev, next) => {
|
|
return prev + next.clockOccupied;
|
|
}, 0);
|
|
|
|
if (idx in itemsData) {
|
|
if (itemsData[idx].nbrCurrentClock) {
|
|
itemsData[idx].nbrCurrentClock += nbrCurrentClock;
|
|
} else {
|
|
itemsData[idx].nbrCurrentClock = nbrCurrentClock;
|
|
}
|
|
} else {
|
|
itemsData[idx] = {...itemsCloned[idx]};
|
|
itemsData[idx].nbrCurrentClock = nbrCurrentClock;
|
|
itemsData[idx].warnings = {};
|
|
}
|
|
|
|
if (nbrCurrentClock > itemsCloned[idx].nbrClockMax) {
|
|
rules[itemsCloned[idx].rules.maxClock.type] = {...itemsCloned[idx].rules.maxClock};
|
|
itemsData[idx].warnings.maxClockWarning = {...itemsCloned[idx].rules.maxClockWarning};
|
|
}
|
|
}
|
|
|
|
|
|
// check for number of recommended EEM connectors
|
|
['novo', 'urukul', 'koster'].map(_type => {
|
|
if (itemsCloned.find(elem => elem.type === _type)) {
|
|
rules[this.state.items[_type].rules.connectors.type] = {...this.state.items[_type].rules.connectors};
|
|
}
|
|
return _type;
|
|
});
|
|
|
|
|
|
if (itemsCloned.find(elem => elem.type === 'urukul')) {
|
|
if (this.state.items['urukul'].rules.info) {
|
|
rules[this.state.items['urukul'].rules.info.type] = {...this.state.items['urukul'].rules.info};
|
|
}
|
|
}
|
|
|
|
|
|
// check if IDC-BNC is correctly positionned (after Zotino or HD68)
|
|
const idxIDCBNC = itemsCloned.reduce((prev, next, i) => {
|
|
if (next.type === 'idc-bnc') {
|
|
prev.push(i);
|
|
}
|
|
return prev;
|
|
}, []);
|
|
for (var i = idxIDCBNC.length - 1; i >= 0; i--) {
|
|
const ce = idxIDCBNC[i];
|
|
let shouldWarning = false;
|
|
|
|
if (ce == 0) {
|
|
shouldWarning = true;
|
|
} else if (ce >= 1) {
|
|
const pe = idxIDCBNC[i] - 1;
|
|
if (itemsCloned[pe].type !== 'zotino' &&
|
|
itemsCloned[pe].type !== 'hd68' &&
|
|
itemsCloned[pe].type !== 'idc-bnc') {
|
|
shouldWarning = true;
|
|
}
|
|
}
|
|
|
|
if (shouldWarning) {
|
|
itemsData[ce] = {...itemsCloned[ce]};
|
|
itemsData[ce].warnings = {};
|
|
itemsData[ce].warnings.wrong = {...itemsCloned[ce].rules.wrong};
|
|
}
|
|
}
|
|
|
|
|
|
// check number of IDC-BNC adapters for a Zotino and HD68-IDC
|
|
const idxZH = itemsCloned.reduce((prev, next, i) => {
|
|
if (next.type === 'zotino' || next.type === 'hd68') {
|
|
prev.push(i);
|
|
}
|
|
return prev;
|
|
}, []);
|
|
for (let i = 0; i <= idxZH.length - 1; i++) {
|
|
let slots;
|
|
let nbUsedSlot = 0;
|
|
let idx = idxZH[i];
|
|
|
|
if (i !== idxZH.length - 1) {
|
|
slots = itemsCloned.slice(idx + 1, idxZH[i + 1]);
|
|
} else {
|
|
slots = itemsCloned.slice(idx + 1);
|
|
}
|
|
|
|
let stopCount = false;
|
|
nbUsedSlot = slots.reduce((prev, next, ci, ca) => {
|
|
if (ci === 0 && next.type === 'idc-bnc') {
|
|
return prev + 1;
|
|
} else if (ca[0].type === 'idc-bnc' && ci > 0 && ca[ci - 1].type === 'idc-bnc') {
|
|
if (next.type !== 'idc-bnc') { stopCount = true; }
|
|
return prev + (next.type === 'idc-bnc' && !stopCount ? 1 : 0);
|
|
}
|
|
return prev;
|
|
}, 0);
|
|
|
|
if (idx in itemsData) {
|
|
itemsData[idx].nbrCurrentSlot = nbUsedSlot;
|
|
if (!('warnings' in itemsData[idx])) {
|
|
itemsData[idx].warnings = {};
|
|
}
|
|
} else {
|
|
itemsData[idx] = {...itemsCloned[idx]};
|
|
itemsData[idx].nbrCurrentSlot = nbUsedSlot;
|
|
itemsData[idx].warnings = {};
|
|
}
|
|
|
|
if (nbUsedSlot > 0) {
|
|
if (itemsCloned[idx].rules.maxSlot.message) {
|
|
rules[itemsCloned[idx].rules.maxSlot.type] = {...itemsCloned[idx].rules.maxSlot};
|
|
}
|
|
}
|
|
if (nbUsedSlot > itemsCloned[idx].nbrSlotMax) {
|
|
itemsData[idx].warnings.maxSlotWarning = {...itemsCloned[idx].rules.maxSlotWarning};
|
|
}
|
|
|
|
// check if HD68-IDC has at least 1 IDC-BNC adapter
|
|
if (itemsCloned[idx].type === 'hd68') {
|
|
let shouldWarning = false;
|
|
|
|
if (idx < itemsCloned.length - 1) {
|
|
if (itemsCloned[idx + 1].type !== 'idc-bnc') {
|
|
shouldWarning = true;
|
|
}
|
|
} else if (idx === itemsCloned.length - 1) {
|
|
shouldWarning = true;
|
|
}
|
|
|
|
if (shouldWarning) {
|
|
if (idx in itemsData) {
|
|
itemsData[idx].warnings.minAdapter = {...itemsCloned[idx].rules.minAdapter};
|
|
} else {
|
|
itemsData[idx] = {...itemsCloned[idx]};
|
|
itemsData[idx].warnings = {};
|
|
itemsData[idx].warnings.minAdapter = {...itemsCloned[idx].rules.minAdapter};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
itemsCloned.forEach((elem, idx) => {
|
|
if (!(idx in itemsData)) itemsData[idx] = elem;
|
|
if (idx in this.state.columns.cart.itemsData && this.state.columns.cart.itemsData[idx].options_data) {
|
|
itemsData[idx].options_data = this.state.columns.cart.itemsData[idx].options_data;
|
|
}
|
|
});
|
|
|
|
// update state with rules
|
|
this.setState({
|
|
...this.state,
|
|
columns: {
|
|
...this.state.columns,
|
|
cart: {
|
|
...this.state.columns.cart,
|
|
itemsData: itemsData,
|
|
}
|
|
},
|
|
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="
|
|
Drag and drop the cards you want into the crate below to see how the combination would look like. If you have any issues with this ordering system, or if you need other configurations, email us directly anytime at sales@m-****.hk. The price is estimated and must be confirmed by a quote."
|
|
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}>
|
|
</Cart>
|
|
}
|
|
rules={Object.values(rules).filter(rule => rule)}>
|
|
</Crate>
|
|
}
|
|
summaryPrice={
|
|
<OrderSumary
|
|
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}>
|
|
</OrderSumary>
|
|
}
|
|
form={
|
|
<OrderForm
|
|
isProcessingComplete={isProcessingComplete}
|
|
processingComplete={this.handleProcessingComplete}
|
|
isProcessing={isProcessing}
|
|
onClickSubmit={this.handleClickSubmit}
|
|
onClickShow={this.handleClickShowOrder}>
|
|
</OrderForm>
|
|
}>
|
|
</OrderPanel>
|
|
)}>
|
|
</Layout>
|
|
|
|
</DragDropContext>
|
|
);
|
|
}
|
|
}
|
|
|
|
createRoot(document.querySelector('#root-shop')).render(<Shop data={data} />);
|