2019-10-02 12:16:07 +08:00
'use strict' ;
const {
DragDropContext ,
Draggable ,
Droppable
} = window . ReactBeautifulDnd ;
const data = window . shop _data ;
const productStyle = ( style , snapshot , removeAnim , hovered , selected ) => {
const custom = {
opacity : snapshot . isDragging ? .7 : 1 ,
backgroundColor : ( hovered || selected ) ? '#eae7f7' : 'initial' ,
} ;
if ( ! snapshot . isDropAnimating ) {
return { ... style , ... custom } ;
}
if ( removeAnim ) {
// cannot be 0, but make it super tiny
custom . transitionDuration = '0.001s' ;
}
return {
... style ,
... custom ,
} ;
}
const cartStyle = ( style , snapshot ) => {
const isDraggingOver = snapshot . isDraggingOver ;
return {
... style ,
... {
backgroundColor : isDraggingOver ? '#f2f2f2' : '#f9f9f9' ,
border : isDraggingOver ? '1px dashed #ccc' : '0' ,
} ,
} ;
}
const nbrConnectorsStyle = ( data ) => {
if ( ! data || ! data . nbrCurrentSlot ) {
return { } ;
}
let p = data . nbrCurrentSlot * 100 / data . nbrSlotMax ;
if ( p > 100 ) {
p = 100 ;
}
return {
width : ` ${ p } % ` ,
}
} ;
const nbrClocksStyle = ( data ) => {
if ( ! data || ! data . nbrCurrentClock ) {
return { } ;
}
let p = data . nbrCurrentClock * 100 / data . nbrClockMax ;
if ( p > 100 ) {
p = 100 ;
}
return {
width : ` ${ p } % ` ,
}
} ;
const copy = (
model ,
source ,
destination ,
droppableSource ,
droppableDestination
) => {
const sourceClone = Array . from ( source . itemIds ) ;
const destClone = Array . from ( destination . items ) ;
const item = sourceClone [ droppableSource . index ] ;
destClone . splice ( droppableDestination . index , 0 , {
... model [ item ] ,
id : uuidv4 ( ) ,
} ) ;
return destClone ;
} ;
const reorder = ( list , startIndex , endIndex ) => {
const result = Array . from ( list ) ;
const [ removed ] = result . splice ( startIndex , 1 ) ;
result . splice ( endIndex , 0 , removed ) ;
return result ;
} ;
const remove = ( list , startIndex ) => {
const result = Array . from ( list ) ;
result . splice ( startIndex , 1 ) ;
return result ;
} ;
const nbrOccupiedSlotsInCrate = ( items ) => {
return items . reduce ( ( prev , next ) => {
return prev + ( next . hp === 8 ? 2 : 1 ) ;
} , 0 ) ;
} ;
2019-11-09 18:41:09 +08:00
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 ;
}
} ;
2019-10-02 12:16:07 +08:00
/ * *
* Component that provides a base layout ( aside / main ) for the page .
* /
class Layout extends React . PureComponent {
static get propTypes ( ) {
return {
aside : PropTypes . any ,
main : PropTypes . any ,
} ;
}
render ( ) {
const {
aside ,
main ,
} = this . props ;
return (
< div className = "layout" >
< aside className = "aside" > { aside } < / aside >
< section className = "main" > { main } < / section >
< / div >
) ;
}
}
/ * *
* Component that renders a product .
* Used in the aside ( e . g backlog of product )
* /
class ProductItem extends React . PureComponent {
static get propTypes ( ) {
return {
id : PropTypes . string . isRequired ,
index : PropTypes . number . isRequired ,
name : PropTypes . string . isRequired ,
price : PropTypes . number . isRequired ,
currency : PropTypes . string . isRequired ,
image : PropTypes . string . isRequired ,
specs : PropTypes . array ,
onClickAddItem : PropTypes . func ,
} ;
}
constructor ( props ) {
super ( props ) ;
this . handleOnClickAddItem = this . handleOnClickAddItem . bind ( this ) ;
}
handleOnClickAddItem ( index , e ) {
if ( this . props . onClickAddItem ) {
this . props . onClickAddItem ( index ) ;
}
e . preventDefault ( ) ;
}
render ( ) {
const {
id ,
index ,
name ,
price ,
currency ,
image ,
specs ,
} = this . props ;
const render _specs = ( specs && specs . length > 0 && (
< ul >
{ specs . map ( ( spec , index ) =>
< li key = { index } > { spec } < / li >
) }
< / ul >
) ) ;
return (
< section className = "productItem" >
< div className = "content" >
< h3 > { name } < / h3 >
2019-11-09 18:41:09 +08:00
< div className = "price" > { ` ${ currency } ${ formatMoney ( price ) } ` } < / div >
2019-10-02 12:16:07 +08:00
{ render _specs }
< / div >
< div className = "content" >
< button onClick = { this . handleOnClickAddItem . bind ( this , index ) } >
< img src = "/images/shop/icon-add.svg" alt = "add" / >
< / button >
< Draggable draggableId = { id } index = { index } >
{ ( provided , snapshot ) => (
< React.Fragment >
< img
ref = { provided . innerRef }
{ ... provided . draggableProps }
{ ... provided . dragHandleProps }
style = { productStyle (
provided . draggableProps . style ,
snapshot ,
true , // hack: remove weird animation after a drop
) }
src = { image } / >
{ /* Allows to simulate a clone */ }
{ snapshot . isDragging && (
< img className = "" src = { image } / >
) }
< / React.Fragment >
) }
< / Draggable >
< / div >
< / section >
) ;
}
}
/ * *
* Component that renders a product .
* Used in the crate
* /
class ProductCartItem extends React . PureComponent {
static get propTypes ( ) {
return {
hovered : PropTypes . bool ,
index : PropTypes . number . isRequired ,
model : PropTypes . object . isRequired ,
data : PropTypes . object ,
onToggleProgress : PropTypes . func ,
onToggleWarning : PropTypes . func ,
onToggleOverlayRemove : PropTypes . func ,
onClickRemoveItem : PropTypes . func ,
} ;
}
static get defaultProps ( ) {
return {
hovered : false ,
} ;
}
constructor ( props ) {
super ( props ) ;
this . handleOnMouseEnterItem = this . handleOnMouseEnterItem . bind ( this ) ;
this . handleOnMouseLeaveItem = this . handleOnMouseLeaveItem . bind ( this ) ;
this . handleOnMouseEnterWarningItem = this . handleOnMouseEnterWarningItem . bind ( this ) ;
this . handleOnMouseLeaveWarningItem = this . handleOnMouseLeaveWarningItem . bind ( this ) ;
this . handleOnMouseEnterRemoveItem = this . handleOnMouseEnterRemoveItem . bind ( this ) ;
this . handleOnMouseLeaveRemoveItem = this . handleOnMouseLeaveRemoveItem . bind ( this ) ;
this . handleOnClickRemoveItem = this . handleOnClickRemoveItem . bind ( this ) ;
}
handleOnMouseEnterItem ( index , e ) {
if ( this . props . onToggleProgress ) {
this . props . onToggleProgress ( index , true ) ;
}
e . preventDefault ( ) ;
}
handleOnMouseLeaveItem ( index , e ) {
if ( this . props . onToggleProgress ) {
this . props . onToggleProgress ( index , false ) ;
}
e . preventDefault ( ) ;
}
handleOnMouseEnterWarningItem ( index , isWarning , e ) {
if ( ! isWarning ) {
return ;
}
if ( this . props . onToggleWarning ) {
this . props . onToggleWarning ( index , true ) ;
}
e . preventDefault ( ) ;
}
handleOnMouseLeaveWarningItem ( index , isWarning , e ) {
if ( ! isWarning ) {
return ;
}
if ( this . props . onToggleWarning ) {
this . props . onToggleWarning ( index , false ) ;
}
e . preventDefault ( ) ;
}
handleOnMouseEnterRemoveItem ( index , e ) {
if ( this . props . onToggleOverlayRemove ) {
this . props . onToggleOverlayRemove ( index , true ) ;
}
e . preventDefault ( ) ;
}
handleOnMouseLeaveRemoveItem ( index , e ) {
if ( this . props . onToggleOverlayRemove ) {
this . props . onToggleOverlayRemove ( index , false ) ;
}
e . preventDefault ( ) ;
}
handleOnClickRemoveItem ( index , e ) {
if ( this . props . onClickRemoveItem ) {
this . props . onClickRemoveItem ( index ) ;
}
}
render ( ) {
const {
hovered ,
model ,
data ,
index ,
} = this . props ;
let warning ;
if ( data && data . warnings ) {
const warningsKeys = Object . keys ( data . warnings ) ;
if ( warningsKeys && warningsKeys . length > 0 ) {
// we display only the first warning
warning = data . warnings [ warningsKeys [ 0 ] ] ;
}
}
let render _progress ;
if ( model . showProgress && data ) {
switch ( model . type ) {
case 'kasli' :
case 'kasli-backplane' :
render _progress = (
< div className = "k-popup-connectors" >
< p > { ` ${ data . nbrCurrentSlot } / ${ model . nbrSlotMax } EEM connectors available ` } < / p >
< p > { ` ${ data . nbrCurrentClock } / ${ model . nbrClockMax } Clock connectors available ` } < / p >
< / div >
) ;
break ;
case 'zotino' :
case 'hd68' :
render _progress = (
< div className = "k-popup-connectors" >
< p > { ` ${ data . nbrCurrentSlot } / ${ model . nbrSlotMax } connectors available ` } < / p >
< / div >
) ;
break ;
case 'clocker' :
render _progress = (
< div className = "k-popup-connectors" >
< p > { ` ${ data . nbrCurrentClock } / ${ model . nbrClockMax } Clock connectors available ` } < / p >
< / div >
) ;
break ;
default :
break ;
}
}
return (
< Draggable draggableId = { model . id } index = { index } >
{ ( provided , snapshot ) => (
< div
ref = { provided . innerRef }
{ ... provided . draggableProps }
{ ... provided . dragHandleProps }
style = { { ... productStyle (
provided . draggableProps . style ,
snapshot ,
true ,
hovered ? true : false ,
model . selected ? true : false ,
) } }
onMouseEnter = { this . handleOnMouseEnterRemoveItem . bind ( this , index ) }
onMouseLeave = { this . handleOnMouseLeaveRemoveItem . bind ( this , index ) }
>
{ /* warning container */ }
< div
style = { { 'height' : '24px' } }
className = "progress-container warning"
onMouseEnter = { this . handleOnMouseEnterWarningItem . bind ( this , index , warning ) }
onMouseLeave = { this . handleOnMouseLeaveWarningItem . bind ( this , index , warning ) } >
{ warning && (
< img className = "alert-warning" src = { warning ? ` /images ${ warning . icon } ` : null } / >
) }
{ warning && model . showWarning && (
< div className = "k-popup-warning" >
< p className = "rule warning" >
< i > { warning . message } < / i >
< / p >
< / div >
) }
< / div >
< h6 > { model . name } < / h6 >
< div
style = { { 'height' : '250px' } }
onMouseEnter = { this . handleOnMouseEnterRemoveItem . bind ( this , index ) }
>
< img
className = 'item-cart'
src = { ` /images ${ model . image } ` } / >
< / div >
{ /* remove container */ }
< div
style = { { 'display' : model . showOverlayRemove ? 'flex' : 'none' } }
className = "overlayRemove"
onClick = { this . handleOnClickRemoveItem . bind ( this , index ) } >
< img src = "/images/shop/icon-remove.svg" alt = "rm" / >
2019-11-11 13:39:17 +08:00
< p > Remove < / p >
2019-10-02 12:16:07 +08:00
< / div >
{ /* progression container */ }
< div
style = { { 'height' : '22px' } }
className = "progress-container"
onMouseEnter = { this . handleOnMouseEnterItem . bind ( this , index ) }
onMouseLeave = { this . handleOnMouseLeaveItem . bind ( this , index ) } >
{ model . nbrSlotMax > 0 && (
< div className = "nbr-connectors" >
< div style = { { ... nbrConnectorsStyle ( data ) } } > < / div >
< / div >
) }
{ model . nbrClockMax > 0 && (
< div className = "nbr-clocks" >
< div style = { { ... nbrClocksStyle ( data ) } } > < / div >
< / div >
) }
{ /* progress info when mouse over */ }
{ render _progress }
< / div >
< / div >
) }
< / Draggable >
) ;
}
}
/ * *
* Component that displays a placeholder inside crate .
* Allows to display how it remains space for the current crate .
* /
class FakePlaceholder extends React . PureComponent {
static get propTypes ( ) {
return {
isDraggingOver : PropTypes . bool ,
nbrSlots : PropTypes . number . isRequired ,
items : PropTypes . array . isRequired ,
} ;
}
render ( ) {
const {
isDraggingOver ,
nbrSlots ,
items ,
} = this . props ;
const fakePlaceholder = [ ] ;
const nbrOccupied = nbrOccupiedSlotsInCrate ( items ) ;
for ( var i = ( nbrSlots - nbrOccupied ) ; i > 0 ; i -- ) {
fakePlaceholder . push (
< div key = { i } style = { {
display : isDraggingOver ? 'none' : 'block' ,
border : '1px dashed #ccc' ,
width : '45px' ,
marginBottom : '5px' ,
} } > < / div >
) ;
}
return (
< React.Fragment >
{ fakePlaceholder }
< / React.Fragment >
) ;
}
}
/ * *
* Component that displays a list of < ProductCartItem >
* /
class Cart extends React . PureComponent {
static get propTypes ( ) {
return {
nbrSlots : PropTypes . number ,
itemHovered : PropTypes . string ,
data : PropTypes . object . isRequired ,
onToggleProgress : PropTypes . func ,
onToggleWarning : PropTypes . func ,
onToggleOverlayRemove : PropTypes . func ,
onClickRemoveItem : PropTypes . func ,
} ;
}
render ( ) {
const {
nbrSlots ,
itemHovered ,
data ,
onToggleProgress ,
onToggleWarning ,
onToggleOverlayRemove ,
onClickRemoveItem ,
} = this . props ;
const products = data . items . map ( ( item , index ) => {
let itemData ;
if ( data . itemsData && index in data . itemsData ) {
itemData = data . itemsData [ index ] ;
}
return (
< ProductCartItem
hovered = { item . id === itemHovered }
key = { item . id }
index = { index }
data = { itemData }
onToggleProgress = { onToggleProgress }
onToggleWarning = { onToggleWarning }
onToggleOverlayRemove = { onToggleOverlayRemove }
onClickRemoveItem = { onClickRemoveItem }
model = { item } >
< / ProductCartItem >
) ;
} ) ;
return (
< Droppable droppableId = { data . id } direction = "horizontal" >
{ ( provided , snapshot ) => (
< div
ref = { provided . innerRef }
{ ... provided . droppableProps }
style = { cartStyle (
provided . droppableProps . style ,
snapshot ,
) }
className = "items-cart-list" >
{ products }
{ provided . placeholder && (
< div style = { { display : 'none' } } >
{ provided . placeholder }
< / div >
) }
< FakePlaceholder
nbrSlots = { nbrSlots }
items = { data . items }
isDraggingOver = { snapshot . isDraggingOver } / >
< / div >
) }
< / Droppable >
) ;
}
}
/ * *
* Component that displays crate modes
* /
class CrateMode extends React . PureComponent {
static get propTypes ( ) {
return {
items : PropTypes . array . isRequired ,
mode : PropTypes . string . isRequired ,
onClickMode : PropTypes . func ,
} ;
}
constructor ( props ) {
super ( props ) ;
this . handleOnClickMode = this . handleOnClickMode . bind ( this ) ;
}
handleOnClickMode ( mode , e ) {
if ( this . props . onClickMode ) {
this . props . onClickMode ( mode ) ;
}
e . preventDefault ( ) ;
}
render ( ) {
const {
mode ,
items ,
} = this . props ;
return (
< div className = "crate-mode" >
{ items . map ( item => (
< a
key = { item . id }
className = { mode == item . id ? 'active' : '' }
onClick = { this . handleOnClickMode . bind ( this , item . id ) }
href = "#"
role = "button" > { item . name } < / a >
) ) }
< / div >
) ;
}
}
/ * *
* Component that displays the main crate with reminder rules .
* It includes < Cart > and rules
* /
class Crate extends React . PureComponent {
static get propTypes ( ) {
return {
rules : PropTypes . array ,
cart : PropTypes . element ,
} ;
}
render ( ) {
const {
rules ,
cart ,
} = this . props ;
return (
< div className = "crate" >
< div className = "crate-products" >
{ cart }
{ rules && rules . length > 0 && (
< div className = "crate-info" >
{ rules . map ( ( rule , index ) => (
2019-11-06 10:13:14 +08:00
< p key = { index } className = "rule" style = { { 'color' : rule . color ? rule . color : 'inherit' } } >
2019-10-02 12:16:07 +08:00
< img src = { ` /images ${ rule . icon } ` } / > < i > < strong > { rule . name } : < / strong > { rule . message } < / i >
< / p >
) ) }
< / div >
) }
< / div >
< / div >
) ;
}
}
/ * *
* Component that renders all things for order .
* It acts like - a layout , this component do nothing more .
* /
class OrderPanel extends React . PureComponent {
static get propTypes ( ) {
return {
title : PropTypes . string ,
description : PropTypes . string ,
crateMode : PropTypes . element ,
crate : PropTypes . element ,
summaryPrice : PropTypes . element ,
form : PropTypes . element ,
} ;
}
render ( ) {
const {
title ,
description ,
crateMode ,
crate ,
summaryPrice ,
form ,
} = this . props ;
return (
< section className = "panel" >
< h2 > { title } < / h2 >
< div className = "control" >
< p className = "description" > { description } < / p >
{ crateMode }
< / div >
{ crate }
< section className = "summary" >
{ summaryPrice }
{ form }
< / section >
< / section >
) ;
}
}
/ * *
* Components that renders the form to request quote .
* /
class OrderForm extends React . PureComponent {
static get propTypes ( ) {
return {
onClickSubmit : PropTypes . func ,
} ;
}
constructor ( props ) {
super ( props ) ;
this . state = { note : '' } ;
this . handleNoteChange = this . handleNoteChange . bind ( this ) ;
this . handleSubmit = this . handleSubmit . bind ( this ) ;
}
handleNoteChange ( event ) {
this . setState ( {
note : event . target . value ,
} ) ;
}
handleSubmit ( event ) {
if ( this . props . onClickSubmit ) {
this . props . onClickSubmit ( this . state . note ) ;
}
event . preventDefault ( ) ;
}
render ( ) {
return (
< div className = "summary-form" >
< form onSubmit = { this . handleSubmit } >
< textarea
value = { this . state . note }
onChange = { this . handleNoteChange }
rows = "5"
placeholder = "Additional notes" / >
< input type = "submit" value = "Request quote" / >
< / form >
< / div >
) ;
}
}
/ * *
* Components that displays the list of card that are used in the crate .
* It is a summary of purchase
* /
class OrderSumary extends React . PureComponent {
static get propTypes ( ) {
return {
2019-11-06 10:20:06 +08:00
currency : PropTypes . string ,
2019-10-02 12:16:07 +08:00
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 {
2019-11-06 10:20:06 +08:00
currency ,
2019-10-02 12:16:07 +08:00
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" >
2019-11-09 18:05:15 +08:00
< div >
2019-11-09 18:41:09 +08:00
{ ` ${ currency } ${ formatMoney ( mode . price ) } ` }
2019-11-09 18:05:15 +08:00
< button style = { { 'opacity' : '0' , 'cursor' : 'initial' } } >
< img src = "/images/shop/icon-remove.svg" / >
< / button >
< / div >
< span style = { {
'display' : 'inline-block' ,
'width' : '30px' ,
} } > & nbsp ; < / span >
2019-10-02 12:16:07 +08:00
< / td >
< / tr >
) }
< / thead >
< tbody >
{ summary . map ( ( item , index ) => {
let alert ;
let warning ;
if ( itemsData [ index ] ) {
alert = itemsData [ index ] ;
const warningsKeys = Object . keys ( alert . warnings ) ;
if ( warningsKeys && warningsKeys . length > 0 ) {
warning = alert . warnings [ warningsKeys [ 0 ] ] ;
}
}
return (
< tr key = { item . id }
className = { ` hoverable ${ item . selected ? 'selected' : '' } ` }
onClick = { this . handleOnClickSelectItem . bind ( this , index ) }
onMouseEnter = { this . handleOnMouseEnterItem . bind ( this , item . id ) }
onMouseLeave = { this . handleOnMouseLeaveItem } >
< td className = "item-card-name" >
< div > { item . name } < / div >
< / td >
< td className = "price" >
< div >
2019-11-09 18:41:09 +08:00
{ ` ${ currency } ${ formatMoney ( item . price ) } ` }
2019-10-02 12:16:07 +08:00
< 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 } ` }
/ >
) }
2019-11-09 18:05:15 +08:00
{ ! warning && (
< span style = { {
'display' : 'inline-block' ,
'width' : '30px' ,
} } > & nbsp ; < / span >
) }
2019-10-02 12:16:07 +08:00
< / td >
< / tr >
) ;
} ) }
< / tbody >
< tfoot >
< tr >
< td className = "item-card-name" > Price estimate < / td >
< td className = "price" >
2019-11-09 18:41:09 +08:00
< 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 >
2019-10-02 12:16:07 +08:00
2019-11-09 18:41:09 +08:00
< span style = { {
'display' : 'inline-block' ,
'width' : '30px' ,
} } > & nbsp ; < / span >
2019-10-02 12:16:07 +08:00
< / td >
< / tr >
< / tfoot >
< / table >
< / div >
) ;
}
}
/ * *
* Component that renders the backlog in the aside
* /
class Backlog extends React . PureComponent {
static get propTypes ( ) {
return {
2019-11-06 10:20:06 +08:00
currency : PropTypes . string ,
2019-10-02 12:16:07 +08:00
data : PropTypes . object . isRequired ,
items : PropTypes . object ,
onClickAddItem : PropTypes . func ,
} ;
}
static get defaultProps ( ) {
return {
items : { } ,
} ;
}
render ( ) {
const {
2019-11-06 10:20:06 +08:00
currency ,
2019-10-02 12:16:07 +08:00
data ,
items ,
onClickAddItem ,
} = this . props ;
const ordered _items = data . itemIds . map ( itemId => items [ itemId ] ) ;
const products = ordered _items . map ( ( item , index ) => {
return (
< ProductItem
key = { item . id }
id = { item . id }
index = { index }
name = { item . name }
price = { item . price }
2019-11-06 10:20:06 +08:00
currency = { currency }
2019-10-02 12:16:07 +08:00
image = { ` /images/ ${ item . image } ` }
specs = { item . specs }
onClickAddItem = { onClickAddItem }
> < / ProductItem >
) ;
} ) ;
return (
< Droppable
droppableId = { data . id }
isDropDisabled = { true } >
{ ( provided ) => (
< div
className = "backlog-container"
ref = { provided . innerRef }
{ ... provided . droppableProps } >
{ products }
{ provided . placeholder && (
< div style = { { display : 'none' } } >
{ provided . placeholder }
< / div >
) }
< / div >
) }
< / Droppable >
) ;
}
}
/ * *
* Component that render the entire shop
* /
class Shop extends React . PureComponent {
static get propTypes ( ) {
return {
data : PropTypes . object . isRequired ,
} ;
}
constructor ( props ) {
super ( props ) ;
this . state = this . props . data ;
this . handleCrateModeChange = this . handleCrateModeChange . bind ( this ) ;
this . handleOnDragEnd = this . handleOnDragEnd . bind ( this ) ;
this . handleDeleteItem = this . handleDeleteItem . bind ( this ) ;
this . handleDeleteAllItems = this . handleDeleteAllItems . bind ( this ) ;
this . handleMouseEnterItem = this . handleMouseEnterItem . bind ( this ) ;
this . handleMouseLeaveItem = this . handleMouseLeaveItem . bind ( this ) ;
this . handleClickAddItem = this . handleClickAddItem . bind ( this ) ;
this . checkAlerts = this . checkAlerts . bind ( this ) ;
this . handleToggleItemProgress = this . handleToggleItemProgress . bind ( this ) ;
this . handleToggleItemWarning = this . handleToggleItemWarning . bind ( this ) ;
this . handleClickSelectItem = this . handleClickSelectItem . bind ( this ) ;
this . handleClickSubmit = this . handleClickSubmit . bind ( this ) ;
this . handleToggleOverlayRemove = this . handleToggleOverlayRemove . bind ( this ) ;
}
componentDidMount ( ) {
// index 0 is a Kasli, we place it as a default conf on the crate.
const source = {
droppableId : 'backlog' ,
index : 0 ,
} ;
const destination = {
droppableId : 'cart' ,
index : 0 ,
} ;
this . handleOnDragEnd ( {
source ,
destination ,
draggableId : null ,
} ) ;
}
componentDidUpdate ( prevProps , prevState ) {
/ * *
* We check alerts ( reminder + warning ) only when items inside crate or
* crate mode change .
*
* In the function checkAlerts , we DO NOT want to change items as we will
* trigger again this function ( componentDidUpdate ) and thus ,
* making an infinite loop .
* /
if (
( prevState . columns . cart . items !== this . state . columns . cart . items ) ||
( prevState . currentMode !== this . state . currentMode )
) {
this . checkAlerts (
prevState . columns . cart . items ,
this . state . columns . cart . items ) ;
}
}
handleCrateModeChange ( mode ) {
this . setState ( {
currentMode : mode ,
} ) ;
}
handleDeleteItem ( index ) {
const cloned = Array . from ( this . state . columns . cart . items ) ;
cloned . splice ( index , 1 ) ;
this . setState ( {
... this . state ,
columns : {
... this . state . columns ,
cart : {
... this . state . columns . cart ,
items : cloned ,
} ,
} ,
} ) ;
}
handleDeleteAllItems ( ) {
this . setState ( {
... this . state ,
columns : {
... this . state . columns ,
cart : {
... this . state . columns . cart ,
items : [ ] ,
} ,
} ,
} ) ;
}
handleMouseEnterItem ( id ) {
this . setState ( {
... this . state ,
currentItemHovered : id ,
} ) ;
}
handleMouseLeaveItem ( ) {
this . setState ( {
... this . state ,
currentItemHovered : null ,
} ) ;
}
handleClickAddItem ( index ) {
const source = {
droppableId : 'backlog' ,
index : index ,
} ;
const destination = {
droppableId : 'cart' ,
index : this . state . columns . cart . items . length ,
} ;
this . handleOnDragEnd ( {
source ,
destination ,
draggableId : null ,
} ) ;
}
handleToggleItemProgress ( index , show ) {
const itemsCloned = Array . from ( this . state . columns . cart . items ) ;
this . setState ( {
... this . state ,
columns : {
... this . state . columns ,
cart : {
... this . state . columns . cart ,
items : itemsCloned . map ( ( item , i ) => {
return {
... item ,
showProgress : i === index ? show : false ,
showOverlayRemove : false ,
showWarning : false ,
} ;
} ) ,
}
} ,
} ) ;
}
handleToggleItemWarning ( index , show ) {
const itemsCloned = Array . from ( this . state . columns . cart . items ) ;
this . setState ( {
... this . state ,
columns : {
... this . state . columns ,
cart : {
... this . state . columns . cart ,
items : itemsCloned . map ( ( item , i ) => {
return {
... item ,
showWarning : i === index ? show : false ,
showProgress : false ,
showOverlayRemove : false ,
} ;
} ) ,
}
} ,
} ) ;
}
handleClickSelectItem ( index ) {
const itemsCloned = Array . from ( this . state . columns . cart . items ) ;
this . setState ( {
... this . state ,
columns : {
... this . state . columns ,
cart : {
... this . state . columns . cart ,
items : itemsCloned . map ( ( item , id ) => {
return { ... item , selected : id === index ? true : false } ;
} ) ,
}
} ,
} ) ;
}
handleToggleOverlayRemove ( index , show ) {
const itemsCloned = Array . from ( this . state . columns . cart . items ) ;
this . setState ( {
... this . state ,
columns : {
... this . state . columns ,
cart : {
... this . state . columns . cart ,
items : itemsCloned . map ( ( item , id ) => {
return {
... item ,
showOverlayRemove : id === index ? show : false ,
showProgress : false ,
showWarning : false ,
} ;
} ) ,
}
} ,
} ) ;
}
handleClickSubmit ( note ) {
const crate = {
items : [ ] ,
type : this . state . currentMode ,
} ;
const clonedCart = Array . from ( this . state . columns . cart . items ) ;
for ( const i in clonedCart ) {
const item = clonedCart [ i ] ;
crate . items . push ( {
'name' : item . name ,
} ) ;
}
const a = document . createElement ( 'a' ) ;
const num = ( new Date ( ) ) . getTime ( ) ;
const subject = ` [Order hardware] - Request Quote ` ;
2019-11-08 12:49:23 +08:00
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 ` ;
2019-10-02 12:16:07 +08:00
if ( note ) {
body = ` ${ body } \ n \ nAdditional note: \ n \ n ${ note ? note . trim ( ) : '' } ` ;
}
document . body . appendChild ( a ) ;
a . style = 'display: none' ;
a . href = ` mailto:sales@m-labs.hk?subject= ${ subject } &body= ${ encodeURIComponent ( body ) } ` ;
a . click ( ) ;
}
handleOnDragEnd ( result ) {
const {
source ,
destination ,
draggableId ,
} = result ;
if ( ! destination ) {
if ( source . droppableId === 'cart' ) {
this . setState ( {
... this . state ,
columns : {
... this . state . columns ,
[ source . droppableId ] : {
... this . state . columns [ source . droppableId ] ,
items : remove (
this . state . columns [ source . droppableId ] . items ,
source . index ,
) ,
} ,
} ,
} ) ;
}
return ;
}
switch ( source . droppableId ) {
case 'backlog' :
this . setState ( {
... this . state ,
columns : {
... this . state . columns ,
[ destination . droppableId ] : {
... this . state . columns [ destination . droppableId ] ,
items : copy (
this . state . items ,
this . state . columns [ source . droppableId ] ,
this . state . columns [ destination . droppableId ] ,
source ,
destination ,
) ,
} ,
} ,
} ) ;
break ;
case destination . droppableId :
this . setState ( {
... this . state ,
columns : {
... this . state . columns ,
[ destination . droppableId ] : {
... this . state . columns [ destination . droppableId ] ,
items : reorder (
this . state . columns [ destination . droppableId ] . items ,
source . index ,
destination . index ,
) ,
} ,
} ,
} ) ;
break ;
default :
break ;
}
}
checkAlerts ( prevItems , newItems ) {
console . log ( '--- START CHECKING CRATE WARNING ---' ) ;
const {
currentMode ,
crateModeSlots ,
crateRules ,
} = this . state ;
const itemsCloned = Array . from ( newItems ) ;
const itemsData = { } ;
const rules = { } ;
// check number of slot in crate
const nbrOccupied = nbrOccupiedSlotsInCrate ( newItems ) ;
if ( nbrOccupied > crateModeSlots [ currentMode ] ) {
rules [ crateRules . maxSlot . type ] = { ... crateRules . maxSlot } ;
}
// check the number of EEM connectors available for all Kasli
const idxK = itemsCloned . reduce ( ( prev , next , i ) => {
if ( next . type === 'kasli' || next . type === 'kasli-backplane' ) {
prev . push ( i ) ;
}
return prev ;
} , [ ] ) ;
for ( let i = 0 ; i <= idxK . length - 1 ; i ++ ) {
let slots ;
let nbUsedSlot = 0 ;
let nbrCurrentClock = 0 ;
let idx = idxK [ i ] ;
if ( i !== idxK . length - 1 ) {
slots = itemsCloned . slice ( idx + 1 , idxK [ i + 1 ] ) ;
} else {
slots = itemsCloned . slice ( idx + 1 ) ;
}
if ( i == 0 ) {
const slots _need _resource = itemsCloned . slice ( 0 , idx ) ;
const idx _need = slots _need _resource . findIndex ( e => ( e . rules && e . rules . resources ) ) ;
if ( idx _need != - 1 ) {
if ( idx _need in itemsData ) {
if ( 'warnings' in itemsData [ idx _need ] ) {
itemsData [ idx _need ] . warnings . resources = { ... itemsCloned [ idx _need ] . rules . resources } ;
} else {
itemsData [ idx _need ] . warnings = { } ;
itemsData [ idx _need ] . warnings . resources = { ... itemsCloned [ idx _need ] . rules . resources } ;
}
} else {
itemsData [ idx _need ] = { ... itemsCloned [ idx _need ] } ;
itemsData [ idx _need ] . warnings = { } ;
itemsData [ idx _need ] . warnings . resources = { ... itemsCloned [ idx _need ] . rules . resources } ;
}
}
}
nbUsedSlot = slots
. filter ( item => item . type !== 'idc-bnc' )
. reduce ( ( prev , next ) => {
return prev + next . slotOccupied ;
} , 0 ) ;
nbrCurrentClock = slots
. reduce ( ( prev , next ) => {
return next . type === 'clocker' ? prev + next . clockOccupied : prev ;
} , 0 ) ;
if ( idx in itemsData ) {
itemsData [ idx ] . nbrCurrentSlot = nbUsedSlot ;
itemsData [ idx ] . nbrCurrentClock = nbrCurrentClock ;
if ( ! ( 'warnings' in itemsData [ idx ] ) ) {
itemsData [ idx ] . warnings = { } ;
}
} else {
itemsData [ idx ] = { ... itemsCloned [ idx ] } ;
itemsData [ idx ] . nbrCurrentSlot = nbUsedSlot ;
itemsData [ idx ] . nbrCurrentClock = nbrCurrentClock ;
itemsData [ idx ] . warnings = { } ;
}
if ( nbUsedSlot > itemsCloned [ idx ] . nbrSlotMax ) {
rules [ itemsCloned [ idx ] . rules . maxSlot . type ] = { ... itemsCloned [ idx ] . rules . maxSlot } ;
itemsData [ idx ] . warnings . maxSlotWarning = { ... itemsCloned [ idx ] . rules . maxSlotWarning } ;
}
if ( nbrCurrentClock > itemsCloned [ idx ] . nbrClockMax ) {
rules [ itemsCloned [ idx ] . rules . maxClock . type ] = { ... itemsCloned [ idx ] . rules . maxClock } ;
itemsData [ idx ] . warnings . maxClockWarning = { ... itemsCloned [ idx ] . rules . maxClockWarning } ;
}
if ( itemsCloned . length > ( idx + 1 ) ) {
const ddkali = itemsCloned [ idx + 1 ] ;
if ( ddkali . type === 'kasli' || ddkali . type === 'kasli-backplane' ) {
rules [ ddkali . rules . follow . type ] = { ... ddkali . rules . follow } ;
}
}
}
// check number of clock connector available
const idxC = itemsCloned . reduce ( ( prev , next , i ) => {
if ( next . type === 'kasli' || next . type === 'kasli-backplane' || next . type === 'clocker' ) {
prev . push ( i ) ;
}
return prev ;
} , [ ] ) ;
for ( let i = 0 ; i <= idxC . length - 1 ; i ++ ) {
let slots ;
let nbrCurrentClock = 0 ;
let idx = idxC [ i ] ;
if ( i !== idxC . length - 1 ) {
slots = itemsCloned . slice ( idx + 1 , idxC [ i + 1 ] ) ;
} else {
slots = itemsCloned . slice ( idx + 1 ) ;
}
nbrCurrentClock = slots . reduce ( ( prev , next ) => {
return prev + next . clockOccupied ;
} , 0 ) ;
if ( idx in itemsData ) {
if ( itemsData [ idx ] . nbrCurrentClock ) {
itemsData [ idx ] . nbrCurrentClock += nbrCurrentClock ;
} else {
itemsData [ idx ] . nbrCurrentClock = nbrCurrentClock ;
}
} else {
itemsData [ idx ] = { ... itemsCloned [ idx ] } ;
itemsData [ idx ] . nbrCurrentClock = nbrCurrentClock ;
itemsData [ idx ] . warnings = { } ;
}
if ( nbrCurrentClock > itemsCloned [ idx ] . nbrClockMax ) {
rules [ itemsCloned [ idx ] . rules . maxClock . type ] = { ... itemsCloned [ idx ] . rules . maxClock } ;
itemsData [ idx ] . warnings . maxClockWarning = { ... itemsCloned [ idx ] . rules . maxClockWarning } ;
}
}
// check for number of recommanded EEM connectors
[ 'novo' , 'urukul' , 'koster' ] . map ( _type => {
if ( itemsCloned . find ( elem => elem . type === _type ) ) {
rules [ this . state . items [ _type ] . rules . connectors . type ] = { ... this . state . items [ _type ] . rules . connectors } ;
}
return _type ;
} ) ;
// check if IDC-BNC is correctly positionned (after Zotino or HD68)
const idxIDCBNC = itemsCloned . reduce ( ( prev , next , i ) => {
if ( next . type === 'idc-bnc' ) {
prev . push ( i ) ;
}
return prev ;
} , [ ] ) ;
for ( var i = idxIDCBNC . length - 1 ; i >= 0 ; i -- ) {
const ce = idxIDCBNC [ i ] ;
let shouldWarning = false ;
if ( ce == 0 ) {
shouldWarning = true ;
} else if ( ce >= 1 ) {
const pe = idxIDCBNC [ i ] - 1 ;
if ( itemsCloned [ pe ] . type !== 'zotino' &&
itemsCloned [ pe ] . type !== 'hd68' &&
itemsCloned [ pe ] . type !== 'idc-bnc' ) {
shouldWarning = true ;
}
}
if ( shouldWarning ) {
itemsData [ ce ] = { ... itemsCloned [ ce ] } ;
itemsData [ ce ] . warnings = { } ;
itemsData [ ce ] . warnings . wrong = { ... itemsCloned [ ce ] . rules . wrong } ;
}
}
// check number of IDC-BNC adapters for a Zotino and HD68-IDC
const idxZH = itemsCloned . reduce ( ( prev , next , i ) => {
if ( next . type === 'zotino' || next . type === 'hd68' ) {
prev . push ( i ) ;
}
return prev ;
} , [ ] ) ;
for ( let i = 0 ; i <= idxZH . length - 1 ; i ++ ) {
let slots ;
let nbUsedSlot = 0 ;
let idx = idxZH [ i ] ;
if ( i !== idxZH . length - 1 ) {
slots = itemsCloned . slice ( idx + 1 , idxZH [ i + 1 ] ) ;
} else {
slots = itemsCloned . slice ( idx + 1 ) ;
}
let stopCount = false ;
nbUsedSlot = slots . reduce ( ( prev , next , ci , ca ) => {
if ( ci === 0 && next . type === 'idc-bnc' ) {
return prev + 1 ;
} else if ( ca [ 0 ] . type === 'idc-bnc' && ci > 0 && ca [ ci - 1 ] . type === 'idc-bnc' ) {
if ( next . type !== 'idc-bnc' ) { stopCount = true ; }
return prev + ( next . type === 'idc-bnc' && ! stopCount ? 1 : 0 ) ;
}
return prev ;
} , 0 ) ;
if ( idx in itemsData ) {
itemsData [ idx ] . nbrCurrentSlot = nbUsedSlot ;
if ( ! ( 'warnings' in itemsData [ idx ] ) ) {
itemsData [ idx ] . warnings = { } ;
}
} else {
itemsData [ idx ] = { ... itemsCloned [ idx ] } ;
itemsData [ idx ] . nbrCurrentSlot = nbUsedSlot ;
itemsData [ idx ] . warnings = { } ;
}
if ( nbUsedSlot > 0 ) {
rules [ itemsCloned [ idx ] . rules . maxSlot . type ] = { ... itemsCloned [ idx ] . rules . maxSlot } ;
}
if ( nbUsedSlot > itemsCloned [ idx ] . nbrSlotMax ) {
itemsData [ idx ] . warnings . maxSlotWarning = { ... itemsCloned [ idx ] . rules . maxSlotWarning } ;
}
// check if HD68-IDC has at least 1 IDC-BNC adapter
if ( itemsCloned [ idx ] . type === 'hd68' ) {
let shouldWarning = false ;
if ( idx < itemsCloned . length - 1 ) {
if ( itemsCloned [ idx + 1 ] . type !== 'idc-bnc' ) {
shouldWarning = true ;
}
} else if ( idx === itemsCloned . length - 1 ) {
shouldWarning = true ;
}
if ( shouldWarning ) {
if ( idx in itemsData ) {
itemsData [ idx ] . warnings . minAdapter = { ... itemsCloned [ idx ] . rules . minAdapter } ;
} else {
itemsData [ idx ] = { ... itemsCloned [ idx ] } ;
itemsData [ idx ] . warnings = { } ;
itemsData [ idx ] . warnings . minAdapter = { ... itemsCloned [ idx ] . rules . minAdapter } ;
}
}
}
}
// update state with rules
this . setState ( {
... this . state ,
columns : {
... this . state . columns ,
cart : {
... this . state . columns . cart ,
itemsData : {
... itemsData ,
} ,
}
} ,
rules : {
... rules ,
} ,
} ) ;
}
render ( ) {
const {
2019-11-06 10:20:06 +08:00
currency ,
2019-10-02 12:16:07 +08:00
currentItemHovered ,
currentMode ,
crateModeSlots ,
crateModeItems ,
items ,
columns ,
rules ,
} = this . state ;
return (
< DragDropContext onDragEnd = { this . handleOnDragEnd } >
< Layout
className = "shop"
aside = {
< Backlog
2019-11-06 10:20:06 +08:00
currency = { currency }
2019-10-02 12:16:07 +08:00
items = { items }
data = { columns [ 'backlog' ] }
onClickAddItem = { this . handleClickAddItem } >
< / Backlog >
}
main = { (
< OrderPanel
title = "Order hardware"
description = "
2019-11-06 14:48:26 +08:00
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 . "
2019-10-02 12:16:07 +08:00
crateMode = {
< CrateMode
items = { crateModeItems }
mode = { currentMode }
onClickMode = { this . handleCrateModeChange } >
< / CrateMode > }
crate = {
< Crate
cart = {
< Cart
nbrSlots = { crateModeSlots [ currentMode ] }
data = { columns [ 'cart' ] }
itemHovered = { currentItemHovered }
onToggleProgress = { this . handleToggleItemProgress }
onToggleWarning = { this . handleToggleItemWarning }
onToggleOverlayRemove = { this . handleToggleOverlayRemove }
onClickRemoveItem = { this . handleDeleteItem } >
< / Cart >
}
rules = { Object . values ( rules ) . filter ( rule => rule ) } >
< / Crate >
}
summaryPrice = {
< OrderSumary
2019-11-06 10:20:06 +08:00
currency = { currency }
2019-10-02 12:16:07 +08:00
currentMode = { currentMode }
modes = { crateModeItems }
summary = { columns [ 'cart' ] . items }
itemsData = { columns . cart . itemsData }
onMouseEnterItem = { this . handleMouseEnterItem }
onMouseLeaveItem = { this . handleMouseLeaveItem }
onDeleteItem = { this . handleDeleteItem }
onDeleteAllItems = { this . handleDeleteAllItems }
onClickSelectItem = { this . handleClickSelectItem } >
< / OrderSumary >
}
form = {
< OrderForm
onClickSubmit = { this . handleClickSubmit } >
< / OrderForm >
} >
< / OrderPanel >
) } >
< / Layout >
< / DragDropContext >
) ;
}
}
ReactDOM . render (
< Shop data = { data } / > ,
document . querySelector ( '#root-shop' ) ,
) ;