Minimal working multiple crates

Signed-off-by: Egor Savkin <es@m-labs.hk>
pull/113/head
Egor Savkin 2023-12-07 17:08:22 +08:00
parent 63d83b5e10
commit 691e5bbd86
13 changed files with 295 additions and 322 deletions

View File

@ -1,101 +1,72 @@
import React, {PureComponent} from 'react'
import PropTypes from "prop-types";
import React from 'react'
import {Droppable} from "@hello-pangea/dnd";
import {cartStyle, nbrOccupiedSlotsInCrate} from "./utils";
import {ProductCartItem} from "./ProductCartItem.jsx";
import {FakePlaceholder} from "./FakePlaceholder.jsx";
import {FillExtData} from "./options/utils";
import {crate_type_to_hp, hp_to_slots, resource_counters} from "./count_resources";
/**
* Component that displays a list of <ProductCartItem>
*/
export class Cart extends PureComponent {
static get propTypes() {
return {
isMobile: PropTypes.bool,
isTouch: PropTypes.bool,
nbrSlots: PropTypes.number,
itemHovered: PropTypes.string,
data: PropTypes.object.isRequired,
onToggleOverlayRemove: PropTypes.func,
onClickRemoveItem: PropTypes.func,
onCardUpdate: PropTypes.func,
onClickItem: PropTypes.func,
};
}
render() {
const {
isMobile,
isTouch,
nbrSlots,
itemHovered,
data,
onToggleOverlayRemove,
onClickRemoveItem,
onClickItem,
onCardUpdate,
} = this.props;
const nbrOccupied = nbrOccupiedSlotsInCrate(data.items);
const products = data.items.map((item, index) => {
let itemData;
let ext_data = FillExtData(data.itemsData, index);
if (data.itemsData && index in data.itemsData) {
itemData = data.itemsData[index];
}
return (
<ProductCartItem
isMobile={isMobile}
isTouch={isTouch}
hovered={item.id === itemHovered}
key={item.id}
id={item.id}
index={index}
first={index === 0}
last={index === data.items.length - 1 && nbrOccupied >= nbrSlots}
data={itemData}
ext_data={ext_data}
onToggleOverlayRemove={onToggleOverlayRemove}
onClickRemoveItem={onClickRemoveItem}
onCardUpdate={onCardUpdate}
onClickItem={onClickItem}
model={item}>
</ProductCartItem>
);
});
export function Cart({isMobile, isTouch, data, onToggleOverlayRemove, onClickRemoveItem, onCardUpdate, onClickItem}) {
const nbrOccupied = resource_counters.hp(data.items);
const nbrSlots = hp_to_slots(crate_type_to_hp(data.crate_type));
const products = data.items.map((item, index) => {
let itemData;
let ext_data = FillExtData(data.items, index);
if (data.items && index in data.items) {
itemData = data.items[index];
}
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>
<ProductCartItem
isMobile={isMobile}
isTouch={isTouch}
hovered={item.hovered}
key={item.id}
id={item.id}
index={index}
first={index === 0}
last={index === data.items.length - 1 && nbrOccupied >= nbrSlots}
data={itemData}
ext_data={ext_data}
onToggleOverlayRemove={onToggleOverlayRemove}
onClickRemoveItem={onClickRemoveItem}
onCardUpdate={onCardUpdate}
onClickItem={onClickItem}
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 - nbrOccupied}
isDraggingOver={snapshot.isDraggingOver}/>
</div>
)}
</Droppable>
);
}

View File

@ -1,44 +1,36 @@
import React, {PureComponent} from 'react';
import PropTypes from "prop-types";
import React from 'react';
import {Cart} from "./Cart.jsx";
import {CrateMode} from "./CrateMode.jsx";
import {CrateWarnings} from "./CrateWarnings.jsx";
/**
* Component that displays the main crate with reminder rules.
* It includes <Cart> and rules
*/
export class Crate extends PureComponent {
export function Crate({data, handleToggleOverlayRemove, handleDeleteItem, handleShowOverlayRemove, handleCardsUpdated, isMobile, isTouch}) {
return (
<div className="crate">
static get propTypes() {
return {
rules: PropTypes.array,
cart: PropTypes.element,
};
}
<CrateMode current={data.mode} onChange={null} />
render() {
const {
rules,
cart,
} = this.props;
<div className="crate-products">
return (
<div className="crate">
<div className="crate-products">
{cart}
{rules && rules.length > 0 && (
<div className="crate-info">
{rules.map((rule, index) => (
<p key={index} className="rule" style={{'color': rule.color ? rule.color : 'inherit'}}>
<img src={`/images${rule.icon}`} /> <i><strong>{rule.name}:</strong> {rule.message}</i>
</p>
))}
</div>
)}
</div>
<Cart
data={data}
isMobile={isMobile}
isTouch={isTouch}
onToggleOverlayRemove={handleToggleOverlayRemove}
onClickRemoveItem={handleDeleteItem}
onClickItem={handleShowOverlayRemove}
onCardUpdate={handleCardsUpdated}>
</Cart>
{1||(rules && rules.length > 0) && (
<CrateWarnings/>
)}
</div>
);
}
</div>
);
}

View File

@ -0,0 +1,17 @@
import React from 'react'
import {Accordion} from "react-bootstrap";
import {Crate} from "./Crate.jsx";
export function CrateList({crates, isMobile, isTouch}) {
return (
<Accordion defaultActiveKey="0">
{Object.entries(crates).map(([crate_id, crate], index) =>
<Accordion.Item eventKey={`${index}`} key={`crate${index}`}>
<Accordion.Header>Crate #{`${index}`}</Accordion.Header>
<Accordion.Body>
<Crate data={{id: crate_id, ...crate}} isTouch={isTouch} isMobile={isMobile}/>
</Accordion.Body>
</Accordion.Item>
)}
</Accordion>)
}

View File

@ -1,48 +1,22 @@
import React, {PureComponent} from 'react';
import PropTypes from "prop-types";
import React from 'react';
import {data as shared_data} from "./utils";
/**
* Component that displays crate modes
*/
export class CrateMode extends PureComponent {
export function CrateMode({current, onChange}) {
return (
<div className="crate-mode">
{shared_data.crateModeOrder.map(item => (
<a
key={item}
className={current === item ? 'active' : ''}
static get propTypes() {
return {
items: PropTypes.array.isRequired,
mode: PropTypes.string.isRequired,
onClickMode: PropTypes.func,
};
}
href="#"
role="button">{shared_data.crateModes[item].name}</a>
))}
</div>
);
}
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>
);
}
}
//onClick={onChange(this, item)}

View File

@ -0,0 +1,13 @@
import React from "react";
export function CrateWarnings() {
return (
<div className="crate-info">
{rules.map((rule, index) => (
<p key={index} className="rule" style={{'color': rule.color ? rule.color : 'inherit'}}>
<img src={`/images${rule.icon}`} /> <i><strong>{rule.name}:</strong> {rule.message}</i>
</p>
))}
</div>
)
}

View File

@ -1,47 +1,27 @@
import React, {PureComponent} from 'react'
import PropTypes from "prop-types";
import {nbrOccupiedSlotsInCrate} from "./utils";
import React from 'react';
/**
* Component that displays a placeholder inside crate.
* Allows to display how it remains space for the current crate.
*/
export class FakePlaceholder extends PureComponent {
export function FakePlaceholder({isDraggingOver, nToDraw}) {
const fakePlaceholder = [];
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>
for (let i = nToDraw; 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>
);
}

View File

@ -0,0 +1,5 @@
export function ImportJSON() {
return (
<div> Import JSON BAOBAO</div>
)
}

View File

@ -11,8 +11,7 @@ export class OrderPanel extends PureComponent {
return {
title: PropTypes.string,
description: PropTypes.element,
crateMode: PropTypes.element,
crate: PropTypes.element,
cratesList: PropTypes.element,
summaryPrice: PropTypes.element,
form: PropTypes.element,
isMobile: PropTypes.bool,
@ -25,8 +24,7 @@ export class OrderPanel extends PureComponent {
const {
title,
description,
crateMode,
crate,
cratesList,
summaryPrice,
form,
isMobile,
@ -41,8 +39,6 @@ export class OrderPanel extends PureComponent {
<div className="control">
{description}
{crateMode}
</div>
<div>
@ -61,7 +57,7 @@ export class OrderPanel extends PureComponent {
</div>
) : null}
{crate}
{cratesList}
<section className="summary">
{summaryPrice}

View File

@ -121,7 +121,7 @@ export class OrderSummary extends PureComponent {
</thead>
<tbody>
{summary.map((item, index) => {
{[].map((item, index) => {
let alert, warning, options, options_data;
if (itemsData[index] && itemsData[index].warnings) {

View File

@ -4,18 +4,16 @@ import {FilterOptions} from "./options/utils";
import {v4 as uuidv4} from "uuid";
import {DragDropContext} from "@hello-pangea/dnd";
import {copy, itemsUnfoldedList, nbrOccupiedSlotsInCrate, remove, reorder} from "./utils";
import {copyFromBacklog, itemsUnfoldedList, nbrOccupiedSlotsInCrate, remove, reorder} from "./utils";
import {Layout} from "./Layout.jsx";
import {Backlog} from "./Backlog.jsx";
import {OrderPanel} from "./OrderPanel.jsx";
import {CrateMode} from "./CrateMode.jsx";
import {Crate} from "./Crate.jsx";
import {Cart} from "./Cart.jsx";
import {OrderSummary} from "./OrderSummary.jsx";
import {OrderForm} from "./OrderForm.jsx";
import {TriggerWarnings} from "./warnings";
import {FillResources} from "./count_resources";
import {CrateList} from "./CrateList.jsx";
/**
* Component that render the entire shop
@ -64,14 +62,14 @@ export class Shop extends PureComponent {
],
};
const destination = {
droppableId: 'cart',
droppableId: 'crate0',
index: 0,
};
this.handleOnDragEnd({
/*this.handleOnDragEnd({
source,
destination
});
});*/
}
componentDidUpdate(prevProps, prevState) {
@ -83,13 +81,15 @@ export class Shop extends PureComponent {
* trigger again this function (componentDidUpdate) and thus,
* making an infinite loop.
*/
return;
if (
(prevState.columns.cart.items !== this.state.columns.cart.items) ||
(prevState.columns.crates !== this.state.columns.crates.items) ||
(prevState.currentMode !== this.state.currentMode)
) {
this.checkAlerts(this.state.columns.cart.items);
}
if (this.state.newCardJustAdded) {
this.timer = setTimeout(() => {
this.setState({
@ -353,11 +353,20 @@ export class Shop extends PureComponent {
})
}
handleOnDragEnd(result, newAdded) {
handleOnDragEnd(result) {
/**
* 4 cases:
* - from backlog to one of the crate - add to the correct crate in correct order
* - from one crate to another - delete from one crate and put to another in correct order
* - within one crate - reorder
* - from crate to backlog - delete
* */
const {
source,
destination,
} = result;
/* better move to another function
let dragged_items = [];
if (source.indexes) {
source.indexes.forEach((card_index, _) => {
@ -365,82 +374,61 @@ export class Shop extends PureComponent {
})
} else if (source.index >= 0) {
dragged_items.push(itemsUnfoldedList[source.index]);
}
}*/
console.log('==> result', result);
// dropped outside the list
if (!destination) {
if (source.droppableId === 'cart') {
this.setState({
...this.state,
newCardJustAdded: false,
columns: {
...this.state.columns,
[source.droppableId]: {
...this.state.columns[source.droppableId],
items: remove(
this.state.columns[source.droppableId].items,
source.index,
),
itemsData: remove(
this.state.columns[source.droppableId].itemsData,
source.index,
)
},
},
});
}
return;
}
switch(source.droppableId) {
case 'backlog':
if (source.droppableId !== destination.droppableId) {
this.setState({
...this.state,
newCardJustAdded: newAdded ? true : false,
columns: {
...this.state.columns,
[destination.droppableId]: {
...this.state.columns[destination.droppableId],
items: copy(
this.state.items,
this.state.columns[source.droppableId],
this.state.columns[destination.droppableId],
dragged_items,
destination,
),
},
},
});
}
break;
switch (source.droppableId) {
// TODO add delete functionality
case destination.droppableId:
this.setState({
...this.state,
newCardJustAdded: false,
columns: {
...this.state.columns,
[destination.droppableId]: {
...this.state.columns[destination.droppableId],
items: reorder(
this.state.columns[destination.droppableId].items,
source.index,
destination.index,
),
itemsData: reorder(
this.state.columns[destination.droppableId].itemsData,
source.index,
destination.index,
),
},
},
crates: {
...this.state.columns.crates,
[destination.droppableId]: {
...this.state.columns.crates[destination.droppableId],
items: reorder(
this.state.columns.crates[source.droppableId].items,
source.index,
destination.index
)}}
}
});
break;
case 'backlog':
this.setState({
...this.state,
columns: {
...this.state.columns,
crates: {
...this.state.columns.crates,
[destination.droppableId]: {
...this.state.columns.crates[destination.droppableId],
items: copyFromBacklog(
this.state.items,
this.state.columns.crates[destination.droppableId],
source,
destination
)}}
}
});
break;
default:
this.setState({
columns: move(
this.state.columns[source.droppableId],
this.state.columns[destination.droppableId],
source,
destination
)
});
break;
}
}
@ -460,6 +448,7 @@ export class Shop extends PureComponent {
checkAlerts(newItems) {
console.log('--- START CHECKING CRATE WARNING ---');
return;
const {
currentMode,
@ -546,7 +535,7 @@ export class Shop extends PureComponent {
<Backlog
currency={currency}
items={items}
data={columns['backlog']}
data={columns.backlog}
onClickAddItem={this.handleClickAddItem}
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
isMobile={isMobile}>
@ -559,37 +548,17 @@ export class Shop extends PureComponent {
isMobile={isMobile}
title="Order hardware"
description={(<p className="description">Drag and drop the cards you want into the crate below to see how the combination would look like. Setup card's configuration by tapping at the top of the card, most of the options can be modified after shipment. If you have any issues with this ordering system, or if you need other configurations, email us directly anytime at <a href="mailto:sales@m-labs.hk">sales@m-labs.hk</a>. The price is estimated and must be confirmed by a quote.</p>)}
crateMode={
<CrateMode
items={crateModeItems}
mode={currentMode}
onClickMode={this.handleCrateModeChange}>
</CrateMode>}
crate={
<Crate
cart={
<Cart
nbrSlots={crateModeSlots[currentMode]}
data={columns['cart']}
isMobile={isMobile}
isTouch={isTouch}
itemHovered={currentItemHovered}
onToggleOverlayRemove={this.handleToggleOverlayRemove}
onClickRemoveItem={this.handleDeleteItem}
onClickItem={this.handleShowOverlayRemove}
onCardUpdate={this.handleCardsUpdated}>
</Cart>
}
rules={Object.values(rules).filter(rule => rule)}>
</Crate>
cratesList={
<CrateList
crates={columns.crates}
/>
}
summaryPrice={
<OrderSummary
currency={currency}
currentMode={currentMode}
modes={crateModeItems}
summary={columns['cart'].items}
itemsData={columns.cart.itemsData}
summary={columns.crates}
onMouseEnterItem={this.handleMouseEnterItem}
onMouseLeaveItem={this.handleMouseLeaveItem}
onDeleteItem={this.handleDeleteItem}

View File

@ -1,3 +1,4 @@
import {data as shared_data} from "./utils";
const count_item_occupied_eem = (item) => {
if (!item.options_data
@ -45,7 +46,7 @@ function CounterFactory(name) {
}
}
const resource_counters = {
export const resource_counters = {
"eem": CounterFactory("eem"),
"clk": CounterFactory("clk"),
"idc": CounterFactory("idc"),
@ -70,4 +71,12 @@ export function FillResources(data) {
element.counted_resources = CountResources(data, index);
return element;
})
}
export function hp_to_slots(hp) {
return Math.trunc(hp / 4);
}
export function crate_type_to_hp(crate_t) {
return shared_data.crateModes[crate_t].hp;
}

View File

@ -5,7 +5,7 @@ import {v4 as uuidv4} from "uuid";
export const data = window.shop_data;
export const itemsUnfoldedList = Array.from(data.columns.backlog.categories.map(groupId => groupId.itemIds).flat());
export const copy = (
/*export const copy = (
model,
source,
destination,
@ -23,7 +23,7 @@ export const copy = (
return destClone;
};
*/
export const reorder = (list, startIndex, endIndex) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
@ -38,6 +38,36 @@ export const remove = (list, startIndex) => {
return result;
};
export const copyFromBacklog = (source, destination, droppableSource, droppableDestination) => {
console.log('==> dest', destination);
const destClone = Array.from(destination.items);
const items = droppableSource.indexes
? droppableSource.indexes .map((item, _) => itemsUnfoldedList[item])
: [itemsUnfoldedList[droppableSource.index]];
destClone.splice(droppableDestination.index, 0, ...items.map((item, _) => {
return {...source[item], id: uuidv4()}
}));
return destClone;
};
export const move = (source, destination, droppableSource, droppableDestination) => {
console.log('==> move', source, destination);
const sourceClone = Array.from(source);
const destClone = Array.from(destination);
const [removed] = sourceClone.splice(droppableSource.index, 1);
destClone.splice(droppableDestination.index, 0, removed);
const result = {columns: {}};
result.columns[droppableSource.droppableId] = sourceClone;
result.columns[droppableDestination.droppableId] = destClone;
return result;
};
export const productStyle = (style, snapshot, removeAnim, hovered, selected, cart=false) => {
const custom = {
opacity: snapshot.isDragging ? .7 : 1,
@ -76,13 +106,6 @@ export const cartStyle = (style, snapshot) => {
};
}
export const nbrOccupiedSlotsInCrate = (items) => {
return items.reduce((prev, next) => {
return prev + (next.hp === 8 ? 2 : 1);
}, 0);
};
export 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

View File

@ -38,6 +38,30 @@ const shop_data = {
price: 500,
}],
crateModes: {
rack: {
id: 'rack',
name: 'Rack mountable crate',
price: 550,
hp: 84
},
desktop: {
id: 'desktop',
name: 'Desktop crate',
price: 500,
hp: 42
},
no_crate: {
id: 'no_crate',
name: 'Standalone cards',
price: 0,
hp: -1
}
},
crateModeOrder: [
"rack", "desktop", "no_crate"
],
items: {
/* keys are also ids, avoid changing them */
'kasli': {
@ -1161,12 +1185,12 @@ const shop_data = {
],
},
'cart': {
id: 'cart',
title: 'Cart',
items: [],
itemsData: [],
},
"crates": {
"crate0": {
crate_type: "rack",
items: []
}
}
},