Fix moving cards in carts, json importer, and further rerender fixes and optimizations

Signed-off-by: Egor Savkin <es@m-labs.hk>
This commit is contained in:
Egor Savkin 2023-12-15 13:49:54 +08:00
parent 2bfc16e3c0
commit c09d583fa6
13 changed files with 198 additions and 76 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,9 +1,14 @@
import {OverlayTrigger} from "react-bootstrap"; import {OverlayTrigger} from "react-bootstrap";
import React from "react"; import React from "react";
import {MaxLevel} from "./warnings"; import {MaxLevel} from "./warnings";
import {useShopStore} from "./shop_store";
import {compareArraysLevelOne} from "./utils";
export function CardWarnings({warnings, prefix}) { export function CardWarnings({crate_index, card_index}) {
const warnings = useShopStore(state => state.crates[crate_index].items[card_index].show_warnings, compareArraysLevelOne);
const max_level = MaxLevel(warnings); const max_level = MaxLevel(warnings);
return ( return (
<OverlayTrigger <OverlayTrigger
@ -14,7 +19,7 @@ export function CardWarnings({warnings, prefix}) {
<div className="k-popup-warning" {...props}> <div className="k-popup-warning" {...props}>
{warnings.map((warning, _i) => { {warnings.map((warning, _i) => {
return ( return (
<p className="rule warning" key={`warnmsg_${prefix}_${warning.name}`}> <p className="rule warning" key={`warnmsg_${card_index}_${warning.name}`}>
<i>{warning.message}</i> <i>{warning.message}</i>
</p> </p>
) )
@ -28,7 +33,9 @@ export function CardWarnings({warnings, prefix}) {
) )
} }
export function WarningIndicator({warnings}) { export function WarningIndicator({crate_index, card_index}) {
const warnings = useShopStore(state => state.crates[crate_index].items[card_index].show_warnings, compareArraysLevelOne);
const max_level = MaxLevel(warnings); const max_level = MaxLevel(warnings);
return ( return (
<img <img

View File

@ -1,14 +1,14 @@
import React from 'react' import React from 'react'
import {Droppable} from "@hello-pangea/dnd"; import {Droppable} from "@hello-pangea/dnd";
import {cartStyle} from "./utils"; import {cartStyle, compareArraysWithIds} from "./utils";
import {ProductCartItem} from "./ProductCartItem"; import {ProductCartItem} from "./ProductCartItem";
import {FakePlaceholder} from "./FakePlaceholder"; import {FakePlaceholder} from "./FakePlaceholder";
import {FillExtData} from "./options/utils"; import {FillExtData} from "./options/utils";
import {hp_to_slots} from "./count_resources"; import {hp_to_slots} from "./count_resources";
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
// #!render_count // #!render_count
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
import {useShallow} from "zustand/react/shallow";
/** /**
* Component that displays a list of <ProductCartItem> * Component that displays a list of <ProductCartItem>
@ -18,9 +18,7 @@ export function Cart({crate_index}) {
const renderCount = useRenderCount(); const renderCount = useRenderCount();
const crate = useShopStore((state) => state.crates[crate_index], (a, b) => { const crate = useShopStore((state) => state.crates[crate_index], (a, b) => {
//console.log(a, b) return compareArraysWithIds(a.items, b.items) && a.occupiedHP === b.occupiedHP && a.crate_mode === b.crate_mode
return a.items.length === b.items.length && a.occupiedHP === b.occupiedHP && a.crate_mode === b.crate_mode
//return a === b
}); });
const crateParams = useShopStore((state) => state.crateParams); const crateParams = useShopStore((state) => state.crateParams);

View File

@ -1,18 +1,17 @@
import React from "react"; import React from "react";
import {LevelUI} from "./warnings"; import {LevelUI} from "./warnings";
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
import {compareArraysWithIds} from "./utils";
// #!render_count // #!render_count
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
const compareArrays = (a, b) =>
a.length === b.length &&
a.every((element, index) => element.id === b[index].id);
export function CrateWarnings({crate_index}) { export function CrateWarnings({crate_index}) {
// #!render_count // #!render_count
const renderCount = useRenderCount(); const renderCount = useRenderCount();
const crate_warnings = useShopStore(state => (state.crates[crate_index].warnings), compareArrays) const crate_warnings = useShopStore(state => (state.crates[crate_index].warnings), compareArraysWithIds)
// #!render_count // #!render_count
console.log("CrateWarnings renders: ", renderCount) console.log("CrateWarnings renders: ", renderCount)

View File

@ -6,10 +6,29 @@ import {Validation} from "./validate";
// #!render_count // #!render_count
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
const JSONExample = JSON.stringify({ const JSONExample = JSON.stringify([
"items": [{"pn": "1124"}, {"pn": "2118"}, {"pn": "2118"}, {"pn": "2128"}], {
"type": "desktop" "items": [
}); {
"pn": "1124",
"options": null
},
{
"pn": "2118",
"options": null
},
{
"pn": "2118",
"options": null
},
{
"pn": "2128",
"options": null
}
],
"type": "rack"
}
]);
export function ImportJSON() { export function ImportJSON() {
// #!render_count // #!render_count

View File

@ -0,0 +1,49 @@
import {DialogPopup} from "./options/DialogPopup";
import React from "react";
import {useShopStore} from "./shop_store";
import {SummaryPopup} from "./options/SummaryPopup";
export function OptionsDialogWrapper({crate_index, card_index, first, last}) {
const crate_id = useShopStore((state) => state.crates[crate_index].id);
const options = useShopStore((state) => state.crates[crate_index].items[card_index].options);
const options_data = useShopStore((state) => state.crates[crate_index].items[card_index].options_data);
const card_size = useShopStore((state) => state.crates[crate_index].items[card_index].size);
const options_class = useShopStore((state) => state.crates[crate_index].items[card_index].options_class);
const onOptionsUpdate = useShopStore((state) => state.updateOptions);
return (
<DialogPopup
options={options}
data={options_data}
options_class={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;
onOptionsUpdate(crate_id, card_index, {[outvar]: value});
})
}}
/>
)
}
export function OptionsSummaryWrapper({crate_index, card_index}) {
const card_id = useShopStore((state) => state.crates[crate_index].items[card_index].id);
const options = useShopStore((state) => state.crates[crate_index].items[card_index].options);
const options_data = useShopStore((state) => state.crates[crate_index].items[card_index].options_data);
return (
<SummaryPopup id={card_id + "options"} options={options}
data={options_data}/>
)
}

View File

@ -1,10 +1,10 @@
import React from 'react' import React from 'react'
import {Draggable} from "@hello-pangea/dnd"; import {Draggable} from "@hello-pangea/dnd";
import {DialogPopup} from "./options/DialogPopup"; import {compareObjectsEmptiness, productStyle} from "./utils";
import {productStyle} from "./utils";
import {Resources} from "./Resources"; import {Resources} from "./Resources";
import {CardWarnings} from "./CardWarnings"; import {CardWarnings} from "./CardWarnings";
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
import {OptionsDialogWrapper} from "./OptionsWrapper";
// #!render_count // #!render_count
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
@ -13,36 +13,30 @@ import {useRenderCount} from "@uidotdev/usehooks";
* Component that renders a product. * Component that renders a product.
* Used in the crate * Used in the crate
*/ */
export function ProductCartItem({card_index, crate_index, ext_data, first, last}) { export function ProductCartItem({card_index, crate_index, first, last}) {
// #!render_count // #!render_count
const renderCount = useRenderCount(); const renderCount = useRenderCount();
const card = useShopStore((state) => state.crates[crate_index].items[card_index], const card = useShopStore((state) => state.crates[crate_index].items[card_index],
(a, b) => { (a, b) => a.id === b.id);
//console.log(a.options_data, b.options_data, a.options_data === b.options_data)
return a.id === b.id && a.show_warnings === b.show_warnings && a.counted_resources === b.counted_resources && a.options_data === b.options_data const card_show_warnings = useShopStore(state => state.crates[crate_index].items[card_index].show_warnings, compareObjectsEmptiness);
} ); const card_counted_resources = useShopStore(state => state.crates[crate_index].items[card_index].counted_resources, compareObjectsEmptiness);
const highlighted = useShopStore((state) => state.crates[crate_index].id === state.highlighted.crate && card_index === state.highlighted.card); const highlighted = useShopStore((state) => state.crates[crate_index].id === state.highlighted.crate && card_index === state.highlighted.card);
const crate_id = useShopStore((state) => state.crates[crate_index].id); const crate_id = useShopStore((state) => state.crates[crate_index].id);
const setHighlight = useShopStore((state) => state.highlightCard); const setHighlight = useShopStore((state) => state.highlightCard);
const removeHighlight = useShopStore((state) => state.highlightReset); const removeHighlight = useShopStore((state) => state.highlightReset);
const onCardUpdate = useShopStore((state) => state.updateOptions);
const onCardRemove = useShopStore((state) => state.deleteCard); const onCardRemove = useShopStore((state) => state.deleteCard);
// #!render_count // #!render_count
console.log("ProductCartItem renders: ", renderCount) console.log("ProductCartItem renders: ", renderCount)
let options, options_data;
const warnings = card && card.show_warnings;
const resources = card && card.counted_resources;
if (card && card.options) { const options = card && card.options && card.options.length > 0;
options = card.options; const warnings = card_show_warnings && card_show_warnings.length > 0;
if (!card.options_data) card.options_data = {}; const resources = card_counted_resources && card_counted_resources.length > 0;
options_data = card.options_data;
options_data.ext_data = ext_data;
}
return ( return (
<Draggable draggableId={card.id} index={card_index}> <Draggable draggableId={card.id} index={card_index}>
@ -69,31 +63,16 @@ export function ProductCartItem({card_index, crate_index, ext_data, first, last}
{/* warning container */} {/* warning container */}
<div className="progress-container warning d-flex justify-content-evenly"> <div className="progress-container warning d-flex justify-content-evenly">
{warnings && warnings.length > 0 && {warnings &&
(<CardWarnings warnings={warnings} prefix={card_index}/>) (<CardWarnings crate_index={crate_index} card_index={card_index} />)
} }
{options && ( {options && (
<DialogPopup <OptionsDialogWrapper
options={options} crate_index={crate_index}
data={options_data} card_index={card_index}
options_class={card.options_class}
key={"popover" + card_index}
id={"popover" + card_index}
big={card.size === "big"}
first={first} first={first}
last={last} 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> </div>
@ -123,7 +102,7 @@ export function ProductCartItem({card_index, crate_index, ext_data, first, last}
{/* progression container */} {/* progression container */}
{resources && ( {resources && (
<Resources resources={resources}/> <Resources crate_index={crate_index} card_index={card_index} />
)} )}

View File

@ -4,6 +4,8 @@ import {v4 as uuidv4} from "uuid";
// #!render_count // #!render_count
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
import {useShopStore} from "./shop_store";
import {compareArraysLevelOne} from "./utils";
const resourcesWidthStyle = (occupied, max) => { const resourcesWidthStyle = (occupied, max) => {
@ -64,9 +66,12 @@ function RenderResources({resources, library}) {
return result; return result;
} }
export function Resources({resources}) { export function Resources({crate_index, card_index}) {
// #!render_count // #!render_count
const renderCount = useRenderCount(); const renderCount = useRenderCount();
const resources = useShopStore(state => state.crates[crate_index].items[card_index].counted_resources, compareArraysLevelOne);
// #!render_count // #!render_count
console.log("Resources renders: ", renderCount) console.log("Resources renders: ", renderCount)
return ( return (

View File

@ -1,12 +1,13 @@
import {formatMoney} from "./utils"; import {compareObjectsEmptiness, formatMoney} from "./utils";
import {WarningIndicator} from "./CardWarnings"; import {WarningIndicator} from "./CardWarnings";
import {SummaryPopup} from "./options/SummaryPopup";
import React from "react"; import React from "react";
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
import {OptionsSummaryWrapper} from "./OptionsWrapper";
// #!render_count // #!render_count
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
export function SummaryCrateCard({crate_index, card_index}) { export function SummaryCrateCard({crate_index, card_index}) {
// #!render_count // #!render_count
const renderCount = useRenderCount(); const renderCount = useRenderCount();
@ -19,15 +20,17 @@ export function SummaryCrateCard({crate_index, card_index}) {
const highlighted = useShopStore((state) => state.crates[crate_index].id === state.highlighted.crate && card_index === state.highlighted.card); const highlighted = useShopStore((state) => state.crates[crate_index].id === state.highlighted.crate && card_index === state.highlighted.card);
const crate_id = useShopStore((state) => state.crates[crate_index].id); const crate_id = useShopStore((state) => state.crates[crate_index].id);
const card = useShopStore((state) => state.crates[crate_index].items[card_index], const card = useShopStore((state) => state.crates[crate_index].items[card_index],
(a, b) => a.id === b.id && a.options_data === b.options_data && a.show_warnings === b.show_warnings); (a, b) => a.id === b.id);
const card_show_warnings = useShopStore(state => state.crates[crate_index].items[card_index].show_warnings, compareObjectsEmptiness);
const card_options_data = useShopStore(state => state.crates[crate_index].items[card_index].options_data, compareObjectsEmptiness);
// #!render_count // #!render_count
console.log("SummaryCrateCard renders: ", renderCount) console.log("SummaryCrateCard renders: ", renderCount)
const options = card && card.options; const options = card && card.options && card.options.length > 0;
const options_data = card && card.options_data; const options_data = card_options_data && Object.keys(card_options_data).length > 0;
const warnings = card && card.show_warnings; const warnings = card_show_warnings && card_show_warnings.length > 0;
return ( return (
<tr <tr
@ -54,8 +57,8 @@ export function SummaryCrateCard({crate_index, card_index}) {
<div style={{'width': '45px', 'height': '20px'}} <div style={{'width': '45px', 'height': '20px'}}
className="d-inline-flex align-content-center align-self-center justify-content-evenly"> className="d-inline-flex align-content-center align-self-center justify-content-evenly">
{(warnings && warnings.length > 0 ? ( {(warnings ? (
<WarningIndicator warnings={warnings}/> <WarningIndicator crate_index={crate_index} card_index={card_index}/>
) : ( ) : (
<span style={{ <span style={{
'display': 'inline-block', 'display': 'inline-block',
@ -63,8 +66,7 @@ export function SummaryCrateCard({crate_index, card_index}) {
}}>&nbsp;</span> }}>&nbsp;</span>
))} ))}
{((options && options_data) ? ( {((options && options_data) ? (
<SummaryPopup id={card.id + "options"} options={options} <OptionsSummaryWrapper crate_index={crate_index} card_index={card_index}/>
data={options_data}/>
) : ( ) : (
<span style={{ <span style={{
'display': 'inline-block', 'display': 'inline-block',

View File

@ -17,7 +17,7 @@ export function validateJSON(description) {
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.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)) return false; if (!(card.pn in pn_to_card) || card.options === undefined) return false;
} }
} }
} catch (e) { } catch (e) {
@ -36,7 +36,7 @@ export function JSONToCrates(description) {
items: Array.from(crate.items.map((card, _i) => ({ items: Array.from(crate.items.map((card, _i) => ({
...pn_to_card(card.pn), ...pn_to_card(card.pn),
id: uuidv4(), id: uuidv4(),
options_data: card.options options_data: card.options || {}
}))), }))),
warnings: [], warnings: [],
occupiedHP: 0, occupiedHP: 0,

View File

@ -5,6 +5,7 @@ import {data as shared_data, itemsUnfoldedList} from "./utils";
import {true_type_of} from "./options/utils"; import {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 {FillExtData} from "./options/utils";
import {TriggerCrateWarnings, TriggerWarnings} from "./warnings"; 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";
@ -67,10 +68,17 @@ const useImportJSON = ((set, get) => ({
closeImport: () => set(state => ({ closeImport: () => set(state => ({
importShouldOpen: false importShouldOpen: false
})), })),
loadDescription: () => set(state => ({ _loadDescription: () => set(state => ({
importShouldOpen: false, importShouldOpen: false,
crates: JSONToCrates(state.importValue.value) crates: JSONToCrates(state.importValue.value)
})), })),
loadDescription: () => {
get()._loadDescription()
get().crates.forEach((crate, _i) => {
get().fillExtData(crate.id)
get().fillWarnings(crate.id)
})
},
updateImportDescription: (new_description) => set(state => ({ updateImportDescription: (new_description) => set(state => ({
importValue: { importValue: {
value: new_description, value: new_description,
@ -254,14 +262,12 @@ const useCart = ((set, get) => ({
return { return {
crates: state.crates.map((crate, _i) => { crates: state.crates.map((crate, _i) => {
if (crate_to === crate_from && crate_to === crate.id) { if (crate_to === crate_from && crate_to === crate.id) {
// TODO fix
//const the_card = {...crate[index_from]};
let items_copy = Array.from(crate.items); let items_copy = Array.from(crate.items);
delete items_copy[index_from]; let item = items_copy.splice(index_from, 1)[0]
console.log(crate_from, index_from, crate_to, index_to, items_copy.toSpliced(index_to+1, 0, the_card).filter((item, _) => !!item)) items_copy.splice(index_to, 0, item).filter((item, _) => !!item)
return { return {
...crate, ...crate,
items: items_copy.toSpliced(index_to+1, 0, {...the_card}).filter((item, _) => !!item) items: items_copy
} }
} else if (crate_to === crate.id) { } else if (crate_to === crate.id) {
return { return {
@ -341,6 +347,26 @@ const useCart = ((set, get) => ({
}) })
})), })),
fillExtData: (crate_id) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate_id === crate.id) {
let itemsCopy = Array.from(crate.items);
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);
return item;
});
return {
...crate,
items: Array.from(itemsCopy)
}
}
else return crate;
})
})),
totalOrderPrice: () => { totalOrderPrice: () => {
let sum = 0; let sum = 0;
get().crates.forEach( (crate, _i) => { get().crates.forEach( (crate, _i) => {
@ -357,11 +383,13 @@ const useCart = ((set, get) => ({
newCrate: () => { newCrate: () => {
const crate_id = "crate" + get().crates.length; const crate_id = "crate" + get().crates.length;
get()._newCrate(crate_id) get()._newCrate(crate_id)
get().fillExtData(crate_id);
get().fillWarnings(crate_id); get().fillWarnings(crate_id);
}, },
setCrateMode: (id, mode) => { setCrateMode: (id, mode) => {
get()._setCrateMode(id, mode) get()._setCrateMode(id, mode)
get().fillExtData(crate_id);
get().fillWarnings(id); get().fillWarnings(id);
get().setActiveCrate(id); get().setActiveCrate(id);
}, },
@ -370,6 +398,7 @@ const useCart = ((set, get) => ({
const dest = crate_to || get().active_crate; const dest = crate_to || get().active_crate;
if (!dest) return {}; if (!dest) return {};
get()._addCardFromBacklog(dest, index_from, index_to) get()._addCardFromBacklog(dest, index_from, index_to)
get().fillExtData(dest);
get().fillWarnings(dest); get().fillWarnings(dest);
get().setActiveCrate(dest); get().setActiveCrate(dest);
if (!just_mounted) { if (!just_mounted) {
@ -379,12 +408,17 @@ const useCart = ((set, get) => ({
moveCard: (crate_from, index_from, crate_to, index_to) => { moveCard: (crate_from, index_from, crate_to, index_to) => {
get()._moveCard(crate_from, index_from, crate_to, index_to); get()._moveCard(crate_from, index_from, crate_to, index_to);
get().fillExtData(crate_to);
get().fillWarnings(crate_to); get().fillWarnings(crate_to);
get().setActiveCrate(crate_to); get().setActiveCrate(crate_to);
if (crate_from !== crate_to) get().fillWarnings(crate_from); if (crate_from !== crate_to) {
get().fillExtData(crate_from);
get().fillWarnings(crate_from);
}
}, },
deleteCard: (crate_id, index) => { deleteCard: (crate_id, index) => {
get()._deleteCard(crate_id, index); get()._deleteCard(crate_id, index);
get().fillExtData(crate_id);
get().fillWarnings(crate_id); get().fillWarnings(crate_id);
if (crate_id === get().highlighted.crate && index === get().highlighted.card) get().highlightReset() if (crate_id === get().highlighted.crate && index === get().highlighted.card) get().highlightReset()
}, },
@ -395,6 +429,7 @@ const useCart = ((set, get) => ({
updateOptions: (crate_id, index, new_options) => { updateOptions: (crate_id, index, new_options) => {
get()._updateOptions(crate_id, index, new_options); get()._updateOptions(crate_id, index, new_options);
get().fillExtData(crate_id);
get().fillWarnings(crate_id); get().fillWarnings(crate_id);
} }
})) }))

View File

@ -63,3 +63,32 @@ export const range = (start, end) => {
const length = end - start; const length = end - start;
return Array.from({ length }, (_, i) => start + i); return Array.from({ length }, (_, i) => start + i);
} }
export const move = (source, destination, droppableSource, droppableDestination) => {
console.log('==> move', source, destination);
const sourceClone = Array.from(source);
const destClone = Array.from(destination);
const [removed] = sourceClone.splice(droppableSource.index, 1);
destClone.splice(droppableDestination.index, 0, removed);
const result = {columns: {}};
result.columns[droppableSource.droppableId] = sourceClone;
result.columns[droppableDestination.droppableId] = destClone;
return result;
};
export const compareArraysWithIds = (a, b) =>
a.length === b.length &&
a.every((element, index) => element.id === b[index].id);
export const compareArraysLevelOne = (a, b) =>
a.length === b.length &&
a.every((element, index) => element === b[index]);
export function compareObjectsEmptiness(a, b) {
return (!a && !b) || (!(!a !== !b) && Object.getPrototypeOf(a) === Object.getPrototypeOf(b) &&
(Object.getPrototypeOf(a) !== Object.getPrototypeOf([]) || !!Object.keys(a).length === !!Object.keys(b).length))
}

View File

@ -20,7 +20,7 @@ module.exports = {
options: { options: {
debug: false, debug: false,
directives: { directives: {
render_count: true, render_count: false,
}, },
params: { params: {
ENV: process.env.NODE_ENV, ENV: process.env.NODE_ENV,