'use strict';
import React from "react";
import axios from "axios";
import { createRoot } from "react-dom/client";
import PropTypes from "prop-types";
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
import { v4 as uuidv4 } from 'uuid';
import { OptionsDialogPopup, OptionsSummaryPopup, FilterOptions, FillExtData } from "./shop_components.jsx";import { OverlayTrigger } from "react-bootstrap";
const data = window.shop_data;
const itemsUnfoldedList = Array.from(data.columns.backlog.categories.map(groupId => groupId.itemIds).flat());
const productStyle = (style, snapshot, removeAnim, hovered, selected, cart=false) => {
const custom = {
opacity: snapshot.isDragging ? .7 : 1,
backgroundColor: (hovered || selected) ? '#eae7f7' : 'initial',
};
if (!cart && snapshot.draggingOver == null && // hack for backlog
((!snapshot.isDragging) // prevent next elements from animation
|| (snapshot.isDragging && snapshot.isDropAnimating))) { // prevent dragged element from weird animation
style.transform = "none";
}
if (!snapshot.isDropAnimating) {
return { ...style, ...custom};
}
if (removeAnim) {
// cannot be 0, but make it super tiny
custom.transitionDuration = '0.001s';
}
return {
...style,
...custom,
};
}
const cartStyle = (style, snapshot) => {
const isDraggingOver = snapshot.isDraggingOver;
return {
...style,
...{
backgroundColor: isDraggingOver ? '#f2f2f2' : '#f9f9f9',
border: isDraggingOver ? '1px dashed #ccc' : '0',
},
};
}
const nbrConnectorsStyle = (data) => {
if (!data || !data.nbrCurrentSlot) {
return {};
}
let p = data.nbrCurrentSlot * 100 / data.nbrSlotMax;
if (p > 100) {
p = 100;
}
return {
width: `${p}%`,
}
};
const nbrClocksStyle = (data) => {
if (!data || !data.nbrCurrentClock) {
return {};
}
let p = data.nbrCurrentClock * 100 / data.nbrClockMax;
if (p > 100) {
p = 100;
}
return {
width: `${p}%`,
}
};
const copy = (
model,
source,
destination,
draggableSource,
droppableDestination
) => {
const destClone = Array.from(destination.items);
destClone.splice(droppableDestination.index, 0, ...draggableSource.map((dragged_item, _) => {
return {
...model[dragged_item],
id: uuidv4(),
}
}));
return destClone;
};
const reorder = (list, startIndex, endIndex) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
const remove = (list, startIndex) => {
const result = Array.from(list);
result.splice(startIndex, 1);
return result;
};
const nbrOccupiedSlotsInCrate = (items) => {
return items.reduce((prev, next) => {
return prev + (next.hp === 8 ? 2 : 1);
}, 0);
};
function formatMoney(amount, decimalCount = 2, decimal = ".", thousands = ",") {
// https://stackoverflow.com/questions/149055/how-can-i-format-numbers-as-currency-string-in-javascript
// changes: return amount if error in order to avoid empty value
try {
decimalCount = Math.abs(decimalCount);
decimalCount = isNaN(decimalCount) ? 2 : decimalCount;
const negativeSign = amount < 0 ? "-" : "";
let i = parseInt(amount = Math.abs(Number(amount) || 0).toFixed(decimalCount)).toString();
let j = (i.length > 3) ? i.length % 3 : 0;
return negativeSign + (j ? i.substr(0, j) + thousands : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + thousands) + (decimalCount ? decimal + Math.abs(amount - i).toFixed(decimalCount).slice(2) : "");
} catch (e) {
return amount;
}
};
/**
* Component that provides a base layout (aside/main) for the page.
*/
class Layout extends React.PureComponent {
static get propTypes() {
return {
aside: PropTypes.any,
main: PropTypes.any,
mobileSideMenuShouldOpen: PropTypes.bool,
isMobile: PropTypes.bool,
newCardJustAdded: PropTypes.bool,
onClickToggleMobileSideMenu: PropTypes.func,
onClickCloseRFQFeedback: PropTypes.func,
RFQBodyType: PropTypes.string,
RFQBodyOrder: PropTypes.string,
onClickLoadCustomConf: PropTypes.func,
items: PropTypes.object,
};
}
static get defaultProps() {
return {
mobileSideMenuShouldOpen: false,
};
}
constructor(props) {
super(props);
this.state = {
customconf: '',
error: null,
};
this.handleCustomConfig = this.handleCustomConfig.bind(this);
this.handleClickLoad = this.handleClickLoad.bind(this);
this.checkValidation = this.checkValidation.bind(this);
// retrieve list of available pn
const items_keys = Object.keys(props.items);
this.list_pn = items_keys.map(function (key) {
return props.items[key].name_number;
});
}
handleCustomConfig(e) {
const value = e.target.value;
this.checkValidation(value);
}
checkValidation(conf) {
let conf_obj;
try {
conf_obj = JSON.parse(conf);
} catch (e) {
return this.setState({
...this.state,
customconf: conf,
customconf_ready: null,
error: 'invalid format',
});
}
if (!conf_obj) {
return this.setState({
...this.state,
customconf: conf,
customconf_ready: null,
error: 'invalid format',
});
}
if ((!conf_obj.items || !conf_obj.type) &&
(Object.prototype.toString.call(conf_obj.items) !== '[object Array]' ||
Object.prototype.toString.call(conf_obj.type) !== '[object String]')) {
return this.setState({
...this.state,
customconf: conf,
customconf_ready: null,
error: 'invalid format',
});
}
if (conf_obj.type !== "desktop" && conf_obj.type !== "rack") {
return this.setState({
...this.state,
customconf: conf,
customconf_ready: null,
error: 'invalid format',
});
}
conf_obj.items.map(function (item) {
try {
return JSON.parse(item);
} catch (e) {
return null;
}
});
conf_obj.items = conf_obj.items.filter(function (item) {
return item;
});
if (conf_obj.items.filter(function (item) {
return Object.prototype.toString.call(item) !== '[object Object]' || !item.pn || Object.prototype.toString.call(item.pn) !== '[object String]';
}).length > 0) {
return this.setState({
...this.state,
customconf: conf,
customconf_ready: null,
error: 'invalid format',
});
}
conf_obj.items = conf_obj.items.map(function (item) {
return {
pn: item.pn,
options: item.options ? item.options : null,
};
});
const self = this;
const unknow_pn = conf_obj.items.filter(function (item_pn) {
return self.list_pn.includes(item_pn.pn) === false;
}).map(function (item_pn) {
return item_pn.pn;
});
if (unknow_pn.length > 0) {
return this.setState({
...this.state,
customconf: conf,
customconf_ready: null,
error: `${unknow_pn.join(', ')} unknown${unknow_pn.length > 1 ? 's':''} pn number`,
});
}
this.setState({
...this.state,
customconf: conf,
error: null,
customconf_ready: conf_obj,
});
}
handleClickLoad() {
this.checkValidation(this.state.customconf);
if (this.props.onClickLoadCustomConf) {
this.props.onClickLoadCustomConf(this.state.customconf_ready);
}
}
render() {
const {
aside,
main,
mobileSideMenuShouldOpen,
isMobile,
newCardJustAdded,
onClickToggleMobileSideMenu,
onClickCloseRFQFeedback,
showRFQFeedback,
RFQBodyType,
RFQBodyOrder,
} = this.props;
return (
{mobileSideMenuShouldOpen ? (
) : (
)}
{isMobile && newCardJustAdded ? (
✓ added
) : null}
{RFQBodyType === 'email' ? (
We've received your request and will be in contact soon.
) : null }
{RFQBodyType === 'show' ? (
{RFQBodyOrder}
) : null}
{RFQBodyType === 'import' ? (
) : null}
);
}
}
/**
* Component that renders a product.
* Used in the aside (e.g backlog of product)
*/
class ProductItem extends React.PureComponent {
static get propTypes() {
return {
id: PropTypes.string.isRequired,
index: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
name_codename: PropTypes.string,
price: PropTypes.number.isRequired,
currency: PropTypes.string.isRequired,
image: PropTypes.string.isRequired,
specs: PropTypes.array,
datasheet_file: PropTypes.string,
datasheet_name: PropTypes.string,
onClickAddItem: PropTypes.func,
};
}
constructor(props) {
super(props);
this.handleOnClickAddItem = this.handleOnClickAddItem.bind(this);
}
handleOnClickAddItem(id, tap, e) {
if (this.props.onClickAddItem) {
this.props.onClickAddItem(id, tap);
}
e.preventDefault();
}
render() {
const {
id,
index,
name,
name_codename,
price,
currency,
image,
specs,
datasheet_file,
datasheet_name,
} = this.props;
const render_specs = (specs && specs.length > 0 && (
{specs.map((spec, index) =>
- {spec}
)}
));
const render_datasheet_link = (datasheet_file && datasheet_name && (
));
return (
{name}
{name_codename ? (
{name_codename}
) : null }
{`${currency} ${formatMoney(price)}`}
{render_specs}
{render_datasheet_link}
{(provided, snapshot) => (
{/* Allows to simulate a clone */}
{snapshot.isDragging && (
)}
)}
);
}
}
/**
* Component that renders a product.
* Used in the crate
*/
class ProductCartItem extends React.PureComponent {
static get propTypes() {
return {
isMobile: PropTypes.bool,
isTouch: PropTypes.bool,
hovered: PropTypes.bool,
index: PropTypes.number.isRequired,
model: PropTypes.object.isRequired,
data: PropTypes.object,
ext_data: PropTypes.object,
onToggleOverlayRemove: PropTypes.func,
onClickRemoveItem: PropTypes.func,
onClickItem: PropTypes.func,
onCardUpdate: PropTypes.func,
};
}
static get defaultProps() {
return {
hovered: false,
};
}
constructor(props) {
super(props);
this.handleOnMouseEnterRemoveItem = this.handleOnMouseEnterRemoveItem.bind(this);
this.handleOnMouseLeaveRemoveItem = this.handleOnMouseLeaveRemoveItem.bind(this);
this.handleOnClickRemoveItem = this.handleOnClickRemoveItem.bind(this);
this.handleOnClickItem = this.handleOnClickItem.bind(this);
}
handleOnMouseEnterRemoveItem(index, e) {
if (this.props.onToggleOverlayRemove && !this.props.isMobile) {
this.props.onToggleOverlayRemove(index, true);
}
e.preventDefault();
}
handleOnMouseLeaveRemoveItem(index, e) {
if (this.props.onToggleOverlayRemove && !this.props.isMobile) {
this.props.onToggleOverlayRemove(index, false);
}
e.preventDefault();
}
handleOnClickItem(index, e) {
if (this.props.onClickItem && this.props.isTouch) {
this.props.onClickItem(index);
}
e.preventDefault();
}
handleOnClickRemoveItem(index, e) {
if (this.props.onClickRemoveItem) {
this.props.onClickRemoveItem(index);
}
}
render() {
const {
hovered,
model,
data,
index,
ext_data,
onCardUpdate,
} = this.props;
let warning, options, options_data;
if (data && data.warnings) {
const warningsKeys = Object.keys(data.warnings);
if (warningsKeys && warningsKeys.length > 0) {
// we display only the first warning
warning = data.warnings[warningsKeys[0]];
}
}
if (data && data.options) {
options = data.options;
if (!data.options_data) data.options_data = {};
options_data = data.options_data;
options_data.ext_data = ext_data;
}
let render_progress;
if (data) {
switch(model.type) {
case 'kasli':
render_progress = (
{`${data.nbrCurrentSlot}/${model.nbrSlotMax} EEM connectors used`}
{`${data.nbrCurrentClock}/${model.nbrClockMax} Clock connectors used`}
);
break;
case 'vhdcicarrier':
render_progress = (
{`${data.nbrCurrentSlot}/${model.nbrSlotMax} EEM connectors used`}
);
break;
case 'zotino':
case 'hd68':
render_progress = (
{`${data.nbrCurrentSlot}/${model.nbrSlotMax} connectors used`}
);
break;
case 'clocker':
render_progress = (
{`${data.nbrCurrentClock}/${model.nbrClockMax} Clock connectors used`}
);
break;
default:
break;
}
}
return (
{(provided, snapshot) => (
{/* warning container */}
{warning.message}
) : null
}
rootClose
>
{warning && (
)}
{warning ? (
) : (options ? (
{
// console.log("construct", outvar, value, options_data);
options_data[outvar] = value;
}),
update: ((outvar, value) => {
// console.log("update", outvar, value, options_data);
if (outvar in options_data) options_data[outvar] = value;
onCardUpdate();
})
}}
/>) : null) }
{warning && model.showWarning && (
)}
{model.name_number}
{/* remove container */}
Remove
{/* progression container */}
{render_progress && (
{model.nbrSlotMax > 0 && (
)}
{model.nbrClockMax > 0 && (
)}
)}
)}
);
}
}
/**
* 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(
);
}
return (
{fakePlaceholder}
);
}
}
/**
* Component that displays a list of
*/
class Cart extends React.PureComponent {
static get propTypes() {
return {
isMobile: PropTypes.bool,
isTouch: PropTypes.bool,
nbrSlots: PropTypes.number,
itemHovered: PropTypes.string,
data: PropTypes.object.isRequired,
onToggleOverlayRemove: PropTypes.func,
onClickRemoveItem: PropTypes.func,
onCardUpdate: PropTypes.func,
onClickItem: PropTypes.func,
};
}
render() {
const {
isMobile,
isTouch,
nbrSlots,
itemHovered,
data,
onToggleOverlayRemove,
onClickRemoveItem,
onClickItem,
onCardUpdate,
} = this.props;
const products = data.items.map((item, index) => {
let itemData;
let ext_data = FillExtData(data.itemsData, index);
if (data.itemsData && index in data.itemsData) {
itemData = data.itemsData[index];
}
return (
);
});
return (
{(provided, snapshot) => (
{products}
{provided.placeholder && (
{provided.placeholder}
)}
)}
);
}
}
/**
* 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 (
);
}
}
/**
* Component that displays the main crate with reminder rules.
* It includes and rules
*/
class Crate extends React.PureComponent {
static get propTypes() {
return {
rules: PropTypes.array,
cart: PropTypes.element,
};
}
render() {
const {
rules,
cart,
} = this.props;
return (
{cart}
{rules && rules.length > 0 && (
{rules.map((rule, index) => (
{rule.name}: {rule.message}
))}
)}
);
}
}
/**
* Component that renders all things for order.
* It acts like-a layout, this component do nothing more.
*/
class OrderPanel extends React.PureComponent {
static get propTypes() {
return {
title: PropTypes.string,
description: PropTypes.string,
crateMode: PropTypes.element,
crate: PropTypes.element,
summaryPrice: PropTypes.element,
form: PropTypes.element,
isMobile: PropTypes.bool,
onClickToggleMobileSideMenu: PropTypes.func,
onClickOpenImport: PropTypes.func,
};
}
render() {
const {
title,
description,
crateMode,
crate,
summaryPrice,
form,
isMobile,
onClickToggleMobileSideMenu,
onClickOpenImport,
} = this.props;
return (
{title}
{description}
{crateMode}
{isMobile ? (
) : null}
{crate}
);
}
}
/**
* Components that renders the form to request quote.
*/
class OrderForm extends React.PureComponent {
static get propTypes() {
return {
isProcessing: PropTypes.bool,
isProcessingComplete: PropTypes.bool,
onClickSubmit: PropTypes.func,
};
}
constructor(props) {
super(props);
this.state = {
note: '',
email: '',
error: {
note: null,
email: null,
},
empty: {
note: null,
email: null,
},
};
this.handleEmail = this.handleEmail.bind(this);
this.handleNote = this.handleNote.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.resetEmptyError = this.resetEmptyError.bind(this);
this.checkValidation = this.checkValidation.bind(this);
}
checkValidation() {
let isValid = true;
let validationFields = {...this.state};
const {
isEmpty: isEmailEmpty,
isError: isEmailError
} = this.validateEmail(this.state.email);
validationFields = {
...validationFields,
error: {
...this.state.error,
email: isEmailError,
},
empty: {
...this.state.empty,
email: isEmailEmpty,
}
}
this.setState(validationFields);
isValid =
!isEmailEmpty &&
!isEmailError
return isValid;
}
validateEmail(value) {
let isEmpty = null;
let isError = null;
const { t } = this.props;
if (!value || value.trim() === '') {
isEmpty = true;
} else if (value && !value.match(/^\w+([\+\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/)) {
isError = {
message: 'Your email is incomplete',
};
}
return { isEmpty, isError };
}
validateNote(value) {
let isEmpty = null;
if (!value || value.trim() === '') {
isEmpty = true;
}
return { isEmpty };
}
resetEmptyError(key) {
this.setState({
...this.state,
error: {
...this.state.error,
[key]: null,
},
empty: {
...this.state.empty,
[key]: null,
},
});
}
handleEmail(e) {
const value = e.target.value;
const { isEmpty, isError } = this.validateEmail(value);
this.setState({
...this.state,
email: value,
error: {
...this.state.error,
email: isError,
},
empty: {
...this.state.empty,
email: isEmpty,
}
});
}
handleNote(e) {
const value = e.target.value;
this.setState({
...this.state,
note: value,
});
}
handleSubmit(event) {
event.preventDefault();
if (this.props.onClickSubmit) {
// check validation input fields
const isValidated = this.checkValidation();
if (!isValidated) {
return false;
}
this.props.onClickSubmit(this.state.note, this.state.email);
}
}
render() {
const {
handleEmail,
handleNote,
resetEmptyError,
handleSubmit,
} = this;
const {
onClickShow,
} = this.props;
const {
email,
note,
error,
empty
} = this.state;
const { isProcessing, isProcessingComplete } = this.props;
return (
);
}
}
/**
* 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 (
Remove all cards
|
{mode && (
{mode.name} |
{`${currency} ${formatMoney(mode.price)}`}
|
)}
{summary.map((item, index) => {
let alert, warning, options, options_data;
if (itemsData[index] && itemsData[index].warnings) {
alert = itemsData[index];
const warningsKeys = Object.keys(alert.warnings);
if (warningsKeys && warningsKeys.length > 0) {
warning = alert.warnings[warningsKeys[0]];
}
}
options = itemsData[index] && itemsData[index].options;
options_data = itemsData[index] && itemsData[index].options_data;
return (
{`${item.name_number} ${item.name} ${item.name_codename}`}
|
{`${currency} ${formatMoney(item.price)}`}
{warning ? (
) : ( (options && options_data) ?
( ) : null
)}
{(!warning && !options) && (
)}
|
);
})}
Price estimate |
{summary.length ? (
`${currency} ${formatMoney(summary.reduce(
(prev, next) => {
return prev + next.price;
}, 0
) + mode.price)}`
) : (
`${currency} ${formatMoney(mode.price)}`
)}
|
);
}
}
/**
* Component that renders the backlog in the aside
*/
class Backlog extends React.PureComponent {
static get propTypes() {
return {
currency: PropTypes.string,
data: PropTypes.object.isRequired,
items: PropTypes.object,
isMobile: PropTypes.bool,
onClickAddItem: PropTypes.func,
onClickToggleMobileSideMenu: PropTypes.func,
};
}
static get defaultProps() {
return {
items: {},
};
}
render() {
const {
currency,
data,
items,
onClickAddItem,
onClickToggleMobileSideMenu,
isMobile,
} = this.props;
const ordered_groups = data.categories.map(groupItem => ({ name: groupItem.name,
items: groupItem.itemIds.map(itemId => items[itemId])
}));
let item_index = -1;
const groups = ordered_groups.map((group, g_index) => {
return (
{group.items.map(item => {
item_index++;
return (
)})}
);
}
);
return (
{(provided) => (
{isMobile ? (
) : null}
{groups}
{provided.placeholder && (
{provided.placeholder}
)}
)}
);
}
}
/**
* Component that render the entire shop
*/
class Shop extends React.PureComponent {
static get propTypes() {
return {
data: PropTypes.object.isRequired,
};
}
constructor(props) {
super(props);
this.state = this.props.data;
this.handleCrateModeChange = this.handleCrateModeChange.bind(this);
this.handleOnDragEnd = this.handleOnDragEnd.bind(this);
this.handleDeleteItem = this.handleDeleteItem.bind(this);
this.handleDeleteAllItems = this.handleDeleteAllItems.bind(this);
this.handleMouseEnterItem = this.handleMouseEnterItem.bind(this);
this.handleMouseLeaveItem = this.handleMouseLeaveItem.bind(this);
this.handleClickAddItem = this.handleClickAddItem.bind(this);
this.checkAlerts = this.checkAlerts.bind(this);
this.handleClickSelectItem = this.handleClickSelectItem.bind(this);
this.handleClickSubmit = this.handleClickSubmit.bind(this);
this.handleToggleOverlayRemove = this.handleToggleOverlayRemove.bind(this);
this.handleShowOverlayRemove = this.handleShowOverlayRemove.bind(this);
this.handleClickToggleMobileSideMenu = this.handleClickToggleMobileSideMenu.bind(this);
this.handleClickCloseRFQFeedback = this.handleClickCloseRFQFeedback.bind(this);
this.handleClickShowOrder = this.handleClickShowOrder.bind(this);
this.handleClickOpenImport = this.handleClickOpenImport.bind(this);
this.handleLoadCustomConf = this.handleLoadCustomConf.bind(this);
this.handleCardsUpdated = this.handleCardsUpdated.bind(this);
this.timer = null;
this.timer_remove = null;
}
componentDidMount() {
const source = {
droppableId: 'backlog',
indexes: [
itemsUnfoldedList.findIndex(element => element === "eem_pwr_mod"),
itemsUnfoldedList.findIndex(element => element === "kasli")
],
};
const destination = {
droppableId: 'cart',
index: 0,
};
this.handleOnDragEnd({
source,
destination
});
}
componentDidUpdate(prevProps, prevState) {
/**
* We check alerts (reminder + warning) only when items inside crate or
* crate mode change.
*
* In the function checkAlerts, we DO NOT want to change items as we will
* trigger again this function (componentDidUpdate) and thus,
* making an infinite loop.
*/
if (
(prevState.columns.cart.items !== this.state.columns.cart.items) ||
(prevState.currentMode !== this.state.currentMode)
) {
this.checkAlerts(this.state.columns.cart.items);
}
if (this.state.newCardJustAdded) {
this.timer = setTimeout(() => {
this.setState({
newCardJustAdded: false,
});
}, 2000);
}
}
componentWillUnmount() {
clearTimeout(this.timer);
}
handleCardsUpdated() {
this.checkAlerts(this.state.columns.cart.items);
}
handleCrateModeChange(mode) {
this.setState({
currentMode: mode,
});
}
handleDeleteItem(index) {
let cloned = Array.from(this.state.columns.cart.items);
let cloned_data = Array.from(this.state.columns.cart.itemsData);
cloned.splice(index, 1);
cloned_data.splice(index, 1);
this.setState({
...this.state,
columns: {
...this.state.columns,
cart: {
...this.state.columns.cart,
items: cloned,
itemsData: cloned_data,
},
},
});
}
handleDeleteAllItems() {
this.setState({
...this.state,
columns: {
...this.state.columns,
cart: {
...this.state.columns.cart,
items: [],
itemsData: []
},
},
});
}
handleMouseEnterItem(id) {
this.setState({
...this.state,
currentItemHovered: id,
});
}
handleMouseLeaveItem() {
this.setState({
...this.state,
currentItemHovered: null,
});
}
handleClickAddItem(index, tap) {
const source = {
droppableId: 'backlog',
index: index,
};
const destination = {
droppableId: 'cart',
index: this.state.columns.cart.items.length,
};
this.handleOnDragEnd({
source,
destination
}, tap);
}
handleClickSelectItem(index) {
const itemsCloned = Array.from(this.state.columns.cart.items);
this.setState({
...this.state,
columns: {
...this.state.columns,
cart: {
...this.state.columns.cart,
items: itemsCloned.map((item, id) => {
return {...item, selected: id === index ? true : false};
}),
}
},
});
}
handleToggleOverlayRemove(index, show) {
const itemsCloned = Array.from(this.state.columns.cart.items);
this.setState({
...this.state,
columns: {
...this.state.columns,
cart: {
...this.state.columns.cart,
items: itemsCloned.map((item, id) => {
return {
...item,
showOverlayRemove: id === index ? show : false
};
}),
}
},
});
}
handleShowOverlayRemove(index) {
if (this.timer_remove)
clearTimeout(this.timer_remove);
this.handleToggleOverlayRemove(index, true);
this.timer_remove = setTimeout(() => {
this.handleToggleOverlayRemove(index, false);
}, 2000);
}
handleClickShowOrder() {
const crate = {
items: [],
type: this.state.currentMode,
};
const clonedCart = Array.from(this.state.columns.cart.items);
const clonedCartData = Array.from(this.state.columns.cart.itemsData);
for (const i in clonedCart) {
const item = clonedCart[i];
const item_data = clonedCartData[i];
crate.items.push({
'pn': item.name_number,
'options': (item_data.options_data && item_data.options) ? FilterOptions(item_data.options, item_data.options_data) : null,
});
}
this.setState({
isProcessing: false,
shouldShowRFQFeedback: true,
RFQBodyType: 'show',
RFQBodyOrder: JSON.stringify(crate, null, 2),
});
}
handleClickOpenImport() {
this.setState({
isProcessing: false,
shouldShowRFQFeedback: true,
RFQBodyType: 'import',
});
}
handleLoadCustomConf(customconf) {
if (!customconf) {return; }
const items = this.props.data.items;
let new_items = [];
let new_items_data = [];
this.setState({
...this.state,
columns: {
...this.state.columns,
cart: {
...this.state.columns.cart,
items: [],
},
},
}, function () {
customconf.items.map(function (item) {
Object.keys(items).map(key => {
if (item.pn && item.pn === items[key].name_number) {
new_items.push(Object.assign({
...items[key],
}, {
id: uuidv4(),
options_data: item.options ? item.options : null,
}));
new_items_data.push({options_data: item.options ? item.options : null});
}
});
return item;
});
this.setState({
...this.state,
columns: {
...this.state.columns,
cart: {
...this.state.columns.cart,
items: new_items,
itemsData: new_items_data,
},
},
currentMode: customconf.type,
});
});
}
handleClickSubmit(note, email) {
const crate = {
items: [],
type: this.state.currentMode,
};
const clonedCart = Array.from(this.state.columns.cart.items);
const clonedCartData = Array.from(this.state.columns.cart.itemsData);
for (const i in clonedCart) {
const item = clonedCart[i];
const item_data = clonedCartData[i];
crate.items.push({
'pn': item.name_number,
'options': (item_data.options_data && item_data.options) ? FilterOptions(item_data.options, item_data.options_data) : null,
});
}
const {data} = this.props;
this.setState({isProcessing: true});
axios.post(data.API_RFQ, {
email,
note,
configuration: JSON.stringify(crate)
}).then(response => {
this.setState({
isProcessing: false,
shouldShowRFQFeedback: true,
RFQBodyType: 'email',
isProcessingComplete: true,
});
}).catch(err => {
this.setState({isProcessing: false}, () => {
alert("We cannot receive your request. Try using the export by coping the configuration and send it to us at sales[at]m-labs.hk");
});
})
}
handleOnDragEnd(result, newAdded) {
const {
source,
destination,
} = result;
let dragged_items = [];
if (source.indexes) {
source.indexes.forEach((card_index, _) => {
dragged_items.push(itemsUnfoldedList[card_index]);
})
} else if (source.index >= 0) {
dragged_items.push(itemsUnfoldedList[source.index]);
}
if (!destination) {
if (source.droppableId === 'cart') {
this.setState({
...this.state,
newCardJustAdded: false,
columns: {
...this.state.columns,
[source.droppableId]: {
...this.state.columns[source.droppableId],
items: remove(
this.state.columns[source.droppableId].items,
source.index,
),
itemsData: remove(
this.state.columns[source.droppableId].itemsData,
source.index,
)
},
},
});
}
return;
}
switch(source.droppableId) {
case 'backlog':
if (source.droppableId !== destination.droppableId) {
this.setState({
...this.state,
newCardJustAdded: newAdded ? true : false,
columns: {
...this.state.columns,
[destination.droppableId]: {
...this.state.columns[destination.droppableId],
items: copy(
this.state.items,
this.state.columns[source.droppableId],
this.state.columns[destination.droppableId],
dragged_items,
destination,
),
},
},
});
}
break;
case destination.droppableId:
this.setState({
...this.state,
newCardJustAdded: false,
columns: {
...this.state.columns,
[destination.droppableId]: {
...this.state.columns[destination.droppableId],
items: reorder(
this.state.columns[destination.droppableId].items,
source.index,
destination.index,
),
itemsData: reorder(
this.state.columns[destination.droppableId].itemsData,
source.index,
destination.index,
),
},
},
});
break;
default:
break;
}
}
handleClickToggleMobileSideMenu() {
this.setState({
...this.state,
mobileSideMenuShouldOpen: !this.state.mobileSideMenuShouldOpen,
});
}
handleClickCloseRFQFeedback() {
this.setState({
shouldShowRFQFeedback: false,
});
}
checkAlerts(newItems) {
console.log('--- START CHECKING CRATE WARNING ---');
const {
currentMode,
crateModeSlots,
crateRules,
} = this.state;
const itemsCloned = Array.from(newItems);
const itemsData = [];
const rules = {};
itemsCloned.forEach((elem, idx) => {
if (!(idx in itemsData)) itemsData[idx] = elem;
if (idx in this.state.columns.cart.itemsData && this.state.columns.cart.itemsData[idx].options_data) {
itemsCloned[idx].options_data = this.state.columns.cart.itemsData[idx].options_data;
}
itemsData[idx].warnings = {};
});
// check number of slot in crate
const nbrOccupied = nbrOccupiedSlotsInCrate(newItems);
if (nbrOccupied > crateModeSlots[currentMode]) {
rules[crateRules.maxSlot.type] = {...crateRules.maxSlot};
} else if (crateModeSlots[currentMode] === 21 && nbrOccupied <= 10) {
rules[crateRules.compactSlot.type] = {...crateRules.compactSlot};
}
// check the number of EEM connectors available for all Kasli
const idxK = itemsCloned.reduce((prev, next, i) => {
if (next.type === 'kasli' || next.type === 'vhdcicarrier') {
prev.push(i);
}
return prev;
}, []);
for (let i = 0; i <= idxK.length - 1; i++) {
let slots;
let nbUsedSlot = 0;
let nbrCurrentClock = 0;
let idx = idxK[i];
if (i !== idxK.length - 1) {
slots = itemsCloned.slice(idx + 1, idxK[i + 1]);
} else {
slots = itemsCloned.slice(idx + 1);
}
if (i == 0) {
const slots_need_resource = itemsCloned.slice(0, idx);
const idx_need = slots_need_resource.findIndex(e => (e.rules && e.rules.resources));
if (idx_need != -1) {
if (idx_need in itemsData) {
if ('warnings' in itemsData[idx_need]) {
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
} else {
itemsData[idx_need].warnings = {};
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
}
} else {
itemsData[idx_need] = {...itemsCloned[idx_need]};
itemsData[idx_need].warnings = {};
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
}
}
}
const process_slots = (item) => {
if (!item.options_data
|| item.options_data.ext_pwr === false
|| item.options_data.mono_eem === false
)
return item.slotOccupied;
else if (item.options_data.ext_pwr === true)
return 0;
else if (item.options_data.mono_eem === true || item.options_data.n_eem === "1 EEM")
return 1;
else if (item.options_data.n_eem === "3 EEM")
return 3;
return item.slotOccupied;
}
nbUsedSlot = slots
.filter(item => item.type !== 'idc-bnc')
.reduce((prev, next) => {
return prev + process_slots(next);
}, 0);
nbrCurrentClock = slots
.reduce((prev, next) => {
return next.type === 'clocker' ? prev + ((next.options_data && next.options_data.ext_clk === true) ? 0 : next.clockOccupied) : prev;
}, 0);
if (idx in itemsData) {
itemsData[idx].nbrCurrentSlot = nbUsedSlot;
itemsData[idx].nbrCurrentClock = nbrCurrentClock;
if (!('warnings' in itemsData[idx])) {
itemsData[idx].warnings = {};
}
} else {
itemsData[idx] = {...itemsCloned[idx]};
itemsData[idx].nbrCurrentSlot = nbUsedSlot;
itemsData[idx].nbrCurrentClock = nbrCurrentClock;
itemsData[idx].warnings = {};
}
if (nbUsedSlot > itemsCloned[idx].nbrSlotMax) {
if (itemsCloned[idx].rules.maxSlot.message) {
rules[itemsCloned[idx].rules.maxSlot.type] = {...itemsCloned[idx].rules.maxSlot};
}
itemsData[idx].warnings.maxSlotWarning = {...itemsCloned[idx].rules.maxSlotWarning};
}
if (nbrCurrentClock > itemsCloned[idx].nbrClockMax) {
rules[itemsCloned[idx].rules.maxClock.type] = {...itemsCloned[idx].rules.maxClock};
itemsData[idx].warnings.maxClockWarning = {...itemsCloned[idx].rules.maxClockWarning};
}
if (itemsCloned.length > (idx + 1)) {
const ddkali = itemsCloned[idx + 1];
if (ddkali.type === 'kasli' || ddkali.type === 'vhdcicarrier') {
rules[ddkali.rules.follow.type] = {...ddkali.rules.follow};
}
}
}
if (idxK.length === 0) {
const slots_need_resource = itemsCloned.slice(0);
const idx_need = slots_need_resource.findIndex(e => (e.rules && e.rules.resources));
if (idx_need != -1) {
if (idx_need in itemsData) {
if ('warnings' in itemsData[idx_need]) {
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
} else {
itemsData[idx_need].warnings = {};
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
}
} else {
itemsData[idx_need] = {...itemsCloned[idx_need]};
itemsData[idx_need].warnings = {};
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
}
}
}
// check number of clock connector available
const idxC = itemsCloned.reduce((prev, next, i) => {
if (next.type === 'kasli' || next.type === 'clocker') {
prev.push(i);
}
return prev;
}, []);
for (let i = 0; i <= idxC.length - 1; i++) {
let slots;
let nbrCurrentClock = 0;
let idx = idxC[i];
if (i !== idxC.length - 1) {
slots = itemsCloned.slice(idx + 1, idxC[i + 1]);
} else {
slots = itemsCloned.slice(idx + 1);
}
nbrCurrentClock = slots.reduce((prev, next) => {
return prev + ((next.options_data && next.options_data.ext_clk && next.options_data.ext_clk.checked) ? 0 : next.clockOccupied);
}, 0);
if (idx in itemsData) {
if (itemsData[idx].nbrCurrentClock && itemsData[idx].type !== "clocker") {
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};
}
}
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,
RFQBodyOrder,
isProcessingComplete,
} = this.state;
const isMobile = window.deviceIsMobile();
const isTouch = window.isTouchEnabled();
return (
}
main={(
}
crate={
onClickRemoveItem={this.handleDeleteItem}
onCardUpdate={this.handleCardsUpdated}>
}
rules={Object.values(rules).filter(rule => rule)}>
}
summaryPrice={
}
form={
}>
)}>
);
}
}
createRoot(document.querySelector('#root-shop')).render();