Minor fixes

Signed-off-by: Egor Savkin <es@m-labs.hk>
pull/113/head
Egor Savkin 2023-12-13 15:17:14 +08:00
parent ff7eac97dc
commit 3b1d9fcb56
18 changed files with 67 additions and 407 deletions

View File

@ -1,5 +1,4 @@
import React from 'react';
import {v4 as uuidv4} from "uuid";
import {Droppable} from "@hello-pangea/dnd";
import {ProductItem} from "./ProductItem";
import {useShopStore} from "./shop_store";
@ -42,7 +41,7 @@ export function Backlog() {
{group.items.map(item => {
item_index++;
return (
<ProductItem card_index={item_index}/>
<ProductItem card_index={item_index} key={item.id} />
)
})}
</div>

View File

@ -4,7 +4,7 @@ import {cartStyle} from "./utils";
import {ProductCartItem} from "./ProductCartItem";
import {FakePlaceholder} from "./FakePlaceholder";
import {FillExtData} from "./options/utils";
import {hp_to_slots, resource_counters} from "./count_resources";
import {hp_to_slots} from "./count_resources";
import {useShopStore} from "./shop_store";
/**

View File

@ -3,7 +3,6 @@ import {Cart} from "./Cart";
import {CrateMode} from "./CrateMode";
import {CrateWarnings} from "./CrateWarnings";
import {useShopStore} from "./shop_store";
import {TriggerCrateWarnings} from "./warnings";
/**
* Component that displays the main crate with reminder rules.

View File

@ -26,11 +26,9 @@ export function CrateList() {
</Accordion.Item>
)}
<Accordion.Item eventKey="last">
<Accordion.Header>
<Accordion.Header onClick={onAddCrate}>
Add new crate
<button style={{width: "32px"}} onClick={onAddCrate}>
<img src="/images/shop/icon-add.svg" alt="add" />
</button>
<img src="/images/shop/icon-add.svg" alt="add" width="32px"/>
</Accordion.Header>
</Accordion.Item>
</Accordion>)

View File

@ -44,7 +44,6 @@ export function ImportJSON() {
<div className="form-group w-100">
<textarea
onChange={(event) => {
console.log(event)
updateImportDescription(event.target.value)
}}
value={data.value}

View File

@ -5,265 +5,14 @@ import {useShopStore} from "./shop_store";
* Component that provides a base layout (aside/main) for the page.
*/
/*
export function Layout({aside, main}) {
static get defaultProps() {
return {
mobileSideMenuShouldOpen: false,
};
}
constructor(props) {
super(props);
this.state = {
customconf: '',
error: null,
};
this.handleCustomConfig = this.handleCustomConfig.bind(this);
this.handleClickLoad = this.handleClickLoad.bind(this);
this.checkValidation = this.checkValidation.bind(this);
// retrieve list of available pn
const items_keys = Object.keys(props.items);
this.list_pn = items_keys.map(function (key) {
return props.items[key].name_number;
});
}
handleCustomConfig(e) {
const value = e.target.value;
this.checkValidation(value);
}
checkValidation(conf) {
let conf_obj;
try {
conf_obj = JSON.parse(conf);
} catch (e) {
return this.setState({
...this.state,
customconf: conf,
customconf_ready: null,
error: 'invalid format',
});
}
if (!conf_obj) {
return this.setState({
...this.state,
customconf: conf,
customconf_ready: null,
error: 'invalid format',
});
}
if ((!conf_obj.items || !conf_obj.type) &&
(Object.prototype.toString.call(conf_obj.items) !== '[object Array]' ||
Object.prototype.toString.call(conf_obj.type) !== '[object String]')) {
return this.setState({
...this.state,
customconf: conf,
customconf_ready: null,
error: 'invalid format',
});
}
if (conf_obj.type !== "desktop" && conf_obj.type !== "rack") {
return this.setState({
...this.state,
customconf: conf,
customconf_ready: null,
error: 'invalid format',
});
}
conf_obj.items.map(function (item) {
try {
return JSON.parse(item);
} catch (e) {
return null;
}
});
conf_obj.items = conf_obj.items.filter(function (item) {
return item;
});
if (conf_obj.items.filter(function (item) {
return Object.prototype.toString.call(item) !== '[object Object]' || !item.pn || Object.prototype.toString.call(item.pn) !== '[object String]';
}).length > 0) {
return this.setState({
...this.state,
customconf: conf,
customconf_ready: null,
error: 'invalid format',
});
}
conf_obj.items = conf_obj.items.map(function (item) {
return {
pn: item.pn,
options: item.options ? item.options : null,
};
});
const self = this;
const unknow_pn = conf_obj.items.filter(function (item_pn) {
return self.list_pn.includes(item_pn.pn) === false;
}).map(function (item_pn) {
return item_pn.pn;
});
if (unknow_pn.length > 0) {
return this.setState({
...this.state,
customconf: conf,
customconf_ready: null,
error: `${unknow_pn.join(', ')} unknown${unknow_pn.length > 1 ? 's':''} pn number`,
});
}
this.setState({
...this.state,
customconf: conf,
error: null,
customconf_ready: conf_obj,
});
}
handleClickLoad() {
this.checkValidation(this.state.customconf);
if (this.props.onClickLoadCustomConf) {
this.props.onClickLoadCustomConf(this.state.customconf_ready);
}
}
render() {
const {
aside,
main,
mobileSideMenuShouldOpen,
isMobile,
newCardJustAdded,
onClickToggleMobileSideMenu,
onClickCloseRFQFeedback,
showRFQFeedback,
RFQBodyType,
RFQBodyOrder,
} = this.props;
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 className={`modal fade ${ showRFQFeedback ? 'show': ''}`} style={{'display': showRFQFeedback ? 'block':'none'}} id="exampleModal" tabIndex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div className="modal-dialog" role="document">
<div className="modal-content">
<div className="modal-body rfqFeedback">
<div className="d-flex w-100">
{RFQBodyType === 'email' ? (
<div className="d-flex">
<div>
<img width="30px" src="/images/shop/icon-done.svg" alt="close" />
</div>
<div style={{'padding': '0 .5em'}}>
We've received your request and will be in contact soon.
</div>
</div>
) : null }
{RFQBodyType === 'show' ? (
<p>
{RFQBodyOrder}
</p>
) : null}
{RFQBodyType === 'import' ? (
<div className="w-100">
<form className="w-100">
<div className="mb-3">
<p className="small">
Input the JSON description below. Should be something like:
<br />
{JSON.stringify({"items":[{"pn":"1124"},{"pn":"2118"},{"pn":"2118"},{"pn":"2128"}],"type":"desktop"})}
</p>
</div>
<div className="mb-3 w-100">
<textarea
onChange={this.handleCustomConfig}
value={this.state.customconf}
className="form-control w-100"
rows="5"
placeholder="Input JSON description here." />
</div>
{this.state.error ? (
<div className="mb-3">
<p className="text-danger">{this.state.error}</p>
</div>
) : null}
</form>
<div className="d-flex flex-column flex-sm-row justify-content-end">
<a type="button" onClick={onClickCloseRFQFeedback} className="btn btn-sm btn-outline-primary m-0 mb-2 mb-sm-0 me-sm-2">Close</a>
<a type="button" onClick={this.handleClickLoad} className={`btn btn-sm btn-primary m-0 ms-sm-2 ${this.state.error ? 'disabled':''}`}>Load configuration</a>
</div>
</div>
) : null}
<div>
<button onClick={onClickCloseRFQFeedback}>
<img src="/images/shop/icon-close.svg" alt="close" />
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div onClick={onClickCloseRFQFeedback} className={`modal-backdrop fade ${ showRFQFeedback ? 'show': ''}`} style={{'display': showRFQFeedback ? 'initial':'none'}}></div>
</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);
const {mobileSideMenuShouldOpen, onClickToggleMobileSideMenu, showCardAddedFeedback} = useShopStore((state) => ({
mobileSideMenuShouldOpen: state.sideMenuIsOpen,
onClickToggleMobileSideMenu: state.switchSideMenu,
showCardAddedFeedback: state.showCardAddedFeedback,
isMobile: state.isMobile,
}));
return (
<div className="layout">
@ -276,7 +25,7 @@ export function Layout({aside, main}) {
<section className="main">{main}</section>
)}
{isMobile && newCardJustAdded ? (
{showCardAddedFeedback ? (
<div className="feedback-add-success">
added
</div>

View File

@ -1,5 +1,5 @@
import React from 'react'
import {validateEmail, Validation} from "./validate.js";
import {Validation} from "./validate.js";
import {useShopStore} from "./shop_store";
import {ShowJSON} from "./ShowJSON";
@ -13,6 +13,7 @@ export function OrderForm() {
isProcessing,
updateEmail,
updateNote,
resetEmailValidation,
submitForm,
submitDisabled,
} = useShopStore(state => ({
@ -22,7 +23,8 @@ export function OrderForm() {
updateEmail: state.updateEmail,
updateNote: state.updateNote,
submitForm: state.submitForm,
submitDisabled: state.submitDisabled
submitDisabled: state.submitDisabled,
resetEmailValidation: state.resetEmailValidation
}));
@ -35,6 +37,7 @@ export function OrderForm() {
className={`${email.error > 0 ? 'errorField' : ''}`}
type="email"
placeholder="Email"
onFocus={resetEmailValidation}
onChange={(event) => updateEmail(event.target.value)}
onBlur={(event) => updateEmail(event.target.value)}
value={email.value}/>

View File

@ -13,7 +13,6 @@ import {RFQFeedback} from "./RFQFeedback";
export function OrderPanel({title, description}) {
const isMobile = useShopStore((state) => state.isMobile);
const onClickToggleMobileSideMenu = useShopStore((state) => state.switchSideMenu);
const onClickOpenImport = useShopStore((state) => state.openImport);
return (<section className="panel">

View File

@ -61,7 +61,7 @@ export function OrderSummary() {
<div>
{`${currency} ${formatMoney(crate_type.price)}`}
<button style={{'opacity': '0', 'cursor': 'initial'}}>
<button onClick={() => clearCrate(crate.id)}>
<img src="/images/shop/icon-remove.svg"/>
</button>
</div>

View File

@ -19,6 +19,10 @@ export function Shop() {
cardIndexById: state.cardIndexById
}));
const handleOnDragEnd = (drop_result, _provided) => {
if (!drop_result.destination) {
console.warn("No drop destination");
return;
}
if (drop_result.source.droppableId === "backlog")
addCardFromBacklog(drop_result.destination.droppableId, drop_result.source.index, drop_result.destination.index);
else if(drop_result.destination.droppableId === "backlog")
@ -28,7 +32,7 @@ export function Shop() {
}
useEffect(() => {
addCardFromBacklog(null, [cardIndexById("eem_pwr_mod"), cardIndexById("kasli")], -1);
addCardFromBacklog(null, [cardIndexById("eem_pwr_mod"), cardIndexById("kasli")], -1, true);
}, []);
return (

View File

@ -1,5 +1,3 @@
import {data as shared_data} from "./utils";
const count_item_occupied_eem = (item) => {
if (!item.options_data
|| item.options_data.ext_pwr === false
@ -75,19 +73,4 @@ export function FillResources(data) {
export function hp_to_slots(hp) {
return Math.trunc(hp / 4);
}
export function crate_type_to_hp(crate_t) {
return shared_data.crateModes[crate_t].hp;
}
export function total_order_price(crates) {
let sum = 0;
Object.entries(crates).forEach( ([key, crate], _i) => {
sum += shared_data.crateModes[crate.crate_type].price;
crate.items.forEach((item, _) => {
sum += item.price;
});
});
return sum;
}

View File

@ -33,7 +33,7 @@ export function JSONToCrates(description) {
return Array.from(crates_raw.map((crate, c_i) => ({
id: "crate" + c_i,
crate_mode: crate.type,
items: Array.from(crate.items.map((card, i) => ({
items: Array.from(crate.items.map((card, _i) => ({
...pn_to_card(card.pn),
id: uuidv4(),
options_data: card.options

View File

@ -11,7 +11,7 @@ export function DialogPopup({options, data, target, id, big, first, last, option
);
let div_classes = `overlayVariant border rounded ${big ? "overlay-bigcard" : "overlay-smallcard"} ${(!big && first) ? "overlay-first" : ""} ${(!big && last) ? "overlay-last" : ""} ${options_class || ""}`;
const handleClick = (event) => {
const handleClick = (_event) => {
setShow(!show);
};

View File

@ -53,7 +53,7 @@ export function SummaryPopup({id, options, data}) {
}
}, [show, size])
const handleClick = (event) => {
const handleClick = (_event) => {
setShow(!show);
if (!show) {
document.addEventListener("scroll", handleScroll, true);

View File

@ -37,11 +37,22 @@ const useLayout = ((set, get) => ({
isTouch: window.isTouchEnabled(),
isMobile: window.deviceIsMobile(),
sideMenuIsOpen: false,
newCardJustAdded: false,
showCardAddedFeedback: false,
timerAdded: null,
switchSideMenu: () => set(state => ({
sideMenuIsOpen: !state.sideMenuIsOpen
})),
cardAdded: () => set(state => ({
showCardAddedFeedback: true,
timerAdded: (!!state.timerAdded ? clearTimeout(state.timerAdded) : null) || (state.isMobile && setTimeout(() => {
get()._endCardAdded()
}, 2000))
})),
_endCardAdded: () => set(state => ({
showCardAddedFeedback: false,
timerAdded: !!state.timerAdded ? clearTimeout(state.timerAdded) : null
}))
}));
const useImportJSON = ((set, get) => ({
@ -70,7 +81,6 @@ const useImportJSON = ((set, get) => ({
}));
const useSubmitForm = ((set, get) => ({
// TODO think about it
isProcessing: false,
shouldShowRFQFeedback: false,
processingResult: {
@ -102,6 +112,12 @@ const useSubmitForm = ((set, get) => ({
error: validateNote(new_notes)
}
})),
resetEmailValidation: () => set(state => ({
email: {
value: state.email.value,
error: Validation.OK
}
})),
_revalidateForm: () => set(state => ({
email: {
@ -166,17 +182,22 @@ const useHighlighted = ((set, get) => ({
crate: "",
card: 0
},
highlightedTimer: null,
highlightCard: (crate_id, index) => set(state => ({
highlighted: {
crate: crate_id,
card: index
}
},
highlightedTimer: (!!state.highlightedTimer ? clearTimeout(state.highlightedTimer) : null) || (state.isTouch && setTimeout(() => {
get().highlightReset()
}, 2000))
})),
highlightReset: () => set(state => ({
highlighted: {
crate: "",
card: 0
}
},
highlightedTimer: !!state.highlightedTimer ? clearTimeout(state.highlightedTimer) : null
})),
}));
@ -185,15 +206,19 @@ const useCart = ((set, get) => ({
crates: shared_data.columns.crates,
active_crate: "crate0",
_newCrate: (crate_id) => set((state) => ({crates: state.crates.concat({
_newCrate: (crate_id) => set((state) => ({
crates: state.crates.concat({
id: crate_id || "crate" + state.crates.length,
crate_mode: "rack",
items: [],
warnings: [],
occupiedHP: 0
})})),
}),
active_crate: crate_id || "crate" + state.crates.length
})),
delCrate: (id) => set(state => ({
crates: state.crates.filter((crate => crate.id !== id))
crates: state.crates.filter((crate => crate.id !== id)),
active_crate: state.active_crate === id ? null : state.active_crate,
})),
_setCrateMode: (id, mode) => set(state => ({
crates: state.crates.map((crate, _i) => {
@ -209,6 +234,7 @@ const useCart = ((set, get) => ({
_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;
if (!dest) return {};
return {
crates: state.crates.map((crate, _i) => {
if (dest === crate.id) {
@ -224,7 +250,6 @@ const useCart = ((set, get) => ({
}
}),
_moveCard: (crate_from, index_from, crate_to, index_to) => set(state => {
console.log(crate_from, index_from, crate_to, index_to)
const the_card = state.crates.find((crate, _) => crate_from === crate.id ).items[index_from];
return {
crates: state.crates.map((crate, _i) => {
@ -293,6 +318,7 @@ const useCart = ((set, get) => ({
fillWarnings: (crate_id) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate_id === crate.id) {
//console.log("--- CHECK ALERTS ---")
let itemsCopy = Array.from(crate.items);
itemsCopy = FillResources(itemsCopy);
itemsCopy = TriggerWarnings(itemsCopy);
@ -332,10 +358,14 @@ const useCart = ((set, get) => ({
get().fillWarnings(id);
},
addCardFromBacklog: (crate_to, index_from, index_to) => {
addCardFromBacklog: (crate_to, index_from, index_to, just_mounted) => {
const dest = crate_to || get().active_crate;
if (!dest) return {};
get()._addCardFromBacklog(dest, index_from, index_to)
get().fillWarnings(dest);
if (!just_mounted) {
get().cardAdded()
}
},
moveCard: (crate_from, index_from, crate_to, index_to) => {
@ -346,6 +376,7 @@ const useCart = ((set, get) => ({
deleteCard: (crate_id, index) => {
get()._deleteCard(crate_id, index);
get().fillWarnings(crate_id);
if (crate_id === get().highlighted.crate && index === get().highlighted.card) get().highlightReset()
},
clearCrate: (id) => {
get()._clearCrate(id);

View File

@ -1,73 +1,8 @@
'use strict';
import {v4 as uuidv4} from "uuid";
export const data = window.shop_data;
export const itemsUnfoldedList = Array.from(data.columns.backlog.categories.map(groupId => groupId.itemIds).flat());
/*export const copy = (
model,
source,
destination,
draggableSource,
droppableDestination
) => {
const destClone = Array.from(destination.items);
destClone.splice(droppableDestination.index, 0, ...draggableSource.map((dragged_item, _) => {
return {
...model[dragged_item],
id: uuidv4(),
}
}));
return destClone;
};
*/
export const reorder = (list, startIndex, endIndex) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
export const remove = (list, startIndex) => {
const result = Array.from(list);
result.splice(startIndex, 1);
return result;
};
export const copyFromBacklog = (source, destination, droppableSource, droppableDestination) => {
console.log('==> dest', destination);
const destClone = Array.from(destination.items);
const items = droppableSource.indexes
? droppableSource.indexes .map((item, _) => itemsUnfoldedList[item])
: [itemsUnfoldedList[droppableSource.index]];
destClone.splice(droppableDestination.index, 0, ...items.map((item, _) => {
return {...source[item], id: uuidv4()}
}));
return destClone;
};
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 productStyle = (style, snapshot, removeAnim, hovered, selected, cart=false) => {
const custom = {
opacity: snapshot.isDragging ? .7 : 1,
@ -122,4 +57,4 @@ export function formatMoney(amount, decimalCount = 2, decimal = ".", thousands =
} catch (e) {
return amount;
}
};
}

View File

@ -5,8 +5,7 @@
* Second - resources indicator should be separate component
*/
import {crate_type_to_hp, item_occupied_counters, resource_counters} from "./count_resources";
import {data as shared_data} from "./utils";
import {item_occupied_counters, resource_counters} from "./count_resources";
import {useShopStore} from "./shop_store";
const Levels = {

View File

@ -1,43 +1,8 @@
const shop_data = {
API_RFQ: 'https://hooks.m-labs.hk/rfq',
mobileSideMenuShouldOpen: false,
currentItemHovered: null,
currentMode: 'rack',
currency: 'USD',
crateModeSlots: {
rack: 21,
desktop: 10,
},
crateRules: {
maxSlot: {
type: 'crate',
icon: '/shop/icon-warning.svg',
color: '#c75e5e',
name: 'Crate',
message: 'You have reached the maximum number of slots allowed for this crate. Consider removing cards.',
},
compactSlot: {
type: 'crate',
icon: '/shop/icon-reminder.svg',
name: 'Crate',
message: 'The selected cards fit in a 42hp desktop crate, consider switching to it for a more compact system',
},
},
crateModeItems: [{
id: 'rack',
name: 'Rack mountable crate',
price: 550,
}, {
id: 'desktop',
name: 'Desktop crate',
price: 500,
}],
crateModes: {
rack: {
id: 'rack',
@ -1194,9 +1159,6 @@ const shop_data = {
}]
},
rules: {},
};