Start refactor to state manager zustand

Signed-off-by: Egor Savkin <es@m-labs.hk>
pull/113/head
Egor Savkin 2023-12-11 17:05:35 +08:00
parent 9bdaca2ca9
commit 9edf410e4d
16 changed files with 634 additions and 536 deletions

31
package-lock.json generated
View File

@ -24,7 +24,8 @@
"react-dom": "^18.2.0",
"uuid": "^9.0.1",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4"
"webpack-cli": "^5.1.4",
"zustand": "^4.4.7"
}
},
"node_modules/@ampproject/remapping": {
@ -4915,6 +4916,34 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zustand": {
"version": "4.4.7",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.7.tgz",
"integrity": "sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==",
"dev": true,
"dependencies": {
"use-sync-external-store": "1.2.0"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
}
}
}

View File

@ -29,7 +29,8 @@
"uuid": "^9.0.1",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4",
"json-logic-js": "^2.0.2"
"json-logic-js": "^2.0.2",
"zustand": "^4.4.7"
},
"babel": {
"presets": [

View File

@ -1,118 +1,106 @@
import React, {PureComponent} from 'react';
import PropTypes from "prop-types";
import React from 'react';
import {v4 as uuidv4} from "uuid";
import {Droppable} from "@hello-pangea/dnd";
import {ProductItem} from "./ProductItem.jsx";
import {useShopStore} from "./shop_store";
/**
* Component that renders the backlog in the aside
*/
export class Backlog extends PureComponent {
static get propTypes() {
return {
currency: PropTypes.string,
data: PropTypes.object.isRequired,
items: PropTypes.object,
isMobile: PropTypes.bool,
onClickAddItem: PropTypes.func,
onClickToggleMobileSideMenu: PropTypes.func,
};
}
static get defaultProps() {
return {
items: {},
};
}
render() {
const {
currency,
data,
items,
onClickAddItem,
onClickToggleMobileSideMenu,
isMobile,
} = this.props;
export function Backlog() {
const {
currency,
data,
items,
onClickAddItem,
onClickToggleMobileSideMenu,
isMobile,
} = useShopStore(state=> ({
currency: state.currency,
data: state.groups,
items: state.cards,
onClickAddItem: state.addCardFromBacklog,
onClickToggleMobileSideMenu: state.switchSideMenu,
isMobile: state.isMobile
}));
const ordered_groups = data.categories.map(groupItem => ({
name: groupItem.name,
items: groupItem.itemIds.map(itemId => items[itemId])
}));
let item_index = -1;
const groups = ordered_groups.map((group, g_index) => {
return (
<div className="accordion-item" key={`${group.name}`}>
<h2 className="accordion-header">
<button className="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target={`#collapse${g_index}`} aria-expanded="true"
aria-controls={`collapse${g_index}`}>
{group.name}
const ordered_groups = data.categories.map(groupItem => ({
name: groupItem.name,
items: groupItem.itemIds.map(itemId => items[itemId])
}));
let item_index = -1;
const groups = ordered_groups.map((group, g_index) => {
return (
<div className="accordion-item" key={`${group.name}`}>
<h2 className="accordion-header">
<button className="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target={`#collapse${g_index}`} aria-expanded="true"
aria-controls={`collapse${g_index}`}>
{group.name}
</button>
</h2>
<div id={`collapse${g_index}`} className="accordion-collapse collapse" aria-labelledby="headingOne"
data-bs-parent="#accordion_categories">
<div className="accordion-body">
{group.items.map(item => {
item_index++;
return (
<ProductItem
key={item.id}
id={uuidv4()}
index={item_index}
name={`${item.name_number} ${item.name}`}
name_codename={item.name_codename}
price={item.price}
currency={currency}
image={`/images/${item.image}`}
specs={item.specs}
datasheet_file={item.datasheet_file}
datasheet_name={item.datasheet_name}
onClickAddItem={onClickAddItem}
></ProductItem>
)
})}
</div>
</div>
</div>
);
}
);
return (
<Droppable
droppableId={data.id}
isDropDisabled={true}>
{(provided) => (
<div
className="backlog-container"
ref={provided.innerRef}
{...provided.droppableProps}>
{isMobile ? (
<div className="mobileCloseMenu">
<button onClick={onClickToggleMobileSideMenu}>
<img src="/images/shop/icon-close-white.svg" alt="add"/>
</button>
</h2>
<div id={`collapse${g_index}`} className="accordion-collapse collapse" aria-labelledby="headingOne"
data-bs-parent="#accordion_categories">
<div className="accordion-body">
{group.items.map(item => {
item_index++;
return (
<ProductItem
key={item.id}
id={uuidv4()}
index={item_index}
name={`${item.name_number} ${item.name}`}
name_codename={item.name_codename}
price={item.price}
currency={currency}
image={`/images/${item.image}`}
specs={item.specs}
datasheet_file={item.datasheet_file}
datasheet_name={item.datasheet_name}
onClickAddItem={onClickAddItem}
></ProductItem>
)
})}
</div>
</div>
) : null}
<div className="accordion accordion-flush" id="accordion_categories">
{groups}
</div>
);
}
);
return (
<Droppable
droppableId={data.id}
isDropDisabled={true}>
{(provided) => (
<div
className="backlog-container"
ref={provided.innerRef}
{...provided.droppableProps}>
{isMobile ? (
<div className="mobileCloseMenu">
<button onClick={onClickToggleMobileSideMenu}>
<img src="/images/shop/icon-close-white.svg" alt="add"/>
</button>
</div>
) : null}
<div className="accordion accordion-flush" id="accordion_categories">
{groups}
{provided.placeholder && (
<div style={{display: 'none'}}>
{provided.placeholder}
</div>
)}
</div>
)}
{provided.placeholder && (
<div style={{display: 'none'}}>
{provided.placeholder}
</div>
)}
</div>
)}
</Droppable>
);
</Droppable>
);
}
}

View File

@ -4,45 +4,42 @@ import {cartStyle} 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";
import {CountResources, crate_type_to_hp, hp_to_slots, resource_counters} from "./count_resources";
import {useShopStore} from "./shop_store";
import {TriggerCardWarnings} from "./warnings";
/**
* Component that displays a list of <ProductCartItem>
*/
export function Cart({isMobile, isTouch, data, onToggleOverlayRemove, onClickRemoveItem, onCardUpdate, onClickItem}) {
const nbrOccupied = hp_to_slots(resource_counters.hp(data.items, -1));
const nbrSlots = hp_to_slots(crate_type_to_hp(data.crate_type));
export function Cart({crate_index}) {
// isMobile, isTouch, crate, onToggleOverlayRemove, onClickRemoveItem, onCardUpdate, onClickItem
const {crate} = useShopStore(state => ({
crate: state.crates[crate_index]
}));
const nbrOccupied = hp_to_slots(resource_counters.hp(crate.items, -1));
const nbrSlots = hp_to_slots(crate_type_to_hp(crate.crate_mode));
console.log(nbrOccupied, nbrSlots);
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];
}
const products = crate.items.map((item, index) => {
const ext_data = FillExtData(crate.items, index);
const resources = CountResources(crate.items, index);
const warnings = TriggerCardWarnings(crate.items, index, resources);
return (
<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}
card_index={index}
crate_index={crate_index}
ext_data={ext_data}
onToggleOverlayRemove={onToggleOverlayRemove}
onClickRemoveItem={onClickRemoveItem}
onCardUpdate={onCardUpdate}
onClickItem={onClickItem}
model={item}>
</ProductCartItem>
first={index === 0}
last={index === crate.items.length - 1 && nbrOccupied >= nbrSlots}
resources={resources}
warnings={warnings}
key={item.id}/>
);
});
return (
<Droppable droppableId={data.id} direction="horizontal">
<Droppable droppableId={crate.id} direction="horizontal">
{(provided, snapshot) => (
<div

View File

@ -2,51 +2,42 @@ import React from 'react';
import {Cart} from "./Cart.jsx";
import {CrateMode} from "./CrateMode.jsx";
import {CrateWarnings} from "./CrateWarnings.jsx";
import {useShopStore} from "./shop_store";
import {TriggerCrateWarnings} from "./warnings";
/**
* Component that displays the main crate with reminder rules.
* It includes <Cart> and rules
*/
export function Crate({
data,
handleToggleOverlayRemove,
handleDeleteItem,
handleShowOverlayRemove,
handleCardsUpdated,
isMobile,
isTouch,
onDelete,
onModeChange
}) {
export function Crate({crate_index}) {
const {
onDeleteCrate,
crate
} = useShopStore(state => ({
onDeleteCrate: state.delCrate,
crate: state.crates[crate_index]
}))
return (
<div className="crate">
<CrateMode current={data.mode} onChange={onModeChange}/>
<CrateMode crate_index={crate_index}/>
<div>
Delete crate
<button style={{width: "32px"}} onClick={() => onDelete(data.id)}>
<button style={{width: "32px"}} onClick={() => onDeleteCrate(crate.id)}>
<img src="/images/shop/icon-remove.svg" alt="remove"/>
</button>
</div>
<div className="crate-products">
<Cart
data={data}
isMobile={isMobile}
isTouch={isTouch}
onToggleOverlayRemove={handleToggleOverlayRemove}
onClickRemoveItem={handleDeleteItem}
onClickItem={handleShowOverlayRemove}
onCardUpdate={handleCardsUpdated}>
</Cart>
<Cart crate_index={crate_index}/>
{1 || (rules && rules.length > 0) && (
<CrateWarnings/>
<CrateWarnings crate_index={crate_index} />
)}
</div>
</div>
);

View File

@ -1,32 +1,34 @@
import React from 'react'
import {Accordion} from "react-bootstrap";
import {Crate} from "./Crate.jsx";
import {useShopStore} from "./shop_store";
export function CrateList({crates, active_crate, isMobile, isTouch, onAddCrate, onDeleteCrate, onModeChange, onCrateSelect}) {
const onClickAdd = (_) => {
onAddCrate("crate" + Object.entries(crates).length);
}
export function CrateList() {
const {
crates,
active_crate,
onAddCrate,
setActiveCrate,
} = useShopStore(state=> ({
crates: state.crates,
active_crate: state.active_crate,
onAddCrate: state.newCrate,
setActiveCrate: state.setActiveCrate,
}));
return (
<Accordion defaultActiveKey={active_crate}>
{Object.entries(crates).map(([crate_id, crate], index) =>
<Accordion.Item eventKey={crate_id} key={`crate${index}`} >
<Accordion.Header onClick={() => onCrateSelect(crate_id)}>Crate #{`${index}`}</Accordion.Header>
{crates.map((crate, index) =>
<Accordion.Item eventKey={crate.id} key={"accordion"+crate.id} >
<Accordion.Header onClick={() => setActiveCrate(crate.id)}>Crate #{`${index}`}</Accordion.Header>
<Accordion.Body>
<Crate
data={{id: crate_id, ...crate}}
isTouch={isTouch}
isMobile={isMobile}
onDelete={onDeleteCrate}
onModeChange={(new_mode) => onModeChange(crate_id, new_mode)}
/>
<Crate crate_index={index}/>
</Accordion.Body>
</Accordion.Item>
)}
<Accordion.Item eventKey="last">
<Accordion.Header>
Add new crate
<button style={{width: "32px"}} onClick={onClickAdd}>
<button style={{width: "32px"}} onClick={onAddCrate}>
<img src="/images/shop/icon-add.svg" alt="add" />
</button>
</Accordion.Header>

View File

@ -1,19 +1,25 @@
import React from 'react';
import {data as shared_data} from "./utils";
import {useShopStore} from "./shop_store";
/**
* Component that displays crate modes
*/
export function CrateMode({current, onChange}) {
export function CrateMode({crate_index}) {
const {modes_order, crate_modes, crate, setMode} = useShopStore(state => ({
modes_order: state.modes_order,
crate_modes: state.crate_modes,
crate: state.crates[crate_index].crate_mode,
setMode: state.setCrateMode
}))
return (
<div className="crate-mode">
{shared_data.crateModeOrder.map(item => (
{modes_order.map((mode_name, _) => (
<a
key={item}
className={current === item ? 'active' : ''}
onClick={() => onChange(item)}
key={mode_name}
className={crate.crate_mode === mode_name ? 'active' : ''}
onClick={() => setMode(crate.id, mode_name)}
href="#"
role="button">{shared_data.crateModes[item].name}</a>
role="button">{crate_modes[mode_name].name}</a>
))}
</div>
);

View File

@ -1,10 +1,15 @@
import React from "react";
import {TriggerCrateWarnings} from "./warnings";
import {useShopStore} from "./shop_store";
export function CrateWarnings() {
export function CrateWarnings({crate_index}) {
const crate = useShopStore(state => (state.crates[crate_index]))
const crate_warnings = TriggerCrateWarnings(crate);
// TODO UI/colors
return (
<div className="crate-info">
{rules.map((rule, index) => (
<p key={index} className="rule" style={{'color': rule.color ? rule.color : 'inherit'}}>
{crate_warnings.map((rule, index) => (
<p key={index} className="rule" style={{'color': "red"}}>
<img src={`/images${rule.icon}`} /> <i><strong>{rule.name}:</strong> {rule.message}</i>
</p>
))}

View File

@ -1,27 +1,12 @@
import React, {PureComponent} from 'react';
import PropTypes from "prop-types";
import React from 'react';
import {useShopStore} from "./shop_store";
/**
* Component that provides a base layout (aside/main) for the page.
*/
export class Layout extends PureComponent {
static get propTypes() {
return {
aside: PropTypes.any,
main: PropTypes.any,
mobileSideMenuShouldOpen: PropTypes.bool,
isMobile: PropTypes.bool,
newCardJustAdded: PropTypes.bool,
onClickToggleMobileSideMenu: PropTypes.func,
onClickCloseRFQFeedback: PropTypes.func,
RFQBodyType: PropTypes.string,
RFQBodyOrder: PropTypes.string,
onClickLoadCustomConf: PropTypes.func,
items: PropTypes.object,
};
}
/*
export function Layout({aside, main}) {
static get defaultProps() {
return {
mobileSideMenuShouldOpen: false,
@ -270,4 +255,32 @@ export class Layout extends PureComponent {
</div>
);
}
}
}
*/
export function Layout({aside, main}) {
const mobileSideMenuShouldOpen = useShopStore((state) => state.sideMenuOpen);
const onClickToggleMobileSideMenu = useShopStore((state) => state.switchSideMenu);
const newCardJustAdded = useShopStore((state) => state.newCardJustAdded);
const isMobile = useShopStore((state) => state.isMobile);
return (
<div className="layout">
<aside className={'aside ' + (mobileSideMenuShouldOpen ? 'menu-opened' : '')}>{aside}</aside>
{mobileSideMenuShouldOpen ? (
<section className="main" onClick={onClickToggleMobileSideMenu}>{main}</section>
) : (
<section className="main">{main}</section>
)}
{isMobile && newCardJustAdded ? (
<div className="feedback-add-success">
added
</div>
) : null}
</div>
);
}

View File

@ -1,71 +1,49 @@
import React, {PureComponent} from 'react'
import PropTypes from "prop-types";
import React from 'react'
import {OrderSummary} from "./OrderSummary";
import {OrderForm} from "./OrderForm";
import {CrateList} from "./CrateList";
import {useShopStore} from "./shop_store";
/**
* Component that renders all things for order.
* It acts like-a layout, this component do nothing more.
*/
export class OrderPanel extends PureComponent {
export function OrderPanel({title, description}) {
const isMobile = useShopStore((state) => state.isMobile);
const onClickToggleMobileSideMenu = useShopStore((state) => state.switchSideMenu);
const onClickOpenImport = useShopStore((state) => state.openImport);
static get propTypes() {
return {
title: PropTypes.string,
description: PropTypes.element,
cratesList: PropTypes.element,
summaryPrice: PropTypes.element,
form: PropTypes.element,
isMobile: PropTypes.bool,
onClickToggleMobileSideMenu: PropTypes.func,
onClickOpenImport: PropTypes.func,
};
}
return (<section className="panel">
render() {
const {
title,
description,
cratesList,
summaryPrice,
form,
isMobile,
onClickToggleMobileSideMenu,
onClickOpenImport,
} = this.props;
<h2>{title}</h2>
return (
<section className="panel">
<div className="control">
{description}
</div>
<h2>{title}</h2>
<div>
<button
className="btn btn-sm btn-outline-primary m-0 mb-2"
style={{'cursor': 'pointer'}}
onClick={onClickOpenImport}>Import JSON
</button>
</div>
<div className="control">
{description}
</div>
{isMobile ? (
<div className="mobileBtnDisplaySideMenu">
<button onClick={onClickToggleMobileSideMenu}>
<img src="/images/shop/icon-add.svg" alt="add"/>
</button>
</div>
) : null}
<div>
<button
className="btn btn-sm btn-outline-primary m-0 mb-2"
style={{'cursor': 'pointer'}}
onClick={onClickOpenImport}>Import JSON
</button>
</div>
<CrateList/>
{isMobile ? (
<div className="mobileBtnDisplaySideMenu">
<button onClick={onClickToggleMobileSideMenu}>
<img src="/images/shop/icon-add.svg" alt="add"/>
</button>
</div>
) : null}
<section className="summary">
<OrderSummary/>
{cratesList}
<OrderForm/>
</section>
<section className="summary">
{summaryPrice}
{form}
</section>
</section>
);
}
</section>);
}

View File

@ -1,186 +1,122 @@
import React, {PureComponent} from 'react'
import PropTypes from "prop-types";
import React from 'react'
import {Draggable} from "@hello-pangea/dnd";
import {DialogPopup} from "./options/DialogPopup.jsx";
import {productStyle} from "./utils";
import {Resources} from "./Resources.jsx";
import {CardWarnings} from "./CardWarnings.jsx";
import {useShopStore} from "./shop_store";
/**
* Component that renders a product.
* Used in the crate
*/
export class ProductCartItem extends PureComponent {
export function ProductCartItem({card_index, crate_index, ext_data, first, last, resources, warnings}) {
const {card, crate, highlighted, setHighlight, removeHighlight, onCardUpdate, onCardRemove} = useShopStore(state => ({
card: state.crates[crate_index].items[card_index],
highlighted: state.crates[crate_index].id === state.highlighted.crate && card_index === state.highlighted.card,
crate: state.crates[crate_index],
setHighlight: state.highlightCard,
removeHighlight: state.highlightReset,
onCardUpdate: state.updateOptions,
onCardRemove: state.deleteCard
}))
static get propTypes() {
return {
isMobile: PropTypes.bool,
isTouch: PropTypes.bool,
hovered: PropTypes.bool,
first: PropTypes.bool,
last: PropTypes.bool,
index: PropTypes.number.isRequired,
model: PropTypes.object.isRequired,
data: PropTypes.object,
ext_data: PropTypes.object,
resources: PropTypes.object,
onToggleOverlayRemove: PropTypes.func,
onClickRemoveItem: PropTypes.func,
onClickItem: PropTypes.func,
onCardUpdate: PropTypes.func,
};
let options, options_data;
//const warnings = data && data.show_warnings;
if (data && data.options) {
options = data.options;
if (!data.options_data) crate.options_data = {};
options_data = crate.options_data;
options_data.ext_data = ext_data;
}
static get defaultProps() {
return {
hovered: false,
};
}
return (
<Draggable draggableId={card.id} index={card_index}>
constructor(props) {
super(props);
this.handleOnMouseEnterRemoveItem = this.handleOnMouseEnterRemoveItem.bind(this);
this.handleOnMouseLeaveRemoveItem = this.handleOnMouseLeaveRemoveItem.bind(this);
this.handleOnClickRemoveItem = this.handleOnClickRemoveItem.bind(this);
this.handleOnClickItem = this.handleOnClickItem.bind(this);
}
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
...productStyle(
provided.draggableProps.style,
snapshot,
true,
!!highlighted,
false,
true
)
}}
onMouseEnter={() => setHighlight(crate.id, card_index)}
onMouseLeave={removeHighlight}
>
handleOnMouseEnterRemoveItem(index, e) {
if (this.props.onToggleOverlayRemove && !this.props.isMobile) {
this.props.onToggleOverlayRemove(index, true);
}
e.preventDefault();
}
{/* warning container */}
handleOnMouseLeaveRemoveItem(index, e) {
if (this.props.onToggleOverlayRemove && !this.props.isMobile) {
this.props.onToggleOverlayRemove(index, false);
}
e.preventDefault();
}
<div className="progress-container warning d-flex justify-content-evenly">
{warnings && warnings.length > 0 &&
(<CardWarnings warnings={warnings} prefix={card_index}/>)
}
handleOnClickItem(index, e) {
if (this.props.onClickItem && this.props.isTouch) {
this.props.onClickItem(index);
}
e.preventDefault();
}
{options && (
<DialogPopup
options={options}
data={options_data}
options_class={card.options_class}
key={"popover" + card_index}
id={"popover" + card_index}
big={card.size === "big"}
first={first}
last={last}
target={{
construct: ((outvar, value) => {
// console.log("construct", outvar, value, options_data);
options_data[outvar] = value;
}),
update: ((outvar, value) => {
// console.log("update", outvar, value, options_data);
if (outvar in options_data) options_data[outvar] = value;
onCardUpdate(crate.id, card_index, {[outvar]: value});
})
}}
/>
)}
</div>
handleOnClickRemoveItem(index, e) {
if (this.props.onClickRemoveItem) {
this.props.onClickRemoveItem(index);
}
}
<h6>{card.name_number}</h6>
render() {
const {
hovered,
model,
data,
index,
first,
last,
ext_data,
onCardUpdate,
} = this.props;
let options, options_data;
const warnings = data && data.show_warnings;
const resources = data && data.counted_resources;
if (data && data.options) {
options = data.options;
if (!data.options_data) data.options_data = {};
options_data = data.options_data;
options_data.ext_data = ext_data;
}
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,
!!model.selected,
true
)}}
onMouseEnter={this.handleOnMouseEnterRemoveItem.bind(this, index)}
onMouseLeave={this.handleOnMouseLeaveRemoveItem.bind(this, index)}
onMouseEnter={() => setHighlight(crate.id, card_index)}
onClick={() => setHighlight(crate.id, card_index)}
>
{/* warning container */}
<div className="progress-container warning d-flex justify-content-evenly">
{warnings && warnings.length > 0 &&
(<CardWarnings warnings={warnings} prefix={index} />)
}
{options && (
<DialogPopup
options={options}
data={options_data}
options_class={model.options_class}
key={"popover" + index}
id={"popover" + index}
big={model.size === "big"}
first={first}
last={last}
target={{
construct: ((outvar, value) => {
// console.log("construct", outvar, value, options_data);
options_data[outvar] = value;
}),
update: ((outvar, value) => {
// console.log("update", outvar, value, options_data);
if (outvar in options_data) options_data[outvar] = value;
onCardUpdate();
})
}}
/>
)}
</div>
<h6>{model.name_number}</h6>
<div
onMouseEnter={this.handleOnMouseEnterRemoveItem.bind(this, index)}
onClick={this.handleOnClickItem.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 */}
{resources && (
<Resources resources={resources}/>
)}
<img
className='item-cart'
src={`/images${card.image}`}/>
</div>
)}
</Draggable>
);
}
{/* remove container */}
<div
style={{'display': highlighted ? 'flex' : 'none'}}
className="overlayRemove"
onClick={() => onCardRemove(crate.id, card_index)}>
<img src="/images/shop/icon-remove.svg" alt="rm"/>
<p>Remove</p>
</div>
{/* progression container */}
{resources && (
<Resources resources={resources}/>
)}
</div>
)}
</Draggable>
);
}

View File

@ -19,7 +19,7 @@ import {CrateList} from "./CrateList.jsx";
* Component that render the entire shop
*/
export class Shop extends PureComponent {
export function Shop() {
static get propTypes() {
return {
@ -37,7 +37,7 @@ export class Shop extends PureComponent {
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.checkAlertsInNewState = this.checkAlertsInNewState.bind(this);
this.handleClickSelectItem = this.handleClickSelectItem.bind(this);
this.handleClickSubmit = this.handleClickSubmit.bind(this);
this.handleToggleOverlayRemove = this.handleToggleOverlayRemove.bind(this);
@ -47,11 +47,11 @@ export class Shop extends PureComponent {
this.handleClickShowOrder = this.handleClickShowOrder.bind(this);
this.handleClickOpenImport = this.handleClickOpenImport.bind(this);
this.handleLoadCustomConf = this.handleLoadCustomConf.bind(this);
this.handleCardsUpdated = this.handleCardsUpdated.bind(this);
this.onAddCrate = this.onAddCrate.bind(this);
this.onDeleteCrate = this.onDeleteCrate.bind(this);
this.onCrateModeChange = this.onCrateModeChange.bind(this);
this.onCrateSelected = this.onCrateSelected.bind(this);
this.onCrateChanged = this.onCrateChanged.bind(this);
this.timer = null;
this.timer_remove = null;
@ -111,14 +111,10 @@ export class Shop extends PureComponent {
onCrateChanged(crate_id) {
// kinda silly that hover over cards triggers checkAlerts
this.checkAlerts(crate_id)
this.checkAlertsInNewState(crate_id, this.state);
}
handleCardsUpdated() {
this.checkAlerts(this.state.columns.cart.items);
}
handleCrateModeChange(mode) {
this.setState({
currentMode: mode,
@ -398,7 +394,22 @@ export class Shop extends PureComponent {
switch (source.droppableId) {
// TODO add delete functionality
case destination.droppableId:
this.setState({
/*this.setState({
...this.state,
columns: {
...this.state.columns,
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
)}}
}
});*/
this.checkAlertsInNewState(destination.droppableId, {
...this.state,
columns: {
...this.state.columns,
@ -413,10 +424,9 @@ export class Shop extends PureComponent {
)}}
}
});
this.onCrateChanged(destination.droppableId);
break;
case 'backlog':
this.setState({
/*this.setState({
...this.state,
columns: {
...this.state.columns,
@ -431,8 +441,23 @@ export class Shop extends PureComponent {
destination
)}}
}
});*/
this.checkAlertsInNewState(destination.droppableId, {
...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
)}}
}
});
this.onCrateChanged(destination.droppableId);
break;
default:
this.setState({
@ -460,17 +485,16 @@ export class Shop extends PureComponent {
});
}
checkAlerts(crate_id) {
checkAlertsInNewState(crate_id, new_state) {
console.log('--- START CHECKING CRATE WARNING ---');
let itemsCloned = Array.from(this.state.columns.crates[crate_id].items);
let itemsCloned = Array.from(new_state.columns.crates[crate_id].items);
const crate_warnings = TriggerCrateWarnings(this.state.columns.crates[crate_id]);
const crate_warnings = TriggerCrateWarnings(new_state.columns.crates[crate_id]);
itemsCloned.forEach((elem, idx) => {
if (!(idx in itemsCloned)) itemsCloned[idx] = elem;
if (idx in this.state.columns.cart.itemsData && this.state.columns.cart.itemsData[idx].options_data) {
itemsCloned[idx].options_data = this.state.columns.cart.itemsData[idx].options_data;
itemsCloned.forEach((elem, _idx) => {
if (!elem.options_data && !!elem.options) {
elem.options_data = {}
}
});
@ -481,11 +505,11 @@ export class Shop extends PureComponent {
this.setState({
...this.state,
columns: {
...this.state.columns,
...new_state.columns,
crates: {
...this.state.columns.crates,
...new_state.columns.crates,
[crate_id]: {
...this.state.columns.crates[crate_id],
...new_state.columns.crates[crate_id],
items: itemsCloned,
warnings: crate_warnings
},
@ -545,98 +569,21 @@ export class Shop extends PureComponent {
this.setState(new_state);
}
render() {
const {
currency,
currentItemHovered,
currentMode,
crateModeSlots,
crateModeItems,
items,
columns,
rules,
mobileSideMenuShouldOpen,
newCardJustAdded,
isProcessing,
shouldShowRFQFeedback,
RFQBodyType,
RFQBodyOrder,
isProcessingComplete,
} = this.state;
return (
<DragDropContext onDragEnd={this.handleOnDragEnd}>
<Layout
aside={
<Backlog/>
}
main={(
<OrderPanel
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>)}
/>
)}>
</Layout>
</DragDropContext>
);
const isMobile = window.deviceIsMobile();
const isTouch = window.isTouchEnabled();
return (
<DragDropContext onDragEnd={this.handleOnDragEnd}>
<Layout
showRFQFeedback={shouldShowRFQFeedback}
RFQBodyType={RFQBodyType}
RFQBodyOrder={RFQBodyOrder}
className="shop"
mobileSideMenuShouldOpen={mobileSideMenuShouldOpen}
isMobile={isMobile}
newCardJustAdded={newCardJustAdded}
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
onClickCloseRFQFeedback={this.handleClickCloseRFQFeedback}
onClickLoadCustomConf={this.handleLoadCustomConf}
items={items}
aside={
<Backlog
currency={currency}
items={items}
data={columns.backlog}
onClickAddItem={this.handleClickAddItem}
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
isMobile={isMobile}>
</Backlog>
}
main={(
<OrderPanel
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
onClickOpenImport={this.handleClickOpenImport}
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>)}
cratesList={
<CrateList
crates={columns.crates}
isTouch={isTouch}
isMobile={isMobile}
onAddCrate={this.onAddCrate}
onDeleteCrate={this.onDeleteCrate}
onModeChange={this.onCrateModeChange}
onCrateSelect={this.onCrateSelected}
active_crate={this.state.active_crate}
/>
}
summaryPrice={
<OrderSummary
currency={currency}
crates={columns.crates}
onMouseEnterItem={this.handleMouseEnterItem}
onMouseLeaveItem={this.handleMouseLeaveItem}
onDeleteItem={this.handleDeleteItem}
onDeleteAllItems={this.handleDeleteAllItems}
onClickSelectItem={this.handleClickSelectItem}>
</OrderSummary>
}
form={
<OrderForm
isProcessingComplete={isProcessingComplete}
processingComplete={this.handleProcessingComplete}
isProcessing={isProcessing}
onClickSubmit={this.handleClickSubmit}
onClickShow={this.handleClickShowOrder}>
</OrderForm>
}>
</OrderPanel>
)}>
</Layout>
</DragDropContext>
);
}
}

View File

@ -53,7 +53,7 @@ export const resource_counters = {
"hp": CounterFactory("hp"),
}
function CountResources(data, index) {
export function CountResources(data, index) {
if (!data[index].resources) return null;
let result = [];
data[index].resources.forEach((item, _) => {

View File

@ -0,0 +1,192 @@
'use strict';
import {create} from "zustand";
import {data, itemsUnfoldedList} from "./utils";
import {true_type_of} from "./options/utils";
import {v4 as uuidv4} from "uuid";
import {FillResources} from "./count_resources";
import {TriggerCrateWarnings, TriggerWarnings} from "./warnings";
const useBacklog = ((set, get) => ({
cards: data.items,
groups: data.columns.backlog,
cards_list: itemsUnfoldedList,
currency: data.currency
}));
const useCrateModes = ((set, get) => ({
crate_modes: data.crateModes,
modes_order: data.crateModeOrder
}));
const useLayout = ((set, get) => ({
isTouch: window.isTouchEnabled(),
isMobile: window.deviceIsMobile(),
sideMenuIsOpen: false,
newCardJustAdded: false,
importIsOpen: false,
switchSideMenu: () => set(state => ({
sideMenuIsOpen: !state.sideMenuIsOpen
})),
openImport: () => set(state => ({
importIsOpen: true
})),
closeImport: () => set(state => ({
importIsOpen: false
})),
}))
const useSubmitForm = ((set, get) => ({
// TODO think about it
isProcessing: false,
shouldShowRFQFeedback: true,
RFQBodyType: 'email',
isProcessingComplete: true,
}));
const useCart = ((set, get) => ({
crates: data.columns.crates,
active_crate: "crate0",
highlighted: {
crate: "",
card: 0
},
newCrate: () => set((state) => ({crates: state.crates.concat({
id: "crate"+state.crates.length,
crate_mode: "rack",
items: [],
warnings: []
})})),
delCrate: (id) => set(state => ({
crates: state.crates.filter((crate => crate.id !== id))
})),
setCrateMode: (id, mode) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate.id === id) {
return {
...crate,
crate_mode: mode
}
} else return crate;
})
})),
setActiveCrate: (id) => set(state => ({active_crate: id})),
addCardFromBacklog: (crate_to, index_from, index_to) => set(state => {
const take_from = (true_type_of(index_from) === "array" ? index_from : [index_from]).map((item, _i) => (state.cards_list[item]));
const dest = crate_to || state.active_crate;
return {
crates: state.crates.map((crate, _i) => {
if (dest === crate.id) {
return {
...crate,
items: crate.items.toSpliced(index_to, 0, ...take_from.map((card_name, _) => {
return {...state.cards[card_name], id: uuidv4()}
}))
}
} else return crate;
})
}
}),
moveCard: (crate_from, index_from, crate_to, index_to) => set(state => {
const the_card = state.crates.find((crate, _) => crate_from === crate.id )[index_from];
const del_card = crate_from === crate_to ? 1 : 0;
return {
crates: state.crates.map((crate, _i) => {
if (crate_to === crate.id) {
return {
...crate,
items: crate.items.toSpliced(index_to, del_card, the_card)
}
} else if (crate_from === crate.id) {
return {
...crate,
items: crate.items.toSpliced(index_to, 1)
}
}
else return crate;
})
}
}),
deleteCard: (crate, index) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate === crate.id) {
return {
...crate,
items: crate.items.splice(index, 1)
}
}
else return crate;
})
})),
clearCrate: (id) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (id === crate.id) {
return {
...crate,
items: []
}
}
else return crate;
})
})),
updateOptions: (crate, index, new_options) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate === crate.id) {
let itemsCopy = Array.from(crate.items);
itemsCopy[index].options_data = {...itemsCopy[index].options_data, ...new_options};
return {
...crate,
items: itemsCopy
}
}
else return crate;
})
})),
highlightCard: (crate, index) => set(state => ({
highlighted: {
crate: crate,
card: index
}
})),
highlightReset: () => set(state => ({
highlighted: {
crate: "",
card: 0
}
})),
fillWarnings: (crate) => set(state => ({
// actually seems to be just render-time action, no need to put data in it,
// though needs to be optimized to be done only on real crate updates
crates: state.crates.map((crate, _i) => {
if (crate === crate.id) {
let itemsCopy = Array.from(crate.items);
itemsCopy = FillResources(itemsCopy);
itemsCopy = TriggerWarnings(itemsCopy);
const crate_warnings = TriggerCrateWarnings(crate);
return {
...crate,
items: itemsCopy,
warnings: crate_warnings
}
}
else return crate;
})
}))
// TODO load and save jsons?
}))
export const useShopStore = create((...params) => ({
...useBacklog(...params),
...useCrateModes(...params),
...useCart(...params),
...useSubmitForm(...params),
...useLayout(...params)
}))

View File

@ -98,6 +98,20 @@ const Types = {
}
export function TriggerCardWarnings(data, index, precounted) {
const element = data[index];
return element.warnings
.map((warning, _) => {
if (!!Types[warning])
return Types[warning].trigger(data, index, precounted) ? {trigger: undefined, name: warning, ...Types[warning]} : null;
else
return Types.default;
})
.filter((warning, _) => {
return !!warning
});
}
export function TriggerWarnings(data) {
return data.map((element, index) => {
if (!element.warnings) return element;

View File

@ -1185,13 +1185,12 @@ const shop_data = {
],
},
"crates": {
"crate0": {
crate_type: "rack",
"crates": [{
id: "crate0",
crate_mode: "rack",
items: [],
warnings: []
}
}
}]
},