1
0
Fork 0
web2019/static/js/shop.jsx

1717 lines
43 KiB
React
Raw Normal View History

feat(place-order): Adds order hardware system fix(place-order): Avoids colliding with page var feat(place-order): Adds drag(copy)/drop system Finally get this to work.. Still some minor issues while drag(copy)/drop This commit allows to drag(copy)/drop inside the crate. Allows to reorder the crate feat(place-order): Adds basic summary of shopping cart allows to delete item feat(place-order): Adds form also applies some cosmetcis css feat(place-order): Applies custom css to drag/drop process feat(place-order): Improves crate UI feat(place-order): Adds missing assets feat(place-order): Updates icon button add feat(place-order): Changes cart layout to increase space - moves up the control crate type (rack/desktop) - increase card size a bit - reduces some padding - adding some space above card title - re-organise react component to achieve this layout feat(place-order): Updates icon remove in summary price - uses icon instead of shitty html/css - re-organises react component fix(place-order): Reduces overlay gradient effect on the aside also increases padding-bottom to avoid overlay above content feat(place-order): Updates summary price layout feat(place-order): Updates CrateMode using data this avoid using hard text in component and also, adding crate mode in data.js will be helpful for other component too (e.g OrderSummary) feat(place-order): Adds crate mode in OrderSummary Allows to add fees to rack mode for exemple feat(place-order): Allows to delete all items in the crate at once feat(place-order): Updates typo uses currentMode instead of crateMode feat(place-order): Displays selection shadow on card when user hover delete button feat(place-order): Allows to add item to the crate feat(place-order): Corrects typo feat(place-order): Corrects layout for browser support feat(place-order): Adds first automatic rules this allows to test how things could be done feat(place-order): Allows to remove card when drop out of crate feat(place-order): Adds icon reminder to rules feat(place-order): Uses internal js production assets feat(place-order): Uses production file feat(place-order): Adds kali first as initialisation feat(place-order): Simulates slots in crate (desktop/rack) feat(place-order): Updates data that prepare for rules algo feat(place-order): Adds some rules feat(place-order): Removes rule 2kasli when no more kasli it's a fix feat(place-order): Corrects typo rules koster fix(place-order): Removes PWA prompt fix(place-order): Corrects size card inside crate refactor(place-order): Reduces padding between items in listing refactor(place-order): Uses USD currency feat(place-order): Upgrades algo for rules feat(place-order): Adds progress bar to kasli/kasli w/backplane feat(place-order): Refactores a bit rule handler feat(place-order): Adds all other rules fix(place-order): Corrects typo fix(place-order): Corrects count zotino/hd68 when IDC-BNC does not follow each others feat(place-order): Backups dev script call just in case For now, I can work with prod build even debugging production code feat(place-order): Adds super tooltip to progress bar feat(place-order): Adds tooltip for zotino/hd68 too refactor(place-order): Updates typo rule for Koster feat(place-order/WIP): Adds warning feat(place-order): Adds Mirny adds css changes from previous commit (i've forgotten) feat(place-order): Updates build feat(place-order): Set RJ45-DIO to occupy 2 EEMs feat(place-order): Make clocker progress bar visible It was hidden by horizontal scrollbar inside crate feat(place-order): Remove red warning for 2 kaslis following each other feat(place-order): Adds rule for cards that need a resource controller feat(place-order): Corrects typo, adds point to end of message feat(place-order): Changes idc-bnc info into a warning one feat(place-order): Moves down warning box feat(place-order): Updates some cosmetics css fix(place-order): Removes IDC-BNC from Kasli connectors count feat(place-order): Displays warning on hover warning icon feat(place-order): Updates design hover item in listing feat(place-order): Updates price estimate feat(place-order): Displays warning in summary feat(place-order): Allows to select item feat(place-order): Allows to send request quote through client email fix(place-order): Allows to click on remove inside summary list feat(place-order): Adds btn remove for each cards inside crate feat(place-order): Builds fix(place-order): Updates icon warning fix(place-order): Corrects recipient email address LOL, forgot to remove mine haha fix(place-order): Removes typo (kasli double) fix(place-order): Removes num from email title subject fix(place-order): Adds warning resources to mirny, zotino Also updates message text feat(place-order): Adds clocker counter feat(place-order): Uses warning for id68 instead of reminder feat(place-order): Adds crate type into the json feat(place-order): Adds btn order hardware in homepage refactor(place-order): Cleans a bit fix(place-order): Improves a bit ux remove item from crate fix(place-order): Improves a bit ux remove item from crate feat(place-order): Builds fix(place-order): Uses cursor pointer on remove button fix(place-order): Corrects card need a resources card chore(place-order): Removes call to ap.js fix(place-order): Tries with fixed height on warning icon fix(place-order): Adds fixed height to other element fix(place-order): Tries to remove up container height to let flex to its job fix(place-order): Removes .trim call which block multiline and augments row to 5 feat(place-order): Builds fix(place-order): Adds min-height for the crate
2019-10-02 12:16:07 +08:00
'use strict';
const {
DragDropContext,
Draggable,
Droppable
} = window.ReactBeautifulDnd;
const data = window.shop_data;
const productStyle = (style, snapshot, removeAnim, hovered, selected) => {
const custom = {
opacity: snapshot.isDragging ? .7 : 1,
backgroundColor: (hovered || selected) ? '#eae7f7' : 'initial',
};
if (!snapshot.isDropAnimating) {
return { ...style, ...custom};
}
if (removeAnim) {
// cannot be 0, but make it super tiny
custom.transitionDuration = '0.001s';
}
return {
...style,
...custom,
};
}
const cartStyle = (style, snapshot) => {
const isDraggingOver = snapshot.isDraggingOver;
return {
...style,
...{
backgroundColor: isDraggingOver ? '#f2f2f2' : '#f9f9f9',
border: isDraggingOver ? '1px dashed #ccc' : '0',
},
};
}
const nbrConnectorsStyle = (data) => {
if (!data || !data.nbrCurrentSlot) {
return {};
}
let p = data.nbrCurrentSlot * 100 / data.nbrSlotMax;
if (p > 100) {
p = 100;
}
return {
width: `${p}%`,
}
};
const nbrClocksStyle = (data) => {
if (!data || !data.nbrCurrentClock) {
return {};
}
let p = data.nbrCurrentClock * 100 / data.nbrClockMax;
if (p > 100) {
p = 100;
}
return {
width: `${p}%`,
}
};
const copy = (
model,
source,
destination,
droppableSource,
droppableDestination
) => {
const sourceClone = Array.from(source.itemIds);
const destClone = Array.from(destination.items);
const item = sourceClone[droppableSource.index];
destClone.splice(droppableDestination.index, 0, {
...model[item],
id: uuidv4(),
});
return destClone;
};
const reorder = (list, startIndex, endIndex) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
const remove = (list, startIndex) => {
const result = Array.from(list);
result.splice(startIndex, 1);
return result;
};
const nbrOccupiedSlotsInCrate = (items) => {
return items.reduce((prev, next) => {
return prev + (next.hp === 8 ? 2 : 1);
}, 0);
};
/**
* Component that provides a base layout (aside/main) for the page.
*/
class Layout extends React.PureComponent {
static get propTypes() {
return {
aside: PropTypes.any,
main: PropTypes.any,
};
}
render() {
const {
aside,
main,
} = this.props;
return (
<div className="layout">
<aside className="aside">{aside}</aside>
<section className="main">{main}</section>
</div>
);
}
}
/**
* Component that renders a product.
* Used in the aside (e.g backlog of product)
*/
class ProductItem extends React.PureComponent {
static get propTypes() {
return {
id: PropTypes.string.isRequired,
index: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
currency: PropTypes.string.isRequired,
image: PropTypes.string.isRequired,
specs: PropTypes.array,
onClickAddItem: PropTypes.func,
};
}
constructor(props) {
super(props);
this.handleOnClickAddItem = this.handleOnClickAddItem.bind(this);
}
handleOnClickAddItem(index, e) {
if (this.props.onClickAddItem) {
this.props.onClickAddItem(index);
}
e.preventDefault();
}
render() {
const {
id,
index,
name,
price,
currency,
image,
specs,
} = this.props;
const render_specs = (specs && specs.length > 0 && (
<ul>
{specs.map((spec, index) =>
<li key={index}>{spec}</li>
)}
</ul>
));
return (
<section className="productItem">
<div className="content">
<h3>{name}</h3>
<div className="price">{`${currency} ${price}`}</div>
{render_specs}
</div>
<div className="content">
<button onClick={this.handleOnClickAddItem.bind(this, index)}>
<img src="/images/shop/icon-add.svg" alt="add" />
</button>
<Draggable draggableId={id} index={index}>
{(provided, snapshot) => (
<React.Fragment>
<img
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={productStyle(
provided.draggableProps.style,
snapshot,
true, // hack: remove weird animation after a drop
)}
src={image} />
{/* Allows to simulate a clone */}
{snapshot.isDragging && (
<img className="" src={image} />
)}
</React.Fragment>
)}
</Draggable>
</div>
</section>
);
}
}
/**
* Component that renders a product.
* Used in the crate
*/
class ProductCartItem extends React.PureComponent {
static get propTypes() {
return {
hovered: PropTypes.bool,
index: PropTypes.number.isRequired,
model: PropTypes.object.isRequired,
data: PropTypes.object,
onToggleProgress: PropTypes.func,
onToggleWarning: PropTypes.func,
onToggleOverlayRemove: PropTypes.func,
onClickRemoveItem: PropTypes.func,
};
}
static get defaultProps() {
return {
hovered: false,
};
}
constructor(props) {
super(props);
this.handleOnMouseEnterItem = this.handleOnMouseEnterItem.bind(this);
this.handleOnMouseLeaveItem = this.handleOnMouseLeaveItem.bind(this);
this.handleOnMouseEnterWarningItem = this.handleOnMouseEnterWarningItem.bind(this);
this.handleOnMouseLeaveWarningItem = this.handleOnMouseLeaveWarningItem.bind(this);
this.handleOnMouseEnterRemoveItem = this.handleOnMouseEnterRemoveItem.bind(this);
this.handleOnMouseLeaveRemoveItem = this.handleOnMouseLeaveRemoveItem.bind(this);
this.handleOnClickRemoveItem = this.handleOnClickRemoveItem.bind(this);
}
handleOnMouseEnterItem(index, e) {
if (this.props.onToggleProgress) {
this.props.onToggleProgress(index, true);
}
e.preventDefault();
}
handleOnMouseLeaveItem(index, e) {
if (this.props.onToggleProgress) {
this.props.onToggleProgress(index, false);
}
e.preventDefault();
}
handleOnMouseEnterWarningItem(index, isWarning, e) {
if (!isWarning) {
return;
}
if (this.props.onToggleWarning) {
this.props.onToggleWarning(index, true);
}
e.preventDefault();
}
handleOnMouseLeaveWarningItem(index, isWarning, e) {
if (!isWarning) {
return;
}
if (this.props.onToggleWarning) {
this.props.onToggleWarning(index, false);
}
e.preventDefault();
}
handleOnMouseEnterRemoveItem(index, e) {
if (this.props.onToggleOverlayRemove) {
this.props.onToggleOverlayRemove(index, true);
}
e.preventDefault();
}
handleOnMouseLeaveRemoveItem(index, e) {
if (this.props.onToggleOverlayRemove) {
this.props.onToggleOverlayRemove(index, false);
}
e.preventDefault();
}
handleOnClickRemoveItem(index, e) {
if (this.props.onClickRemoveItem) {
this.props.onClickRemoveItem(index);
}
}
render() {
const {
hovered,
model,
data,
index,
} = this.props;
let warning;
if (data && data.warnings) {
const warningsKeys = Object.keys(data.warnings);
if (warningsKeys && warningsKeys.length > 0) {
// we display only the first warning
warning = data.warnings[warningsKeys[0]];
}
}
let render_progress;
if (model.showProgress && data) {
switch(model.type) {
case 'kasli':
case 'kasli-backplane':
render_progress = (
<div className="k-popup-connectors">
<p>{`${data.nbrCurrentSlot}/${model.nbrSlotMax} EEM connectors available`}</p>
<p>{`${data.nbrCurrentClock}/${model.nbrClockMax} Clock connectors available`}</p>
</div>
);
break;
case 'zotino':
case 'hd68':
render_progress = (
<div className="k-popup-connectors">
<p>{`${data.nbrCurrentSlot}/${model.nbrSlotMax} connectors available`}</p>
</div>
);
break;
case 'clocker':
render_progress = (
<div className="k-popup-connectors">
<p>{`${data.nbrCurrentClock}/${model.nbrClockMax} Clock connectors available`}</p>
</div>
);
break;
default:
break;
}
}
return (
<Draggable draggableId={model.id} index={index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{ ...productStyle(
provided.draggableProps.style,
snapshot,
true,
hovered ? true : false,
model.selected ? true : false,
)}}
onMouseEnter={this.handleOnMouseEnterRemoveItem.bind(this, index)}
onMouseLeave={this.handleOnMouseLeaveRemoveItem.bind(this, index)}
>
{/* warning container */}
<div
style={{'height': '24px'}}
className="progress-container warning"
onMouseEnter={this.handleOnMouseEnterWarningItem.bind(this, index, warning)}
onMouseLeave={this.handleOnMouseLeaveWarningItem.bind(this, index, warning)}>
{warning && (
<img className="alert-warning" src={warning ? `/images${warning.icon}` : null} />
)}
{warning && model.showWarning && (
<div className="k-popup-warning">
<p className="rule warning">
<i>{warning.message}</i>
</p>
</div>
)}
</div>
<h6>{model.name}</h6>
<div
style={{'height': '250px'}}
onMouseEnter={this.handleOnMouseEnterRemoveItem.bind(this, index)}
>
<img
className='item-cart'
src={`/images${model.image}`} />
</div>
{/* remove container */}
<div
style={{'display': model.showOverlayRemove ? 'flex':'none'}}
className="overlayRemove"
onClick={this.handleOnClickRemoveItem.bind(this, index)}>
<img src="/images/shop/icon-remove.svg" alt="rm"/>
<p>remove</p>
</div>
{/* progression container */}
<div
style={{'height': '22px'}}
className="progress-container"
onMouseEnter={this.handleOnMouseEnterItem.bind(this, index)}
onMouseLeave={this.handleOnMouseLeaveItem.bind(this, index)}>
{model.nbrSlotMax > 0 && (
<div className="nbr-connectors">
<div style={{ ...nbrConnectorsStyle(data)}}></div>
</div>
)}
{model.nbrClockMax > 0 && (
<div className="nbr-clocks">
<div style={{ ...nbrClocksStyle(data)}}></div>
</div>
)}
{/* progress info when mouse over */}
{render_progress}
</div>
</div>
)}
</Draggable>
);
}
}
/**
* Component that displays a placeholder inside crate.
* Allows to display how it remains space for the current crate.
*/
class FakePlaceholder extends React.PureComponent {
static get propTypes() {
return {
isDraggingOver: PropTypes.bool,
nbrSlots: PropTypes.number.isRequired,
items: PropTypes.array.isRequired,
};
}
render() {
const {
isDraggingOver,
nbrSlots,
items,
} = this.props;
const fakePlaceholder = [];
const nbrOccupied = nbrOccupiedSlotsInCrate(items);
for (var i = (nbrSlots - nbrOccupied); i > 0; i--) {
fakePlaceholder.push(
<div key={i} style={{
display: isDraggingOver ? 'none' : 'block',
border: '1px dashed #ccc',
width: '45px',
marginBottom: '5px',
}}></div>
);
}
return (
<React.Fragment>
{fakePlaceholder}
</React.Fragment>
);
}
}
/**
* Component that displays a list of <ProductCartItem>
*/
class Cart extends React.PureComponent {
static get propTypes() {
return {
nbrSlots: PropTypes.number,
itemHovered: PropTypes.string,
data: PropTypes.object.isRequired,
onToggleProgress: PropTypes.func,
onToggleWarning: PropTypes.func,
onToggleOverlayRemove: PropTypes.func,
onClickRemoveItem: PropTypes.func,
};
}
render() {
const {
nbrSlots,
itemHovered,
data,
onToggleProgress,
onToggleWarning,
onToggleOverlayRemove,
onClickRemoveItem,
} = this.props;
const products = data.items.map((item, index) => {
let itemData;
if (data.itemsData && index in data.itemsData) {
itemData = data.itemsData[index];
}
return (
<ProductCartItem
hovered={item.id === itemHovered}
key={item.id}
index={index}
data={itemData}
onToggleProgress={onToggleProgress}
onToggleWarning={onToggleWarning}
onToggleOverlayRemove={onToggleOverlayRemove}
onClickRemoveItem={onClickRemoveItem}
model={item}>
</ProductCartItem>
);
});
return (
<Droppable droppableId={data.id} direction="horizontal">
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
style={cartStyle(
provided.droppableProps.style,
snapshot,
)}
className="items-cart-list">
{products}
{provided.placeholder && (
<div style={{ display: 'none' }}>
{provided.placeholder}
</div>
)}
<FakePlaceholder
nbrSlots={nbrSlots}
items={data.items}
isDraggingOver={snapshot.isDraggingOver} />
</div>
)}
</Droppable>
);
}
}
/**
* Component that displays crate modes
*/
class CrateMode extends React.PureComponent {
static get propTypes() {
return {
items: PropTypes.array.isRequired,
mode: PropTypes.string.isRequired,
onClickMode: PropTypes.func,
};
}
constructor(props) {
super(props);
this.handleOnClickMode = this.handleOnClickMode.bind(this);
}
handleOnClickMode(mode, e) {
if (this.props.onClickMode) {
this.props.onClickMode(mode);
}
e.preventDefault();
}
render() {
const {
mode,
items,
} = this.props;
return (
<div className="crate-mode">
{items.map(item => (
<a
key={item.id}
className={mode == item.id ? 'active' : ''}
onClick={this.handleOnClickMode.bind(this, item.id)}
href="#"
role="button">{item.name}</a>
))}
</div>
);
}
}
/**
* Component that displays the main crate with reminder rules.
* It includes <Cart> and rules
*/
class Crate extends React.PureComponent {
static get propTypes() {
return {
rules: PropTypes.array,
cart: PropTypes.element,
};
}
render() {
const {
rules,
cart,
} = this.props;
return (
<div className="crate">
<div className="crate-products">
{cart}
{rules && rules.length > 0 && (
<div className="crate-info">
{rules.map((rule, index) => (
<p key={index} className="rule" style={{'color': rule.color ? rule.color : 'inherit'}}>
feat(place-order): Adds order hardware system fix(place-order): Avoids colliding with page var feat(place-order): Adds drag(copy)/drop system Finally get this to work.. Still some minor issues while drag(copy)/drop This commit allows to drag(copy)/drop inside the crate. Allows to reorder the crate feat(place-order): Adds basic summary of shopping cart allows to delete item feat(place-order): Adds form also applies some cosmetcis css feat(place-order): Applies custom css to drag/drop process feat(place-order): Improves crate UI feat(place-order): Adds missing assets feat(place-order): Updates icon button add feat(place-order): Changes cart layout to increase space - moves up the control crate type (rack/desktop) - increase card size a bit - reduces some padding - adding some space above card title - re-organise react component to achieve this layout feat(place-order): Updates icon remove in summary price - uses icon instead of shitty html/css - re-organises react component fix(place-order): Reduces overlay gradient effect on the aside also increases padding-bottom to avoid overlay above content feat(place-order): Updates summary price layout feat(place-order): Updates CrateMode using data this avoid using hard text in component and also, adding crate mode in data.js will be helpful for other component too (e.g OrderSummary) feat(place-order): Adds crate mode in OrderSummary Allows to add fees to rack mode for exemple feat(place-order): Allows to delete all items in the crate at once feat(place-order): Updates typo uses currentMode instead of crateMode feat(place-order): Displays selection shadow on card when user hover delete button feat(place-order): Allows to add item to the crate feat(place-order): Corrects typo feat(place-order): Corrects layout for browser support feat(place-order): Adds first automatic rules this allows to test how things could be done feat(place-order): Allows to remove card when drop out of crate feat(place-order): Adds icon reminder to rules feat(place-order): Uses internal js production assets feat(place-order): Uses production file feat(place-order): Adds kali first as initialisation feat(place-order): Simulates slots in crate (desktop/rack) feat(place-order): Updates data that prepare for rules algo feat(place-order): Adds some rules feat(place-order): Removes rule 2kasli when no more kasli it's a fix feat(place-order): Corrects typo rules koster fix(place-order): Removes PWA prompt fix(place-order): Corrects size card inside crate refactor(place-order): Reduces padding between items in listing refactor(place-order): Uses USD currency feat(place-order): Upgrades algo for rules feat(place-order): Adds progress bar to kasli/kasli w/backplane feat(place-order): Refactores a bit rule handler feat(place-order): Adds all other rules fix(place-order): Corrects typo fix(place-order): Corrects count zotino/hd68 when IDC-BNC does not follow each others feat(place-order): Backups dev script call just in case For now, I can work with prod build even debugging production code feat(place-order): Adds super tooltip to progress bar feat(place-order): Adds tooltip for zotino/hd68 too refactor(place-order): Updates typo rule for Koster feat(place-order/WIP): Adds warning feat(place-order): Adds Mirny adds css changes from previous commit (i've forgotten) feat(place-order): Updates build feat(place-order): Set RJ45-DIO to occupy 2 EEMs feat(place-order): Make clocker progress bar visible It was hidden by horizontal scrollbar inside crate feat(place-order): Remove red warning for 2 kaslis following each other feat(place-order): Adds rule for cards that need a resource controller feat(place-order): Corrects typo, adds point to end of message feat(place-order): Changes idc-bnc info into a warning one feat(place-order): Moves down warning box feat(place-order): Updates some cosmetics css fix(place-order): Removes IDC-BNC from Kasli connectors count feat(place-order): Displays warning on hover warning icon feat(place-order): Updates design hover item in listing feat(place-order): Updates price estimate feat(place-order): Displays warning in summary feat(place-order): Allows to select item feat(place-order): Allows to send request quote through client email fix(place-order): Allows to click on remove inside summary list feat(place-order): Adds btn remove for each cards inside crate feat(place-order): Builds fix(place-order): Updates icon warning fix(place-order): Corrects recipient email address LOL, forgot to remove mine haha fix(place-order): Removes typo (kasli double) fix(place-order): Removes num from email title subject fix(place-order): Adds warning resources to mirny, zotino Also updates message text feat(place-order): Adds clocker counter feat(place-order): Uses warning for id68 instead of reminder feat(place-order): Adds crate type into the json feat(place-order): Adds btn order hardware in homepage refactor(place-order): Cleans a bit fix(place-order): Improves a bit ux remove item from crate fix(place-order): Improves a bit ux remove item from crate feat(place-order): Builds fix(place-order): Uses cursor pointer on remove button fix(place-order): Corrects card need a resources card chore(place-order): Removes call to ap.js fix(place-order): Tries with fixed height on warning icon fix(place-order): Adds fixed height to other element fix(place-order): Tries to remove up container height to let flex to its job fix(place-order): Removes .trim call which block multiline and augments row to 5 feat(place-order): Builds fix(place-order): Adds min-height for the crate
2019-10-02 12:16:07 +08:00
<img src={`/images${rule.icon}`} /> <i><strong>{rule.name}:</strong> {rule.message}</i>
</p>
))}
</div>
)}
</div>
</div>
);
}
}
/**
* Component that renders all things for order.
* It acts like-a layout, this component do nothing more.
*/
class OrderPanel extends React.PureComponent {
static get propTypes() {
return {
title: PropTypes.string,
description: PropTypes.string,
crateMode: PropTypes.element,
crate: PropTypes.element,
summaryPrice: PropTypes.element,
form: PropTypes.element,
};
}
render() {
const {
title,
description,
crateMode,
crate,
summaryPrice,
form,
} = this.props;
return (
<section className="panel">
<h2>{title}</h2>
<div className="control">
<p className="description">{description}</p>
{crateMode}
</div>
{crate}
<section className="summary">
{summaryPrice}
{form}
</section>
</section>
);
}
}
/**
* Components that renders the form to request quote.
*/
class OrderForm extends React.PureComponent {
static get propTypes() {
return {
onClickSubmit: PropTypes.func,
};
}
constructor(props) {
super(props);
this.state = {note: ''};
this.handleNoteChange = this.handleNoteChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleNoteChange(event) {
this.setState({
note: event.target.value,
});
}
handleSubmit(event) {
if (this.props.onClickSubmit) {
this.props.onClickSubmit(this.state.note);
}
event.preventDefault();
}
render() {
return (
<div className="summary-form">
<form onSubmit={this.handleSubmit}>
<textarea
value={this.state.note}
onChange={this.handleNoteChange}
rows="5"
placeholder="Additional notes" />
<input type="submit" value="Request quote" />
</form>
</div>
);
}
}
/**
* Components that displays the list of card that are used in the crate.
* It is a summary of purchase
*/
class OrderSumary extends React.PureComponent {
static get propTypes() {
return {
modes: PropTypes.array,
currentMode: PropTypes.string,
summary: PropTypes.array,
itemsData: PropTypes.array,
onDeleteItem: PropTypes.func,
onDeleteAllItems: PropTypes.func,
onMouseEnterItem: PropTypes.func,
onMouseLeaveItem: PropTypes.func,
onClickSelectItem: PropTypes.func,
};
}
constructor(props) {
super(props);
this.handleOnDeleteItem = this.handleOnDeleteItem.bind(this);
this.handleOnDeleteAllItems = this.handleOnDeleteAllItems.bind(this);
this.handleOnMouseEnterItem = this.handleOnMouseEnterItem.bind(this);
this.handleOnMouseLeaveItem = this.handleOnMouseLeaveItem.bind(this);
this.handleOnClickSelectItem = this.handleOnClickSelectItem.bind(this);
}
handleOnDeleteItem(index, e) {
if (this.props.onDeleteItem) {
this.props.onDeleteItem(index);
}
e.preventDefault();
}
handleOnDeleteAllItems(e) {
if (this.props.onDeleteAllItems) {
this.props.onDeleteAllItems();
}
e.preventDefault();
}
handleOnMouseEnterItem(id, e) {
if (this.props.onMouseEnterItem) {
this.props.onMouseEnterItem(id);
}
e.preventDefault();
}
handleOnMouseLeaveItem(e) {
if (this.props.onMouseLeaveItem) {
this.props.onMouseLeaveItem();
}
e.preventDefault();
}
handleOnClickSelectItem(index, e) {
if (e.target.tagName !== 'IMG') {
if (this.props.onClickSelectItem) {
this.props.onClickSelectItem(index);
}
}
return e.preventDefault();
}
render() {
const {
modes,
currentMode,
summary,
itemsData,
} = this.props;
const mode = modes.find(elem => elem.id === currentMode);
return (
<div className="summary-price">
<table>
<thead>
<tr>
<td colSpan="2" className="summary-remove-all">
<span className="item-card-name">Remove all cards</span>
<button onClick={this.handleOnDeleteAllItems}>
<img src="/images/shop/icon-remove.svg" />
</button>
</td>
</tr>
{mode && (
<tr>
<td className="item-card-name">{mode.name}</td>
<td className="price">
{`${mode.currency} ${mode.price}`}
<button style={{'opacity': '0', 'cursor': 'initial'}}>
<img src="/images/shop/icon-remove.svg" />
</button>
</td>
</tr>
)}
</thead>
<tbody>
{summary.map((item, index) => {
let alert;
let warning;
if (itemsData[index]) {
alert = itemsData[index];
const warningsKeys = Object.keys(alert.warnings);
if (warningsKeys && warningsKeys.length > 0) {
warning = alert.warnings[warningsKeys[0]];
}
}
return (
<tr key={item.id}
className={`hoverable ${item.selected ? 'selected' : ''}`}
onClick={this.handleOnClickSelectItem.bind(this, index)}
onMouseEnter={this.handleOnMouseEnterItem.bind(this, item.id)}
onMouseLeave={this.handleOnMouseLeaveItem}>
<td className="item-card-name">
<div>{item.name}</div>
</td>
<td className="price">
<div>
{`${item.currency} ${item.price}`}
<button onClick={this.handleOnDeleteItem.bind(this, index)}>
<img src="/images/shop/icon-remove.svg" />
</button>
</div>
{warning && (
<img
style={{'marginLeft': '10px'}}
className="alert-warning"
src={`/images/${warning.icon}`}
/>
)}
</td>
</tr>
);
})}
</tbody>
<tfoot>
<tr>
<td className="item-card-name">Price estimate</td>
<td className="price">
{summary.length ? (
`${summary[0].currency} ${summary.reduce(
(prev, next) => {
return prev + next.price;
}, 0
) + mode.price}`
) : (
`${mode.currency} ${mode.price}`
)}
<button style={{'opacity': '0', 'cursor': 'initial'}}>
<img src="/images/shop/icon-remove.svg" alt="icon remove"/>
</button>
</td>
</tr>
</tfoot>
</table>
</div>
);
}
}
/**
* Component that renders the backlog in the aside
*/
class Backlog extends React.PureComponent {
static get propTypes() {
return {
data: PropTypes.object.isRequired,
items: PropTypes.object,
onClickAddItem: PropTypes.func,
};
}
static get defaultProps() {
return {
items: {},
};
}
render() {
const {
data,
items,
onClickAddItem,
} = this.props;
const ordered_items = data.itemIds.map(itemId => items[itemId]);
const products = ordered_items.map((item, index) => {
return (
<ProductItem
key={item.id}
id={item.id}
index={index}
name={item.name}
price={item.price}
currency={item.currency}
image={`/images/${item.image}`}
specs={item.specs}
onClickAddItem={onClickAddItem}
></ProductItem>
);
});
return (
<Droppable
droppableId={data.id}
isDropDisabled={true}>
{(provided) => (
<div
className="backlog-container"
ref={provided.innerRef}
{...provided.droppableProps}>
{products}
{provided.placeholder && (
<div style={{ display: 'none' }}>
{provided.placeholder}
</div>
)}
</div>
)}
</Droppable>
);
}
}
/**
* Component that render the entire shop
*/
class Shop extends React.PureComponent {
static get propTypes() {
return {
data: PropTypes.object.isRequired,
};
}
constructor(props) {
super(props);
this.state = this.props.data;
this.handleCrateModeChange = this.handleCrateModeChange.bind(this);
this.handleOnDragEnd = this.handleOnDragEnd.bind(this);
this.handleDeleteItem = this.handleDeleteItem.bind(this);
this.handleDeleteAllItems = this.handleDeleteAllItems.bind(this);
this.handleMouseEnterItem = this.handleMouseEnterItem.bind(this);
this.handleMouseLeaveItem = this.handleMouseLeaveItem.bind(this);
this.handleClickAddItem = this.handleClickAddItem.bind(this);
this.checkAlerts = this.checkAlerts.bind(this);
this.handleToggleItemProgress = this.handleToggleItemProgress.bind(this);
this.handleToggleItemWarning = this.handleToggleItemWarning.bind(this);
this.handleClickSelectItem = this.handleClickSelectItem.bind(this);
this.handleClickSubmit = this.handleClickSubmit.bind(this);
this.handleToggleOverlayRemove = this.handleToggleOverlayRemove.bind(this);
}
componentDidMount() {
// index 0 is a Kasli, we place it as a default conf on the crate.
const source = {
droppableId: 'backlog',
index: 0,
};
const destination = {
droppableId: 'cart',
index: 0,
};
this.handleOnDragEnd({
source,
destination,
draggableId: null,
});
}
componentDidUpdate(prevProps, prevState) {
/**
* We check alerts (reminder + warning) only when items inside crate or
* crate mode change.
*
* In the function checkAlerts, we DO NOT want to change items as we will
* trigger again this function (componentDidUpdate) and thus,
* making an infinite loop.
*/
if (
(prevState.columns.cart.items !== this.state.columns.cart.items) ||
(prevState.currentMode !== this.state.currentMode)
) {
this.checkAlerts(
prevState.columns.cart.items,
this.state.columns.cart.items);
}
}
handleCrateModeChange(mode) {
this.setState({
currentMode: mode,
});
}
handleDeleteItem(index) {
const cloned = Array.from(this.state.columns.cart.items);
cloned.splice(index, 1);
this.setState({
...this.state,
columns: {
...this.state.columns,
cart: {
...this.state.columns.cart,
items: cloned,
},
},
});
}
handleDeleteAllItems() {
this.setState({
...this.state,
columns: {
...this.state.columns,
cart: {
...this.state.columns.cart,
items: [],
},
},
});
}
handleMouseEnterItem(id) {
this.setState({
...this.state,
currentItemHovered: id,
});
}
handleMouseLeaveItem() {
this.setState({
...this.state,
currentItemHovered: null,
});
}
handleClickAddItem(index) {
const source = {
droppableId: 'backlog',
index: index,
};
const destination = {
droppableId: 'cart',
index: this.state.columns.cart.items.length,
};
this.handleOnDragEnd({
source,
destination,
draggableId: null,
});
}
handleToggleItemProgress(index, show) {
const itemsCloned = Array.from(this.state.columns.cart.items);
this.setState({
...this.state,
columns: {
...this.state.columns,
cart: {
...this.state.columns.cart,
items: itemsCloned.map((item, i) => {
return {
...item,
showProgress: i === index ? show : false,
showOverlayRemove: false,
showWarning: false,
};
}),
}
},
});
}
handleToggleItemWarning(index, show) {
const itemsCloned = Array.from(this.state.columns.cart.items);
this.setState({
...this.state,
columns: {
...this.state.columns,
cart: {
...this.state.columns.cart,
items: itemsCloned.map((item, i) => {
return {
...item,
showWarning: i === index ? show : false,
showProgress: false,
showOverlayRemove: false,
};
}),
}
},
});
}
handleClickSelectItem(index) {
const itemsCloned = Array.from(this.state.columns.cart.items);
this.setState({
...this.state,
columns: {
...this.state.columns,
cart: {
...this.state.columns.cart,
items: itemsCloned.map((item, id) => {
return {...item, selected: id === index ? true : false};
}),
}
},
});
}
handleToggleOverlayRemove(index, show) {
const itemsCloned = Array.from(this.state.columns.cart.items);
this.setState({
...this.state,
columns: {
...this.state.columns,
cart: {
...this.state.columns.cart,
items: itemsCloned.map((item, id) => {
return {
...item,
showOverlayRemove: id === index ? show : false,
showProgress: false,
showWarning: false,
};
}),
}
},
});
}
handleClickSubmit(note) {
const crate = {
items: [],
type: this.state.currentMode,
};
const clonedCart = Array.from(this.state.columns.cart.items);
for (const i in clonedCart) {
const item = clonedCart[i];
crate.items.push({
'name': item.name,
});
}
const a = document.createElement('a');
const num = (new Date()).getTime();
const subject = `[Order hardware] - Request Quote`;
let body = `Hello!\n\nI would like to request a quotation for my below configuration:\n\n${JSON.stringify(crate)}\n\n`;
if (note) {
body = `${body}\n\nAdditional note:\n\n${note ? note.trim() : ''}`;
}
document.body.appendChild(a);
a.style = 'display: none';
a.href = `mailto:sales@m-labs.hk?subject=${subject}&body=${encodeURIComponent(body)}`;
a.click();
}
handleOnDragEnd(result) {
const {
source,
destination,
draggableId,
} = result;
if (!destination) {
if (source.droppableId === 'cart') {
this.setState({
...this.state,
columns: {
...this.state.columns,
[source.droppableId]: {
...this.state.columns[source.droppableId],
items: remove(
this.state.columns[source.droppableId].items,
source.index,
),
},
},
});
}
return;
}
switch(source.droppableId) {
case 'backlog':
this.setState({
...this.state,
columns: {
...this.state.columns,
[destination.droppableId]: {
...this.state.columns[destination.droppableId],
items: copy(
this.state.items,
this.state.columns[source.droppableId],
this.state.columns[destination.droppableId],
source,
destination,
),
},
},
});
break;
case destination.droppableId:
this.setState({
...this.state,
columns: {
...this.state.columns,
[destination.droppableId]: {
...this.state.columns[destination.droppableId],
items: reorder(
this.state.columns[destination.droppableId].items,
source.index,
destination.index,
),
},
},
});
break;
default:
break;
}
}
checkAlerts(prevItems, newItems) {
console.log('--- START CHECKING CRATE WARNING ---');
const {
currentMode,
crateModeSlots,
crateRules,
} = this.state;
const itemsCloned = Array.from(newItems);
const itemsData = {};
const rules = {};
// check number of slot in crate
const nbrOccupied = nbrOccupiedSlotsInCrate(newItems);
if (nbrOccupied > crateModeSlots[currentMode]) {
rules[crateRules.maxSlot.type] = {...crateRules.maxSlot};
}
// check the number of EEM connectors available for all Kasli
const idxK = itemsCloned.reduce((prev, next, i) => {
if (next.type === 'kasli' || next.type === 'kasli-backplane') {
prev.push(i);
}
return prev;
}, []);
for (let i = 0; i <= idxK.length - 1; i++) {
let slots;
let nbUsedSlot = 0;
let nbrCurrentClock = 0;
let idx = idxK[i];
if (i !== idxK.length - 1) {
slots = itemsCloned.slice(idx + 1, idxK[i + 1]);
} else {
slots = itemsCloned.slice(idx + 1);
}
if (i == 0) {
const slots_need_resource = itemsCloned.slice(0, idx);
const idx_need = slots_need_resource.findIndex(e => (e.rules && e.rules.resources));
if (idx_need != -1) {
if (idx_need in itemsData) {
if ('warnings' in itemsData[idx_need]) {
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
} else {
itemsData[idx_need].warnings = {};
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
}
} else {
itemsData[idx_need] = {...itemsCloned[idx_need]};
itemsData[idx_need].warnings = {};
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
}
}
}
nbUsedSlot = slots
.filter(item => item.type !== 'idc-bnc')
.reduce((prev, next) => {
return prev + next.slotOccupied;
}, 0);
nbrCurrentClock = slots
.reduce((prev, next) => {
return next.type === 'clocker' ? prev + next.clockOccupied : prev;
}, 0);
if (idx in itemsData) {
itemsData[idx].nbrCurrentSlot = nbUsedSlot;
itemsData[idx].nbrCurrentClock = nbrCurrentClock;
if (!('warnings' in itemsData[idx])) {
itemsData[idx].warnings = {};
}
} else {
itemsData[idx] = {...itemsCloned[idx]};
itemsData[idx].nbrCurrentSlot = nbUsedSlot;
itemsData[idx].nbrCurrentClock = nbrCurrentClock;
itemsData[idx].warnings = {};
}
if (nbUsedSlot > itemsCloned[idx].nbrSlotMax) {
rules[itemsCloned[idx].rules.maxSlot.type] = {...itemsCloned[idx].rules.maxSlot};
itemsData[idx].warnings.maxSlotWarning = {...itemsCloned[idx].rules.maxSlotWarning};
}
if (nbrCurrentClock > itemsCloned[idx].nbrClockMax) {
rules[itemsCloned[idx].rules.maxClock.type] = {...itemsCloned[idx].rules.maxClock};
itemsData[idx].warnings.maxClockWarning = {...itemsCloned[idx].rules.maxClockWarning};
}
if (itemsCloned.length > (idx + 1)) {
const ddkali = itemsCloned[idx + 1];
if (ddkali.type === 'kasli' || ddkali.type === 'kasli-backplane') {
rules[ddkali.rules.follow.type] = {...ddkali.rules.follow};
}
}
}
// check number of clock connector available
const idxC = itemsCloned.reduce((prev, next, i) => {
if (next.type === 'kasli' || next.type === 'kasli-backplane' || next.type === 'clocker') {
prev.push(i);
}
return prev;
}, []);
for (let i = 0; i <= idxC.length - 1; i++) {
let slots;
let nbrCurrentClock = 0;
let idx = idxC[i];
if (i !== idxC.length - 1) {
slots = itemsCloned.slice(idx + 1, idxC[i + 1]);
} else {
slots = itemsCloned.slice(idx + 1);
}
nbrCurrentClock = slots.reduce((prev, next) => {
return prev + next.clockOccupied;
}, 0);
if (idx in itemsData) {
if (itemsData[idx].nbrCurrentClock) {
itemsData[idx].nbrCurrentClock += nbrCurrentClock;
} else {
itemsData[idx].nbrCurrentClock = nbrCurrentClock;
}
} else {
itemsData[idx] = {...itemsCloned[idx]};
itemsData[idx].nbrCurrentClock = nbrCurrentClock;
itemsData[idx].warnings = {};
}
if (nbrCurrentClock > itemsCloned[idx].nbrClockMax) {
rules[itemsCloned[idx].rules.maxClock.type] = {...itemsCloned[idx].rules.maxClock};
itemsData[idx].warnings.maxClockWarning = {...itemsCloned[idx].rules.maxClockWarning};
}
}
// check for number of recommanded EEM connectors
['novo', 'urukul', 'koster'].map(_type => {
if (itemsCloned.find(elem => elem.type === _type)) {
rules[this.state.items[_type].rules.connectors.type] = {...this.state.items[_type].rules.connectors};
}
return _type;
});
// check if IDC-BNC is correctly positionned (after Zotino or HD68)
const idxIDCBNC = itemsCloned.reduce((prev, next, i) => {
if (next.type === 'idc-bnc') {
prev.push(i);
}
return prev;
}, []);
for (var i = idxIDCBNC.length - 1; i >= 0; i--) {
const ce = idxIDCBNC[i];
let shouldWarning = false;
if (ce == 0) {
shouldWarning = true;
} else if (ce >= 1) {
const pe = idxIDCBNC[i] - 1;
if (itemsCloned[pe].type !== 'zotino' &&
itemsCloned[pe].type !== 'hd68' &&
itemsCloned[pe].type !== 'idc-bnc') {
shouldWarning = true;
}
}
if (shouldWarning) {
itemsData[ce] = {...itemsCloned[ce]};
itemsData[ce].warnings = {};
itemsData[ce].warnings.wrong = {...itemsCloned[ce].rules.wrong};
}
}
// check number of IDC-BNC adapters for a Zotino and HD68-IDC
const idxZH = itemsCloned.reduce((prev, next, i) => {
if (next.type === 'zotino' || next.type === 'hd68') {
prev.push(i);
}
return prev;
}, []);
for (let i = 0; i <= idxZH.length - 1; i++) {
let slots;
let nbUsedSlot = 0;
let idx = idxZH[i];
if (i !== idxZH.length - 1) {
slots = itemsCloned.slice(idx + 1, idxZH[i + 1]);
} else {
slots = itemsCloned.slice(idx + 1);
}
let stopCount = false;
nbUsedSlot = slots.reduce((prev, next, ci, ca) => {
if (ci === 0 && next.type === 'idc-bnc') {
return prev + 1;
} else if (ca[0].type === 'idc-bnc' && ci > 0 && ca[ci - 1].type === 'idc-bnc') {
if (next.type !== 'idc-bnc') { stopCount = true; }
return prev + (next.type === 'idc-bnc' && !stopCount ? 1 : 0);
}
return prev;
}, 0);
if (idx in itemsData) {
itemsData[idx].nbrCurrentSlot = nbUsedSlot;
if (!('warnings' in itemsData[idx])) {
itemsData[idx].warnings = {};
}
} else {
itemsData[idx] = {...itemsCloned[idx]};
itemsData[idx].nbrCurrentSlot = nbUsedSlot;
itemsData[idx].warnings = {};
}
if (nbUsedSlot > 0) {
rules[itemsCloned[idx].rules.maxSlot.type] = {...itemsCloned[idx].rules.maxSlot};
}
if (nbUsedSlot > itemsCloned[idx].nbrSlotMax) {
itemsData[idx].warnings.maxSlotWarning = {...itemsCloned[idx].rules.maxSlotWarning};
}
// check if HD68-IDC has at least 1 IDC-BNC adapter
if (itemsCloned[idx].type === 'hd68') {
let shouldWarning = false;
if (idx < itemsCloned.length - 1) {
if (itemsCloned[idx + 1].type !== 'idc-bnc') {
shouldWarning = true;
}
} else if (idx === itemsCloned.length - 1) {
shouldWarning = true;
}
if (shouldWarning) {
if (idx in itemsData) {
itemsData[idx].warnings.minAdapter = {...itemsCloned[idx].rules.minAdapter};
} else {
itemsData[idx] = {...itemsCloned[idx]};
itemsData[idx].warnings = {};
itemsData[idx].warnings.minAdapter = {...itemsCloned[idx].rules.minAdapter};
}
}
}
}
// update state with rules
this.setState({
...this.state,
columns: {
...this.state.columns,
cart: {
...this.state.columns.cart,
itemsData: {
...itemsData,
},
}
},
rules: {
...rules,
},
});
}
render() {
const {
currentItemHovered,
currentMode,
crateModeSlots,
crateModeItems,
items,
columns,
rules,
} = this.state;
return (
<DragDropContext onDragEnd={this.handleOnDragEnd}>
<Layout
className="shop"
aside={
<Backlog
items={items}
data={columns['backlog']}
onClickAddItem={this.handleClickAddItem}>
</Backlog>
}
main={(
<OrderPanel
title="Order hardware"
description="
Choose your crate and drag/add the cards you want
to the crate below to see
how the combination would look like."
crateMode={
<CrateMode
items={crateModeItems}
mode={currentMode}
onClickMode={this.handleCrateModeChange}>
</CrateMode>}
crate={
<Crate
cart={
<Cart
nbrSlots={crateModeSlots[currentMode]}
data={columns['cart']}
itemHovered={currentItemHovered}
onToggleProgress={this.handleToggleItemProgress}
onToggleWarning={this.handleToggleItemWarning}
onToggleOverlayRemove={this.handleToggleOverlayRemove}
onClickRemoveItem={this.handleDeleteItem}>
</Cart>
}
rules={Object.values(rules).filter(rule => rule)}>
</Crate>
}
summaryPrice={
<OrderSumary
currentMode={currentMode}
modes={crateModeItems}
summary={columns['cart'].items}
itemsData={columns.cart.itemsData}
onMouseEnterItem={this.handleMouseEnterItem}
onMouseLeaveItem={this.handleMouseLeaveItem}
onDeleteItem={this.handleDeleteItem}
onDeleteAllItems={this.handleDeleteAllItems}
onClickSelectItem={this.handleClickSelectItem}>
</OrderSumary>
}
form={
<OrderForm
onClickSubmit={this.handleClickSubmit}>
</OrderForm>
}>
</OrderPanel>
)}>
</Layout>
</DragDropContext>
);
}
}
ReactDOM.render(
<Shop data={data} />,
document.querySelector('#root-shop'),
);