From 4527189994c16c87b79b7e6cf64ca2d291449eea Mon Sep 17 00:00:00 2001 From: Egor Savkin Date: Tue, 30 Jan 2024 13:02:01 +0800 Subject: [PATCH] Add flexible crate options Signed-off-by: Egor Savkin --- static/js/shop/Cart.jsx | 3 - static/js/shop/Crate.jsx | 3 +- static/js/shop/CrateOptions.jsx | 43 ++++++++++++ static/js/shop/Shop.jsx | 2 + static/js/shop/SummaryCrate.jsx | 6 +- static/js/shop/SummaryCratePricedOptions.jsx | 48 ++++++++++++++ static/js/shop/options/Options.jsx | 5 +- static/js/shop/options/components/Switch.jsx | 9 +++ static/js/shop/options/utils.js | 8 ++- static/js/shop/shop_store.js | 69 ++++++++++++++++++-- static/js/shop_data.js | 8 ++- 11 files changed, 187 insertions(+), 17 deletions(-) create mode 100644 static/js/shop/CrateOptions.jsx create mode 100644 static/js/shop/SummaryCratePricedOptions.jsx diff --git a/static/js/shop/Cart.jsx b/static/js/shop/Cart.jsx index fcc0aaa..1876a35 100644 --- a/static/js/shop/Cart.jsx +++ b/static/js/shop/Cart.jsx @@ -3,7 +3,6 @@ import {Droppable} from "@hello-pangea/dnd"; import {cartStyle, compareArraysWithIds} from "./utils"; import {ProductCartItem} from "./ProductCartItem"; import {FakePlaceholder} from "./FakePlaceholder"; -import {FillExtData} from "./options/utils"; import {hp_to_slots} from "./count_resources"; import {useShopStore} from "./shop_store"; @@ -29,12 +28,10 @@ export function Cart({crate_index}) { const nbrSlots = hp_to_slots(crateParams(crate.crate_mode).hp); const products = crate.items.map((item, index) => { - const ext_data = FillExtData(crate.items, index); return ( = nbrSlots} key={item.id}/> diff --git a/static/js/shop/Crate.jsx b/static/js/shop/Crate.jsx index bf2d837..432b866 100644 --- a/static/js/shop/Crate.jsx +++ b/static/js/shop/Crate.jsx @@ -7,6 +7,7 @@ import {useShopStore} from "./shop_store"; // #!render_count import {useRenderCount} from "@uidotdev/usehooks"; import {CrateFanTray} from "./CrateFanTray"; +import {CrateOptions} from "./CrateOptions"; /** @@ -45,7 +46,7 @@ export function Crate({crate_index}) { - + ); diff --git a/static/js/shop/CrateOptions.jsx b/static/js/shop/CrateOptions.jsx new file mode 100644 index 0000000..1380a30 --- /dev/null +++ b/static/js/shop/CrateOptions.jsx @@ -0,0 +1,43 @@ +import React from 'react'; + +import {useShopStore} from "./shop_store"; +import {ProcessOptions, ProcessOptionsToData} from "./options/Options"; + +export function CrateOptions({crate_index}) { + const crate_id = useShopStore((state) => state.crates[crate_index].id); + const optionsLogic = useShopStore((state) => state.crate_options); + const updateOptions = useShopStore((state) => state.updateCrateOptions); + const options_data = useShopStore((state) => state.crates[crate_index].options_data || {}); + + console.log(options_data) + + const options = ProcessOptions({ + options: optionsLogic, + data: options_data, + id: "crate_options" + crate_id, + target: { + construct: ((outvar, value) => { + // #!options_log + console.log("construct", outvar, value, options_data); + + options_data[outvar] = value; + }), + update: ((outvar, value) => { + // #!options_log + console.log("update", outvar, value, options_data); + + if (outvar in options_data) options_data[outvar] = value; + + updateOptions(crate_id, {[outvar]: value}); + }) + } + }); + + console.log(options) + + return ( +
+ {options} +
+ ) +} \ No newline at end of file diff --git a/static/js/shop/Shop.jsx b/static/js/shop/Shop.jsx index 7e5e63e..6ea7582 100644 --- a/static/js/shop/Shop.jsx +++ b/static/js/shop/Shop.jsx @@ -19,6 +19,7 @@ export function Shop() { const renderCount = useRenderCount(); const addCardFromBacklog = useShopStore((state) => state.addCardFromBacklog); + const initExtData = useShopStore((state) => state.initExtData); const moveCard = useShopStore((state) => state.moveCard); const deleteCard = useShopStore((state) => state.deleteCard); const cardIndexById = useShopStore((state) => state.cardIndexById); @@ -38,6 +39,7 @@ export function Shop() { useEffect(() => { addCardFromBacklog(null, [cardIndexById("eem_pwr_mod"), cardIndexById("kasli")], -1, true); + initExtData(); }, []); // #!render_count diff --git a/static/js/shop/SummaryCrate.jsx b/static/js/shop/SummaryCrate.jsx index e739ee2..caa2abf 100644 --- a/static/js/shop/SummaryCrate.jsx +++ b/static/js/shop/SummaryCrate.jsx @@ -3,10 +3,11 @@ import React from "react"; import {useShopStore} from "./shop_store"; import {SummaryCrateHeader} from "./SummaryCrateHeader"; import {SummaryCrateCard} from "./SummaryCrateCard"; +import {SummaryCratePricedOptions} from "./SummaryCratePricedOptions"; // #!render_count import {useRenderCount} from "@uidotdev/usehooks"; -import {SummaryCrateFanTray} from "./SummaryCrateFanTray"; + export function SummaryCrate({crate_index}) { // #!render_count @@ -26,7 +27,8 @@ export function SummaryCrate({crate_index}) { {range(0, crate_len).map((index, _i) => )} - + + ) } \ No newline at end of file diff --git a/static/js/shop/SummaryCratePricedOptions.jsx b/static/js/shop/SummaryCratePricedOptions.jsx new file mode 100644 index 0000000..f779e28 --- /dev/null +++ b/static/js/shop/SummaryCratePricedOptions.jsx @@ -0,0 +1,48 @@ +import {formatMoney} from "./utils"; +import React from "react"; +import {useShopStore} from "./shop_store"; +import {ProcessOptionsToData} from "./options/Options"; + +// #!render_count +import {useRenderCount} from "@uidotdev/usehooks"; + +export function SummaryCratePricedOptions({crate_index}) { + // #!render_count + const renderCount = useRenderCount(); + + const currency = useShopStore((state) => state.currency); + const crate_id = useShopStore((state) => state.crates[crate_index].id); + const optionsPrices = useShopStore((state) => state.crate_prices); + const updateOptions = useShopStore((state) => state.updateCrateOptions); + const options_data = useShopStore((state) => state.crates[crate_index].options_data || {}); + + const options = ProcessOptionsToData({options: optionsPrices, data: options_data}); + + console.log(options, options_data, optionsPrices) + + // #!render_count + console.log("SummaryCratePricedOptions renders: ", renderCount) + + return options.map((option, i) => ( + + +   +
{option.title}
+ + + +
+ {`${currency} ${formatMoney(option.price)}`} + + + +
+
+ + + )); +} \ No newline at end of file diff --git a/static/js/shop/options/Options.jsx b/static/js/shop/options/Options.jsx index 5fffb4f..a0c68ac 100644 --- a/static/js/shop/options/Options.jsx +++ b/static/js/shop/options/Options.jsx @@ -49,7 +49,7 @@ export function ProcessOptionsToData({options, data}) { options: option_item, data: data, })) - ).flat(); + ).filter((item, i) => !!item).flat(); } else if (options_t === "object") { if (true_type_of(options.title) === "string") { return options; @@ -57,6 +57,7 @@ export function ProcessOptionsToData({options, data}) { return ProcessOptionsToData({options: json_logic_apply(options, data), data: data}); } } else { - throw Error("Incompatible type for the option: " + options_t) + //throw Error("Incompatible type for the option: " + options_t) + return null; } } \ No newline at end of file diff --git a/static/js/shop/options/components/Switch.jsx b/static/js/shop/options/components/Switch.jsx index 64e5d4c..44cbe54 100644 --- a/static/js/shop/options/components/Switch.jsx +++ b/static/js/shop/options/components/Switch.jsx @@ -23,6 +23,15 @@ class Switch extends Component { this.props.target.update(this.props.outvar, new_checked); } + static getDerivedStateFromProps(props, current_state) { + if (current_state.checked !== props.data[props.outvar]) { + return { + checked: props.data[props.outvar] + } + } + return null + } + render() { let key = this.props.id + this.props.outvar; return ( diff --git a/static/js/shop/options/utils.js b/static/js/shop/options/utils.js index 79e0670..56288ae 100644 --- a/static/js/shop/options/utils.js +++ b/static/js/shop/options/utils.js @@ -4,7 +4,7 @@ import {componentsList} from "./components/components"; // https://stackoverflow.com/a/70511311 export const true_type_of = (obj) => Object.prototype.toString.call(obj).slice(8, -1).toLowerCase(); -export function FillExtData(data, index) { +export function FillExtCardData(data, index) { return { has_other_dio: data.filter((value, item_index) => index !== item_index && value.name &&value.name.endsWith("-TTL")).length > 0, has_dds: data.filter(((value, _) => value.name === "DDS" && value.name_number === "4410" && (!value.options_data || !value.options_data.mono_eem))).length > 0, @@ -12,6 +12,12 @@ export function FillExtData(data, index) { } } +export function FillExtCrateData(crate) { + return { + crate_mode: crate.crate_mode + } +} + export function FilterOptions(options, data) { let options_t = true_type_of(options); let target = {}; diff --git a/static/js/shop/shop_store.js b/static/js/shop/shop_store.js index 02f78ba..7900155 100644 --- a/static/js/shop/shop_store.js +++ b/static/js/shop/shop_store.js @@ -2,10 +2,10 @@ import {createWithEqualityFn} from "zustand/traditional"; import {data as shared_data, itemsUnfoldedList} from "./utils"; -import {true_type_of} from "./options/utils"; +import {FillExtCrateData, true_type_of} from "./options/utils"; import {v4 as uuidv4} from "uuid"; import {FillResources} from "./count_resources"; -import {FillExtData} from "./options/utils"; +import {FillExtCardData} from "./options/utils"; import {TriggerCrateWarnings, TriggerWarnings} from "./warnings"; import {Validation, validateEmail, validateNote, validateJSONInput} from "./validate"; import {CratesToJSON, JSONToCrates} from "./json_porter"; @@ -38,6 +38,49 @@ const useCrateOptions = ((set, get) => ({ crate_options: shared_data.crateOptions.options, crate_prices: shared_data.crateOptions.prices, + fillExtCrateData: (crate_id) => set(state => ({ + crates: state.crates.map((crate, _i) => { + if (crate_id === crate.id) { + const previous_options = crate.options_data || {}; + return { + ...crate, + options_data: { + ...previous_options, + ext_data: FillExtCrateData(crate) + } + } + } + else return crate; + }) + })), + + _updateCrateOption: (crate_id, new_options) => set(state => ({ + crates: state.crates.map((crate, _i) => { + if (crate_id === crate.id) { + const previous_options = crate.options_data || {}; + console.log(crate_id, new_options, { + ...crate, + options_data: { + ...previous_options, + ...new_options + } + }) + return { + ...crate, + options_data: { + ...previous_options, + ...new_options + } + } + } + else return crate; + }) + })), + + updateCrateOptions: (crate_id, new_options) => { + get().fillExtCrateData(crate_id); + get()._updateCrateOption(crate_id, new_options); + } })); const useOrderOptions = ((set, get) => ({ @@ -45,14 +88,20 @@ const useOrderOptions = ((set, get) => ({ orderPrices: shared_data.crateOptions.prices, order_options_data: {}, + // in case of future needs fillOrderExtData: _ => {}, - _updateOrderOption: (new_options) => set(state => ({ + _updateOrderOptions: (new_options) => set(state => ({ order_options_data: { ...state.order_options_data, ...new_options } - })) + })), + + updateOrderOptions: (new_options) => { + get().fillOrderExtData(); + get()._updateOrderOptions(new_options); + } })); const useLayout = ((set, get) => ({ @@ -400,7 +449,7 @@ const useCart = ((set, get) => ({ itemsCopy = itemsCopy.map((item, index) => { if (!item.options) return item; if (!item.options_data) item.options_data = {}; - item.options_data.ext_data = FillExtData(itemsCopy, index); + item.options_data.ext_data = FillExtCardData(itemsCopy, index); return item; }); return { @@ -430,12 +479,14 @@ const useCart = ((set, get) => ({ const crate_id = "crate" + get().crates.length; get()._newCrate(crate_id) get().fillExtData(crate_id); + get().fillExtCrateData(crate_id); get().fillWarnings(crate_id); }, setCrateMode: (id, mode) => { get()._setCrateMode(id, mode) get().fillExtData(id); + get().fillExtCrateData(id); get().fillWarnings(id); get().setActiveCrate(id); }, @@ -497,6 +548,14 @@ const useCart = ((set, get) => ({ fanTrayAvailableByIndex: (crate_index) => { return get().fanTrayAvailableForMode(get().crates[crate_index].crate_mode); + }, + + initExtData: () => { + get().fillOrderExtData(); + get().crates.forEach((crate, _i) => { + get().fillExtData(crate.id); + get().fillExtCrateData(crate.id); + }) } })) diff --git a/static/js/shop_data.js b/static/js/shop_data.js index 9862564..9660093 100644 --- a/static/js/shop_data.js +++ b/static/js/shop_data.js @@ -34,14 +34,16 @@ const shop_data = { {"==": [{"var": "ext_data.crate_mode"}, "rack",]}, {type: "Switch", args: { title: "Add fan tray", - outvar: "nuc", + outvar: "fan_tray", tip: "Add 1U 84hp fan tray (to be mounted under the crate) to improve cooling. " + "Fans need 220VAC 50/60Hz power. 3 fans, 167m³/h air flow.", - fallback: false, + fallback: false }} ]}, ], - prices: [{"if": [{"and": [{"var": "fan_tray"}, {"==": [{"var": "ext_data.crate_mode"}, "rack",]}]}, {title: "Add fan tray", price: 470}]}] + prices: [{"if": [ + {"and": [{"var": "fan_tray"}, {"==": [{"var": "ext_data.crate_mode"}, "rack",]}]}, + {title: "Add fan tray", price: 470, disable_patch: {"fan_tray": false}, id: "fan_tray"}]}] }, orderOptions: {