Compare commits
No commits in common. "master" and "109-demangle-emails" have entirely different histories.
master
...
109-demang
|
@ -18,10 +18,8 @@ Start:
|
|||
zola serve
|
||||
```
|
||||
|
||||
To build the .bundle.js from .jsx files:
|
||||
To update the .bundle.js and .jsx file:
|
||||
|
||||
```
|
||||
nix-shell -p nodejs
|
||||
npm install
|
||||
npm run build
|
||||
nix-shell -p nodejs --run "npm run build"
|
||||
```
|
||||
|
|
|
@ -73,7 +73,7 @@ We post specific vacancies on job boards such as Indeed.com, but if you think yo
|
|||
|
||||
{% layout_centered_content(min_width=true) %}
|
||||
|
||||
Our main office is located in the center of Hong Kong, a cosmopolitan city with world-class infrastructure, many cultural and social events, and <a href="https://www.discoverhongkong.com/eng/see-do/great-outdoors/hikes/index.jsp" rel="noopener noreferrer" target="_blank">beautiful natural scenery</a>. It has a separate system from mainland China, where, for example, communications are unrestricted, taxes are low, and customs tariffs virtually inexistent. Hong Kong is located next to Shenzhen, a city with <a href="https://www.wired.com/video/watch/shenzhen-the-silicon-valley-of-hardware-full-documentary-future-cities-wired" rel="noopener noreferrer" target="_blank">a bustling tech scene</a>, and where many of the world's electronic gadgets are designed and manufactured.
|
||||
Our main office is located in the center of Hong Kong, a cosmopolitan city with world-class infrastructure, many cultural and social events, and <a href="https://www.discoverhongkong.com/eng/see-do/great-outdoors/hikes/index.jsp" rel="noopener noreferrer" target="_blank">beautiful natural scenery</a>. It has a separate system from mainland China, where, for example, communications are unrestricted, taxes are low, and customs tariffs virtually inexistent. Hong Kong is located next to Shenzhen, a city with <a href="https://www.wired.co.uk/video/shenzhen-full-documentary" rel="noopener noreferrer" target="_blank">a bustling tech scene</a>, and where many of the world's electronic gadgets are designed and manufactured.
|
||||
|
||||
We also have a location in Manila, Philippines.
|
||||
|
||||
|
|
|
@ -184,12 +184,6 @@ We welcome inquiries from research groups of all sizes.<br>[See what has been fu
|
|||
{% 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">Repository</a>
|
||||
{% end %}
|
||||
|
||||
{% layout_card(title="flake8-artiq", sameheight=120) %}
|
||||
<small>A Flake8 plugin for checking ARTIQ code</small>
|
||||
|
||||
|
|
|
@ -12,9 +12,9 @@ title = "Sinara hardware"
|
|||
|
||||
The first ARTIQ core devices used hardware built in-house by physicists (based on a Xilinx KC705 development board with custom FMC cards). To improve the quality, features and scalability of ARTIQ systems, we have been developing the Sinara device family. It provides turnkey control hardware that is reproducible, open, flexible, modular, well-tested, and well-supported by the ARTIQ control software.
|
||||
|
||||
The Sinara hardware is in active development, and the latest information is available <a href="https://github.com/sinara-hw" target="_blank" rel="noopener noreferrer">on the wiki of each project's page</a>. Most of the hardware engineering is done at the <a href="https://www.ise.pw.edu.pl/" target="_blank" rel="noopener noreferrer">Institute for Electronics Systems</a> at the Warsaw University of Technology.
|
||||
The Sinara hardware is in active development, and the latest information is available <a href="https://github.com/sinara-hw" target="_blank" rel="noopener noreferrer">on the wiki of each project's page</a>. Most of the hardware engineering is done at the <a href="http://www.ise.pw.edu.pl/" target="_blank" rel="noopener noreferrer">Institute for Electronics Systems</a> at the Warsaw University of Technology.
|
||||
|
||||
Kasli and EEMs can be ordered now. We can deliver a rack-mountable crate that contains all the cards, is fully tested, and is ready to be connected to your experiment and computer network. Use our [web-based configuration and ordering tool](../place-order), or contact <a href="mailto:sales@m-labs.hk">sales@m-labs.hk</a> with your requirements, and we will establish a quote.
|
||||
Kasli and EEMs can be ordered now. We can deliver a rack-mountable crate that contains all the cards, is fully tested, and is ready to be connected to your experiment and computer network. Contact <a href="mailto:sales@m-labs.hk">sales@m-labs.hk</a> with your requirements and we will establish a quote.
|
||||
|
||||
{% end %}
|
||||
|
||||
|
@ -293,11 +293,11 @@ The 4624 AWG "Phaser" is a quad channel 1.25 GS/s RF generator card with dual IQ
|
|||
|
||||
{% layout_centered_content(min_width=true, css="row d-flex align-items-center mt-5") %}
|
||||
|
||||
##### Ordering from M-Labs is easy and quick
|
||||
##### Kasli and EEMs can be ordered now
|
||||
|
||||
We can deliver a rack-mountable crate that contains all the cards, is fully tested, and is ready to be connected to your experiment and computer network. The lead time can be as short as a few working days and we will provide assistance to help you set up your new equipment with ARTIQ via the online helpdesk. Using our AFWS tool, you can keep the firmware of your M-Labs devices up-to-date easily, and benefit from the new features we continuously develop into ARTIQ.
|
||||
We can deliver a rack-mountable crate that contains all the cards, is fully tested, and is ready to be connected to your experiment and computer network.
|
||||
|
||||
Use our [web-based configuration and ordering tool](../place-order), or contact <a href="mailto:sales@m-labs.hk">sales@m-labs.hk</a> with your requirements, and we will establish a quote.
|
||||
Contact <a href="mailto:sales@m-labs.hk">sales@m-labs.hk</a> with your requirements and we will establish a quote.
|
||||
|
||||
{% end %}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ The Milkymist One is programmable and customizable at many different levels.
|
|||
|
||||
The simplest way one can use a Milkymist One is by affecting MIDI controls to the existing snippets of code (called "patches") that create the effects. The Milkymist One ships with dozens of pre-existing patches.
|
||||
|
||||
Creating new patches can be done with a simple programming language based on the <a href="https://web.archive.org/web/20130825023759/http://www.milkdrop.co.uk/guide.htm" target="_blank" rel="noopener noreferrer">MilkDrop preset format</a>. The Milkymist One device comes with a built-in editor.
|
||||
Creating new patches can be done with a simple programming language based on the <a href="http://www.milkdrop.co.uk/guide.htm" target="_blank" rel="noopener noreferrer">MilkDrop preset format</a>. The Milkymist One device comes with a built-in editor.
|
||||
|
||||
Under the hood, the Milkymist One is like a mini-computer running our Flickernoise video synthesis software.
|
||||
|
||||
|
@ -50,10 +50,10 @@ But we did not stop at open source <em>software</em>. As a matter of fact, when
|
|||
##### Press
|
||||
|
||||
- Create Digital Motion (10/02/2012): <a href="http://createdigitalmotion.com/2012/02/milkymist-is-digital-visual-synthesizer-and-processor-built-on-sophisticated-open-source-hardware/">Milkymist is Digital Visual Synthesizer and Processor, Built as Sophisticated Open Source Hardware</a>
|
||||
- MikroBitti (03/2012): <a href="https://web.archive.org/web/20140226181735/http://www.mbnet.fi/artikkeli/lehti/avointa_vj_rautaa_3_2012">Milkymist One -visualisaattori: Avointa vj-rautaa</a> [FI]
|
||||
- MikroBitti (03/2012): <a href="http://www.mbnet.fi/artikkeli/lehti/avointa_vj_rautaa_3_2012">Milkymist One -visualisaattori: Avointa vj-rautaa</a> [FI]
|
||||
- Make Magazine (30/09/2011): <a href="http://blog.makezine.com/2011/09/30/milkymist-one-an-open-source-vj-console-goes-on-sale/">Milkymist One, an Open Source VJ Console, Goes on Sale</a>
|
||||
- The Register (28/09/2011): <a href="http://www.theregister.co.uk/2011/09/28/milkymist/print.html">Open-source hardware group puts out vid system-on-a-chip</a>
|
||||
- Theory&Practice (22/09/2011): <a href="https://web.archive.org/web/20160409033154/http://theoryandpractice.ru/seminars/19402-videosintezator-s-otkrytym-kodom-milkymist-one-22-9">Видеосинтезатор с открытым кодом Milkymist One</a> [RU]
|
||||
- Theory&Practice (22/09/2011): <a href="http://theoryandpractice.ru/seminars/19402-videosintezator-s-otkrytym-kodom-milkymist-one-22-9">Видеосинтезатор с открытым кодом Milkymist One</a> [RU]
|
||||
- ETN (17/05/2011): <a href="http://www.etn.se/index.php?option=com_content&view=article&id=53785">Videoeffekter i öppen hårdvara söker partners</a> [SE]
|
||||
- Linux-Magazin (28/01/2011): <a href="http://www.linux-magazin.de/NEWS/VJ-System-Milkymist-als-Entwicklerboard-erhaeltlich">VJ-System Milkymist als Entwicklerboard erhältlich</a> [DE]
|
||||
- Create Digital Motion (16/08/2010): <a href="http://createdigitalmotion.com/2010/08/milkymist-one-all-in-one-open-source-vj-workstation/">Milkymist One, All-in-One Open Source VJ Workstation</a>
|
||||
|
@ -154,17 +154,17 @@ Milkymist SoC is phased out in favor of the more powerful [MiSoC](../migen).
|
|||
|
||||
##### Flickernoise
|
||||
|
||||
Flickernoise is the video synthesis application developed for the Milkymist One and the Milkymist SoC. It renders hardware-accelerated visual effects comparable (and, to some extent, compatible) with those of <a href="https://www.geisswerks.com/milkdrop/">MilkDrop</a> 1.x, the popular audio visualization plug-in for Winamp.
|
||||
Flickernoise is the video synthesis application developed for the Milkymist One and the Milkymist SoC. It renders hardware-accelerated visual effects comparable (and, to some extent, compatible) with those of <a href="http://www.nullsoft.com/free/milkdrop">MilkDrop</a> 1.x, the popular audio visualization plug-in for Winamp.
|
||||
|
||||
Flickernoise allows the creation of visual patches and their connection with all the interfaces that the Milkymist One provides thanks to a built-in graphical user interface. Let the visuals react to sound and MIDI events, connect a camera and create live phantasmagoric images of yourself...
|
||||
|
||||
Flickernoise uses:
|
||||
|
||||
- The <a href="http://www.rtems.org">RTEMS</a> real-time operating system.
|
||||
- The <a href="https://web.archive.org/web/20151025081338/http://www.yaffs.org/">YAFFS</a> flash filesystem (modified version <a href="http://www.github.com/m-labs/rtems-yaffs2">here</a>).
|
||||
- The <a href="http://www.yaffs.org">YAFFS</a> flash filesystem (modified version <a href="http://www.github.com/m-labs/rtems-yaffs2">here</a>).
|
||||
- The <a href="http://www.libpng.org">libpng</a>, <a href="http://www.ijg.org">libjpeg</a>, <a href="http://www.ijg.org">openjpeg</a> and <a href="http://jbig2dec.sourceforge.net/">jbig2dec</a> image decompression libraries.
|
||||
- The <a href="http://www.freetype.org">freetype</a> font rendering system.
|
||||
- The <a href="https://www.mupdf.com">MuPDF</a> library for the online help system.
|
||||
- The <a href="http://www.mupdf.com">MuPDF</a> library for the online help system.
|
||||
- <a href="http://www.github.com/m-labs/mtk">MTK</a>, a modified version of the <a href="http://www.genode-labs.com/products/fpga-graphics">Genode FX</a> embedded GUI toolkit, which provides all the elements for common user interaction (windows, buttons, etc.).
|
||||
- <a href="http://www.github.com/m-labs/liboscparse">liboscparse</a>, a variant of <a href="http://liblo.sourceforge.net/">liblo</a>, for <a href="http://www.opensoundcontrol.org">OpenSoundControl</a> communications.
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ You can find the Migen source <a href="http://github.com/m-labs/migen" target="_
|
|||
- <a href="https://www.wdj-consulting.com/blog/migen-port.html" target="_blank" rel="noopener noreferrer">Tutorial "Porting a New Board To Migen"</a> by cr1901
|
||||
- <a href="https://lab.whitequark.org/notes/2016-10-18/implementing-an-uart-in-verilog-and-migen/" target="_blank" rel="noopener noreferrer">"Implementing a UART in Verilog and Migen"</a> by whitequark
|
||||
- <a href="https://lab.whitequark.org/notes/2016-10-19/implementing-a-simple-soc-in-migen/" target="_blank" rel="noopener noreferrer">"Implementing a simple SoC in Migen"</a> by whitequark
|
||||
- <a href="https://web.archive.org/web/20210416131215/http://blog.lambdaconcept.com/doku.php?id=migen:tutorial" target="_blank" rel="noopener noreferrer">Migen Step by Step Tutorial</a> by LambdaConcept
|
||||
- <a href="http://blog.lambdaconcept.com/doku.php?id=migen:tutorial" target="_blank" rel="noopener noreferrer">Migen Step by Step Tutorial</a> by LambdaConcept
|
||||
|
||||
{% end %}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ nMigen itself provides the core language, and is complemented by a number of ext
|
|||
|
||||
##### Documentation
|
||||
|
||||
Documentation for nMigen and its external components is rather limited at the moment. However, you can follow the <a href="https://web.archive.org/web/20210518042504/http://blog.lambdaconcept.com/doku.php?id=nmigen:tutorial" target="_blank" rel="noopener noreferrer">tutorial by LambdaConcept</a>, and another <a href="https://github.com/RobertBaruch/nmigen-tutorial" target="_blank" rel="noopener noreferrer">tutorial by Robert Baruch</a>, as well as his video series on building a 6800 CPU on an FPGA with nMigen: <a href="https://www.youtube.com/watch?v=85ZCTuekjGA" target="_blank" rel="noopener noreferrer">part 1</a> <a href="https://www.youtube.com/watch?v=AQOXoKQhG3I" target="_blank" rel="noopener noreferrer">part 2</a> <a href="https://www.youtube.com/watch?v=aLQqOxnVMOQ" target="_blank" rel="noopener noreferrer">part 3</a> <a href="https://www.youtube.com/watch?v=xqMtyCu4lME" target="_blank" rel="noopener noreferrer">part 4</a>.
|
||||
Documentation for nMigen and its external components is rather limited at the moment. However, you can follow the <a href="http://blog.lambdaconcept.com/doku.php?id=nmigen:tutorial" target="_blank" rel="noopener noreferrer">tutorial by LambdaConcept</a>, and another <a href="https://github.com/RobertBaruch/nmigen-tutorial" target="_blank" rel="noopener noreferrer">tutorial by Robert Baruch</a>, as well as his video series on building a 6800 CPU on an FPGA with nMigen: <a href="https://www.youtube.com/watch?v=85ZCTuekjGA" target="_blank" rel="noopener noreferrer">part 1</a> <a href="https://www.youtube.com/watch?v=AQOXoKQhG3I" target="_blank" rel="noopener noreferrer">part 2</a> <a href="https://www.youtube.com/watch?v=aLQqOxnVMOQ" target="_blank" rel="noopener noreferrer">part 3</a> <a href="https://www.youtube.com/watch?v=xqMtyCu4lME" target="_blank" rel="noopener noreferrer">part 4</a>.
|
||||
|
||||
{% end %}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"name": "m-labs-zola",
|
||||
"sideEffects": false,
|
||||
"version": "1.0.0",
|
||||
"description": "These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. For deployment, see the nix-scripts repository. Commits to https://git.m-labs.hk/M-Labs/web2019.git are automatically deployed to m-labs.hk through Hydra.",
|
||||
"scripts": {
|
||||
|
@ -13,38 +12,29 @@
|
|||
"url": "https://git.m-labs.hk/M-Labs/web2019.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.23.9",
|
||||
"@babel/core": "^7.24.0",
|
||||
"@babel/preset-env": "^7.24.0",
|
||||
"@babel/preset-react": "^7.23.3",
|
||||
"@babel/cli": "^7.23.0",
|
||||
"@babel/core": "^7.23.2",
|
||||
"@babel/preset-env": "^7.23.2",
|
||||
"@babel/preset-react": "^7.22.15",
|
||||
"babel-loader": "^9.1.3",
|
||||
"babel-preset-minify": "^0.5.2",
|
||||
"bootstrap": "^5.3.3",
|
||||
"jquery": "^3.7.1",
|
||||
"bootstrap": "^5.3.0",
|
||||
"jquery": "^3.7.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.10.2",
|
||||
"@hello-pangea/dnd": "^16.5.0",
|
||||
"react-bootstrap": "^2.9.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"uuid": "^9.0.1",
|
||||
"webpack": "^5.90.3",
|
||||
"webpack": "^5.89.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"json-logic-js": "^2.0.2",
|
||||
"zustand": "^4.5.2",
|
||||
"@uidotdev/usehooks":"^2.4.1",
|
||||
"webpack-preprocessor-loader": "^1.3.0"
|
||||
"@uidotdev/usehooks": "^2.4.1"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"@babel/preset-react",
|
||||
["@babel/preset-env", {
|
||||
"targets": {
|
||||
"browsers": [
|
||||
">0.25%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
]
|
||||
}
|
||||
}]
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-react"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -149,7 +149,7 @@ a {
|
|||
border: 1px solid $btn-primary-2 !important;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover, &:disabled {
|
||||
&:hover {
|
||||
background-color: $btn-secondary-2;
|
||||
border: 1px solid $btn-secondary-2 !important;
|
||||
}
|
||||
|
|
|
@ -8,62 +8,51 @@ button {
|
|||
outline: none!important;
|
||||
}
|
||||
|
||||
.rfqFeedback {
|
||||
/* -webkit-box-shadow: 0px 0px 33px -7px rgba(0,0,0,0.75);
|
||||
-moz-box-shadow: 0px 0px 33px -7px rgba(0,0,0,0.75);
|
||||
box-shadow: 0px 0px 33px -7px rgba(0,0,0,0.75);*/
|
||||
border: 1px solid $brand-color;
|
||||
.modal-body, .modal-content, .modal {
|
||||
border-radius: 0;
|
||||
}
|
||||
.form-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-align: center;
|
||||
|
||||
/*position: absolute;
|
||||
width: 350px;*/
|
||||
background: white;
|
||||
/*left: calc(100%/2 - 350px/2);*/
|
||||
|
||||
/*top: calc(50% - 50px);*/
|
||||
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: inherit;
|
||||
align-self: center;
|
||||
border: 0;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
|
||||
img {
|
||||
width: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn.btn-primary.disabled {
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
.btn-outline-primary {
|
||||
color: $btn-primary-2;
|
||||
border-color: $btn-primary-2;
|
||||
background-color: inherit;
|
||||
&:hover {
|
||||
background-color: inherit;
|
||||
color: $btn-secondary-2;
|
||||
border-color: $btn-secondary-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#root-shop {
|
||||
|
||||
.layout {
|
||||
|
||||
.rfqFeedback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2rem 3rem;
|
||||
text-align: center;
|
||||
/*position: absolute;
|
||||
width: 350px;*/
|
||||
background: white;
|
||||
/*left: calc(100%/2 - 350px/2);*/
|
||||
-webkit-box-shadow: 0px 0px 33px -7px rgba(0,0,0,0.75);
|
||||
-moz-box-shadow: 0px 0px 33px -7px rgba(0,0,0,0.75);
|
||||
box-shadow: 0px 0px 33px -7px rgba(0,0,0,0.75);
|
||||
/*top: calc(50% - 50px);*/
|
||||
border: 1px solid $brand-color;
|
||||
font-size: .9rem;
|
||||
|
||||
button {
|
||||
background-color: inherit;
|
||||
align-self: center;
|
||||
border: 0;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
|
||||
img {
|
||||
width: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn.btn-primary.disabled {
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
.btn-outline-primary,
|
||||
.btn-outline-primary:hover {
|
||||
color: $btn-primary-2;
|
||||
border-color: $btn-primary-2;
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
display: flex;
|
||||
|
||||
> aside.aside {
|
||||
|
@ -72,32 +61,26 @@ button {
|
|||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
max-width: max(1/4 * 100%, 310px);
|
||||
scrollbar-width: thin;
|
||||
scrollbar-gutter: stable;
|
||||
scrollbar-color: #6e7e87 transparent;
|
||||
scrollbar-arrow-color: transparent;
|
||||
/*padding-bottom: 4rem!important;*/
|
||||
}
|
||||
|
||||
.gradient-bottom {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
height: 100px;
|
||||
//width: max(1/4 * 100%, 310px);
|
||||
width: inherit;
|
||||
content: "";
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
rgba(13, 53, 71, 1),
|
||||
rgba(13, 53, 71, 0)
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
> aside.aside:after {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
height: 100px;
|
||||
width: calc(2 / 6 * 100%);
|
||||
content: "";
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
rgba(13, 53, 71, 1),
|
||||
rgba(13, 53, 71, 0)
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> section.main {
|
||||
flex: 4;
|
||||
width: calc(3/4 * 100%);
|
||||
max-width: calc(4 / 6 * 100%);
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +89,7 @@ button {
|
|||
|
||||
display: flex;
|
||||
color: white;
|
||||
padding: 1rem 0rem 1rem 1.5rem;
|
||||
padding: 3rem 2rem 1rem;
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
|
@ -125,7 +108,6 @@ button {
|
|||
|
||||
h3 {
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
|
@ -183,50 +165,8 @@ button {
|
|||
}
|
||||
}
|
||||
|
||||
.catalog-container {
|
||||
.backlog-container {
|
||||
padding-bottom: 4rem;
|
||||
|
||||
.catalog-bar {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin: 13px 0 0.4rem 0;
|
||||
padding: 0 0.5rem;
|
||||
justify-content: space-around;
|
||||
|
||||
.mobileCloseMenu {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 10%;
|
||||
align-content: center;
|
||||
}
|
||||
.search-catalog {
|
||||
display: inline-block;
|
||||
border: 0;
|
||||
width: 90%;
|
||||
|
||||
.search-catalog-input {
|
||||
display: inline;
|
||||
border: 0;
|
||||
color: white;
|
||||
border-radius: 2rem;
|
||||
background: rgba(255, 255, 255, 0.15) url("/images/shop/icon-search.svg") no-repeat;
|
||||
background-position: left 2% center;
|
||||
background-size: 20px;
|
||||
padding-right: 1rem;
|
||||
text-indent: 20px;
|
||||
|
||||
&::placeholder {
|
||||
color: white;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rule {
|
||||
|
@ -246,25 +186,37 @@ button {
|
|||
padding-bottom: .5rem;
|
||||
}
|
||||
|
||||
.btn-outline-primary {
|
||||
.btn-outline-primary,
|
||||
.btn-outline-primary:hover {
|
||||
color: $btn-primary-2;
|
||||
border-color: $btn-primary-2;
|
||||
background-color: inherit;
|
||||
&:hover {
|
||||
background-color: inherit;
|
||||
color: $btn-secondary-2;
|
||||
border-color: $btn-secondary-2;
|
||||
}
|
||||
}
|
||||
|
||||
.control {
|
||||
display: flex;
|
||||
font-size: .8rem;
|
||||
|
||||
> .description {
|
||||
width: 80%;
|
||||
> p {
|
||||
width: 50%;
|
||||
padding-right: 30px;
|
||||
}
|
||||
.crate-mode {
|
||||
text-align: right;
|
||||
width: 50%;
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
margin-right: 1rem;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
a.active {
|
||||
font-weight: 700;
|
||||
border-bottom: 3px solid $btn-primary-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.summary {
|
||||
|
@ -290,9 +242,6 @@ button {
|
|||
|
||||
.item-card-name {
|
||||
font-weight: 700;
|
||||
&.tabbed {
|
||||
padding-left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.price {
|
||||
|
@ -367,19 +316,13 @@ button {
|
|||
button {
|
||||
background-color: inherit;
|
||||
border: 0;
|
||||
margin-left: 16px;
|
||||
margin-left: 20px;
|
||||
|
||||
img {
|
||||
width: 20px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
span {
|
||||
width: 28px;
|
||||
}
|
||||
.span-with-margin {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
> .summary-form {
|
||||
|
@ -404,7 +347,7 @@ button {
|
|||
outline: none;
|
||||
}
|
||||
|
||||
.order-form-submit,
|
||||
input[type="submit"],
|
||||
.btn-cla {
|
||||
/*background-color: $btn-primary-2;*/
|
||||
font-weight: 700;
|
||||
|
@ -423,39 +366,16 @@ button {
|
|||
border: 1px solid #e53e3e !important;
|
||||
}
|
||||
|
||||
.btn-outline-primary {
|
||||
.btn-outline-primary,
|
||||
.btn-outline-primary:hover {
|
||||
color: $btn-primary-2;
|
||||
border-color: $btn-primary-2;
|
||||
background-color: inherit;
|
||||
&:hover {
|
||||
background-color: inherit;
|
||||
color: $btn-secondary-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.order-bar {
|
||||
width: 90%;
|
||||
font-size: 0.9rem;
|
||||
padding: 0;
|
||||
input[type="text"] {
|
||||
padding: 0;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.options-group {
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.shop-radio-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.crate {
|
||||
position: relative;
|
||||
|
||||
|
@ -635,43 +555,6 @@ button {
|
|||
.crate-info {
|
||||
padding: 1rem 0 0;
|
||||
}
|
||||
|
||||
.crate-bar {
|
||||
width: 100%;
|
||||
font-size: 0.9rem;
|
||||
|
||||
.crate-mode {
|
||||
text-align: left;
|
||||
width: 75%;
|
||||
display: inline;
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
margin-right: 1rem;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
padding-bottom: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
a.active {
|
||||
font-weight: 700;
|
||||
border-bottom: 3px solid $btn-primary-2;
|
||||
}
|
||||
}
|
||||
.delete-crate {
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
display: inline-flex;
|
||||
width: 25%;
|
||||
color: inherit;
|
||||
img {
|
||||
margin-left: 1rem;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -704,37 +587,6 @@ button {
|
|||
margin-left: -10px;
|
||||
}
|
||||
|
||||
#accordion_crates {
|
||||
background-color: inherit;
|
||||
.accordion_crates_item {
|
||||
.accordion-header {
|
||||
padding-bottom: 0;
|
||||
|
||||
}
|
||||
.accordion-button {
|
||||
background-color: inherit;
|
||||
font-weight: bold;
|
||||
&:hover {
|
||||
background-color: $color-highlight;
|
||||
}
|
||||
}
|
||||
}
|
||||
#accordion_crates_add {
|
||||
.accordion-header {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.accordion-button {
|
||||
font-weight: bold;
|
||||
&:hover {
|
||||
background-color: $color-highlight;
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-button:after {
|
||||
background-image: url("/images/shop/icon-add.svg");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.k-popup-connectors,
|
||||
|
@ -743,13 +595,17 @@ button {
|
|||
color: white;
|
||||
font-weight: 700;
|
||||
font-size: .6rem;
|
||||
padding: .5rem .8rem;
|
||||
padding: .8rem 1rem;
|
||||
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);
|
||||
text-align: left;
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
p + p {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.k-popup-connectors {
|
||||
|
|
|
@ -24,8 +24,7 @@ $btn-secondary-2: #a88cfd !default;
|
|||
$link-primary-dark: #c2affd !default;
|
||||
$link-secondary-dark: #cec2ea !default;
|
||||
|
||||
$color-hover: #eae7f7 !default;
|
||||
$color-highlight: #dfe9ff !default;
|
||||
$color-hover: #eae7f7 !default;
|
||||
|
||||
// Import partials.
|
||||
@import
|
||||
|
|
|
@ -10,18 +10,10 @@ from flask import request
|
|||
from flask_mail import Mail
|
||||
from flask_mail import Message
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
from jinja2.utils import htmlsafe_json_dumps
|
||||
|
||||
|
||||
load_dotenv()
|
||||
|
||||
mail_password_file = getenv("FLASK_MAIL_PASSWORD_FILE")
|
||||
if mail_password_file is not None:
|
||||
with open(mail_password_file, "r") as f:
|
||||
mail_password = f.read().strip()
|
||||
else:
|
||||
mail_password = None
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.update(
|
||||
DEBUG=getenv("FLASK_DEBUG") == "True",
|
||||
|
@ -30,7 +22,7 @@ app.config.update(
|
|||
MAIL_USE_SSL=getenv("FLASK_MAIL_USE_SSL"),
|
||||
MAIL_DEBUG=False,
|
||||
MAIL_USERNAME=getenv("FLASK_MAIL_USERNAME"),
|
||||
MAIL_PASSWORD=mail_password,
|
||||
MAIL_PASSWORD=getenv("FLASK_MAIL_PASSWORD"),
|
||||
MAIL_RECIPIENT=getenv("FLASK_MAIL_RECIPIENT"),
|
||||
MAIL_SENDER=getenv("FLASK_MAIL_SENDER")
|
||||
)
|
||||
|
@ -49,7 +41,7 @@ def after(response):
|
|||
@app.route("/rfq", methods=["POST"])
|
||||
def send_rfq():
|
||||
payload = request.json
|
||||
payload = json.loads(htmlsafe_json_dumps(payload))
|
||||
payload = json.loads(json.htmlsafe_dumps(payload))
|
||||
|
||||
if payload is None:
|
||||
resp = jsonify(error="invalid data")
|
||||
|
|
|
@ -11,18 +11,6 @@
|
|||
.feedback-add-success {
|
||||
display: none;
|
||||
}
|
||||
.feedback-add-failure {
|
||||
background-color: #c75e5e;
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 1em;
|
||||
z-index: 100000;
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 5px 3px;
|
||||
}
|
||||
|
||||
#accordion_categories,
|
||||
#accordion_categories .accordion-header,
|
||||
|
@ -55,7 +43,7 @@
|
|||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 1.75rem;
|
||||
padding: .75rem 1.5rem;
|
||||
padding: .75rem 2rem;
|
||||
}
|
||||
|
||||
#accordion_categories .accordion-body {
|
||||
|
@ -73,10 +61,6 @@
|
|||
text-decoration: none;
|
||||
}
|
||||
|
||||
.options-invalid {
|
||||
box-shadow: 0 0 0 .25rem rgba(229, 62, 62, 0.25)!important;
|
||||
--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important;
|
||||
}
|
||||
/*
|
||||
##Device = Tablets, Ipads (portrait)
|
||||
##Screen = B/w 768px to 1024px
|
||||
|
@ -98,14 +82,6 @@
|
|||
height: calc(100vh - 10px - 2.5rem); /* .navbar vertical padding + line height (.navbar-brand.font-size.rem * body.font-size * body.line-height)*/
|
||||
}
|
||||
|
||||
#root-shop .layout>aside.aside {
|
||||
min-width: max(300px, 30%);
|
||||
}
|
||||
|
||||
#root-shop .layout>aside.aside .gradient-bottom {
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
#root-shop .productItem {
|
||||
padding: 2rem 1rem 1rem;
|
||||
}
|
||||
|
@ -123,7 +99,7 @@
|
|||
}
|
||||
|
||||
#root-shop .productItem .content ul {
|
||||
font-size: .75rem;
|
||||
font-size: .6rem;
|
||||
}
|
||||
|
||||
#root-shop .panel .control {
|
||||
|
@ -132,11 +108,11 @@
|
|||
}
|
||||
|
||||
#root-shop .panel .control > .description,
|
||||
#root-shop .crate-mode {
|
||||
#root-shop .panel .control > .crate-mode {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#root-shop .crate-mode {
|
||||
#root-shop .panel .control > .crate-mode {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
@ -145,11 +121,10 @@
|
|||
}
|
||||
|
||||
#root-shop .panel .summary>.summary-price table {
|
||||
font-size: 0.8rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#root-shop .panel .summary>.summary-form form,
|
||||
#root-shop .panel .summary>.summary-form .order-bar {
|
||||
#root-shop .panel .summary>.summary-form form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
@ -175,7 +150,7 @@
|
|||
}
|
||||
|
||||
#root-shop table tr {
|
||||
padding: .2em 0;
|
||||
padding: .8em 0;
|
||||
display: flex !important;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
@ -204,7 +179,7 @@
|
|||
}
|
||||
|
||||
body {
|
||||
font-size: .8rem;
|
||||
font-size: .7rem;
|
||||
}
|
||||
|
||||
#root-shop, #root-shop>div {
|
||||
|
@ -229,7 +204,7 @@
|
|||
}
|
||||
|
||||
#root-shop table tr {
|
||||
padding: .2em 0;
|
||||
padding: .8em 0;
|
||||
display: flex !important;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
@ -239,16 +214,15 @@
|
|||
}
|
||||
|
||||
#root-shop .panel .summary>.summary-price table {
|
||||
font-size: .8rem;
|
||||
font-size: .7rem;
|
||||
}
|
||||
|
||||
#root-shop .panel .summary>.summary-form form,
|
||||
#root-shop .panel .summary>.summary-form .order-bar {
|
||||
#root-shop .panel .summary>.summary-form form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#root-shop .panel .summary>.summary-price tfoot {
|
||||
font-size: 1.0rem;
|
||||
font-size: .85rem;
|
||||
}
|
||||
|
||||
/*#root-shop .panel .summary>.summary-form form input[type="submit"] {
|
||||
|
@ -269,7 +243,7 @@
|
|||
##Screen = B/w 481px to 767px
|
||||
*/
|
||||
@media (min-width: 481px) and (max-width: 767px) {
|
||||
.feedback-add-success, .feedback-add-failure {
|
||||
.feedback-add-success {
|
||||
background-color: green;
|
||||
display: block;
|
||||
position: fixed;
|
||||
|
@ -281,9 +255,6 @@
|
|||
border-radius: 10px;
|
||||
box-shadow: 0 0 5px 3px;
|
||||
}
|
||||
.feedback-add-failure {
|
||||
background-color: #c75e5e;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
font-size: .75em;
|
||||
|
@ -300,7 +271,7 @@
|
|||
}
|
||||
|
||||
body {
|
||||
font-size: .8rem;
|
||||
font-size: .7rem;
|
||||
}
|
||||
|
||||
#root-shop, #root-shop>div {
|
||||
|
@ -320,11 +291,11 @@
|
|||
}
|
||||
|
||||
#root-shop .productItem .content h3 {
|
||||
font-size: 1.25rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#root-shop .productItem .content ul {
|
||||
font-size: .75rem;
|
||||
font-size: .5rem;
|
||||
}
|
||||
|
||||
#root-shop .panel {
|
||||
|
@ -336,16 +307,13 @@
|
|||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
#root-shop .panel .control > .description {
|
||||
#root-shop .panel .control > .description,
|
||||
#root-shop .panel .control > .crate-mode {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#root-shop .panel .crate .crate-bar .crate-mode {
|
||||
#root-shop .panel .control > .crate-mode {
|
||||
text-align: left;
|
||||
width: 50%;
|
||||
}
|
||||
#root-shop .panel .crate .crate-bar .crate-mode a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#root-shop .panel .summary {
|
||||
|
@ -357,16 +325,15 @@
|
|||
}
|
||||
|
||||
#root-shop .panel .summary>.summary-price table {
|
||||
font-size: .8rem;
|
||||
font-size: .7rem;
|
||||
}
|
||||
|
||||
#root-shop .panel .summary>.summary-form form,
|
||||
#root-shop .panel .summary>.summary-form .order-bar {
|
||||
#root-shop .panel .summary>.summary-form form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#root-shop .panel .summary>.summary-price tfoot {
|
||||
font-size: 1rem;
|
||||
font-size: .85rem;
|
||||
}
|
||||
|
||||
/*#root-shop .panel .summary>.summary-form form input[type="submit"] {
|
||||
|
@ -379,6 +346,7 @@
|
|||
border-top-right-radius: 30px;
|
||||
width: 80px;
|
||||
padding: 5px 0 5px 10px;
|
||||
margin-bottom: -25px;
|
||||
margin-left: -1.3rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
@ -397,7 +365,7 @@
|
|||
}
|
||||
|
||||
#root-shop table tr {
|
||||
padding: .2em 0;
|
||||
padding: .8em 0;
|
||||
display: flex !important;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
@ -405,22 +373,21 @@
|
|||
#root-shop .layout>aside.aside.menu-opened {
|
||||
/*transform: translate3d(0, 0, 0);*/
|
||||
transition: left .3s;
|
||||
width: min(310px, 60vw);
|
||||
max-width: 60%;
|
||||
width: 310px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
#root-shop .layout>aside.aside.menu-opened + section.main {
|
||||
/*transform: translate3d(310px, 0, 0);*/
|
||||
transition: left .3s;
|
||||
left: min(310px, 60vw);
|
||||
left: 310px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#root-shop .layout>aside.aside.menu-opened + section.main:after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: rgba(0, 0, 0, .3);
|
||||
|
@ -434,13 +401,13 @@
|
|||
transition: left .3s;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
left: max(-310px, -60vw);
|
||||
width: min(310px, 60vw);
|
||||
left: -310px;
|
||||
width: 310px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#root-shop .layout>aside.aside .gradient-bottom {
|
||||
display: none;
|
||||
#root-shop .layout>aside.aside:after {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
#root-shop .layout>aside.aside + section.main {
|
||||
|
@ -462,7 +429,7 @@
|
|||
overflow: initial;
|
||||
}
|
||||
|
||||
#root-shop .layout>aside.aside.menu-opened > .catalog-container {
|
||||
#root-shop .layout>aside.aside.menu-opened > .backlog-container {
|
||||
overflow-y: scroll;
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -472,7 +439,7 @@
|
|||
}
|
||||
|
||||
#accordion_categories button {
|
||||
font-size: 1.5rem;
|
||||
font-size: 1rem;
|
||||
padding: .5rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
@ -481,7 +448,7 @@
|
|||
##Screen = B/w 320px to 479px
|
||||
*/
|
||||
@media (min-width: 320px) and (max-width: 480px) {
|
||||
.feedback-add-success, .feedback-add-failure {
|
||||
.feedback-add-success {
|
||||
background-color: green;
|
||||
display: block;
|
||||
position: fixed;
|
||||
|
@ -493,9 +460,6 @@
|
|||
border-radius: 10px;
|
||||
box-shadow: 0 0 5px 3px;
|
||||
}
|
||||
.feedback-add-failure {
|
||||
background-color: #c75e5e;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
font-size: .75em;
|
||||
|
@ -512,7 +476,7 @@
|
|||
}
|
||||
|
||||
body {
|
||||
font-size: .8rem;
|
||||
font-size: .7rem;
|
||||
}
|
||||
|
||||
#root-shop, #root-shop>div {
|
||||
|
@ -528,21 +492,20 @@
|
|||
}
|
||||
|
||||
#root-shop .productItem .content h3 {
|
||||
font-size: 1.25rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#root-shop .layout>aside.aside.menu-opened {
|
||||
/*transform: translate3d(0, 0, 0);*/
|
||||
transition: left .3s;
|
||||
width: min(310px, 90vw);
|
||||
max-width: 90%;
|
||||
width: 310px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
#root-shop .layout>aside.aside.menu-opened + section.main {
|
||||
/*transform: translate3d(310px, 0, 0);*/
|
||||
transition: left .3s;
|
||||
left: min(310px, 90vw);
|
||||
left: 310px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
@ -562,13 +525,13 @@
|
|||
transition: left .3s;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
left: max(-310px, -90vw);
|
||||
width: min(310px, 90vw);
|
||||
left: -310px;
|
||||
width: 310px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#root-shop .layout>aside.aside .gradient-bottom {
|
||||
display: none;
|
||||
#root-shop .layout>aside.aside:after {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
#root-shop .layout>aside.aside + section.main {
|
||||
|
@ -607,16 +570,13 @@
|
|||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
#root-shop .panel .control > .description {
|
||||
#root-shop .panel .control > .description,
|
||||
#root-shop .panel .control > .crate-mode {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#root-shop .panel .crate .crate-bar .crate-mode {
|
||||
#root-shop .panel .control > .crate-mode {
|
||||
text-align: left;
|
||||
width: 50%;
|
||||
}
|
||||
#root-shop .panel .crate .crate-bar .crate-mode a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#root-shop .panel .summary {
|
||||
|
@ -624,16 +584,15 @@
|
|||
}
|
||||
|
||||
#root-shop .panel .summary>.summary-price table {
|
||||
font-size: .8rem;
|
||||
font-size: .7rem;
|
||||
}
|
||||
|
||||
#root-shop .panel .summary>.summary-form form,
|
||||
#root-shop .panel .summary>.summary-form .order-bar {
|
||||
#root-shop .panel .summary>.summary-form form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#root-shop .panel .summary>.summary-price tfoot {
|
||||
font-size: 1rem;
|
||||
font-size: .85rem;
|
||||
}
|
||||
|
||||
/*#root-shop .panel .summary>.summary-form form input[type="submit"] {
|
||||
|
@ -646,6 +605,7 @@
|
|||
border-top-right-radius: 30px;
|
||||
width: 80px;
|
||||
padding: 5px 0 5px 10px;
|
||||
margin-bottom: 15px;
|
||||
margin-left: -1.3rem;
|
||||
}
|
||||
|
||||
|
@ -662,7 +622,7 @@
|
|||
}
|
||||
|
||||
#root-shop table tr {
|
||||
padding: .2em 0;
|
||||
padding: .8em 0;
|
||||
display: flex !important;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
@ -671,7 +631,7 @@
|
|||
overflow: initial;
|
||||
}
|
||||
|
||||
#root-shop .layout>aside.aside.menu-opened > .catalog-container {
|
||||
#root-shop .layout>aside.aside.menu-opened > .backlog-container {
|
||||
overflow-y: scroll;
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -681,7 +641,7 @@
|
|||
}
|
||||
|
||||
#accordion_categories button {
|
||||
font-size: 1.5rem;
|
||||
font-size: 1rem;
|
||||
padding: .5rem 0.5rem;
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 37 KiB |
|
@ -1 +0,0 @@
|
|||
<svg width="232" height="232" version="1.1" viewBox="0 0 232 232" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-4.0001 -12)" fill="none" stroke="#715ec7" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"><path d="m61.7 204.1-45.7-76.1 45.7-76.1a7.9 7.9 0 0 1 6.8-3.9h147.5a8 8 0 0 1 8 8v144a8 8 0 0 1-8 8h-147.5a7.9 7.9 0 0 1-6.8-3.9z"/><line x1="160" x2="112" y1="104" y2="152"/><line x1="160" x2="112" y1="152" y2="104"/></g></svg>
|
Before Width: | Height: | Size: 463 B |
|
@ -1 +0,0 @@
|
|||
<svg width="32" height="32" version="1.1" viewBox="0 0 8.4667 8.4667" xmlns="http://www.w3.org/2000/svg"><circle cx="4.2333" cy="4.2333" r="3.9077" fill="#d0423f" stroke="#d0423f" stroke-linecap="round" stroke-width=".65136"/></svg>
|
Before Width: | Height: | Size: 233 B |
|
@ -1,39 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="512"
|
||||
height="512"
|
||||
version="1.1"
|
||||
viewBox="0 0 512 512"
|
||||
id="svg1"
|
||||
sodipodi:docname="icon-search.svg"
|
||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="1.59375"
|
||||
inkscape:cx="255.68627"
|
||||
inkscape:cy="256"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<path
|
||||
d="M456.69,421.39,362.6,327.3a173.81,173.81,0,0,0,34.84-104.58C397.44,126.38,319.06,48,222.72,48S48,126.38,48,222.72s78.38,174.72,174.72,174.72A173.81,173.81,0,0,0,327.3,362.6l94.09,94.09a25,25,0,0,0,35.3-35.3ZM97.92,222.72a124.8,124.8,0,1,1,124.8,124.8A124.95,124.95,0,0,1,97.92,222.72Z"
|
||||
fill="#fff"
|
||||
id="path1"
|
||||
style="fill:#ffffff;fill-opacity:0.5" />
|
||||
</svg>
|
Before Width: | Height: | Size: 1.4 KiB |
File diff suppressed because one or more lines are too long
2484
static/js/shop.jsx
2484
static/js/shop.jsx
File diff suppressed because it is too large
Load Diff
|
@ -1,46 +0,0 @@
|
|||
import {OverlayTrigger} from "react-bootstrap";
|
||||
import React from "react";
|
||||
import {Levels, MaxLevel} from "./warnings";
|
||||
import {useShopStore} from "./shop_store";
|
||||
import {compareArraysLevelOne} from "./utils";
|
||||
|
||||
|
||||
export function CardWarnings({crate_index, card_index}) {
|
||||
|
||||
const warnings = useShopStore(state => state.crates[crate_index].items[card_index].show_warnings, compareArraysLevelOne);
|
||||
|
||||
const max_level = MaxLevel(warnings);
|
||||
return (
|
||||
<OverlayTrigger
|
||||
placement="bottom"
|
||||
trigger={['click', 'hover', 'focus']}
|
||||
overlay={
|
||||
({arrowProps, hasDoneInitialMeasure, show, style, ...props}) => (
|
||||
<div className="k-popup-warning" style={{...style, backgroundColor: max_level.color}} {...props}>
|
||||
{warnings.map((warning, _i) => {
|
||||
return (
|
||||
<p className="rule warning" key={`warnmsg_${card_index}_${warning.name}`}>
|
||||
<i>{warning.message}</i>
|
||||
</p>
|
||||
)
|
||||
})}
|
||||
</div>)
|
||||
}
|
||||
rootClose
|
||||
>
|
||||
<img className="alert-warning p-0" src={max_level.icon}/>
|
||||
</OverlayTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
export function WarningIndicator({crate_index, card_index}) {
|
||||
const warnings = useShopStore(state => state.crates[crate_index].items[card_index].show_warnings, compareArraysLevelOne);
|
||||
|
||||
const max_level = MaxLevel(warnings);
|
||||
return max_level.priority === Levels.warning.priority ? (
|
||||
<img
|
||||
className="alert-warning align-self-start d-block"
|
||||
src={max_level.icon}
|
||||
/>
|
||||
) : (<span className="alert-warning align-self-start d-block"></span>);
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
import React from 'react'
|
||||
import {Droppable} from "@hello-pangea/dnd";
|
||||
import {cartStyle, compareArraysWithIds} from "./utils";
|
||||
import {ProductCartItem} from "./ProductCartItem";
|
||||
import {FakePlaceholder} from "./FakePlaceholder";
|
||||
import {hp_to_slots} from "./count_resources";
|
||||
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 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);
|
||||
|
||||
// #!render_count
|
||||
console.log("Cart renders: ", renderCount)
|
||||
|
||||
const nbrOccupied = hp_to_slots(crate.occupiedHP);
|
||||
const nbrSlots = hp_to_slots(crateParams(crate.crate_mode).hp);
|
||||
|
||||
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}
|
||||
{...provided.droppableProps}
|
||||
style={cartStyle(
|
||||
provided.droppableProps.style,
|
||||
snapshot,
|
||||
)}
|
||||
className="items-cart-list">
|
||||
|
||||
{products}
|
||||
|
||||
{provided.placeholder && (
|
||||
<div style={{display: 'none'}}>
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FakePlaceholder
|
||||
nToDraw={nbrSlots - nbrOccupied}
|
||||
isDraggingOver={snapshot.isDraggingOver}/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</Droppable>
|
||||
);
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
import React from 'react';
|
||||
import {Droppable} from "@hello-pangea/dnd";
|
||||
import {useShopStore} from "./shop_store";
|
||||
// #!render_count
|
||||
import {useRenderCount} from "@uidotdev/usehooks";
|
||||
import {CatalogGroups} from "./CatalogGroups";
|
||||
import {SearchBar} from "./SearchBar";
|
||||
import {CatalogSearchResult} from "./CatalogSearchResult";
|
||||
import {GradientBottom} from "./GradientBottom";
|
||||
|
||||
/**
|
||||
* Component that renders the catalog in the aside
|
||||
*/
|
||||
export function Catalog() {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
const data = useShopStore((state) => state.groups);
|
||||
const items = useShopStore((state) => state.cards);
|
||||
|
||||
const onClickToggleMobileSideMenu = useShopStore((state) => state.switchSideMenu);
|
||||
const isMobile = useShopStore((state) => state.isMobile);
|
||||
const showSearch = useShopStore((state) => state.listed_cards.length > 0);
|
||||
|
||||
// #!render_count
|
||||
console.log("Catalog renders: ", renderCount)
|
||||
|
||||
return (
|
||||
<Droppable
|
||||
droppableId={data.id}
|
||||
isDropDisabled={false}>
|
||||
|
||||
{(provided) => (
|
||||
<div
|
||||
className="catalog-container"
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}>
|
||||
|
||||
|
||||
|
||||
<div className="catalog-bar">
|
||||
<SearchBar/>
|
||||
|
||||
{isMobile ? (
|
||||
<div className="mobileCloseMenu">
|
||||
<button onClick={onClickToggleMobileSideMenu}>
|
||||
<img src="/images/shop/icon-close-white.svg" alt="add"/>
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{showSearch ? <CatalogSearchResult/> : <CatalogGroups/>}
|
||||
|
||||
{provided.placeholder && (
|
||||
<div style={{display: 'none'}}>
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<GradientBottom/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</Droppable>
|
||||
);
|
||||
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
import {ProductItem} from "./ProductItem";
|
||||
import React from "react";
|
||||
import {useShopStore} from "./shop_store";
|
||||
|
||||
export function CatalogGroups() {
|
||||
const data = useShopStore((state) => state.groups);
|
||||
const items = useShopStore((state) => state.cards);
|
||||
|
||||
const ordered_groups = data.categories.map(groupItem => ({
|
||||
name: groupItem.name,
|
||||
items: groupItem.itemIds.map(itemId => items[itemId])
|
||||
}));
|
||||
let item_index = -1;
|
||||
const groups = ordered_groups.map((group, g_index) => {
|
||||
return (
|
||||
<div className="accordion-item" key={`${group.name}`}>
|
||||
<h2 className="accordion-header">
|
||||
<button className="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target={`#collapse${g_index}`} aria-expanded="true"
|
||||
aria-controls={`collapse${g_index}`}>
|
||||
{group.name}
|
||||
</button>
|
||||
</h2>
|
||||
<div id={`collapse${g_index}`} className="accordion-collapse collapse" aria-labelledby="headingOne"
|
||||
data-bs-parent="#accordion_categories">
|
||||
<div className="accordion-body">
|
||||
{group.items.map(item => {
|
||||
item_index++;
|
||||
return (
|
||||
<ProductItem card_index={item_index} key={item.id} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<div className="accordion accordion-flush" id="accordion_categories">
|
||||
{groups}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import React from 'react';
|
||||
import {useShopStore} from "./shop_store";
|
||||
import {ProductItem} from "./ProductItem";
|
||||
|
||||
|
||||
export function CatalogSearchResult() {
|
||||
const cards_to_display = useShopStore((state) => state.listed_cards);
|
||||
return ( <>
|
||||
{cards_to_display.map((item, _) => {
|
||||
return (
|
||||
<ProductItem card_index={item} key={"searched_" + item} />
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
import React from 'react';
|
||||
import {Cart} from "./Cart";
|
||||
import {CrateMode} from "./CrateMode";
|
||||
import {CrateWarnings} from "./CrateWarnings";
|
||||
import {useShopStore} from "./shop_store";
|
||||
import {CrateOptions} from "./CrateOptions";
|
||||
|
||||
// #!render_count
|
||||
import {useRenderCount} from "@uidotdev/usehooks";
|
||||
|
||||
|
||||
/**
|
||||
* Component that displays the main crate with reminder rules.
|
||||
* It includes <Cart> and rules
|
||||
*/
|
||||
export function Crate({crate_index}) {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
const crate = useShopStore((state) => state.crates[crate_index],
|
||||
(a, b) => a.length === b.length && a.id === b.id);
|
||||
const modes_order = useShopStore((state) => state.modes_order);
|
||||
const onDeleteCrate = useShopStore((state) => state.delCrate);
|
||||
|
||||
// #!render_count
|
||||
console.log("Crate renders: ", renderCount)
|
||||
|
||||
return (
|
||||
<div className="crate">
|
||||
{
|
||||
modes_order.includes(crate.crate_mode) ? (
|
||||
<div className="crate-bar d-inline-flex justify-content-between">
|
||||
<CrateMode crate_index={crate_index}/>
|
||||
|
||||
<div className="delete-crate align-self-start align-content-start justify-content-end" onClick={() => onDeleteCrate(crate.id)}>
|
||||
Delete crate <img src="/images/shop/icon-remove.svg" alt="remove"/>
|
||||
</div>
|
||||
</div>
|
||||
) : <></>
|
||||
}
|
||||
|
||||
<div className="crate-products">
|
||||
|
||||
<Cart crate_index={crate_index}/>
|
||||
|
||||
<CrateWarnings crate_index={crate_index} />
|
||||
|
||||
<CrateOptions crate_index={crate_index}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
import React from 'react'
|
||||
import {Accordion} from "react-bootstrap";
|
||||
import {Crate} from "./Crate";
|
||||
import {useShopStore} from "./shop_store";
|
||||
|
||||
// #!render_count
|
||||
import {useRenderCount} from "@uidotdev/usehooks";
|
||||
|
||||
export function CrateList() {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
const crates = useShopStore((state) => state.crates,
|
||||
(a, b) => a.length === b.length);
|
||||
const active_crate = useShopStore((state) => state.active_crate);
|
||||
const onAddCrate = useShopStore((state) => state.newCrate);
|
||||
const setActiveCrate = useShopStore((state) => state.setActiveCrate);
|
||||
|
||||
const onSelectHandler = (e) => {
|
||||
// if e === null, that means that an accordion item was collapsed rather than expanded. e will be non-null when an item is expanded
|
||||
if (e !== null)
|
||||
setActiveCrate(e);
|
||||
else
|
||||
setActiveCrate("")
|
||||
};
|
||||
|
||||
// #!render_count
|
||||
console.log("CrateList renders: ", renderCount)
|
||||
return (
|
||||
<Accordion id="accordion_crates" flush activeKey={active_crate} onSelect={onSelectHandler}>
|
||||
{crates.map((crate, index) =>
|
||||
<Accordion.Item eventKey={crate.id} key={"accordion"+crate.id} className="accordion_crates_item">
|
||||
<Accordion.Header>{crate.name ? crate.name : <>Crate #{`${index}`}</>} </Accordion.Header>
|
||||
<Accordion.Body>
|
||||
<Crate crate_index={index}/>
|
||||
</Accordion.Body>
|
||||
</Accordion.Item>
|
||||
)}
|
||||
<Accordion.Item eventKey="last" id="accordion_crates_add">
|
||||
<Accordion.Header onClick={onAddCrate}>
|
||||
Add new crate
|
||||
</Accordion.Header>
|
||||
</Accordion.Item>
|
||||
</Accordion>)
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
import React from 'react';
|
||||
import {useShopStore} from "./shop_store";
|
||||
|
||||
// #!render_count
|
||||
import {useRenderCount} from "@uidotdev/usehooks";
|
||||
|
||||
/**
|
||||
* Component that displays crate modes
|
||||
*/
|
||||
export function CrateMode({crate_index}) {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
const modes_order = useShopStore((state) => state.modes_order);
|
||||
const crate_modes = useShopStore((state) => state.crate_modes);
|
||||
const crate = useShopStore((state) => state.crates[crate_index],
|
||||
(a, b) => a.id === b.id && a.crate_mode === b.crate_mode);
|
||||
const setMode = useShopStore((state) => state.setCrateMode);
|
||||
|
||||
// #!render_count
|
||||
console.log("CrateMode renders: ", renderCount)
|
||||
|
||||
return (
|
||||
<div className="crate-mode">
|
||||
{modes_order.map((mode_name, _) => (
|
||||
<a
|
||||
key={mode_name}
|
||||
className={(crate.crate_mode === mode_name ? 'active' : '')}
|
||||
onClick={() => setMode(crate.id, mode_name)}
|
||||
href="#"
|
||||
role="button">{crate_modes[mode_name].name}</a>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import {useShopStore} from "./shop_store";
|
||||
import {ProcessOptions} from "./options/Options";
|
||||
|
||||
export function CrateOptions({crate_index}) {
|
||||
const crate_id = useShopStore((state) => state.crates[crate_index].id);
|
||||
const optionsLogic = useShopStore((state) => state.crate_options);
|
||||
const updateOptions = useShopStore((state) => state.updateCrateOptions);
|
||||
const options_data = useShopStore((state) => state.crates[crate_index].options_data || {});
|
||||
|
||||
const options = ProcessOptions({
|
||||
options: optionsLogic,
|
||||
data: options_data,
|
||||
id: "crate_options" + crate_id,
|
||||
target: {
|
||||
construct: ((outvar, value) => {
|
||||
// #!options_log
|
||||
console.log("construct", outvar, value, options_data);
|
||||
|
||||
options_data[outvar] = value;
|
||||
}),
|
||||
update: ((outvar, value) => {
|
||||
// #!options_log
|
||||
console.log("update", outvar, value, options_data);
|
||||
|
||||
if (outvar in options_data) options_data[outvar] = value;
|
||||
|
||||
updateOptions(crate_id, {[outvar]: value});
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="crate-bar">
|
||||
{options}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
import React from "react";
|
||||
import {LevelUI} from "./warnings";
|
||||
import {useShopStore} from "./shop_store";
|
||||
import {compareArraysWithIds} from "./utils";
|
||||
|
||||
// #!render_count
|
||||
import {useRenderCount} from "@uidotdev/usehooks";
|
||||
|
||||
|
||||
export function CrateWarnings({crate_index}) {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
const crate_warnings = useShopStore(state => (state.crates[crate_index].warnings), compareArraysWithIds)
|
||||
|
||||
// #!render_count
|
||||
console.log("CrateWarnings renders: ", renderCount)
|
||||
return (
|
||||
<div className="crate-info">
|
||||
{crate_warnings.map((rule, index) => (
|
||||
<p key={index} className="rule" style={{'color': LevelUI(rule.level).color}}>
|
||||
<img src={LevelUI(rule.level).icon} /> <i>{rule.message}</i>
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Component that displays a placeholder inside crate.
|
||||
* Allows to display how it remains space for the current crate.
|
||||
*/
|
||||
export function FakePlaceholder({isDraggingOver, nToDraw}) {
|
||||
const fakePlaceholder = [];
|
||||
|
||||
for (let i = nToDraw; i > 0; i--) {
|
||||
fakePlaceholder.push(
|
||||
<div key={i} style={{
|
||||
display: isDraggingOver ? 'none' : 'block',
|
||||
border: '1px dashed #ccc',
|
||||
width: '45px',
|
||||
marginBottom: '5px',
|
||||
}}></div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{fakePlaceholder}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
export function GradientBottom() {
|
||||
return (
|
||||
<div className="gradient-bottom"></div>
|
||||
)
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
import {useShopStore} from "./shop_store";
|
||||
import {useClickAway} from "./options/useClickAway";
|
||||
import {Modal} from "react-bootstrap";
|
||||
import React from "react";
|
||||
import {Validation} from "./validate";
|
||||
// #!render_count
|
||||
import {useRenderCount} from "@uidotdev/usehooks";
|
||||
|
||||
const JSONExample = JSON.stringify({
|
||||
"crates": [
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"pn": "1124",
|
||||
"options": null
|
||||
},
|
||||
{
|
||||
"pn": "2128",
|
||||
"options": null
|
||||
},
|
||||
{
|
||||
"pn": "2128",
|
||||
"options": null
|
||||
},
|
||||
{
|
||||
"pn": "2128",
|
||||
"options": null
|
||||
}
|
||||
],
|
||||
"type": "rack",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"items": [],
|
||||
"type": "no_crate",
|
||||
"options": {}
|
||||
}
|
||||
],
|
||||
"options": {}
|
||||
});
|
||||
|
||||
export function ImportJSON() {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
const shouldShow = useShopStore((state) => state.importShouldOpen);
|
||||
const data = useShopStore((state) => state.importValue);
|
||||
const loadDescription = useShopStore((state) => state.loadDescription);
|
||||
const updateImportDescription = useShopStore((state) => state.updateImportDescription);
|
||||
const closeImport = useShopStore((state) => state.closeImport);
|
||||
const showImport = useShopStore((state) => state.openImport);
|
||||
|
||||
// #!render_count
|
||||
console.log("ImportJSON renders: ", renderCount)
|
||||
|
||||
const ref = useClickAway((e) => {
|
||||
if (e.type === "mousedown") // ignore touchstart
|
||||
closeImport()
|
||||
}
|
||||
);
|
||||
|
||||
return (<>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-primary m-0 mb-2"
|
||||
style={{'cursor': 'pointer'}}
|
||||
onClick={showImport}>Import JSON
|
||||
</button>
|
||||
<Modal show={shouldShow} animation={true} centered className="rfqFeedback">
|
||||
<Modal.Body ref={ref}>
|
||||
<div className="form-group">
|
||||
<p className="small">
|
||||
Input the JSON description below. Should be something like:
|
||||
<br/>
|
||||
{JSONExample}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="form-group w-100">
|
||||
<textarea
|
||||
onChange={(event) => {
|
||||
updateImportDescription(event.target.value)
|
||||
}}
|
||||
value={data.value}
|
||||
className="form-control w-100"
|
||||
rows="5"
|
||||
placeholder="Input JSON description here."/>
|
||||
</div>
|
||||
{data.error !== Validation.OK ? (
|
||||
<div className="form-group">
|
||||
<p className="text-danger">{data.error === Validation.Empty ? "Empty input" : "Invalid JSON"}</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="d-flex flex-column flex-sm-row justify-content-end">
|
||||
<a type="button" onClick={closeImport}
|
||||
className="btn btn-sm btn-outline-primary m-0 mb-2 mt-2 mb-sm-0 me-sm-2">Close</a>
|
||||
<a type="button" onClick={loadDescription}
|
||||
className={`btn btn-sm btn-primary m-0 ms-sm-2 mt-2 ${data.error ? 'disabled' : ''}`}>Load
|
||||
configuration</a>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</>)
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
import React from 'react';
|
||||
import {useShopStore} from "./shop_store";
|
||||
|
||||
// #!render_count
|
||||
import {useRenderCount} from "@uidotdev/usehooks";
|
||||
|
||||
/**
|
||||
* Component that provides a base layout (aside/main) for the page.
|
||||
*/
|
||||
|
||||
|
||||
export function Layout({aside, main}) {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
const mobileSideMenuShouldOpen = useShopStore(state => state.sideMenuIsOpen);
|
||||
const onClickToggleMobileSideMenu = useShopStore(state => state.switchSideMenu);
|
||||
const showCardAddedFeedback = useShopStore(state => state.showCardAddedFeedback);
|
||||
const showNoDestination = useShopStore(state => state.showNoDestination);
|
||||
|
||||
// #!render_count
|
||||
console.log("Layout renders: ", renderCount)
|
||||
|
||||
return (
|
||||
<div className="layout">
|
||||
|
||||
<aside className={'aside ' + (mobileSideMenuShouldOpen ? 'menu-opened' : '')}>{aside}</aside>
|
||||
|
||||
{mobileSideMenuShouldOpen ? (
|
||||
<section className="main" onClick={onClickToggleMobileSideMenu}>{main}</section>
|
||||
) : (
|
||||
<section className="main">{main}</section>
|
||||
)}
|
||||
|
||||
{showCardAddedFeedback ? (
|
||||
!showNoDestination ?
|
||||
(<div className="feedback-add-success">
|
||||
✓ added
|
||||
</div>)
|
||||
: (<div className="feedback-add-failure">
|
||||
No cards added: all crates are closed
|
||||
</div>)
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
import {DialogPopup} from "./options/DialogPopup";
|
||||
import React from "react";
|
||||
import {useShopStore} from "./shop_store";
|
||||
import {SummaryPopup} from "./options/SummaryPopup";
|
||||
|
||||
export function OptionsDialogWrapper({crate_index, card_index, first, last}) {
|
||||
const crate_id = useShopStore((state) => state.crates[crate_index].id);
|
||||
const options = useShopStore((state) => state.crates[crate_index].items[card_index].options);
|
||||
const options_data = useShopStore((state) => state.crates[crate_index].items[card_index].options_data);
|
||||
const card_size = useShopStore((state) => state.crates[crate_index].items[card_index].size);
|
||||
const 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 sideMenuIsOpen = useShopStore((state) => state.sideMenuIsOpen);
|
||||
const _notificationTimer = useShopStore((state) => state.notificationTimer);
|
||||
const hideNotification = useShopStore((state) => state.hideNotification);
|
||||
const displayNotification = useShopStore((state) =>
|
||||
state.notificationCrateId === crate_id &&
|
||||
(state.notificationCardIndex === card_index || (state.crates[crate_index].items.length + (state.notificationCardIndex || -1)) === card_index));
|
||||
|
||||
const onOptionsUpdate = useShopStore((state) => state.updateOptions);
|
||||
|
||||
return (
|
||||
<DialogPopup
|
||||
options={options}
|
||||
data={options_data}
|
||||
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}
|
||||
target={{
|
||||
construct: ((outvar, value) => {
|
||||
// #!options_log
|
||||
console.log("construct", outvar, value, options_data);
|
||||
|
||||
options_data[outvar] = value;
|
||||
}),
|
||||
update: ((outvar, value) => {
|
||||
// #!options_log
|
||||
console.log("update", outvar, value, options_data);
|
||||
|
||||
if (outvar in options_data) options_data[outvar] = value;
|
||||
onOptionsUpdate(crate_id, card_index, {[outvar]: value});
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function OptionsSummaryWrapper({crate_index, card_index}) {
|
||||
const card_id = useShopStore((state) => state.crates[crate_index].items[card_index].id);
|
||||
const options = useShopStore((state) => state.crates[crate_index].items[card_index].options);
|
||||
const options_data = useShopStore((state) => state.crates[crate_index].items[card_index].options_data);
|
||||
|
||||
return (
|
||||
<SummaryPopup id={card_id + "options"} options={options}
|
||||
data={options_data}/>
|
||||
)
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
import React from 'react'
|
||||
import {Validation} from "./validate.js";
|
||||
import {useShopStore} from "./shop_store";
|
||||
import {ShowJSON} from "./ShowJSON";
|
||||
|
||||
// #!render_count
|
||||
import {useRenderCount} from "@uidotdev/usehooks";
|
||||
import {OrderOptions} from "./OrderOptions";
|
||||
|
||||
|
||||
/**
|
||||
* Components that renders the form to request quote.
|
||||
*/
|
||||
export function OrderForm() {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
const email = useShopStore((state) => state.email);
|
||||
const note = useShopStore((state) => state.note);
|
||||
const isProcessing = useShopStore((state) => state.isProcessing);
|
||||
const updateEmail = useShopStore((state) => state.updateEmail);
|
||||
const updateNote = useShopStore((state) => state.updateNote);
|
||||
const submitForm = useShopStore((state) => state.submitForm);
|
||||
const submitDisabled = useShopStore((state) => state.submitDisabled);
|
||||
const resetEmailValidation = useShopStore((state) => state.resetEmailValidation);
|
||||
|
||||
// #!render_count
|
||||
console.log("OrderForm renders: ", renderCount)
|
||||
|
||||
return (
|
||||
<div className="summary-form">
|
||||
|
||||
<OrderOptions/>
|
||||
|
||||
<form onSubmit={submitForm} noValidate>
|
||||
|
||||
<input
|
||||
className={`${email.error > 0 ? 'errorField' : ''}`}
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
onFocus={resetEmailValidation}
|
||||
onChange={(event) => updateEmail(event.target.value)}
|
||||
onBlur={(event) => updateEmail(event.target.value)}
|
||||
value={email.value}/>
|
||||
|
||||
{email.error === Validation.Empty ? (
|
||||
<div className="error">
|
||||
<small>Required</small>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{email.error === Validation.Invalid ? (
|
||||
<div className="error">
|
||||
<small>Your email is incomplete</small>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<textarea
|
||||
onChange={(event) => updateNote(event.target.value)}
|
||||
defaultValue={note.value}
|
||||
rows="5"
|
||||
placeholder="Additional notes"/>
|
||||
|
||||
<div className="d-flex flex-column flex-sm-row justify-content-between">
|
||||
<ShowJSON/>
|
||||
|
||||
<input className="btn btn-primary w-100 m-0 ms-sm-2 order-form-submit" type="button"
|
||||
disabled={submitDisabled()}
|
||||
onClick={submitForm}
|
||||
value={`${isProcessing ? 'Processing ...' : 'Request quote'}`}/>
|
||||
</div>
|
||||
{/*This will open an email window. Send the email to make your request.*/}
|
||||
</form>
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import {useShopStore} from "./shop_store";
|
||||
import {ProcessOptions} from "./options/Options";
|
||||
import React from "react";
|
||||
|
||||
export function OrderOptions() {
|
||||
const optionsLogic = useShopStore((state) => state.order_options);
|
||||
const updateOptions = useShopStore((state) => state.updateOrderOptions);
|
||||
const options_data = useShopStore((state) => state.order_options_data || {});
|
||||
|
||||
const options = ProcessOptions({
|
||||
options: optionsLogic,
|
||||
data: options_data,
|
||||
id: "order_options",
|
||||
target: {
|
||||
construct: ((outvar, value) => {
|
||||
// #!options_log
|
||||
console.log("construct", outvar, value, options_data);
|
||||
|
||||
options_data[outvar] = value;
|
||||
updateOptions({[outvar]: value});
|
||||
}),
|
||||
update: ((outvar, value) => {
|
||||
// #!options_log
|
||||
console.log("update", outvar, value, options_data);
|
||||
|
||||
if (outvar in options_data) options_data[outvar] = value;
|
||||
|
||||
updateOptions({[outvar]: value});
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="order-bar">
|
||||
{options}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
import React from 'react'
|
||||
import {SummaryOrder} from "./SummaryOrder";
|
||||
import {OrderForm} from "./OrderForm";
|
||||
import {CrateList} from "./CrateList";
|
||||
import {useShopStore} from "./shop_store";
|
||||
import {ImportJSON} from "./ImportJSON";
|
||||
import {RFQFeedback} from "./RFQFeedback";
|
||||
|
||||
// #!render_count
|
||||
import {useRenderCount} from "@uidotdev/usehooks";
|
||||
import {OrderOptions} from "./OrderOptions";
|
||||
|
||||
/**
|
||||
* Component that renders all things for order.
|
||||
* It acts like-a layout, this component do nothing more.
|
||||
*/
|
||||
export function OrderPanel({title, description}) {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
const isMobile = useShopStore((state) => state.isMobile);
|
||||
const onClickToggleMobileSideMenu = useShopStore((state) => state.switchSideMenu);
|
||||
|
||||
// #!render_count
|
||||
console.log("OrderPanel renders: ", renderCount)
|
||||
|
||||
return (<section className="panel">
|
||||
|
||||
<h2>{title}</h2>
|
||||
|
||||
<div className="control justify-content-between">
|
||||
{description}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ImportJSON/>
|
||||
</div>
|
||||
|
||||
<RFQFeedback/>
|
||||
|
||||
{isMobile ? (
|
||||
<div className="mobileBtnDisplaySideMenu">
|
||||
<button onClick={onClickToggleMobileSideMenu}>
|
||||
<img src="/images/shop/icon-add.svg" alt="add"/>
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<CrateList/>
|
||||
|
||||
<section className="summary">
|
||||
<SummaryOrder/>
|
||||
<OrderForm/>
|
||||
</section>
|
||||
|
||||
</section>);
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
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 ProductCartItem({card_index, crate_index, first, last}) {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
|
||||
const card = useShopStore((state) => state.crates[crate_index].items[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_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 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 = !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;
|
||||
|
||||
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}
|
||||
first={first}
|
||||
last={last}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h6>{card.name_number}</h6>
|
||||
|
||||
<div
|
||||
onMouseEnter={() => setHighlight(crate_id, card_index)}
|
||||
onClick={() => setHighlight(crate_id, card_index)}
|
||||
>
|
||||
|
||||
<img
|
||||
className='item-cart'
|
||||
src={card.image}/>
|
||||
</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>
|
||||
|
||||
{/* progression container */}
|
||||
{resources && (
|
||||
<Resources crate_index={crate_index} card_index={card_index} />
|
||||
)}
|
||||
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
</Draggable>
|
||||
);
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
import React from 'react';
|
||||
import {Draggable} from "@hello-pangea/dnd";
|
||||
import {formatMoney, productStyle} from "./utils";
|
||||
import {useShopStore} from "./shop_store";
|
||||
|
||||
// #!render_count
|
||||
import {useRenderCount} from "@uidotdev/usehooks";
|
||||
|
||||
/**
|
||||
* Component that renders a product.
|
||||
* Used in the aside (e.g catalog of product)
|
||||
*/
|
||||
export function ProductItem({card_index}) {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
const getCardDescription = useShopStore((state) => state.getCardDescription);
|
||||
const currency = useShopStore((state) => state.currency);
|
||||
const onAddCard = useShopStore((state) => state.addCardFromCatalog);
|
||||
const card = getCardDescription(card_index);
|
||||
|
||||
// #!render_count
|
||||
console.log("ProductItem renders: ", renderCount)
|
||||
|
||||
|
||||
const render_specs = (card.specs && card.specs.length > 0 && (
|
||||
<ul>
|
||||
{card.specs.map((spec, index) =>
|
||||
<li key={index}>{spec}</li>
|
||||
)}
|
||||
</ul>
|
||||
));
|
||||
|
||||
const render_datasheet_link = (card.datasheet_file && card.datasheet_name && (
|
||||
<div className="ds">
|
||||
<span className='doc-icon'></span>
|
||||
<a href={card.datasheet_file} target="_blank" rel="noopener noreferrer">
|
||||
{card.datasheet_name}
|
||||
</a>
|
||||
</div>
|
||||
));
|
||||
|
||||
return (
|
||||
<section className="productItem">
|
||||
|
||||
<div className="content">
|
||||
<h3 style={{'marginBottom': card.name_codename ? '5px' : '20px'}}>{card.name_number} {card.name}</h3>
|
||||
{card.name_codename ? (
|
||||
<p>{card.name_codename}</p>
|
||||
) : null}
|
||||
|
||||
<div className="price">{`${currency} ${formatMoney(card.price)}`}</div>
|
||||
|
||||
{render_specs}
|
||||
|
||||
{render_datasheet_link}
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
|
||||
<button onClick={() => onAddCard(null, card_index, null)}>
|
||||
<img src="/images/shop/icon-add.svg" alt="add"/>
|
||||
</button>
|
||||
|
||||
<Draggable draggableId={card.id + card_index} index={card_index}>
|
||||
{(provided, snapshot) => (
|
||||
<React.Fragment>
|
||||
<img
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={productStyle(
|
||||
provided.draggableProps.style,
|
||||
snapshot,
|
||||
true, // hack: remove weird animation after a drop
|
||||
)}
|
||||
src={card.image}/>
|
||||
|
||||
{/* Allows to simulate a clone */}
|
||||
{snapshot.isDragging && (
|
||||
<img className="simclone" src={card.image}/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Draggable>
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
);
|
||||
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
import {useShopStore} from "./shop_store";
|
||||
import {useClickAway} from "./options/useClickAway";
|
||||
import {Modal} from "react-bootstrap";
|
||||
import {Validation} from "./validate";
|
||||
import React from "react";
|
||||
// #!render_count
|
||||
import {useRenderCount} from "@uidotdev/usehooks";
|
||||
|
||||
export function RFQFeedback() {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
const closeRFQ = useShopStore((state) => state.closeRFQFeedback);
|
||||
const shouldShow = useShopStore((state) => state.shouldShowRFQFeedback);
|
||||
const status = useShopStore((state) => state.processingResult);
|
||||
|
||||
// #!render_count
|
||||
console.log("RFQFeedback renders: ", renderCount)
|
||||
|
||||
const ref = useClickAway((e) => {
|
||||
if (e.type === "mousedown") // ignore touchstart
|
||||
closeRFQ()
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal show={shouldShow} animation={true} centered>
|
||||
<Modal.Body ref={ref} className="rfqFeedback">
|
||||
<div className="d-flex">
|
||||
<div>
|
||||
{status.status === Validation.OK ?
|
||||
<img width="30px" src="/images/shop/icon-done.svg" alt="close"/>
|
||||
: <img width="30px" src="/images/shop/icon-warning.svg" alt="close"/>
|
||||
}
|
||||
</div>
|
||||
<div style={{'padding': '0 .5em'}}>
|
||||
{status.message}
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
)
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
import {OverlayTrigger} from "react-bootstrap";
|
||||
import React from "react";
|
||||
import {v4 as uuidv4} from "uuid";
|
||||
|
||||
// #!render_count
|
||||
import {useRenderCount} from "@uidotdev/usehooks";
|
||||
import {useShopStore} from "./shop_store";
|
||||
import {compareArraysLevelOne} from "./utils";
|
||||
|
||||
|
||||
const resourcesWidthStyle = (occupied, max) => {
|
||||
return {
|
||||
width: `${Math.min(occupied * 100 / max, 100)}%`,
|
||||
}
|
||||
};
|
||||
|
||||
function EEMRenderer({occupied, max}) {
|
||||
return (
|
||||
<div className="nbr-connectors" key={uuidv4()}>
|
||||
<div style={{...resourcesWidthStyle(occupied, max)}}></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ClockRenderer({occupied, max}) {
|
||||
return (
|
||||
<div className="nbr-clocks" key={uuidv4()}>
|
||||
<div style={{...resourcesWidthStyle(occupied, max)}}></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const resource_progress_renderers = {
|
||||
"eem": EEMRenderer,
|
||||
"clk": ClockRenderer,
|
||||
"idc": EEMRenderer
|
||||
}
|
||||
|
||||
|
||||
function EEMTipRender({occupied, max}) {
|
||||
return (<p key={uuidv4()}>{`${occupied}/${max} EEM connectors used`}</p>);
|
||||
}
|
||||
|
||||
function IDCTipRender({occupied, max}) {
|
||||
return (<p key={uuidv4()}>{`${occupied}/${max} IDC connectors used`}</p>);
|
||||
}
|
||||
|
||||
function ClockTipRender({occupied, max}) {
|
||||
return (<p key={uuidv4()}>{`${occupied}/${max} clock connectors used`}</p>);
|
||||
}
|
||||
|
||||
const resource_tip = {
|
||||
"eem": EEMTipRender,
|
||||
"clk": ClockTipRender,
|
||||
"idc": IDCTipRender
|
||||
}
|
||||
|
||||
function RenderResources({resources, library}) {
|
||||
if (!resources) return null;
|
||||
let result = [];
|
||||
|
||||
resources.forEach((value, _) => {
|
||||
if (library[value.name]) result.push(library[value.name](value));
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function Resources({crate_index, card_index}) {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
const resources = useShopStore(state => state.crates[crate_index].items[card_index].counted_resources, compareArraysLevelOne);
|
||||
|
||||
// #!render_count
|
||||
console.log("Resources renders: ", renderCount)
|
||||
return (
|
||||
<OverlayTrigger
|
||||
placement="top"
|
||||
trigger={['click', 'hover', 'focus']}
|
||||
overlay={({arrowProps, hasDoneInitialMeasure, show, ...props}) => (
|
||||
<div className="k-popup-connectors" {...props}>
|
||||
<RenderResources
|
||||
resources={resources}
|
||||
library={resource_tip}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
rootClose
|
||||
>
|
||||
<div className="progress-container">
|
||||
<RenderResources
|
||||
resources={resources}
|
||||
library={resource_progress_renderers}
|
||||
/>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
)
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import React from 'react';
|
||||
import {useShopStore} from "./shop_store";
|
||||
|
||||
export function SearchBar() {
|
||||
const search_bar_value = useShopStore((state) => state.search_bar_value);
|
||||
const updateSearchBar = useShopStore((state) => state.updateSearchBar);
|
||||
return (
|
||||
<div className="search-catalog form-outline">
|
||||
<input type="search"
|
||||
id="search_bar"
|
||||
className="search-catalog-input form-control"
|
||||
placeholder="Search"
|
||||
value={search_bar_value}
|
||||
onChange={event => updateSearchBar(event.target.value)}
|
||||
aria-label="Search"/>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
import React, {useEffect} from 'react';
|
||||
import {DragDropContext} from "@hello-pangea/dnd";
|
||||
|
||||
// #!render_count
|
||||
import {useRenderCount} from "@uidotdev/usehooks";
|
||||
|
||||
|
||||
import {Layout} from "./Layout";
|
||||
import {Catalog} from "./Catalog";
|
||||
import {OrderPanel} from "./OrderPanel";
|
||||
import {useShopStore} from "./shop_store";
|
||||
|
||||
/**
|
||||
* Component that renders the entire shop
|
||||
*/
|
||||
|
||||
export function Shop() {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
const addCardFromCatalog = useShopStore((state) => state.addCardFromCatalog);
|
||||
const initExtData = useShopStore((state) => state.initExtData);
|
||||
const moveCard = useShopStore((state) => state.moveCard);
|
||||
const deleteCard = useShopStore((state) => state.deleteCard);
|
||||
const cardIndexById = useShopStore((state) => state.cardIndexById);
|
||||
|
||||
const handleOnDragEnd = (drop_result, _provided) => {
|
||||
if (!drop_result.destination) {
|
||||
console.warn("No drop destination");
|
||||
console.log(drop_result)
|
||||
return;
|
||||
}
|
||||
if (drop_result.source.droppableId === "catalog")
|
||||
addCardFromCatalog(drop_result.destination.droppableId, drop_result.source.index, drop_result.destination.index);
|
||||
else if (drop_result.destination.droppableId === "catalog")
|
||||
deleteCard(drop_result.source.droppableId, drop_result.source.index);
|
||||
else
|
||||
moveCard(drop_result.source.droppableId, drop_result.source.index, drop_result.destination.droppableId, drop_result.destination.index)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
addCardFromCatalog(null, [cardIndexById("eem_pwr_mod"), cardIndexById("kasli")], -1, true);
|
||||
initExtData();
|
||||
}, []);
|
||||
|
||||
// #!render_count
|
||||
console.log("Shop renders: ", renderCount)
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||
<Layout
|
||||
aside={
|
||||
<Catalog/>
|
||||
}
|
||||
main={(
|
||||
<OrderPanel
|
||||
title="Order hardware"
|
||||
description={(
|
||||
<p className="description">
|
||||
Drag and drop the cards you want into the crate below to see how
|
||||
the combination would look like. Configure the card settings by tapping on the top of
|
||||
the card; many of the options can be adjusted even after the card has been shipped.
|
||||
If you have any issues with this ordering system, or if you need other configurations,
|
||||
email us directly anytime at <a href="mailto:sales@m-labs.hk">sales@m-labs.hk</a>.
|
||||
The price excludes shipping, is estimated, and must be confirmed by a quote.
|
||||
</p>
|
||||
)}
|
||||
/>
|
||||
)}>
|
||||
</Layout>
|
||||
</DragDropContext>
|
||||
);
|
||||
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
import React, {useState} from "react";
|
||||
import {Modal} from "react-bootstrap";
|
||||
import {useShopStore} from "./shop_store";
|
||||
import {useClickAway} from "./options/useClickAway";
|
||||
|
||||
// #!render_count
|
||||
import {useRenderCount} from "@uidotdev/usehooks";
|
||||
import {Validation} from "./validate";
|
||||
|
||||
|
||||
const copyButtonStates = {
|
||||
[Validation.OK]: {
|
||||
style: "btn-outline-success",
|
||||
content: "✓ copied"
|
||||
},
|
||||
[Validation.Empty]: {
|
||||
style: "btn-outline-primary",
|
||||
content: "Copy"
|
||||
},
|
||||
[Validation.Invalid]: {
|
||||
style: "btn-outline-danger",
|
||||
content: "Error"
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export function ShowJSON() {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
const shouldShow = useShopStore((state) => state.shouldShowDescription);
|
||||
const description = useShopStore((state) => state.description);
|
||||
const closeDescription = useShopStore((state) => state.closeDescription);
|
||||
const showDescription = useShopStore((state) => state.showDescription);
|
||||
|
||||
const [copiedState, setCopiedState] = useState(Validation.Empty);
|
||||
|
||||
// #!render_count
|
||||
console.log("ShowJSON renders: ", renderCount)
|
||||
|
||||
const ref = useClickAway((e) => {
|
||||
if (e.type === "mousedown") // ignore touchstart
|
||||
closeDescription()
|
||||
}
|
||||
);
|
||||
|
||||
const copyToClipboard = (text) => {
|
||||
try {
|
||||
navigator.clipboard.writeText(text)
|
||||
.then((_value) => { // success
|
||||
setCopiedState(Validation.OK);
|
||||
setTimeout(() => {setCopiedState(Validation.Empty)}, 1500);
|
||||
}, (reason) => { // error
|
||||
setCopiedState(Validation.Invalid);
|
||||
setTimeout(() => {setCopiedState(Validation.Empty)}, 3000);
|
||||
console.warn("Copy to clipboard rejected: ", reason)
|
||||
});
|
||||
} catch (e) {
|
||||
setCopiedState(Validation.Invalid);
|
||||
setTimeout(() => {setCopiedState(Validation.Empty)}, 3000);
|
||||
console.warn("Copy to clipboard error: ", e)
|
||||
}
|
||||
};
|
||||
|
||||
return (<>
|
||||
<input
|
||||
className="btn btn-outline-primary w-100 m-0 mb-2 mb-sm-0 me-sm-2"
|
||||
style={{'cursor': 'pointer', 'fontWeight': '700'}}
|
||||
defaultValue="Show JSON"
|
||||
onClick={showDescription}
|
||||
readOnly={true}/>
|
||||
<Modal show={shouldShow} animation={true} className="rfqFeedback" centered>
|
||||
<Modal.Body ref={ref}>
|
||||
<textarea
|
||||
value={description}
|
||||
className="form-control w-100"
|
||||
rows={10}
|
||||
readOnly={true}
|
||||
placeholder="There should be description of the crate"/>
|
||||
|
||||
<div className="d-flex flex-column flex-sm-row justify-content-end">
|
||||
{window.isSecureContext && (
|
||||
<a type="button"
|
||||
onClick={() => {
|
||||
copyToClipboard(description)
|
||||
}}
|
||||
className={"btn btn-sm m-0 mb-1 mt-2 mb-sm-0 me-sm-2 " + copyButtonStates[copiedState].style}
|
||||
>
|
||||
{copyButtonStates[copiedState].content}
|
||||
</a>
|
||||
)}
|
||||
|
||||
<a type="button" onClick={closeDescription}
|
||||
className="btn btn-sm btn-outline-primary m-0 mb-1 mt-2 mb-sm-0 me-sm-2">Close</a>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</>)
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import {range} from "./utils";
|
||||
import React from "react";
|
||||
import {useShopStore} from "./shop_store";
|
||||
import {SummaryCrateHeader} from "./SummaryCrateHeader";
|
||||
import {SummaryCrateCard} from "./SummaryCrateCard";
|
||||
import {SummaryCratePricedOptions} from "./SummaryCratePricedOptions";
|
||||
|
||||
// #!render_count
|
||||
import {useRenderCount} from "@uidotdev/usehooks";
|
||||
|
||||
|
||||
export function SummaryCrate({crate_index}) {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
const crate_id = useShopStore((state) => state.crates[crate_index].id);
|
||||
const crate_len = useShopStore((state) => state.crates[crate_index].items.length);
|
||||
|
||||
// #!render_count
|
||||
console.log("SummaryCrate renders: ", renderCount)
|
||||
|
||||
return (
|
||||
<tbody key={"summary_crate_body" + crate_id}>
|
||||
|
||||
<SummaryCrateHeader crate_index={crate_index}/>
|
||||
|
||||
{range(0, crate_len).map((index, _i) =>
|
||||
<SummaryCrateCard crate_index={crate_index} card_index={index} key={"summary_crate_" + crate_id + "_" +index} />
|
||||
)}
|
||||
|
||||
<SummaryCratePricedOptions crate_index={crate_index}/>
|
||||
</tbody>
|
||||
)
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
import {compareObjectsEmptiness, formatMoney} from "./utils";
|
||||
import {WarningIndicator} from "./CardWarnings";
|
||||
import React from "react";
|
||||
import {useShopStore} from "./shop_store";
|
||||
import {OptionsSummaryWrapper} from "./OptionsWrapper";
|
||||
|
||||
// #!render_count
|
||||
import {useRenderCount} from "@uidotdev/usehooks";
|
||||
|
||||
|
||||
export function SummaryCrateCard({crate_index, card_index}) {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
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 crate_id = useShopStore((state) => state.crates[crate_index].id);
|
||||
const card = useShopStore((state) => state.crates[crate_index].items[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);
|
||||
|
||||
|
||||
// #!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;
|
||||
|
||||
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)}
|
||||
onMouseLeave={() => resetHighlight()}>
|
||||
<td className="item-card-name tabbed">
|
||||
<div>{`${card.name_number} ${card.name} ${card.name_codename}`}</div>
|
||||
</td>
|
||||
|
||||
<td className="price">
|
||||
<div className="d-inline-flex align-content-center">
|
||||
{`${currency} ${formatMoney(card.price)}`}
|
||||
|
||||
<button onClick={() => deleteCard(crate_id, card_index)}>
|
||||
<img src="/images/shop/icon-remove.svg" className="d-block"/>
|
||||
</button>
|
||||
|
||||
<div style={{'width': '45px', 'height': '20px'}}
|
||||
className="d-inline-flex align-content-center align-self-center justify-content-evenly">
|
||||
{(warnings ? (
|
||||
<WarningIndicator crate_index={crate_index} card_index={card_index}/>
|
||||
) : (
|
||||
<span style={{
|
||||
'display': 'inline-block',
|
||||
'minWidth': '20px',
|
||||
}}> </span>
|
||||
))}
|
||||
{((options && options_data) ? (
|
||||
<OptionsSummaryWrapper crate_index={crate_index} card_index={card_index}/>
|
||||
) : (
|
||||
<span style={{
|
||||
'display': 'inline-block',
|
||||
'width': '20px',
|
||||
}}> </span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>);
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
import {formatMoney} from "./utils";
|
||||
import React from "react";
|
||||
import {useShopStore} from "./shop_store";
|
||||
|
||||
// #!render_count
|
||||
import {useRenderCount} from "@uidotdev/usehooks";
|
||||
|
||||
export function SummaryCrateHeader({crate_index}) {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
const currency = useShopStore((state) => state.currency);
|
||||
const crateParams = useShopStore((state) => state.crateParams);
|
||||
const clearCrate = useShopStore((state) => state.clearCrate);
|
||||
const delCrate = useShopStore((state) => state.delCrate);
|
||||
|
||||
const crate_mode = useShopStore((state) => state.crates[crate_index].crate_mode);
|
||||
const crate_id = useShopStore((state) => state.crates[crate_index].id);
|
||||
const crate_name = useShopStore((state) => state.crates[crate_index].name);
|
||||
const modes_order = useShopStore((state) => state.modes_order);
|
||||
const crate_mode_displayed = modes_order.includes(crate_mode);
|
||||
|
||||
|
||||
// #!render_count
|
||||
console.log("SummaryCrateHeader renders: ", renderCount)
|
||||
|
||||
let crate_type = crateParams(crate_mode);
|
||||
|
||||
return (
|
||||
<tr key={"summary_crate_" + crate_id}>
|
||||
<td className="item-card-name">{!!crate_name ? crate_name : crate_type.name + " #" + crate_index}</td>
|
||||
<td className="price">
|
||||
<div className="d-inline-flex">
|
||||
|
||||
{crate_mode_displayed && `${currency} ${formatMoney(crate_type.price)}`}
|
||||
|
||||
<button onClick={() => clearCrate(crate_id)}>
|
||||
<img src="/images/shop/icon-clear.svg" alt="empty crate"/>
|
||||
</button>
|
||||
|
||||
{
|
||||
crate_mode_displayed ? (
|
||||
<button onClick={() => delCrate(crate_id)}>
|
||||
<img src="/images/shop/icon-remove.svg" alt="remove crate"/>
|
||||
</button>
|
||||
) : <span className="span-with-margin"></span>
|
||||
}
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
import {formatMoney} from "./utils";
|
||||
import React from "react";
|
||||
import {useShopStore} from "./shop_store";
|
||||
import {ProcessOptionsToData} from "./options/Options";
|
||||
|
||||
// #!render_count
|
||||
import {useRenderCount} from "@uidotdev/usehooks";
|
||||
|
||||
export function SummaryCratePricedOptions({crate_index}) {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
const currency = useShopStore((state) => state.currency);
|
||||
const crate_id = useShopStore((state) => state.crates[crate_index].id);
|
||||
const optionsPrices = useShopStore((state) => state.crate_prices);
|
||||
const updateOptions = useShopStore((state) => state.updateCrateOptions);
|
||||
const options_data = useShopStore((state) => state.crates[crate_index].options_data || {});
|
||||
|
||||
const options = ProcessOptionsToData({options: optionsPrices, data: options_data});
|
||||
|
||||
// #!render_count
|
||||
console.log("SummaryCratePricedOptions renders: ", renderCount)
|
||||
|
||||
return options.map((option, _i) => (
|
||||
<tr key={"summary_crate_" + crate_id +"option_" + option.id}>
|
||||
<td className="item-card-name tabbed">
|
||||
<div>{option.title}</div>
|
||||
</td>
|
||||
|
||||
<td className="price">
|
||||
<div className="d-inline-flex align-content-center">
|
||||
{`${currency} ${formatMoney(option.price)}`}
|
||||
|
||||
<button onClick={() => updateOptions(crate_id, option.disable_patch)}>
|
||||
<img src="/images/shop/icon-remove.svg" className="d-block"/>
|
||||
</button>
|
||||
|
||||
<div style={{'width': '45px', 'height': '20px'}} className="d-inline"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
));
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
import {range} from "./utils";
|
||||
import React from "react";
|
||||
import {useShopStore} from "./shop_store";
|
||||
import {SummaryCrate} from "./SummaryCrate";
|
||||
|
||||
// #!render_count
|
||||
import {useRenderCount} from "@uidotdev/usehooks";
|
||||
import {SummaryOrderPricedOptions} from "./SummaryOrderPricedOptions";
|
||||
import {SummaryOrderShipping} from "./SummaryOrderShipping";
|
||||
|
||||
export function SummaryCrates() {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
const crates_l = useShopStore((state) => state.crates.length);
|
||||
|
||||
// #!render_count
|
||||
console.log("SummaryCrates renders: ", renderCount)
|
||||
|
||||
return (
|
||||
<>
|
||||
{range(0, crates_l).map((index, _i) => {
|
||||
return <SummaryCrate crate_index={index} key={"summary_crate_body_" + index} />
|
||||
})}
|
||||
<SummaryOrderPricedOptions/>
|
||||
<SummaryOrderShipping/>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
import React from 'react';
|
||||
import {SummaryCrates} from "./SummaryCrates";
|
||||
import {SummaryTotalPrice} from "./SummaryTotalPrice";
|
||||
|
||||
// #!render_count
|
||||
import {useRenderCount} from "@uidotdev/usehooks";
|
||||
|
||||
|
||||
/**
|
||||
* Components that displays the list of card that are used in the crate.
|
||||
* It is a summary of purchase
|
||||
*/
|
||||
export function SummaryOrder() {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
// #!render_count
|
||||
console.log("SummaryOrder renders: ", renderCount)
|
||||
|
||||
return (
|
||||
<div className="summary-price">
|
||||
|
||||
<table>
|
||||
<SummaryCrates/>
|
||||
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td className="item-card-name">Price estimate</td>
|
||||
<td className="price">
|
||||
<SummaryTotalPrice/>
|
||||
|
||||
<span style={{
|
||||
'display': 'inline-block',
|
||||
'width': '30px',
|
||||
}}> </span>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
import {formatMoney} from "./utils";
|
||||
import React from "react";
|
||||
import {useShopStore} from "./shop_store";
|
||||
import {ProcessOptionsToData} from "./options/Options";
|
||||
|
||||
// #!render_count
|
||||
import {useRenderCount} from "@uidotdev/usehooks";
|
||||
|
||||
export function SummaryOrderPricedOptions() {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
const currency = useShopStore((state) => state.currency);
|
||||
const optionsPrices = useShopStore((state) => state.order_prices);
|
||||
const updateOptions = useShopStore((state) => state.updateOrderOptions);
|
||||
const options_data = useShopStore((state) => state.order_options_data);
|
||||
|
||||
const options = ProcessOptionsToData({options: optionsPrices, data: options_data});
|
||||
|
||||
// #!render_count
|
||||
console.log("SummaryOrderPricedOptions renders: ", renderCount)
|
||||
|
||||
return <tbody key={"summary_order_body"}>
|
||||
{options.map((option, _i) => (
|
||||
<tr key={"summary_order" + "option_" + option.id}>
|
||||
<td className="item-card-name">
|
||||
<div>{option.title}</div>
|
||||
</td>
|
||||
<td className="price">
|
||||
<div className="d-inline-flex align-content-center">
|
||||
{`${currency} ${formatMoney(option.price)}`}
|
||||
|
||||
<button onClick={() => updateOptions(option.disable_patch)}>
|
||||
<img src="/images/shop/icon-remove.svg" className="d-block"/>
|
||||
</button>
|
||||
|
||||
<div style={{'width': '45px', 'height': '20px'}} className="d-inline"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>;
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
import {formatMoney} from "./utils";
|
||||
import React from "react";
|
||||
import {useShopStore} from "./shop_store";
|
||||
import {ProcessOptions, ProcessOptionsToData} from "./options/Options";
|
||||
|
||||
// #!render_count
|
||||
import {useRenderCount} from "@uidotdev/usehooks";
|
||||
|
||||
export function SummaryOrderShipping() {
|
||||
// #!render_count
|
||||
const renderCount = useRenderCount();
|
||||
|
||||
const shipping_summary = useShopStore((state) => state.shipping_summary);
|
||||
const options_data = useShopStore((state) => state.order_options_data);
|
||||
|
||||
// #!render_count
|
||||
console.log("SummaryOrderShipping renders: ", renderCount)
|
||||
|
||||
const options = ProcessOptions({
|
||||
options: shipping_summary,
|
||||
data: options_data,
|
||||
id: "shipping_options",
|
||||
target: null,
|
||||
});
|
||||
|
||||
return <tbody key="summary_shipping_order_body">
|
||||
{options.map((option, i) => (
|
||||
<tr key={"summary_shipping_order_option_" + i} id={"summary_shipping_order_option_" + i}>
|
||||
<td className="item-card-name" key={"summary_shipping_order_key_option_" + i} id={"summary_shipping_order_key_option_" + i}>
|
||||
{option}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import {useShopStore} from "./shop_store";
|
||||
import {formatMoney} from "./utils";
|
||||
import React from "react";
|
||||
|
||||
export function SummaryTotalPrice() {
|
||||
const currency = useShopStore((state) => state.currency);
|
||||
const total_price = useShopStore((state) => state.total_order_price);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{currency} {formatMoney(total_price)}
|
||||
<button style={{'opacity': '0', 'cursor': 'initial'}}>
|
||||
<img src="/images/shop/icon-remove.svg" alt="icon remove"/>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
const count_item_occupied_eem = (item) => {
|
||||
if (!item.options_data
|
||||
|| item.options_data.ext_pwr === false
|
||||
|| item.options_data.mono_eem === false
|
||||
)
|
||||
return (item.consumes && item.consumes.eem) || 0;
|
||||
else if (item.options_data.ext_pwr === true)
|
||||
return 0;
|
||||
else if (item.options_data.mono_eem === true || item.options_data.n_eem === "1 EEM")
|
||||
return 1;
|
||||
else if (item.options_data.n_eem === "3 EEM")
|
||||
return 3;
|
||||
|
||||
return (item.consumes && item.consumes.eem) || 0;
|
||||
}
|
||||
|
||||
const count_item_occupied_clock = (item) => {
|
||||
return (item.options_data && (item.options_data.ext_clk === true || (item.options_data.ext_clk && item.options_data.ext_clk.checked === true)) ) ? 0 : ((item.consumes && item.consumes.clk) || 0);
|
||||
}
|
||||
|
||||
const count_item_occupied_idc = (item) => {
|
||||
return (item.consumes && item.consumes.idc) || 0;
|
||||
}
|
||||
|
||||
const count_item_occupied_hp = (item) => {
|
||||
return (item.consumes && item.consumes.hp) || 0;
|
||||
}
|
||||
|
||||
export const item_occupied_counters = {
|
||||
"eem": count_item_occupied_eem,
|
||||
"clk": count_item_occupied_clock,
|
||||
"idc": count_item_occupied_idc,
|
||||
"hp": count_item_occupied_hp,
|
||||
}
|
||||
|
||||
function CounterFactory(name) {
|
||||
return (data, index) => {
|
||||
let count = 0;
|
||||
for (let i = index + 1; i < data.length; i++) {
|
||||
count += item_occupied_counters[name](data[i]);
|
||||
if (data[i].resources && !!data[i].resources.find((value, _i) => value.name === name)) break;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
export const resource_counters = {
|
||||
"eem": CounterFactory("eem"),
|
||||
"clk": CounterFactory("clk"),
|
||||
"idc": CounterFactory("idc"),
|
||||
"hp": CounterFactory("hp"),
|
||||
}
|
||||
|
||||
export function CountResources(data, index) {
|
||||
if (!data[index].resources) return null;
|
||||
let result = [];
|
||||
data[index].resources.forEach((item, _) => {
|
||||
if (resource_counters[item.name]) result.push({
|
||||
name: item.name,
|
||||
occupied: resource_counters[item.name](data, index),
|
||||
max: item.max
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function FillResources(data, disabled) {
|
||||
return data.map((element, index) => {
|
||||
element.counted_resources = disabled ? [] : CountResources(data, index);
|
||||
return element;
|
||||
})
|
||||
}
|
||||
|
||||
export function hp_to_slots(hp) {
|
||||
return Math.trunc(hp / 4);
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
import {useShopStore} from "./shop_store";
|
||||
import {FilterOptions} from "./options/utils";
|
||||
import {v4 as uuidv4} from "uuid";
|
||||
|
||||
|
||||
export function validateJSON(description) {
|
||||
let crates_raw;
|
||||
let order_options;
|
||||
try {
|
||||
const parsed = JSON.parse(description);
|
||||
// here we can check additional fields
|
||||
crates_raw = parsed.crates;
|
||||
order_options = parsed.options;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
if (!order_options) return false;
|
||||
const crate_modes = useShopStore.getState().crate_modes;
|
||||
const modes_order = useShopStore.getState().modes_order;
|
||||
const pn_to_card = useShopStore.getState().pn_to_cards;
|
||||
|
||||
try {
|
||||
for (const crate of crates_raw) {
|
||||
if (!crate.type || !crate.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;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// only last one should be spare cards
|
||||
return crates_raw.filter((crate) => !modes_order.includes(crate.type)).length === 1 && crates_raw[crates_raw.length - 1].type === "no_crate";
|
||||
}
|
||||
|
||||
// no validation in this function
|
||||
export function JSONToCrates(description) {
|
||||
const parsed = JSON.parse(description);
|
||||
const crates_raw = parsed.crates;
|
||||
const pn_to_card = useShopStore.getState().getCardDescriptionByPn;
|
||||
|
||||
const crates = Array.from(crates_raw.map((crate, c_i) => ({
|
||||
id: crate.type === "no_crate" ? "spare" : "crate" + c_i,
|
||||
name: crate.type === "no_crate" ? "Spare cards" : undefined,
|
||||
options_data: crate.options,
|
||||
crate_mode: crate.type,
|
||||
items: Array.from(crate.items.map((card, _i) => ({
|
||||
...pn_to_card(card.pn),
|
||||
id: uuidv4(),
|
||||
options_data: card.options || {}
|
||||
}))),
|
||||
warnings: [],
|
||||
occupiedHP: 0,
|
||||
})));
|
||||
|
||||
return {
|
||||
// some additional fields go here
|
||||
order_options_data: parsed.options,
|
||||
crates: crates
|
||||
};
|
||||
}
|
||||
|
||||
export function CratesToJSON(crates) {
|
||||
const crateOptions = useShopStore.getState().crate_options;
|
||||
const orderOptions = useShopStore.getState().order_options;
|
||||
const orderOptionsData = useShopStore.getState().order_options_data;
|
||||
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
|
||||
}))),
|
||||
type: crate.crate_mode,
|
||||
options: FilterOptions(crateOptions, crate.options_data)
|
||||
}))),
|
||||
options: FilterOptions(orderOptions, orderOptionsData)
|
||||
}, null, 2)
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
import React, {useState} from "react";
|
||||
import {useClickAway} from "./useClickAway";
|
||||
import {ProcessOptions} from "./Options";
|
||||
import {Notification} from "./Notification";
|
||||
|
||||
export function DialogPopup({options, data, target, id, big, first, last, options_class,
|
||||
sideMenuIsOpen, displayNotification, onHideNotification}) {
|
||||
const [show, setShow] = useState(false);
|
||||
const ref = useClickAway((e) => {
|
||||
if (e.type === "mousedown") // ignore touchstart
|
||||
setShow(false)
|
||||
}
|
||||
);
|
||||
|
||||
let div_classes = `overlayVariant border rounded ${big ? "overlay-bigcard" : "overlay-smallcard"} ${(!big && first) ? "overlay-first" : ""} ${(!big && last) ? "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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import {OverlayTrigger, Tooltip} from "react-bootstrap";
|
||||
import React from "react";
|
||||
|
||||
export function Notification({id, tip, content, sideMenuIsOpen, show, onHide}) {
|
||||
return (
|
||||
<OverlayTrigger
|
||||
placement="top"
|
||||
trigger={["click", "hover"]}
|
||||
style={{display: 'inline'}}
|
||||
show={show}
|
||||
overlay={props => <Tooltip id={id} {...props}>{tip}</Tooltip>}
|
||||
rootClose={!sideMenuIsOpen}
|
||||
onToggle={onHide}
|
||||
popperConfig={{
|
||||
strategy: 'fixed'
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</OverlayTrigger>
|
||||
)
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
import React from "react";
|
||||
import {apply as json_logic_apply} from "json-logic-js";
|
||||
import {componentsList} from "./components/components";
|
||||
import {true_type_of} from "./utils";
|
||||
|
||||
export function ProcessOptions({options, data, target, id}) {
|
||||
let options_t = true_type_of(options);
|
||||
|
||||
if (options_t === "array") {
|
||||
return Array.from(
|
||||
options.map((option_item, i) => ProcessOptions({
|
||||
options: option_item,
|
||||
data: data,
|
||||
target: target,
|
||||
id: id + i
|
||||
}))
|
||||
);
|
||||
} else if (options_t === "object") {
|
||||
if (
|
||||
true_type_of(options.type) === "string" &&
|
||||
(true_type_of(options.args) === "object" || true_type_of(options.items) === "array")
|
||||
) {
|
||||
if (options.type in componentsList) {
|
||||
return componentsList[options.type](target, id + options.type, data, options.args);
|
||||
} else if (options.type === "Group") {
|
||||
return (
|
||||
<div className="border rounded options-group" key={id + "group"}>
|
||||
{ProcessOptions({
|
||||
options: json_logic_apply(options.items, data),
|
||||
data: data,
|
||||
target: target,
|
||||
id: id
|
||||
})}
|
||||
</div>);
|
||||
} else {
|
||||
return componentsList["Default"](options.type, id + "missing");
|
||||
}
|
||||
} else {
|
||||
return ProcessOptions({options: json_logic_apply(options, data), data: data, target: target, id: id});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function ProcessOptionsToData({options, data}) {
|
||||
let options_t = true_type_of(options);
|
||||
if (options_t === "array") {
|
||||
return Array.from(
|
||||
options.map((option_item, _i) => ProcessOptionsToData({
|
||||
options: option_item,
|
||||
data: data,
|
||||
}))
|
||||
).filter((item, _i) => !!item).flat();
|
||||
} else if (options_t === "object") {
|
||||
if (true_type_of(options.title) === "string") {
|
||||
return options;
|
||||
} else {
|
||||
return ProcessOptionsToData({options: json_logic_apply(options, data), data: data});
|
||||
}
|
||||
} else {
|
||||
//throw Error("Incompatible type for the option: " + options_t)
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
import React, {useEffect, useState} from "react";
|
||||
import {useClickAway} from "./useClickAway";
|
||||
import {FilterOptions, true_type_of} from "./utils";
|
||||
|
||||
export function SummaryPopup({id, options, data}) {
|
||||
const [show, setShow] = useState(false);
|
||||
const [position, setPosition] = useState({x: 0, y: 0});
|
||||
const [size, setSize] = useState({w: 0, h: 0});
|
||||
let display_options = FilterOptions(options, data);
|
||||
const close = () => {
|
||||
setShow(false);
|
||||
document.removeEventListener("scroll", handleScroll, true);
|
||||
}
|
||||
|
||||
const ref = useClickAway(close);
|
||||
|
||||
const reposition = () => {
|
||||
let popup_button = document.getElementById(id + "img");
|
||||
if (!popup_button) {
|
||||
document.removeEventListener("scroll", handleScroll, true);
|
||||
return;
|
||||
}
|
||||
let rect = popup_button.getBoundingClientRect()
|
||||
let pos_x = (rect.left + rect.right) / 2;
|
||||
let pos_y = (rect.top + rect.bottom) / 2;
|
||||
if (pos_x + size.w > window.innerWidth) {
|
||||
setPosition({x: pos_x - size.w - 20, y: pos_y - size.h / 2});
|
||||
} else {
|
||||
setPosition({x: pos_x - size.w / 2, y: pos_y - size.h - 20});
|
||||
}
|
||||
}
|
||||
|
||||
const handleScroll = (e) => {
|
||||
if (e.target !== document.getElementById(id)) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
let popup = document.getElementById(id);
|
||||
let width = popup.offsetWidth;
|
||||
let height = popup.offsetHeight;
|
||||
setSize({w: width, h: height});
|
||||
reposition()
|
||||
}
|
||||
}, [show])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
reposition();
|
||||
}
|
||||
}, [show, size])
|
||||
|
||||
const handleClick = (_event) => {
|
||||
setShow(!show);
|
||||
if (!show) {
|
||||
document.addEventListener("scroll", handleScroll, true);
|
||||
}
|
||||
};
|
||||
|
||||
const stringify = (value) => {
|
||||
let value_type = true_type_of(value);
|
||||
if (value_type === "string") {
|
||||
return value;
|
||||
} else if (value_type === "object") {
|
||||
if (value.checked === false) {
|
||||
return "off";
|
||||
} else if (value.checked === true && value.text) {
|
||||
return value.text;
|
||||
}
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<img className="alert-info d-block" src={show ? "/images/shop/icon-close.svg" : "/images/shop/icon-customize.svg"}
|
||||
id={id + "img"}
|
||||
onClick={handleClick}/>
|
||||
<div style={{'display': show ? 'flex' : 'none', 'top': position.y, 'left': position.x}}
|
||||
className="overlayVariant card border rounded"
|
||||
id={id}>
|
||||
<div className="card-body">
|
||||
{Array.from(Object.entries(display_options)
|
||||
.filter(([key, value], _) => key !== "ext_data")
|
||||
.map(([key, value], _) => {
|
||||
return (<p className="card-text" key={id + key}><i>{key}</i>: {stringify(value)}</p>);
|
||||
}))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import React from "react";
|
||||
import {apply as json_logic_apply, add_operation as json_add_operation} from "json-logic-js";
|
||||
|
||||
|
||||
json_add_operation("lower", (some_str) => some_str && some_str.toLowerCase(some_str));
|
||||
json_add_operation("upper", (some_str) => some_str && some_str.toUpperCase(some_str));
|
||||
json_add_operation("capitalize", (some_str) => some_str && some_str.capitalizeFirstLetter(some_str));
|
||||
|
||||
export function Label(target, id, data, {content}) {
|
||||
const resulting_string = json_logic_apply(content, data).flat().join("");
|
||||
|
||||
return (
|
||||
<div id={id+"label"} key={id+"label"} className="options-label">
|
||||
{resulting_string}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
import React, {Component} from "react";
|
||||
import {Tip} from "./Tip";
|
||||
import {Validation} from "../validation";
|
||||
|
||||
class Line extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// Initialize the state object with the initial values from the props
|
||||
this.state = {
|
||||
text: props.outvar in props.data ? props.data[props.outvar] : (props.fallback ? props.fallback : ""),
|
||||
valid: true
|
||||
};
|
||||
// Bind the event handler to this
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.props.target.construct(this.props.outvar, this.state.text);
|
||||
}
|
||||
|
||||
handleChange(element) {
|
||||
let text = element.target.value;
|
||||
this.setState({
|
||||
text: text,
|
||||
valid: this.props.validator ? this.props.validator(text) : true
|
||||
});
|
||||
this.props.target.update(this.props.outvar, text);
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, current_state) {
|
||||
if (current_state.text !== props.data[props.outvar]) {
|
||||
return {
|
||||
text: props.data[props.outvar]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
render() {
|
||||
let key = this.props.id + this.props.outvar;
|
||||
return (
|
||||
<div className="shop-line" key={this.props.id}>
|
||||
<label htmlFor={key} className="form-label">
|
||||
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
|
||||
{this.props.title}:
|
||||
</label>
|
||||
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
|
||||
<input type="text" className={`form-control form-control-sm ${this.state.valid ? "" : "options-invalid"}`} id={key} onChange={this.handleChange}
|
||||
value={this.state.text}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function LineWrapper(target, id, data, {title, fallback, outvar, icon, tip, classes, validator}) {
|
||||
return <Line target={target} title={title} fallback={fallback} outvar={outvar} icon={icon} tip={tip} key={id}
|
||||
id={id} data={data} classes={classes} validator={validator && Validation[validator.name](validator.params)}/>;
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
import React, {Component} from "react";
|
||||
import {Tip} from "./Tip";
|
||||
|
||||
class Radio extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// Initialize the state object with the initial values from the props
|
||||
this.state = {
|
||||
variant: props.outvar in props.data ? props.data[props.outvar] : props.variants[props.fallback ? props.fallback : 0],
|
||||
};
|
||||
|
||||
// Bind the event handler to this
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.props.target.construct(this.props.outvar, this.state.variant);
|
||||
}
|
||||
|
||||
handleClick(variant) {
|
||||
// Update the state object with the new value for outvar
|
||||
this.setState({
|
||||
...this.state,
|
||||
variant: variant
|
||||
});
|
||||
this.props.target.update(this.props.outvar, variant);
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, current_state) {
|
||||
if (current_state.variant !== props.data[props.outvar]) {
|
||||
return {
|
||||
variant: props.data[props.outvar]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
render() {
|
||||
let key = this.props.id + this.props.outvar;
|
||||
return (
|
||||
<div className="shop-radio" key={this.props.id}>
|
||||
<div style={{"display": "inline"}} className="shop-radio-label">
|
||||
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
|
||||
{this.props.title}
|
||||
</div>
|
||||
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
|
||||
<div className="d-block">
|
||||
{this.props.variants.map((variant, _) => (
|
||||
<div className={`form-check shop-radio-variant ${this.props.classes}`} key={key + variant}>
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="radio"
|
||||
name={key}
|
||||
id={key + variant}
|
||||
checked={this.state.variant === variant}
|
||||
onClick={() => this.handleClick(variant)}
|
||||
onChange={() => this.handleClick(variant)}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor={key + variant}>
|
||||
{variant}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function RadioWrapper(target, id, data, {title, variants, outvar, fallback, icon, tip, classes}) {
|
||||
return <Radio target={target} title={title} variants={variants} outvar={outvar} icon={icon} tip={tip} key={id}
|
||||
fallback={fallback} classes={classes}
|
||||
id={id} data={data}/>;
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
import React, {Component} from "react";
|
||||
import {Tip} from "./Tip";
|
||||
|
||||
class Switch extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// Initialize the state object with the initial values from the props
|
||||
this.state = {
|
||||
checked: props.outvar in props.data ? !!(props.data[props.outvar]) : !!(props.fallback)
|
||||
};
|
||||
|
||||
// Bind the event handler to this
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.props.target.construct(this.props.outvar, this.state.checked);
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
// Update the state object with the new value for outvar
|
||||
let new_checked = !this.state.checked;
|
||||
this.setState({
|
||||
checked: new_checked
|
||||
});
|
||||
this.props.target.update(this.props.outvar, new_checked);
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, current_state) {
|
||||
if (current_state.checked !== props.data[props.outvar]) {
|
||||
return {
|
||||
checked: props.data[props.outvar]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
render() {
|
||||
let key = this.props.id + this.props.outvar;
|
||||
return (
|
||||
<div className="shop-switch" key={this.props.id}>
|
||||
<div className="form-check form-switch" key={key}>
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
id={key}
|
||||
checked={this.state.checked}
|
||||
onClick={this.handleClick}
|
||||
onChange={this.handleClick}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor={key} style={{"display": "inline"}}>
|
||||
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
|
||||
{this.props.title}
|
||||
</label>
|
||||
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function SwitchWrapper(target, id, data, {title, fallback, outvar, icon, tip, classes}) {
|
||||
return <Switch target={target} title={title} fallback={fallback} outvar={outvar} icon={icon} tip={tip} key={id}
|
||||
id={id} data={data} classes={classes}/>;
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
import React, {Component} from "react";
|
||||
import {Tip} from "./Tip";
|
||||
import {Validation} from "../validation";
|
||||
|
||||
class SwitchLine extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// Initialize the state object with the initial values from the props
|
||||
this.state = {
|
||||
text: props.outvar in props.data ? props.data[props.outvar].text : (props.fallback ? props.fallback.text : ""),
|
||||
checked: props.outvar in props.data ? props.data[props.outvar].checked : (props.fallback ? props.fallback.checked : false),
|
||||
valid: true
|
||||
};
|
||||
// Bind the event handler to this
|
||||
this.handleText = this.handleText.bind(this);
|
||||
this.handleCheck = this.handleCheck.bind(this);
|
||||
this.props.target.construct(this.props.outvar, this.state);
|
||||
}
|
||||
|
||||
handleText(element) {
|
||||
let new_state = {
|
||||
...this.state,
|
||||
text: element.target.value,
|
||||
valid: this.props.validator ? this.props.validator(element.target.value) : true
|
||||
}
|
||||
this.setState(new_state);
|
||||
this.props.target.update(this.props.outvar, new_state);
|
||||
}
|
||||
|
||||
handleCheck() {
|
||||
// Update the state object with the new value for outvar
|
||||
let new_state = {
|
||||
...this.state,
|
||||
checked: !this.state.checked
|
||||
}
|
||||
this.setState(new_state);
|
||||
this.props.target.update(this.props.outvar, new_state);
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, current_state) {
|
||||
if (current_state.checked !== props.data[props.outvar].checked || current_state.text !== props.data[props.outvar].text) {
|
||||
return {
|
||||
checked: props.data[props.outvar].checked,
|
||||
text: props.data[props.outvar].text,
|
||||
valid: this.props.validator ? this.props.validator(props.data[props.outvar].text) : true
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
render() {
|
||||
let key = this.props.id + this.props.outvar;
|
||||
return (
|
||||
<div className="shop-switch-line" key={this.props.id}>
|
||||
<div className="form-check form-switch" key={key}>
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
id={key + "switch"}
|
||||
checked={this.state.checked}
|
||||
onClick={this.handleCheck}
|
||||
onChange={this.handleCheck}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor={key + "switch"}>
|
||||
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
|
||||
{this.props.title}
|
||||
</label>
|
||||
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
|
||||
</div>
|
||||
<input type="text" className={`form-control form-control-sm ${this.state.valid ? "" : "options-invalid"}`} id={key + "line"} onChange={this.handleText}
|
||||
value={this.state.text} disabled={!this.state.checked}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function SwitchLineWrapper(target, id, data, {title, fallback, outvar, icon, tip, classes, validator}) {
|
||||
return <SwitchLine target={target} title={title} fallback={fallback} outvar={outvar} icon={icon} tip={tip} key={id}
|
||||
id={id} data={data} classes={classes} validator={validator && Validation[validator.name](validator.params)}/>;
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import React from "react";
|
||||
import {OverlayTrigger, Tooltip} from "react-bootstrap";
|
||||
|
||||
export function Tip({id, tip}) {
|
||||
return (
|
||||
<OverlayTrigger
|
||||
placement="auto"
|
||||
trigger={['click', 'hover', 'focus']}
|
||||
style={{display: 'inline'}}
|
||||
overlay={props => <Tooltip id={id} {...props}>{tip}</Tooltip>}
|
||||
rootClose
|
||||
>
|
||||
<img src={`/images/shop/icon-reminder.svg`} className="options-icon"/>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
export function UnimplementedComponent(type, id) {
|
||||
//console.error("Missing component with type:", type)
|
||||
return <div key={type + id} style={{background: "red"}}>UNIMPLEMENTED</div>
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
import {LineWrapper} from "./Line";
|
||||
import {RadioWrapper} from "./Radio";
|
||||
import {SwitchWrapper} from "./Switch";
|
||||
import {SwitchLineWrapper} from "./SwitchLine";
|
||||
import {UnimplementedComponent} from "./UnimplementedComponent";
|
||||
import {Label} from "./Label";
|
||||
|
||||
|
||||
// Class components are used because we cannot use hooks for updating the state
|
||||
export const componentsList = {
|
||||
"Radio": RadioWrapper,
|
||||
"Switch": SwitchWrapper,
|
||||
"Line": LineWrapper,
|
||||
"SwitchLine": SwitchLineWrapper,
|
||||
"Label": Label,
|
||||
"Default": UnimplementedComponent,
|
||||
};
|
|
@ -1,31 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
// copy from "@uidotdev/usehooks"
|
||||
|
||||
export function useClickAway(cb) {
|
||||
const ref = React.useRef(null);
|
||||
const refCb = React.useRef(cb);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
refCb.current = cb;
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const handler = (e) => {
|
||||
const element = ref.current;
|
||||
if (element && !element.contains(e.target)) {
|
||||
refCb.current(e);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handler);
|
||||
document.addEventListener("touchstart", handler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handler);
|
||||
document.removeEventListener("touchstart", handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return ref;
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
import {apply as json_logic_apply} from "json-logic-js";
|
||||
import {componentsList} from "./components/components";
|
||||
|
||||
// https://stackoverflow.com/a/70511311
|
||||
export const true_type_of = (obj) => Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
|
||||
|
||||
export function FillExtCardData(data, index) {
|
||||
return {
|
||||
has_other_dio: data.filter((value, item_index) => index !== item_index && value.name &&value.name.endsWith("-TTL")).length > 0,
|
||||
has_dds: data.filter(((value, _) => value.name === "DDS" && value.name_number === "4410" && (!value.options_data || !value.options_data.mono_eem))).length > 0,
|
||||
has_sampler: data.filter(((value, _) => value.name === "Sampler" && (!value.options_data || !value.options_data.mono_eem))).length > 0,
|
||||
}
|
||||
}
|
||||
|
||||
export function FillExtCrateData(crate) {
|
||||
return {
|
||||
crate_mode: crate.crate_mode
|
||||
}
|
||||
}
|
||||
|
||||
export function FillExtOrderData(crates, modes_order) {
|
||||
return {
|
||||
has_crate: crates.filter((crate) => modes_order.includes(crate.crate_mode)).length >= 1,
|
||||
}
|
||||
}
|
||||
|
||||
export function FilterOptions(options, data) {
|
||||
let options_t = true_type_of(options);
|
||||
let target = {};
|
||||
|
||||
if (options_t === "array") {
|
||||
options.map((option_item, _) => {
|
||||
Object.assign(target, FilterOptions(option_item, data))
|
||||
});
|
||||
} else if (options_t === "object") {
|
||||
if (
|
||||
true_type_of(options.type) === "string" &&
|
||||
(true_type_of(options.args) === "object" || true_type_of(options.items) === "array")
|
||||
) {
|
||||
if (options.type in componentsList) {
|
||||
target[options.args.outvar] = data[options.args.outvar];
|
||||
} else if (options.type === "Group") {
|
||||
Object.assign(target, FilterOptions(json_logic_apply(options.items, data), data))
|
||||
}
|
||||
} else {
|
||||
Object.assign(target, FilterOptions(json_logic_apply(options, data), data))
|
||||
}
|
||||
}
|
||||
return target
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
|
||||
|
||||
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 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*$)/;
|
||||
|
||||
|
||||
return (text) => {
|
||||
return ipv6WithMaskPattern.test(text);
|
||||
}
|
||||
}
|
||||
|
||||
const ipv4or6 = (params) => {
|
||||
const ipv4Local = ipv4(params);
|
||||
const ipv6Local = ipv6(params);
|
||||
return (text) => {
|
||||
return ipv4Local(text) || ipv6Local(text);
|
||||
}
|
||||
}
|
||||
|
||||
const frequency = (params) => {
|
||||
const factors = {
|
||||
"mhz": 1e6,
|
||||
"khz": 1e3,
|
||||
"hz": 1e1,
|
||||
"ghz": 1e9,
|
||||
};
|
||||
|
||||
return (text) => {
|
||||
const splited = text.split(/(\s+)/);
|
||||
const numerator = parseFloat(splited[0]);
|
||||
if (splited.length !== 3 || isNaN(numerator)) return false;
|
||||
const factor = factors[splited[2].toLowerCase()];
|
||||
if (!factor) return false;
|
||||
const realFreq = factor * numerator;
|
||||
return realFreq >= (params.min || 10*factors.mhz) && realFreq <= (params.max || 1*factors.ghz);
|
||||
}
|
||||
}
|
||||
|
||||
export const Validation = {
|
||||
ipv4: ipv4,
|
||||
ipv6: ipv6,
|
||||
ipv4or6: ipv4or6,
|
||||
frequency: frequency
|
||||
};
|
|
@ -1,627 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
import {createWithEqualityFn} from "zustand/traditional";
|
||||
import {data as shared_data, itemsUnfoldedList} from "./utils";
|
||||
import {FillExtCrateData, FillExtOrderData, true_type_of} from "./options/utils";
|
||||
import {v4 as uuidv4} from "uuid";
|
||||
import {FillResources} from "./count_resources";
|
||||
import {FillExtCardData} from "./options/utils";
|
||||
import {TriggerCrateWarnings, TriggerWarnings} from "./warnings";
|
||||
import {Validation, validateEmail, validateNote, validateJSONInput} from "./validate";
|
||||
import {CratesToJSON, JSONToCrates} from "./json_porter";
|
||||
import {ProcessOptionsToData} from "./options/Options";
|
||||
|
||||
|
||||
const cards_to_pn_map = (cards) => {
|
||||
let result = {};
|
||||
Object.entries(cards).forEach(([key, card], _i) => { result[card.name_number] = key})
|
||||
return result;
|
||||
};
|
||||
|
||||
const useCatalog = ((set, get) => ({
|
||||
cards: shared_data.items,
|
||||
groups: shared_data.columns.catalog,
|
||||
cards_list: itemsUnfoldedList,
|
||||
currency: shared_data.currency,
|
||||
pn_to_cards: cards_to_pn_map(shared_data.items),
|
||||
getCardDescription: index => get().cards[get().cards_list[index]],
|
||||
getCardDescriptionByPn: pn => get().cards[get().pn_to_cards[pn]],
|
||||
cardIndexById: card_id => get().cards_list.findIndex((element) => (card_id === element))
|
||||
}));
|
||||
|
||||
const useOptionsNotification = ((set, get) => ({
|
||||
notificationCrateId: null,
|
||||
notificationCardIndex: null,
|
||||
notificationTimer: null,
|
||||
_showNotification: (crate_id, card_index) => set(state => ({
|
||||
notificationCrateId: crate_id,
|
||||
notificationCardIndex: card_index,
|
||||
notificationTimer: setTimeout(() => {
|
||||
state.hideNotification()
|
||||
}, 5000)
|
||||
})),
|
||||
showNotification: (crate_id, card_index) => {
|
||||
get().hideNotification()
|
||||
setTimeout(() => get()._showNotification(crate_id, card_index), 100);
|
||||
},
|
||||
hideNotification: () => set(state => ({
|
||||
notificationCrateId: null,
|
||||
notificationCardIndex: null,
|
||||
notificationTimer: (state.notificationTimer && clearTimeout(state.notificationTimer)) || null,
|
||||
}))
|
||||
}));
|
||||
|
||||
const useSearch = ((set, get) => ({
|
||||
search_index: Array.from(Object.values(shared_data.items)
|
||||
.map((card, _) => (
|
||||
[(card.name + " " + card.name_number + " " + card.name_codename).toLowerCase(), card.id]
|
||||
))),
|
||||
search_bar_value: "",
|
||||
listed_cards: [],
|
||||
updateSearchBar: text => set(state => ({
|
||||
search_bar_value: text,
|
||||
listed_cards: text.length > 0 ? Array.from(get().search_index
|
||||
.filter((card, _) => card[0].includes(text.toLowerCase()))
|
||||
.map(([index, card_id], _) => get().cards_list.findIndex(elem => elem === card_id))) : []
|
||||
}))
|
||||
}));
|
||||
|
||||
const useCrateModes = ((set, get) => ({
|
||||
crate_modes: shared_data.crateModes,
|
||||
modes_order: shared_data.crateModeOrder,
|
||||
crateParams: mode => get().crate_modes[mode],
|
||||
}));
|
||||
|
||||
const useCrateOptions = ((set, get) => ({
|
||||
crate_options: shared_data.crateOptions.options,
|
||||
crate_prices: shared_data.crateOptions.prices,
|
||||
|
||||
fillExtCrateData: (crate_id) => set(state => ({
|
||||
crates: state.crates.map((crate, _i) => {
|
||||
if (crate_id === crate.id) {
|
||||
const previous_options = crate.options_data || {};
|
||||
return {
|
||||
...crate,
|
||||
options_data: {
|
||||
...previous_options,
|
||||
ext_data: FillExtCrateData(crate)
|
||||
}
|
||||
}
|
||||
}
|
||||
else return crate;
|
||||
})
|
||||
})),
|
||||
|
||||
_updateCrateOption: (crate_id, new_options) => set(state => ({
|
||||
crates: state.crates.map((crate, _i) => {
|
||||
if (crate_id === crate.id) {
|
||||
const previous_options = crate.options_data || {};
|
||||
return {
|
||||
...crate,
|
||||
options_data: {
|
||||
...previous_options,
|
||||
...new_options
|
||||
}
|
||||
}
|
||||
}
|
||||
else return crate;
|
||||
})
|
||||
})),
|
||||
|
||||
updateCrateOptions: (crate_id, new_options) => {
|
||||
get().fillExtCrateData(crate_id);
|
||||
get().fillOrderExtData();
|
||||
get()._updateCrateOption(crate_id, new_options);
|
||||
get()._updateTotalOrderPrice();
|
||||
}
|
||||
}));
|
||||
|
||||
const useOrderOptions = ((set, get) => ({
|
||||
order_options: shared_data.orderOptions.options,
|
||||
order_prices: shared_data.orderOptions.prices,
|
||||
shipping_summary: shared_data.orderOptions.shippingSummary,
|
||||
order_options_data: {},
|
||||
|
||||
fillOrderExtData: () => set(state => ({
|
||||
order_options_data: {
|
||||
...state.order_options_data,
|
||||
ext_data: FillExtOrderData(state.crates, state.modes_order)
|
||||
}
|
||||
})),
|
||||
|
||||
_updateOrderOptions: (new_options) => set(state => ({
|
||||
order_options_data: {
|
||||
...state.order_options_data,
|
||||
...new_options
|
||||
}
|
||||
})),
|
||||
|
||||
updateOrderOptions: (new_options) => {
|
||||
get()._updateOrderOptions(new_options);
|
||||
get()._updateTotalOrderPrice();
|
||||
get().fillOrderExtData();
|
||||
}
|
||||
}));
|
||||
|
||||
const useLayout = ((set, get) => ({
|
||||
isTouch: window.isTouchEnabled(),
|
||||
isMobile: window.deviceIsMobile(),
|
||||
sideMenuIsOpen: false,
|
||||
showCardAddedFeedback: false,
|
||||
showNoDestination: false,
|
||||
timerAdded: null,
|
||||
|
||||
_switchSideMenu: () => set(state => ({
|
||||
sideMenuIsOpen: !state.sideMenuIsOpen
|
||||
})),
|
||||
switchSideMenu: () => {
|
||||
if (!get().sideMenuIsOpen) {
|
||||
get().hideNotification()
|
||||
}
|
||||
get()._switchSideMenu();
|
||||
},
|
||||
cardAdded: () => set(state => ({
|
||||
showCardAddedFeedback: true,
|
||||
showNoDestination: false,
|
||||
timerAdded: (!!state.timerAdded ? clearTimeout(state.timerAdded) : null) || (state.isMobile && setTimeout(() => {
|
||||
get()._endCardAdded()
|
||||
}, 2000))
|
||||
})),
|
||||
noDestinationWarning: () => set(state => ({
|
||||
showCardAddedFeedback: true,
|
||||
showNoDestination: true,
|
||||
timerAdded: (!!state.timerAdded ? clearTimeout(state.timerAdded) : null) || (setTimeout(() => {
|
||||
get()._endCardAdded()
|
||||
}, 2000))
|
||||
})),
|
||||
_endCardAdded: () => set(state => ({
|
||||
showCardAddedFeedback: false,
|
||||
timerAdded: !!state.timerAdded ? clearTimeout(state.timerAdded) : null
|
||||
}))
|
||||
}));
|
||||
|
||||
const useImportJSON = ((set, get) => ({
|
||||
importShouldOpen: false,
|
||||
importValue: {
|
||||
value: "",
|
||||
error: Validation.OK
|
||||
},
|
||||
openImport: () => set(state => ({
|
||||
importShouldOpen: true
|
||||
})),
|
||||
closeImport: () => set(state => ({
|
||||
importShouldOpen: false
|
||||
})),
|
||||
_loadDescription: () => set(state => {
|
||||
const parsed = JSONToCrates(state.importValue.value);
|
||||
// if (parsed.crates[-1].crate_mode !== "")
|
||||
return {
|
||||
importShouldOpen: false,
|
||||
// additional fields go here
|
||||
crates: parsed.crates,
|
||||
order_options_data: parsed.order_options_data
|
||||
}}),
|
||||
loadDescription: () => {
|
||||
get()._loadDescription()
|
||||
get().fillOrderExtData();
|
||||
get().crates.forEach((crate, _i) => {
|
||||
get().fillExtData(crate.id);
|
||||
get().fillWarnings(crate.id);
|
||||
get().fillExtCrateData(crate.id);
|
||||
});
|
||||
get()._updateTotalOrderPrice();
|
||||
get().showNotification(get().active_crate, null);
|
||||
},
|
||||
updateImportDescription: (new_description) => set(state => ({
|
||||
importValue: {
|
||||
value: new_description,
|
||||
error: validateJSONInput(new_description)
|
||||
}
|
||||
}))
|
||||
|
||||
}));
|
||||
|
||||
const useSubmitForm = ((set, get) => ({
|
||||
isProcessing: false,
|
||||
shouldShowRFQFeedback: false,
|
||||
processingResult: {
|
||||
status: Validation.OK,
|
||||
message: ""
|
||||
},
|
||||
API_RFQ: shared_data.API_RFQ,
|
||||
email: {
|
||||
value: "",
|
||||
error: null
|
||||
},
|
||||
note: {
|
||||
value: "",
|
||||
error: Validation.OK
|
||||
},
|
||||
|
||||
description: "",
|
||||
shouldShowDescription: false,
|
||||
|
||||
updateEmail: (new_email) => set(state => ({
|
||||
email: {
|
||||
value: new_email,
|
||||
error: validateEmail(new_email)
|
||||
}
|
||||
})),
|
||||
updateNote: (new_notes) => set(state => ({
|
||||
note: {
|
||||
value: new_notes,
|
||||
error: validateNote(new_notes)
|
||||
}
|
||||
})),
|
||||
resetEmailValidation: () => set(state => ({
|
||||
email: {
|
||||
value: state.email.value,
|
||||
error: Validation.OK
|
||||
}
|
||||
})),
|
||||
|
||||
_revalidateForm: () => set(state => ({
|
||||
email: {
|
||||
value: state.email.value,
|
||||
error: validateEmail(state.email.value)
|
||||
},
|
||||
note: {
|
||||
value: state.note.value,
|
||||
error: validateEmail(state.note.value)
|
||||
},
|
||||
})),
|
||||
|
||||
updateDescription: () => set(state => ({
|
||||
description: CratesToJSON(state.crates)
|
||||
})),
|
||||
showDescription: () => set(state => ({
|
||||
description: CratesToJSON(state.crates),
|
||||
shouldShowDescription: true
|
||||
})),
|
||||
closeDescription: () => set(state => ({
|
||||
shouldShowDescription: false
|
||||
})),
|
||||
_submitForm: () => set(state => ({isProcessing: true})),
|
||||
finishSubmitForm: (result) => set(state => ({
|
||||
isProcessing: false,
|
||||
shouldShowRFQFeedback: true,
|
||||
processingResult: result})),
|
||||
|
||||
submitDisabled: () => (get().email.error !== Validation.OK),
|
||||
|
||||
submitForm: () => {
|
||||
get().updateDescription();
|
||||
get()._revalidateForm();
|
||||
get()._submitForm();
|
||||
if (get().submitDisabled()) return;
|
||||
fetch(get().API_RFQ, {
|
||||
method: "POST",
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
email: get().email.value,
|
||||
note: get().note.value,
|
||||
configuration: get().description
|
||||
})
|
||||
}).then(response => {
|
||||
if (response.status !== 200) {
|
||||
throw Error("Response status is not OK: " + response.status + ".\n" + response);
|
||||
}
|
||||
get().finishSubmitForm({status: Validation.OK, message: "We've received your request and will be in contact soon."})
|
||||
}, reason => {
|
||||
console.error("Request rejected, reason:", reason)
|
||||
get().finishSubmitForm({
|
||||
status: Validation.Invalid,
|
||||
message: "We cannot receive your request. Try using the export by coping the configuration and send it to us at sales@m-labs.hk"
|
||||
})
|
||||
}).catch(err => {
|
||||
console.error("Request failed, reason:", err)
|
||||
get().finishSubmitForm({
|
||||
status: Validation.Invalid,
|
||||
message: "We cannot receive your request. Try using the export by coping the configuration and send it to us at sales@m-labs.hk"
|
||||
})
|
||||
})
|
||||
},
|
||||
closeRFQFeedback: () => set(state => ({shouldShowRFQFeedback: false}))
|
||||
}));
|
||||
|
||||
const useHighlighted = ((set, get) => ({
|
||||
highlighted: {
|
||||
crate: "",
|
||||
card: 0
|
||||
},
|
||||
highlightedTimer: null,
|
||||
|
||||
// #!if disable_card_highlight === false
|
||||
highlightCard: (crate_id, index) => set(state => ({
|
||||
highlighted: {
|
||||
crate: crate_id,
|
||||
card: index
|
||||
},
|
||||
highlightedTimer: (!!state.highlightedTimer ? clearTimeout(state.highlightedTimer) : null) || (state.isTouch && setTimeout(() => {
|
||||
get().highlightReset()
|
||||
}, 2000))
|
||||
})),
|
||||
highlightReset: () => set(state => ({
|
||||
highlighted: {
|
||||
crate: "",
|
||||
card: 0
|
||||
},
|
||||
highlightedTimer: !!state.highlightedTimer ? clearTimeout(state.highlightedTimer) : null
|
||||
})),
|
||||
// #!else
|
||||
highlightCard: () => {return null;},
|
||||
highlightReset: () => {return null;},
|
||||
// #!endif
|
||||
}));
|
||||
|
||||
|
||||
const useCart = ((set, get) => ({
|
||||
crates: shared_data.columns.crates,
|
||||
active_crate: "crate0",
|
||||
_defaultCrates: Array.from(shared_data.columns.crates),
|
||||
total_order_price: 0,
|
||||
|
||||
_newCrate: (crate_id) => set((state) => ({
|
||||
crates: state.crates.toSpliced(-1, 0, {
|
||||
...state._defaultCrates[0],
|
||||
id: crate_id || "crate" + state.crates.length,
|
||||
}),
|
||||
active_crate: crate_id || "crate" + state.crates.length
|
||||
})),
|
||||
_delCrate: (id) => set(state => ({
|
||||
crates: state.crates.filter((crate => crate.id !== id || !state.modes_order.includes(crate.crate_mode))),
|
||||
active_crate: state.active_crate === id ? null : state.active_crate,
|
||||
})),
|
||||
_setCrateMode: (id, mode) => set(state => ({
|
||||
crates: state.crates.map((crate, _i) => {
|
||||
if (crate.id === id) {
|
||||
return {
|
||||
...crate,
|
||||
crate_mode: mode
|
||||
}
|
||||
} else return crate;
|
||||
})
|
||||
})),
|
||||
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]));
|
||||
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;
|
||||
return {
|
||||
...crate,
|
||||
items: crate.items.toSpliced(index_to, 0, ...take_from.map((card_name, _) => {
|
||||
return {...state.cards[card_name], id: uuidv4()}
|
||||
}))
|
||||
}
|
||||
} else return crate;
|
||||
})
|
||||
}
|
||||
}),
|
||||
_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];
|
||||
return {
|
||||
crates: state.crates.map((crate, _i) => {
|
||||
if (crate_to === crate_from && crate_to === crate.id) {
|
||||
let items_copy = Array.from(crate.items);
|
||||
let item = items_copy.splice(index_from, 1)[0]
|
||||
items_copy.splice(index_to, 0, item).filter((item, _) => !!item)
|
||||
return {
|
||||
...crate,
|
||||
items: items_copy
|
||||
}
|
||||
} else if (crate_to === crate.id) {
|
||||
return {
|
||||
...crate,
|
||||
items: crate.items.toSpliced(index_to, 0, the_card)
|
||||
}
|
||||
} else if (crate_from === crate.id) {
|
||||
return {
|
||||
...crate,
|
||||
items: crate.items.toSpliced(index_to, 1)
|
||||
}
|
||||
}
|
||||
else return crate;
|
||||
})
|
||||
}
|
||||
}),
|
||||
_deleteCard: (crate_id, index) => set(state => ({
|
||||
crates: state.crates.map((crate, _i) => {
|
||||
if (crate_id === crate.id) {
|
||||
return {
|
||||
...crate,
|
||||
items: crate.items.toSpliced(index, 1)
|
||||
}
|
||||
}
|
||||
else return crate;
|
||||
})
|
||||
})),
|
||||
_clearCrate: (id) => set(state => ({
|
||||
crates: state.crates.map((crate, _i) => {
|
||||
if (id === crate.id) {
|
||||
return {
|
||||
...crate,
|
||||
items: []
|
||||
}
|
||||
}
|
||||
else return crate;
|
||||
})
|
||||
})),
|
||||
clearAll: () => set(state => ({
|
||||
crates: state._defaultCrates
|
||||
})),
|
||||
_updateOptions: (crate_id, index, new_options) => set(state => ({
|
||||
crates: state.crates.map((crate, _i) => {
|
||||
if (crate_id === crate.id) {
|
||||
let itemsCopy = Array.from(crate.items);
|
||||
itemsCopy[index] = {
|
||||
...itemsCopy[index],
|
||||
options_data: {
|
||||
...itemsCopy[index].options_data,
|
||||
...new_options
|
||||
}};
|
||||
return {
|
||||
...crate,
|
||||
items: itemsCopy
|
||||
}
|
||||
}
|
||||
else return crate;
|
||||
})
|
||||
})),
|
||||
|
||||
fillWarnings: (crate_id) => set(state => ({
|
||||
crates: state.crates.map((crate, _i) => {
|
||||
if (crate_id === crate.id) {
|
||||
//console.log("--- CHECK ALERTS ---")
|
||||
let itemsCopy = Array.from(crate.items);
|
||||
const disabled = !!get().crateParams(crate.crate_mode).warnings_disabled;
|
||||
itemsCopy = FillResources(itemsCopy, disabled);
|
||||
itemsCopy = TriggerWarnings(itemsCopy, disabled);
|
||||
const [crate_warnings, occupied] = TriggerCrateWarnings(crate);
|
||||
return {
|
||||
...crate,
|
||||
items: itemsCopy,
|
||||
warnings: crate_warnings,
|
||||
occupiedHP: occupied
|
||||
}
|
||||
}
|
||||
else return crate;
|
||||
})
|
||||
})),
|
||||
|
||||
fillExtData: (crate_id) => set(state => ({
|
||||
crates: state.crates.map((crate, _i) => {
|
||||
if (crate_id === crate.id) {
|
||||
let itemsCopy = Array.from(crate.items);
|
||||
|
||||
itemsCopy = itemsCopy.map((item, index) => {
|
||||
if (!item.options) return item;
|
||||
if (!item.options_data) item.options_data = {};
|
||||
item.options_data.ext_data = FillExtCardData(itemsCopy, index);
|
||||
return item;
|
||||
});
|
||||
return {
|
||||
...crate,
|
||||
items: Array.from(itemsCopy)
|
||||
}
|
||||
}
|
||||
else return crate;
|
||||
})
|
||||
})),
|
||||
|
||||
_updateTotalOrderPrice: () => set(state => {
|
||||
let sum = 0;
|
||||
get().crates.forEach( (crate, i) => {
|
||||
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, _) => {
|
||||
sum += item.price;
|
||||
});
|
||||
});
|
||||
const order_options = ProcessOptionsToData({options: get().order_prices, data: get().order_options_data || {}});
|
||||
sum += order_options ? order_options.reduce((accumulator, currentValue) => accumulator+currentValue.price, 0) : 0;
|
||||
return {total_order_price: sum};
|
||||
}),
|
||||
|
||||
// Composite actions that require warnings recalculation:
|
||||
|
||||
newCrate: () => {
|
||||
const crate_id = "crate" + get().crates.length;
|
||||
get()._newCrate(crate_id)
|
||||
get().fillExtData(crate_id);
|
||||
get().fillExtCrateData(crate_id);
|
||||
get().fillOrderExtData();
|
||||
get().fillWarnings(crate_id);
|
||||
get()._updateTotalOrderPrice();
|
||||
},
|
||||
|
||||
setCrateMode: (id, mode) => {
|
||||
get()._setCrateMode(id, mode)
|
||||
get().fillExtData(id);
|
||||
get().fillExtCrateData(id);
|
||||
get().fillOrderExtData();
|
||||
get().fillWarnings(id);
|
||||
get().setActiveCrate(id);
|
||||
get()._updateTotalOrderPrice();
|
||||
},
|
||||
|
||||
delCrate: (id) => {
|
||||
get()._delCrate(id);
|
||||
get().fillOrderExtData();
|
||||
},
|
||||
|
||||
addCardFromCatalog: (crate_to, index_from, index_to, just_mounted) => {
|
||||
const dest = crate_to || 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().fillWarnings(dest);
|
||||
get().setActiveCrate(dest);
|
||||
get()._updateTotalOrderPrice();
|
||||
if (!just_mounted) {
|
||||
get().cardAdded()
|
||||
}
|
||||
},
|
||||
|
||||
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);
|
||||
get()._updateTotalOrderPrice();
|
||||
if (crate_from !== crate_to) {
|
||||
get().fillExtData(crate_from);
|
||||
get().fillWarnings(crate_from);
|
||||
}
|
||||
},
|
||||
deleteCard: (crate_id, index) => {
|
||||
get()._deleteCard(crate_id, index);
|
||||
get().fillExtData(crate_id);
|
||||
get().fillWarnings(crate_id);
|
||||
get()._updateTotalOrderPrice();
|
||||
if (crate_id === get().highlighted.crate && index === get().highlighted.card) get().highlightReset()
|
||||
},
|
||||
clearCrate: (id) => {
|
||||
get()._clearCrate(id);
|
||||
get().fillWarnings(id);
|
||||
},
|
||||
|
||||
updateOptions: (crate_id, index, new_options) => {
|
||||
get()._updateOptions(crate_id, index, new_options);
|
||||
get().fillExtData(crate_id);
|
||||
get().fillWarnings(crate_id);
|
||||
},
|
||||
|
||||
initExtData: () => {
|
||||
get().fillOrderExtData();
|
||||
get().crates.forEach((crate, _i) => {
|
||||
get().fillExtData(crate.id);
|
||||
get().fillExtCrateData(crate.id);
|
||||
})
|
||||
get()._updateTotalOrderPrice();
|
||||
}
|
||||
}))
|
||||
|
||||
|
||||
export const useShopStore = createWithEqualityFn((...params) => ({
|
||||
...useOptionsNotification(...params),
|
||||
...useCatalog(...params),
|
||||
...useSearch(...params),
|
||||
...useCrateModes(...params),
|
||||
...useCart(...params),
|
||||
...useSubmitForm(...params),
|
||||
...useLayout(...params),
|
||||
...useHighlighted(...params),
|
||||
...useImportJSON(...params),
|
||||
...useCrateOptions(...params),
|
||||
...useOrderOptions(...params),
|
||||
}))
|
|
@ -1,94 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
export const data = window.shop_data;
|
||||
export const itemsUnfoldedList = Array.from(data.columns.catalog.categories.map(groupId => groupId.itemIds).flat());
|
||||
|
||||
export const productStyle = (style, snapshot, removeAnim, hovered, selected, cart=false) => {
|
||||
const custom = {
|
||||
opacity: snapshot.isDragging ? .7 : 1,
|
||||
backgroundColor: (hovered || selected) ? '#eae7f7' : 'initial',
|
||||
};
|
||||
|
||||
if (!cart && snapshot.draggingOver == null && // hack for catalog
|
||||
((!snapshot.isDragging) // prevent next elements from animation
|
||||
|| (snapshot.isDragging && snapshot.isDropAnimating))) { // prevent dragged element from weird animation
|
||||
style.transform = "none";
|
||||
}
|
||||
|
||||
if (!snapshot.isDropAnimating) {
|
||||
return { ...style, ...custom};
|
||||
}
|
||||
|
||||
if (removeAnim) {
|
||||
// cannot be 0, but make it super tiny
|
||||
custom.transitionDuration = '0.001s';
|
||||
}
|
||||
|
||||
return {
|
||||
...style,
|
||||
...custom,
|
||||
};
|
||||
}
|
||||
|
||||
export const cartStyle = (style, snapshot) => {
|
||||
const isDraggingOver = snapshot.isDraggingOver;
|
||||
return {
|
||||
...style,
|
||||
...{
|
||||
backgroundColor: isDraggingOver ? '#f2f2f2' : '#f9f9f9',
|
||||
border: isDraggingOver ? '1px dashed #ccc' : '0',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function formatMoney(amount, decimalCount = 2, decimal = ".", thousands = ",") {
|
||||
// https://stackoverflow.com/questions/149055/how-can-i-format-numbers-as-currency-string-in-javascript
|
||||
// changes: return amount if error in order to avoid empty value
|
||||
try {
|
||||
decimalCount = Math.abs(decimalCount);
|
||||
decimalCount = isNaN(decimalCount) ? 2 : decimalCount;
|
||||
|
||||
const negativeSign = amount < 0 ? "-" : "";
|
||||
|
||||
let i = parseInt(amount = Math.abs(Number(amount) || 0).toFixed(decimalCount)).toString();
|
||||
let j = (i.length > 3) ? i.length % 3 : 0;
|
||||
|
||||
return negativeSign + (j ? i.substring(0, j) + thousands : '') + i.substring(j).replace(/(\d{3})(?=\d)/g, "$1" + thousands) + (decimalCount ? decimal + Math.abs(amount - i).toFixed(decimalCount).slice(2) : "");
|
||||
} catch (e) {
|
||||
return amount;
|
||||
}
|
||||
}
|
||||
|
||||
export const range = (start, end) => {
|
||||
const length = end - start;
|
||||
return Array.from({ length }, (_, i) => start + i);
|
||||
}
|
||||
|
||||
|
||||
export const move = (source, destination, droppableSource, droppableDestination) => {
|
||||
console.log('==> move', source, destination);
|
||||
const sourceClone = Array.from(source);
|
||||
const destClone = Array.from(destination);
|
||||
const [removed] = sourceClone.splice(droppableSource.index, 1);
|
||||
|
||||
destClone.splice(droppableDestination.index, 0, removed);
|
||||
|
||||
const result = {columns: {}};
|
||||
result.columns[droppableSource.droppableId] = sourceClone;
|
||||
result.columns[droppableDestination.droppableId] = destClone;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const compareArraysWithIds = (a, b) =>
|
||||
a.length === b.length &&
|
||||
a.every((element, index) => element.id === b[index].id);
|
||||
|
||||
export const compareArraysLevelOne = (a, b) =>
|
||||
a.length === b.length &&
|
||||
a.every((element, index) => element === b[index]);
|
||||
|
||||
export function compareObjectsEmptiness(a, b) {
|
||||
return (!a && !b) || (!(!a !== !b) && Object.getPrototypeOf(a) === Object.getPrototypeOf(b) &&
|
||||
(Object.getPrototypeOf(a) !== Object.getPrototypeOf([]) || !!Object.keys(a).length === !!Object.keys(b).length))
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
|
||||
import {validateJSON} from "./json_porter";
|
||||
|
||||
export const Validation = {
|
||||
OK: 0,
|
||||
Empty: 1,
|
||||
Invalid: 2,
|
||||
};
|
||||
|
||||
export function validateEmail(value) {
|
||||
if (!value || value.trim() === '') {
|
||||
return Validation.Empty
|
||||
} else if (value && !value.match(/^\w+([\+\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/)) {
|
||||
return Validation.Invalid;
|
||||
}
|
||||
return Validation.OK;
|
||||
}
|
||||
|
||||
|
||||
export function validateNote(value) {
|
||||
if (!value || value.trim() === '') {
|
||||
return Validation.Empty
|
||||
}
|
||||
return Validation.OK;
|
||||
}
|
||||
|
||||
export function validateJSONInput(value) {
|
||||
if (!value || value.trim() === '') {
|
||||
return Validation.Empty
|
||||
} else if (value && !(validateJSON(value))) {
|
||||
return Validation.Invalid;
|
||||
}
|
||||
return Validation.OK;
|
||||
}
|
|
@ -1,156 +0,0 @@
|
|||
/**
|
||||
* This module should contain warnings subsystem.
|
||||
* First idea - there should be either definitive list of available warnings,
|
||||
* or defined as some json logic leading to the warning message(s), similar way how FillExtData works.
|
||||
* Second - resources indicator should be separate component
|
||||
*/
|
||||
|
||||
import {item_occupied_counters, resource_counters} from "./count_resources";
|
||||
import {useShopStore} from "./shop_store";
|
||||
|
||||
export const Levels = {
|
||||
"reminder": {priority: 1, icon: '/images/shop/icon-reminder.svg', color: "#0d3547"},
|
||||
"warning": {priority: 2, icon: '/images/shop/icon-warning.svg', color: "#c75e5e"},
|
||||
}
|
||||
|
||||
const find_in_counters = (counters, name) => {
|
||||
return counters.find((element) => element.name === name)
|
||||
}
|
||||
|
||||
const find_previous_source = (data, index, source) => {
|
||||
return data.slice(0, index).find((element) => {
|
||||
return element.resources && find_in_counters(element.resources, source)
|
||||
})
|
||||
}
|
||||
|
||||
const find_next_source_index = (data, index, source) => {
|
||||
return data.slice(index + 1).findIndex((element) => {
|
||||
return element.resources && find_in_counters(element.resources, source)
|
||||
}) + index + 1
|
||||
}
|
||||
|
||||
const not_enough_resource_trigger = (name) => {
|
||||
return (_data, _index, counters) => {
|
||||
const resource = find_in_counters(counters, name);
|
||||
return resource.occupied > resource.max;
|
||||
}
|
||||
}
|
||||
|
||||
const no_source_trigger = (name) => {
|
||||
return (data, index, _counters) => {
|
||||
const occupied = item_occupied_counters[name](data[index]);
|
||||
if (occupied > 0)
|
||||
return !find_previous_source(data, index, name);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const wiring_constraint = (name) => {
|
||||
return (data, index, _counters) => {
|
||||
const next = find_next_source_index(data, index, name);
|
||||
return next - index === 1;
|
||||
}
|
||||
}
|
||||
|
||||
const Types = {
|
||||
"eem_resource": {
|
||||
level: "warning",
|
||||
trigger: not_enough_resource_trigger("eem"),
|
||||
message: "Insufficient EEM connectors"
|
||||
},
|
||||
"no_eem_source": {
|
||||
level: "warning",
|
||||
trigger: no_source_trigger("eem"),
|
||||
message: 'This card needs a card that provides a EEM connector (e.g. Kasli) at its left.'
|
||||
},
|
||||
"idc_resource": {
|
||||
level: "warning",
|
||||
trigger: not_enough_resource_trigger("idc"),
|
||||
message: "Insufficient IDC connectors."
|
||||
},
|
||||
"no_idc_source": {
|
||||
level: "warning",
|
||||
trigger: no_source_trigger("idc"),
|
||||
message: 'Should be after a Zotino or a HD68-IDC or with another IDC adapter.'
|
||||
},
|
||||
"clk_resource": {
|
||||
level: "warning",
|
||||
trigger: not_enough_resource_trigger("clk"),
|
||||
message: "Insufficient clock connectors."
|
||||
},
|
||||
"no_clk_source": {
|
||||
level: "warning",
|
||||
trigger: no_source_trigger("clk"),
|
||||
message: 'This card needs either a card that provides a clock source (e.g. Kasli or Clocker) at its left or use an external clock source.'
|
||||
},
|
||||
"eem_wiring_constraint": {
|
||||
level: "reminder",
|
||||
trigger: wiring_constraint("eem"),
|
||||
message: "Due to wiring constraints, the carrier can only connect to EEM cards immediately at its right, without crossing another carrier."
|
||||
},
|
||||
"default": {
|
||||
level: "warning",
|
||||
trigger: (_a, _b, _c) => {
|
||||
return true;
|
||||
},
|
||||
message: 'This item has unimplemented warning'
|
||||
}
|
||||
}
|
||||
|
||||
export function TriggerWarnings(data, disabled) {
|
||||
return data.map((element, index) => {
|
||||
if (!element.warnings) return element;
|
||||
element.show_warnings = disabled ? [] :element.warnings
|
||||
.map((warning, _) => {
|
||||
if (!!Types[warning])
|
||||
return Types[warning].trigger(data, index, element.counted_resources) ? {trigger: undefined, name: warning, ...Types[warning]} : null;
|
||||
else
|
||||
return Types.default;
|
||||
})
|
||||
.filter((warning, _) => {
|
||||
return !!warning
|
||||
});
|
||||
return element;
|
||||
})
|
||||
}
|
||||
|
||||
export function MaxLevel(warnings) {
|
||||
let mx = {priority: 0, icon: null};
|
||||
for (const warning of warnings) {
|
||||
if (Levels[warning.level].priority > mx.priority) mx = Levels[warning.level];
|
||||
}
|
||||
return mx;
|
||||
}
|
||||
|
||||
export function LevelUI(warning_level) {
|
||||
const warning_t = Levels[warning_level];
|
||||
return {icon: warning_t.icon, color: warning_t.color};
|
||||
}
|
||||
|
||||
const crate_warnings = {
|
||||
"overfit": {
|
||||
message: "You have reached the maximum number of slots allowed for this crate. Consider removing cards.",
|
||||
level: "warning",
|
||||
trigger: (crate, occupied) => {
|
||||
const nbrHP = useShopStore.getState().crateParams(crate.crate_mode).hp;
|
||||
return occupied > nbrHP && nbrHP > 0;
|
||||
}
|
||||
},
|
||||
"underfit_rack": {
|
||||
message: "The selected cards fit in a 42hp desktop crate, consider switching to it for a more compact system",
|
||||
level: "reminder",
|
||||
trigger: (crate, occupied) => {
|
||||
const nbrHPDesktop = useShopStore.getState().crate_modes.desktop.hp;
|
||||
return crate.crate_mode === useShopStore.getState().crate_modes.rack.id && occupied < nbrHPDesktop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function TriggerCrateWarnings(crate) {
|
||||
const nbrOccupied = resource_counters.hp(crate.items, -1);
|
||||
let warnings = [];
|
||||
Object.entries(crate_warnings).forEach(([id, warning], _) => {
|
||||
if (warning.trigger(crate, nbrOccupied)) warnings.push({...warning, id: id, trigger: undefined});
|
||||
})
|
||||
return [warnings, nbrOccupied];
|
||||
}
|
|
@ -0,0 +1,450 @@
|
|||
'use strict';
|
||||
|
||||
import React, {Component} from "react";
|
||||
import jsonLogic from 'json-logic-js';
|
||||
import {useState, useEffect} from 'react';
|
||||
import {useClickAway} from "@uidotdev/usehooks";
|
||||
import {OverlayTrigger, Tooltip} from "react-bootstrap";
|
||||
|
||||
// https://stackoverflow.com/a/70511311
|
||||
const true_type_of = (obj) => Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
|
||||
|
||||
|
||||
function Tip({id, tip}) {
|
||||
return (
|
||||
<OverlayTrigger
|
||||
placement="auto"
|
||||
trigger={['click', 'hover', 'focus']}
|
||||
style={{display: 'inline'}}
|
||||
overlay={<Tooltip id={id}>{tip}</Tooltip>}
|
||||
>
|
||||
<img src={`/images/shop/icon-reminder.svg`} className="options-icon"/>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
class Radio extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// Initialize the state object with the initial values from the props
|
||||
this.state = {
|
||||
variant: props.outvar in props.data ? props.data[props.outvar] : props.variants[props.fallback ? props.fallback : 0],
|
||||
};
|
||||
|
||||
// Bind the event handler to this
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.props.target.construct(this.props.outvar, this.state.variant);
|
||||
}
|
||||
|
||||
handleClick(variant) {
|
||||
// Update the state object with the new value for outvar
|
||||
this.setState({
|
||||
...this.state,
|
||||
variant: variant
|
||||
});
|
||||
this.props.target.update(this.props.outvar, variant);
|
||||
}
|
||||
|
||||
render() {
|
||||
let key = this.props.id + this.props.outvar;
|
||||
return (
|
||||
<div className="shop-radio" key={this.props.id}>
|
||||
<div style={{"display": "inline"}}>
|
||||
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
|
||||
{this.props.title}
|
||||
</div>
|
||||
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
|
||||
{this.props.variants.map((variant, _) => (
|
||||
<div className="form-check" key={key + variant}>
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="radio"
|
||||
name={key}
|
||||
id={key + variant}
|
||||
checked={this.state.variant === variant}
|
||||
onClick={() => this.handleClick(variant)}
|
||||
onChange={() => this.handleClick(variant)}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor={key + variant}>
|
||||
{variant}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function RadioWrapper(target, id, data, {title, variants, outvar, fallback, icon, tip}) {
|
||||
return <Radio target={target} title={title} variants={variants} outvar={outvar} icon={icon} tip={tip} key={id}
|
||||
fallback={fallback}
|
||||
id={id} data={data}/>;
|
||||
}
|
||||
|
||||
class Switch extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// Initialize the state object with the initial values from the props
|
||||
this.state = {
|
||||
checked: props.outvar in props.data ? !!(props.data[props.outvar]) : !!(props.fallback)
|
||||
};
|
||||
|
||||
// Bind the event handler to this
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.props.target.construct(this.props.outvar, this.state.checked);
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
// Update the state object with the new value for outvar
|
||||
let new_checked = !this.state.checked;
|
||||
this.setState({
|
||||
checked: new_checked
|
||||
});
|
||||
this.props.target.update(this.props.outvar, new_checked);
|
||||
}
|
||||
|
||||
render() {
|
||||
let key = this.props.id + this.props.outvar;
|
||||
return (
|
||||
<div className="shop-switch" key={this.props.id}>
|
||||
<div className="form-check form-switch" key={key}>
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
id={key}
|
||||
checked={this.state.checked}
|
||||
onClick={this.handleClick}
|
||||
onChange={this.handleClick}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor={key} style={{"display": "inline"}}>
|
||||
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
|
||||
{this.props.title}
|
||||
</label>
|
||||
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function SwitchWrapper(target, id, data, {title, fallback, outvar, icon, tip}) {
|
||||
return <Switch target={target} title={title} fallback={fallback} outvar={outvar} icon={icon} tip={tip} key={id}
|
||||
id={id} data={data}/>;
|
||||
}
|
||||
|
||||
|
||||
class Line extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// Initialize the state object with the initial values from the props
|
||||
this.state = {
|
||||
text: props.outvar in props.data ? props.data[props.outvar] : (props.fallback ? props.fallback : "")
|
||||
};
|
||||
// Bind the event handler to this
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.props.target.construct(this.props.outvar, this.state.text);
|
||||
}
|
||||
|
||||
handleClick(element) {
|
||||
let text = element.target.value;
|
||||
this.setState({
|
||||
text: text
|
||||
});
|
||||
this.props.target.update(this.props.outvar, text);
|
||||
}
|
||||
|
||||
render() {
|
||||
let key = this.props.id + this.props.outvar;
|
||||
return (
|
||||
<div className="shop-line" key={this.props.id}>
|
||||
<label htmlFor={key} className="form-label">
|
||||
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
|
||||
{this.props.title}:
|
||||
</label>
|
||||
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
|
||||
<input type="text" className="form-control form-control-sm" id={key} onChange={this.handleClick}
|
||||
value={this.state.text}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function LineWrapper(target, id, data, {title, fallback, outvar, icon, tip}) {
|
||||
return <Line target={target} title={title} fallback={fallback} outvar={outvar} icon={icon} tip={tip} key={id}
|
||||
id={id} data={data}/>;
|
||||
}
|
||||
|
||||
class SwitchLine extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// Initialize the state object with the initial values from the props
|
||||
this.state = {
|
||||
text: props.outvar in props.data ? props.data[props.outvar].text : (props.fallback ? props.fallback.text : ""),
|
||||
checked: props.outvar in props.data ? props.data[props.outvar].checked : (props.fallback ? props.fallback.checked : false)
|
||||
};
|
||||
// Bind the event handler to this
|
||||
this.handleText = this.handleText.bind(this);
|
||||
this.handleCheck = this.handleCheck.bind(this);
|
||||
this.props.target.construct(this.props.outvar, this.state);
|
||||
}
|
||||
|
||||
handleText(element) {
|
||||
let new_state = {
|
||||
...this.state,
|
||||
text: element.target.value
|
||||
}
|
||||
this.setState(new_state);
|
||||
this.props.target.update(this.props.outvar, new_state);
|
||||
}
|
||||
|
||||
handleCheck() {
|
||||
// Update the state object with the new value for outvar
|
||||
let new_state = {
|
||||
...this.state,
|
||||
checked: !this.state.checked
|
||||
}
|
||||
this.setState(new_state);
|
||||
this.props.target.update(this.props.outvar, new_state);
|
||||
}
|
||||
|
||||
render() {
|
||||
let key = this.props.id + this.props.outvar;
|
||||
return (
|
||||
<div className="shop-switch-line" key={this.props.id}>
|
||||
<div className="form-check form-switch" key={key}>
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
id={key + "switch"}
|
||||
checked={this.state.checked}
|
||||
onClick={this.handleCheck}
|
||||
onChange={this.handleCheck}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor={key + "switch"}>
|
||||
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
|
||||
{this.props.title}
|
||||
</label>
|
||||
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
|
||||
</div>
|
||||
<input type="text" className="form-control form-control-sm" id={key + "line"} onChange={this.handleText}
|
||||
value={this.state.text} disabled={!this.state.checked}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function SwitchLineWrapper(target, id, data, {title, fallback, outvar, icon, tip}) {
|
||||
return <SwitchLine target={target} title={title} fallback={fallback} outvar={outvar} icon={icon} tip={tip} key={id}
|
||||
id={id} data={data}/>;
|
||||
}
|
||||
|
||||
|
||||
function UnimplementedComponent(type, id) {
|
||||
//console.error("Missing component with type:", type)
|
||||
return <div key={type + id} style={{background: "red"}}>UNIMPLEMENTED</div>
|
||||
}
|
||||
|
||||
const componentsList = {
|
||||
"Radio": RadioWrapper,
|
||||
"Switch": SwitchWrapper,
|
||||
"Line": LineWrapper,
|
||||
"SwitchLine": SwitchLineWrapper,
|
||||
"Default": UnimplementedComponent,
|
||||
};
|
||||
|
||||
|
||||
export function ProcessOptions({options, data, target, id}) {
|
||||
let options_t = true_type_of(options);
|
||||
|
||||
if (options_t === "array") {
|
||||
return Array.from(
|
||||
options.map((option_item, i) => ProcessOptions({
|
||||
options: option_item,
|
||||
data: data,
|
||||
target: target,
|
||||
id: id + i
|
||||
}))
|
||||
);
|
||||
} else if (options_t === "object") {
|
||||
if (
|
||||
true_type_of(options.type) === "string" &&
|
||||
(true_type_of(options.args) === "object" || true_type_of(options.items) === "array")
|
||||
) {
|
||||
if (options.type in componentsList) {
|
||||
return componentsList[options.type](target, id + options.type, data, options.args);
|
||||
} else if (options.type === "Group") {
|
||||
return (
|
||||
<div className="border rounded" key={id + "group"}>
|
||||
{ProcessOptions({
|
||||
options: jsonLogic.apply(options.items, data),
|
||||
data: data,
|
||||
target: target,
|
||||
id: id
|
||||
})}
|
||||
</div>);
|
||||
} else {
|
||||
return componentsList["Default"](options.type, id + "missing");
|
||||
}
|
||||
} else {
|
||||
return ProcessOptions({options: jsonLogic.apply(options, data), data: data, target: target, id: id});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function FilterOptions(options, data) {
|
||||
let options_t = true_type_of(options);
|
||||
let target = {};
|
||||
|
||||
if (options_t === "array") {
|
||||
options.map((option_item, _) => {
|
||||
Object.assign(target, FilterOptions(option_item, data))
|
||||
});
|
||||
} else if (options_t === "object") {
|
||||
if (
|
||||
true_type_of(options.type) === "string" &&
|
||||
(true_type_of(options.args) === "object" || true_type_of(options.items) === "array")
|
||||
) {
|
||||
if (options.type in componentsList) {
|
||||
target[options.args.outvar] = data[options.args.outvar];
|
||||
} else if (options.type === "Group") {
|
||||
Object.assign(target, FilterOptions(jsonLogic.apply(options.items, data), data))
|
||||
}
|
||||
} else {
|
||||
Object.assign(target, FilterOptions(jsonLogic.apply(options, data), data))
|
||||
}
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
export function OptionsDialogPopup({options, data, target, id, big, first, last, options_class}) {
|
||||
const [show, setShow] = useState(false);
|
||||
const ref = useClickAway((e) => {
|
||||
if (e.type === "mousedown") // ignore touchstart
|
||||
setShow(false)
|
||||
}
|
||||
);
|
||||
|
||||
let div_classes = `overlayVariant border rounded ${big ? "overlay-bigcard" : "overlay-smallcard"} ${(!big && first) ? "overlay-first" : ""} ${(!big && last) ? "overlay-last" : ""} ${options_class || ""}`;
|
||||
const handleClick = (event) => {
|
||||
setShow(!show);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
export function OptionsSummaryPopup({id, options, data}) {
|
||||
const [show, setShow] = useState(false);
|
||||
const [position, setPosition] = useState({x: 0, y: 0});
|
||||
const [size, setSize] = useState({w: 0, h: 0});
|
||||
let display_options = FilterOptions(options, data);
|
||||
const close = () => {
|
||||
setShow(false);
|
||||
document.removeEventListener("scroll", handleScroll, true);
|
||||
}
|
||||
|
||||
const ref = useClickAway(close);
|
||||
|
||||
const reposition = () => {
|
||||
let popup_button = document.getElementById(id + "img");
|
||||
if (!popup_button) {
|
||||
document.removeEventListener("scroll", handleScroll, true);
|
||||
return;
|
||||
}
|
||||
let rect = popup_button.getBoundingClientRect()
|
||||
let pos_x = (rect.left + rect.right) / 2;
|
||||
let pos_y = (rect.top + rect.bottom) / 2;
|
||||
if (pos_x + size.w > window.innerWidth) {
|
||||
setPosition({x: pos_x - size.w - 20, y: pos_y - size.h / 2});
|
||||
} else {
|
||||
setPosition({x: pos_x - size.w / 2, y: pos_y - size.h - 20});
|
||||
}
|
||||
}
|
||||
|
||||
const handleScroll = (e) => {
|
||||
if (e.target !== document.getElementById(id)) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
let popup = document.getElementById(id);
|
||||
let width = popup.offsetWidth;
|
||||
let height = popup.offsetHeight;
|
||||
setSize({w: width, h: height});
|
||||
reposition()
|
||||
}
|
||||
}, [show])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
reposition();
|
||||
}
|
||||
}, [show, size])
|
||||
|
||||
const handleClick = (event) => {
|
||||
setShow(!show);
|
||||
if (!show) {
|
||||
document.addEventListener("scroll", handleScroll, true);
|
||||
}
|
||||
};
|
||||
|
||||
const stringify = (value) => {
|
||||
let value_type = true_type_of(value);
|
||||
if (value_type === "string") {
|
||||
return value;
|
||||
} else if (value_type === "object") {
|
||||
if (value.checked === false) {
|
||||
return "off";
|
||||
} else if (value.checked === true && value.text) {
|
||||
return value.text;
|
||||
}
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<img className="alert-info" src={show ? "/images/shop/icon-close.svg" : "/images/shop/icon-customize.svg"}
|
||||
id={id + "img"}
|
||||
onClick={handleClick}/>
|
||||
<div style={{'display': show ? 'flex' : 'none', 'top': position.y, 'left': position.x}}
|
||||
className="overlayVariant card border rounded"
|
||||
id={id}>
|
||||
<div className="card-body">
|
||||
{Array.from(Object.entries(display_options)
|
||||
.filter(([key, value], _) => key !== "ext_data")
|
||||
.map(([key, value], _) => {
|
||||
return (<p className="card-text" key={id + key}><i>{key}</i>: {stringify(value)}</p>);
|
||||
}))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FillExtData(data, index) {
|
||||
return {
|
||||
has_other_dio: data.filter((value, item_index) => index !== item_index && value.name &&value.name.endsWith("-TTL")).length > 0,
|
||||
has_dds: data.filter(((value, _) => value.name === "DDS" && value.name_number === "4410" && (!value.options_data || !value.options_data.mono_eem))).length > 0,
|
||||
has_sampler: data.filter(((value, _) => value.name === "Sampler" && (!value.options_data || !value.options_data.mono_eem))).length > 0,
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -13,30 +13,10 @@ module.exports = {
|
|||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
'babel-loader',
|
||||
{
|
||||
loader: "webpack-preprocessor-loader",
|
||||
options: {
|
||||
debug: false,
|
||||
directives: {
|
||||
render_count: false,
|
||||
options_log: false
|
||||
},
|
||||
params: {
|
||||
ENV: process.env.NODE_ENV,
|
||||
disable_card_highlight: false
|
||||
},
|
||||
verbose: false,
|
||||
}
|
||||
}
|
||||
],
|
||||
use: 'babel-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js', '.jsx'],
|
||||
},
|
||||
//devtool: "inline-source-map",
|
||||
//mode: "development"
|
||||
devtool: false,
|
||||
|
|
Loading…
Reference in New Issue