diff --git a/package-lock.json b/package-lock.json index 4c8b0154..b1e5e38c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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 + } + } } } } diff --git a/package.json b/package.json index cdf0984e..3aa28660 100644 --- a/package.json +++ b/package.json @@ -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": [ diff --git a/static/js/shop/Backlog.jsx b/static/js/shop/Backlog.jsx index dc0985a9..c207e6a0 100644 --- a/static/js/shop/Backlog.jsx +++ b/static/js/shop/Backlog.jsx @@ -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 ( -
-

- +

+
+
+ {group.items.map(item => { + item_index++; + return ( + + ) + })} +
+
+
+ ); + } + ); + + return ( + + + {(provided) => ( +
+ + {isMobile ? ( +
+ - -
-
- {group.items.map(item => { - item_index++; - return ( - - ) - })} -
+ ) : null} + +
+ {groups}
- ); - } - ); - return ( - - - {(provided) => ( -
- - {isMobile ? ( -
- -
- ) : null} - -
- {groups} + {provided.placeholder && ( +
+ {provided.placeholder}
+ )} +
+ )} - {provided.placeholder && ( -
- {provided.placeholder} -
- )} -
- )} +
+ ); - - ); - } } \ No newline at end of file diff --git a/static/js/shop/Cart.jsx b/static/js/shop/Cart.jsx index 7e20658c..2b180aa2 100644 --- a/static/js/shop/Cart.jsx +++ b/static/js/shop/Cart.jsx @@ -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 */ -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 ( = nbrSlots} - data={itemData} + card_index={index} + crate_index={crate_index} ext_data={ext_data} - onToggleOverlayRemove={onToggleOverlayRemove} - onClickRemoveItem={onClickRemoveItem} - onCardUpdate={onCardUpdate} - onClickItem={onClickItem} - model={item}> - + first={index === 0} + last={index === crate.items.length - 1 && nbrOccupied >= nbrSlots} + resources={resources} + warnings={warnings} + key={item.id}/> ); }); return ( - + {(provided, snapshot) => (
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 (
- +
Delete crate -
- - + {1 || (rules && rules.length > 0) && ( - + )}
-
); diff --git a/static/js/shop/CrateList.jsx b/static/js/shop/CrateList.jsx index 369dae72..5b617889 100644 --- a/static/js/shop/CrateList.jsx +++ b/static/js/shop/CrateList.jsx @@ -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 ( - {Object.entries(crates).map(([crate_id, crate], index) => - - onCrateSelect(crate_id)}>Crate #{`${index}`} + {crates.map((crate, index) => + + setActiveCrate(crate.id)}>Crate #{`${index}`} - onModeChange(crate_id, new_mode)} - /> + )} Add new crate - diff --git a/static/js/shop/CrateMode.jsx b/static/js/shop/CrateMode.jsx index 8e2aa20e..c8bcd911 100644 --- a/static/js/shop/CrateMode.jsx +++ b/static/js/shop/CrateMode.jsx @@ -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 ( ); diff --git a/static/js/shop/CrateWarnings.jsx b/static/js/shop/CrateWarnings.jsx index e9887c5d..4f80d3ad 100644 --- a/static/js/shop/CrateWarnings.jsx +++ b/static/js/shop/CrateWarnings.jsx @@ -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 (
- {rules.map((rule, index) => ( -

+ {crate_warnings.map((rule, index) => ( +

{rule.name}: {rule.message}

))} diff --git a/static/js/shop/Layout.jsx b/static/js/shop/Layout.jsx index 2c399175..a0e62db2 100644 --- a/static/js/shop/Layout.jsx +++ b/static/js/shop/Layout.jsx @@ -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 {
); } -} \ No newline at end of file +} +*/ + + +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 ( +
+ + + + {mobileSideMenuShouldOpen ? ( +
{main}
+ ) : ( +
{main}
+ )} + + {isMobile && newCardJustAdded ? ( +
+ ✓ added +
+ ) : null} +
+ ); +} diff --git a/static/js/shop/OrderPanel.jsx b/static/js/shop/OrderPanel.jsx index fff6f412..bb770505 100644 --- a/static/js/shop/OrderPanel.jsx +++ b/static/js/shop/OrderPanel.jsx @@ -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 (
- render() { - const { - title, - description, - cratesList, - summaryPrice, - form, - isMobile, - onClickToggleMobileSideMenu, - onClickOpenImport, - } = this.props; +

{title}

- return ( -
+
+ {description} +
-

{title}

+
+ +
-
- {description} -
+ {isMobile ? ( +
+ +
+ ) : null} -
- -
+ - {isMobile ? ( -
- -
- ) : null} +
+ - {cratesList} + +
-
- {summaryPrice} - - {form} -
- -
- ); - } +
); } \ No newline at end of file diff --git a/static/js/shop/ProductCartItem.jsx b/static/js/shop/ProductCartItem.jsx index 8be65009..0e5a9283 100644 --- a/static/js/shop/ProductCartItem.jsx +++ b/static/js/shop/ProductCartItem.jsx @@ -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 ( + - 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) => ( +
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(); - } +
+ {warnings && warnings.length > 0 && + () + } - handleOnClickItem(index, e) { - if (this.props.onClickItem && this.props.isTouch) { - this.props.onClickItem(index); - } - e.preventDefault(); - } + {options && ( + { + // 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}); + }) + }} + /> + )} +
- handleOnClickRemoveItem(index, e) { - if (this.props.onClickRemoveItem) { - this.props.onClickRemoveItem(index); - } - } +
{card.name_number}
- 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 ( - - - {(provided, snapshot) => (
setHighlight(crate.id, card_index)} + onClick={() => setHighlight(crate.id, card_index)} > - {/* warning container */} - -
- {warnings && warnings.length > 0 && - () - } - - {options && ( - { - // 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(); - }) - }} - /> - )} -
- -
{model.name_number}
- -
- - -
- - {/* remove container */} -
- - rm - -

Remove

-
- - {/* progression container */} - {resources && ( - - )} - - +
- )} -
- ); - } + {/* remove container */} +
onCardRemove(crate.id, card_index)}> + + rm + +

Remove

+
+ + {/* progression container */} + {resources && ( + + )} + + +
+ )} + +
+ ); } diff --git a/static/js/shop/Shop.jsx b/static/js/shop/Shop.jsx index c6ca37fa..ef8d1e1a 100644 --- a/static/js/shop/Shop.jsx +++ b/static/js/shop/Shop.jsx @@ -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 ( + + + } + main={( + 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 sales@m-labs.hk. The price is estimated and must be confirmed by a quote.

)} + /> + )}> +
+
+ ); - const isMobile = window.deviceIsMobile(); - const isTouch = window.isTouchEnabled(); - - return ( - - - - - } - main={( - 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 sales@m-labs.hk. The price is estimated and must be confirmed by a quote.

)} - cratesList={ - - } - summaryPrice={ - - - } - form={ - - - }> -
- )}> -
- -
- ); - } } \ No newline at end of file diff --git a/static/js/shop/count_resources.js b/static/js/shop/count_resources.js index 419d28a4..d9fd3748 100644 --- a/static/js/shop/count_resources.js +++ b/static/js/shop/count_resources.js @@ -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, _) => { diff --git a/static/js/shop/shop_store.js b/static/js/shop/shop_store.js new file mode 100644 index 00000000..124ff57e --- /dev/null +++ b/static/js/shop/shop_store.js @@ -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) +})) \ No newline at end of file diff --git a/static/js/shop/warnings.js b/static/js/shop/warnings.js index 0f21c823..beaca90d 100644 --- a/static/js/shop/warnings.js +++ b/static/js/shop/warnings.js @@ -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; diff --git a/static/js/shop_data.js b/static/js/shop_data.js index 1be11d0c..ca685f98 100644 --- a/static/js/shop_data.js +++ b/static/js/shop_data.js @@ -1185,13 +1185,12 @@ const shop_data = { ], }, - "crates": { - "crate0": { - crate_type: "rack", + "crates": [{ + id: "crate0", + crate_mode: "rack", items: [], warnings: [] - } - } + }] },