2019-10-02 12:16:07 +08:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
const {
|
|
|
|
DragDropContext,
|
|
|
|
Draggable,
|
|
|
|
Droppable
|
|
|
|
} = window.ReactBeautifulDnd;
|
|
|
|
|
|
|
|
const data = window.shop_data;
|
|
|
|
|
|
|
|
|
|
|
|
const productStyle = (style, snapshot, removeAnim, hovered, selected) => {
|
|
|
|
const custom = {
|
|
|
|
opacity: snapshot.isDragging ? .7 : 1,
|
|
|
|
backgroundColor: (hovered || selected) ? '#eae7f7' : 'initial',
|
|
|
|
};
|
|
|
|
|
|
|
|
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,
|
|
|
|
droppableSource,
|
|
|
|
droppableDestination
|
|
|
|
) => {
|
|
|
|
const sourceClone = Array.from(source.itemIds);
|
|
|
|
const destClone = Array.from(destination.items);
|
|
|
|
const item = sourceClone[droppableSource.index];
|
|
|
|
|
|
|
|
destClone.splice(droppableDestination.index, 0, {
|
|
|
|
...model[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);
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const {
|
|
|
|
aside,
|
|
|
|
main,
|
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="layout">
|
|
|
|
|
|
|
|
<aside className="aside">{aside}</aside>
|
|
|
|
|
|
|
|
<section className="main">{main}</section>
|
|
|
|
|
|
|
|
</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,
|
|
|
|
price: PropTypes.number.isRequired,
|
|
|
|
currency: PropTypes.string.isRequired,
|
|
|
|
image: PropTypes.string.isRequired,
|
|
|
|
specs: PropTypes.array,
|
|
|
|
onClickAddItem: PropTypes.func,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
|
|
|
this.handleOnClickAddItem = this.handleOnClickAddItem.bind(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
handleOnClickAddItem(index, e) {
|
|
|
|
if (this.props.onClickAddItem) {
|
|
|
|
this.props.onClickAddItem(index);
|
|
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const {
|
|
|
|
id,
|
|
|
|
index,
|
|
|
|
name,
|
|
|
|
price,
|
|
|
|
currency,
|
|
|
|
image,
|
|
|
|
specs,
|
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
const render_specs = (specs && specs.length > 0 && (
|
|
|
|
<ul>
|
|
|
|
{specs.map((spec, index) =>
|
|
|
|
<li key={index}>{spec}</li>
|
|
|
|
)}
|
|
|
|
</ul>
|
|
|
|
));
|
|
|
|
|
|
|
|
return (
|
|
|
|
<section className="productItem">
|
|
|
|
|
|
|
|
<div className="content">
|
|
|
|
<h3>{name}</h3>
|
|
|
|
|
|
|
|
<div className="price">{`${currency} ${price}`}</div>
|
|
|
|
|
|
|
|
{render_specs}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="content">
|
|
|
|
|
|
|
|
<button onClick={this.handleOnClickAddItem.bind(this, index)}>
|
|
|
|
<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="" 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 {
|
|
|
|
hovered: PropTypes.bool,
|
|
|
|
index: PropTypes.number.isRequired,
|
|
|
|
model: PropTypes.object.isRequired,
|
|
|
|
data: PropTypes.object,
|
|
|
|
onToggleProgress: PropTypes.func,
|
|
|
|
onToggleWarning: PropTypes.func,
|
|
|
|
onToggleOverlayRemove: PropTypes.func,
|
|
|
|
onClickRemoveItem: PropTypes.func,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
static get defaultProps() {
|
|
|
|
return {
|
|
|
|
hovered: false,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
|
|
|
this.handleOnMouseEnterItem = this.handleOnMouseEnterItem.bind(this);
|
|
|
|
this.handleOnMouseLeaveItem = this.handleOnMouseLeaveItem.bind(this);
|
|
|
|
this.handleOnMouseEnterWarningItem = this.handleOnMouseEnterWarningItem.bind(this);
|
|
|
|
this.handleOnMouseLeaveWarningItem = this.handleOnMouseLeaveWarningItem.bind(this);
|
|
|
|
this.handleOnMouseEnterRemoveItem = this.handleOnMouseEnterRemoveItem.bind(this);
|
|
|
|
this.handleOnMouseLeaveRemoveItem = this.handleOnMouseLeaveRemoveItem.bind(this);
|
|
|
|
this.handleOnClickRemoveItem = this.handleOnClickRemoveItem.bind(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
handleOnMouseEnterItem(index, e) {
|
|
|
|
if (this.props.onToggleProgress) {
|
|
|
|
this.props.onToggleProgress(index, true);
|
|
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
handleOnMouseLeaveItem(index, e) {
|
|
|
|
if (this.props.onToggleProgress) {
|
|
|
|
this.props.onToggleProgress(index, false);
|
|
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
handleOnMouseEnterWarningItem(index, isWarning, e) {
|
|
|
|
if (!isWarning) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (this.props.onToggleWarning) {
|
|
|
|
this.props.onToggleWarning(index, true);
|
|
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
handleOnMouseLeaveWarningItem(index, isWarning, e) {
|
|
|
|
if (!isWarning) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (this.props.onToggleWarning) {
|
|
|
|
this.props.onToggleWarning(index, false);
|
|
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
handleOnMouseEnterRemoveItem(index, e) {
|
|
|
|
if (this.props.onToggleOverlayRemove) {
|
|
|
|
this.props.onToggleOverlayRemove(index, true);
|
|
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
handleOnMouseLeaveRemoveItem(index, e) {
|
|
|
|
if (this.props.onToggleOverlayRemove) {
|
|
|
|
this.props.onToggleOverlayRemove(index, false);
|
|
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
handleOnClickRemoveItem(index, e) {
|
|
|
|
if (this.props.onClickRemoveItem) {
|
|
|
|
this.props.onClickRemoveItem(index);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const {
|
|
|
|
hovered,
|
|
|
|
model,
|
|
|
|
data,
|
|
|
|
index,
|
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
let warning;
|
|
|
|
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]];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let render_progress;
|
|
|
|
if (model.showProgress && data) {
|
|
|
|
switch(model.type) {
|
|
|
|
case 'kasli':
|
|
|
|
case 'kasli-backplane':
|
|
|
|
render_progress = (
|
|
|
|
<div className="k-popup-connectors">
|
|
|
|
<p>{`${data.nbrCurrentSlot}/${model.nbrSlotMax} EEM connectors available`}</p>
|
|
|
|
<p>{`${data.nbrCurrentClock}/${model.nbrClockMax} Clock connectors available`}</p>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'zotino':
|
|
|
|
case 'hd68':
|
|
|
|
render_progress = (
|
|
|
|
<div className="k-popup-connectors">
|
|
|
|
<p>{`${data.nbrCurrentSlot}/${model.nbrSlotMax} connectors available`}</p>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'clocker':
|
|
|
|
render_progress = (
|
|
|
|
<div className="k-popup-connectors">
|
|
|
|
<p>{`${data.nbrCurrentClock}/${model.nbrClockMax} Clock connectors available`}</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,
|
|
|
|
)}}
|
|
|
|
onMouseEnter={this.handleOnMouseEnterRemoveItem.bind(this, index)}
|
|
|
|
onMouseLeave={this.handleOnMouseLeaveRemoveItem.bind(this, index)}
|
|
|
|
>
|
|
|
|
|
|
|
|
{/* warning container */}
|
|
|
|
<div
|
|
|
|
style={{'height': '24px'}}
|
|
|
|
className="progress-container warning"
|
|
|
|
onMouseEnter={this.handleOnMouseEnterWarningItem.bind(this, index, warning)}
|
|
|
|
onMouseLeave={this.handleOnMouseLeaveWarningItem.bind(this, index, warning)}>
|
|
|
|
|
|
|
|
{warning && (
|
|
|
|
<img className="alert-warning" src={warning ? `/images${warning.icon}` : null} />
|
|
|
|
)}
|
|
|
|
|
|
|
|
{warning && model.showWarning && (
|
|
|
|
<div className="k-popup-warning">
|
|
|
|
<p className="rule warning">
|
|
|
|
<i>{warning.message}</i>
|
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<h6>{model.name}</h6>
|
|
|
|
|
|
|
|
<div
|
|
|
|
style={{'height': '250px'}}
|
|
|
|
onMouseEnter={this.handleOnMouseEnterRemoveItem.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 */}
|
|
|
|
<div
|
|
|
|
style={{'height': '22px'}}
|
|
|
|
className="progress-container"
|
|
|
|
onMouseEnter={this.handleOnMouseEnterItem.bind(this, index)}
|
|
|
|
onMouseLeave={this.handleOnMouseLeaveItem.bind(this, index)}>
|
|
|
|
|
|
|
|
{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>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{/* progress info when mouse over */}
|
|
|
|
{render_progress}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</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 {
|
|
|
|
nbrSlots: PropTypes.number,
|
|
|
|
itemHovered: PropTypes.string,
|
|
|
|
data: PropTypes.object.isRequired,
|
|
|
|
onToggleProgress: PropTypes.func,
|
|
|
|
onToggleWarning: PropTypes.func,
|
|
|
|
onToggleOverlayRemove: PropTypes.func,
|
|
|
|
onClickRemoveItem: PropTypes.func,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const {
|
|
|
|
nbrSlots,
|
|
|
|
itemHovered,
|
|
|
|
data,
|
|
|
|
onToggleProgress,
|
|
|
|
onToggleWarning,
|
|
|
|
onToggleOverlayRemove,
|
|
|
|
onClickRemoveItem,
|
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
const products = data.items.map((item, index) => {
|
|
|
|
let itemData;
|
|
|
|
if (data.itemsData && index in data.itemsData) {
|
|
|
|
itemData = data.itemsData[index];
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
<ProductCartItem
|
|
|
|
hovered={item.id === itemHovered}
|
|
|
|
key={item.id}
|
|
|
|
index={index}
|
|
|
|
data={itemData}
|
|
|
|
onToggleProgress={onToggleProgress}
|
|
|
|
onToggleWarning={onToggleWarning}
|
|
|
|
onToggleOverlayRemove={onToggleOverlayRemove}
|
|
|
|
onClickRemoveItem={onClickRemoveItem}
|
|
|
|
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) => (
|
2019-11-06 10:13:14 +08:00
|
|
|
<p key={index} className="rule" style={{'color': rule.color ? rule.color : 'inherit'}}>
|
2019-10-02 12:16:07 +08:00
|
|
|
<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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const {
|
|
|
|
title,
|
|
|
|
description,
|
|
|
|
crateMode,
|
|
|
|
crate,
|
|
|
|
summaryPrice,
|
|
|
|
form,
|
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<section className="panel">
|
|
|
|
|
|
|
|
<h2>{title}</h2>
|
|
|
|
|
|
|
|
<div className="control">
|
|
|
|
<p className="description">{description}</p>
|
|
|
|
|
|
|
|
{crateMode}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{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 {
|
|
|
|
onClickSubmit: PropTypes.func,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
|
|
|
this.state = {note: ''};
|
|
|
|
|
|
|
|
this.handleNoteChange = this.handleNoteChange.bind(this);
|
|
|
|
this.handleSubmit = this.handleSubmit.bind(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
handleNoteChange(event) {
|
|
|
|
this.setState({
|
|
|
|
note: event.target.value,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
handleSubmit(event) {
|
|
|
|
if (this.props.onClickSubmit) {
|
|
|
|
this.props.onClickSubmit(this.state.note);
|
|
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
return (
|
|
|
|
<div className="summary-form">
|
|
|
|
|
|
|
|
<form onSubmit={this.handleSubmit}>
|
|
|
|
<textarea
|
|
|
|
value={this.state.note}
|
|
|
|
onChange={this.handleNoteChange}
|
|
|
|
rows="5"
|
|
|
|
placeholder="Additional notes" />
|
|
|
|
<input type="submit" value="Request quote" />
|
|
|
|
</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 {
|
|
|
|
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 {
|
|
|
|
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">
|
|
|
|
{`${mode.currency} ${mode.price}`}
|
|
|
|
|
|
|
|
<button style={{'opacity': '0', 'cursor': 'initial'}}>
|
|
|
|
<img src="/images/shop/icon-remove.svg" />
|
|
|
|
</button>
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
)}
|
|
|
|
</thead>
|
|
|
|
|
|
|
|
<tbody>
|
|
|
|
{summary.map((item, index) => {
|
|
|
|
let alert;
|
|
|
|
let warning;
|
|
|
|
|
|
|
|
if (itemsData[index]) {
|
|
|
|
alert = itemsData[index];
|
|
|
|
const warningsKeys = Object.keys(alert.warnings);
|
|
|
|
if (warningsKeys && warningsKeys.length > 0) {
|
|
|
|
warning = alert.warnings[warningsKeys[0]];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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}</div>
|
|
|
|
</td>
|
|
|
|
|
|
|
|
<td className="price">
|
|
|
|
<div>
|
|
|
|
{`${item.currency} ${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}`}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</tbody>
|
|
|
|
|
|
|
|
<tfoot>
|
|
|
|
<tr>
|
|
|
|
<td className="item-card-name">Price estimate</td>
|
|
|
|
<td className="price">
|
|
|
|
{summary.length ? (
|
|
|
|
`${summary[0].currency} ${summary.reduce(
|
|
|
|
(prev, next) => {
|
|
|
|
return prev + next.price;
|
|
|
|
}, 0
|
|
|
|
) + mode.price}`
|
|
|
|
) : (
|
|
|
|
`${mode.currency} ${mode.price}`
|
|
|
|
)}
|
|
|
|
|
|
|
|
<button style={{'opacity': '0', 'cursor': 'initial'}}>
|
|
|
|
<img src="/images/shop/icon-remove.svg" alt="icon remove"/>
|
|
|
|
</button>
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
</tfoot>
|
|
|
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Component that renders the backlog in the aside
|
|
|
|
*/
|
|
|
|
class Backlog extends React.PureComponent {
|
|
|
|
|
|
|
|
static get propTypes() {
|
|
|
|
return {
|
|
|
|
data: PropTypes.object.isRequired,
|
|
|
|
items: PropTypes.object,
|
|
|
|
onClickAddItem: PropTypes.func,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
static get defaultProps() {
|
|
|
|
return {
|
|
|
|
items: {},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const {
|
|
|
|
data,
|
|
|
|
items,
|
|
|
|
onClickAddItem,
|
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
const ordered_items = data.itemIds.map(itemId => items[itemId]);
|
|
|
|
const products = ordered_items.map((item, index) => {
|
|
|
|
return (
|
|
|
|
<ProductItem
|
|
|
|
key={item.id}
|
|
|
|
id={item.id}
|
|
|
|
index={index}
|
|
|
|
name={item.name}
|
|
|
|
price={item.price}
|
|
|
|
currency={item.currency}
|
|
|
|
image={`/images/${item.image}`}
|
|
|
|
specs={item.specs}
|
|
|
|
onClickAddItem={onClickAddItem}
|
|
|
|
></ProductItem>
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Droppable
|
|
|
|
droppableId={data.id}
|
|
|
|
isDropDisabled={true}>
|
|
|
|
|
|
|
|
{(provided) => (
|
|
|
|
<div
|
|
|
|
className="backlog-container"
|
|
|
|
ref={provided.innerRef}
|
|
|
|
{...provided.droppableProps}>
|
|
|
|
|
|
|
|
{products}
|
|
|
|
|
|
|
|
{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.handleToggleItemProgress = this.handleToggleItemProgress.bind(this);
|
|
|
|
this.handleToggleItemWarning = this.handleToggleItemWarning.bind(this);
|
|
|
|
this.handleClickSelectItem = this.handleClickSelectItem.bind(this);
|
|
|
|
this.handleClickSubmit = this.handleClickSubmit.bind(this);
|
|
|
|
this.handleToggleOverlayRemove = this.handleToggleOverlayRemove.bind(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidMount() {
|
|
|
|
// index 0 is a Kasli, we place it as a default conf on the crate.
|
|
|
|
const source = {
|
|
|
|
droppableId: 'backlog',
|
|
|
|
index: 0,
|
|
|
|
};
|
|
|
|
const destination = {
|
|
|
|
droppableId: 'cart',
|
|
|
|
index: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
this.handleOnDragEnd({
|
|
|
|
source,
|
|
|
|
destination,
|
|
|
|
draggableId: null,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
|
|
|
const source = {
|
|
|
|
droppableId: 'backlog',
|
|
|
|
index: index,
|
|
|
|
};
|
|
|
|
const destination = {
|
|
|
|
droppableId: 'cart',
|
|
|
|
index: this.state.columns.cart.items.length,
|
|
|
|
};
|
|
|
|
|
|
|
|
this.handleOnDragEnd({
|
|
|
|
source,
|
|
|
|
destination,
|
|
|
|
draggableId: null,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
handleToggleItemProgress(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, i) => {
|
|
|
|
return {
|
|
|
|
...item,
|
|
|
|
showProgress: i === index ? show : false,
|
|
|
|
showOverlayRemove: false,
|
|
|
|
showWarning: false,
|
|
|
|
};
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
handleToggleItemWarning(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, i) => {
|
|
|
|
return {
|
|
|
|
...item,
|
|
|
|
showWarning: i === index ? show : false,
|
|
|
|
showProgress: false,
|
|
|
|
showOverlayRemove: false,
|
|
|
|
};
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
showProgress: false,
|
|
|
|
showWarning: false,
|
|
|
|
};
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
handleClickSubmit(note) {
|
|
|
|
const crate = {
|
|
|
|
items: [],
|
|
|
|
type: this.state.currentMode,
|
|
|
|
};
|
|
|
|
const clonedCart = Array.from(this.state.columns.cart.items);
|
|
|
|
for (const i in clonedCart) {
|
|
|
|
const item = clonedCart[i];
|
|
|
|
crate.items.push({
|
|
|
|
'name': item.name,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
const a = document.createElement('a');
|
|
|
|
const num = (new Date()).getTime();
|
|
|
|
const subject = `[Order hardware] - Request Quote`;
|
|
|
|
let body = `Hello!\n\nI would like to request a quotation for my below configuration:\n\n${JSON.stringify(crate)}\n\n`;
|
|
|
|
|
|
|
|
if (note) {
|
|
|
|
body = `${body}\n\nAdditional note:\n\n${note ? note.trim() : ''}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
document.body.appendChild(a);
|
|
|
|
|
|
|
|
a.style = 'display: none';
|
|
|
|
a.href = `mailto:sales@m-labs.hk?subject=${subject}&body=${encodeURIComponent(body)}`;
|
|
|
|
a.click();
|
|
|
|
}
|
|
|
|
|
|
|
|
handleOnDragEnd(result) {
|
|
|
|
const {
|
|
|
|
source,
|
|
|
|
destination,
|
|
|
|
draggableId,
|
|
|
|
} = result;
|
|
|
|
|
|
|
|
if (!destination) {
|
|
|
|
if (source.droppableId === 'cart') {
|
|
|
|
this.setState({
|
|
|
|
...this.state,
|
|
|
|
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':
|
|
|
|
this.setState({
|
|
|
|
...this.state,
|
|
|
|
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],
|
|
|
|
source,
|
|
|
|
destination,
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
|
|
|
|
case destination.droppableId:
|
|
|
|
this.setState({
|
|
|
|
...this.state,
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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};
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// check the number of EEM connectors available for all Kasli
|
|
|
|
const idxK = itemsCloned.reduce((prev, next, i) => {
|
|
|
|
if (next.type === 'kasli' || next.type === 'kasli-backplane') {
|
|
|
|
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) {
|
|
|
|
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 === 'kasli-backplane') {
|
|
|
|
rules[ddkali.rules.follow.type] = {...ddkali.rules.follow};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// check number of clock connector available
|
|
|
|
const idxC = itemsCloned.reduce((prev, next, i) => {
|
|
|
|
if (next.type === 'kasli' || next.type === 'kasli-backplane' || 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 recommanded 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;
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
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};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// update state with rules
|
|
|
|
this.setState({
|
|
|
|
...this.state,
|
|
|
|
columns: {
|
|
|
|
...this.state.columns,
|
|
|
|
cart: {
|
|
|
|
...this.state.columns.cart,
|
|
|
|
itemsData: {
|
|
|
|
...itemsData,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
},
|
|
|
|
rules: {
|
|
|
|
...rules,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
|
|
|
|
const {
|
|
|
|
currentItemHovered,
|
|
|
|
currentMode,
|
|
|
|
crateModeSlots,
|
|
|
|
crateModeItems,
|
|
|
|
items,
|
|
|
|
columns,
|
|
|
|
rules,
|
|
|
|
} = this.state;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<DragDropContext onDragEnd={this.handleOnDragEnd}>
|
|
|
|
|
|
|
|
<Layout
|
|
|
|
className="shop"
|
|
|
|
aside={
|
|
|
|
<Backlog
|
|
|
|
items={items}
|
|
|
|
data={columns['backlog']}
|
|
|
|
onClickAddItem={this.handleClickAddItem}>
|
|
|
|
</Backlog>
|
|
|
|
}
|
|
|
|
main={(
|
|
|
|
<OrderPanel
|
|
|
|
title="Order hardware"
|
|
|
|
description="
|
|
|
|
Choose your crate and drag/add the cards you want
|
|
|
|
to the crate below to see
|
|
|
|
how the combination would look like."
|
|
|
|
crateMode={
|
|
|
|
<CrateMode
|
|
|
|
items={crateModeItems}
|
|
|
|
mode={currentMode}
|
|
|
|
onClickMode={this.handleCrateModeChange}>
|
|
|
|
</CrateMode>}
|
|
|
|
crate={
|
|
|
|
<Crate
|
|
|
|
cart={
|
|
|
|
<Cart
|
|
|
|
nbrSlots={crateModeSlots[currentMode]}
|
|
|
|
data={columns['cart']}
|
|
|
|
itemHovered={currentItemHovered}
|
|
|
|
onToggleProgress={this.handleToggleItemProgress}
|
|
|
|
onToggleWarning={this.handleToggleItemWarning}
|
|
|
|
onToggleOverlayRemove={this.handleToggleOverlayRemove}
|
|
|
|
onClickRemoveItem={this.handleDeleteItem}>
|
|
|
|
</Cart>
|
|
|
|
}
|
|
|
|
rules={Object.values(rules).filter(rule => rule)}>
|
|
|
|
</Crate>
|
|
|
|
}
|
|
|
|
summaryPrice={
|
|
|
|
<OrderSumary
|
|
|
|
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
|
|
|
|
onClickSubmit={this.handleClickSubmit}>
|
|
|
|
</OrderForm>
|
|
|
|
}>
|
|
|
|
</OrderPanel>
|
|
|
|
)}>
|
|
|
|
</Layout>
|
|
|
|
|
|
|
|
</DragDropContext>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ReactDOM.render(
|
|
|
|
<Shop data={data} />,
|
|
|
|
document.querySelector('#root-shop'),
|
|
|
|
);
|