Make cards behavior working somewhat properly

pull/113/head
火焚 富良 2023-12-12 16:09:29 +08:00 committed by Egor Savkin
parent ec2c0a3b80
commit 6b92bf9145
13 changed files with 356 additions and 418 deletions

View File

@ -9,17 +9,13 @@ import {useShopStore} from "./shop_store";
*/
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
}));
@ -46,20 +42,7 @@ export function Backlog() {
{group.items.map(item => {
item_index++;
return (
<ProductItem
key={item.id}
id={uuidv4()}
index={item_index}
name={`${item.name_number} ${item.name}`}
name_codename={item.name_codename}
price={item.price}
currency={currency}
image={`/images/${item.image}`}
specs={item.specs}
datasheet_file={item.datasheet_file}
datasheet_name={item.datasheet_name}
onClickAddItem={onClickAddItem}
></ProductItem>
<ProductItem card_index={item_index}/>
)
})}
</div>

View File

@ -4,29 +4,23 @@ import {cartStyle} from "./utils";
import {ProductCartItem} from "./ProductCartItem.jsx";
import {FakePlaceholder} from "./FakePlaceholder.jsx";
import {FillExtData} from "./options/utils";
import {CountResources, crate_type_to_hp, hp_to_slots, resource_counters} from "./count_resources";
import {hp_to_slots, resource_counters} from "./count_resources";
import {useShopStore} from "./shop_store";
import {TriggerCardWarnings} from "./warnings";
/**
* Component that displays a list of <ProductCartItem>
*/
export function Cart({crate_index}) {
// isMobile, isTouch, crate, onToggleOverlayRemove, onClickRemoveItem, onCardUpdate, onClickItem
const {crate} = useShopStore(state => ({
crate: state.crates[crate_index]
const {crate, crateParams} = useShopStore(state => ({
crate: state.crates[crate_index],
crateParams: state.crateParams
}));
console.log(resource_counters, crate)
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 nbrOccupied = hp_to_slots(crate.occupiedHP);
const nbrSlots = hp_to_slots(crateParams(crate.crate_mode).hp);
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 (
<ProductCartItem
card_index={index}
@ -34,8 +28,6 @@ export function Cart({crate_index}) {
ext_data={ext_data}
first={index === 0}
last={index === crate.items.length - 1 && nbrOccupied >= nbrSlots}
resources={resources}
warnings={warnings}
key={item.id}/>
);
});

View File

@ -34,9 +34,7 @@ export function Crate({crate_index}) {
<Cart crate_index={crate_index}/>
{1 || (rules && rules.length > 0) && (
<CrateWarnings crate_index={crate_index} />
)}
<CrateWarnings crate_index={crate_index} />
</div>
</div>
);

View File

@ -8,7 +8,7 @@ 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,
crate: state.crates[crate_index],
setMode: state.setCrateMode
}))
return (

View File

@ -1,16 +1,16 @@
import React from "react";
import {TriggerCrateWarnings} from "./warnings";
import {LevelUI} from "./warnings";
import {useShopStore} from "./shop_store";
export function CrateWarnings({crate_index}) {
const crate = useShopStore(state => (state.crates[crate_index]))
const crate_warnings = TriggerCrateWarnings(crate);
const crate_warnings = crate.warnings;
// TODO UI/colors
return (
<div className="crate-info">
{crate_warnings.map((rule, index) => (
<p key={index} className="rule" style={{'color': "red"}}>
<img src={`/images${rule.icon}`} /> <i><strong>{rule.name}:</strong> {rule.message}</i>
<p key={index} className="rule" style={{'color': LevelUI(rule.level).color}}>
<img src={LevelUI(rule.level).icon} /> <i>{rule.message}</i>
</p>
))}
</div>

View File

@ -40,7 +40,7 @@ export function OrderPanel({title, description}) {
<CrateList/>
<section className="summary">
<OrderSummary/>
<OrderForm/>
</section>

View File

@ -1,190 +1,149 @@
import React, {PureComponent} from 'react';
import PropTypes from "prop-types";
import React from 'react';
import {SummaryPopup} from "./options/SummaryPopup.jsx";
import {formatMoney} from "./utils";
import {WarningIndicator} from "./CardWarnings.jsx";
import {total_order_price} from "./count_resources";
import {data as shared_data} from "./utils";
import {useShopStore} from "./shop_store";
/**
* Components that displays the list of card that are used in the crate.
* It is a summary of purchase
*/
export class OrderSummary extends PureComponent {
export function OrderSummary() {
static get propTypes() {
return {
currency: PropTypes.string,
crates: PropTypes.object,
onDeleteItem: PropTypes.func,
onDeleteAllItems: PropTypes.func,
onMouseEnterItem: PropTypes.func,
onMouseLeaveItem: PropTypes.func,
onClickSelectItem: PropTypes.func,
};
}
const {
currency,
crates,
total_price,
crateParams,
deleteCard,
setHighlight,
resetHighlight,
highlighted,
clearCrate,
clearAll
} = useShopStore(state =>({
currency: state.currency,
crates: state.crates,
total_price: state.totalOrderPrice(),
crateParams: state.crateParams,
deleteCard: state.deleteCard,
setHighlight: state.highlightCard,
resetHighlight: state.highlightReset,
highlighted: state.highlighted,
clearAll: state.clearAll,
clearCrate: state.clearCrate
}));
constructor(props) {
super(props);
this.handleOnDeleteItem = this.handleOnDeleteItem.bind(this);
this.handleOnDeleteAllItems = this.handleOnDeleteAllItems.bind(this);
this.handleOnMouseEnterItem = this.handleOnMouseEnterItem.bind(this);
this.handleOnMouseLeaveItem = this.handleOnMouseLeaveItem.bind(this);
this.handleOnClickSelectItem = this.handleOnClickSelectItem.bind(this);
}
return (
<div className="summary-price">
handleOnDeleteItem(index, e) {
if (this.props.onDeleteItem) {
this.props.onDeleteItem(index);
}
e.preventDefault();
}
<table>
handleOnDeleteAllItems(e) {
if (this.props.onDeleteAllItems) {
this.props.onDeleteAllItems();
}
e.preventDefault();
}
<thead>
<tr>
<td colSpan="2" className="summary-remove-all">
<span className="item-card-name">Remove all cards</span>
handleOnMouseEnterItem(id, e) {
if (this.props.onMouseEnterItem) {
this.props.onMouseEnterItem(id);
}
e.preventDefault();
}
<button onClick={clearAll}>
<img src="/images/shop/icon-remove.svg"/>
</button>
</td>
</tr>
</thead>
handleOnMouseLeaveItem(e) {
if (this.props.onMouseLeaveItem) {
this.props.onMouseLeaveItem();
}
e.preventDefault();
}
{crates.map((crate, _i) => {
let crate_type = crateParams(crate.crate_mode);
return (
<tbody key={"summary_crate_body" + crate.id}>
<tr key={"summary_crate_" + crate.id}>
<td className="item-card-name">{crate_type.name}</td>
<td className="price">
<div>
{`${currency} ${formatMoney(crate_type.price)}`}
handleOnClickSelectItem(index, e) {
if (e.target.tagName !== 'IMG') {
if (this.props.onClickSelectItem) {
this.props.onClickSelectItem(index);
}
}
return e.preventDefault();
}
<button style={{'opacity': '0', 'cursor': 'initial'}}>
<img src="/images/shop/icon-remove.svg"/>
</button>
</div>
render() {
const {
currency,
crates
} = this.props;
<span style={{
'display': 'inline-block',
'width': '30px',
}}>&nbsp;</span>
</td>
</tr>
{crate.items.map((item, index) => {
const options = item && item.options;
const options_data = item && item.options_data;
const warnings = item && item.show_warnings;
const selected = crate.id === highlighted.crate && index === highlighted.card;
const total_price = total_order_price(crates);
return (
<div className="summary-price">
<table>
<thead>
<tr>
<td colSpan="2" className="summary-remove-all">
<span className="item-card-name">Remove all cards</span>
<button onClick={this.handleOnDeleteAllItems}>
<img src="/images/shop/icon-remove.svg" />
</button>
</td>
</tr>
</thead>
{Object.entries(crates).map(([crate_id, crate], _i) => {
let crate_type = shared_data.crateModes[crate.crate_type];
return (
<tbody key={"summary_crate_body"+crate_id}>
<tr key={"summary_crate_"+crate_id}>
<td className="item-card-name">{crate_type.name}</td>
<td className="price">
<div>
{`${currency} ${formatMoney(crate_type.price)}`}
<button style={{'opacity': '0', 'cursor': 'initial'}}>
<img src="/images/shop/icon-remove.svg" />
</button>
</div>
<span style={{
'display': 'inline-block',
'width': '30px',
}}>&nbsp;</span>
return (<tr key={"summary_crate_" + crate.id + item.id}
className={`hoverable ${selected ? 'selected' : ''}`}
onClick={() => setHighlight(crate.id, index)}
onMouseEnter={() => setHighlight(crate.id, index)}
onMouseLeave={() => resetHighlight()}>
<td className="item-card-name">
<div>{`${item.name_number} ${item.name} ${item.name_codename}`}</div>
</td>
</tr>
{crate.items.map((item, index) => {
let options = item && item.options;
let options_data = item && item.options_data;
const warnings = item && item.show_warnings;
return (<tr key={"summary_crate_" + crate_id+item.id}
className={`hoverable ${item.selected ? 'selected' : ''}`}
onClick={this.handleOnClickSelectItem.bind(this, index)}
onMouseEnter={this.handleOnMouseEnterItem.bind(this, item.id)}
onMouseLeave={this.handleOnMouseLeaveItem}>
<td className="item-card-name">
<div>{`${item.name_number} ${item.name} ${item.name_codename}`}</div>
</td>
<td className="price">
<div className="d-inline-flex align-content-center">
{`${currency} ${formatMoney(item.price)}`}
<td className="price">
<div className="d-inline-flex align-content-center">
{`${currency} ${formatMoney(item.price)}`}
<button onClick={() => deleteCard(crate.id, index)}>
<img src="/images/shop/icon-remove.svg"/>
</button>
<button onClick={this.handleOnDeleteItem.bind(this, index)}>
<img src="/images/shop/icon-remove.svg" />
</button>
<div style={{'width': '45px', 'height': '20px'}}
className="d-inline-flex align-content-center align-self-center justify-content-evenly">
{(warnings && warnings.length > 0 ? (
<WarningIndicator warnings={warnings}/>
) : (
<span style={{
'display': 'inline-block',
'width': '20px',
}}>&nbsp;</span>
))}
{((options && options_data) ? (
<SummaryPopup id={item.id + "options"} options={options}
data={options_data}/>
) : (
<span style={{
'display': 'inline-block',
'width': '20px',
}}>&nbsp;</span>
))}
</div>
</div>
</td>
</tr>);
})}
</tbody>
)
})}
<div style={{'width': '45px', 'height': '20px'}} className="d-inline-flex align-content-center align-self-center justify-content-evenly">
{(warnings && warnings.length > 0 ? (
<WarningIndicator warnings={warnings}/>
) : (
<span style={{
'display': 'inline-block',
'width': '20px',
}}>&nbsp;</span>
))}
{((options && options_data) ? (
<SummaryPopup id={item.id + "options"} options={options} data={options_data} />
) : (
<span style={{
'display': 'inline-block',
'width': '20px',
}}>&nbsp;</span>
))}
</div>
</div>
</td>
</tr>);
})}
</tbody>
)})}
<tfoot>
<tr>
<td className="item-card-name">Price estimate</td>
<td className="price">
<div>
{currency} {formatMoney(total_price)}
<button style={{'opacity': '0', 'cursor': 'initial'}}>
<img src="/images/shop/icon-remove.svg" alt="icon remove"/>
</button>
</div>
<tfoot>
<tr>
<td className="item-card-name">Price estimate</td>
<td className="price">
<div>
${currency} ${formatMoney(total_price)}
<button style={{'opacity': '0', 'cursor': 'initial'}}>
<img src="/images/shop/icon-remove.svg" alt="icon remove"/>
</button>
</div>
<span style={{
'display': 'inline-block',
'width': '30px',
}}>&nbsp;</span>
</td>
</tr>
</tfoot>
<span style={{
'display': 'inline-block',
'width': '30px',
}}>&nbsp;</span>
</td>
</tr>
</tfoot>
</table>
</table>
</div>
);
}
</div>
);
}

View File

@ -10,7 +10,7 @@ import {useShopStore} from "./shop_store";
* Component that renders a product.
* Used in the crate
*/
export function ProductCartItem({card_index, crate_index, ext_data, first, last, resources, warnings}) {
export function ProductCartItem({card_index, crate_index, ext_data, first, last}) {
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,
@ -22,7 +22,8 @@ export function ProductCartItem({card_index, crate_index, ext_data, first, last,
}))
let options, options_data;
//const warnings = data && data.show_warnings;
const warnings = card && card.show_warnings;
const resources = card && card.counted_resources;
if (card && card.options) {
options = card.options;
@ -94,7 +95,7 @@ export function ProductCartItem({card_index, crate_index, ext_data, first, last,
<img
className='item-cart'
src={`/images${card.image}`}/>
src={card.image}/>
</div>
{/* remove container */}

View File

@ -1,120 +1,84 @@
import React, {PureComponent} from 'react';
import PropTypes from "prop-types";
import React from 'react';
import {Draggable} from "@hello-pangea/dnd";
import {formatMoney, productStyle} from "./utils";
import {useShopStore} from "./shop_store";
/**
* Component that renders a product.
* Used in the aside (e.g backlog of product)
*/
export class ProductItem extends PureComponent {
export function ProductItem({card_index}) {
const {card, currency, onAddCard} = useShopStore(state => ({
card: state.getCardDescription(card_index),
currency: state.currency,
onAddCard: state.addCardFromBacklog
}));
static get propTypes() {
return {
id: PropTypes.string.isRequired,
index: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
name_codename: PropTypes.string,
price: PropTypes.number.isRequired,
currency: PropTypes.string.isRequired,
image: PropTypes.string.isRequired,
specs: PropTypes.array,
datasheet_file: PropTypes.string,
datasheet_name: PropTypes.string,
onClickAddItem: PropTypes.func,
};
}
constructor(props) {
super(props);
this.handleOnClickAddItem = this.handleOnClickAddItem.bind(this);
}
const render_specs = (card.specs && card.specs.length > 0 && (
<ul>
{card.specs.map((spec, index) =>
<li key={index}>{spec}</li>
)}
</ul>
));
handleOnClickAddItem(id, tap, e) {
if (this.props.onClickAddItem) {
this.props.onClickAddItem(id, tap);
}
e.preventDefault();
}
const render_datasheet_link = (card.datasheet_file && card.datasheet_name && (
<div className="ds">
<span className='doc-icon'></span>
<a href={card.datasheet_file} target="_blank" rel="noopener noreferrer">
{card.datasheet_name}
</a>
</div>
));
render() {
const {
id,
index,
name,
name_codename,
price,
currency,
image,
specs,
datasheet_file,
datasheet_name,
} = this.props;
return (
<section className="productItem">
const render_specs = (specs && specs.length > 0 && (
<ul>
{specs.map((spec, index) =>
<li key={index}>{spec}</li>
)}
</ul>
));
<div className="content">
<h3 style={{'marginBottom': card.name_codename ? '5px' : '20px'}}>{card.name}</h3>
{card.name_codename ? (
<p>{card.name_codename}</p>
) : null}
const render_datasheet_link = (datasheet_file && datasheet_name && (
<div className="ds">
<span className='doc-icon'></span>
<a href={datasheet_file} target="_blank" rel="noopener noreferrer">
{datasheet_name}
</a>
<div className="price">{`${currency} ${formatMoney(card.price)}`}</div>
{render_specs}
{render_datasheet_link}
</div>
));
return (
<section className="productItem">
<div className="content">
<div className="content">
<h3 style={{ 'marginBottom': name_codename ? '5px' : '20px'}}>{name}</h3>
{name_codename ? (
<p>{name_codename}</p>
) : null }
<button onClick={() => onAddCard(null, card_index, null)}>
<img src="/images/shop/icon-add.svg" alt="add"/>
</button>
<div className="price">{`${currency} ${formatMoney(price)}`}</div>
{render_specs}
{render_datasheet_link}
</div>
<div className="content">
<button onClick={this.handleOnClickAddItem.bind(this, index, true)}>
<img src="/images/shop/icon-add.svg" alt="add" />
</button>
<Draggable draggableId={id} index={index}>
{(provided, snapshot) => (
<React.Fragment>
<img
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={productStyle(
provided.draggableProps.style,
snapshot,
true, // hack: remove weird animation after a drop
)}
src={image} />
{/* Allows to simulate a clone */}
{snapshot.isDragging && (
<img className="simclone" src={image} />
<Draggable draggableId={card.id} index={card_index}>
{(provided, snapshot) => (
<React.Fragment>
<img
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={productStyle(
provided.draggableProps.style,
snapshot,
true, // hack: remove weird animation after a drop
)}
</React.Fragment>
)}
</Draggable>
src={card.image}/>
</div>
{/* Allows to simulate a clone */}
{snapshot.isDragging && (
<img className="simclone" src={card.image}/>
)}
</React.Fragment>
)}
</Draggable>
</div>
</section>
);
</section>
);
}
}

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, {useEffect} from 'react';
import {DragDropContext} from "@hello-pangea/dnd";
@ -12,28 +12,13 @@ import {useShopStore} from "./shop_store";
*/
export function Shop() {
const {addCardFromBacklog, moveCard, deleteCard} = useShopStore(state => ({
const {addCardFromBacklog, moveCard, deleteCard, cardIndexById} = useShopStore(state => ({
addCardFromBacklog: state.addCardFromBacklog,
moveCard: state.moveCard,
deleteCard: state.deleteCard
deleteCard: state.deleteCard,
cardIndexById: state.cardIndexById
}));
const handleOnDragEnd = (drop_result, provided) => {
console.log(drop_result, provided)
//{
// "draggableId": "42dc17e9-9e75-45ee-ad27-2233b6f07a5e",
// "type": "DEFAULT",
// "source": {
// "index": 17,
// "droppableId": "backlog"
// },
// "reason": "DROP",
// "mode": "FLUID",
// "destination": {
// "droppableId": "crate0",
// "index": 0
// },
// "combine": null
// }
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")
@ -42,6 +27,10 @@ export function Shop() {
moveCard(drop_result.source.droppableId, drop_result.source.index, drop_result.destination.droppableId, drop_result.destination.index)
}
useEffect(() => {
addCardFromBacklog(null, [cardIndexById("kasli"), cardIndexById("eem_pwr_mod")], -1);
}, []);
return (
<DragDropContext onDragEnd={handleOnDragEnd}>
<Layout

View File

@ -1,7 +1,7 @@
'use strict';
import {create} from "zustand";
import {data, itemsUnfoldedList} from "./utils";
import {data as shared_data, itemsUnfoldedList} from "./utils";
import {true_type_of} from "./options/utils";
import {v4 as uuidv4} from "uuid";
import {FillResources} from "./count_resources";
@ -9,15 +9,18 @@ import {TriggerCrateWarnings, TriggerWarnings} from "./warnings";
const useBacklog = ((set, get) => ({
cards: data.items,
groups: data.columns.backlog,
cards: shared_data.items,
groups: shared_data.columns.backlog,
cards_list: itemsUnfoldedList,
currency: data.currency
currency: shared_data.currency,
getCardDescription: index => get().cards[get().cards_list[index]],
cardIndexById: card_id => get().cards_list.findIndex((element) => (card_id === element))
}));
const useCrateModes = ((set, get) => ({
crate_modes: data.crateModes,
modes_order: data.crateModeOrder
crate_modes: shared_data.crateModes,
modes_order: shared_data.crateModeOrder,
crateParams: mode => get().crate_modes[mode],
}));
const useLayout = ((set, get) => ({
@ -47,25 +50,41 @@ const useSubmitForm = ((set, get) => ({
isProcessingComplete: true,
}));
const useCart = ((set, get) => ({
crates: data.columns.crates,
active_crate: "crate0",
const useHighlighted = ((set, get) => ({
highlighted: {
crate: "",
card: 0
},
highlightCard: (crate_id, index) => set(state => ({
highlighted: {
crate: crate_id,
card: index
}
})),
highlightReset: () => set(state => ({
highlighted: {
crate: "",
card: 0
}
})),
}));
newCrate: () => set((state) => ({crates: state.crates.concat({
id: "crate"+state.crates.length,
const useCart = ((set, get) => ({
crates: shared_data.columns.crates,
active_crate: "crate0",
_newCrate: (crate_id) => set((state) => ({crates: state.crates.concat({
id: crate_id || "crate" + state.crates.length,
crate_mode: "rack",
items: [],
warnings: []
warnings: [],
occupiedHP: 0
})})),
delCrate: (id) => set(state => ({
crates: state.crates.filter((crate => crate.id !== id))
})),
setCrateMode: (id, mode) => set(state => ({
_setCrateMode: (id, mode) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate.id === id) {
return {
@ -76,12 +95,13 @@ const useCart = ((set, get) => ({
})
})),
setActiveCrate: (id) => set(state => ({active_crate: id})),
addCardFromBacklog: (crate_to, index_from, index_to) => set(state => {
_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) {
index_to = index_to != null ? index_to : crate.items.length;
return {
...crate,
items: crate.items.toSpliced(index_to, 0, ...take_from.map((card_name, _) => {
@ -92,7 +112,8 @@ const useCart = ((set, get) => ({
})
}
}),
moveCard: (crate_from, index_from, crate_to, index_to) => set(state => {
_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) => {
@ -102,7 +123,7 @@ const useCart = ((set, get) => ({
delete items_copy[index_from];
return {
...crate,
items: items_copy.toSpliced(index_to, 0, the_card).filter((item, _) => !!item)
items: items_copy.toSpliced(index_to+1, 0, the_card).filter((item, _) => !!item)
}
} else if (crate_to === crate.id) {
return {
@ -119,18 +140,18 @@ const useCart = ((set, get) => ({
})
}
}),
deleteCard: (crate, index) => set(state => ({
_deleteCard: (crate_id, index) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate === crate.id) {
if (crate_id === crate.id) {
return {
...crate,
items: crate.items.splice(index, 1)
items: crate.items.toSpliced(index, 1)
}
}
else return crate;
})
})),
clearCrate: (id) => set(state => ({
_clearCrate: (id) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (id === crate.id) {
return {
@ -141,9 +162,12 @@ const useCart = ((set, get) => ({
else return crate;
})
})),
updateOptions: (crate, index, new_options) => set(state => ({
clearAll: () => set(state => ({
crates: []
})),
_updateOptions: (crate_id, index, new_options) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate === crate.id) {
if (crate_id === crate.id) {
let itemsCopy = Array.from(crate.items);
itemsCopy[index].options_data = {...itemsCopy[index].options_data, ...new_options};
return {
@ -154,37 +178,74 @@ const useCart = ((set, get) => ({
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
fillWarnings: (crate_id) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate === crate.id) {
if (crate_id === crate.id) {
let itemsCopy = Array.from(crate.items);
itemsCopy = FillResources(itemsCopy);
itemsCopy = TriggerWarnings(itemsCopy);
const crate_warnings = TriggerCrateWarnings(crate);
const [crate_warnings, occupied] = TriggerCrateWarnings(crate);
return {
...crate,
items: itemsCopy,
warnings: crate_warnings
warnings: crate_warnings,
occupiedHP: occupied
}
}
else return crate;
})
}))
})),
totalOrderPrice: () => {
let sum = 0;
get().crates.forEach( (crate, _i) => {
sum += get().crate_modes[crate.crate_mode].price;
crate.items.forEach((item, _) => {
sum += item.price;
});
});
return sum;
},
// Composite actions that require warnings recalculation:
newCrate: () => {
const crate_id = "crate" + get().crates.length;
get()._newCrate(crate_id)
get().fillWarnings(crate_id);
},
setCrateMode: (id, mode) => {
console.log("setCrateMode", id, mode)
get()._setCrateMode(id, mode)
get().fillWarnings(id);
},
addCardFromBacklog: (crate_to, index_from, index_to) => {
const dest = crate_to || get().active_crate;
get()._addCardFromBacklog(dest, index_from, index_to)
get().fillWarnings(dest);
},
moveCard: (crate_from, index_from, crate_to, index_to) => {
get()._moveCard(crate_from, index_from, crate_to, index_to);
get().fillWarnings(crate_to);
if (crate_from !== crate_to) get().fillWarnings(crate_from);
},
deleteCard: (crate_id, index) => {
get()._deleteCard(crate_id, index);
get().fillWarnings(crate_id);
},
clearCrate: (id) => {
get()._clearCrate(id);
get().fillWarnings(id);
},
updateOptions: (crate_id, index, new_options) => {
get()._updateOptions(crate_id, index, new_options);
get().fillWarnings(crate_id);
}
// TODO load and save jsons?
}))
@ -195,5 +256,6 @@ export const useShopStore = create((...params) => ({
...useCrateModes(...params),
...useCart(...params),
...useSubmitForm(...params),
...useLayout(...params)
...useLayout(...params),
...useHighlighted(...params),
}))

View File

@ -7,10 +7,11 @@
import {crate_type_to_hp, item_occupied_counters, resource_counters} from "./count_resources";
import {data as shared_data} from "./utils";
import {useShopStore} from "./shop_store";
const Levels = {
"reminder": {priority: 1, icon: '/images/shop/icon-reminder.svg'},
"warning": {priority: 2, icon: '/images/shop/icon-warning.svg'},
"reminder": {priority: 1, icon: '/images/shop/icon-reminder.svg', color: "black"},
"warning": {priority: 2, icon: '/images/shop/icon-warning.svg', color: "#c75e5e"},
}
const find_in_counters = (counters, name) => {
@ -97,21 +98,6 @@ const Types = {
}
}
export function TriggerCardWarnings(data, index, precounted) {
const element = data[index];
return (element.warnings && 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;
@ -137,23 +123,26 @@ export function MaxLevel(warnings) {
return mx;
}
export function LevelUI(warning_level) {
const warning_t = Levels[warning_level];
return {icon: warning_t.icon, color: warning_t.color};
}
const crate_warnings = {
"overfit": {
message: "You have reached the maximum number of slots allowed for this crate. Consider removing cards.",
level: "warning",
trigger: (crate, occupied) => {
const nbrHP = crate_type_to_hp(crate.crate_type);
return occupied > nbrHP;
const nbrHP = useShopStore.getState().crateParams(crate.crate_mode).hp;
return occupied > nbrHP && nbrHP > 0;
}
},
"underfit_rack": {
message: "The selected cards fit in a 42hp desktop crate, consider switching to it for a more compact system",
level: "reminder",
trigger: (crate, occupied) => {
const nbrHPDesktop = shared_data.crateModes.desktop.hp;
return crate.crate_type === shared_data.crateModes.rack.id && occupied < nbrHPDesktop;
const nbrHPDesktop = useShopStore.getState().crate_modes.desktop.hp;
return crate.crate_mode === useShopStore.getState().crate_modes.rack.id && occupied < nbrHPDesktop;
}
}
}
@ -164,5 +153,5 @@ export function TriggerCrateWarnings(crate) {
Object.entries(crate_warnings).forEach(([id, warning], _) => {
if (warning.trigger(crate, nbrOccupied)) warnings.push({...warning, id: id, trigger: undefined});
})
return warnings;
return [warnings, nbrOccupied];
}

View File

@ -70,7 +70,7 @@ const shop_data = {
name_number: '1124',
name_codename: 'Kasli 2.0',
price: 3600,
image: '/shop/graphic-03_kasli.svg',
image: '/images/shop/graphic-03_kasli.svg',
specs: [
'FPGA core device, runs ARTIQ kernels, controls the EEMs.',
'4 SFP 6Gb/s slots for Ethernet or DRTIO.',
@ -126,7 +126,7 @@ const shop_data = {
name_number: '1125',
name_codename: 'Kasli-SoC',
price: 5100,
image: '/shop/graphic-03_kaslisoc.svg',
image: '/images/shop/graphic-03_kaslisoc.svg',
specs: [
'Core device based on Zynq-7000 CPU+FPGA system-on-chip.',
'Runs ARTIQ kernels on 1GHz Cortex-A9 CPU with hardware FPU.',
@ -194,7 +194,7 @@ const shop_data = {
name_number: '1008',
name_codename: '',
price: 400,
image: '/shop/graphic-03_VHDCI_carrier.svg',
image: '/images/shop/graphic-03_VHDCI_carrier.svg',
specs: [
'Passive adapter between VHDCI and EEMs.',
'VHDCI (SCSI-3) cables can carry EEM signals over short distances between crates.',
@ -221,7 +221,7 @@ const shop_data = {
name_number: '2118',
name_codename: '',
price: 450,
image: '/shop/graphic-03_BNC-TTL.svg',
image: '/images/shop/graphic-03_BNC-TTL.svg',
specs: [
'Two banks of four digital channels each, with BNC connectors.',
'Each bank with individual ground isolation.',
@ -293,7 +293,7 @@ const shop_data = {
name_number: '2128',
name_codename: '',
price: 400,
image: '/shop/graphic-03_SMA-TTL.svg',
image: '/images/shop/graphic-03_SMA-TTL.svg',
specs: [
'Same as above, but with SMA connectors.'
],
@ -359,7 +359,7 @@ const shop_data = {
name_number: '2238',
name_codename: '',
price: 600,
image: '/shop/graphic-03_MCX-TTL.svg',
image: '/images/shop/graphic-03_MCX-TTL.svg',
specs: [
'16 single-ended digital signals on MCX connectors.',
'Direction selectable in banks of four signals.',
@ -451,7 +451,7 @@ const shop_data = {
name_number: '2245',
name_codename: '',
price: 390,
image: '/shop/graphic-03_LVDS.svg',
image: '/images/shop/graphic-03_LVDS.svg',
specs: [
'Supplies 16 LVDS pairs via 4 front-panel RJ45 connectors.',
'Each RJ45 supplies 4 LVDS DIOs.',
@ -538,7 +538,7 @@ const shop_data = {
name_number: '4410',
name_codename: 'Urukul',
price: 2350,
image: '/shop/graphic-03_Urukul.svg',
image: '/images/shop/graphic-03_Urukul.svg',
specs: [
'4 channel 1GS/s DDS.',
'Output frequency (-3 dB): <1 to >400 MHz.',
@ -599,7 +599,7 @@ const shop_data = {
name_number: '4412',
name_codename: 'Urukul',
price: 2350,
image: '/shop/graphic-03_Urukul-4412.svg',
image: '/images/shop/graphic-03_Urukul-4412.svg',
specs: [
'4 channel 1GS/s DDS.',
'Higher frequency resolution ~8 µHz (47 bit)',
@ -635,7 +635,7 @@ const shop_data = {
name_number: '4624',
name_codename: 'Phaser',
price: 4260,
image: '/shop/graphic-03_Phaser.svg',
image: '/images/shop/graphic-03_Phaser.svg',
specs: [
'2x 1.25 GS/s IQ upconverters.',
'dual IQ mixer + 0.3 GHz to 4.8 GHz VCO + PLL.',
@ -667,7 +667,7 @@ const shop_data = {
name_number: '5432',
name_codename: 'Zotino',
price: 1600,
image: '/shop/graphic-03_Zotino.svg',
image: '/images/shop/graphic-03_Zotino.svg',
specs: [
'32-channel DAC.',
'16-bit resolution.',
@ -702,7 +702,7 @@ const shop_data = {
name_number: '5632',
name_codename: 'Fastino',
price: 3390,
image: '/shop/graphic-03_Fastino.svg',
image: '/images/shop/graphic-03_Fastino.svg',
specs: [
'32-channel DAC.',
'16-bit resolution.',
@ -731,7 +731,7 @@ const shop_data = {
name_number: '5518',
name_codename: '',
price: 160,
image: '/shop/graphic-03_IDC-BNC-adapter.svg',
image: '/images/shop/graphic-03_IDC-BNC-adapter.svg',
specs: [
'Breaks out analog signals from Zotino or HD68-IDC to BNC connectors.',
'Each card provides 8 channels.',
@ -753,7 +753,7 @@ const shop_data = {
name_number: '5528',
name_codename: '',
price: 160,
image: '/shop/graphic-03_SMA-IDC.svg',
image: '/images/shop/graphic-03_SMA-IDC.svg',
specs: [
'Breaks out analog signals from Zotino or HD68-IDC to SMA connectors.',
'Each card provides 8 channels.',
@ -775,7 +775,7 @@ const shop_data = {
name_number: '5538',
name_codename: '',
price: 160,
image: '/shop/graphic-03_MCX-IDC.svg',
image: '/images/shop/graphic-03_MCX-IDC.svg',
specs: [
'Breaks out analog signals from Zotino or HD68-IDC to MCX connectors.',
'Each card provides 8 channels.',
@ -797,7 +797,7 @@ const shop_data = {
name_number: '5568',
name_codename: '',
price: 150,
image: '/shop/graphic-03_HD68.svg',
image: '/images/shop/graphic-03_HD68.svg',
specs: [
'Connects an external HD68 cable to IDC-BNC, IDC-SMA or IDC-MCX cards.',
],
@ -823,7 +823,7 @@ const shop_data = {
name_number: '5108',
name_codename: '',
price: 1600,
image: '/shop/graphic-03_Sampler.svg',
image: '/images/shop/graphic-03_Sampler.svg',
specs: [
'8-channel ADC.',
'16-bit resolution.',
@ -874,7 +874,7 @@ const shop_data = {
name_number: '6302',
name_codename: '',
price: 550,
image: '/shop/graphic-03_Grabber.svg',
image: '/images/shop/graphic-03_Grabber.svg',
specs: [
'Camera input interface card.',
'Supports some EMCCD cameras.',
@ -901,7 +901,7 @@ const shop_data = {
name_number: '7210',
name_codename: '',
price: 525,
image: '/shop/graphic-03_Clocker.svg',
image: '/images/shop/graphic-03_Clocker.svg',
specs: [
'Distribute a low jitter clock signal among cards.',
'2 inputs.',
@ -936,7 +936,7 @@ const shop_data = {
name_number: '8452',
name_codename: 'Stabilizer',
price: 2000,
image: '/shop/graphic-03_Stabilizer.svg',
image: '/images/shop/graphic-03_Stabilizer.svg',
specs: [
'CPU-based dual-channel fast servo.',
'400MHz STM32H743ZIT6.',
@ -968,7 +968,7 @@ const shop_data = {
name_number: '4456',
name_codename: 'Mirny',
price: 2660,
image: '/shop/graphic-03_Mirny.svg',
image: '/images/shop/graphic-03_Mirny.svg',
specs: [
'4-channel Wide-band PLL/VCO-based microwave frequency synthesiser.',
'53 MHz to >4 GHz.',
@ -998,7 +998,7 @@ const shop_data = {
name_number: '4457',
name_codename: 'Mirny + Almazny',
price: 3660,
image: '/shop/graphic-03_Almazny.svg',
image: '/images/shop/graphic-03_Almazny.svg',
specs: [
'Mirny with high frequency mezzanine.',
'Additional 4 channels up to 12 GHz.',
@ -1025,7 +1025,7 @@ const shop_data = {
name_number: '8453',
name_codename: '',
price: 2600,
image: '/shop/graphic-03_Thermostat-EEM.svg',
image: '/images/shop/graphic-03_Thermostat-EEM.svg',
specs: [
'4 TEC channels.',
'Sensor channel count: 8 differential, 16 single-ended.',
@ -1053,7 +1053,7 @@ const shop_data = {
name_number: '5716',
name_codename: 'Shuttler',
price: 8500,
image: '/shop/graphic-03_Shuttler.svg',
image: '/images/shop/graphic-03_Shuttler.svg',
specs: [
'16-ch, 125 MSPS DAC EEM with remote analog front end board.',
'High DC resolution (up to ~18 bits with sigma-delta modulation) for trap electrode bias.',
@ -1080,7 +1080,7 @@ const shop_data = {
name_number: '4459',
name_codename: 'Stabilizer + Pounder',
price: 4460,
image: '/shop/graphic-03_Pounder.svg',
image: '/images/shop/graphic-03_Pounder.svg',
specs: [
'Stabilizer with Pounder daughter card.',
'2-channel Pound Drever Hall (PDH) lock generator.',
@ -1111,7 +1111,7 @@ const shop_data = {
name_number: '1106',
name_codename: '',
price: 750,
image: '/shop/graphic-03_eem_pwr_mod.svg',
image: '/images/shop/graphic-03_eem_pwr_mod.svg',
specs: [
"EEM AC power module.",
"400W with forced cooling (25CFM), 200W with free air convection.",
@ -1189,7 +1189,8 @@ const shop_data = {
id: "crate0",
crate_mode: "rack",
items: [],
warnings: []
warnings: [],
occupiedHP: 0,
}]
},