Compare commits

..

7 Commits

Author SHA1 Message Date
478e852eed Add horizontal items into the cart
Signed-off-by: Egor Savkin <es@m-labs.hk>
2025-02-03 16:19:22 +08:00
be578d09cb Make dialog popup rendered by bootstrap overlay library
This makes it more dynamic and eases proper placement in case of vertical layout

Signed-off-by: Egor Savkin <es@m-labs.hk>
2025-02-03 16:17:38 +08:00
f8ba2175aa Add AFWS item to the shop and update bundle
Signed-off-by: Egor Savkin <es@m-labs.hk>

# Conflicts:
#	static/js/shop.bundle.js
2025-02-03 16:17:36 +08:00
8823316ed4 Fix exception on thermostat2ch and build the bundle
Signed-off-by: Egor Savkin <es@m-labs.hk>
2025-02-03 16:17:21 +08:00
586d9efd01 Update cards
Signed-off-by: Egor Savkin <es@m-labs.hk>
2025-02-03 16:17:21 +08:00
81e7a71513 Add possibility for crateless options
Signed-off-by: Egor Savkin <es@m-labs.hk>
2025-02-03 16:17:21 +08:00
cf50bef053 Automatically open spare cards for crateless items
Signed-off-by: Egor Savkin <es@m-labs.hk>
2025-02-03 16:17:21 +08:00
30 changed files with 662 additions and 300 deletions

View File

@ -88,10 +88,6 @@ We accept all major currencies including USD, EUR, RMB, GBP, BTC and XMR.
Yes, however processing credit cards is expensive and you need to cover the costs. 4\% card processing fee applies for payment by credit card in Hong Kong dollars. 7\% card processing and exchange fee applies for payment by credit card in US dollars.
##### I have overpaid or double-paid an invoice. What should I do?
Contact sb@m-labs.hk and we will return the excess funds within a few business days. You shall pay all banking and credit card charges incurred.
##### We are a distributor. Can we have exclusive rights to your products?
Exclusivity is subject to contractual minimum order volumes determined at our discretion, but generally exceeding USD 300,000.00 per year for most countries.

View File

@ -1,11 +1,3 @@
- title: "Distributed quantum computing across an optical network link"
authors: "D. Main, P. Drmota, D. P. Nadlinger, E. M. Ainley, A. Agrawal, B. C. Nichol, R. Srinivas, G. Araneda & D. M. Lucas"
links:
- name: "Nature (2025)"
path: "https://www.nature.com/articles/s41586-024-08404-x"
- name: "Announcement"
path: "https://www.ox.ac.uk/news/2025-02-06-first-distributed-quantum-algorithm-brings-quantum-supercomputers-closer"
- title: "Fast quantum logic gates with trapped-ion qubits"
authors: "V.M. Schäfer, C.J. Ballance, K. Thirumalai, L.J. Stephenson, T.G. Ballance, A.M. Steane & D.M. Lucas"
links:

View File

@ -139,17 +139,6 @@ We welcome inquiries from research groups of all sizes.<br>[See what has been fu
<a href="https://github.com/OxfordIonTrapGroup/ndscan" target="_blank" rel="noopener noreferrer" itemprop="url">Repository</a>
{% end %}
{% layout_card(title="DAX - Duke ARTIQ extensions", sameheight=120) %}
<small>A library to provide tools for system organization/abstraction and to improve usability by automating common functionality.</small>
<a href="https://gitlab.com/duke-artiq" target="_blank" rel="noopener noreferrer" itemprop="url">Repositories</a>
{% end %}
{% layout_card(title="flake8-artiq", sameheight=120) %}
<small>A Flake8 plugin for checking ARTIQ code</small>
<a href="https://gitlab.com/duke-artiq/flake8-artiq" target="_blank" rel="noopener noreferrer" itemprop="url">Repository</a>
{% end %}
{% layout_card(title="Oxford routines", sameheight=120) %}
<small>Oxford Ion-Trap Group routines</small>
@ -157,12 +146,6 @@ We welcome inquiries from research groups of all sizes.<br>[See what has been fu
<a href="https://github.com/OxfordIonTrapGroup/oitg" target="_blank" rel="noopener noreferrer" itemprop="url">Repository</a>
{% end %}
{% layout_card(title="ATOMIQ", sameheight=120) %}
<small>An abstraction layer to move hardware specific information into a configuration layer and enables working with generic software objects representing the actual hardware in the lab.</small>
<a href="https://thequantumlaend.de/2024/03/07/atomiq-our-convenience-layer-for-artiq-now-available/" target="_blank" rel="noopener noreferrer" itemprop="url">Announcement</a> | <a href="https://gitlab.com/atomiq-project/atomiq" target="_blank" rel="noopener noreferrer" itemprop="url">Repository</a>
{% end %}
{% layout_card(title="UCLA routines", sameheight=120) %}
<small>ARTIQ experiments in use at UCLA AMO</small>
@ -187,12 +170,25 @@ We welcome inquiries from research groups of all sizes.<br>[See what has been fu
<a href="https://github.com/cnourshargh/Bham-ARTIQ-examples" target="_blank" rel="noopener noreferrer" itemprop="url">Repository</a>
{% end %}
{% layout_card(title="DAX - Duke ARTIQ extensions", sameheight=120) %}
<small>A library to provide tools for system organization/abstraction and to improve usability by automating common functionality.</small>
<a href="https://gitlab.com/duke-artiq" target="_blank" rel="noopener noreferrer" itemprop="url">Repositories</a>
{% end %}
{% layout_card(title="Argent", sameheight=120) %}
<small>High-level sequence control interface for ARTIQ.</small>
<a href="https://github.com/robertfasano/argent" target="_blank" rel="noopener noreferrer" itemprop="url">Repository</a>
{% end %}
{% layout_card(title="flake8-artiq", sameheight=120) %}
<small>A Flake8 plugin for checking ARTIQ code</small>
<a href="https://gitlab.com/duke-artiq/flake8-artiq" target="_blank" rel="noopener noreferrer" itemprop="url">Repository</a>
{% end %}
{% layout_card(title="GenericSCPIDriver", sameheight=120) %}
<small>A generic Python driver for SCPI devices driven over serial connections. Compatible with ARTIQ.</small>

View File

@ -56,13 +56,6 @@
background-size: cover;
}
.card-featured {
background: #fff url("../images/fireworks-phone@2x.png") no-repeat top center;
background-size: contain;
}
.card-featured > div {
padding-top: 66.64%;
}
.card-artiq {
background: #fff url("../images/artiq-phone@2x.png") no-repeat top center;
@ -129,10 +122,6 @@ img.kf25 {
// Small devices (landscape phones, 576px and up)
@media (min-width: 576px) {
.card-featured > div {
padding-top: 0;
}
.card-artiq > div {
padding-top: 0;
}
@ -224,11 +213,6 @@ img.kf25 {
box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important;
}
.card-featured {
background: #fff url("../images/fireworks@2x.png") no-repeat top right;
}
.card-artiq {
background: #fff url("../images/artiq@2x.png") no-repeat top right;
}

View File

@ -117,10 +117,20 @@ button {
margin-bottom: 20px;
}
button {
img {
cursor: pointer;
}
}
img {
height: 400px;
align-self: center;
border: 0;
cursor: grab;
&.default-icon {
height: 144px;
}
}
h3 {
@ -484,6 +494,45 @@ button {
background-color: #ebebeb;
padding: 5px 5px 12px;
position: relative;
&.horizontal {
flex-direction: column;
min-height: 10px;
overflow-y: auto;
overflow-x: hidden;
width: 100%;
> div {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-content: center;
align-items: center;
width: 100%;
max-width: 100%;
padding: 5px 3px;
}
.progress-container {
max-width: 40px;
padding: 0 10px;
justify-self: start;
}
.product-name {
justify-self: start;
padding: 0 10px;
max-width: 100%;
width: 100%;
font-size: 1rem;
}
.removeHorizontal {
justify-self: end;
padding: 0 10px;
img {
width: 20px;
height: 20px;
}
}
}
> div {
display: flex;
@ -571,67 +620,6 @@ button {
}
}
.overlayVariant {
top: 24px;
width: 140px;
min-height: 40px;
max-height: 320px;
overflow-y: scroll;
position: absolute;
display: flex;
align-items: start;
text-align: left;
background-color: white;
color: black;
flex-direction: column;
cursor: pointer;
padding: 0.2rem 0;
p {
font-size: .65rem;
margin: 0;
}
div {
margin: 0.1rem 0.2rem;
font-size: 0.75rem;
input {
padding: 0;
font-size: 0.75rem;
line-height: 1.1;
}
label {
margin-bottom: 0.1rem;
}
.options-icon {
display: inline;
height: .875rem;
margin-right: 0.2rem;
margin-left: 0.2rem;
}
}
.form-check {
min-height: 1rem;
}
}
}
.overlay-smallcard {
left: -38.5px; // (card width (63) - overlay width (140)) / 2
&.overlay-first {
left: 0;
}
&.overlay-last {
left: -67px;
}
}
.overlay-bigcard {
left: -7px; // (card width (126) - overlay width (140)) / 2
}
.hovered {
@ -778,3 +766,49 @@ button {
}
}
.overlayVariant {
width: 140px;
min-height: 40px;
max-height: 320px;
overflow-y: scroll;
position: absolute;
display: flex;
align-items: start;
text-align: left;
background-color: white;
color: black;
flex-direction: column;
cursor: pointer;
padding: 0.2rem 0;
p {
font-size: .65rem;
margin: 0;
}
div {
margin: 0.1rem 0.2rem;
font-size: 0.75rem;
input {
padding: 0;
font-size: 0.75rem;
line-height: 1.1;
}
label {
margin-bottom: 0.1rem;
}
.options-icon {
display: inline;
height: .875rem;
margin-right: 0.2rem;
margin-left: 0.2rem;
}
}
.form-check {
min-height: 1rem;
}
}

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1 @@
<svg width="256" height="256" version="1.1" viewBox="0 0 67.733 67.733" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(1.2797 0 0 1.2797 -4.4788 -4.4788)" stroke-width="1.0001"><g fill-opacity="0" stroke="#fff" stroke-linecap="round" stroke-width="1.0001"><g stroke-linejoin="round" stroke-opacity=".8" stroke-width="1.0001"><path d="m4 28.965v2"/><path d="m4 40.948v2"/><path d="m4 16.983v2"/></g><g stroke-linejoin="round" stroke-opacity=".8" stroke-width="1.0001"><path d="m28.965 4h2"/><path d="m16.983 4h2"/><path d="m40.948 4.0001h2"/></g><g transform="translate(0 3.3081)" stroke-linejoin="round" stroke-opacity=".8" stroke-width="1.0001"><path d="m28.965 52.623h2"/><path d="m40.948 52.623h2"/><path d="m16.983 52.623h2"/></g><g transform="translate(3.3082)" stroke-linejoin="round" stroke-opacity=".8" stroke-width="1.0001"><path d="m52.623 28.965v2"/><path d="m52.623 16.983v2.0001"/><path d="m52.623 40.948v2.0001"/></g><g stroke-linejoin="round"><path d="m7 4h-3v3" stroke-opacity=".8" stroke-width="1.0001"/><path d="m52.931 4h3v3" stroke-opacity=".8" stroke-width="1.0001"/><path d="m7 55.931h-3v-3" stroke-opacity=".8" stroke-width="1.0001"/></g><path d="m28.311 27.311v2.0001m-1-1h2" stroke-opacity=".8" stroke-width="1.0001"/><path d="m52.931 55.931h3v-3" stroke-linejoin="round" stroke-opacity=".8" stroke-width="1.0001"/></g></g><g transform="matrix(.64211 -.68634 .59172 .63248 35.302 51.828)"><path transform="translate(1.6405e-6)" d="m7 13.5v-7c0-0.82842 0.67157-1.5 1.5-1.5s1.5 0.55521 1.5 1.3836v0.11637 2m0-1e-5v-2c0-2.2857 3-2.2857 3 0v0.52708 1.4729m0 3e-5v-1.4729c0-2.2857 3-2.2857 3 0v0.9015 0.57143m-3 0v-1.4729m0-0.52707v0.52707m3 1.4729v-0.57143-0.9015c0-2.2857-3-2.2857-3 0m3 1.4728v-0.57143c0-2.2857 3-2.2857 3 0m-12 2.5715-2.0041 2.6721c-0.57746 0.77-0.52464 1.842 0.12569 2.5515l3.7839 4.1279c0.37882 0.4132 0.91276 0.6485 1.4734 0.6485h4.6211c2.4 0 4-1.5 4-4v-8.5714" fill-opacity="0" stroke="#fff" stroke-opacity=".9"/></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
import React from 'react'
import React, {useRef, useEffect, useState} from 'react'
import {Droppable} from "@hello-pangea/dnd";
import {cartStyle, compareArraysWithIds} from "./utils";
import {ProductCartItem} from "./ProductCartItem";
@ -9,17 +9,17 @@ import {useShopStore} from "./shop_store";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
/**
* Component that displays a list of <ProductCartItem>
*/
export function Cart({crate_index}) {
// #!render_count
const renderCount = useRenderCount();
const containerRef = useRef(null);
const [visiblePlaceholders, setVisiblePlaceholders] = useState(0);
const crate = useShopStore((state) => state.crates[crate_index], (a, b) => {
return compareArraysWithIds(a.items, b.items) && a.occupiedHP === b.occupiedHP && a.crate_mode === b.crate_mode
});
const crateParams = useShopStore((state) => state.crateParams);
const isCrate = useShopStore((state) => state.modes_order.includes(state.crates[crate_index].crate_mode));
// #!render_count
console.log("Cart renders: ", renderCount)
@ -27,23 +27,43 @@ export function Cart({crate_index}) {
const nbrOccupied = hp_to_slots(crate.occupiedHP);
const nbrSlots = hp_to_slots(crateParams(crate.crate_mode).hp);
useEffect(() => {
const updateVisiblePlaceholders = () => {
if (!containerRef.current) return;
setVisiblePlaceholders(isCrate ?
(nbrSlots - nbrOccupied) :
// 10 is padding and 77 is the actual size with all the margins
Math.max(1, Math.floor((containerRef.current.offsetWidth - 10) / 77) - nbrOccupied));
};
updateVisiblePlaceholders();
const resizeObserver = new ResizeObserver(updateVisiblePlaceholders);
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}
return () => {
if (containerRef.current) {
resizeObserver.unobserve(containerRef.current);
}
};
}, [nbrOccupied, nbrSlots]);
const products = crate.items.map((item, index) => {
return (
<ProductCartItem
card_index={index}
crate_index={crate_index}
first={index === 0}
last={index === crate.items.length - 1 && nbrOccupied >= nbrSlots}
key={item.id}/>
);
});
return (
<Droppable droppableId={crate.id} direction="horizontal">
{(provided, snapshot) => (
<div
ref={provided.innerRef}
ref={(element) => {
containerRef.current = element;
provided.innerRef(element);
}}
{...provided.droppableProps}
style={cartStyle(
provided.droppableProps.style,
@ -60,11 +80,10 @@ export function Cart({crate_index}) {
)}
<FakePlaceholder
nToDraw={nbrSlots - nbrOccupied}
nToDraw={visiblePlaceholders}
isDraggingOver={snapshot.isDraggingOver}/>
</div>
)}
</Droppable>
);
}

View File

@ -0,0 +1,59 @@
import React from 'react'
import {Droppable} from "@hello-pangea/dnd";
import {cartStyle, compareArraysWithIds} from "./utils";
import {ProductCartItemHorizontal} from "./ProductCartItemHorizontal";
import {HORIZONTAL_CART_MARKER, useShopStore} from "./shop_store";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
/**
* Component that displays a list of <ProductCartItem>
*/
export function CartHorizontal({crate_index}) {
// #!render_count
const renderCount = useRenderCount();
const crate = useShopStore((state) => state.crates[crate_index], (a, b) => {
return compareArraysWithIds(a.h_items, b.h_items)
});
// #!render_count
console.log("CartHorizontal renders: ", renderCount)
const products = crate.h_items.map((item, index) => {
return (
<ProductCartItemHorizontal
card_index={index}
crate_index={crate_index}
key={item.id}/>
);
});
return (
<Droppable droppableId={crate.id+HORIZONTAL_CART_MARKER} direction="vertical">
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
style={cartStyle(
provided.droppableProps.style,
snapshot,
)}
className="items-cart-list horizontal">
{products}
{provided.placeholder && (
<div style={{display: 'none'}}>
{provided.placeholder}
</div>
)}
</div>
)}
</Droppable>
);
}

View File

@ -16,7 +16,7 @@ export function Catalog() {
const renderCount = useRenderCount();
const data = useShopStore((state) => state.groups);
const items = useShopStore((state) => state.cards);
const _items = useShopStore((state) => state.cards);
const onClickToggleMobileSideMenu = useShopStore((state) => state.switchSideMenu);
const isMobile = useShopStore((state) => state.isMobile);

View File

@ -4,6 +4,7 @@ import {CrateMode} from "./CrateMode";
import {CrateWarnings} from "./CrateWarnings";
import {useShopStore} from "./shop_store";
import {CrateOptions} from "./CrateOptions";
import {CartHorizontal} from "./CartHorizontal";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
@ -28,7 +29,7 @@ export function Crate({crate_index}) {
return (
<div className="crate">
{
modes_order.includes(crate.crate_mode) ? (
modes_order.includes(crate.crate_mode) && (
<div className="crate-bar d-inline-flex justify-content-between">
<CrateMode crate_index={crate_index}/>
@ -36,13 +37,17 @@ export function Crate({crate_index}) {
Delete crate <img src="/images/shop/icon-remove.svg" alt="remove"/>
</div>
</div>
) : <></>
)
}
<div className="crate-products">
<Cart crate_index={crate_index}/>
{ !modes_order.includes(crate.crate_mode) &&
<CartHorizontal crate_index={crate_index}/>
}
<CrateWarnings crate_index={crate_index} />
<CrateOptions crate_index={crate_index}/>

View File

@ -12,7 +12,7 @@ export function FakePlaceholder({isDraggingOver, nToDraw}) {
<div key={i} style={{
display: isDraggingOver ? 'none' : 'block',
border: '1px dashed #ccc',
width: '45px',
width: '69px',
marginBottom: '5px',
}}></div>
);

View File

@ -1,21 +1,23 @@
import {DialogPopup} from "./options/DialogPopup";
import React from "react";
import {useShopStore} from "./shop_store";
import {useShopStore, whichItems} from "./shop_store";
import {SummaryPopup} from "./options/SummaryPopup";
export function OptionsDialogWrapper({crate_index, card_index, first, last}) {
export function OptionsDialogWrapper({crate_index, card_index, horizontal}) {
const whichH = whichItems(horizontal);
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 card_id = useShopStore((state) => state.crates[crate_index].items[card_index].id);
const options_class = useShopStore((state) => state.crates[crate_index].items[card_index].options_class);
const use_options = useShopStore((state) => state.crateParams(state.crates[crate_index].crate_mode).options);
const options = useShopStore((state) => state.crates[crate_index][whichH][card_index][use_options]);
const options_data = useShopStore((state) => state.crates[crate_index][whichH][card_index].options_data);
const card_id = useShopStore((state) => state.crates[crate_index][whichH][card_index].id);
const options_class = useShopStore((state) => state.crates[crate_index][whichH][card_index].options_class);
const sideMenuIsOpen = useShopStore((state) => state.sideMenuIsOpen);
const _notificationTimer = useShopStore((state) => state.notificationTimer);
const hideNotification = useShopStore((state) => state.hideNotification);
const displayNotification = useShopStore((state) =>
const displayNotification = useShopStore((state) =>
!!state.notificationHorizontal === !!horizontal &&
state.notificationCrateId === crate_id &&
(state.notificationCardIndex === card_index || (state.crates[crate_index].items.length + (state.notificationCardIndex || -1)) === card_index));
(state.notificationCardIndex === card_index || (state.crates[crate_index][whichH].length + (state.notificationCardIndex || -1)) === card_index));
const onOptionsUpdate = useShopStore((state) => state.updateOptions);
@ -26,12 +28,10 @@ export function OptionsDialogWrapper({crate_index, card_index, first, last}) {
options_class={options_class}
key={"popover" + crate_id +card_id}
id={"popover"+ crate_id + card_id}
big={card_size === "big"}
first={first}
last={last}
sideMenuIsOpen={sideMenuIsOpen}
onHideNotification={hideNotification}
displayNotification={displayNotification}
horizontal={horizontal}
target={{
construct: ((outvar, value) => {
// #!options_log
@ -44,17 +44,19 @@ export function OptionsDialogWrapper({crate_index, card_index, first, last}) {
console.log("update", outvar, value, options_data);
if (outvar in options_data) options_data[outvar] = value;
onOptionsUpdate(crate_id, card_index, {[outvar]: value});
onOptionsUpdate(crate_id, card_index, {[outvar]: value}, horizontal);
})
}}
/>
)
}
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);
export function OptionsSummaryWrapper({crate_index, card_index, horizontal}) {
const whichH = whichItems(horizontal);
const card_id = useShopStore((state) => state.crates[crate_index][whichH][card_index].id);
const use_options = useShopStore((state) => state.crateParams(state.crates[crate_index].crate_mode).options);
const options = useShopStore((state) => state.crates[crate_index][whichH][card_index][use_options]);
const options_data = useShopStore((state) => state.crates[crate_index][whichH][card_index].options_data);
return (
<SummaryPopup id={card_id + "options"} options={options}

View File

@ -8,7 +8,6 @@ import {RFQFeedback} from "./RFQFeedback";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
import {OrderOptions} from "./OrderOptions";
/**
* Component that renders all things for order.

View File

@ -13,7 +13,7 @@ import {useRenderCount} from "@uidotdev/usehooks";
* Component that renders a product.
* Used in the crate
*/
export function ProductCartItem({card_index, crate_index, first, last}) {
export function ProductCartItem({card_index, crate_index}) {
// #!render_count
const renderCount = useRenderCount();
@ -24,8 +24,12 @@ export function ProductCartItem({card_index, crate_index, first, last}) {
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 options_disabled = useShopStore((state) => !!state.crateParams(state.crates[crate_index].crate_mode).warnings_disabled);
const highlighted = useShopStore((state) =>
!state.highlighted.horizontal &&
state.crates[crate_index].id === state.highlighted.crate &&
card_index === state.highlighted.card);
const warnings_disabled = useShopStore((state) => !!state.crateParams(state.crates[crate_index].crate_mode).warnings_disabled);
const use_options = useShopStore((state) => state.crateParams(state.crates[crate_index].crate_mode).options);
const crate_id = useShopStore((state) => state.crates[crate_index].id);
const setHighlight = useShopStore((state) => state.highlightCard);
const removeHighlight = useShopStore((state) => state.highlightReset);
@ -35,9 +39,9 @@ export function ProductCartItem({card_index, crate_index, first, last}) {
console.log("ProductCartItem renders: ", renderCount)
const options = !options_disabled && card && card.options && card.options.length > 0;
const warnings = !options_disabled && card_show_warnings && card_show_warnings.length > 0;
const resources = !options_disabled && card_counted_resources && card_counted_resources.length > 0;
const options = use_options && card && card[use_options] && card[use_options].length > 0;
const warnings = !warnings_disabled && card_show_warnings && card_show_warnings.length > 0;
const resources = !warnings_disabled && card_counted_resources && card_counted_resources.length > 0;
return (
<Draggable draggableId={card.id} index={card_index}>
@ -57,7 +61,7 @@ export function ProductCartItem({card_index, crate_index, first, last}) {
true
)
}}
onMouseEnter={() => setHighlight(crate_id, card_index)}
onMouseEnter={() => setHighlight(crate_id, card_index, false)}
onMouseLeave={removeHighlight}
>
@ -72,8 +76,6 @@ export function ProductCartItem({card_index, crate_index, first, last}) {
<OptionsDialogWrapper
crate_index={crate_index}
card_index={card_index}
first={first}
last={last}
/>
)}
</div>

View File

@ -0,0 +1,96 @@
import React from 'react'
import {Draggable} from "@hello-pangea/dnd";
import {productStyle} from "./utils";
import {useShopStore} from "./shop_store";
import {OptionsDialogWrapper} from "./OptionsWrapper";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
/**
* Component that renders a product.
* Used in the crate
*/
export function ProductCartItemHorizontal({card_index, crate_index}) {
// #!render_count
const renderCount = useRenderCount();
const card = useShopStore((state) => state.crates[crate_index].h_items[card_index],
(a, b) => a.id === b.id);
const highlighted = useShopStore((state) =>
!!state.highlighted.horizontal &&
state.crates[crate_index].id === state.highlighted.crate &&
card_index === state.highlighted.card
);
const use_options = useShopStore((state) => state.crateParams(state.crates[crate_index].crate_mode).options);
const crate_id = useShopStore((state) => state.crates[crate_index].id);
const setHighlight = useShopStore((state) => state.highlightCard);
const removeHighlight = useShopStore((state) => state.highlightReset);
const onCardRemove = useShopStore((state) => state.deleteCard);
// #!render_count
console.log("ProductCartItem renders: ", renderCount)
const options = use_options && card && card[use_options] && card[use_options].length > 0;
return (
<Draggable draggableId={card.id} index={card_index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
...productStyle(
provided.draggableProps.style,
snapshot,
true,
!!highlighted,
false,
true
)
}}
onMouseEnter={() => setHighlight(crate_id, card_index, true)}
onMouseLeave={removeHighlight}
>
{/* warning container */}
<div className="progress-container warning d-flex justify-content-evenly">
{options && (
<OptionsDialogWrapper
crate_index={crate_index}
card_index={card_index}
horizontal={true}
/>
)}
</div>
<div
className="product-name"
onMouseEnter={() => setHighlight(crate_id, card_index, true)}
onClick={() => setHighlight(crate_id, card_index, true)}
>
{`${card.name_number} ${card.name} ${card.name_codename}`}
</div>
{/* remove container */}
<div
style={{'display': 'flex'}}
className="removeHorizontal"
onClick={() => onCardRemove(crate_id, card_index, true)}>
<img src="/images/shop/icon-remove.svg" alt="rm"/>
</div>
</div>
)}
</Draggable>
);
}

View File

@ -7,6 +7,8 @@ import {useShopStore} from "./shop_store";
import {useRenderCount} from "@uidotdev/usehooks";
const DNDIcon = "/images/shop/icon-drag-and-drop.svg";
function DatasheetLink({datasheet_file, datasheet_name}) {
return datasheet_file && datasheet_name && (<div className="ds">
<span className='doc-icon'></span>
@ -76,11 +78,12 @@ export function ProductItem({card_index}) {
snapshot,
true, // hack: remove weird animation after a drop
)}
src={card.image}/>
className={card.image ? "" : "default-icon"}
src={card.image || DNDIcon}/>
{/* Allows to simulate a clone */}
{snapshot.isDragging && (
<img className="simclone" src={card.image}/>
<img className={"simclone " + (card.image ? "" : "default-icon")} src={card.image || DNDIcon}/>
)}
</React.Fragment>
)}

View File

@ -15,6 +15,7 @@ export function SummaryCrate({crate_index}) {
const crate_id = useShopStore((state) => state.crates[crate_index].id);
const crate_len = useShopStore((state) => state.crates[crate_index].items.length);
const crate_h_len = useShopStore((state) => state.crates[crate_index].h_items.length);
// #!render_count
console.log("SummaryCrate renders: ", renderCount)
@ -28,6 +29,14 @@ export function SummaryCrate({crate_index}) {
<SummaryCrateCard crate_index={crate_index} card_index={index} key={"summary_crate_" + crate_id + "_" +index} />
)}
{range(0, crate_h_len).map((index, _i) =>
<SummaryCrateCard
crate_index={crate_index}
card_index={index}
horizontal={true}
key={"summary_crate_h_" + crate_id + "_" +index} />
)}
<SummaryCratePricedOptions crate_index={crate_index}/>
</tbody>
)

View File

@ -1,45 +1,56 @@
import {compareObjectsEmptiness, formatMoney} from "./utils";
import {WarningIndicator} from "./CardWarnings";
import React from "react";
import {useShopStore} from "./shop_store";
import {useShopStore, whichItems} from "./shop_store";
import {OptionsSummaryWrapper} from "./OptionsWrapper";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
export function SummaryCrateCard({crate_index, card_index}) {
export function SummaryCrateCard({crate_index, card_index, horizontal}) {
// #!render_count
const renderCount = useRenderCount();
const whichH = whichItems(horizontal);
const currency = useShopStore((state) => state.currency);
const deleteCard = useShopStore((state) => state.deleteCard);
const setHighlight = useShopStore((state) => state.highlightCard);
const resetHighlight = useShopStore((state) => state.highlightReset);
const highlighted = useShopStore((state) => state.crates[crate_index].id === state.highlighted.crate && card_index === state.highlighted.card);
const highlighted = useShopStore((state) =>
state.highlighted.horizontal === horizontal &&
state.crates[crate_index].id === state.highlighted.crate &&
card_index === state.highlighted.card);
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][whichH][card_index],
(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);
const options_disabled = useShopStore((state) => !!state.crateParams(state.crates[crate_index].crate_mode).warnings_disabled);
// additional hooks for updating warning and options
const card_show_warnings = useShopStore(state =>
state.crates[crate_index][whichH][card_index].show_warnings,
compareObjectsEmptiness);
const card_options_data = useShopStore(state =>
state.crates[crate_index][whichH][card_index].options_data,
compareObjectsEmptiness);
const warnings_disabled = useShopStore((state) => !!state.crateParams(state.crates[crate_index].crate_mode).warnings_disabled);
const use_options = useShopStore((state) => state.crateParams(state.crates[crate_index].crate_mode).options);
// #!render_count
console.log("SummaryCrateCard renders: ", renderCount)
const options = !options_disabled && card && card.options && card.options.length > 0;
const options_data = !options_disabled && card_options_data && Object.keys(card_options_data).length > 0;
const warnings = !options_disabled && card_show_warnings && card_show_warnings.length > 0;
const options = use_options && card && card[use_options] && card[use_options].length > 0;
const options_data = card_options_data && Object.keys(card_options_data).length > 0;
const warnings = !warnings_disabled && card_show_warnings && card_show_warnings.length > 0;
return (
<tr
key={"summary_crate_" + crate_id + "_" + card_index}
className={`hoverable ${highlighted ? 'selected' : ''}`}
onClick={() => setHighlight(crate_id, card_index)}
onMouseEnter={() => setHighlight(crate_id, card_index)}
onClick={() => setHighlight(crate_id, card_index, horizontal)}
onMouseEnter={() => setHighlight(crate_id, card_index, horizontal)}
onMouseLeave={() => resetHighlight()}>
<td className="item-card-name tabbed">
<div>{`${card.name_number} ${card.name} ${card.name_codename}`}</div>
@ -49,7 +60,7 @@ export function SummaryCrateCard({crate_index, card_index}) {
<div className="d-inline-flex align-content-center">
{`${currency} ${formatMoney(card.price)}`}
<button onClick={() => deleteCard(crate_id, card_index)}>
<button onClick={() => deleteCard(crate_id, card_index, horizontal)}>
<img src="/images/shop/icon-remove.svg" className="d-block"/>
</button>
@ -64,7 +75,10 @@ export function SummaryCrateCard({crate_index, card_index}) {
}}>&nbsp;</span>
))}
{((options && options_data) ? (
<OptionsSummaryWrapper crate_index={crate_index} card_index={card_index}/>
<OptionsSummaryWrapper
crate_index={crate_index}
card_index={card_index}
horizontal={horizontal}/>
) : (
<span style={{
'display': 'inline-block',

View File

@ -1,7 +1,6 @@
import {formatMoney} from "./utils";
import React from "react";
import {useShopStore} from "./shop_store";
import {ProcessOptions, ProcessOptionsToData} from "./options/Options";
import {ProcessOptions} from "./options/Options";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";

View File

@ -21,10 +21,13 @@ export function validateJSON(description) {
try {
for (const crate of crates_raw) {
if (!crate.type || !crate.items || !crate.options || !(crate.type in crate_modes)) return false;
if (!crate.type || !crate.items || !crate.h_items || !crate.options || !(crate.type in crate_modes)) return false;
for (const card of crate.items) {
if (!(card.pn in pn_to_card) || card.options === undefined) return false;
}
for (const card of crate.h_items) {
if (!(card.pn in pn_to_card) || card.options === undefined) return false;
}
}
} catch (e) {
return false;
@ -50,6 +53,11 @@ export function JSONToCrates(description) {
id: uuidv4(),
options_data: card.options || {}
}))),
h_items: Array.from(crate.h_items.map((card, _i) => ({
...pn_to_card(card.pn),
id: uuidv4(),
options_data: card.options || {}
}))),
warnings: [],
occupiedHP: 0,
})));
@ -65,12 +73,17 @@ export function CratesToJSON(crates) {
const crateOptions = useShopStore.getState().crate_options;
const orderOptions = useShopStore.getState().order_options;
const orderOptionsData = useShopStore.getState().order_options_data;
const crateParams = useShopStore.getState().crateParams;
return JSON.stringify({
// additional fields can go here
crates: Array.from(crates.map((crate, _i) => ({
items: Array.from(crate.items.map((card, _) => ({
pn: card.name_number,
options: (card.options_data && card.options) ? FilterOptions(card.options, card.options_data) : null
options: (card.options_data && card[crateParams(crate.crate_mode).options]) ? FilterOptions(card[crateParams(crate.crate_mode).options], card.options_data) : null
}))),
h_items: Array.from(crate.h_items.map((card, _) => ({
pn: card.name_number,
options: (card.options_data && card[crateParams(crate.crate_mode).options]) ? FilterOptions(card[crateParams(crate.crate_mode).options], card.options_data) : null
}))),
type: crate.crate_mode,
options: FilterOptions(crateOptions, crate.options_data)

View File

@ -1,46 +1,56 @@
import React, {useState} from "react";
import {useClickAway} from "./useClickAway";
import React, {useRef, useState} from "react";
import {ProcessOptions} from "./Options";
import {Notification} from "./Notification";
import {Overlay} from "react-bootstrap";
export function DialogPopup({options, data, target, id, big, first, last, options_class,
sideMenuIsOpen, displayNotification, onHideNotification}) {
export function DialogPopup({options, data, target, id, options_class,
sideMenuIsOpen, displayNotification, onHideNotification, horizontal}) {
const [show, setShow] = useState(false);
const ref = useClickAway((e) => {
if (e.type === "mousedown") // ignore touchstart
setShow(false)
}
);
const ref = useRef(null);
let div_classes = `overlayVariant border rounded ${options_class || ""}`;
let div_classes = `overlayVariant border rounded ${big ? "overlay-bigcard" : "overlay-smallcard"} ${(!big && first) ? "overlay-first" : ""} ${(!big && last && !first) ? "overlay-last" : ""} ${options_class || ""}`;
const handleClick = (_event) => {
setShow(!show);
};
return (
<div ref={ref}>
<Notification
id={"processed_options_notification" + id}
tip="Customization options available"
sideMenuIsOpen={sideMenuIsOpen}
show={displayNotification}
onHide={onHideNotification}
content={
<img className="alert-info"
src={show ? "/images/shop/icon-close.svg" : "/images/shop/icon-customize.svg"}
onClick={handleClick}/>
}
/>
<div style={{'display': show ? 'flex' : 'none'}} className={div_classes}>
<ProcessOptions
options={options}
data={data}
key={"processed_options_" + id}
id={"processed_options_" + id}
target={target}
/>
</div>
</div>
);
return (<>
<Notification
id={"processed_options_notification" + id}
tip="Customization options available"
sideMenuIsOpen={sideMenuIsOpen}
show={displayNotification}
onHide={onHideNotification}
content={
<img className="alert-info" ref={ref}
src={show ? "/images/shop/icon-close.svg" : "/images/shop/icon-customize.svg"}
onClick={handleClick}/>
}
/>
<Overlay target={ref.current}
show={show}
placement={horizontal ? "right" : "bottom"}
onHide={() => setShow(false)}
rootClose={true}>
{({
placement: _placement,
arrowProps: _arrowProps,
show: _show,
popper: _popper,
hasDoneInitialMeasure: _hasDoneInitialMeasure,
...props
}) => (
<div style={{'display': show ? 'flex' : 'none', ...props.style}} {...props} className={div_classes}>
<ProcessOptions
options={options}
data={data}
key={"processed_options_" + id}
id={"processed_options_" + id}
target={target}
/>
</div>
)}
</Overlay>
</>);
}

View File

@ -1,13 +1,13 @@
const ipv4 = (params) => {
const ipv4 = (_params) => {
const ipv4WithMaskPattern = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/(0|1[0-9]|2[0-9]|3[0-2]|[0-9])$/;
return (text) => {
return ipv4WithMaskPattern.test(text);
}
}
const ipv6 = (params) => {
const ipv6 = (_params) => {
const ipv6WithMaskPattern = /(^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(\/(\d{1,2}|1[0-1]\d|12[0-8]))(%.+)?\s*$)/;
@ -16,6 +16,35 @@ const ipv6 = (params) => {
}
}
const hostname = (_params) => {
const maxHostnameLength = 253;
const maxLabelLength = 63;
const labelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
return (text) => {
if (text.length > maxHostnameLength) {
return false;
}
const labels = text.split('.');
for (const label of labels) {
if (label.length < 1
|| label.length > maxLabelLength
|| !labelRegex.test(label)) {
return false;
}
}
return true;
}
}
const ipv4OrHost = (params) => {
const hostnameLocal = hostname(params);
const ipv4Local = ipv4(params);
return (text) => {
return ipv4Local(text) || hostnameLocal(text);
}
}
const ipv4or6 = (params) => {
const ipv4Local = ipv4(params);
const ipv6Local = ipv6(params);
@ -47,5 +76,7 @@ export const Validation = {
ipv4: ipv4,
ipv6: ipv6,
ipv4or6: ipv4or6,
hostname: hostname,
ipv4OrHost: ipv4OrHost,
frequency: frequency
};

View File

@ -2,7 +2,7 @@
import {createWithEqualityFn} from "zustand/traditional";
import {DATA as shared_data, itemsUnfoldedList, API_RFQ} from "./utils";
import {FillExtCrateData, FillExtOrderData, true_type_of} from "./options/utils";
import {FillExtCrateData, FillExtOrderData} from "./options/utils";
import {v4 as uuidv4} from "uuid";
import {FillResources} from "./count_resources";
import {FillExtCardData} from "./options/utils";
@ -13,12 +13,24 @@ import {ProcessOptionsToData} from "./options/Options";
import {DomainedRFQMessages} from "./Domained";
export const HORIZONTAL_CART_MARKER = "_h";
const cards_to_pn_map = (cards) => {
let result = {};
Object.entries(cards).forEach(([key, card], _i) => { result[card.name_number] = key})
return result;
};
const toArray = (arg) => {
return Array.isArray(arg) ? arg : [arg];
};
const unwrapCrateId = (crate_id= "") => {
return crate_id.endsWith(HORIZONTAL_CART_MARKER) ? [true, crate_id.substring(0, crate_id.length - HORIZONTAL_CART_MARKER.length)] : [false, crate_id]
}
export const whichItems = (horizontal = false) => horizontal ? "h_items" : "items"
const useCatalog = ((set, get) => ({
cards: shared_data.items,
groups: shared_data.columns.catalog,
@ -33,21 +45,24 @@ const useCatalog = ((set, get) => ({
const useOptionsNotification = ((set, get) => ({
notificationCrateId: null,
notificationCardIndex: null,
notificationHorizontal: false,
notificationTimer: null,
_showNotification: (crate_id, card_index) => set(state => ({
_showNotification: (crate_id, card_index, horizontal) => set(state => ({
notificationCrateId: crate_id,
notificationCardIndex: card_index,
notificationHorizontal: horizontal,
notificationTimer: setTimeout(() => {
state.hideNotification()
}, 5000)
})),
showNotification: (crate_id, card_index) => {
showNotification: (crate_id, card_index, horizontal) => {
get().hideNotification()
setTimeout(() => get()._showNotification(crate_id, card_index), 100);
setTimeout(() => get()._showNotification(crate_id, card_index, horizontal), 100);
},
hideNotification: () => set(state => ({
notificationCrateId: null,
notificationCardIndex: null,
notificationHorizontal: false,
notificationTimer: (state.notificationTimer && clearTimeout(state.notificationTimer)) || null,
}))
}));
@ -211,7 +226,7 @@ const useImportJSON = ((set, get) => ({
get().fillExtCrateData(crate.id);
});
get()._updateTotalOrderPrice();
get().showNotification(get().active_crate, null);
get().showNotification(get().active_crate, null, false);
},
updateImportDescription: (new_description) => set(state => ({
importValue: {
@ -328,15 +343,17 @@ const useSubmitForm = ((set, get) => ({
const useHighlighted = ((set, get) => ({
highlighted: {
crate: "",
card: 0
card: 0,
horizontal: false
},
highlightedTimer: null,
// #!if disable_card_highlight === false
highlightCard: (crate_id, index) => set(state => ({
highlightCard: (crate_id, index, horizontal) => set(state => ({
highlighted: {
crate: crate_id,
card: index
card: index,
horizontal: horizontal
},
highlightedTimer: (!!state.highlightedTimer ? clearTimeout(state.highlightedTimer) : null) || (state.isTouch && setTimeout(() => {
get().highlightReset()
@ -345,7 +362,8 @@ const useHighlighted = ((set, get) => ({
highlightReset: () => set(state => ({
highlighted: {
crate: "",
card: 0
card: 0,
horizontal: false
},
highlightedTimer: !!state.highlightedTimer ? clearTimeout(state.highlightedTimer) : null
})),
@ -384,17 +402,18 @@ const useCart = ((set, get) => ({
})
})),
setActiveCrate: (id) => set(state => ({active_crate: id})),
_addCardFromCatalog: (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]));
_addCardFromCatalog: (crate_to, index_from, index_to, horizontal) => set(state => {
const whichH = whichItems(horizontal)
const take_from = toArray(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) {
index_to = index_to != null ? index_to : crate.items.length;
index_to = index_to != null ? index_to : crate[whichH].length;
return {
...crate,
items: crate.items.toSpliced(index_to, 0, ...take_from.map((card_name, _) => {
[whichH]: crate[whichH].toSpliced(index_to, 0, ...take_from.map((card_name, _) => {
return {...state.cards[card_name], id: uuidv4()}
}))
}
@ -402,39 +421,41 @@ const useCart = ((set, get) => ({
})
}
}),
_moveCard: (crate_from, index_from, crate_to, index_to) => set(state => {
const the_card = state.crates.find((crate, _) => crate_from === crate.id ).items[index_from];
_moveCard: (crate_from, index_from, crate_to, index_to, horizontal) => set(state => {
const whichH = whichItems(horizontal)
const the_card = state.crates.find((crate, _) => crate_from === crate.id )[whichH][index_from];
return {
crates: state.crates.map((crate, _i) => {
if (crate_to === crate_from && crate_to === crate.id) {
let items_copy = Array.from(crate.items);
let items_copy = Array.from(crate[whichH]);
let item = items_copy.splice(index_from, 1)[0]
items_copy.splice(index_to, 0, item).filter((item, _) => !!item)
return {
...crate,
items: items_copy
[whichH]: items_copy
}
} else if (crate_to === crate.id) {
return {
...crate,
items: crate.items.toSpliced(index_to, 0, the_card)
[whichH]: crate[whichH].toSpliced(index_to, 0, the_card)
}
} else if (crate_from === crate.id) {
return {
...crate,
items: crate.items.toSpliced(index_to, 1)
[whichH]: crate[whichH].toSpliced(index_to, 1)
}
}
else return crate;
})
}
}),
_deleteCard: (crate_id, index) => set(state => ({
_deleteCard: (crate_id, index, horizontal) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate_id === crate.id) {
const whichH = whichItems(horizontal)
return {
...crate,
items: crate.items.toSpliced(index, 1)
[whichH]: crate[whichH].toSpliced(index, 1)
}
}
else return crate;
@ -445,7 +466,8 @@ const useCart = ((set, get) => ({
if (id === crate.id) {
return {
...crate,
items: []
items: [],
h_items: []
}
}
else return crate;
@ -454,10 +476,11 @@ const useCart = ((set, get) => ({
clearAll: () => set(state => ({
crates: state._defaultCrates
})),
_updateOptions: (crate_id, index, new_options) => set(state => ({
_updateOptions: (crate_id, index, new_options, horizontal) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate_id === crate.id) {
let itemsCopy = Array.from(crate.items);
const whichH = whichItems(horizontal)
let itemsCopy = Array.from(crate[whichH]);
itemsCopy[index] = {
...itemsCopy[index],
options_data: {
@ -466,7 +489,7 @@ const useCart = ((set, get) => ({
}};
return {
...crate,
items: itemsCopy
[whichH]: itemsCopy
}
}
else return crate;
@ -476,7 +499,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 ---")
// Warnings for horizontal items are not available
let itemsCopy = Array.from(crate.items);
const disabled = !!get().crateParams(crate.crate_mode).warnings_disabled;
itemsCopy = FillResources(itemsCopy, disabled);
@ -493,20 +516,23 @@ const useCart = ((set, get) => ({
})
})),
fillExtData: (crate_id) => set(state => ({
fillExtData: (crate_id, horizontal) => set(state => ({
crates: state.crates.map((crate, _i) => {
// horizontal items do not interact with each other for now
if (crate_id === crate.id) {
let itemsCopy = Array.from(crate.items);
const whichH = whichItems(horizontal)
const options_name = state.crateParams(crate.crate_mode).options;
let itemsCopy = Array.from(crate[whichH]);
itemsCopy = itemsCopy.map((item, index) => {
if (!item.options) return item;
if (!item[options_name]) return item;
if (!item.options_data) item.options_data = {};
item.options_data.ext_data = FillExtCardData(itemsCopy, index);
return item;
});
return {
...crate,
items: Array.from(itemsCopy)
[whichH]: Array.from(itemsCopy)
}
}
else return crate;
@ -519,7 +545,7 @@ const useCart = ((set, get) => ({
sum += get().crate_modes[crate.crate_mode].price;
const crate_options = ProcessOptionsToData({options: get().crate_prices, data: crate.options_data || {}});
sum += crate_options ? crate_options.reduce((accumulator, currentValue) => accumulator+currentValue.price, 0) : 0;
crate.items.forEach((item, _) => {
crate.items.concat(crate.h_items).forEach((item, _) => {
sum += item.price;
});
});
@ -534,6 +560,7 @@ const useCart = ((set, get) => ({
const crate_id = "crate" + get().crates.length;
get()._newCrate(crate_id)
get().fillExtData(crate_id);
get().fillExtData(crate_id, true);
get().fillExtCrateData(crate_id);
get().fillOrderExtData();
get().fillWarnings(crate_id);
@ -543,6 +570,7 @@ const useCart = ((set, get) => ({
setCrateMode: (id, mode) => {
get()._setCrateMode(id, mode)
get().fillExtData(id);
get().fillExtData(id, true);
get().fillExtCrateData(id);
get().fillOrderExtData();
get().fillWarnings(id);
@ -557,15 +585,18 @@ const useCart = ((set, get) => ({
},
addCardFromCatalog: (crate_to, index_from, index_to, just_mounted) => {
const dest = crate_to || get().active_crate;
const isCrateless = toArray(index_from).some(value => get().getCardDescription(value).crateless === true);
const [isHorizontal, crateTo] = crate_to ? unwrapCrateId(crate_to) : [false, crate_to];
const isHorizontalOnly = toArray(index_from).some(value => !!get().getCardDescription(value).horizontal);
const dest = isCrateless ? "spare" : crateTo || get().active_crate;
if (!dest) {
console.warn("No destination");
get().noDestinationWarning();
return {};
}
get().showNotification(dest, index_to);
get()._addCardFromCatalog(dest, index_from, index_to)
get().fillExtData(dest);
get().showNotification(dest, index_to, isHorizontalOnly);
get()._addCardFromCatalog(dest, index_from, index_to, isHorizontalOnly)
get().fillExtData(dest, isHorizontalOnly);
get().fillWarnings(dest);
get().setActiveCrate(dest);
get()._updateTotalOrderPrice();
@ -575,22 +606,25 @@ const useCart = ((set, 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().setActiveCrate(crate_to);
const [isHorizontal, crateFrom] = unwrapCrateId(crate_from)
const [_, crateTo] = unwrapCrateId(crate_to)
get()._moveCard(crateFrom, index_from, crateTo, index_to, isHorizontal);
get().fillExtData(crateTo, isHorizontal);
get().fillWarnings(crateTo);
get().setActiveCrate(crateTo);
get()._updateTotalOrderPrice();
if (crate_from !== crate_to) {
get().fillExtData(crate_from);
get().fillWarnings(crate_from);
if (crateFrom !== crate_to) {
get().fillExtData(crateFrom, isHorizontal);
get().fillWarnings(crateFrom);
}
},
deleteCard: (crate_id, index) => {
get()._deleteCard(crate_id, index);
get().fillExtData(crate_id);
get().fillWarnings(crate_id);
deleteCard: (crate_id, index, horizontal) => {
const [isHorizontal, crateId] = horizontal ? [horizontal, crate_id] : unwrapCrateId(crate_id);
get()._deleteCard(crateId, index, isHorizontal);
get().fillExtData(crateId, isHorizontal);
get().fillWarnings(crateId);
get()._updateTotalOrderPrice();
if (crate_id === get().highlighted.crate && index === get().highlighted.card) get().highlightReset()
if (crateId === get().highlighted.crate && index === get().highlighted.card) get().highlightReset()
},
clearCrate: (id) => {
get()._clearCrate(id);
@ -598,16 +632,19 @@ const useCart = ((set, get) => ({
get()._updateTotalOrderPrice();
},
updateOptions: (crate_id, index, new_options) => {
get()._updateOptions(crate_id, index, new_options);
get().fillExtData(crate_id);
get().fillWarnings(crate_id);
updateOptions: (crate_id, index, new_options, horizontal) => {
get()._updateOptions(crate_id, index, new_options, horizontal);
get().fillExtData(crate_id, horizontal);
if (!horizontal) {
get().fillWarnings(crate_id);
}
},
initExtData: () => {
get().fillOrderExtData();
get().crates.forEach((crate, _i) => {
get().fillExtData(crate.id);
get().fillExtData(crate.id, true);
get().fillExtData(crate.id, false);
get().fillExtCrateData(crate.id);
})
get()._updateTotalOrderPrice();

View File

@ -6,20 +6,23 @@ const shop_data = {
id: 'rack',
name: 'Rack mountable crate',
price: 550,
hp: 84
hp: 84,
options: "options"
},
desktop: {
id: 'desktop',
name: 'Desktop crate',
price: 500,
hp: 42
hp: 42,
options: "options"
},
no_crate: {
id: 'no_crate',
name: 'Spare cards',
name: 'Spare items',
price: 0,
hp: -1,
warnings_disabled: true
warnings_disabled: true,
options: "crateless_options"
}
},
crateModeOrder: [
@ -901,6 +904,9 @@ const shop_data = {
warnings: [
"no_idc_source"
],
resources: [
{name: "idc", max: 4}
],
consumes: {
hp: 4,
idc: 4
@ -1111,10 +1117,24 @@ const shop_data = {
validator: {name: "ipv4or6"},
fallback: {text: "DHCP", checked: false},
tip: "Set up IP address used by the device"}},
{type: "SwitchLine", args: {title: "MQTT broker address", outvar: "broker",
validator: {name: "ipv4OrHost"},
fallback: {text: "mqtt", checked: false},
tip: "Set up domain name or IPv4 of the MQTT broker"}},
{type: "Switch", args: {title: "Ext power", outvar: "ext_pwr", "tip": "Use external power supply in order to reduce number of used EEM connectors"}},
{type: "Switch", args: {title: "Term #0", outvar: "term_0", tip: "Enable termination on ADC channel #0"}},
{type: "Switch", args: {title: "Term #1", outvar: "term_1", tip: "Enable termination on ADC channel #1"}}
],
crateless_options: [
{type: "SwitchLine", args: {title: "IP", outvar: "ip",
validator: {name: "ipv4or6"},
fallback: {text: "DHCP", checked: false},
tip: "Set up IP address used by the device"}},
{type: "SwitchLine", args: {title: "MQTT broker address", outvar: "broker",
validator: {name: "ipv4OrHost"},
fallback: {text: "mqtt", checked: false},
tip: "Set up domain name or IPv4 of the MQTT broker"}},
],
options_class: "stabilizer",
size: 'small',
warnings: [
@ -1145,10 +1165,24 @@ const shop_data = {
validator: {name: "ipv4or6"},
fallback: {text: "DHCP", checked: false},
tip: "Set up IP address used by the device"}},
{type: "SwitchLine", args: {title: "MQTT broker address", outvar: "broker",
validator: {name: "ipv4OrHost"},
fallback: {text: "mqtt", checked: false},
tip: "Set up domain name or IPv4 of the MQTT broker"}},
{type: "Switch", args: {title: "Ext power", outvar: "ext_pwr", "tip": "Use external power supply in order to reduce number of used EEM connectors"}},
{type: "Switch", args: {title: "Term #0", outvar: "term_0", tip: "Enable termination on ADC channel #0"}},
{type: "Switch", args: {title: "Term #1", outvar: "term_1", tip: "Enable termination on ADC channel #1"}}
],
crateless_options: [
{type: "SwitchLine", args: {title: "IP", outvar: "ip",
validator: {name: "ipv4or6"},
fallback: {text: "DHCP", checked: false},
tip: "Set up IP address used by the device"}},
{type: "SwitchLine", args: {title: "MQTT broker address", outvar: "broker",
validator: {name: "ipv4OrHost"},
fallback: {text: "mqtt", checked: false},
tip: "Set up domain name or IPv4 of the MQTT broker"}},
],
options_class: "stabilizer",
size: 'small',
warnings: [
@ -1236,7 +1270,25 @@ const shop_data = {
'100Base-T Ethernet with PoE.'
],
options: [
{type: "Switch", args: {title: "Ext power", outvar: "ext_pwr", "tip": "Use external power supply in order to reduce number of used EEM connectors"}}
{type: "Switch", args: {title: "Ext power", outvar: "ext_pwr", "tip": "Use external power supply in order to reduce number of used EEM connectors"}},
{type: "SwitchLine", args: {title: "Static IPv4", outvar: "ip",
validator: {name: "ipv4"},
fallback: {text: "0.0.0.0", checked: false},
tip: "Set up static IPv4 address used by the device"}},
{type: "SwitchLine", args: {title: "MQTT broker address", outvar: "broker",
validator: {name: "ipv4OrHost"},
fallback: {text: "mqtt", checked: false},
tip: "Set up domain name or IPv4 of the MQTT broker"}},
],
crateless_options: [
{type: "SwitchLine", args: {title: "Static IPv4", outvar: "ip",
validator: {name: "ipv4"},
fallback: {text: "0.0.0.0", checked: false},
tip: "Set up static IPv4 address used by the device"}},
{type: "SwitchLine", args: {title: "MQTT broker address", outvar: "broker",
validator: {name: "ipv4OrHost"},
fallback: {text: "mqtt", checked: false},
tip: "Set up domain name or IPv4 of the MQTT broker"}},
],
size: 'small',
warnings: [
@ -1261,6 +1313,12 @@ const shop_data = {
'100Base-T Ethernet with PoE.',
'Can stabilize temperature of Sinara 5432 DAC or external devices containing TEC and thermistor.'
],
crateless_options: [
{type: "SwitchLine", args: {title: "IPv4", outvar: "ip",
validator: {name: "ipv4"},
fallback: {text: "192.168.1.26/24", checked: false},
tip: "Set up IP address used by the device"}},
],
size: 'small',
consumes: {
hp: 4
@ -1312,12 +1370,26 @@ const shop_data = {
validator: {name: "ipv4or6"},
fallback: {text: "DHCP", checked: false},
tip: "Set up IP address used by the device"}},
{type: "SwitchLine", args: {title: "MQTT broker address", outvar: "broker",
validator: {name: "ipv4OrHost"},
fallback: {text: "mqtt", checked: false},
tip: "Set up domain name or IPv4 of the MQTT broker"}},
{type: "Switch", args: {title: "Ext power", outvar: "ext_pwr", "tip": "Use external power supply in order to reduce number of used EEM connectors"}},
{type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk",
fallback: {text: "125 MHz", checked: false}, validator: {name: "frequency", params: {min: 10e6, max: 1e9}}}},
{type: "Switch", args: {title: "Termination #0", outvar: "term_0", tip: "Enable termination on ADC channel #0"}},
{type: "Switch", args: {title: "Termination #1", outvar: "term_1", tip: "Enable termination on ADC channel #1"}}
],
crateless_options: [
{type: "SwitchLine", args: {title: "Static IPv4", outvar: "ip",
validator: {name: "ipv4"},
fallback: {text: "0.0.0.0", checked: false},
tip: "Set up static IPv4 address used by the device"}},
{type: "SwitchLine", args: {title: "MQTT broker address", outvar: "broker",
validator: {name: "ipv4OrHost"},
fallback: {text: "mqtt", checked: false},
tip: "Set up domain name or IPv4 of the MQTT broker"}},
],
size: 'big',
warnings: [
"no_eem_source",
@ -1384,6 +1456,27 @@ const shop_data = {
hp: 8,
},
},
'afws': {
id: 'afws',
name: 'Subscription',
name_number: 'AFWS',
name_codename: '',
price: 800,
image: null,
horizontal: true,
specs: [
"Artiq Firmware Service for one variant for one year.",
"Includes support at helpdesk.",
"Included with purchase of any Carrier with no additional cost.",
],
crateless: true,
crateless_options: [
{type: "Line", args: {title: "Variant name", outvar: "variant_name", fallback: "",
tip: "Variant name can be found on the sticker on top of the crate. If you don't have one, leave the preferred name here."}},
],
size: 'big',
warnings: [],
},
},
columns: {
@ -1441,6 +1534,7 @@ const shop_data = {
'koster',
'eem_pwr_mod',
'kirdy',
'afws',
]}
],
},
@ -1450,14 +1544,16 @@ const shop_data = {
crate_mode: "rack",
fan_tray: false,
items: [],
h_items: [],
warnings: [],
occupiedHP: 0,
},
{
id: "spare",
name: "Spare cards",
name: "Spare items",
crate_mode: "no_crate",
items: [],
h_items: [],
warnings: [],
occupiedHP: 0,
}

View File

@ -35,42 +35,6 @@
<div class="row">
<div class="col-12">
<div class="card shadow mt-3 mb-3" itemscope itemtype="https://schema.org/Poster">
<div class="card-body p-3 p-md-5 card-featured">
<div class="col-12 col-md-6 ps-0 pe-0">
<h5 class="card-title" itemprop="headline">News</h5>
<div class="desc-wrapper" itemprop="description">
<p class="card-text pt-3">
The Sinara 1550 Kirdy is a laser diode driver which combines a low-noise current source and a precision temperature controller. In combination with Fast-Servo, it can lock lasers to spectral lines and reference cavities. See <a href="docs/kirdybrochure.pdf">the brochure</a>.
</p>
<p class="card-text pt-3">
The world's <a href="https://www.ox.ac.uk/news/2025-02-06-first-distributed-quantum-algorithm-brings-quantum-supercomputers-closer">first distributed quantum algorithm experiment</a> was performed at Oxford University using ARTIQ. See <a href="https://www.nature.com/articles/s41586-024-08404-x">the paper</a>.
</p>
<p class="card-text pt-3">
The Sinara 5716 Shuttler is a 16-channel 125MSPS 14-bit DAC optimized for trapped ion shuttling. More information in the <a href="https://forum.m-labs.hk/d/745-artiq-8-released">ARTIQ-8 release announcement</a>.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row pb-5">
<div class="col-12">
<div class="card shadow mt-3 mb-3" itemscope itemtype="https://schema.org/Poster">