Minimal working multiple crates

Signed-off-by: Egor Savkin <es@m-labs.hk>
This commit is contained in:
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,56 +1,29 @@
import React, {PureComponent} from 'react' import React from 'react'
import PropTypes from "prop-types";
import {Droppable} from "@hello-pangea/dnd"; import {Droppable} from "@hello-pangea/dnd";
import {cartStyle, nbrOccupiedSlotsInCrate} from "./utils"; import {cartStyle, nbrOccupiedSlotsInCrate} from "./utils";
import {ProductCartItem} from "./ProductCartItem.jsx"; import {ProductCartItem} from "./ProductCartItem.jsx";
import {FakePlaceholder} from "./FakePlaceholder.jsx"; import {FakePlaceholder} from "./FakePlaceholder.jsx";
import {FillExtData} from "./options/utils"; 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> * Component that displays a list of <ProductCartItem>
*/ */
export class Cart extends PureComponent { export function Cart({isMobile, isTouch, data, onToggleOverlayRemove, onClickRemoveItem, onCardUpdate, onClickItem}) {
const nbrOccupied = resource_counters.hp(data.items);
static get propTypes() { const nbrSlots = hp_to_slots(crate_type_to_hp(data.crate_type));
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) => { const products = data.items.map((item, index) => {
let itemData; let itemData;
let ext_data = FillExtData(data.itemsData, index); let ext_data = FillExtData(data.items, index);
if (data.itemsData && index in data.itemsData) { if (data.items && index in data.items) {
itemData = data.itemsData[index]; itemData = data.items[index];
} }
return ( return (
<ProductCartItem <ProductCartItem
isMobile={isMobile} isMobile={isMobile}
isTouch={isTouch} isTouch={isTouch}
hovered={item.id === itemHovered} hovered={item.hovered}
key={item.id} key={item.id}
id={item.id} id={item.id}
index={index} index={index}
@ -89,8 +62,7 @@ export class Cart extends PureComponent {
)} )}
<FakePlaceholder <FakePlaceholder
nbrSlots={nbrSlots} nbrSlots={nbrSlots - nbrOccupied}
items={data.items}
isDraggingOver={snapshot.isDraggingOver}/> isDraggingOver={snapshot.isDraggingOver}/>
</div> </div>
)} )}
@ -98,4 +70,3 @@ export class Cart extends PureComponent {
</Droppable> </Droppable>
); );
} }
}

View File

@ -1,44 +1,36 @@
import React, {PureComponent} from 'react'; import React from 'react';
import PropTypes from "prop-types"; import {Cart} from "./Cart.jsx";
import {CrateMode} from "./CrateMode.jsx";
import {CrateWarnings} from "./CrateWarnings.jsx";
/** /**
* Component that displays the main crate with reminder rules. * Component that displays the main crate with reminder rules.
* It includes <Cart> and rules * It includes <Cart> and rules
*/ */
export class Crate extends PureComponent { export function Crate({data, handleToggleOverlayRemove, handleDeleteItem, handleShowOverlayRemove, handleCardsUpdated, isMobile, isTouch}) {
static get propTypes() {
return {
rules: PropTypes.array,
cart: PropTypes.element,
};
}
render() {
const {
rules,
cart,
} = this.props;
return ( return (
<div className="crate"> <div className="crate">
<CrateMode current={data.mode} onChange={null} />
<div className="crate-products"> <div className="crate-products">
{cart} <Cart
data={data}
isMobile={isMobile}
isTouch={isTouch}
onToggleOverlayRemove={handleToggleOverlayRemove}
onClickRemoveItem={handleDeleteItem}
onClickItem={handleShowOverlayRemove}
onCardUpdate={handleCardsUpdated}>
</Cart>
{rules && rules.length > 0 && ( {1||(rules && rules.length > 0) && (
<div className="crate-info"> <CrateWarnings/>
{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> </div>
</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 React from 'react';
import PropTypes from "prop-types"; import {data as shared_data} from "./utils";
/** /**
* Component that displays crate modes * Component that displays crate modes
*/ */
export class CrateMode extends PureComponent { export function CrateMode({current, onChange}) {
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 ( return (
<div className="crate-mode"> <div className="crate-mode">
{items.map(item => ( {shared_data.crateModeOrder.map(item => (
<a <a
key={item.id} key={item}
className={mode == item.id ? 'active' : ''} className={current === item ? 'active' : ''}
onClick={this.handleOnClickMode.bind(this, item.id)}
href="#" href="#"
role="button">{item.name}</a> role="button">{shared_data.crateModes[item].name}</a>
))} ))}
</div> </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,32 +1,13 @@
import React, {PureComponent} from 'react' import React from 'react';
import PropTypes from "prop-types";
import {nbrOccupiedSlotsInCrate} from "./utils";
/** /**
* Component that displays a placeholder inside crate. * Component that displays a placeholder inside crate.
* Allows to display how it remains space for the current crate. * Allows to display how it remains space for the current crate.
*/ */
export class FakePlaceholder extends PureComponent { export function FakePlaceholder({isDraggingOver, nToDraw}) {
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 fakePlaceholder = [];
const nbrOccupied = nbrOccupiedSlotsInCrate(items);
for (var i = (nbrSlots - nbrOccupied); i > 0; i--) { for (let i = nToDraw; i > 0; i--) {
fakePlaceholder.push( fakePlaceholder.push(
<div key={i} style={{ <div key={i} style={{
display: isDraggingOver ? 'none' : 'block', display: isDraggingOver ? 'none' : 'block',
@ -44,4 +25,3 @@ export class FakePlaceholder extends PureComponent {
); );
} }
}

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

View File

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

View File

@ -4,18 +4,16 @@ import {FilterOptions} from "./options/utils";
import {v4 as uuidv4} from "uuid"; import {v4 as uuidv4} from "uuid";
import {DragDropContext} from "@hello-pangea/dnd"; 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 {Layout} from "./Layout.jsx";
import {Backlog} from "./Backlog.jsx"; import {Backlog} from "./Backlog.jsx";
import {OrderPanel} from "./OrderPanel.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 {OrderSummary} from "./OrderSummary.jsx";
import {OrderForm} from "./OrderForm.jsx"; import {OrderForm} from "./OrderForm.jsx";
import {TriggerWarnings} from "./warnings"; import {TriggerWarnings} from "./warnings";
import {FillResources} from "./count_resources"; import {FillResources} from "./count_resources";
import {CrateList} from "./CrateList.jsx";
/** /**
* Component that render the entire shop * Component that render the entire shop
@ -64,14 +62,14 @@ export class Shop extends PureComponent {
], ],
}; };
const destination = { const destination = {
droppableId: 'cart', droppableId: 'crate0',
index: 0, index: 0,
}; };
this.handleOnDragEnd({ /*this.handleOnDragEnd({
source, source,
destination destination
}); });*/
} }
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
@ -83,13 +81,15 @@ export class Shop extends PureComponent {
* trigger again this function (componentDidUpdate) and thus, * trigger again this function (componentDidUpdate) and thus,
* making an infinite loop. * making an infinite loop.
*/ */
return;
if ( if (
(prevState.columns.cart.items !== this.state.columns.cart.items) || (prevState.columns.crates !== this.state.columns.crates.items) ||
(prevState.currentMode !== this.state.currentMode) (prevState.currentMode !== this.state.currentMode)
) { ) {
this.checkAlerts(this.state.columns.cart.items); this.checkAlerts(this.state.columns.cart.items);
} }
if (this.state.newCardJustAdded) { if (this.state.newCardJustAdded) {
this.timer = setTimeout(() => { this.timer = setTimeout(() => {
this.setState({ 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 { const {
source, source,
destination, destination,
} = result; } = result;
/* better move to another function
let dragged_items = []; let dragged_items = [];
if (source.indexes) { if (source.indexes) {
source.indexes.forEach((card_index, _) => { source.indexes.forEach((card_index, _) => {
@ -365,82 +374,61 @@ export class Shop extends PureComponent {
}) })
} else if (source.index >= 0) { } else if (source.index >= 0) {
dragged_items.push(itemsUnfoldedList[source.index]); dragged_items.push(itemsUnfoldedList[source.index]);
} }*/
console.log('==> result', result);
// dropped outside the list
if (!destination) { 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; return;
} }
switch (source.droppableId) { switch (source.droppableId) {
// TODO add delete functionality
case 'backlog':
if (source.droppableId !== destination.droppableId) {
this.setState({
...this.state,
newCardJustAdded: newAdded ? true : false,
columns: {
...this.state.columns,
[destination.droppableId]: {
...this.state.columns[destination.droppableId],
items: copy(
this.state.items,
this.state.columns[source.droppableId],
this.state.columns[destination.droppableId],
dragged_items,
destination,
),
},
},
});
}
break;
case destination.droppableId: case destination.droppableId:
this.setState({ this.setState({
...this.state, ...this.state,
newCardJustAdded: false,
columns: { columns: {
...this.state.columns, ...this.state.columns,
crates: {
...this.state.columns.crates,
[destination.droppableId]: { [destination.droppableId]: {
...this.state.columns[destination.droppableId], ...this.state.columns.crates[destination.droppableId],
items: reorder( items: reorder(
this.state.columns[destination.droppableId].items, this.state.columns.crates[source.droppableId].items,
source.index, source.index,
destination.index, destination.index
), )}}
itemsData: reorder( }
this.state.columns[destination.droppableId].itemsData, });
source.index, break;
destination.index, 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; break;
default: default:
this.setState({
columns: move(
this.state.columns[source.droppableId],
this.state.columns[destination.droppableId],
source,
destination
)
});
break; break;
} }
} }
@ -460,6 +448,7 @@ export class Shop extends PureComponent {
checkAlerts(newItems) { checkAlerts(newItems) {
console.log('--- START CHECKING CRATE WARNING ---'); console.log('--- START CHECKING CRATE WARNING ---');
return;
const { const {
currentMode, currentMode,
@ -546,7 +535,7 @@ export class Shop extends PureComponent {
<Backlog <Backlog
currency={currency} currency={currency}
items={items} items={items}
data={columns['backlog']} data={columns.backlog}
onClickAddItem={this.handleClickAddItem} onClickAddItem={this.handleClickAddItem}
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu} onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
isMobile={isMobile}> isMobile={isMobile}>
@ -559,37 +548,17 @@ export class Shop extends PureComponent {
isMobile={isMobile} isMobile={isMobile}
title="Order hardware" 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>)} 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={ cratesList={
<CrateMode <CrateList
items={crateModeItems} crates={columns.crates}
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>
} }
summaryPrice={ summaryPrice={
<OrderSummary <OrderSummary
currency={currency} currency={currency}
currentMode={currentMode} currentMode={currentMode}
modes={crateModeItems} modes={crateModeItems}
summary={columns['cart'].items} summary={columns.crates}
itemsData={columns.cart.itemsData}
onMouseEnterItem={this.handleMouseEnterItem} onMouseEnterItem={this.handleMouseEnterItem}
onMouseLeaveItem={this.handleMouseLeaveItem} onMouseLeaveItem={this.handleMouseLeaveItem}
onDeleteItem={this.handleDeleteItem} onDeleteItem={this.handleDeleteItem}

View File

@ -1,3 +1,4 @@
import {data as shared_data} from "./utils";
const count_item_occupied_eem = (item) => { const count_item_occupied_eem = (item) => {
if (!item.options_data if (!item.options_data
@ -45,7 +46,7 @@ function CounterFactory(name) {
} }
} }
const resource_counters = { export const resource_counters = {
"eem": CounterFactory("eem"), "eem": CounterFactory("eem"),
"clk": CounterFactory("clk"), "clk": CounterFactory("clk"),
"idc": CounterFactory("idc"), "idc": CounterFactory("idc"),
@ -71,3 +72,11 @@ export function FillResources(data) {
return element; 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 data = window.shop_data;
export const itemsUnfoldedList = Array.from(data.columns.backlog.categories.map(groupId => groupId.itemIds).flat()); export const itemsUnfoldedList = Array.from(data.columns.backlog.categories.map(groupId => groupId.itemIds).flat());
export const copy = ( /*export const copy = (
model, model,
source, source,
destination, destination,
@ -23,7 +23,7 @@ export const copy = (
return destClone; return destClone;
}; };
*/
export const reorder = (list, startIndex, endIndex) => { export const reorder = (list, startIndex, endIndex) => {
const result = Array.from(list); const result = Array.from(list);
const [removed] = result.splice(startIndex, 1); const [removed] = result.splice(startIndex, 1);
@ -38,6 +38,36 @@ export const remove = (list, startIndex) => {
return result; 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) => { export const productStyle = (style, snapshot, removeAnim, hovered, selected, cart=false) => {
const custom = { const custom = {
opacity: snapshot.isDragging ? .7 : 1, 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 = ",") { export function formatMoney(amount, decimalCount = 2, decimal = ".", thousands = ",") {
// https://stackoverflow.com/questions/149055/how-can-i-format-numbers-as-currency-string-in-javascript // 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 // changes: return amount if error in order to avoid empty value

View File

@ -38,6 +38,30 @@ const shop_data = {
price: 500, 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: { items: {
/* keys are also ids, avoid changing them */ /* keys are also ids, avoid changing them */
'kasli': { 'kasli': {
@ -1161,12 +1185,12 @@ const shop_data = {
], ],
}, },
'cart': { "crates": {
id: 'cart', "crate0": {
title: 'Cart', crate_type: "rack",
items: [], items: []
itemsData: [], }
}, }
}, },