1
0
Fork 0

Compare commits

..

11 Commits

Author SHA1 Message Date
Egor Savkin 608f253684 WIP add horizontal items into the cart
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-11-01 17:32:20 +08:00
Egor Savkin 66bb5b25d8 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>
2024-11-01 12:39:30 +08:00
Egor Savkin 34a86e24c5 Add AFWS item to the shop and update bundle
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-10-28 13:36:33 +08:00
Egor Savkin 175e625cc6 Fix exception on thermostat2ch and build the bundle
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-10-28 13:36:33 +08:00
Egor Savkin 49882c7af2 Update cards
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-10-28 13:36:33 +08:00
Egor Savkin efd6b12c83 Add possibility for crateless options
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-10-28 13:36:33 +08:00
Egor Savkin 18bd98a204 Automatically open spare cards for crateless items
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-10-28 13:36:33 +08:00
Egor Savkin f29807a913 Optimize the download button
Replace download button with windows icon (just four squares).
Make the download button last so on certain screens it wouldn't cause additional empty-ish lines.
Redo the wording so the button is shorter, but the dropdown menu is filled.

Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-10-21 17:46:40 +08:00
Egor Savkin 9d9a4c9f5a Split dropdown
On main click it downloads stable, in dropdown there are two choices - stable and beta. Similar to the downloads on jetbrains website

Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-10-21 15:32:19 +08:00
Egor Savkin 92f7428ac8 Replace download button with dropdown
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-10-18 12:29:19 +08:00
Egor Savkin a163a269b9 Update NUC
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-10-17 11:10:40 +08:00
15 changed files with 483 additions and 200 deletions

View File

@ -90,8 +90,9 @@ a {
} }
} }
.navbar-light .navbar-nav .nav-link, .navbar {
.dropdown-item { .navbar-light .navbar-nav .nav-link,
.dropdown-item {
outline: none; outline: none;
color: $color-primary; color: $color-primary;
text-decoration: none; text-decoration: none;
@ -103,15 +104,38 @@ a {
&:hover { &:hover {
color: $color-secondary; color: $color-secondary;
} }
} }
.navbar-light .navbar-nav .active>.nav-link, .navbar-light .navbar-nav .active>.nav-link,
.navbar-light .navbar-nav .nav-link.active, .navbar-light .navbar-nav .nav-link.active,
.navbar-light .navbar-nav .nav-link.show, .navbar-light .navbar-nav .nav-link.show,
.navbar-light .navbar-nav .show>.nav-link { .navbar-light .navbar-nav .show>.nav-link {
color: $color-secondary; color: $color-secondary;
} }
.dropdown-menu {
border: none;
}
.dropdown-item {
&:hover,
&:active {
background-color: transparent;
}
}
.dropdown-item.active {
color: $color-secondary;
background-color: transparent;
}
.navbar-toggler {
outline: none;
&:focus,
&:hover {
outline: none;
}
}
}
/** /**
@ -127,22 +151,6 @@ a {
border: 1px solid transparent; border: 1px solid transparent;
} }
.dropdown-menu {
border: none;
margin-top: 0;
padding-top: 0;
}
.dropdown-item {
&:hover,
&:active {
background-color: transparent;
}
}
.dropdown-item.active {
color: $color-secondary;
background-color: transparent;
}
.btn-primary { .btn-primary {
background-color: $btn-primary-2; background-color: $btn-primary-2;
color: #fff !important; color: #fff !important;
@ -155,18 +163,11 @@ a {
border: 1px solid $btn-secondary-2 !important; border: 1px solid $btn-secondary-2 !important;
} }
} }
.btn-lg { .btn-lg {
font-size: 1rem; font-size: 1rem;
padding: 1rem 1.25rem; padding: 1rem 1.25rem;
} }
.navbar-toggler {
outline: none;
&:focus,
&:hover {
outline: none;
}
}
ul.th { ul.th {
list-style: none; list-style: none;
@ -200,6 +201,63 @@ ul:not(.navbar-nav) li {
} }
.download-selector {
display: inline-flex;
.divider {
border-right: solid 1px white;
margin: 0.125rem 0;
height: inherit;
z-index: 10;
}
.dropdown-menu {
border: none;
margin-top: 0;
padding-top: 0;
}
.btn {
background-color: $btn-primary-2;
&:hover {
background-color: $btn-secondary-2;
}
&:after {
align-self: center;
}
}
button {
&[aria-expanded='true']:after {
transform: rotate(-180deg);
}
span {
margin-right: 0.5rem;
}
}
ul {
list-style: none;
margin-left: 0!important;
width: 100%;
padding: 0;
li {
padding: 0;
margin: 0;
a {
padding: 0.75rem 0.5rem 0.75rem 1.25rem;
}
}
li::before {
content: none;
display: none;
}
}
}
.bg-white-shadow { .bg-white-shadow {
background: url(../images/migen-links@2x.png); background: url(../images/migen-links@2x.png);
background-repeat: no-repeat; background-repeat: no-repeat;

View File

@ -485,6 +485,12 @@ button {
padding: 5px 5px 12px; padding: 5px 5px 12px;
position: relative; position: relative;
&.horizontal {
flex-direction: column;
min-height: 50px;
overflow-y: auto;
}
> div { > div {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -571,67 +577,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 { .hovered {
@ -778,3 +723,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;
}
}

View File

@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 12.7 12.7" xmlns="http://www.w3.org/2000/svg"><g fill="#fff" stroke-linecap="round" stroke-opacity=".6" stroke-width=".35269"><path d="m0.52917 0.52917h5.5563v5.5563h-5.5563z"/><path d="m6.6146 0.52917h5.5563v5.5563h-5.5563z"/><path d="m0.52917 6.6146h5.5563v5.5563h-5.5563z"/><path d="m6.6146 6.6146h5.5563v5.5563h-5.5563z"/></g></svg>

After

Width:  |  Height:  |  Size: 392 B

View File

@ -32,8 +32,6 @@ export function Cart({crate_index}) {
<ProductCartItem <ProductCartItem
card_index={index} card_index={index}
crate_index={crate_index} crate_index={crate_index}
first={index === 0}
last={index === crate.items.length - 1 && nbrOccupied >= nbrSlots}
key={item.id}/> key={item.id}/>
); );
}); });

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

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

View File

@ -3,12 +3,11 @@ import React from "react";
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
import {SummaryPopup} from "./options/SummaryPopup"; import {SummaryPopup} from "./options/SummaryPopup";
export function OptionsDialogWrapper({crate_index, card_index, first, last}) { export function OptionsDialogWrapper({crate_index, card_index, horizontal}) {
const crate_id = useShopStore((state) => state.crates[crate_index].id); const crate_id = useShopStore((state) => state.crates[crate_index].id);
const use_options = useShopStore((state) => state.crateParams(state.crates[crate_index].crate_mode).options); const use_options = useShopStore((state) => state.crateParams(state.crates[crate_index].crate_mode).options);
const options = useShopStore((state) => state.crates[crate_index].items[card_index][use_options]); const options = useShopStore((state) => state.crates[crate_index].items[card_index][use_options]);
const options_data = useShopStore((state) => state.crates[crate_index].items[card_index].options_data); 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 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 options_class = useShopStore((state) => state.crates[crate_index].items[card_index].options_class);
const sideMenuIsOpen = useShopStore((state) => state.sideMenuIsOpen); const sideMenuIsOpen = useShopStore((state) => state.sideMenuIsOpen);
@ -27,12 +26,10 @@ export function OptionsDialogWrapper({crate_index, card_index, first, last}) {
options_class={options_class} options_class={options_class}
key={"popover" + crate_id +card_id} key={"popover" + crate_id +card_id}
id={"popover"+ crate_id + card_id} id={"popover"+ crate_id + card_id}
big={card_size === "big"}
first={first}
last={last}
sideMenuIsOpen={sideMenuIsOpen} sideMenuIsOpen={sideMenuIsOpen}
onHideNotification={hideNotification} onHideNotification={hideNotification}
displayNotification={displayNotification} displayNotification={displayNotification}
horizontal={horizontal}
target={{ target={{
construct: ((outvar, value) => { construct: ((outvar, value) => {
// #!options_log // #!options_log

View File

@ -13,7 +13,7 @@ import {useRenderCount} from "@uidotdev/usehooks";
* Component that renders a product. * Component that renders a product.
* Used in the crate * Used in the crate
*/ */
export function ProductCartItem({card_index, crate_index, first, last}) { export function ProductCartItem({card_index, crate_index}) {
// #!render_count // #!render_count
const renderCount = useRenderCount(); const renderCount = useRenderCount();
@ -73,8 +73,6 @@ export function ProductCartItem({card_index, crate_index, first, last}) {
<OptionsDialogWrapper <OptionsDialogWrapper
crate_index={crate_index} crate_index={crate_index}
card_index={card_index} card_index={card_index}
first={first}
last={last}
/> />
)} )}
</div> </div>

View File

@ -0,0 +1,107 @@
import React from 'react'
import {Draggable} from "@hello-pangea/dnd";
import {compareObjectsEmptiness, productStyle} from "./utils";
import {Resources} from "./Resources";
import {CardWarnings} from "./CardWarnings";
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 card_show_warnings = useShopStore(state => state.crates[crate_index].h_items[card_index].show_warnings, compareObjectsEmptiness);
const card_counted_resources = useShopStore(state => state.crates[crate_index].h_items[card_index].counted_resources, compareObjectsEmptiness);
const highlighted = useShopStore((state) => 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);
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;
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}>
{(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)}
onMouseLeave={removeHighlight}
>
{/* warning container */}
<div className="progress-container warning d-flex justify-content-evenly">
{warnings &&
(<CardWarnings crate_index={crate_index} card_index={card_index} />)
}
{options && (
<OptionsDialogWrapper
crate_index={crate_index}
card_index={card_index}
horizontal={true}
/>
)}
</div>
<h6>{card.name_number}</h6>
<div
onMouseEnter={() => setHighlight(crate_id, card_index)}
onClick={() => setHighlight(crate_id, card_index)}
>
{card.name}
</div>
{/* remove container */}
{/*<div
style={{'display': highlighted ? 'flex' : 'none'}}
className="overlayRemove"
onClick={() => onCardRemove(crate_id, card_index)}>
<img src="/images/shop/icon-remove.svg" alt="rm"/>
<p>Remove</p>
</div>*/}
</div>
)}
</Draggable>
);
}

View File

@ -8,7 +8,7 @@ import {OptionsSummaryWrapper} from "./OptionsWrapper";
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
export function SummaryCrateCard({crate_index, card_index}) { export function SummaryCrateCard({crate_index, card_index, horizontal}) {
// #!render_count // #!render_count
const renderCount = useRenderCount(); const renderCount = useRenderCount();
@ -19,10 +19,16 @@ export function SummaryCrateCard({crate_index, card_index}) {
const highlighted = useShopStore((state) => state.crates[crate_index].id === state.highlighted.crate && card_index === state.highlighted.card); const highlighted = useShopStore((state) => state.crates[crate_index].id === state.highlighted.crate && card_index === state.highlighted.card);
const crate_id = useShopStore((state) => state.crates[crate_index].id); const crate_id = useShopStore((state) => state.crates[crate_index].id);
const card = useShopStore((state) => state.crates[crate_index].items[card_index], const card = useShopStore((state) =>
horizontal ? state.crates[crate_index].h_items[card_index] : state.crates[crate_index].items[card_index],
(a, b) => a.id === b.id); (a, b) => a.id === b.id);
const card_show_warnings = useShopStore(state => state.crates[crate_index].items[card_index].show_warnings, compareObjectsEmptiness); // additional hooks for updating warning and options
const card_options_data = useShopStore(state => state.crates[crate_index].items[card_index].options_data, compareObjectsEmptiness); const card_show_warnings = useShopStore(state =>
horizontal ? state.crates[crate_index].h_items[card_index].show_warnings : state.crates[crate_index].items[card_index].show_warnings,
compareObjectsEmptiness);
const card_options_data = useShopStore(state =>
horizontal ? state.crates[crate_index].h_items[card_index].options_data : state.crates[crate_index].items[card_index].options_data,
compareObjectsEmptiness);
const warnings_disabled = useShopStore((state) => !!state.crateParams(state.crates[crate_index].crate_mode).warnings_disabled); 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 use_options = useShopStore((state) => state.crateParams(state.crates[crate_index].crate_mode).options);
@ -38,8 +44,8 @@ export function SummaryCrateCard({crate_index, card_index}) {
<tr <tr
key={"summary_crate_" + crate_id + "_" + card_index} key={"summary_crate_" + crate_id + "_" + card_index}
className={`hoverable ${highlighted ? 'selected' : ''}`} className={`hoverable ${highlighted ? 'selected' : ''}`}
onClick={() => setHighlight(crate_id, card_index)} onClick={() => setHighlight(crate_id, card_index, horizontal)}
onMouseEnter={() => setHighlight(crate_id, card_index)} onMouseEnter={() => setHighlight(crate_id, card_index, horizontal)}
onMouseLeave={() => resetHighlight()}> onMouseLeave={() => resetHighlight()}>
<td className="item-card-name tabbed"> <td className="item-card-name tabbed">
<div>{`${card.name_number} ${card.name} ${card.name_codename}`}</div> <div>{`${card.name_number} ${card.name} ${card.name_codename}`}</div>

View File

@ -21,10 +21,13 @@ export function validateJSON(description) {
try { try {
for (const crate of crates_raw) { 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) { for (const card of crate.items) {
if (!(card.pn in pn_to_card) || card.options === undefined) return false; if (!(card.pn in pn_to_card) || card.options === undefined) return false;
} }
for (const card of crate.h_items) {
if (!(card.pn in pn_to_card) || card.options === undefined) return false;
}
} }
} catch (e) { } catch (e) {
return false; return false;
@ -50,6 +53,11 @@ export function JSONToCrates(description) {
id: uuidv4(), id: uuidv4(),
options_data: card.options || {} 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: [], warnings: [],
occupiedHP: 0, occupiedHP: 0,
}))); })));
@ -73,6 +81,10 @@ export function CratesToJSON(crates) {
pn: card.name_number, 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 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, type: crate.crate_mode,
options: FilterOptions(crateOptions, crate.options_data) options: FilterOptions(crateOptions, crate.options_data)
}))), }))),

View File

@ -1,24 +1,20 @@
import React, {useState} from "react"; import React, {useRef, useState} from "react";
import {useClickAway} from "./useClickAway";
import {ProcessOptions} from "./Options"; import {ProcessOptions} from "./Options";
import {Notification} from "./Notification"; import {Notification} from "./Notification";
import {Overlay} from "react-bootstrap";
export function DialogPopup({options, data, target, id, big, first, last, options_class, export function DialogPopup({options, data, target, id, options_class,
sideMenuIsOpen, displayNotification, onHideNotification}) { sideMenuIsOpen, displayNotification, onHideNotification, horizontal}) {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const ref = useClickAway((e) => { const ref = useRef(null);
if (e.type === "mousedown") // ignore touchstart
setShow(false) 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) => { const handleClick = (_event) => {
setShow(!show); setShow(!show);
}; };
return ( return (<>
<div ref={ref}>
<Notification <Notification
id={"processed_options_notification" + id} id={"processed_options_notification" + id}
tip="Customization options available" tip="Customization options available"
@ -26,12 +22,25 @@ export function DialogPopup({options, data, target, id, big, first, last, option
show={displayNotification} show={displayNotification}
onHide={onHideNotification} onHide={onHideNotification}
content={ content={
<img className="alert-info" <img className="alert-info" ref={ref}
src={show ? "/images/shop/icon-close.svg" : "/images/shop/icon-customize.svg"} src={show ? "/images/shop/icon-close.svg" : "/images/shop/icon-customize.svg"}
onClick={handleClick}/> onClick={handleClick}/>
} }
/> />
<div style={{'display': show ? 'flex' : 'none'}} className={div_classes}> <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 <ProcessOptions
options={options} options={options}
data={data} data={data}
@ -40,7 +49,8 @@ export function DialogPopup({options, data, target, id, big, first, last, option
target={target} target={target}
/> />
</div> </div>
</div> )}
); </Overlay>
</>);
} }

View File

@ -13,6 +13,8 @@ import {ProcessOptionsToData} from "./options/Options";
import {DomainedRFQMessages} from "./Domained"; import {DomainedRFQMessages} from "./Domained";
export const HORIZONTAL_CART_MARKER = "_h";
const cards_to_pn_map = (cards) => { const cards_to_pn_map = (cards) => {
let result = {}; let result = {};
Object.entries(cards).forEach(([key, card], _i) => { result[card.name_number] = key}) Object.entries(cards).forEach(([key, card], _i) => { result[card.name_number] = key})
@ -23,6 +25,12 @@ const toArray = (arg) => {
return Array.isArray(arg) ? arg : [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]
}
const whichItems = (horizontal = false) => horizontal ? "h_items" : "items"
const useCatalog = ((set, get) => ({ const useCatalog = ((set, get) => ({
cards: shared_data.items, cards: shared_data.items,
groups: shared_data.columns.catalog, groups: shared_data.columns.catalog,
@ -37,21 +45,24 @@ const useCatalog = ((set, get) => ({
const useOptionsNotification = ((set, get) => ({ const useOptionsNotification = ((set, get) => ({
notificationCrateId: null, notificationCrateId: null,
notificationCardIndex: null, notificationCardIndex: null,
notificationHorizontal: false,
notificationTimer: null, notificationTimer: null,
_showNotification: (crate_id, card_index) => set(state => ({ _showNotification: (crate_id, card_index, horizontal) => set(state => ({
notificationCrateId: crate_id, notificationCrateId: crate_id,
notificationCardIndex: card_index, notificationCardIndex: card_index,
notificationHorizontal: horizontal,
notificationTimer: setTimeout(() => { notificationTimer: setTimeout(() => {
state.hideNotification() state.hideNotification()
}, 5000) }, 5000)
})), })),
showNotification: (crate_id, card_index) => { showNotification: (crate_id, card_index, horizontal) => {
get().hideNotification() get().hideNotification()
setTimeout(() => get()._showNotification(crate_id, card_index), 100); setTimeout(() => get()._showNotification(crate_id, card_index, horizontal), 100);
}, },
hideNotification: () => set(state => ({ hideNotification: () => set(state => ({
notificationCrateId: null, notificationCrateId: null,
notificationCardIndex: null, notificationCardIndex: null,
notificationHorizontal: false,
notificationTimer: (state.notificationTimer && clearTimeout(state.notificationTimer)) || null, notificationTimer: (state.notificationTimer && clearTimeout(state.notificationTimer)) || null,
})) }))
})); }));
@ -215,7 +226,7 @@ const useImportJSON = ((set, get) => ({
get().fillExtCrateData(crate.id); get().fillExtCrateData(crate.id);
}); });
get()._updateTotalOrderPrice(); get()._updateTotalOrderPrice();
get().showNotification(get().active_crate, null); get().showNotification(get().active_crate, null, false);
}, },
updateImportDescription: (new_description) => set(state => ({ updateImportDescription: (new_description) => set(state => ({
importValue: { importValue: {
@ -332,15 +343,17 @@ const useSubmitForm = ((set, get) => ({
const useHighlighted = ((set, get) => ({ const useHighlighted = ((set, get) => ({
highlighted: { highlighted: {
crate: "", crate: "",
card: 0 card: 0,
horizontal: false
}, },
highlightedTimer: null, highlightedTimer: null,
// #!if disable_card_highlight === false // #!if disable_card_highlight === false
highlightCard: (crate_id, index) => set(state => ({ highlightCard: (crate_id, index, horizontal) => set(state => ({
highlighted: { highlighted: {
crate: crate_id, crate: crate_id,
card: index card: index,
horizontal: horizontal
}, },
highlightedTimer: (!!state.highlightedTimer ? clearTimeout(state.highlightedTimer) : null) || (state.isTouch && setTimeout(() => { highlightedTimer: (!!state.highlightedTimer ? clearTimeout(state.highlightedTimer) : null) || (state.isTouch && setTimeout(() => {
get().highlightReset() get().highlightReset()
@ -349,7 +362,8 @@ const useHighlighted = ((set, get) => ({
highlightReset: () => set(state => ({ highlightReset: () => set(state => ({
highlighted: { highlighted: {
crate: "", crate: "",
card: 0 card: 0,
horizontal: false
}, },
highlightedTimer: !!state.highlightedTimer ? clearTimeout(state.highlightedTimer) : null highlightedTimer: !!state.highlightedTimer ? clearTimeout(state.highlightedTimer) : null
})), })),
@ -388,17 +402,18 @@ const useCart = ((set, get) => ({
}) })
})), })),
setActiveCrate: (id) => set(state => ({active_crate: id})), setActiveCrate: (id) => set(state => ({active_crate: id})),
_addCardFromCatalog: (crate_to, index_from, index_to) => set(state => { _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 take_from = toArray(index_from).map((item, _i) => (state.cards_list[item]));
const dest = crate_to || state.active_crate; const dest = crate_to || state.active_crate;
if (!dest) return {}; if (!dest) return {};
return { return {
crates: state.crates.map((crate, _i) => { crates: state.crates.map((crate, _i) => {
if (dest === crate.id) { 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 { return {
...crate, ...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()} return {...state.cards[card_name], id: uuidv4()}
})) }))
} }
@ -406,39 +421,41 @@ const useCart = ((set, get) => ({
}) })
} }
}), }),
_moveCard: (crate_from, index_from, crate_to, index_to) => set(state => { _moveCard: (crate_from, index_from, crate_to, index_to, horizontal) => set(state => {
const the_card = state.crates.find((crate, _) => crate_from === crate.id ).items[index_from]; const whichH = whichItems(horizontal)
const the_card = state.crates.find((crate, _) => crate_from === crate.id )[whichH][index_from];
return { return {
crates: state.crates.map((crate, _i) => { crates: state.crates.map((crate, _i) => {
if (crate_to === crate_from && crate_to === crate.id) { if (crate_to === crate_from && crate_to === crate.id) {
let items_copy = Array.from(crate.items); let items_copy = Array.from(crate[whichH]);
let item = items_copy.splice(index_from, 1)[0] let item = items_copy.splice(index_from, 1)[0]
items_copy.splice(index_to, 0, item).filter((item, _) => !!item) items_copy.splice(index_to, 0, item).filter((item, _) => !!item)
return { return {
...crate, ...crate,
items: items_copy [whichH]: items_copy
} }
} else if (crate_to === crate.id) { } else if (crate_to === crate.id) {
return { return {
...crate, ...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) { } else if (crate_from === crate.id) {
return { return {
...crate, ...crate,
items: crate.items.toSpliced(index_to, 1) [whichH]: crate[whichH].toSpliced(index_to, 1)
} }
} }
else return crate; else return crate;
}) })
} }
}), }),
_deleteCard: (crate_id, index) => set(state => ({ _deleteCard: (crate_id, index, horizontal) => set(state => ({
crates: state.crates.map((crate, _i) => { crates: state.crates.map((crate, _i) => {
if (crate_id === crate.id) { if (crate_id === crate.id) {
const whichH = whichItems(horizontal)
return { return {
...crate, ...crate,
items: crate.items.toSpliced(index, 1) [whichH]: crate[whichH].toSpliced(index, 1)
} }
} }
else return crate; else return crate;
@ -449,7 +466,8 @@ const useCart = ((set, get) => ({
if (id === crate.id) { if (id === crate.id) {
return { return {
...crate, ...crate,
items: [] items: [],
h_items: []
} }
} }
else return crate; else return crate;
@ -458,10 +476,11 @@ const useCart = ((set, get) => ({
clearAll: () => set(state => ({ clearAll: () => set(state => ({
crates: state._defaultCrates 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) => { crates: state.crates.map((crate, _i) => {
if (crate_id === crate.id) { 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] = {
...itemsCopy[index], ...itemsCopy[index],
options_data: { options_data: {
@ -470,7 +489,7 @@ const useCart = ((set, get) => ({
}}; }};
return { return {
...crate, ...crate,
items: itemsCopy [whichH]: itemsCopy
} }
} }
else return crate; else return crate;
@ -524,7 +543,7 @@ const useCart = ((set, get) => ({
sum += get().crate_modes[crate.crate_mode].price; sum += get().crate_modes[crate.crate_mode].price;
const crate_options = ProcessOptionsToData({options: get().crate_prices, data: crate.options_data || {}}); 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; 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; sum += item.price;
}); });
}); });
@ -563,13 +582,14 @@ const useCart = ((set, get) => ({
addCardFromCatalog: (crate_to, index_from, index_to, just_mounted) => { addCardFromCatalog: (crate_to, index_from, index_to, just_mounted) => {
const isCrateless = toArray(index_from).some(value => get().getCardDescription(value).crateless === true); const isCrateless = toArray(index_from).some(value => get().getCardDescription(value).crateless === true);
const isHorizontal = toArray(index_from).some(value => !get().getCardDescription(value).image);
const dest = isCrateless ? "spare" : crate_to || get().active_crate; const dest = isCrateless ? "spare" : crate_to || get().active_crate;
if (!dest) { if (!dest) {
console.warn("No destination"); console.warn("No destination");
get().noDestinationWarning(); get().noDestinationWarning();
return {}; return {};
} }
get().showNotification(dest, index_to); get().showNotification(dest, index_to, isHorizontal);
get()._addCardFromCatalog(dest, index_from, index_to) get()._addCardFromCatalog(dest, index_from, index_to)
get().fillExtData(dest); get().fillExtData(dest);
get().fillWarnings(dest); get().fillWarnings(dest);
@ -581,22 +601,25 @@ const useCart = ((set, get) => ({
}, },
moveCard: (crate_from, index_from, crate_to, index_to) => { moveCard: (crate_from, index_from, crate_to, index_to) => {
get()._moveCard(crate_from, index_from, crate_to, index_to); const [isHorizontal, crateFrom] = unwrapCrateId(crate_from)
get().fillExtData(crate_to); const [_, crateTo] = unwrapCrateId(crate_to)
get().fillWarnings(crate_to); get()._moveCard(crateFrom, index_from, crateTo, index_to, isHorizontal);
get().setActiveCrate(crate_to); get().fillExtData(crateTo);
get().fillWarnings(crateTo);
get().setActiveCrate(crateTo);
get()._updateTotalOrderPrice(); get()._updateTotalOrderPrice();
if (crate_from !== crate_to) { if (crateFrom !== crate_to) {
get().fillExtData(crate_from); get().fillExtData(crateFrom);
get().fillWarnings(crate_from); get().fillWarnings(crateFrom);
} }
}, },
deleteCard: (crate_id, index) => { deleteCard: (crate_id, index) => {
get()._deleteCard(crate_id, index); const [isHorizontal, crateId] = unwrapCrateId(crate_id);
get().fillExtData(crate_id); get()._deleteCard(crateId, index, isHorizontal);
get().fillWarnings(crate_id); get().fillExtData(crateId);
get().fillWarnings(crateId);
get()._updateTotalOrderPrice(); 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) => { clearCrate: (id) => {
get()._clearCrate(id); get()._clearCrate(id);
@ -605,7 +628,8 @@ const useCart = ((set, get) => ({
}, },
updateOptions: (crate_id, index, new_options) => { updateOptions: (crate_id, index, new_options) => {
get()._updateOptions(crate_id, index, new_options); const [isHorizontal, crateId] = unwrapCrateId(crate_id);
get()._updateOptions(crate_id, index, new_options, isHorizontal);
get().fillExtData(crate_id); get().fillExtData(crate_id);
get().fillWarnings(crate_id); get().fillWarnings(crate_id);
}, },

View File

@ -54,8 +54,8 @@ const shop_data = {
title: "Optional pre-installed NUC mini-computer", title: "Optional pre-installed NUC mini-computer",
outvar: "nuc", outvar: "nuc",
tip: "Pre-installed NixOS desktop with ARTIQ and other scientific software. " + tip: "Pre-installed NixOS desktop with ARTIQ and other scientific software. " +
"Hardware: Intel® NUC 13 Pro Kit NUC13ANKi7, i7-1360P CPU, " + "Hardware: ASUS® NUC 14 Pro Kit, Intel® i7-155H CPU, " +
"32GB RAM, 1TB NVMe. Other options contact us.", "32GB RAM, 1TB NVMe. For other options contact us.",
fallback: true, fallback: true,
}}, }},
{ {
@ -1449,7 +1449,7 @@ const shop_data = {
name_number: 'AFWS', name_number: 'AFWS',
name_codename: '', name_codename: '',
price: 800, price: 800,
image: '/images/shop/graphic-03_AFWS.svg', image: null,
specs: [ specs: [
"Artiq Firmware Service for one variant for one year.", "Artiq Firmware Service for one variant for one year.",
"Includes support at helpdesk.", "Includes support at helpdesk.",
@ -1530,6 +1530,7 @@ const shop_data = {
crate_mode: "rack", crate_mode: "rack",
fan_tray: false, fan_tray: false,
items: [], items: [],
h_items: [],
warnings: [], warnings: [],
occupiedHP: 0, occupiedHP: 0,
}, },
@ -1538,6 +1539,7 @@ const shop_data = {
name: "Spare items", name: "Spare items",
crate_mode: "no_crate", crate_mode: "no_crate",
items: [], items: [],
h_items: [],
warnings: [], warnings: [],
occupiedHP: 0, occupiedHP: 0,
} }

View File

@ -53,12 +53,29 @@
<a href="{{ get_url(path='@/experiment-control/place-order.md') }}" class="btn btn-primary btn-inversed btn-lg">Order hardware</a> <a href="{{ get_url(path='@/experiment-control/place-order.md') }}" class="btn btn-primary btn-inversed btn-lg">Order hardware</a>
<a href="https://m-labs.hk/artiq/manual/" class="btn btn-primary btn-lg">Manual</a> <a href="https://m-labs.hk/artiq/manual/" class="btn btn-primary btn-lg">Manual</a>
<a href="https://forum.m-labs.hk" class="btn btn-primary btn-lg">Forum</a> <a href="https://forum.m-labs.hk" class="btn btn-primary btn-lg">Forum</a>
<a href="https://nixbld.m-labs.hk/job/artiq/extra/msys2-offline-installer/latest/download/1" class="btn btn-primary btn-lg d-inline-flex">
<img src="/images/icons/icon-download.svg" class="d-inline-block align-self-center mx-1" style="height: 1rem" alt="download">
Windows installer
</a>
<a href="{{ get_url(path='@/experiment-control/artiq.md') }}" class="btn btn-primary btn-lg">More...</a> <a href="{{ get_url(path='@/experiment-control/artiq.md') }}" class="btn btn-primary btn-lg">More...</a>
<div class="btn-group download-selector">
<a class="btn btn-primary btn-lg d-inline-flex" href="https://nixbld.m-labs.hk/job/artiq/extra/msys2-offline-installer/latest/download/1">
<img src="/images/icons/icon-windows.svg" class="d-inline-block align-self-center mx-1" style="height: 1rem" alt="windows">
<span class="d-inline-block">Download <sup>.exe</sup></span>
</a>
<div class="divider"></div>
<button type="button" class="btn btn-primary btn-lg dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
<span>stable</span>
</button>
<ul class="dropdown-menu shadow dropdown-menu-end">
<li>
<a class="dropdown-item" href="https://nixbld.m-labs.hk/job/artiq/extra/msys2-offline-installer/latest/download/1">
ARTIQ-8 (stable) for Windows
</a>
</li>
<li>
<a class="dropdown-item" href="https://nixbld.m-labs.hk/job/artiq/extra-beta/msys2-offline-installer/latest/download/1">
ARTIQ-9 (beta) for Windows
</a>
</li>
</ul>
</div>
</div> </div>
</div> </div>