forked from M-Labs/web2019
2124 lines
56 KiB
JavaScript
2124 lines
56 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
DragDropContext,
|
|
Draggable,
|
|
Droppable
|
|
} = window.ReactBeautifulDnd;
|
|
|
|
const data = window.shop_data;
|
|
|
|
const axios = window.axios;
|
|
|
|
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);
|
|
};
|
|
|
|
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,
|
|
};
|
|
}
|
|
|
|
static get defaultProps() {
|
|
return {
|
|
mobileSideMenuShouldOpen: false,
|
|
};
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
aside,
|
|
main,
|
|
mobileSideMenuShouldOpen,
|
|
isMobile,
|
|
newCardJustAdded,
|
|
onClickToggleMobileSideMenu,
|
|
onClickCloseRFQFeedback,
|
|
showRFQFeedback,
|
|
RFQBodyType,
|
|
} = 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 class={`modal fade ${ showRFQFeedback ? 'show': ''}`} style={{'display': showRFQFeedback ? 'block':'none'}} id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-body rfqFeedback">
|
|
|
|
{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>
|
|
<button onClick={onClickCloseRFQFeedback}>
|
|
<img src="/images/shop/icon-close.svg" alt="close" />
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
) : null }
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div 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,
|
|
onClickAddItem: PropTypes.func,
|
|
};
|
|
}
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
this.handleOnClickAddItem = this.handleOnClickAddItem.bind(this);
|
|
}
|
|
|
|
handleOnClickAddItem(index, tap, e) {
|
|
if (this.props.onClickAddItem) {
|
|
this.props.onClickAddItem(index, tap);
|
|
}
|
|
e.preventDefault();
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
id,
|
|
index,
|
|
name,
|
|
name_codename,
|
|
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 style={{ 'margin-bottom': name_codename ? '5px' : '20px'}}>{name}</h3>
|
|
{name_codename ? (
|
|
<p>{name_codename}</p>
|
|
) : null }
|
|
|
|
<div className="price">{`${currency} ${formatMoney(price)}`}</div>
|
|
|
|
{render_specs}
|
|
</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,
|
|
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.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();
|
|
}
|
|
|
|
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 used`}</p>
|
|
<p>{`${data.nbrCurrentClock}/${model.nbrClockMax} Clock 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,
|
|
)}}
|
|
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_number}</h6>
|
|
|
|
<div
|
|
style={{'height': '350px'}}
|
|
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 {
|
|
isMobile: PropTypes.bool,
|
|
nbrSlots: PropTypes.number,
|
|
itemHovered: PropTypes.string,
|
|
data: PropTypes.object.isRequired,
|
|
onToggleProgress: PropTypes.func,
|
|
onToggleWarning: PropTypes.func,
|
|
onToggleOverlayRemove: PropTypes.func,
|
|
onClickRemoveItem: PropTypes.func,
|
|
};
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
isMobile,
|
|
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
|
|
isMobile={isMobile}
|
|
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) => (
|
|
<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,
|
|
};
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
title,
|
|
description,
|
|
crateMode,
|
|
crate,
|
|
summaryPrice,
|
|
form,
|
|
isMobile,
|
|
onClickToggleMobileSideMenu
|
|
} = this.props;
|
|
|
|
return (
|
|
<section className="panel">
|
|
|
|
<h2>{title}</h2>
|
|
|
|
<div className="control">
|
|
<p className="description">{description}</p>
|
|
|
|
{crateMode}
|
|
</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 {
|
|
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}
|
|
value={note}
|
|
rows="5"
|
|
placeholder="Additional notes" />
|
|
|
|
<input style={{'backgroundColor': `${isProcessingComplete ? 'gray':'#715ec7'}`}} disabled={isProcessingComplete} type="submit" value={`${isProcessing ? 'Processing ...' : 'Request quote'}`} />
|
|
{/*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;
|
|
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_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}`}
|
|
/>
|
|
)}
|
|
|
|
{!warning && (
|
|
<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_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_number} ${item.name}`}
|
|
name_codename={item.name_codename}
|
|
price={item.price}
|
|
currency={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}>
|
|
|
|
{isMobile ? (
|
|
<div className="mobileCloseMenu">
|
|
<button onClick={onClickToggleMobileSideMenu}>
|
|
<img src="/images/shop/icon-close-white.svg" alt="add" />
|
|
</button>
|
|
</div>
|
|
) : null}
|
|
|
|
{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);
|
|
this.handleClickToggleMobileSideMenu = this.handleClickToggleMobileSideMenu.bind(this);
|
|
this.handleClickCloseRFQFeedback = this.handleClickCloseRFQFeedback.bind(this);
|
|
|
|
this.timer = null;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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,
|
|
draggableId: null
|
|
}, tap);
|
|
}
|
|
|
|
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, email) {
|
|
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({
|
|
'pn': item.name_number,
|
|
});
|
|
}
|
|
|
|
const {data} = this.props;
|
|
|
|
const a = document.createElement('a');
|
|
const num = (new Date()).getTime();
|
|
const subject = `[Order hardware] - Request Quote`;
|
|
let body = `Hello!<br><br>I would like to request a quotation for my below configuration:<br><br>${JSON.stringify(crate)}<br><br>(Please do not edit the machine-readable representation above)<br><br>`;
|
|
|
|
if (note) {
|
|
body = `${body}<br><br>Additional note:<br><br>${note ? note.trim() : ''}`;
|
|
}
|
|
|
|
this.setState({isProcessing: true});
|
|
|
|
axios.post(data.API_RFQ, {
|
|
email,
|
|
body,
|
|
headers: {'X-MLABS-OH': 'rlebcleu'}
|
|
}).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");
|
|
});
|
|
})
|
|
|
|
return;
|
|
|
|
document.body.appendChild(a);
|
|
|
|
a.style = 'display: none';
|
|
a.href = `mailto:sales@m-labs.hk?subject=${subject}&body=${encodeURIComponent(body)}`;
|
|
a.click();
|
|
}
|
|
|
|
handleOnDragEnd(result, newAdded) {
|
|
const {
|
|
source,
|
|
destination,
|
|
draggableId,
|
|
} = result;
|
|
|
|
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':
|
|
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],
|
|
source,
|
|
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 === '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) {
|
|
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 === 'kasli-backplane') {
|
|
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 === '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;
|
|
});
|
|
|
|
|
|
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};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// 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,
|
|
isProcessingComplete,
|
|
} = this.state;
|
|
|
|
const isMobile = window.deviceIsMobile();
|
|
|
|
return (
|
|
<DragDropContext onDragEnd={this.handleOnDragEnd}>
|
|
|
|
<Layout
|
|
showRFQFeedback={shouldShowRFQFeedback}
|
|
RFQBodyType={RFQBodyType}
|
|
className="shop"
|
|
mobileSideMenuShouldOpen={mobileSideMenuShouldOpen}
|
|
isMobile={isMobile}
|
|
newCardJustAdded={newCardJustAdded}
|
|
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
|
|
onClickCloseRFQFeedback={this.handleClickCloseRFQFeedback}
|
|
aside={
|
|
<Backlog
|
|
currency={currency}
|
|
items={items}
|
|
data={columns['backlog']}
|
|
onClickAddItem={this.handleClickAddItem}
|
|
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
|
|
isMobile={isMobile}>
|
|
</Backlog>
|
|
}
|
|
main={(
|
|
<OrderPanel
|
|
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
|
|
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}
|
|
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
|
|
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}>
|
|
</OrderForm>
|
|
}>
|
|
</OrderPanel>
|
|
)}>
|
|
</Layout>
|
|
|
|
</DragDropContext>
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
ReactDOM.render(
|
|
<Shop data={data} />,
|
|
document.querySelector('#root-shop'),
|
|
);
|