Add order options

Signed-off-by: Egor Savkin <es@m-labs.hk>
pull/117/head
Egor Savkin 2024-01-30 17:20:29 +08:00
parent 15d9124025
commit bcc8db6819
19 changed files with 189 additions and 61789 deletions

View File

@ -384,6 +384,17 @@ button {
} }
} }
.order-bar {
width: 100%;
font-size: 0.9rem;
padding: 0.75rem 1.25rem;
input[type="text"] {
padding: 0;
font-size: 0.9rem;
line-height: 1.1;
}
}
.crate { .crate {
position: relative; position: relative;

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
import {ProcessOptions, ProcessOptionsToData} from "./options/Options"; import {ProcessOptions} from "./options/Options";
export function CrateOptions({crate_index}) { export function CrateOptions({crate_index}) {
const crate_id = useShopStore((state) => state.crates[crate_index].id); const crate_id = useShopStore((state) => state.crates[crate_index].id);

View File

@ -0,0 +1,38 @@
import {useShopStore} from "./shop_store";
import {ProcessOptions} from "./options/Options";
import React from "react";
export function OrderOptions() {
const optionsLogic = useShopStore((state) => state.order_options);
const updateOptions = useShopStore((state) => state.updateOrderOptions);
const options_data = useShopStore((state) => state.order_options_data || {});
const options = ProcessOptions({
options: optionsLogic,
data: options_data,
id: "order_options",
target: {
construct: ((outvar, value) => {
// #!options_log
console.log("construct", outvar, value, options_data);
options_data[outvar] = value;
updateOptions({[outvar]: value});
}),
update: ((outvar, value) => {
// #!options_log
console.log("update", outvar, value, options_data);
if (outvar in options_data) options_data[outvar] = value;
updateOptions({[outvar]: value});
})
}
});
return (
<div className="order-bar border rounded">
{options}
</div>
)
}

View File

@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import {OrderSummary} from "./OrderSummary"; import {SummaryOrder} from "./SummaryOrder";
import {OrderForm} from "./OrderForm"; import {OrderForm} from "./OrderForm";
import {CrateList} from "./CrateList"; import {CrateList} from "./CrateList";
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
@ -8,6 +8,7 @@ import {RFQFeedback} from "./RFQFeedback";
// #!render_count // #!render_count
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
import {OrderOptions} from "./OrderOptions";
/** /**
* Component that renders all things for order. * Component that renders all things for order.
@ -46,8 +47,10 @@ export function OrderPanel({title, description}) {
<CrateList/> <CrateList/>
<OrderOptions/>
<section className="summary"> <section className="summary">
<OrderSummary/> <SummaryOrder/>
<OrderForm/> <OrderForm/>
</section> </section>

View File

@ -4,7 +4,6 @@ import {useShopStore} from "./shop_store";
// #!render_count // #!render_count
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
import {CrateMode} from "./CrateMode";
export function SummaryCrateHeader({crate_index}) { export function SummaryCrateHeader({crate_index}) {
// #!render_count // #!render_count

View File

@ -21,7 +21,7 @@ export function SummaryCratePricedOptions({crate_index}) {
// #!render_count // #!render_count
console.log("SummaryCratePricedOptions renders: ", renderCount) console.log("SummaryCratePricedOptions renders: ", renderCount)
return options.map((option, i) => ( return options.map((option, _i) => (
<tr key={"summary_crate_" + crate_id +"option_" + option.id}> <tr key={"summary_crate_" + crate_id +"option_" + option.id}>
<td className="item-card-name"> <td className="item-card-name">
<span style={{ <span style={{

View File

@ -5,6 +5,7 @@ import {SummaryCrate} from "./SummaryCrate";
// #!render_count // #!render_count
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
import {SummaryOrderPricedOptions} from "./SummaryOrderPricedOptions";
export function SummaryCrates() { export function SummaryCrates() {
// #!render_count // #!render_count
@ -20,6 +21,7 @@ export function SummaryCrates() {
{range(0, crates_l).map((index, _i) => { {range(0, crates_l).map((index, _i) => {
return <SummaryCrate crate_index={index} key={"summary_crate_body_" + index} /> return <SummaryCrate crate_index={index} key={"summary_crate_body_" + index} />
})} })}
<SummaryOrderPricedOptions/>
</> </>
) )
} }

View File

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import {useShopStore} from "./shop_store";
import {SummaryCrates} from "./SummaryCrates"; import {SummaryCrates} from "./SummaryCrates";
import {SummaryTotalPrice} from "./SummaryTotalPrice"; import {SummaryTotalPrice} from "./SummaryTotalPrice";
@ -11,12 +10,12 @@ import {useRenderCount} from "@uidotdev/usehooks";
* Components that displays the list of card that are used in the crate. * Components that displays the list of card that are used in the crate.
* It is a summary of purchase * It is a summary of purchase
*/ */
export function OrderSummary() { export function SummaryOrder() {
// #!render_count // #!render_count
const renderCount = useRenderCount(); const renderCount = useRenderCount();
// #!render_count // #!render_count
console.log("OrderSummary renders: ", renderCount) console.log("SummaryOrder renders: ", renderCount)
return ( return (
<div className="summary-price"> <div className="summary-price">

View File

@ -0,0 +1,43 @@
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 SummaryOrderPricedOptions() {
// #!render_count
const renderCount = useRenderCount();
const currency = useShopStore((state) => state.currency);
const optionsPrices = useShopStore((state) => state.order_prices);
const updateOptions = useShopStore((state) => state.updateOrderOptions);
const options_data = useShopStore((state) => state.order_options_data);
const options = ProcessOptionsToData({options: optionsPrices, data: options_data});
// #!render_count
console.log("SummaryOrderPricedOptions renders: ", renderCount)
return <tbody key={"summary_order_body"}>
{options.map((option, _i) => (
<tr key={"summary_order" + "option_" + option.id}>
<td className="item-card-name">
<div>{option.title}</div>
</td>
<td className="price">
<div className="d-inline-flex align-content-center">
{`${currency} ${formatMoney(option.price)}`}
<button onClick={() => updateOptions(option.disable_patch)}>
<img src="/images/shop/icon-remove.svg" className="d-block"/>
</button>
<div style={{'width': '45px', 'height': '20px'}} className="d-inline"></div>
</div>
</td>
</tr>
))}
</tbody>;
}

View File

@ -5,20 +5,23 @@ import {v4 as uuidv4} from "uuid";
export function validateJSON(description) { export function validateJSON(description) {
let crates_raw; let crates_raw;
let order_options;
try { try {
const parsed = JSON.parse(description); const parsed = JSON.parse(description);
// here we can check additional fields // here we can check additional fields
crates_raw = parsed.crates; crates_raw = parsed.crates;
order_options = parsed.options;
} catch (e) { } catch (e) {
return false; return false;
} }
if (!order_options) return false;
const crate_modes = useShopStore.getState().crate_modes; const crate_modes = useShopStore.getState().crate_modes;
const modes_order = useShopStore.getState().modes_order; const modes_order = useShopStore.getState().modes_order;
const pn_to_card = useShopStore.getState().pn_to_cards; const pn_to_card = useShopStore.getState().pn_to_cards;
try { try {
for (const crate of crates_raw) { for (const crate of crates_raw) {
if (!crate.type || !crate.items || !(crate.type in crate_modes)) return false; if (!crate.type || !crate.items || !crate.options || !(crate.type in crate_modes)) return false;
for (const card of crate.items) { for (const card of crate.items) {
if (!(card.pn in pn_to_card) || card.options === undefined) return false; if (!(card.pn in pn_to_card) || card.options === undefined) return false;
} }
@ -53,12 +56,15 @@ export function JSONToCrates(description) {
return { return {
// some additional fields go here // some additional fields go here
order_options_data: parsed.options,
crates: crates crates: crates
}; };
} }
export function CratesToJSON(crates) { export function CratesToJSON(crates) {
const crateOptions = useShopStore.getState().crate_options; const crateOptions = useShopStore.getState().crate_options;
const orderOptions = useShopStore.getState().order_options;
const orderOptionsData = useShopStore.getState().order_options_data;
return JSON.stringify({ return JSON.stringify({
// additional fields can go here // additional fields can go here
crates: Array.from(crates.map((crate, _i) => ({ crates: Array.from(crates.map((crate, _i) => ({
@ -68,6 +74,7 @@ export function CratesToJSON(crates) {
}))), }))),
type: crate.crate_mode, type: crate.crate_mode,
options: FilterOptions(crateOptions, crate.options_data) options: FilterOptions(crateOptions, crate.options_data)
}))) }))),
options: FilterOptions(orderOptions, orderOptionsData)
}, null, 2) }, null, 2)
} }

View File

@ -45,11 +45,11 @@ export function ProcessOptionsToData({options, data}) {
let options_t = true_type_of(options); let options_t = true_type_of(options);
if (options_t === "array") { if (options_t === "array") {
return Array.from( return Array.from(
options.map((option_item, i) => ProcessOptionsToData({ options.map((option_item, _i) => ProcessOptionsToData({
options: option_item, options: option_item,
data: data, data: data,
})) }))
).filter((item, i) => !!item).flat(); ).filter((item, _i) => !!item).flat();
} else if (options_t === "object") { } else if (options_t === "object") {
if (true_type_of(options.title) === "string") { if (true_type_of(options.title) === "string") {
return options; return options;

View File

@ -21,6 +21,15 @@ class Line extends Component {
this.props.target.update(this.props.outvar, text); this.props.target.update(this.props.outvar, text);
} }
static getDerivedStateFromProps(props, current_state) {
if (current_state.text !== props.data[props.outvar]) {
return {
text: props.data[props.outvar]
}
}
return null
}
render() { render() {
let key = this.props.id + this.props.outvar; let key = this.props.id + this.props.outvar;
return ( return (

View File

@ -23,6 +23,15 @@ class Radio extends Component {
this.props.target.update(this.props.outvar, variant); this.props.target.update(this.props.outvar, variant);
} }
static getDerivedStateFromProps(props, current_state) {
if (current_state.variant !== props.data[props.outvar]) {
return {
variant: props.data[props.outvar]
}
}
return null
}
render() { render() {
let key = this.props.id + this.props.outvar; let key = this.props.id + this.props.outvar;
return ( return (

View File

@ -34,6 +34,16 @@ class SwitchLine extends Component {
this.props.target.update(this.props.outvar, new_state); this.props.target.update(this.props.outvar, new_state);
} }
static getDerivedStateFromProps(props, current_state) {
if (current_state.checked !== props.data[props.outvar].checked || current_state.text !== props.data[props.outvar].text) {
return {
checked: props.data[props.outvar].checked,
text: props.data[props.outvar].text,
}
}
return null
}
render() { render() {
let key = this.props.id + this.props.outvar; let key = this.props.id + this.props.outvar;
return ( return (

View File

@ -18,6 +18,12 @@ export function FillExtCrateData(crate) {
} }
} }
export function FillExtOrderData(crates, modes_order) {
return {
has_crate: crates.filter((crate) => modes_order.includes(crate.crate_mode)).length >= 1,
}
}
export function FilterOptions(options, data) { export function FilterOptions(options, data) {
let options_t = true_type_of(options); let options_t = true_type_of(options);
let target = {}; let target = {};

View File

@ -2,7 +2,7 @@
import {createWithEqualityFn} from "zustand/traditional"; import {createWithEqualityFn} from "zustand/traditional";
import {data as shared_data, itemsUnfoldedList} from "./utils"; import {data as shared_data, itemsUnfoldedList} from "./utils";
import {FillExtCrateData, true_type_of} from "./options/utils"; import {FillExtCrateData, FillExtOrderData, true_type_of} from "./options/utils";
import {v4 as uuidv4} from "uuid"; import {v4 as uuidv4} from "uuid";
import {FillResources} from "./count_resources"; import {FillResources} from "./count_resources";
import {FillExtCardData} from "./options/utils"; import {FillExtCardData} from "./options/utils";
@ -10,7 +10,6 @@ import {TriggerCrateWarnings, TriggerWarnings} from "./warnings";
import {Validation, validateEmail, validateNote, validateJSONInput} from "./validate"; import {Validation, validateEmail, validateNote, validateJSONInput} from "./validate";
import {CratesToJSON, JSONToCrates} from "./json_porter"; import {CratesToJSON, JSONToCrates} from "./json_porter";
import {ProcessOptionsToData} from "./options/Options"; import {ProcessOptionsToData} from "./options/Options";
import {forEach} from "react-bootstrap/ElementChildren";
const cards_to_pn_map = (cards) => { const cards_to_pn_map = (cards) => {
@ -74,18 +73,23 @@ const useCrateOptions = ((set, get) => ({
updateCrateOptions: (crate_id, new_options) => { updateCrateOptions: (crate_id, new_options) => {
get().fillExtCrateData(crate_id); get().fillExtCrateData(crate_id);
get().fillOrderExtData();
get()._updateCrateOption(crate_id, new_options); get()._updateCrateOption(crate_id, new_options);
get()._updateTotalOrderPrice(); get()._updateTotalOrderPrice();
} }
})); }));
const useOrderOptions = ((set, get) => ({ const useOrderOptions = ((set, get) => ({
orderOptions: shared_data.crateOptions.options, order_options: shared_data.orderOptions.options,
orderPrices: shared_data.crateOptions.prices, order_prices: shared_data.orderOptions.prices,
order_options_data: {}, order_options_data: {},
// in case of future needs fillOrderExtData: () => set(state => ({
fillOrderExtData: _ => {}, order_options_data: {
...state.order_options_data,
ext_data: FillExtOrderData(state.crates, state.modes_order)
}
})),
_updateOrderOptions: (new_options) => set(state => ({ _updateOrderOptions: (new_options) => set(state => ({
order_options_data: { order_options_data: {
@ -95,9 +99,9 @@ const useOrderOptions = ((set, get) => ({
})), })),
updateOrderOptions: (new_options) => { updateOrderOptions: (new_options) => {
get().fillOrderExtData();
get()._updateOrderOptions(new_options); get()._updateOrderOptions(new_options);
get()._updateTotalOrderPrice(); get()._updateTotalOrderPrice();
get().fillOrderExtData();
} }
})); }));
@ -148,12 +152,14 @@ const useImportJSON = ((set, get) => ({
const parsed = JSONToCrates(state.importValue.value); const parsed = JSONToCrates(state.importValue.value);
// if (parsed.crates[-1].crate_mode !== "") // if (parsed.crates[-1].crate_mode !== "")
return { return {
importShouldOpen: false, importShouldOpen: false,
// additional fields go here // additional fields go here
crates: parsed.crates crates: parsed.crates,
order_options_data: parsed.order_options_data
}}), }}),
loadDescription: () => { loadDescription: () => {
get()._loadDescription() get()._loadDescription()
get().fillOrderExtData();
get().crates.forEach((crate, _i) => { get().crates.forEach((crate, _i) => {
get().fillExtData(crate.id); get().fillExtData(crate.id);
get().fillWarnings(crate.id); get().fillWarnings(crate.id);
@ -317,7 +323,7 @@ const useCart = ((set, get) => ({
}), }),
active_crate: crate_id || "crate" + state.crates.length active_crate: crate_id || "crate" + state.crates.length
})), })),
delCrate: (id) => set(state => ({ _delCrate: (id) => set(state => ({
crates: state.crates.filter((crate => crate.id !== id || !state.modes_order.includes(crate.crate_mode))), crates: state.crates.filter((crate => crate.id !== id || !state.modes_order.includes(crate.crate_mode))),
active_crate: state.active_crate === id ? null : state.active_crate, active_crate: state.active_crate === id ? null : state.active_crate,
})), })),
@ -471,6 +477,8 @@ const useCart = ((set, get) => ({
sum += item.price; sum += item.price;
}); });
}); });
const order_options = ProcessOptionsToData({options: get().order_prices, data: get().order_options_data || {}});
sum += order_options ? order_options.reduce((accumulator, currentValue) => accumulator+currentValue.price, 0) : 0;
return {total_order_price: sum}; return {total_order_price: sum};
}), }),
@ -481,6 +489,7 @@ const useCart = ((set, get) => ({
get()._newCrate(crate_id) get()._newCrate(crate_id)
get().fillExtData(crate_id); get().fillExtData(crate_id);
get().fillExtCrateData(crate_id); get().fillExtCrateData(crate_id);
get().fillOrderExtData();
get().fillWarnings(crate_id); get().fillWarnings(crate_id);
get()._updateTotalOrderPrice(); get()._updateTotalOrderPrice();
}, },
@ -489,11 +498,17 @@ const useCart = ((set, get) => ({
get()._setCrateMode(id, mode) get()._setCrateMode(id, mode)
get().fillExtData(id); get().fillExtData(id);
get().fillExtCrateData(id); get().fillExtCrateData(id);
get().fillOrderExtData();
get().fillWarnings(id); get().fillWarnings(id);
get().setActiveCrate(id); get().setActiveCrate(id);
get()._updateTotalOrderPrice(); get()._updateTotalOrderPrice();
}, },
delCrate: (id) => {
get()._delCrate(id);
get().fillOrderExtData();
},
addCardFromBacklog: (crate_to, index_from, index_to, just_mounted) => { addCardFromBacklog: (crate_to, index_from, index_to, just_mounted) => {
const dest = crate_to || get().active_crate; const dest = crate_to || get().active_crate;
if (!dest) { if (!dest) {

View File

@ -53,7 +53,7 @@ export function formatMoney(amount, decimalCount = 2, decimal = ".", thousands =
let i = parseInt(amount = Math.abs(Number(amount) || 0).toFixed(decimalCount)).toString(); let i = parseInt(amount = Math.abs(Number(amount) || 0).toFixed(decimalCount)).toString();
let j = (i.length > 3) ? i.length % 3 : 0; let j = (i.length > 3) ? i.length % 3 : 0;
return negativeSign + (j ? i.substr(0, j) + thousands : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + thousands) + (decimalCount ? decimal + Math.abs(amount - i).toFixed(decimalCount).slice(2) : ""); return negativeSign + (j ? i.substring(0, j) + thousands : '') + i.substring(j).replace(/(\d{3})(?=\d)/g, "$1" + thousands) + (decimalCount ? decimal + Math.abs(amount - i).toFixed(decimalCount).slice(2) : "");
} catch (e) { } catch (e) {
return amount; return amount;
} }

View File

@ -66,20 +66,28 @@ const shop_data = {
title: "Desktop Environment", title: "Desktop Environment",
outvar: "nuc_desktop", outvar: "nuc_desktop",
variants: ["Gnome", "KDE"], variants: ["Gnome", "KDE"],
tip: "Gnome vs KDE", tip: "Gnome has clean and minimalist design. KDE has more feature-rich and classic interface.",
fallback: 0 fallback: 0
} }
}, },
{type: "Line", args: {title: "Additional software to be pre-installed", outvar: "software", fallback: "", {type: "Line", args: {title: "Additional software to be pre-installed", outvar: "software", fallback: "",
tip: "Pre-install additional software, if needed"}}, tip: "Pre-install additional software, if needed."}},
], ],
{"if": [
{"var": "ext_data.has_crate"},
{type: "Switch", args: {
title: "Opt-out from promotional USB stick",
outvar: "usb_stick_opt_out",
tip: "Choose if you don't need a USB stick and wish to receive configuration files via other electronic means (e.g. email or cloud).",
fallback: false,
}}
]},
] ]
}, },
], ],
prices: [{ prices: [{
"if": [{"var": "nuc"}, {title: "Include optional pre-installed Intel® NUC mini-computer", price: 1300}, 0], "if": [{"var": "nuc"}, {title: "Include optional pre-installed Intel® NUC mini-computer", price: 1300, disable_patch: {"nuc": false}, id: "nuc"}],
}] }]
}, },