You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1815 line
47KB

  1. 'use strict';
  2. const {
  3. DragDropContext,
  4. Draggable,
  5. Droppable
  6. } = window.ReactBeautifulDnd;
  7. const data = window.shop_data;
  8. const productStyle = (style, snapshot, removeAnim, hovered, selected) => {
  9. const custom = {
  10. opacity: snapshot.isDragging ? .7 : 1,
  11. backgroundColor: (hovered || selected) ? '#eae7f7' : 'initial',
  12. };
  13. if (!snapshot.isDropAnimating) {
  14. return { ...style, ...custom};
  15. }
  16. if (removeAnim) {
  17. // cannot be 0, but make it super tiny
  18. custom.transitionDuration = '0.001s';
  19. }
  20. return {
  21. ...style,
  22. ...custom,
  23. };
  24. }
  25. const cartStyle = (style, snapshot) => {
  26. const isDraggingOver = snapshot.isDraggingOver;
  27. return {
  28. ...style,
  29. ...{
  30. backgroundColor: isDraggingOver ? '#f2f2f2' : '#f9f9f9',
  31. border: isDraggingOver ? '1px dashed #ccc' : '0',
  32. },
  33. };
  34. }
  35. const nbrConnectorsStyle = (data) => {
  36. if (!data || !data.nbrCurrentSlot) {
  37. return {};
  38. }
  39. let p = data.nbrCurrentSlot * 100 / data.nbrSlotMax;
  40. if (p > 100) {
  41. p = 100;
  42. }
  43. return {
  44. width: `${p}%`,
  45. }
  46. };
  47. const nbrClocksStyle = (data) => {
  48. if (!data || !data.nbrCurrentClock) {
  49. return {};
  50. }
  51. let p = data.nbrCurrentClock * 100 / data.nbrClockMax;
  52. if (p > 100) {
  53. p = 100;
  54. }
  55. return {
  56. width: `${p}%`,
  57. }
  58. };
  59. const copy = (
  60. model,
  61. source,
  62. destination,
  63. droppableSource,
  64. droppableDestination
  65. ) => {
  66. const sourceClone = Array.from(source.itemIds);
  67. const destClone = Array.from(destination.items);
  68. const item = sourceClone[droppableSource.index];
  69. destClone.splice(droppableDestination.index, 0, {
  70. ...model[item],
  71. id: uuidv4(),
  72. });
  73. return destClone;
  74. };
  75. const reorder = (list, startIndex, endIndex) => {
  76. const result = Array.from(list);
  77. const [removed] = result.splice(startIndex, 1);
  78. result.splice(endIndex, 0, removed);
  79. return result;
  80. };
  81. const remove = (list, startIndex) => {
  82. const result = Array.from(list);
  83. result.splice(startIndex, 1);
  84. return result;
  85. };
  86. const nbrOccupiedSlotsInCrate = (items) => {
  87. return items.reduce((prev, next) => {
  88. return prev + (next.hp === 8 ? 2 : 1);
  89. }, 0);
  90. };
  91. function formatMoney(amount, decimalCount = 2, decimal = ".", thousands = ",") {
  92. // https://stackoverflow.com/questions/149055/how-can-i-format-numbers-as-currency-string-in-javascript
  93. // changes: return amount if error in order to avoid empty value
  94. try {
  95. decimalCount = Math.abs(decimalCount);
  96. decimalCount = isNaN(decimalCount) ? 2 : decimalCount;
  97. const negativeSign = amount < 0 ? "-" : "";
  98. let i = parseInt(amount = Math.abs(Number(amount) || 0).toFixed(decimalCount)).toString();
  99. let j = (i.length > 3) ? i.length % 3 : 0;
  100. 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) : "");
  101. } catch (e) {
  102. return amount;
  103. }
  104. };
  105. /**
  106. * Component that provides a base layout (aside/main) for the page.
  107. */
  108. class Layout extends React.PureComponent {
  109. static get propTypes() {
  110. return {
  111. aside: PropTypes.any,
  112. main: PropTypes.any,
  113. mobileSideMenuShouldOpen: PropTypes.bool,
  114. onClickToggleMobileSideMenu: PropTypes.func,
  115. };
  116. }
  117. static get defaultProps() {
  118. return {
  119. mobileSideMenuShouldOpen: false,
  120. };
  121. }
  122. render() {
  123. const {
  124. aside,
  125. main,
  126. mobileSideMenuShouldOpen,
  127. onClickToggleMobileSideMenu
  128. } = this.props;
  129. return (
  130. <div className="layout">
  131. <aside className={'aside ' + (mobileSideMenuShouldOpen ? 'menu-opened' : '')}>{aside}</aside>
  132. {mobileSideMenuShouldOpen ? (
  133. <section className="main" onClick={onClickToggleMobileSideMenu}>{main}</section>
  134. ) : (
  135. <section className="main">{main}</section>
  136. )}
  137. </div>
  138. );
  139. }
  140. }
  141. /**
  142. * Component that renders a product.
  143. * Used in the aside (e.g backlog of product)
  144. */
  145. class ProductItem extends React.PureComponent {
  146. static get propTypes() {
  147. return {
  148. id: PropTypes.string.isRequired,
  149. index: PropTypes.number.isRequired,
  150. name: PropTypes.string.isRequired,
  151. price: PropTypes.number.isRequired,
  152. currency: PropTypes.string.isRequired,
  153. image: PropTypes.string.isRequired,
  154. specs: PropTypes.array,
  155. onClickAddItem: PropTypes.func,
  156. };
  157. }
  158. constructor(props) {
  159. super(props);
  160. this.handleOnClickAddItem = this.handleOnClickAddItem.bind(this);
  161. }
  162. handleOnClickAddItem(index, e) {
  163. if (this.props.onClickAddItem) {
  164. this.props.onClickAddItem(index);
  165. }
  166. e.preventDefault();
  167. }
  168. render() {
  169. const {
  170. id,
  171. index,
  172. name,
  173. price,
  174. currency,
  175. image,
  176. specs,
  177. } = this.props;
  178. const render_specs = (specs && specs.length > 0 && (
  179. <ul>
  180. {specs.map((spec, index) =>
  181. <li key={index}>{spec}</li>
  182. )}
  183. </ul>
  184. ));
  185. return (
  186. <section className="productItem">
  187. <div className="content">
  188. <h3>{name}</h3>
  189. <div className="price">{`${currency} ${formatMoney(price)}`}</div>
  190. {render_specs}
  191. </div>
  192. <div className="content">
  193. <button onClick={this.handleOnClickAddItem.bind(this, index)}>
  194. <img src="/images/shop/icon-add.svg" alt="add" />
  195. </button>
  196. <Draggable draggableId={id} index={index}>
  197. {(provided, snapshot) => (
  198. <React.Fragment>
  199. <img
  200. ref={provided.innerRef}
  201. {...provided.draggableProps}
  202. {...provided.dragHandleProps}
  203. style={productStyle(
  204. provided.draggableProps.style,
  205. snapshot,
  206. true, // hack: remove weird animation after a drop
  207. )}
  208. src={image} />
  209. {/* Allows to simulate a clone */}
  210. {snapshot.isDragging && (
  211. <img className="simclone" src={image} />
  212. )}
  213. </React.Fragment>
  214. )}
  215. </Draggable>
  216. </div>
  217. </section>
  218. );
  219. }
  220. }
  221. /**
  222. * Component that renders a product.
  223. * Used in the crate
  224. */
  225. class ProductCartItem extends React.PureComponent {
  226. static get propTypes() {
  227. return {
  228. hovered: PropTypes.bool,
  229. index: PropTypes.number.isRequired,
  230. model: PropTypes.object.isRequired,
  231. data: PropTypes.object,
  232. onToggleProgress: PropTypes.func,
  233. onToggleWarning: PropTypes.func,
  234. onToggleOverlayRemove: PropTypes.func,
  235. onClickRemoveItem: PropTypes.func,
  236. };
  237. }
  238. static get defaultProps() {
  239. return {
  240. hovered: false,
  241. };
  242. }
  243. constructor(props) {
  244. super(props);
  245. this.handleOnMouseEnterItem = this.handleOnMouseEnterItem.bind(this);
  246. this.handleOnMouseLeaveItem = this.handleOnMouseLeaveItem.bind(this);
  247. this.handleOnMouseEnterWarningItem = this.handleOnMouseEnterWarningItem.bind(this);
  248. this.handleOnMouseLeaveWarningItem = this.handleOnMouseLeaveWarningItem.bind(this);
  249. this.handleOnMouseEnterRemoveItem = this.handleOnMouseEnterRemoveItem.bind(this);
  250. this.handleOnMouseLeaveRemoveItem = this.handleOnMouseLeaveRemoveItem.bind(this);
  251. this.handleOnClickRemoveItem = this.handleOnClickRemoveItem.bind(this);
  252. }
  253. handleOnMouseEnterItem(index, e) {
  254. if (this.props.onToggleProgress) {
  255. this.props.onToggleProgress(index, true);
  256. }
  257. e.preventDefault();
  258. }
  259. handleOnMouseLeaveItem(index, e) {
  260. if (this.props.onToggleProgress) {
  261. this.props.onToggleProgress(index, false);
  262. }
  263. e.preventDefault();
  264. }
  265. handleOnMouseEnterWarningItem(index, isWarning, e) {
  266. if (!isWarning) {
  267. return;
  268. }
  269. if (this.props.onToggleWarning) {
  270. this.props.onToggleWarning(index, true);
  271. }
  272. e.preventDefault();
  273. }
  274. handleOnMouseLeaveWarningItem(index, isWarning, e) {
  275. if (!isWarning) {
  276. return;
  277. }
  278. if (this.props.onToggleWarning) {
  279. this.props.onToggleWarning(index, false);
  280. }
  281. e.preventDefault();
  282. }
  283. handleOnMouseEnterRemoveItem(index, e) {
  284. if (this.props.onToggleOverlayRemove) {
  285. this.props.onToggleOverlayRemove(index, true);
  286. }
  287. e.preventDefault();
  288. }
  289. handleOnMouseLeaveRemoveItem(index, e) {
  290. if (this.props.onToggleOverlayRemove) {
  291. this.props.onToggleOverlayRemove(index, false);
  292. }
  293. e.preventDefault();
  294. }
  295. handleOnClickRemoveItem(index, e) {
  296. if (this.props.onClickRemoveItem) {
  297. this.props.onClickRemoveItem(index);
  298. }
  299. }
  300. render() {
  301. const {
  302. hovered,
  303. model,
  304. data,
  305. index,
  306. } = this.props;
  307. let warning;
  308. if (data && data.warnings) {
  309. const warningsKeys = Object.keys(data.warnings);
  310. if (warningsKeys && warningsKeys.length > 0) {
  311. // we display only the first warning
  312. warning = data.warnings[warningsKeys[0]];
  313. }
  314. }
  315. let render_progress;
  316. if (model.showProgress && data) {
  317. switch(model.type) {
  318. case 'kasli':
  319. case 'kasli-backplane':
  320. render_progress = (
  321. <div className="k-popup-connectors">
  322. <p>{`${data.nbrCurrentSlot}/${model.nbrSlotMax} EEM connectors used`}</p>
  323. <p>{`${data.nbrCurrentClock}/${model.nbrClockMax} Clock connectors used`}</p>
  324. </div>
  325. );
  326. break;
  327. case 'zotino':
  328. case 'hd68':
  329. render_progress = (
  330. <div className="k-popup-connectors">
  331. <p>{`${data.nbrCurrentSlot}/${model.nbrSlotMax} connectors used`}</p>
  332. </div>
  333. );
  334. break;
  335. case 'clocker':
  336. render_progress = (
  337. <div className="k-popup-connectors">
  338. <p>{`${data.nbrCurrentClock}/${model.nbrClockMax} Clock connectors used`}</p>
  339. </div>
  340. );
  341. break;
  342. default:
  343. break;
  344. }
  345. }
  346. return (
  347. <Draggable draggableId={model.id} index={index}>
  348. {(provided, snapshot) => (
  349. <div
  350. ref={provided.innerRef}
  351. {...provided.draggableProps}
  352. {...provided.dragHandleProps}
  353. style={{ ...productStyle(
  354. provided.draggableProps.style,
  355. snapshot,
  356. true,
  357. hovered ? true : false,
  358. model.selected ? true : false,
  359. )}}
  360. onMouseEnter={this.handleOnMouseEnterRemoveItem.bind(this, index)}
  361. onMouseLeave={this.handleOnMouseLeaveRemoveItem.bind(this, index)}
  362. >
  363. {/* warning container */}
  364. <div
  365. style={{'height': '24px'}}
  366. className="progress-container warning"
  367. onMouseEnter={this.handleOnMouseEnterWarningItem.bind(this, index, warning)}
  368. onMouseLeave={this.handleOnMouseLeaveWarningItem.bind(this, index, warning)}>
  369. {warning && (
  370. <img className="alert-warning" src={warning ? `/images${warning.icon}` : null} />
  371. )}
  372. {warning && model.showWarning && (
  373. <div className="k-popup-warning">
  374. <p className="rule warning">
  375. <i>{warning.message}</i>
  376. </p>
  377. </div>
  378. )}
  379. </div>
  380. <h6>{model.name}</h6>
  381. <div
  382. style={{'height': '250px'}}
  383. onMouseEnter={this.handleOnMouseEnterRemoveItem.bind(this, index)}
  384. >
  385. <img
  386. className='item-cart'
  387. src={`/images${model.image}`} />
  388. </div>
  389. {/* remove container */}
  390. <div
  391. style={{'display': model.showOverlayRemove ? 'flex':'none'}}
  392. className="overlayRemove"
  393. onClick={this.handleOnClickRemoveItem.bind(this, index)}>
  394. <img src="/images/shop/icon-remove.svg" alt="rm"/>
  395. <p>Remove</p>
  396. </div>
  397. {/* progression container */}
  398. <div
  399. style={{'height': '22px'}}
  400. className="progress-container"
  401. onMouseEnter={this.handleOnMouseEnterItem.bind(this, index)}
  402. onMouseLeave={this.handleOnMouseLeaveItem.bind(this, index)}>
  403. {model.nbrSlotMax > 0 && (
  404. <div className="nbr-connectors">
  405. <div style={{ ...nbrConnectorsStyle(data)}}></div>
  406. </div>
  407. )}
  408. {model.nbrClockMax > 0 && (
  409. <div className="nbr-clocks">
  410. <div style={{ ...nbrClocksStyle(data)}}></div>
  411. </div>
  412. )}
  413. {/* progress info when mouse over */}
  414. {render_progress}
  415. </div>
  416. </div>
  417. )}
  418. </Draggable>
  419. );
  420. }
  421. }
  422. /**
  423. * Component that displays a placeholder inside crate.
  424. * Allows to display how it remains space for the current crate.
  425. */
  426. class FakePlaceholder extends React.PureComponent {
  427. static get propTypes() {
  428. return {
  429. isDraggingOver: PropTypes.bool,
  430. nbrSlots: PropTypes.number.isRequired,
  431. items: PropTypes.array.isRequired,
  432. };
  433. }
  434. render() {
  435. const {
  436. isDraggingOver,
  437. nbrSlots,
  438. items,
  439. } = this.props;
  440. const fakePlaceholder = [];
  441. const nbrOccupied = nbrOccupiedSlotsInCrate(items);
  442. for (var i = (nbrSlots - nbrOccupied); i > 0; i--) {
  443. fakePlaceholder.push(
  444. <div key={i} style={{
  445. display: isDraggingOver ? 'none' : 'block',
  446. border: '1px dashed #ccc',
  447. width: '45px',
  448. marginBottom: '5px',
  449. }}></div>
  450. );
  451. }
  452. return (
  453. <React.Fragment>
  454. {fakePlaceholder}
  455. </React.Fragment>
  456. );
  457. }
  458. }
  459. /**
  460. * Component that displays a list of <ProductCartItem>
  461. */
  462. class Cart extends React.PureComponent {
  463. static get propTypes() {
  464. return {
  465. nbrSlots: PropTypes.number,
  466. itemHovered: PropTypes.string,
  467. data: PropTypes.object.isRequired,
  468. onToggleProgress: PropTypes.func,
  469. onToggleWarning: PropTypes.func,
  470. onToggleOverlayRemove: PropTypes.func,
  471. onClickRemoveItem: PropTypes.func,
  472. };
  473. }
  474. render() {
  475. const {
  476. nbrSlots,
  477. itemHovered,
  478. data,
  479. onToggleProgress,
  480. onToggleWarning,
  481. onToggleOverlayRemove,
  482. onClickRemoveItem,
  483. } = this.props;
  484. const products = data.items.map((item, index) => {
  485. let itemData;
  486. if (data.itemsData && index in data.itemsData) {
  487. itemData = data.itemsData[index];
  488. }
  489. return (
  490. <ProductCartItem
  491. hovered={item.id === itemHovered}
  492. key={item.id}
  493. index={index}
  494. data={itemData}
  495. onToggleProgress={onToggleProgress}
  496. onToggleWarning={onToggleWarning}
  497. onToggleOverlayRemove={onToggleOverlayRemove}
  498. onClickRemoveItem={onClickRemoveItem}
  499. model={item}>
  500. </ProductCartItem>
  501. );
  502. });
  503. return (
  504. <Droppable droppableId={data.id} direction="horizontal">
  505. {(provided, snapshot) => (
  506. <div
  507. ref={provided.innerRef}
  508. {...provided.droppableProps}
  509. style={cartStyle(
  510. provided.droppableProps.style,
  511. snapshot,
  512. )}
  513. className="items-cart-list">
  514. {products}
  515. {provided.placeholder && (
  516. <div style={{ display: 'none' }}>
  517. {provided.placeholder}
  518. </div>
  519. )}
  520. <FakePlaceholder
  521. nbrSlots={nbrSlots}
  522. items={data.items}
  523. isDraggingOver={snapshot.isDraggingOver} />
  524. </div>
  525. )}
  526. </Droppable>
  527. );
  528. }
  529. }
  530. /**
  531. * Component that displays crate modes
  532. */
  533. class CrateMode extends React.PureComponent {
  534. static get propTypes() {
  535. return {
  536. items: PropTypes.array.isRequired,
  537. mode: PropTypes.string.isRequired,
  538. onClickMode: PropTypes.func,
  539. };
  540. }
  541. constructor(props) {
  542. super(props);
  543. this.handleOnClickMode = this.handleOnClickMode.bind(this);
  544. }
  545. handleOnClickMode(mode, e) {
  546. if (this.props.onClickMode) {
  547. this.props.onClickMode(mode);
  548. }
  549. e.preventDefault();
  550. }
  551. render() {
  552. const {
  553. mode,
  554. items,
  555. } = this.props;
  556. return (
  557. <div className="crate-mode">
  558. {items.map(item => (
  559. <a
  560. key={item.id}
  561. className={mode == item.id ? 'active' : ''}
  562. onClick={this.handleOnClickMode.bind(this, item.id)}
  563. href="#"
  564. role="button">{item.name}</a>
  565. ))}
  566. </div>
  567. );
  568. }
  569. }
  570. /**
  571. * Component that displays the main crate with reminder rules.
  572. * It includes <Cart> and rules
  573. */
  574. class Crate extends React.PureComponent {
  575. static get propTypes() {
  576. return {
  577. rules: PropTypes.array,
  578. cart: PropTypes.element,
  579. };
  580. }
  581. render() {
  582. const {
  583. rules,
  584. cart,
  585. } = this.props;
  586. return (
  587. <div className="crate">
  588. <div className="crate-products">
  589. {cart}
  590. {rules && rules.length > 0 && (
  591. <div className="crate-info">
  592. {rules.map((rule, index) => (
  593. <p key={index} className="rule" style={{'color': rule.color ? rule.color : 'inherit'}}>
  594. <img src={`/images${rule.icon}`} /> <i><strong>{rule.name}:</strong> {rule.message}</i>
  595. </p>
  596. ))}
  597. </div>
  598. )}
  599. </div>
  600. </div>
  601. );
  602. }
  603. }
  604. /**
  605. * Component that renders all things for order.
  606. * It acts like-a layout, this component do nothing more.
  607. */
  608. class OrderPanel extends React.PureComponent {
  609. static get propTypes() {
  610. return {
  611. title: PropTypes.string,
  612. description: PropTypes.string,
  613. crateMode: PropTypes.element,
  614. crate: PropTypes.element,
  615. summaryPrice: PropTypes.element,
  616. form: PropTypes.element,
  617. isMobile: PropTypes.bool,
  618. onClickToggleMobileSideMenu: PropTypes.func,
  619. };
  620. }
  621. render() {
  622. const {
  623. title,
  624. description,
  625. crateMode,
  626. crate,
  627. summaryPrice,
  628. form,
  629. isMobile,
  630. onClickToggleMobileSideMenu
  631. } = this.props;
  632. return (
  633. <section className="panel">
  634. <h2>{title}</h2>
  635. <div className="control">
  636. <p className="description">{description}</p>
  637. {crateMode}
  638. </div>
  639. {isMobile ? (
  640. <div className="mobileBtnDisplaySideMenu">
  641. <button onClick={onClickToggleMobileSideMenu}>
  642. <img src="/images/shop/icon-add.svg" alt="add" />
  643. </button>
  644. </div>
  645. ) : null}
  646. {crate}
  647. <section className="summary">
  648. {summaryPrice}
  649. {form}
  650. </section>
  651. </section>
  652. );
  653. }
  654. }
  655. /**
  656. * Components that renders the form to request quote.
  657. */
  658. class OrderForm extends React.PureComponent {
  659. static get propTypes() {
  660. return {
  661. onClickSubmit: PropTypes.func,
  662. };
  663. }
  664. constructor(props) {
  665. super(props);
  666. this.state = {note: ''};
  667. this.handleNoteChange = this.handleNoteChange.bind(this);
  668. this.handleSubmit = this.handleSubmit.bind(this);
  669. }
  670. handleNoteChange(event) {
  671. this.setState({
  672. note: event.target.value,
  673. });
  674. }
  675. handleSubmit(event) {
  676. if (this.props.onClickSubmit) {
  677. this.props.onClickSubmit(this.state.note);
  678. }
  679. event.preventDefault();
  680. }
  681. render() {
  682. return (
  683. <div className="summary-form">
  684. <form onSubmit={this.handleSubmit}>
  685. <textarea
  686. value={this.state.note}
  687. onChange={this.handleNoteChange}
  688. rows="5"
  689. placeholder="Additional notes" />
  690. <input type="submit" value="Request quote" />
  691. This will open an email window. Send the email to make your request.
  692. </form>
  693. </div>
  694. );
  695. }
  696. }
  697. /**
  698. * Components that displays the list of card that are used in the crate.
  699. * It is a summary of purchase
  700. */
  701. class OrderSumary extends React.PureComponent {
  702. static get propTypes() {
  703. return {
  704. currency: PropTypes.string,
  705. modes: PropTypes.array,
  706. currentMode: PropTypes.string,
  707. summary: PropTypes.array,
  708. itemsData: PropTypes.array,
  709. onDeleteItem: PropTypes.func,
  710. onDeleteAllItems: PropTypes.func,
  711. onMouseEnterItem: PropTypes.func,
  712. onMouseLeaveItem: PropTypes.func,
  713. onClickSelectItem: PropTypes.func,
  714. };
  715. }
  716. constructor(props) {
  717. super(props);
  718. this.handleOnDeleteItem = this.handleOnDeleteItem.bind(this);
  719. this.handleOnDeleteAllItems = this.handleOnDeleteAllItems.bind(this);
  720. this.handleOnMouseEnterItem = this.handleOnMouseEnterItem.bind(this);
  721. this.handleOnMouseLeaveItem = this.handleOnMouseLeaveItem.bind(this);
  722. this.handleOnClickSelectItem = this.handleOnClickSelectItem.bind(this);
  723. }
  724. handleOnDeleteItem(index, e) {
  725. if (this.props.onDeleteItem) {
  726. this.props.onDeleteItem(index);
  727. }
  728. e.preventDefault();
  729. }
  730. handleOnDeleteAllItems(e) {
  731. if (this.props.onDeleteAllItems) {
  732. this.props.onDeleteAllItems();
  733. }
  734. e.preventDefault();
  735. }
  736. handleOnMouseEnterItem(id, e) {
  737. if (this.props.onMouseEnterItem) {
  738. this.props.onMouseEnterItem(id);
  739. }
  740. e.preventDefault();
  741. }
  742. handleOnMouseLeaveItem(e) {
  743. if (this.props.onMouseLeaveItem) {
  744. this.props.onMouseLeaveItem();
  745. }
  746. e.preventDefault();
  747. }
  748. handleOnClickSelectItem(index, e) {
  749. if (e.target.tagName !== 'IMG') {
  750. if (this.props.onClickSelectItem) {
  751. this.props.onClickSelectItem(index);
  752. }
  753. }
  754. return e.preventDefault();
  755. }
  756. render() {
  757. const {
  758. currency,
  759. modes,
  760. currentMode,
  761. summary,
  762. itemsData,
  763. } = this.props;
  764. const mode = modes.find(elem => elem.id === currentMode);
  765. return (
  766. <div className="summary-price">
  767. <table>
  768. <thead>
  769. <tr>
  770. <td colSpan="2" className="summary-remove-all">
  771. <span className="item-card-name">Remove all cards</span>
  772. <button onClick={this.handleOnDeleteAllItems}>
  773. <img src="/images/shop/icon-remove.svg" />
  774. </button>
  775. </td>
  776. </tr>
  777. {mode && (
  778. <tr>
  779. <td className="item-card-name">{mode.name}</td>
  780. <td className="price">
  781. <div>
  782. {`${currency} ${formatMoney(mode.price)}`}
  783. <button style={{'opacity': '0', 'cursor': 'initial'}}>
  784. <img src="/images/shop/icon-remove.svg" />
  785. </button>
  786. </div>
  787. <span style={{
  788. 'display': 'inline-block',
  789. 'width': '30px',
  790. }}>&nbsp;</span>
  791. </td>
  792. </tr>
  793. )}
  794. </thead>
  795. <tbody>
  796. {summary.map((item, index) => {
  797. let alert;
  798. let warning;
  799. if (itemsData[index]) {
  800. alert = itemsData[index];
  801. const warningsKeys = Object.keys(alert.warnings);
  802. if (warningsKeys && warningsKeys.length > 0) {
  803. warning = alert.warnings[warningsKeys[0]];
  804. }
  805. }
  806. return (
  807. <tr key={item.id}
  808. className={`hoverable ${item.selected ? 'selected' : ''}`}
  809. onClick={this.handleOnClickSelectItem.bind(this, index)}
  810. onMouseEnter={this.handleOnMouseEnterItem.bind(this, item.id)}
  811. onMouseLeave={this.handleOnMouseLeaveItem}>
  812. <td className="item-card-name">
  813. <div>{item.name}</div>
  814. </td>
  815. <td className="price">
  816. <div>
  817. {`${currency} ${formatMoney(item.price)}`}
  818. <button onClick={this.handleOnDeleteItem.bind(this, index)}>
  819. <img src="/images/shop/icon-remove.svg" />
  820. </button>
  821. </div>
  822. {warning && (
  823. <img
  824. style={{'marginLeft': '10px'}}
  825. className="alert-warning"
  826. src={`/images/${warning.icon}`}
  827. />
  828. )}
  829. {!warning && (
  830. <span style={{
  831. 'display': 'inline-block',
  832. 'width': '30px',
  833. }}>&nbsp;</span>
  834. )}
  835. </td>
  836. </tr>
  837. );
  838. })}
  839. </tbody>
  840. <tfoot>
  841. <tr>
  842. <td className="item-card-name">Price estimate</td>
  843. <td className="price">
  844. <div>
  845. {summary.length ? (
  846. `${currency} ${formatMoney(summary.reduce(
  847. (prev, next) => {
  848. return prev + next.price;
  849. }, 0
  850. ) + mode.price)}`
  851. ) : (
  852. `${currency} ${formatMoney(mode.price)}`
  853. )}
  854. <button style={{'opacity': '0', 'cursor': 'initial'}}>
  855. <img src="/images/shop/icon-remove.svg" alt="icon remove"/>
  856. </button>
  857. </div>
  858. <span style={{
  859. 'display': 'inline-block',
  860. 'width': '30px',
  861. }}>&nbsp;</span>
  862. </td>
  863. </tr>
  864. </tfoot>
  865. </table>
  866. </div>
  867. );
  868. }
  869. }
  870. /**
  871. * Component that renders the backlog in the aside
  872. */
  873. class Backlog extends React.PureComponent {
  874. static get propTypes() {
  875. return {
  876. currency: PropTypes.string,
  877. data: PropTypes.object.isRequired,
  878. items: PropTypes.object,
  879. isMobile: PropTypes.bool,
  880. onClickAddItem: PropTypes.func,
  881. onClickToggleMobileSideMenu: PropTypes.func,
  882. };
  883. }
  884. static get defaultProps() {
  885. return {
  886. items: {},
  887. };
  888. }
  889. render() {
  890. const {
  891. currency,
  892. data,
  893. items,
  894. onClickAddItem,
  895. onClickToggleMobileSideMenu,
  896. isMobile,
  897. } = this.props;
  898. const ordered_items = data.itemIds.map(itemId => items[itemId]);
  899. const products = ordered_items.map((item, index) => {
  900. return (
  901. <ProductItem
  902. key={item.id}
  903. id={item.id}
  904. index={index}
  905. name={item.name}
  906. price={item.price}
  907. currency={currency}
  908. image={`/images/${item.image}`}
  909. specs={item.specs}
  910. onClickAddItem={onClickAddItem}
  911. ></ProductItem>
  912. );
  913. });
  914. return (
  915. <Droppable
  916. droppableId={data.id}
  917. isDropDisabled={true}>
  918. {(provided) => (
  919. <div
  920. className="backlog-container"
  921. ref={provided.innerRef}
  922. {...provided.droppableProps}>
  923. {isMobile ? (
  924. <div className="mobileCloseMenu">
  925. <button onClick={onClickToggleMobileSideMenu}>
  926. <img src="/images/shop/icon-close-white.svg" alt="add" />
  927. </button>
  928. </div>
  929. ) : null}
  930. {products}
  931. {provided.placeholder && (
  932. <div style={{ display: 'none' }}>
  933. {provided.placeholder}
  934. </div>
  935. )}
  936. </div>
  937. )}
  938. </Droppable>
  939. );
  940. }
  941. }
  942. /**
  943. * Component that render the entire shop
  944. */
  945. class Shop extends React.PureComponent {
  946. static get propTypes() {
  947. return {
  948. data: PropTypes.object.isRequired,
  949. };
  950. }
  951. constructor(props) {
  952. super(props);
  953. this.state = this.props.data;
  954. this.handleCrateModeChange = this.handleCrateModeChange.bind(this);
  955. this.handleOnDragEnd = this.handleOnDragEnd.bind(this);
  956. this.handleDeleteItem = this.handleDeleteItem.bind(this);
  957. this.handleDeleteAllItems = this.handleDeleteAllItems.bind(this);
  958. this.handleMouseEnterItem = this.handleMouseEnterItem.bind(this);
  959. this.handleMouseLeaveItem = this.handleMouseLeaveItem.bind(this);
  960. this.handleClickAddItem = this.handleClickAddItem.bind(this);
  961. this.checkAlerts = this.checkAlerts.bind(this);
  962. this.handleToggleItemProgress = this.handleToggleItemProgress.bind(this);
  963. this.handleToggleItemWarning = this.handleToggleItemWarning.bind(this);
  964. this.handleClickSelectItem = this.handleClickSelectItem.bind(this);
  965. this.handleClickSubmit = this.handleClickSubmit.bind(this);
  966. this.handleToggleOverlayRemove = this.handleToggleOverlayRemove.bind(this);
  967. this.handleClickToggleMobileSideMenu = this.handleClickToggleMobileSideMenu.bind(this);
  968. }
  969. componentDidMount() {
  970. // index 0 is a Kasli, we place it as a default conf on the crate.
  971. const source = {
  972. droppableId: 'backlog',
  973. index: 0,
  974. };
  975. const destination = {
  976. droppableId: 'cart',
  977. index: 0,
  978. };
  979. this.handleOnDragEnd({
  980. source,
  981. destination,
  982. draggableId: null,
  983. });
  984. }
  985. componentDidUpdate(prevProps, prevState) {
  986. /**
  987. * We check alerts (reminder + warning) only when items inside crate or
  988. * crate mode change.
  989. *
  990. * In the function checkAlerts, we DO NOT want to change items as we will
  991. * trigger again this function (componentDidUpdate) and thus,
  992. * making an infinite loop.
  993. */
  994. if (
  995. (prevState.columns.cart.items !== this.state.columns.cart.items) ||
  996. (prevState.currentMode !== this.state.currentMode)
  997. ) {
  998. this.checkAlerts(
  999. prevState.columns.cart.items,
  1000. this.state.columns.cart.items);
  1001. }
  1002. }
  1003. handleCrateModeChange(mode) {
  1004. this.setState({
  1005. currentMode: mode,
  1006. });
  1007. }
  1008. handleDeleteItem(index) {
  1009. const cloned = Array.from(this.state.columns.cart.items);
  1010. cloned.splice(index, 1);
  1011. this.setState({
  1012. ...this.state,
  1013. columns: {
  1014. ...this.state.columns,
  1015. cart: {
  1016. ...this.state.columns.cart,
  1017. items: cloned,
  1018. },
  1019. },
  1020. });
  1021. }
  1022. handleDeleteAllItems() {
  1023. this.setState({
  1024. ...this.state,
  1025. columns: {
  1026. ...this.state.columns,
  1027. cart: {
  1028. ...this.state.columns.cart,
  1029. items: [],
  1030. },
  1031. },
  1032. });
  1033. }
  1034. handleMouseEnterItem(id) {
  1035. this.setState({
  1036. ...this.state,
  1037. currentItemHovered: id,
  1038. });
  1039. }
  1040. handleMouseLeaveItem() {
  1041. this.setState({
  1042. ...this.state,
  1043. currentItemHovered: null,
  1044. });
  1045. }
  1046. handleClickAddItem(index) {
  1047. const source = {
  1048. droppableId: 'backlog',
  1049. index: index,
  1050. };
  1051. const destination = {
  1052. droppableId: 'cart',
  1053. index: this.state.columns.cart.items.length,
  1054. };
  1055. this.handleOnDragEnd({
  1056. source,
  1057. destination,
  1058. draggableId: null,
  1059. });
  1060. }
  1061. handleToggleItemProgress(index, show) {
  1062. const itemsCloned = Array.from(this.state.columns.cart.items);
  1063. this.setState({
  1064. ...this.state,
  1065. columns: {
  1066. ...this.state.columns,
  1067. cart: {
  1068. ...this.state.columns.cart,
  1069. items: itemsCloned.map((item, i) => {
  1070. return {
  1071. ...item,
  1072. showProgress: i === index ? show : false,
  1073. showOverlayRemove: false,
  1074. showWarning: false,
  1075. };
  1076. }),
  1077. }
  1078. },
  1079. });
  1080. }
  1081. handleToggleItemWarning(index, show) {
  1082. const itemsCloned = Array.from(this.state.columns.cart.items);
  1083. this.setState({
  1084. ...this.state,
  1085. columns: {
  1086. ...this.state.columns,
  1087. cart: {
  1088. ...this.state.columns.cart,
  1089. items: itemsCloned.map((item, i) => {
  1090. return {
  1091. ...item,
  1092. showWarning: i === index ? show : false,
  1093. showProgress: false,
  1094. showOverlayRemove: false,
  1095. };
  1096. }),
  1097. }
  1098. },
  1099. });
  1100. }
  1101. handleClickSelectItem(index) {
  1102. const itemsCloned = Array.from(this.state.columns.cart.items);
  1103. this.setState({
  1104. ...this.state,
  1105. columns: {
  1106. ...this.state.columns,
  1107. cart: {
  1108. ...this.state.columns.cart,
  1109. items: itemsCloned.map((item, id) => {
  1110. return {...item, selected: id === index ? true : false};
  1111. }),
  1112. }
  1113. },
  1114. });
  1115. }
  1116. handleToggleOverlayRemove(index, show) {
  1117. const itemsCloned = Array.from(this.state.columns.cart.items);
  1118. this.setState({
  1119. ...this.state,
  1120. columns: {
  1121. ...this.state.columns,
  1122. cart: {
  1123. ...this.state.columns.cart,
  1124. items: itemsCloned.map((item, id) => {
  1125. return {
  1126. ...item,
  1127. showOverlayRemove: id === index ? show : false,
  1128. showProgress: false,
  1129. showWarning: false,
  1130. };
  1131. }),
  1132. }
  1133. },
  1134. });
  1135. }
  1136. handleClickSubmit(note) {
  1137. const crate = {
  1138. items: [],
  1139. type: this.state.currentMode,
  1140. };
  1141. const clonedCart = Array.from(this.state.columns.cart.items);
  1142. for (const i in clonedCart) {
  1143. const item = clonedCart[i];
  1144. crate.items.push({
  1145. 'name': item.name,
  1146. });
  1147. }
  1148. const a = document.createElement('a');
  1149. const num = (new Date()).getTime();
  1150. const subject = `[Order hardware] - Request Quote`;
  1151. let body = `Hello!\n\nI would like to request a quotation for my below configuration:\n\n${JSON.stringify(crate)}\n\n(Please do not edit the machine-readable representation above)\n\n`;
  1152. if (note) {
  1153. body = `${body}\n\nAdditional note:\n\n${note ? note.trim() : ''}`;
  1154. }
  1155. document.body.appendChild(a);
  1156. a.style = 'display: none';
  1157. a.href = `mailto:sales@m-labs.hk?subject=${subject}&body=${encodeURIComponent(body)}`;
  1158. a.click();
  1159. }
  1160. handleOnDragEnd(result) {
  1161. const {
  1162. source,
  1163. destination,
  1164. draggableId,
  1165. } = result;
  1166. if (!destination) {
  1167. if (source.droppableId === 'cart') {
  1168. this.setState({
  1169. ...this.state,
  1170. columns: {
  1171. ...this.state.columns,
  1172. [source.droppableId]: {
  1173. ...this.state.columns[source.droppableId],
  1174. items: remove(
  1175. this.state.columns[source.droppableId].items,
  1176. source.index,
  1177. ),
  1178. },
  1179. },
  1180. });
  1181. }
  1182. return;
  1183. }
  1184. switch(source.droppableId) {
  1185. case 'backlog':
  1186. this.setState({
  1187. ...this.state,
  1188. columns: {
  1189. ...this.state.columns,
  1190. [destination.droppableId]: {
  1191. ...this.state.columns[destination.droppableId],
  1192. items: copy(
  1193. this.state.items,
  1194. this.state.columns[source.droppableId],
  1195. this.state.columns[destination.droppableId],
  1196. source,
  1197. destination,
  1198. ),
  1199. },
  1200. },
  1201. });
  1202. break;
  1203. case destination.droppableId:
  1204. this.setState({
  1205. ...this.state,
  1206. columns: {
  1207. ...this.state.columns,
  1208. [destination.droppableId]: {
  1209. ...this.state.columns[destination.droppableId],
  1210. items: reorder(
  1211. this.state.columns[destination.droppableId].items,
  1212. source.index,
  1213. destination.index,
  1214. ),
  1215. },
  1216. },
  1217. });
  1218. break;
  1219. default:
  1220. break;
  1221. }
  1222. }
  1223. handleClickToggleMobileSideMenu() {
  1224. this.setState({
  1225. ...this.state,
  1226. mobileSideMenuShouldOpen: !this.state.mobileSideMenuShouldOpen,
  1227. });
  1228. }
  1229. checkAlerts(prevItems, newItems) {
  1230. console.log('--- START CHECKING CRATE WARNING ---');
  1231. const {
  1232. currentMode,
  1233. crateModeSlots,
  1234. crateRules,
  1235. } = this.state;
  1236. const itemsCloned = Array.from(newItems);
  1237. const itemsData = {};
  1238. const rules = {};
  1239. // check number of slot in crate
  1240. const nbrOccupied = nbrOccupiedSlotsInCrate(newItems);
  1241. if (nbrOccupied > crateModeSlots[currentMode]) {
  1242. rules[crateRules.maxSlot.type] = {...crateRules.maxSlot};
  1243. }
  1244. // check the number of EEM connectors available for all Kasli
  1245. const idxK = itemsCloned.reduce((prev, next, i) => {
  1246. if (next.type === 'kasli' || next.type === 'kasli-backplane') {
  1247. prev.push(i);
  1248. }
  1249. return prev;
  1250. }, []);
  1251. for (let i = 0; i <= idxK.length - 1; i++) {
  1252. let slots;
  1253. let nbUsedSlot = 0;
  1254. let nbrCurrentClock = 0;
  1255. let idx = idxK[i];
  1256. if (i !== idxK.length - 1) {
  1257. slots = itemsCloned.slice(idx + 1, idxK[i + 1]);
  1258. } else {
  1259. slots = itemsCloned.slice(idx + 1);
  1260. }
  1261. if (i == 0) {
  1262. const slots_need_resource = itemsCloned.slice(0, idx);
  1263. const idx_need = slots_need_resource.findIndex(e => (e.rules && e.rules.resources));
  1264. if (idx_need != -1) {
  1265. if (idx_need in itemsData) {
  1266. if ('warnings' in itemsData[idx_need]) {
  1267. itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
  1268. } else {
  1269. itemsData[idx_need].warnings = {};
  1270. itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
  1271. }
  1272. } else {
  1273. itemsData[idx_need] = {...itemsCloned[idx_need]};
  1274. itemsData[idx_need].warnings = {};
  1275. itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
  1276. }
  1277. }
  1278. }
  1279. nbUsedSlot = slots
  1280. .filter(item => item.type !== 'idc-bnc')
  1281. .reduce((prev, next) => {
  1282. return prev + next.slotOccupied;
  1283. }, 0);
  1284. nbrCurrentClock = slots
  1285. .reduce((prev, next) => {
  1286. return next.type === 'clocker' ? prev + next.clockOccupied : prev;
  1287. }, 0);
  1288. if (idx in itemsData) {
  1289. itemsData[idx].nbrCurrentSlot = nbUsedSlot;
  1290. itemsData[idx].nbrCurrentClock = nbrCurrentClock;
  1291. if (!('warnings' in itemsData[idx])) {
  1292. itemsData[idx].warnings = {};
  1293. }
  1294. } else {
  1295. itemsData[idx] = {...itemsCloned[idx]};
  1296. itemsData[idx].nbrCurrentSlot = nbUsedSlot;
  1297. itemsData[idx].nbrCurrentClock = nbrCurrentClock;
  1298. itemsData[idx].warnings = {};
  1299. }
  1300. if (nbUsedSlot > itemsCloned[idx].nbrSlotMax) {
  1301. rules[itemsCloned[idx].rules.maxSlot.type] = {...itemsCloned[idx].rules.maxSlot};
  1302. itemsData[idx].warnings.maxSlotWarning = {...itemsCloned[idx].rules.maxSlotWarning};
  1303. }
  1304. if (nbrCurrentClock > itemsCloned[idx].nbrClockMax) {
  1305. rules[itemsCloned[idx].rules.maxClock.type] = {...itemsCloned[idx].rules.maxClock};
  1306. itemsData[idx].warnings.maxClockWarning = {...itemsCloned[idx].rules.maxClockWarning};
  1307. }
  1308. if (itemsCloned.length > (idx + 1)) {
  1309. const ddkali = itemsCloned[idx + 1];
  1310. if (ddkali.type === 'kasli' || ddkali.type === 'kasli-backplane') {
  1311. rules[ddkali.rules.follow.type] = {...ddkali.rules.follow};
  1312. }
  1313. }
  1314. }
  1315. // check number of clock connector available
  1316. const idxC = itemsCloned.reduce((prev, next, i) => {
  1317. if (next.type === 'kasli' || next.type === 'kasli-backplane' || next.type === 'clocker') {
  1318. prev.push(i);
  1319. }
  1320. return prev;
  1321. }, []);
  1322. for (let i = 0; i <= idxC.length - 1; i++) {
  1323. let slots;
  1324. let nbrCurrentClock = 0;
  1325. let idx = idxC[i];
  1326. if (i !== idxC.length - 1) {
  1327. slots = itemsCloned.slice(idx + 1, idxC[i + 1]);
  1328. } else {
  1329. slots = itemsCloned.slice(idx + 1);
  1330. }
  1331. nbrCurrentClock = slots.reduce((prev, next) => {
  1332. return prev + next.clockOccupied;
  1333. }, 0);
  1334. if (idx in itemsData) {
  1335. if (itemsData[idx].nbrCurrentClock) {
  1336. itemsData[idx].nbrCurrentClock += nbrCurrentClock;
  1337. } else {
  1338. itemsData[idx].nbrCurrentClock = nbrCurrentClock;
  1339. }
  1340. } else {
  1341. itemsData[idx] = {...itemsCloned[idx]};
  1342. itemsData[idx].nbrCurrentClock = nbrCurrentClock;
  1343. itemsData[idx].warnings = {};
  1344. }
  1345. if (nbrCurrentClock > itemsCloned[idx].nbrClockMax) {
  1346. rules[itemsCloned[idx].rules.maxClock.type] = {...itemsCloned[idx].rules.maxClock};
  1347. itemsData[idx].warnings.maxClockWarning = {...itemsCloned[idx].rules.maxClockWarning};
  1348. }
  1349. }
  1350. // check for number of recommanded EEM connectors
  1351. ['novo', 'urukul', 'koster'].map(_type => {
  1352. if (itemsCloned.find(elem => elem.type === _type)) {
  1353. rules[this.state.items[_type].rules.connectors.type] = {...this.state.items[_type].rules.connectors};
  1354. }
  1355. return _type;
  1356. });
  1357. // check if IDC-BNC is correctly positionned (after Zotino or HD68)
  1358. const idxIDCBNC = itemsCloned.reduce((prev, next, i) => {
  1359. if (next.type === 'idc-bnc') {
  1360. prev.push(i);
  1361. }
  1362. return prev;
  1363. }, []);
  1364. for (var i = idxIDCBNC.length - 1; i >= 0; i--) {
  1365. const ce = idxIDCBNC[i];
  1366. let shouldWarning = false;
  1367. if (ce == 0) {
  1368. shouldWarning = true;
  1369. } else if (ce >= 1) {
  1370. const pe = idxIDCBNC[i] - 1;
  1371. if (itemsCloned[pe].type !== 'zotino' &&
  1372. itemsCloned[pe].type !== 'hd68' &&
  1373. itemsCloned[pe].type !== 'idc-bnc') {
  1374. shouldWarning = true;
  1375. }
  1376. }
  1377. if (shouldWarning) {
  1378. itemsData[ce] = {...itemsCloned[ce]};
  1379. itemsData[ce].warnings = {};
  1380. itemsData[ce].warnings.wrong = {...itemsCloned[ce].rules.wrong};
  1381. }
  1382. }
  1383. // check number of IDC-BNC adapters for a Zotino and HD68-IDC
  1384. const idxZH = itemsCloned.reduce((prev, next, i) => {
  1385. if (next.type === 'zotino' || next.type === 'hd68') {
  1386. prev.push(i);
  1387. }
  1388. return prev;
  1389. }, []);
  1390. for (let i = 0; i <= idxZH.length - 1; i++) {
  1391. let slots;
  1392. let nbUsedSlot = 0;
  1393. let idx = idxZH[i];
  1394. if (i !== idxZH.length - 1) {
  1395. slots = itemsCloned.slice(idx + 1, idxZH[i + 1]);
  1396. } else {
  1397. slots = itemsCloned.slice(idx + 1);
  1398. }
  1399. let stopCount = false;
  1400. nbUsedSlot = slots.reduce((prev, next, ci, ca) => {
  1401. if (ci === 0 && next.type === 'idc-bnc') {
  1402. return prev + 1;
  1403. } else if (ca[0].type === 'idc-bnc' && ci > 0 && ca[ci - 1].type === 'idc-bnc') {
  1404. if (next.type !== 'idc-bnc') { stopCount = true; }
  1405. return prev + (next.type === 'idc-bnc' && !stopCount ? 1 : 0);
  1406. }
  1407. return prev;
  1408. }, 0);
  1409. if (idx in itemsData) {
  1410. itemsData[idx].nbrCurrentSlot = nbUsedSlot;
  1411. if (!('warnings' in itemsData[idx])) {
  1412. itemsData[idx].warnings = {};
  1413. }
  1414. } else {
  1415. itemsData[idx] = {...itemsCloned[idx]};
  1416. itemsData[idx].nbrCurrentSlot = nbUsedSlot;
  1417. itemsData[idx].warnings = {};
  1418. }
  1419. if (nbUsedSlot > 0) {
  1420. rules[itemsCloned[idx].rules.maxSlot.type] = {...itemsCloned[idx].rules.maxSlot};
  1421. }
  1422. if (nbUsedSlot > itemsCloned[idx].nbrSlotMax) {
  1423. itemsData[idx].warnings.maxSlotWarning = {...itemsCloned[idx].rules.maxSlotWarning};
  1424. }
  1425. // check if HD68-IDC has at least 1 IDC-BNC adapter
  1426. if (itemsCloned[idx].type === 'hd68') {
  1427. let shouldWarning = false;
  1428. if (idx < itemsCloned.length - 1) {
  1429. if (itemsCloned[idx + 1].type !== 'idc-bnc') {
  1430. shouldWarning = true;
  1431. }
  1432. } else if (idx === itemsCloned.length - 1) {
  1433. shouldWarning = true;
  1434. }
  1435. if (shouldWarning) {
  1436. if (idx in itemsData) {
  1437. itemsData[idx].warnings.minAdapter = {...itemsCloned[idx].rules.minAdapter};
  1438. } else {
  1439. itemsData[idx] = {...itemsCloned[idx]};
  1440. itemsData[idx].warnings = {};
  1441. itemsData[idx].warnings.minAdapter = {...itemsCloned[idx].rules.minAdapter};
  1442. }
  1443. }
  1444. }
  1445. }
  1446. // update state with rules
  1447. this.setState({
  1448. ...this.state,
  1449. columns: {
  1450. ...this.state.columns,
  1451. cart: {
  1452. ...this.state.columns.cart,
  1453. itemsData: {
  1454. ...itemsData,
  1455. },
  1456. }
  1457. },
  1458. rules: {
  1459. ...rules,
  1460. },
  1461. });
  1462. }
  1463. render() {
  1464. const {
  1465. currency,
  1466. currentItemHovered,
  1467. currentMode,
  1468. crateModeSlots,
  1469. crateModeItems,
  1470. items,
  1471. columns,
  1472. rules,
  1473. mobileSideMenuShouldOpen,
  1474. } = this.state;
  1475. return (
  1476. <DragDropContext onDragEnd={this.handleOnDragEnd}>
  1477. <Layout
  1478. className="shop"
  1479. mobileSideMenuShouldOpen={mobileSideMenuShouldOpen}
  1480. onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
  1481. aside={
  1482. <Backlog
  1483. currency={currency}
  1484. items={items}
  1485. data={columns['backlog']}
  1486. onClickAddItem={this.handleClickAddItem}
  1487. onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
  1488. isMobile={window.deviceIsMobile()}>
  1489. </Backlog>
  1490. }
  1491. main={(
  1492. <OrderPanel
  1493. onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
  1494. isMobile={window.deviceIsMobile()}
  1495. title="Order hardware"
  1496. description="
  1497. 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."
  1498. crateMode={
  1499. <CrateMode
  1500. items={crateModeItems}
  1501. mode={currentMode}
  1502. onClickMode={this.handleCrateModeChange}>
  1503. </CrateMode>}
  1504. crate={
  1505. <Crate
  1506. cart={
  1507. <Cart
  1508. nbrSlots={crateModeSlots[currentMode]}
  1509. data={columns['cart']}
  1510. itemHovered={currentItemHovered}
  1511. onToggleProgress={this.handleToggleItemProgress}
  1512. onToggleWarning={this.handleToggleItemWarning}
  1513. onToggleOverlayRemove={this.handleToggleOverlayRemove}
  1514. onClickRemoveItem={this.handleDeleteItem}>
  1515. </Cart>
  1516. }
  1517. rules={Object.values(rules).filter(rule => rule)}>
  1518. </Crate>
  1519. }
  1520. summaryPrice={
  1521. <OrderSumary
  1522. currency={currency}
  1523. currentMode={currentMode}
  1524. modes={crateModeItems}
  1525. summary={columns['cart'].items}
  1526. itemsData={columns.cart.itemsData}
  1527. onMouseEnterItem={this.handleMouseEnterItem}
  1528. onMouseLeaveItem={this.handleMouseLeaveItem}
  1529. onDeleteItem={this.handleDeleteItem}
  1530. onDeleteAllItems={this.handleDeleteAllItems}
  1531. onClickSelectItem={this.handleClickSelectItem}>
  1532. </OrderSumary>
  1533. }
  1534. form={
  1535. <OrderForm
  1536. onClickSubmit={this.handleClickSubmit}>
  1537. </OrderForm>
  1538. }>
  1539. </OrderPanel>
  1540. )}>
  1541. </Layout>
  1542. </DragDropContext>
  1543. );
  1544. }
  1545. }
  1546. ReactDOM.render(
  1547. <Shop data={data} />,
  1548. document.querySelector('#root-shop'),
  1549. );