1
0
Fork 0

Compare commits

...

76 Commits

Author SHA1 Message Date
Sebastien Bourdeauducq a5a8589b28 add link to OffsetStabilizer 2024-11-16 11:55:30 +08:00
Sebastien Bourdeauducq bb42788fd3 remove dead link/project 2024-10-30 22:03:16 +08:00
Egor Savkin f29807a913 Optimize the download button
Replace download button with windows icon (just four squares).
Make the download button last so on certain screens it wouldn't cause additional empty-ish lines.
Redo the wording so the button is shorter, but the dropdown menu is filled.

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

Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-10-21 15:32:19 +08:00
Egor Savkin 92f7428ac8 Replace download button with dropdown
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-10-18 12:29:19 +08:00
Egor Savkin a163a269b9 Update NUC
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-10-17 11:10:40 +08:00
Egor Savkin f087a00cee Fix price not getting updated on crate clear and deletion
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-10-04 11:25:49 +08:00
Sébastien Bourdeauducq f69102fa0e update FAQ 2024-10-02 13:56:35 +08:00
Egor Savkin 0d978c1e22 Add mcx-idc 32 channel to the shop
The icon had to be altered in order to lower the risk of triggering trypophobia

Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-09-27 13:47:04 +08:00
Sébastien Bourdeauducq 76bc87ce5c faq: add entries 2024-09-10 18:45:12 +08:00
Sébastien Bourdeauducq 3987343e16 advertise Phaser MIQRO gateware 2024-08-27 22:48:42 +08:00
Sébastien Bourdeauducq 8a12d35c06 faq: add US mirror 2024-08-14 12:51:09 +08:00
Sébastien Bourdeauducq d2ae816cb0 add links to cheap boards 2024-08-12 17:11:38 +08:00
Egor Savkin bcdf005896 Fix pounder and eem pwr mode hp consumption
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-08-09 12:03:43 +08:00
Egor Savkin b53e191528 Update and clean up NPM dependencies
npm audit also reported 0 vulnerabilities

Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-08-05 11:26:28 +08:00
Egor Savkin 52c05c2e2a Move out subcomponents of ProductItem
This improves maintenance as it reduces complexity of the ProductItem component and allows seeing components in react dev tools

Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-08-05 11:25:32 +08:00
Egor Savkin c7b0d5568c Fix options pop up being both first and last
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-08-05 11:25:32 +08:00
Egor Savkin 68677dce6e Make tooltip icons inline
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-08-05 10:29:26 +08:00
Egor Savkin b0ef9d07fc Remove unused type field from shop_data
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-08-05 10:29:01 +08:00
Sébastien Bourdeauducq cedf853301 edit Kirdy description 2024-08-04 12:01:07 +08:00
Egor Savkin c5c723ae40 Add Kirdy to the shop
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-08-02 17:14:35 +08:00
Egor Savkin ff56f369af Show No results instead of all cards when search failed
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-08-02 15:39:34 +08:00
Sébastien Bourdeauducq 580dd1964c advertise AOM integration 2024-07-29 12:55:22 +08:00
Sébastien Bourdeauducq a2e672ffc9 update ordering FAQ 2024-07-27 17:25:35 +08:00
Sébastien Bourdeauducq 950518ba97 add FAQ 2024-07-26 18:16:30 +08:00
Egor Savkin c6696f5098 Fix typo
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-07-26 16:18:08 +08:00
Egor Savkin c63249e8a0 Inject JS in templates with correct domains
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-07-18 11:56:58 +08:00
Egor Savkin 95304cfd61 Add HVAMP32 to the webshop
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-07-09 17:41:35 +08:00
Egor Savkin cba6ab72c4 Add development scripts for smoother development process
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-07-05 15:07:12 +08:00
Egor Savkin d3cc86dfa5 Add 2 channel thermostat to shop
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-07-05 14:48:55 +08:00
Egor Savkin 69cd11e3f1 Add Fast Servo to shop
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-07-03 17:09:55 +08:00
Egor Savkin c91020b72b Add icon for windows installer download button
Icon from https://www.iconfinder.com/icons/9026548/download_thin_icon - MIT license.
In accessibility options, the link reads as `download Windows installer`

Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-06-21 13:10:20 +08:00
Sébastien Bourdeauducq 243851ea56 update installer button 2024-06-06 11:14:39 +08:00
Egor Savkin 5abec7c676 Add installer to the main page
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-06-06 11:10:56 +08:00
Egor Savkin da39ac3f56 External clock reference variants -> options
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-05-10 09:58:41 +08:00
Sebastien Bourdeauducq 6f7c2c020b Revert "Add installer to the main page"
This reverts commit a6fbae7bcb.
2024-05-08 19:51:32 +08:00
Egor Savkin 63d33210c1 Fix broken links
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-05-08 12:36:57 +08:00
Egor Savkin a6fbae7bcb Add installer to the main page
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-05-08 11:22:22 +08:00
火焚富良 c02b57c4c2 Add WRPLL option to Kasli SoC 2024-05-07 15:28:47 +08:00
Sébastien Bourdeauducq 6fe4f124bc add link to Argent 2024-04-23 13:14:23 +08:00
Egor Savkin 3fd43e0de8 Increase padding in searchbar to avoid shadow
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-04-10 15:17:31 +08:00
Egor Savkin 4448029757 Update bundle and make example configuration correct
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-04-09 14:56:05 +08:00
Egor Savkin 51c9031f24 Add text options validation
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-04-09 14:56:05 +08:00
Egor Savkin be50b2a3c3 Optimize bundle size, and drop its support for J2ME's Opera (layout doesn't support small screens anyway)
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-04-09 14:56:05 +08:00
Egor Savkin ee6da1b282 Update bundle
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-04-09 14:56:05 +08:00
Egor Savkin 5e3a9af749 Fix notification remained visible when side menu is opened
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-04-09 14:56:05 +08:00
Egor Savkin d3fb46956e Fix notification overlay making "added" notification disappear on touchables
When notification doesn't fit viewport on touch-enabled devices, it makes the canvas extend to its boundaries, braking the fixed positioned elements placement

Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-04-09 14:56:05 +08:00
Egor Savkin 6bfed3e779 Show at max only one options notification
Closes #124

Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-04-09 14:56:05 +08:00
Egor Savkin dfe1f0ea2d Rename backlog to catalog
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-04-09 14:56:05 +08:00
Egor Savkin 839d7c6612 Fix search bar icon conflicting in webkit and make backlog scroll bar thin
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-04-09 14:56:05 +08:00
Egor Savkin 1cb9c90c65 Fix bottom side gradient
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-04-09 14:56:05 +08:00
Egor Savkin eb196b086e Make fonts sizes more consistent and fiz minor paddings issues
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-04-09 14:56:05 +08:00
Egor Savkin 56a44ce4a3 Do not show groups when there are search results
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-04-09 14:56:05 +08:00
Egor Savkin aa35348288 Apply styles to the search bar
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-04-09 14:56:05 +08:00
Egor Savkin 4bc6f6a3ee Prototype search bar for the backlog
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-04-09 14:56:05 +08:00
Sébastien Bourdeauducq 2ba10dd2e8 improve sales info 2024-03-08 09:31:30 +08:00
Egor Savkin df7607ffd8 Do not show copy button in not secured contexts and add product number to the backlog header
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-02-21 11:57:33 +08:00
Sébastien Bourdeauducq 4906ad1713 shop: price excludes shipping 2024-02-15 12:36:09 +08:00
Sébastien Bourdeauducq 41e20d205c shop: improve wording 2024-02-15 12:34:01 +08:00
Egor Savkin 67cef120b6 Move order options to the OrderForm, fix missing tooltip props, update tooltip contents
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-02-15 12:17:19 +08:00
Egor Savkin 759f7cffcc Add css classes to the radio component, add shipping summary and other content fixes
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-02-15 12:17:19 +08:00
Egor Savkin 0b5797b1ba Add more order options and enhance the look of them
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-02-15 12:17:19 +08:00
Egor Savkin bcc8db6819 Add order options
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-02-15 12:17:19 +08:00
Egor Savkin 15d9124025 Cache total price calculation
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-02-15 12:17:19 +08:00
Egor Savkin 4527189994 Add flexible crate options
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-02-15 12:17:19 +08:00
Egor Savkin bc81035555 Prototype crate and order options
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-02-15 12:17:19 +08:00
Egor Savkin cbe61efc1d Delete idc-mcx adapter
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-02-15 11:38:14 +08:00
Sébastien Bourdeauducq ad262c6657 avoid uncommon/incorrect 'TEC chip' terminology 2024-02-07 10:31:53 +08:00
Sébastien Bourdeauducq 3fb2a219de fix fan tray price and description 2024-01-26 15:47:30 +08:00
Egor Savkin ddd8b2d894 Add fan tray option to the crate
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-01-25 17:10:14 +08:00
Egor Savkin 3366f80ed7 Fix reminder background color
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-01-23 16:30:27 +08:00
Egor Savkin 9d5ed66985 Do not show reminders in summary
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-01-23 11:44:01 +08:00
Egor Savkin ca2eb29fbf Alignment fixes, remove price of spare cards, reworded the description phrase,
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-01-23 11:36:56 +08:00
Egor Savkin 7ceba1d461 Extend shop description to 80% and fix notification on mobiles when sidemenu is open
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-01-15 15:51:04 +08:00
Egor Savkin 4f702e9064 Remove delete crate from spare cards and add crate numbers to the summary
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-01-15 13:01:27 +08:00
Egor Savkin 6d6809af7f Show tooltip instead of badge by default
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-01-15 12:28:07 +08:00
81 changed files with 2871 additions and 1639 deletions

View File

@ -12,12 +12,14 @@ Clone the project:
Install Zola. Install Zola.
Start: Start with targeting `m-labs.hk` domain:
``` ```
zola serve zola serve
``` ```
Environmental variable `DOMAINNAME` can be specified to [substitute links on the website](#domain-handling).
To build the .bundle.js from .jsx files: To build the .bundle.js from .jsx files:
``` ```
@ -25,3 +27,14 @@ To build the .bundle.js from .jsx files:
npm install npm install
npm run build npm run build
``` ```
Development builds are also available. `npm run build-dev` to re/build once, or `npm run start-dev` for incremental
continuous builds as source files change.
## Domain handling
Environmental variable `DOMAINNAME` controls only the following links on the website:
* email `mailto:` links
* hook for the RFQ server (`window.API_RFQ`) variable
If absent, `DOMAINNAME` defaults to `m-labs.hk`.

View File

@ -18,4 +18,4 @@ Selling drinks is not our main business and only a service we do to other Club M
* In case of any dispute, the seller is always right. * In case of any dispute, the seller is always right.
Contact: [sb@m-labs.hk](mailto:sb@m-labs.hk) Contact: {{ email(address="sb") }}

112
content/about-us/faq.md Normal file
View File

@ -0,0 +1,112 @@
+++
title = "FAQ"
weight = 3
template = "page.html"
+++
##### Why are you in Hong Kong?
Low taxes, efficient infrastructure, no import/export fees and red tape for most products including high-tech items, good quality of life.
##### Why are you in Manila?
For the convenience of our customers who may be discouraged from doing business with companies registered in Hong Kong.
##### Where will my Sinara order be manufactured and where will it ship from?
M-Labs can manufacture Sinara hardware orders in Hong Kong, or, upon request, in Manila, Philippines. Additional fees, minimum quantities, and/or extended lead times may apply to orders to be made in the Philippines. Shipping is from the manufacturing place.
##### Do I need to pay additional import fees?
M-Labs ships orders according to FCA or DAP terms as defined by the International Commercial Terms (Incoterms<sup>®</sup>) published by the International Chamber of Commerce, edition 2020.
According to these terms, you are responsible for resolving problems created by customs at the destination country or territory, at your own expense.
If you are a credit customer (eligible for Net 15 or Net 30 payment), upon your request and for your convenience, we may ask UPS to deal with customs on your behalf and we will add the fees to your final invoice.
Import fees are determined by customs at the time of import in the destination country or territory, and are outside our control.
##### My institution cannot import your products. Do you have a distributor in my country?
Typical countries where this situation happens and suitable distributor contacts are as follows:
* China (mainland): kitty-zheng@kehua-trade.com
* Japan: hishida@symphotony.com or h_yamamoto@autex-inc.co.jp
* India: sale.sannidhi@gmail.com
* Thailand: info@irct.co.th or thayika@ryts-instruments.co.th
You are responsible for paying distributor fees and we do not grant discounts on the basis of requiring a distributor. On our side, we do not mandate the use of any distributor and we can ship directly to you or via another distributor of your choosing not listed here. Generally, you may solve the import problems created by customs and other bureaucracy in any way that you deem acceptable.
##### How will my order be shipped?
M-Labs ships with UPS by default, and we also offer FedEx or DHL shipping for an additional fee. We can ship on your courier account and you may also arrange your own pickup and shipping. For orders exceeding USD 200.00, there is no handling fee.
##### Can I have a discount?
M-Labs offers discounts in the following cases:
* 10% discount plus free DAP shipping if the research supported by our equipment is made public and exclusively published in a non-predatory scientific journal. A non-predatory journal is defined as an open access journal with an article processing fee of USD 500.00 or less.
* Volume discounts (applies to new orders and not retroactively, volume is determined over the 365 days prior to the new order, the new order is included in the amount compared against the threshold):
- 2% discount with an order volume exceeding USD 100,000.00.
- 4% discount with an order volume exceeding USD 250,000.00.
* Use of our equipment exclusively in jurisdictions outside the World Bank's "High Income" group, and where the R&D expenditure is less than 2% of GDP as determined by the World Bank, and which do not possess nuclear weapons.
* Equipment with cosmetic or other minor damage - please enquire.
* Advanced hobbyist and amateur use. Please describe your project when applying for the discount and provide supporting evidence (e.g. publications of past projects, personal website, copy of amateur radio license, ...).
We reserve the right of final decision regarding discount eligibility and amount.
##### I am on a really tight budget, what can I do?
You can build a useful ARTIQ system and spend very little money by running the open-source code on mass-produced low-cost electronics that were not made for this purpose. For example, in theory the [Colorlight-75E](https://hackaday.com/2020/01/24/new-part-day-led-driver-is-fpga-dev-board-in-disguise/) and [EBAZ4205](https://github.com/xjtuecho/EBAZ4205) can be used as core devices, and the Taobao shop [ZONRI](https://world.taobao.com/dianpu/73267337.htm) sells many inexpensive boards that can be useful as I/O peripherals such as DDS and data converters.
This is not a turn-key solution; experience with electronics, FPGA, and software development is required, and you will have to spend some time studying the code, porting it to these boards, writing drivers, and building hardware adapters.
See also [193THz.com](https://193THz.com) for ideas about low-cost homebrew laser systems.
##### Do you have a warranty?
For most products, we offer a 1-year warranty against manufacturing defects.
##### What should I do if I need technical support, or there is a problem with my order?
Please contact us at helpdesk@m-labs.hk.
If you have contacted the helpdesk and your issue cannot be satisfactorily resolved, then contact sb@m-labs.hk or text +852 65873703. Mention your helpdesk ticket number.
##### What are your payment terms?
For most established institutions (determined at our discretion), we offer Net 15 and Net 30 terms after delivery. Please enquire from your institutional email address to help us determine your eligibility for these terms.
For all others, payment is due 100% in advance before shipment.
Letters of credit are not an option.
##### What currency can I pay in?
We accept all major currencies including USD, EUR, RMB, GBP, BTC and XMR.
##### Can I pay by credit card?
Yes, however processing credit cards is expensive and you need to cover the costs. 4\% card processing fee applies for payment by credit card in Hong Kong dollars. 7\% card processing and exchange fee applies for payment by credit card in US dollars.
##### We are a distributor. Can we have exclusive rights to your products?
Exclusivity is subject to contractual minimum order volumes determined at our discretion, but generally exceeding USD 300,000.00 per year for most countries.
##### What is m-labs-intl.com?
[m-labs-intl.com](https://m-labs-intl.com) is a mirror site operated by us and hosted in the USA by Hetzner. It is useful in situations where your institution or local authorities block access to Hong Kong websites (.hk domains and/or Hong Kong IP addresses). To our knowledge, no internet blocking is done on the Hong Kong side other than through ISP DNS resolvers, which we do not use and which does not affect your access to our website.
Some features are currently not available on the mirror site, and you may use a VPN or another circumvention measure of your choice to access these as required.
##### Why are you ignoring my emails?
If you are using the email server of a large institution, be aware that some may silently drop emails sent to .hk email addresses and/or to servers located in Hong Kong.
Try sending your message from your personal address (for example Gmail, Outlook and Yahoo do not block our email system), or to the same email address at the m-labs-intl.com domain (for example, sales@m-labs-intl.com or sb@m-labs-intl.com).
Alternatively, you may use our [contact phone numbers](../office/), which are also registered with several popular messaging apps.
##### We are a recruitment firm. Can we help you find talent?
No.

View File

@ -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) %} {% 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.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. 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.
We also have a location in Manila, Philippines. We also have a location in Manila, Philippines.
@ -83,5 +83,5 @@ Now is your chance to work on top-notch science and technology projects that get
{% layout_centered_content() %} {% layout_centered_content() %}
##### Contact us at [jobs@m-labs.hk](mailto:jobs@m-labs.hk) or [jobs@m-labs.ph](mailto:jobs@m-labs.ph)! ##### Contact us at {{ email(address="jobs") }} or [jobs@m-labs.ph](mailto:jobs@m-labs.ph)!
{% end %} {% end %}

View File

@ -62,7 +62,7 @@ The <a href="https://github.com/quartiq/stabilizer" target="_blank" rel="noopene
<div class="col-12 col-md-4"> <div class="col-12 col-md-4">
<p> <p>
To purchase this controller, email <a href="mailto:sales@m-labs.hk">sales@m-labs.hk</a>. We also offer firmware customizations and development of new features. Note that features that are not implemented in the open source code above (e.g. control from Kasli) are not supported unless purchased separately. To purchase this controller, email {{ email(address="sales") }}. We also offer firmware customizations and development of new features. Note that features that are not implemented in the open source code above (e.g. control from Kasli) are not supported unless purchased separately.
</p> </p>
</div> </div>

View File

@ -19,7 +19,7 @@ ARTIQ and the related components that we are developing ([Migen/MiSoC](/gateware
{% layout_funding(position="center", title="Sponsors") %} {% layout_funding(position="center", title="Sponsors") %}
We acknowledge support from our partners below. Please get in touch ([sales@m-labs.hk](mailto:sales@m-labs.hk)) if you also want to move ARTIQ forward! We acknowledge support from our partners below. Please get in touch ({{ email(address="sales") }}) if you also want to move ARTIQ forward!
{% end %} {% end %}

View File

@ -114,7 +114,7 @@ template = "page.html"
**Sinara hardware purchases, ports to your hardware, feature development, technical support, bugfixing** **Sinara hardware purchases, ports to your hardware, feature development, technical support, bugfixing**
contact [sales@m-labs.hk](mailto:sales@m-labs.hk) contact {{ email(address="sales") }}
We welcome inquiries from research groups of all sizes.<br>[See what has been funded before](/experiment-control/funding) We welcome inquiries from research groups of all sizes.<br>[See what has been funded before](/experiment-control/funding)
@ -170,13 +170,6 @@ We welcome inquiries from research groups of all sizes.<br>[See what has been fu
<a href="https://github.com/cnourshargh/Bham-ARTIQ-examples" target="_blank" rel="noopener noreferrer">Repository</a> <a href="https://github.com/cnourshargh/Bham-ARTIQ-examples" target="_blank" rel="noopener noreferrer">Repository</a>
{% end %} {% end %}
{% layout_card(title="Deltaflow-on-ARTIQ", sameheight=120) %}
<small>Run programs in the Deltaflow language from Riverlane on simulated ARTIQ</small>
<a href="https://github.com/riverlane/deltaflow-on-artiq" target="_blank" rel="noopener noreferrer">Repository</a>
{% end %}
{% layout_card(title="DAX - Duke ARTIQ extensions", sameheight=120) %} {% layout_card(title="DAX - Duke ARTIQ extensions", sameheight=120) %}
<small>A library to provide tools for system organization/abstraction and to improve usability by automating common functionality.</small> <small>A library to provide tools for system organization/abstraction and to improve usability by automating common functionality.</small>
@ -184,6 +177,12 @@ We welcome inquiries from research groups of all sizes.<br>[See what has been fu
{% end %} {% 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) %} {% layout_card(title="flake8-artiq", sameheight=120) %}
<small>A Flake8 plugin for checking ARTIQ code</small> <small>A Flake8 plugin for checking ARTIQ code</small>
@ -228,9 +227,15 @@ We welcome inquiries from research groups of all sizes.<br>[See what has been fu
<a href="https://github.com/vuthalab/artiq" target="_blank" rel="noopener noreferrer">Repository</a> <a href="https://github.com/vuthalab/artiq" target="_blank" rel="noopener noreferrer">Repository</a>
{% end %} {% end %}
{% layout_card(title="OffsetStabilizer", sameheight=120) %}
<small>Stabilizer firmware for laser frequency offset stabilization</small>
<a href="https://github.com/PhBrb/OffsetStabilizer" target="_blank" rel="noopener noreferrer">Repository</a>
{% end %}
</div> </div>
{% layout_div(css="col-12 text-center") %} {% layout_div(css="col-12 text-center") %}
Want your project listed here? Write to [sb@m-labs.hk](mailto:sb@m-labs.hk). Want your project listed here? Write to {{ email(address="sb") }}.
{% end %} {% end %}

View File

@ -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 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="http://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="https://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. 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. Use our [web-based configuration and ordering tool](../place-order), or contact {{ email(address="sales") }} with your requirements, and we will establish a quote.
{% end %} {% end %}
@ -270,6 +270,7 @@ The 4624 AWG "Phaser" is a quad channel 1.25 GS/s RF generator card with dual IQ
- 2x 1.25 GS/s IQ upconverters. - 2x 1.25 GS/s IQ upconverters.
- dual IQ mixer + 0.3 GHz to 4.8 GHz VCO + PLL. - dual IQ mixer + 0.3 GHz to 4.8 GHz VCO + PLL.
- up to 16 dynamic tones per channel using <a href="https://github.com/quartiq/miqro-sim" target="_blank" rel="noopener noreferrer">MIQRO gateware</a> (available separately from QUARTIQ).
- 31.5 dB range digital step attenuator (similar to Urukul). - 31.5 dB range digital step attenuator (similar to Urukul).
- 2 channels of 5 MS/s ADC (similar to Sampler). - 2 channels of 5 MS/s ADC (similar to Sampler).
- Artix-7 FPGA. - Artix-7 FPGA.
@ -293,11 +294,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") %} {% layout_centered_content(min_width=true, css="row d-flex align-items-center mt-5") %}
##### Kasli and EEMs can be ordered now ##### Ordering from M-Labs is easy and quick
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. 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.
Contact <a href="mailto:sales@m-labs.hk">sales@m-labs.hk</a> with your requirements and we will establish a quote. Use our [web-based configuration and ordering tool](../place-order), or contact {{ email(address="sales") }} with your requirements, and we will establish a quote.
{% end %} {% end %}

View File

@ -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. 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="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="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.
Under the hood, the Milkymist One is like a mini-computer running our Flickernoise video synthesis software. 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 ##### 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> - 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="http://www.mbnet.fi/artikkeli/lehti/avointa_vj_rautaa_3_2012">Milkymist One -visualisaattori: Avointa vj-rautaa</a> [FI] - 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]
- 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> - 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> - 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&amp;Practice (22/09/2011): <a href="http://theoryandpractice.ru/seminars/19402-videosintezator-s-otkrytym-kodom-milkymist-one-22-9">Видеосинтезатор с открытым кодом Milkymist One</a> [RU] - Theory&amp;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]
- 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] - 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] - 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> - 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
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 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 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 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: Flickernoise uses:
- The <a href="http://www.rtems.org">RTEMS</a> real-time operating system. - The <a href="http://www.rtems.org">RTEMS</a> real-time operating system.
- 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="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.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.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="http://www.freetype.org">freetype</a> font rendering system.
- The <a href="http://www.mupdf.com">MuPDF</a> library for the online help system. - The <a href="https://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/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. - <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.

View File

@ -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://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-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://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="http://blog.lambdaconcept.com/doku.php?id=migen:tutorial" target="_blank" rel="noopener noreferrer">Migen Step by Step Tutorial</a> by LambdaConcept - <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
{% end %} {% end %}

View File

@ -28,7 +28,7 @@ nMigen itself provides the core language, and is complemented by a number of ext
##### Documentation ##### Documentation
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>. 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>.
{% end %} {% end %}

View File

@ -30,7 +30,7 @@ Built on the <a href="/gateware/migen/">MiSoC and Migen</a> technologies that or
The Mixxeo supported mixing from two DVI or HDMI sources up to 720p60, with crossfade, fade to black and potentially other effects with a latency of less than two frames. The Mixxeo supported mixing from two DVI or HDMI sources up to 720p60, with crossfade, fade to black and potentially other effects with a latency of less than two frames.
<b>Status (Aug 2014)</b> - Main board and gateware have been <a href="/images/mixxeo_result.jpg">mostly functional</a> for a while, mechanical design and manufacturing for the case/mechatronics are progressing slowly. If you have the skills and would like to help out, email [sb@m-labs.hk](mailto:sb@m-labs.hk) or the mailing list. <b>Status (Aug 2014)</b> - Main board and gateware have been <a href="/images/mixxeo_result.jpg">mostly functional</a> for a while, mechanical design and manufacturing for the case/mechatronics are progressing slowly. If you have the skills and would like to help out, email {{ email(address="sb") }} or the mailing list.
<center><img src="/images/mixxeo_menu.png"><br /><br /><img src="/images/mixxeo_board.jpg" class="picimg"></center> <center><img src="/images/mixxeo_menu.png"><br /><br /><img src="/images/mixxeo_board.jpg" class="picimg"></center>

View File

@ -26,6 +26,6 @@ smoltcp achieves <a href="https://github.com/smoltcp-rs/smoltcp#examplesbenchmar
The source code is available <a href="https://github.com/smoltcp-rs/smoltcp" rel="noopener noreferrer">on GitHub</a>. The source code is available <a href="https://github.com/smoltcp-rs/smoltcp" rel="noopener noreferrer">on GitHub</a>.
**Commercial support for smoltcp is available.** Email [sales@m-labs.hk](mailto:sales@m-labs.hk). **Commercial support for smoltcp is available.** Email {{ email(address="sales") }}.
{% end %} {% end %}

2180
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,38 +6,50 @@
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"start": "npx webpack --watch", "start": "npx webpack --watch",
"build": "npx webpack" "build": "npx webpack",
"start-dev": "npx webpack --watch --mode=development --devtool=inline-source-map",
"build-dev": "npx webpack --mode=development --devtool=inline-source-map"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.m-labs.hk/M-Labs/web2019.git" "url": "https://git.m-labs.hk/M-Labs/web2019.git"
}, },
"dependencies": {
"@hello-pangea/dnd": "^16.6.0",
"bootstrap": "^5.3.3",
"json-logic-js": "^2.0.5",
"react": "^18.3.1",
"react-bootstrap": "^2.10.4",
"uuid": "^9.0.1",
"zustand": "^4.5.4"
},
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.23.0", "@babel/cli": "^7.24.8",
"@babel/core": "^7.23.2", "@babel/core": "^7.25.2",
"@babel/preset-env": "^7.23.2", "@babel/preset-env": "^7.25.3",
"@babel/preset-react": "^7.22.15", "@babel/preset-react": "^7.24.7",
"@uidotdev/usehooks": "^2.4.1",
"babel-loader": "^9.1.3", "babel-loader": "^9.1.3",
"babel-preset-minify": "^0.5.2", "babel-preset-minify": "^0.5.2",
"bootstrap": "^5.3.0", "webpack": "^5.93.0",
"jquery": "^3.7.0",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-bootstrap": "^2.9.1",
"@hello-pangea/dnd": "^16.5.0",
"react-dom": "^18.2.0",
"uuid": "^9.0.1",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4", "webpack-cli": "^5.1.4",
"json-logic-js": "^2.0.2",
"zustand": "^4.4.7",
"@uidotdev/usehooks":"^2.4.1",
"webpack-preprocessor-loader": "^1.3.0" "webpack-preprocessor-loader": "^1.3.0"
}, },
"babel": { "babel": {
"presets": [ "presets": [
"@babel/preset-env", "@babel/preset-react",
"@babel/preset-react" [
"@babel/preset-env",
{
"targets": {
"browsers": [
">0.25%",
"not dead",
"not op_mini all"
]
}
}
]
] ]
} }
} }

View File

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

View File

@ -72,26 +72,32 @@ button {
overflow-y: scroll; overflow-y: scroll;
overflow-x: hidden; overflow-x: hidden;
position: relative; 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;*/ /*padding-bottom: 4rem!important;*/
}
> aside.aside:after { .gradient-bottom {
position: fixed; position: sticky;
bottom: 0; bottom: 0;
height: 100px; height: 100px;
width: calc(2 / 6 * 100%); //width: max(1/4 * 100%, 310px);
content: ""; width: inherit;
background: linear-gradient( content: "";
to top, background: linear-gradient(
rgba(13, 53, 71, 1), to top,
rgba(13, 53, 71, 0) rgba(13, 53, 71, 1),
); rgba(13, 53, 71, 0)
pointer-events: none; );
pointer-events: none;
}
} }
> section.main { > section.main {
flex: 4; flex: 4;
max-width: calc(4 / 6 * 100%); width: calc(3/4 * 100%);
overflow-y: scroll; overflow-y: scroll;
} }
} }
@ -100,7 +106,7 @@ button {
display: flex; display: flex;
color: white; color: white;
padding: 3rem 2rem 1rem; padding: 1rem 0rem 1rem 1.5rem;
.content { .content {
flex: 1; flex: 1;
@ -119,6 +125,7 @@ button {
h3 { h3 {
color: white; color: white;
font-size: 1.5rem;
} }
button { button {
@ -176,8 +183,57 @@ button {
} }
} }
.backlog-container { .catalog-container {
padding-bottom: 4rem; padding-bottom: 4rem;
.no-results {
color: rgba(255, 255, 255, 0.58);
display: inline-block;
padding: 0rem 1.5rem;
font-size: 1rem;
}
.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 { .rule {
@ -213,7 +269,7 @@ button {
font-size: .8rem; font-size: .8rem;
> .description { > .description {
width: 50%; width: 80%;
padding-right: 30px; padding-right: 30px;
} }
} }
@ -241,6 +297,9 @@ button {
.item-card-name { .item-card-name {
font-weight: 700; font-weight: 700;
&.tabbed {
padding-left: 16px;
}
} }
.price { .price {
@ -322,6 +381,12 @@ button {
height: auto; height: auto;
} }
} }
span {
width: 28px;
}
.span-with-margin {
margin-left: 16px;
}
} }
> .summary-form { > .summary-form {
@ -378,6 +443,26 @@ button {
} }
} }
.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 { .crate {
position: relative; position: relative;
@ -454,29 +539,12 @@ button {
height: 24px; height: 24px;
} }
> .alert-warning, .alert-info, .options-notification { > .alert-warning, .alert-info {
background-color: inherit; background-color: inherit;
height: inherit; height: inherit;
width: 20px; width: 20px;
padding-bottom: 3px; padding-bottom: 3px;
} }
.options-notification {
position: relative;
display: inline-block;
.options-badge {
position: absolute;
top: -2.5px;
right: -3.5px;
padding: 0;
color: white;
width: 7px;
height: 7px;
background-image: url("/images/shop/icon-notification.svg") ;
background-repeat: no-repeat;
background-size: 7px;
}
}
} }
.overlayRemove { .overlayRemove {

View File

@ -55,7 +55,7 @@
color: white; color: white;
font-weight: bold; font-weight: bold;
font-size: 1.75rem; font-size: 1.75rem;
padding: .75rem 2rem; padding: .75rem 1.5rem;
} }
#accordion_categories .accordion-body { #accordion_categories .accordion-body {
@ -73,14 +73,10 @@
text-decoration: none; text-decoration: none;
} }
.options-notification { .options-invalid {
width: 5px; box-shadow: 0 0 0 .25rem rgba(229, 62, 62, 0.25)!important;
height: 5px; --bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important;
position: relative;
top: -2.5px;
right: -2.5px;
} }
/* /*
##Device = Tablets, Ipads (portrait) ##Device = Tablets, Ipads (portrait)
##Screen = B/w 768px to 1024px ##Screen = B/w 768px to 1024px
@ -102,6 +98,14 @@
height: calc(100vh - 10px - 2.5rem); /* .navbar vertical padding + line height (.navbar-brand.font-size.rem * body.font-size * body.line-height)*/ 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 { #root-shop .productItem {
padding: 2rem 1rem 1rem; padding: 2rem 1rem 1rem;
} }
@ -119,7 +123,7 @@
} }
#root-shop .productItem .content ul { #root-shop .productItem .content ul {
font-size: .6rem; font-size: .75rem;
} }
#root-shop .panel .control { #root-shop .panel .control {
@ -141,10 +145,11 @@
} }
#root-shop .panel .summary>.summary-price table { #root-shop .panel .summary>.summary-price table {
font-size: 1rem; font-size: 0.8rem;
} }
#root-shop .panel .summary>.summary-form form { #root-shop .panel .summary>.summary-form form,
#root-shop .panel .summary>.summary-form .order-bar {
width: 100%; width: 100%;
} }
@ -170,7 +175,7 @@
} }
#root-shop table tr { #root-shop table tr {
padding: .8em 0; padding: .2em 0;
display: flex !important; display: flex !important;
justify-content: space-between; justify-content: space-between;
} }
@ -199,7 +204,7 @@
} }
body { body {
font-size: .7rem; font-size: .8rem;
} }
#root-shop, #root-shop>div { #root-shop, #root-shop>div {
@ -224,7 +229,7 @@
} }
#root-shop table tr { #root-shop table tr {
padding: .8em 0; padding: .2em 0;
display: flex !important; display: flex !important;
justify-content: space-between; justify-content: space-between;
} }
@ -234,15 +239,16 @@
} }
#root-shop .panel .summary>.summary-price table { #root-shop .panel .summary>.summary-price table {
font-size: .7rem; font-size: .8rem;
} }
#root-shop .panel .summary>.summary-form form { #root-shop .panel .summary>.summary-form form,
#root-shop .panel .summary>.summary-form .order-bar {
width: 100%; width: 100%;
} }
#root-shop .panel .summary>.summary-price tfoot { #root-shop .panel .summary>.summary-price tfoot {
font-size: .85rem; font-size: 1.0rem;
} }
/*#root-shop .panel .summary>.summary-form form input[type="submit"] { /*#root-shop .panel .summary>.summary-form form input[type="submit"] {
@ -294,7 +300,7 @@
} }
body { body {
font-size: .7rem; font-size: .8rem;
} }
#root-shop, #root-shop>div { #root-shop, #root-shop>div {
@ -314,11 +320,11 @@
} }
#root-shop .productItem .content h3 { #root-shop .productItem .content h3 {
font-size: 1rem; font-size: 1.25rem;
} }
#root-shop .productItem .content ul { #root-shop .productItem .content ul {
font-size: .5rem; font-size: .75rem;
} }
#root-shop .panel { #root-shop .panel {
@ -351,15 +357,16 @@
} }
#root-shop .panel .summary>.summary-price table { #root-shop .panel .summary>.summary-price table {
font-size: .7rem; font-size: .8rem;
} }
#root-shop .panel .summary>.summary-form form { #root-shop .panel .summary>.summary-form form,
#root-shop .panel .summary>.summary-form .order-bar {
width: 100%; width: 100%;
} }
#root-shop .panel .summary>.summary-price tfoot { #root-shop .panel .summary>.summary-price tfoot {
font-size: .85rem; font-size: 1rem;
} }
/*#root-shop .panel .summary>.summary-form form input[type="submit"] { /*#root-shop .panel .summary>.summary-form form input[type="submit"] {
@ -372,7 +379,6 @@
border-top-right-radius: 30px; border-top-right-radius: 30px;
width: 80px; width: 80px;
padding: 5px 0 5px 10px; padding: 5px 0 5px 10px;
margin-bottom: -25px;
margin-left: -1.3rem; margin-left: -1.3rem;
position: relative; position: relative;
z-index: 1; z-index: 1;
@ -391,7 +397,7 @@
} }
#root-shop table tr { #root-shop table tr {
padding: .8em 0; padding: .2em 0;
display: flex !important; display: flex !important;
justify-content: space-between; justify-content: space-between;
} }
@ -399,21 +405,22 @@
#root-shop .layout>aside.aside.menu-opened { #root-shop .layout>aside.aside.menu-opened {
/*transform: translate3d(0, 0, 0);*/ /*transform: translate3d(0, 0, 0);*/
transition: left .3s; transition: left .3s;
width: 310px; width: min(310px, 60vw);
max-width: 60%;
left: 0; left: 0;
} }
#root-shop .layout>aside.aside.menu-opened + section.main { #root-shop .layout>aside.aside.menu-opened + section.main {
/*transform: translate3d(310px, 0, 0);*/ /*transform: translate3d(310px, 0, 0);*/
transition: left .3s; transition: left .3s;
left: 310px; left: min(310px, 60vw);
position: relative; position: relative;
z-index: 0; z-index: 0;
} }
#root-shop .layout>aside.aside.menu-opened + section.main:after { #root-shop .layout>aside.aside.menu-opened + section.main:after {
content: ''; content: '';
position: absolute; position: fixed;
height: 100%; height: 100%;
width: 100%; width: 100%;
background-color: rgba(0, 0, 0, .3); background-color: rgba(0, 0, 0, .3);
@ -427,13 +434,13 @@
transition: left .3s; transition: left .3s;
position: fixed; position: fixed;
z-index: 1; z-index: 1;
left: -310px; left: max(-310px, -60vw);
width: 310px; width: min(310px, 60vw);
height: 100%; height: 100%;
} }
#root-shop .layout>aside.aside:after { #root-shop .layout>aside.aside .gradient-bottom {
width: 0; display: none;
} }
#root-shop .layout>aside.aside + section.main { #root-shop .layout>aside.aside + section.main {
@ -455,7 +462,7 @@
overflow: initial; overflow: initial;
} }
#root-shop .layout>aside.aside.menu-opened > .backlog-container { #root-shop .layout>aside.aside.menu-opened > .catalog-container {
overflow-y: scroll; overflow-y: scroll;
height: 100%; height: 100%;
} }
@ -465,7 +472,7 @@
} }
#accordion_categories button { #accordion_categories button {
font-size: 1rem; font-size: 1.5rem;
padding: .5rem 0.5rem; padding: .5rem 0.5rem;
} }
} }
@ -505,7 +512,7 @@
} }
body { body {
font-size: .7rem; font-size: .8rem;
} }
#root-shop, #root-shop>div { #root-shop, #root-shop>div {
@ -521,20 +528,21 @@
} }
#root-shop .productItem .content h3 { #root-shop .productItem .content h3 {
font-size: 1rem; font-size: 1.25rem;
} }
#root-shop .layout>aside.aside.menu-opened { #root-shop .layout>aside.aside.menu-opened {
/*transform: translate3d(0, 0, 0);*/ /*transform: translate3d(0, 0, 0);*/
transition: left .3s; transition: left .3s;
width: 310px; width: min(310px, 90vw);
max-width: 90%;
left: 0; left: 0;
} }
#root-shop .layout>aside.aside.menu-opened + section.main { #root-shop .layout>aside.aside.menu-opened + section.main {
/*transform: translate3d(310px, 0, 0);*/ /*transform: translate3d(310px, 0, 0);*/
transition: left .3s; transition: left .3s;
left: 310px; left: min(310px, 90vw);
position: relative; position: relative;
z-index: 0; z-index: 0;
} }
@ -554,13 +562,13 @@
transition: left .3s; transition: left .3s;
position: fixed; position: fixed;
z-index: 1; z-index: 1;
left: -310px; left: max(-310px, -90vw);
width: 310px; width: min(310px, 90vw);
height: 100%; height: 100%;
} }
#root-shop .layout>aside.aside:after { #root-shop .layout>aside.aside .gradient-bottom {
width: 0; display: none;
} }
#root-shop .layout>aside.aside + section.main { #root-shop .layout>aside.aside + section.main {
@ -616,15 +624,16 @@
} }
#root-shop .panel .summary>.summary-price table { #root-shop .panel .summary>.summary-price table {
font-size: .7rem; font-size: .8rem;
} }
#root-shop .panel .summary>.summary-form form { #root-shop .panel .summary>.summary-form form,
#root-shop .panel .summary>.summary-form .order-bar {
width: 100%; width: 100%;
} }
#root-shop .panel .summary>.summary-price tfoot { #root-shop .panel .summary>.summary-price tfoot {
font-size: .85rem; font-size: 1rem;
} }
/*#root-shop .panel .summary>.summary-form form input[type="submit"] { /*#root-shop .panel .summary>.summary-form form input[type="submit"] {
@ -637,7 +646,6 @@
border-top-right-radius: 30px; border-top-right-radius: 30px;
width: 80px; width: 80px;
padding: 5px 0 5px 10px; padding: 5px 0 5px 10px;
margin-bottom: 15px;
margin-left: -1.3rem; margin-left: -1.3rem;
} }
@ -654,7 +662,7 @@
} }
#root-shop table tr { #root-shop table tr {
padding: .8em 0; padding: .2em 0;
display: flex !important; display: flex !important;
justify-content: space-between; justify-content: space-between;
} }
@ -663,7 +671,7 @@
overflow: initial; overflow: initial;
} }
#root-shop .layout>aside.aside.menu-opened > .backlog-container { #root-shop .layout>aside.aside.menu-opened > .catalog-container {
overflow-y: scroll; overflow-y: scroll;
height: 100%; height: 100%;
} }
@ -673,7 +681,7 @@
} }
#accordion_categories button { #accordion_categories button {
font-size: 1rem; font-size: 1.5rem;
padding: .5rem 0.5rem; padding: .5rem 0.5rem;
} }
} }

View File

@ -0,0 +1 @@
<svg version="1.1" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(1.1429 0 0 1.1429 -18.286 -4.5719)" stroke="#fff"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="15.999"><path d="m176 128h48a8 8 0 0 1 8 8v64a8 8 0 0 1-8 8h-192a8 8 0 0 1-8-8v-64a8 8 0 0 1 8-8h48"/><line x1="128" x2="128" y1="24" y2="128"/><polyline points="80 80 128 128 176 80"/></g><circle cx="188" cy="168" r="8" fill="#fff" stroke-width="3.9994"/></g></svg>

After

Width:  |  Height:  |  Size: 490 B

View File

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

After

Width:  |  Height:  |  Size: 392 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 46 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 33 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 37 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 117 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -0,0 +1,39 @@
<?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>

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
import {OverlayTrigger} from "react-bootstrap"; import {OverlayTrigger} from "react-bootstrap";
import React from "react"; import React from "react";
import {MaxLevel} from "./warnings"; import {Levels, MaxLevel} from "./warnings";
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
import {compareArraysLevelOne} from "./utils"; import {compareArraysLevelOne} from "./utils";
@ -15,8 +15,8 @@ export function CardWarnings({crate_index, card_index}) {
placement="bottom" placement="bottom"
trigger={['click', 'hover', 'focus']} trigger={['click', 'hover', 'focus']}
overlay={ overlay={
({arrowProps, hasDoneInitialMeasure, show, ...props}) => ( ({arrowProps, hasDoneInitialMeasure, show, style, ...props}) => (
<div className="k-popup-warning" {...props}> <div className="k-popup-warning" style={{...style, backgroundColor: max_level.color}} {...props}>
{warnings.map((warning, _i) => { {warnings.map((warning, _i) => {
return ( return (
<p className="rule warning" key={`warnmsg_${card_index}_${warning.name}`}> <p className="rule warning" key={`warnmsg_${card_index}_${warning.name}`}>
@ -28,7 +28,7 @@ export function CardWarnings({crate_index, card_index}) {
} }
rootClose rootClose
> >
<img className="alert-warning" src={max_level.icon}/> <img className="alert-warning p-0" src={max_level.icon}/>
</OverlayTrigger> </OverlayTrigger>
) )
} }
@ -37,10 +37,10 @@ export function WarningIndicator({crate_index, card_index}) {
const warnings = useShopStore(state => state.crates[crate_index].items[card_index].show_warnings, compareArraysLevelOne); const warnings = useShopStore(state => state.crates[crate_index].items[card_index].show_warnings, compareArraysLevelOne);
const max_level = MaxLevel(warnings); const max_level = MaxLevel(warnings);
return ( return max_level.priority === Levels.warning.priority ? (
<img <img
className="alert-warning align-self-start" className="alert-warning align-self-start d-block"
src={max_level.icon} src={max_level.icon}
/> />
) ) : (<span className="alert-warning align-self-start d-block"></span>);
} }

View File

@ -3,7 +3,6 @@ import {Droppable} from "@hello-pangea/dnd";
import {cartStyle, compareArraysWithIds} from "./utils"; import {cartStyle, compareArraysWithIds} from "./utils";
import {ProductCartItem} from "./ProductCartItem"; import {ProductCartItem} from "./ProductCartItem";
import {FakePlaceholder} from "./FakePlaceholder"; import {FakePlaceholder} from "./FakePlaceholder";
import {FillExtData} from "./options/utils";
import {hp_to_slots} from "./count_resources"; import {hp_to_slots} from "./count_resources";
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
@ -29,12 +28,10 @@ export function Cart({crate_index}) {
const nbrSlots = hp_to_slots(crateParams(crate.crate_mode).hp); const nbrSlots = hp_to_slots(crateParams(crate.crate_mode).hp);
const products = crate.items.map((item, index) => { const products = crate.items.map((item, index) => {
const ext_data = FillExtData(crate.items, index);
return ( return (
<ProductCartItem <ProductCartItem
card_index={index} card_index={index}
crate_index={crate_index} crate_index={crate_index}
ext_data={ext_data}
first={index === 0} first={index === 0}
last={index === crate.items.length - 1 && nbrOccupied >= nbrSlots} last={index === crate.items.length - 1 && nbrOccupied >= nbrSlots}
key={item.id}/> key={item.id}/>

View File

@ -0,0 +1,69 @@
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 && state.search_bar_value.length > 0);
const noResults = useShopStore((state) => state.listed_cards.length === 0 && state.search_bar_value.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/>) ||
(noResults && <p className="no-results">No results</p>) ||
<CatalogGroups/>}
{provided.placeholder && (
<div style={{display: 'none'}}>
{provided.placeholder}
</div>
)}
<GradientBottom/>
</div>
)}
</Droppable>
);
}

View File

@ -1,24 +1,10 @@
import React from 'react';
import {Droppable} from "@hello-pangea/dnd";
import {ProductItem} from "./ProductItem"; import {ProductItem} from "./ProductItem";
import React from "react";
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
/**
* Component that renders the backlog in the aside
*/
export function Backlog() {
// #!render_count
const renderCount = useRenderCount();
export function CatalogGroups() {
const data = useShopStore((state) => state.groups); const data = useShopStore((state) => state.groups);
const items = useShopStore((state) => state.cards); const items = useShopStore((state) => state.cards);
const onClickToggleMobileSideMenu = useShopStore((state) => state.switchSideMenu);
const isMobile = useShopStore((state) => state.isMobile);
// #!render_count
console.log("Backlog renders: ", renderCount)
const ordered_groups = data.categories.map(groupItem => ({ const ordered_groups = data.categories.map(groupItem => ({
name: groupItem.name, name: groupItem.name,
@ -50,39 +36,9 @@ export function Backlog() {
); );
} }
); );
return ( return (
<Droppable <div className="accordion accordion-flush" id="accordion_categories">
droppableId={data.id} {groups}
isDropDisabled={false}> </div>
)
{(provided) => (
<div
className="backlog-container"
ref={provided.innerRef}
{...provided.droppableProps}>
{isMobile ? (
<div className="mobileCloseMenu">
<button onClick={onClickToggleMobileSideMenu}>
<img src="/images/shop/icon-close-white.svg" alt="add"/>
</button>
</div>
) : null}
<div className="accordion accordion-flush" id="accordion_categories">
{groups}
</div>
{provided.placeholder && (
<div style={{display: 'none'}}>
{provided.placeholder}
</div>
)}
</div>
)}
</Droppable>
);
} }

View File

@ -0,0 +1,16 @@
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} />
)
})}
</>
)
}

View File

@ -3,6 +3,7 @@ import {Cart} from "./Cart";
import {CrateMode} from "./CrateMode"; import {CrateMode} from "./CrateMode";
import {CrateWarnings} from "./CrateWarnings"; import {CrateWarnings} from "./CrateWarnings";
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
import {CrateOptions} from "./CrateOptions";
// #!render_count // #!render_count
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
@ -17,7 +18,8 @@ export function Crate({crate_index}) {
const renderCount = useRenderCount(); const renderCount = useRenderCount();
const crate = useShopStore((state) => state.crates[crate_index], const crate = useShopStore((state) => state.crates[crate_index],
(a, b) => a.length === b.length); (a, b) => a.length === b.length && a.id === b.id);
const modes_order = useShopStore((state) => state.modes_order);
const onDeleteCrate = useShopStore((state) => state.delCrate); const onDeleteCrate = useShopStore((state) => state.delCrate);
// #!render_count // #!render_count
@ -25,20 +27,25 @@ export function Crate({crate_index}) {
return ( return (
<div className="crate"> <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="crate-bar d-inline-flex justify-content-between"> <div className="delete-crate align-self-start align-content-start justify-content-end" onClick={() => onDeleteCrate(crate.id)}>
<CrateMode crate_index={crate_index}/> Delete crate <img src="/images/shop/icon-remove.svg" alt="remove"/>
</div>
<div className="delete-crate align-self-start align-content-start justify-content-end" onClick={() => onDeleteCrate(crate.id)}> </div>
Delete crate <img src="/images/shop/icon-remove.svg" alt="remove"/> ) : <></>
</div> }
</div>
<div className="crate-products"> <div className="crate-products">
<Cart crate_index={crate_index}/> <Cart crate_index={crate_index}/>
<CrateWarnings crate_index={crate_index} /> <CrateWarnings crate_index={crate_index} />
<CrateOptions crate_index={crate_index}/>
</div> </div>
</div> </div>
); );

View File

@ -19,20 +19,17 @@ export function CrateMode({crate_index}) {
// #!render_count // #!render_count
console.log("CrateMode renders: ", renderCount) console.log("CrateMode renders: ", renderCount)
if (modes_order.includes(crate.crate_mode)) {
return ( return (
<div className="crate-mode"> <div className="crate-mode">
{modes_order.map((mode_name, _) => ( {modes_order.map((mode_name, _) => (
<a <a
key={mode_name} key={mode_name}
className={(crate.crate_mode === mode_name ? 'active' : '')} className={(crate.crate_mode === mode_name ? 'active' : '')}
onClick={() => setMode(crate.id, mode_name)} onClick={() => setMode(crate.id, mode_name)}
href="#" href="#"
role="button">{crate_modes[mode_name].name}</a> role="button">{crate_modes[mode_name].name}</a>
))} ))}
</div> </div>
); );
} else {
return <div className="crate-mode"></div>
}
} }

View File

@ -0,0 +1,39 @@
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>
)
}

View File

@ -0,0 +1,13 @@
import React from "react";
import {DOMAIN} from "./utils";
export function DomainedEmail({address}) {
const target = `${address}@${DOMAIN}`;
return <a href={"mailto:" + target}>{target}</a>
}
export const DomainedRFQMessages = {
OK: <>We've received your request and will be in contact soon.</>,
ERROR: <>We cannot receive your request. Try using the export by coping the configuration and send it to us at <DomainedEmail address="sales"/></>
}

View File

@ -0,0 +1,7 @@
import React from 'react';
export function GradientBottom() {
return (
<div className="gradient-bottom"></div>
)
}

View File

@ -27,13 +27,16 @@ const JSONExample = JSON.stringify({
"options": null "options": null
} }
], ],
"type": "rack" "type": "rack",
"options": {}
}, },
{ {
"items": [], "items": [],
"type": "no_crate" "type": "no_crate",
"options": {}
} }
] ],
"options": {}
}); });
export function ImportJSON() { export function ImportJSON() {

View File

@ -8,7 +8,14 @@ export function OptionsDialogWrapper({crate_index, card_index, first, last}) {
const options = useShopStore((state) => state.crates[crate_index].items[card_index].options); 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 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_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 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); const onOptionsUpdate = useShopStore((state) => state.updateOptions);
@ -17,11 +24,14 @@ export function OptionsDialogWrapper({crate_index, card_index, first, last}) {
options={options} options={options}
data={options_data} data={options_data}
options_class={options_class} options_class={options_class}
key={"popover" + crate_id +card_index} key={"popover" + crate_id +card_id}
id={"popover"+ crate_id + card_index} id={"popover"+ crate_id + card_id}
big={card_size === "big"} big={card_size === "big"}
first={first} first={first}
last={last} last={last}
sideMenuIsOpen={sideMenuIsOpen}
onHideNotification={hideNotification}
displayNotification={displayNotification}
target={{ target={{
construct: ((outvar, value) => { construct: ((outvar, value) => {
// #!options_log // #!options_log

View File

@ -5,6 +5,7 @@ import {ShowJSON} from "./ShowJSON";
// #!render_count // #!render_count
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
import {OrderOptions} from "./OrderOptions";
/** /**
@ -29,6 +30,8 @@ export function OrderForm() {
return ( return (
<div className="summary-form"> <div className="summary-form">
<OrderOptions/>
<form onSubmit={submitForm} noValidate> <form onSubmit={submitForm} noValidate>
<input <input

View File

@ -0,0 +1,38 @@
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>
)
}

View File

@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import {OrderSummary} from "./OrderSummary"; import {SummaryOrder} from "./SummaryOrder";
import {OrderForm} from "./OrderForm"; import {OrderForm} from "./OrderForm";
import {CrateList} from "./CrateList"; import {CrateList} from "./CrateList";
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
@ -8,6 +8,7 @@ import {RFQFeedback} from "./RFQFeedback";
// #!render_count // #!render_count
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
import {OrderOptions} from "./OrderOptions";
/** /**
* Component that renders all things for order. * Component that renders all things for order.
@ -47,8 +48,7 @@ export function OrderPanel({title, description}) {
<CrateList/> <CrateList/>
<section className="summary"> <section className="summary">
<OrderSummary/> <SummaryOrder/>
<OrderForm/> <OrderForm/>
</section> </section>

View File

@ -6,9 +6,33 @@ import {useShopStore} from "./shop_store";
// #!render_count // #!render_count
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
function DatasheetLink({datasheet_file, datasheet_name}) {
return datasheet_file && datasheet_name && (<div className="ds">
<span className='doc-icon'></span>
<a href={datasheet_file} target="_blank" rel="noopener noreferrer">
{datasheet_name}
</a>
</div>)
}
function CardSpecs({specs}) {
return specs && specs.length > 0 && (<ul>
{specs.map((spec, index) =>
<li key={index}>{spec}</li>
)}
</ul>)
}
function AddButton({onAdd}) {
return <button onClick={onAdd}>
<img src="/images/shop/icon-add.svg" alt="add"/>
</button>
}
/** /**
* Component that renders a product. * Component that renders a product.
* Used in the aside (e.g backlog of product) * Used in the aside (e.g catalog of products)
*/ */
export function ProductItem({card_index}) { export function ProductItem({card_index}) {
// #!render_count // #!render_count
@ -16,53 +40,31 @@ export function ProductItem({card_index}) {
const getCardDescription = useShopStore((state) => state.getCardDescription); const getCardDescription = useShopStore((state) => state.getCardDescription);
const currency = useShopStore((state) => state.currency); const currency = useShopStore((state) => state.currency);
const onAddCard = useShopStore((state) => state.addCardFromBacklog); const addCardFromCatalog = useShopStore((state) => state.addCardFromCatalog);
const card = getCardDescription(card_index); const card = getCardDescription(card_index);
// #!render_count // #!render_count
console.log("ProductItem renders: ", renderCount) 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 ( return (
<section className="productItem"> <section className="productItem">
<div className="content"> <div className="content">
<h3 style={{'marginBottom': card.name_codename ? '5px' : '20px'}}>{card.name}</h3> <h3 style={{'marginBottom': card.name_codename ? '5px' : '20px'}}>{card.name_number} {card.name}</h3>
{card.name_codename ? ( {card.name_codename ? (
<p>{card.name_codename}</p> <p>{card.name_codename}</p>
) : null} ) : null}
<div className="price">{`${currency} ${formatMoney(card.price)}`}</div> <div className="price">{`${currency} ${formatMoney(card.price)}`}</div>
{render_specs} <CardSpecs specs={card.specs}/>
{render_datasheet_link} <DatasheetLink datasheet_file={card.datasheet_file} datasheet_name={card.datasheet_name}/>
</div> </div>
<div className="content"> <div className="content">
<AddButton onAdd={() => addCardFromCatalog(null, card_index, null)} />
<button onClick={() => onAddCard(null, card_index, null)}> <Draggable draggableId={card.id + card_index} index={card_index}>
<img src="/images/shop/icon-add.svg" alt="add"/>
</button>
<Draggable draggableId={card.id} index={card_index}>
{(provided, snapshot) => ( {(provided, snapshot) => (
<React.Fragment> <React.Fragment>
<img <img
@ -83,10 +85,7 @@ export function ProductItem({card_index}) {
</React.Fragment> </React.Fragment>
)} )}
</Draggable> </Draggable>
</div> </div>
</section> </section>
); );
} }

View File

@ -33,7 +33,8 @@ function ClockRenderer({occupied, max}) {
const resource_progress_renderers = { const resource_progress_renderers = {
"eem": EEMRenderer, "eem": EEMRenderer,
"clk": ClockRenderer, "clk": ClockRenderer,
"idc": EEMRenderer "idc": EEMRenderer,
"tec": EEMRenderer
} }
@ -45,6 +46,10 @@ function IDCTipRender({occupied, max}) {
return (<p key={uuidv4()}>{`${occupied}/${max} IDC connectors used`}</p>); return (<p key={uuidv4()}>{`${occupied}/${max} IDC connectors used`}</p>);
} }
function TECTipRender({occupied, max}) {
return (<p key={uuidv4()}>{`${occupied}/${max} TEC connectors used`}</p>);
}
function ClockTipRender({occupied, max}) { function ClockTipRender({occupied, max}) {
return (<p key={uuidv4()}>{`${occupied}/${max} clock connectors used`}</p>); return (<p key={uuidv4()}>{`${occupied}/${max} clock connectors used`}</p>);
} }
@ -52,7 +57,8 @@ function ClockTipRender({occupied, max}) {
const resource_tip = { const resource_tip = {
"eem": EEMTipRender, "eem": EEMTipRender,
"clk": ClockTipRender, "clk": ClockTipRender,
"idc": IDCTipRender "idc": IDCTipRender,
"tec": TECTipRender
} }
function RenderResources({resources, library}) { function RenderResources({resources, library}) {

View File

@ -0,0 +1,18 @@
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>
)
}

View File

@ -6,9 +6,10 @@ import {useRenderCount} from "@uidotdev/usehooks";
import {Layout} from "./Layout"; import {Layout} from "./Layout";
import {Backlog} from "./Backlog"; import {Catalog} from "./Catalog";
import {OrderPanel} from "./OrderPanel"; import {OrderPanel} from "./OrderPanel";
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
import {DomainedEmail} from "./Domained";
/** /**
* Component that renders the entire shop * Component that renders the entire shop
@ -18,7 +19,8 @@ export function Shop() {
// #!render_count // #!render_count
const renderCount = useRenderCount(); const renderCount = useRenderCount();
const addCardFromBacklog = useShopStore((state) => state.addCardFromBacklog); const addCardFromCatalog = useShopStore((state) => state.addCardFromCatalog);
const initExtData = useShopStore((state) => state.initExtData);
const moveCard = useShopStore((state) => state.moveCard); const moveCard = useShopStore((state) => state.moveCard);
const deleteCard = useShopStore((state) => state.deleteCard); const deleteCard = useShopStore((state) => state.deleteCard);
const cardIndexById = useShopStore((state) => state.cardIndexById); const cardIndexById = useShopStore((state) => state.cardIndexById);
@ -26,18 +28,20 @@ export function Shop() {
const handleOnDragEnd = (drop_result, _provided) => { const handleOnDragEnd = (drop_result, _provided) => {
if (!drop_result.destination) { if (!drop_result.destination) {
console.warn("No drop destination"); console.warn("No drop destination");
console.log(drop_result)
return; return;
} }
if (drop_result.source.droppableId === "backlog") if (drop_result.source.droppableId === "catalog")
addCardFromBacklog(drop_result.destination.droppableId, drop_result.source.index, drop_result.destination.index); addCardFromCatalog(drop_result.destination.droppableId, drop_result.source.index, drop_result.destination.index);
else if (drop_result.destination.droppableId === "backlog") else if (drop_result.destination.droppableId === "catalog")
deleteCard(drop_result.source.droppableId, drop_result.source.index); deleteCard(drop_result.source.droppableId, drop_result.source.index);
else else
moveCard(drop_result.source.droppableId, drop_result.source.index, drop_result.destination.droppableId, drop_result.destination.index) moveCard(drop_result.source.droppableId, drop_result.source.index, drop_result.destination.droppableId, drop_result.destination.index)
} }
useEffect(() => { useEffect(() => {
addCardFromBacklog(null, [cardIndexById("eem_pwr_mod"), cardIndexById("kasli")], -1, true); addCardFromCatalog(null, [cardIndexById("eem_pwr_mod"), cardIndexById("kasli")], -1, true);
initExtData();
}, []); }, []);
// #!render_count // #!render_count
@ -47,22 +51,25 @@ export function Shop() {
<DragDropContext onDragEnd={handleOnDragEnd}> <DragDropContext onDragEnd={handleOnDragEnd}>
<Layout <Layout
aside={ aside={
<Backlog/> <Catalog/>
} }
main={( main={(
<OrderPanel <OrderPanel
title="Order hardware" title="Order hardware"
description={( description={(
<p className="description">Drag and drop the cards you want into the crate below to see how <p className="description">
the combination would look like. Setup card's configuration by tapping at the top of the Drag and drop the cards you want into the crate below to see how
card, most of the options can be modified after shipment. If you have any issues with the combination would look like. Configure the card settings by tapping on the top of
this ordering system, or if you need other configurations, email us directly anytime the card; many of the options can be adjusted even after the card has been shipped.
at <a href="mailto:sales@m-labs.hk">sales@m-labs.hk</a>. The price is estimated and must If you have any issues with this ordering system, or if you need other configurations,
be confirmed by a quote.</p>)} email us directly anytime at <DomainedEmail address="sales"/>.
The price excludes shipping, is estimated, and must be confirmed by a quote.
</p>
)}
/> />
)}> )}>
</Layout> </Layout>
</DragDropContext> </DragDropContext>
); );
} }

View File

@ -79,12 +79,17 @@ export function ShowJSON() {
placeholder="There should be description of the crate"/> placeholder="There should be description of the crate"/>
<div className="d-flex flex-column flex-sm-row justify-content-end"> <div className="d-flex flex-column flex-sm-row justify-content-end">
<a type="button" {window.isSecureContext && (
onClick={() => {copyToClipboard(description)}} <a type="button"
className={"btn btn-sm m-0 mb-1 mt-2 mb-sm-0 me-sm-2 " + copyButtonStates[copiedState].style} onClick={() => {
> copyToClipboard(description)
{ copyButtonStates[copiedState].content } }}
</a> 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} <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> className="btn btn-sm btn-outline-primary m-0 mb-1 mt-2 mb-sm-0 me-sm-2">Close</a>
</div> </div>

View File

@ -3,10 +3,12 @@ import React from "react";
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
import {SummaryCrateHeader} from "./SummaryCrateHeader"; import {SummaryCrateHeader} from "./SummaryCrateHeader";
import {SummaryCrateCard} from "./SummaryCrateCard"; import {SummaryCrateCard} from "./SummaryCrateCard";
import {SummaryCratePricedOptions} from "./SummaryCratePricedOptions";
// #!render_count // #!render_count
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
export function SummaryCrate({crate_index}) { export function SummaryCrate({crate_index}) {
// #!render_count // #!render_count
const renderCount = useRenderCount(); const renderCount = useRenderCount();
@ -25,6 +27,8 @@ export function SummaryCrate({crate_index}) {
{range(0, crate_len).map((index, _i) => {range(0, crate_len).map((index, _i) =>
<SummaryCrateCard crate_index={crate_index} card_index={index} key={"summary_crate_" + crate_id + "_" +index} /> <SummaryCrateCard crate_index={crate_index} card_index={index} key={"summary_crate_" + crate_id + "_" +index} />
)} )}
<SummaryCratePricedOptions crate_index={crate_index}/>
</tbody> </tbody>
) )
} }

View File

@ -41,11 +41,7 @@ export function SummaryCrateCard({crate_index, card_index}) {
onClick={() => setHighlight(crate_id, card_index)} onClick={() => setHighlight(crate_id, card_index)}
onMouseEnter={() => setHighlight(crate_id, card_index)} onMouseEnter={() => setHighlight(crate_id, card_index)}
onMouseLeave={() => resetHighlight()}> onMouseLeave={() => resetHighlight()}>
<td className="item-card-name"> <td className="item-card-name tabbed">
<span style={{
'display': 'inline-block',
'width': '16px',
}}>&nbsp;</span>
<div>{`${card.name_number} ${card.name} ${card.name_codename}`}</div> <div>{`${card.name_number} ${card.name} ${card.name_codename}`}</div>
</td> </td>
@ -54,7 +50,7 @@ export function SummaryCrateCard({crate_index, card_index}) {
{`${currency} ${formatMoney(card.price)}`} {`${currency} ${formatMoney(card.price)}`}
<button onClick={() => deleteCard(crate_id, card_index)}> <button onClick={() => deleteCard(crate_id, card_index)}>
<img src="/images/shop/icon-remove.svg"/> <img src="/images/shop/icon-remove.svg" className="d-block"/>
</button> </button>
<div style={{'width': '45px', 'height': '20px'}} <div style={{'width': '45px', 'height': '20px'}}
@ -64,7 +60,7 @@ export function SummaryCrateCard({crate_index, card_index}) {
) : ( ) : (
<span style={{ <span style={{
'display': 'inline-block', 'display': 'inline-block',
'width': '20px', 'minWidth': '20px',
}}>&nbsp;</span> }}>&nbsp;</span>
))} ))}
{((options && options_data) ? ( {((options && options_data) ? (

View File

@ -16,6 +16,10 @@ export function SummaryCrateHeader({crate_index}) {
const crate_mode = useShopStore((state) => state.crates[crate_index].crate_mode); const crate_mode = useShopStore((state) => state.crates[crate_index].crate_mode);
const crate_id = useShopStore((state) => state.crates[crate_index].id); 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 // #!render_count
console.log("SummaryCrateHeader renders: ", renderCount) console.log("SummaryCrateHeader renders: ", renderCount)
@ -24,18 +28,24 @@ export function SummaryCrateHeader({crate_index}) {
return ( return (
<tr key={"summary_crate_" + crate_id}> <tr key={"summary_crate_" + crate_id}>
<td className="item-card-name">{crate_type.name}</td> <td className="item-card-name">{!!crate_name ? crate_name : crate_type.name + " #" + crate_index}</td>
<td className="price"> <td className="price">
<div className="d-inline-flex"> <div className="d-inline-flex">
{`${currency} ${formatMoney(crate_type.price)}`}
{crate_mode_displayed && `${currency} ${formatMoney(crate_type.price)}`}
<button onClick={() => clearCrate(crate_id)}> <button onClick={() => clearCrate(crate_id)}>
<img src="/images/shop/icon-clear.svg" alt="empty crate"/> <img src="/images/shop/icon-clear.svg" alt="empty crate"/>
</button> </button>
<button onClick={() => delCrate(crate_id)}> {
<img src="/images/shop/icon-remove.svg" alt="remove crate"/> crate_mode_displayed ? (
</button> <button onClick={() => delCrate(crate_id)}>
<img src="/images/shop/icon-remove.svg" alt="remove crate"/>
</button>
) : <span className="span-with-margin"></span>
}
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -0,0 +1,43 @@
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>
));
}

View File

@ -5,6 +5,8 @@ import {SummaryCrate} from "./SummaryCrate";
// #!render_count // #!render_count
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
import {SummaryOrderPricedOptions} from "./SummaryOrderPricedOptions";
import {SummaryOrderShipping} from "./SummaryOrderShipping";
export function SummaryCrates() { export function SummaryCrates() {
// #!render_count // #!render_count
@ -20,6 +22,8 @@ export function SummaryCrates() {
{range(0, crates_l).map((index, _i) => { {range(0, crates_l).map((index, _i) => {
return <SummaryCrate crate_index={index} key={"summary_crate_body_" + index} /> return <SummaryCrate crate_index={index} key={"summary_crate_body_" + index} />
})} })}
<SummaryOrderPricedOptions/>
<SummaryOrderShipping/>
</> </>
) )
} }

View File

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import {useShopStore} from "./shop_store";
import {SummaryCrates} from "./SummaryCrates"; import {SummaryCrates} from "./SummaryCrates";
import {SummaryTotalPrice} from "./SummaryTotalPrice"; import {SummaryTotalPrice} from "./SummaryTotalPrice";
@ -11,31 +10,17 @@ import {useRenderCount} from "@uidotdev/usehooks";
* Components that displays the list of card that are used in the crate. * Components that displays the list of card that are used in the crate.
* It is a summary of purchase * It is a summary of purchase
*/ */
export function OrderSummary() { export function SummaryOrder() {
// #!render_count // #!render_count
const renderCount = useRenderCount(); const renderCount = useRenderCount();
const clearAll = useShopStore((state) => state.clearAll);
// #!render_count // #!render_count
console.log("OrderSummary renders: ", renderCount) console.log("SummaryOrder renders: ", renderCount)
return ( return (
<div className="summary-price"> <div className="summary-price">
<table> <table>
<thead>
<tr>
<td colSpan="2" className="summary-remove-all">
<span className="item-card-name">Remove all cards</span>
<button onClick={clearAll}>
<img src="/images/shop/icon-remove.svg"/>
</button>
</td>
</tr>
</thead>
<SummaryCrates/> <SummaryCrates/>
<tfoot> <tfoot>

View File

@ -0,0 +1,43 @@
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>;
}

View File

@ -0,0 +1,35 @@
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>;
}

View File

@ -4,7 +4,7 @@ import React from "react";
export function SummaryTotalPrice() { export function SummaryTotalPrice() {
const currency = useShopStore((state) => state.currency); const currency = useShopStore((state) => state.currency);
const total_price = useShopStore((state) => state.totalOrderPrice()); const total_price = useShopStore((state) => state.total_order_price);
return ( return (
<div> <div>

View File

@ -26,10 +26,15 @@ const count_item_occupied_hp = (item) => {
return (item.consumes && item.consumes.hp) || 0; return (item.consumes && item.consumes.hp) || 0;
} }
const count_item_occupied_tec = (item) => {
return (item.options_data && item.options_data.tec === false) ? 0 : (item.consumes && item.consumes.tec);
}
export const item_occupied_counters = { export const item_occupied_counters = {
"eem": count_item_occupied_eem, "eem": count_item_occupied_eem,
"clk": count_item_occupied_clock, "clk": count_item_occupied_clock,
"idc": count_item_occupied_idc, "idc": count_item_occupied_idc,
"tec": count_item_occupied_tec,
"hp": count_item_occupied_hp, "hp": count_item_occupied_hp,
} }
@ -49,6 +54,7 @@ export const resource_counters = {
"clk": CounterFactory("clk"), "clk": CounterFactory("clk"),
"idc": CounterFactory("idc"), "idc": CounterFactory("idc"),
"hp": CounterFactory("hp"), "hp": CounterFactory("hp"),
"tec": CounterFactory("tec"),
} }
export function CountResources(data, index) { export function CountResources(data, index) {

View File

@ -5,20 +5,23 @@ import {v4 as uuidv4} from "uuid";
export function validateJSON(description) { export function validateJSON(description) {
let crates_raw; let crates_raw;
let order_options;
try { try {
const parsed = JSON.parse(description); const parsed = JSON.parse(description);
// here we can check additional fields // here we can check additional fields
crates_raw = parsed.crates; crates_raw = parsed.crates;
order_options = parsed.options;
} catch (e) { } catch (e) {
return false; return false;
} }
if (!order_options) return false;
const crate_modes = useShopStore.getState().crate_modes; const crate_modes = useShopStore.getState().crate_modes;
const modes_order = useShopStore.getState().modes_order; const modes_order = useShopStore.getState().modes_order;
const pn_to_card = useShopStore.getState().pn_to_cards; const pn_to_card = useShopStore.getState().pn_to_cards;
try { try {
for (const crate of crates_raw) { for (const crate of crates_raw) {
if (!crate.type || !crate.items || !(crate.type in crate_modes)) return false; if (!crate.type || !crate.items || !crate.options || !(crate.type in crate_modes)) return false;
for (const card of crate.items) { for (const card of crate.items) {
if (!(card.pn in pn_to_card) || card.options === undefined) return false; if (!(card.pn in pn_to_card) || card.options === undefined) return false;
} }
@ -40,6 +43,7 @@ export function JSONToCrates(description) {
const crates = Array.from(crates_raw.map((crate, c_i) => ({ const crates = Array.from(crates_raw.map((crate, c_i) => ({
id: crate.type === "no_crate" ? "spare" : "crate" + c_i, id: crate.type === "no_crate" ? "spare" : "crate" + c_i,
name: crate.type === "no_crate" ? "Spare cards" : undefined, name: crate.type === "no_crate" ? "Spare cards" : undefined,
options_data: crate.options,
crate_mode: crate.type, crate_mode: crate.type,
items: Array.from(crate.items.map((card, _i) => ({ items: Array.from(crate.items.map((card, _i) => ({
...pn_to_card(card.pn), ...pn_to_card(card.pn),
@ -52,11 +56,15 @@ export function JSONToCrates(description) {
return { return {
// some additional fields go here // some additional fields go here
order_options_data: parsed.options,
crates: crates crates: crates
}; };
} }
export function CratesToJSON(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({ return JSON.stringify({
// additional fields can go here // additional fields can go here
crates: Array.from(crates.map((crate, _i) => ({ crates: Array.from(crates.map((crate, _i) => ({
@ -64,7 +72,9 @@ export function CratesToJSON(crates) {
pn: card.name_number, pn: card.name_number,
options: (card.options_data && card.options) ? FilterOptions(card.options, card.options_data) : null options: (card.options_data && card.options) ? FilterOptions(card.options, card.options_data) : null
}))), }))),
type: crate.crate_mode type: crate.crate_mode,
}))) options: FilterOptions(crateOptions, crate.options_data)
}))),
options: FilterOptions(orderOptions, orderOptionsData)
}, null, 2) }, null, 2)
} }

View File

@ -3,7 +3,8 @@ import {useClickAway} from "./useClickAway";
import {ProcessOptions} from "./Options"; import {ProcessOptions} from "./Options";
import {Notification} from "./Notification"; import {Notification} from "./Notification";
export function DialogPopup({options, data, target, id, big, first, last, options_class}) { export function DialogPopup({options, data, target, id, big, first, last, options_class,
sideMenuIsOpen, displayNotification, onHideNotification}) {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const ref = useClickAway((e) => { const ref = useClickAway((e) => {
if (e.type === "mousedown") // ignore touchstart if (e.type === "mousedown") // ignore touchstart
@ -11,7 +12,7 @@ export function DialogPopup({options, data, target, id, big, first, last, option
} }
); );
let div_classes = `overlayVariant border rounded ${big ? "overlay-bigcard" : "overlay-smallcard"} ${(!big && first) ? "overlay-first" : ""} ${(!big && last) ? "overlay-last" : ""} ${options_class || ""}`; let div_classes = `overlayVariant border rounded ${big ? "overlay-bigcard" : "overlay-smallcard"} ${(!big && first) ? "overlay-first" : ""} ${(!big && last && !first) ? "overlay-last" : ""} ${options_class || ""}`;
const handleClick = (_event) => { const handleClick = (_event) => {
setShow(!show); setShow(!show);
}; };
@ -21,8 +22,12 @@ export function DialogPopup({options, data, target, id, big, first, last, option
<Notification <Notification
id={"processed_options_notification" + id} id={"processed_options_notification" + id}
tip="Customization options available" tip="Customization options available"
sideMenuIsOpen={sideMenuIsOpen}
show={displayNotification}
onHide={onHideNotification}
content={ content={
<img className="alert-info" src={show ? "/images/shop/icon-close.svg" : "/images/shop/icon-customize.svg"} <img className="alert-info"
src={show ? "/images/shop/icon-close.svg" : "/images/shop/icon-customize.svg"}
onClick={handleClick}/> onClick={handleClick}/>
} }
/> />

View File

@ -1,26 +1,21 @@
import {OverlayTrigger, Tooltip} from "react-bootstrap"; import {OverlayTrigger, Tooltip} from "react-bootstrap";
import React, {useState} from "react"; import React from "react";
export function Notification({id, tip, content}) {
const [show, setShow] = useState(true);
const onClickHandler = (event) => {
// prevent removing badge on touchables
if (!event.target.classList.contains("options-badge"))
setShow(false);
};
export function Notification({id, tip, content, sideMenuIsOpen, show, onHide}) {
return ( return (
<div className="options-notification" onClick={onClickHandler}> <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} {content}
{show ? <OverlayTrigger </OverlayTrigger>
placement="auto"
trigger={['click', 'hover', 'focus']}
style={{display: 'inline'}}
overlay={<Tooltip id={id}>{tip}</Tooltip>}
>
<span className="options-badge"></span>
</OverlayTrigger> : null}
</div>
) )
} }

View File

@ -24,7 +24,7 @@ export function ProcessOptions({options, data, target, id}) {
return componentsList[options.type](target, id + options.type, data, options.args); return componentsList[options.type](target, id + options.type, data, options.args);
} else if (options.type === "Group") { } else if (options.type === "Group") {
return ( return (
<div className="border rounded" key={id + "group"}> <div className="border rounded options-group" key={id + "group"}>
{ProcessOptions({ {ProcessOptions({
options: json_logic_apply(options.items, data), options: json_logic_apply(options.items, data),
data: data, data: data,
@ -41,3 +41,23 @@ export function ProcessOptions({options, data, target, 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;
}
}

View File

@ -76,7 +76,7 @@ export function SummaryPopup({id, options, data}) {
return ( return (
<div ref={ref}> <div ref={ref}>
<img className="alert-info" src={show ? "/images/shop/icon-close.svg" : "/images/shop/icon-customize.svg"} <img className="alert-info d-block" src={show ? "/images/shop/icon-close.svg" : "/images/shop/icon-customize.svg"}
id={id + "img"} id={id + "img"}
onClick={handleClick}/> onClick={handleClick}/>
<div style={{'display': show ? 'flex' : 'none', 'top': position.y, 'left': position.x}} <div style={{'display': show ? 'flex' : 'none', 'top': position.y, 'left': position.x}}

View File

@ -0,0 +1,17 @@
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>
)
}

View File

@ -1,43 +1,56 @@
import React, {Component} from "react"; import React, {Component} from "react";
import {Tip} from "./Tip"; import {Tip} from "./Tip";
import {Validation} from "../validation";
class Line extends Component { class Line extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
// Initialize the state object with the initial values from the props // Initialize the state object with the initial values from the props
this.state = { this.state = {
text: props.outvar in props.data ? props.data[props.outvar] : (props.fallback ? props.fallback : "") text: props.outvar in props.data ? props.data[props.outvar] : (props.fallback ? props.fallback : ""),
valid: true
}; };
// Bind the event handler to this // Bind the event handler to this
this.handleClick = this.handleClick.bind(this); this.handleChange = this.handleChange.bind(this);
this.props.target.construct(this.props.outvar, this.state.text); this.props.target.construct(this.props.outvar, this.state.text);
} }
handleClick(element) { handleChange(element) {
let text = element.target.value; let text = element.target.value;
this.setState({ this.setState({
text: text text: text,
valid: this.props.validator ? this.props.validator(text) : true
}); });
this.props.target.update(this.props.outvar, text); 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() { render() {
let key = this.props.id + this.props.outvar; let key = this.props.id + this.props.outvar;
return ( return (
<div className="shop-line" key={this.props.id}> <div className="shop-line" key={this.props.id}>
<label htmlFor={key} className="form-label"> <label htmlFor={key} className="form-label">
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>} {this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
{this.props.title}: {this.props.title}
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
:
</label> </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}
<input type="text" className="form-control form-control-sm" id={key} onChange={this.handleClick}
value={this.state.text}/> value={this.state.text}/>
</div> </div>
); );
} }
} }
export function LineWrapper(target, id, data, {title, fallback, outvar, icon, tip}) { 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} return <Line target={target} title={title} fallback={fallback} outvar={outvar} icon={icon} tip={tip} key={id}
id={id} data={data}/>; id={id} data={data} classes={classes} validator={validator && Validation[validator.name](validator.params)}/>;
} }

View File

@ -23,38 +23,49 @@ class Radio extends Component {
this.props.target.update(this.props.outvar, 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() { render() {
let key = this.props.id + this.props.outvar; let key = this.props.id + this.props.outvar;
return ( return (
<div className="shop-radio" key={this.props.id}> <div className="shop-radio" key={this.props.id}>
<div style={{"display": "inline"}}> <div style={{"display": "inline"}} className="shop-radio-label">
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>} {this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
{this.props.title} {this.props.title}
</div> </div>
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>} {this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
{this.props.variants.map((variant, _) => ( <div className="d-block">
<div className="form-check" key={key + variant}> {this.props.variants.map((variant, _) => (
<input <div className={`form-check shop-radio-variant ${this.props.classes}`} key={key + variant}>
className="form-check-input" <input
type="radio" className="form-check-input"
name={key} type="radio"
id={key + variant} name={key}
checked={this.state.variant === variant} id={key + variant}
onClick={() => this.handleClick(variant)} checked={this.state.variant === variant}
onChange={() => this.handleClick(variant)} onClick={() => this.handleClick(variant)}
/> onChange={() => this.handleClick(variant)}
<label className="form-check-label" htmlFor={key + variant}> />
{variant} <label className="form-check-label" htmlFor={key + variant}>
</label> {variant}
</div> </label>
))} </div>
))}
</div>
</div> </div>
); );
} }
} }
export function RadioWrapper(target, id, data, {title, variants, outvar, fallback, icon, tip}) { 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} return <Radio target={target} title={title} variants={variants} outvar={outvar} icon={icon} tip={tip} key={id}
fallback={fallback} fallback={fallback} classes={classes}
id={id} data={data}/>; id={id} data={data}/>;
} }

View File

@ -23,6 +23,15 @@ class Switch extends Component {
this.props.target.update(this.props.outvar, 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() { render() {
let key = this.props.id + this.props.outvar; let key = this.props.id + this.props.outvar;
return ( return (
@ -40,15 +49,15 @@ class Switch extends Component {
<label className="form-check-label" htmlFor={key} style={{"display": "inline"}}> <label className="form-check-label" htmlFor={key} style={{"display": "inline"}}>
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>} {this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
{this.props.title} {this.props.title}
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
</label> </label>
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
</div> </div>
</div> </div>
); );
} }
} }
export function SwitchWrapper(target, id, data, {title, fallback, outvar, icon, tip}) { 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} return <Switch target={target} title={title} fallback={fallback} outvar={outvar} icon={icon} tip={tip} key={id}
id={id} data={data}/>; id={id} data={data} classes={classes}/>;
} }

View File

@ -1,5 +1,6 @@
import React, {Component} from "react"; import React, {Component} from "react";
import {Tip} from "./Tip"; import {Tip} from "./Tip";
import {Validation} from "../validation";
class SwitchLine extends Component { class SwitchLine extends Component {
constructor(props) { constructor(props) {
@ -7,7 +8,8 @@ class SwitchLine extends Component {
// Initialize the state object with the initial values from the props // Initialize the state object with the initial values from the props
this.state = { this.state = {
text: props.outvar in props.data ? props.data[props.outvar].text : (props.fallback ? props.fallback.text : ""), 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) 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 // Bind the event handler to this
this.handleText = this.handleText.bind(this); this.handleText = this.handleText.bind(this);
@ -18,7 +20,8 @@ class SwitchLine extends Component {
handleText(element) { handleText(element) {
let new_state = { let new_state = {
...this.state, ...this.state,
text: element.target.value text: element.target.value,
valid: this.props.validator ? this.props.validator(element.target.value) : true
} }
this.setState(new_state); this.setState(new_state);
this.props.target.update(this.props.outvar, new_state); this.props.target.update(this.props.outvar, new_state);
@ -34,6 +37,17 @@ class SwitchLine extends Component {
this.props.target.update(this.props.outvar, 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() { render() {
let key = this.props.id + this.props.outvar; let key = this.props.id + this.props.outvar;
return ( return (
@ -51,17 +65,17 @@ class SwitchLine extends Component {
<label className="form-check-label" htmlFor={key + "switch"}> <label className="form-check-label" htmlFor={key + "switch"}>
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>} {this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
{this.props.title} {this.props.title}
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
</label> </label>
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
</div> </div>
<input type="text" className="form-control form-control-sm" id={key + "line"} onChange={this.handleText} <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}/> value={this.state.text} disabled={!this.state.checked}/>
</div> </div>
); );
} }
} }
export function SwitchLineWrapper(target, id, data, {title, fallback, outvar, icon, tip}) { 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} return <SwitchLine target={target} title={title} fallback={fallback} outvar={outvar} icon={icon} tip={tip} key={id}
id={id} data={data}/>; id={id} data={data} classes={classes} validator={validator && Validation[validator.name](validator.params)}/>;
} }

View File

@ -7,7 +7,8 @@ export function Tip({id, tip}) {
placement="auto" placement="auto"
trigger={['click', 'hover', 'focus']} trigger={['click', 'hover', 'focus']}
style={{display: 'inline'}} style={{display: 'inline'}}
overlay={<Tooltip id={id}>{tip}</Tooltip>} overlay={props => <Tooltip id={id} {...props}>{tip}</Tooltip>}
rootClose
> >
<img src={`/images/shop/icon-reminder.svg`} className="options-icon"/> <img src={`/images/shop/icon-reminder.svg`} className="options-icon"/>
</OverlayTrigger> </OverlayTrigger>

View File

@ -5,6 +5,7 @@ import {RadioWrapper} from "./Radio";
import {SwitchWrapper} from "./Switch"; import {SwitchWrapper} from "./Switch";
import {SwitchLineWrapper} from "./SwitchLine"; import {SwitchLineWrapper} from "./SwitchLine";
import {UnimplementedComponent} from "./UnimplementedComponent"; import {UnimplementedComponent} from "./UnimplementedComponent";
import {Label} from "./Label";
// Class components are used because we cannot use hooks for updating the state // Class components are used because we cannot use hooks for updating the state
@ -13,5 +14,6 @@ export const componentsList = {
"Switch": SwitchWrapper, "Switch": SwitchWrapper,
"Line": LineWrapper, "Line": LineWrapper,
"SwitchLine": SwitchLineWrapper, "SwitchLine": SwitchLineWrapper,
"Label": Label,
"Default": UnimplementedComponent, "Default": UnimplementedComponent,
}; };

View File

@ -4,11 +4,25 @@ import {componentsList} from "./components/components";
// https://stackoverflow.com/a/70511311 // https://stackoverflow.com/a/70511311
export const true_type_of = (obj) => Object.prototype.toString.call(obj).slice(8, -1).toLowerCase(); export const true_type_of = (obj) => Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
export function FillExtData(data, index) { export function FillExtCardData(data, index) {
return { return {
// we cannot use value id, because they are substituted with uuid
has_other_dio: data.filter((value, item_index) => index !== item_index && value.name &&value.name.endsWith("-TTL")).length > 0, 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_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, has_sampler: data.filter(((value, _) => value.name === "Sampler" && (!value.options_data || !value.options_data.mono_eem))).length > 0,
has_thermostat: data.filter((value, item_index) => index !== item_index && value.name_number && value.name_number === '8451').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,
} }
} }

View File

@ -0,0 +1,51 @@
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
};

View File

@ -1,14 +1,16 @@
'use strict'; 'use strict';
import {createWithEqualityFn} from "zustand/traditional"; import {createWithEqualityFn} from "zustand/traditional";
import {data as shared_data, itemsUnfoldedList} from "./utils"; import {DATA as shared_data, itemsUnfoldedList, API_RFQ} from "./utils";
import {true_type_of} from "./options/utils"; import {FillExtCrateData, FillExtOrderData, true_type_of} from "./options/utils";
import {v4 as uuidv4} from "uuid"; import {v4 as uuidv4} from "uuid";
import {FillResources} from "./count_resources"; import {FillResources} from "./count_resources";
import {FillExtData} from "./options/utils"; import {FillExtCardData} from "./options/utils";
import {TriggerCrateWarnings, TriggerWarnings} from "./warnings"; import {TriggerCrateWarnings, TriggerWarnings} from "./warnings";
import {Validation, validateEmail, validateNote, validateJSONInput} from "./validate"; import {Validation, validateEmail, validateNote, validateJSONInput} from "./validate";
import {CratesToJSON, JSONToCrates} from "./json_porter"; import {CratesToJSON, JSONToCrates} from "./json_porter";
import {ProcessOptionsToData} from "./options/Options";
import {DomainedRFQMessages} from "./Domained";
const cards_to_pn_map = (cards) => { const cards_to_pn_map = (cards) => {
@ -17,9 +19,9 @@ const cards_to_pn_map = (cards) => {
return result; return result;
}; };
const useBacklog = ((set, get) => ({ const useCatalog = ((set, get) => ({
cards: shared_data.items, cards: shared_data.items,
groups: shared_data.columns.backlog, groups: shared_data.columns.catalog,
cards_list: itemsUnfoldedList, cards_list: itemsUnfoldedList,
currency: shared_data.currency, currency: shared_data.currency,
pn_to_cards: cards_to_pn_map(shared_data.items), pn_to_cards: cards_to_pn_map(shared_data.items),
@ -28,12 +30,120 @@ const useBacklog = ((set, get) => ({
cardIndexById: card_id => get().cards_list.findIndex((element) => (card_id === element)) 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) => ({ const useCrateModes = ((set, get) => ({
crate_modes: shared_data.crateModes, crate_modes: shared_data.crateModes,
modes_order: shared_data.crateModeOrder, modes_order: shared_data.crateModeOrder,
crateParams: mode => get().crate_modes[mode], 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) => ({ const useLayout = ((set, get) => ({
isTouch: window.isTouchEnabled(), isTouch: window.isTouchEnabled(),
isMobile: window.deviceIsMobile(), isMobile: window.deviceIsMobile(),
@ -42,9 +152,15 @@ const useLayout = ((set, get) => ({
showNoDestination: false, showNoDestination: false,
timerAdded: null, timerAdded: null,
switchSideMenu: () => set(state => ({ _switchSideMenu: () => set(state => ({
sideMenuIsOpen: !state.sideMenuIsOpen sideMenuIsOpen: !state.sideMenuIsOpen
})), })),
switchSideMenu: () => {
if (!get().sideMenuIsOpen) {
get().hideNotification()
}
get()._switchSideMenu();
},
cardAdded: () => set(state => ({ cardAdded: () => set(state => ({
showCardAddedFeedback: true, showCardAddedFeedback: true,
showNoDestination: false, showNoDestination: false,
@ -81,16 +197,21 @@ const useImportJSON = ((set, get) => ({
const parsed = JSONToCrates(state.importValue.value); const parsed = JSONToCrates(state.importValue.value);
// if (parsed.crates[-1].crate_mode !== "") // if (parsed.crates[-1].crate_mode !== "")
return { return {
importShouldOpen: false, importShouldOpen: false,
// additional fields go here // additional fields go here
crates: parsed.crates crates: parsed.crates,
order_options_data: parsed.order_options_data
}}), }}),
loadDescription: () => { loadDescription: () => {
get()._loadDescription() get()._loadDescription()
get().fillOrderExtData();
get().crates.forEach((crate, _i) => { get().crates.forEach((crate, _i) => {
get().fillExtData(crate.id) get().fillExtData(crate.id);
get().fillWarnings(crate.id) get().fillWarnings(crate.id);
}) get().fillExtCrateData(crate.id);
});
get()._updateTotalOrderPrice();
get().showNotification(get().active_crate, null);
}, },
updateImportDescription: (new_description) => set(state => ({ updateImportDescription: (new_description) => set(state => ({
importValue: { importValue: {
@ -108,7 +229,7 @@ const useSubmitForm = ((set, get) => ({
status: Validation.OK, status: Validation.OK,
message: "" message: ""
}, },
API_RFQ: shared_data.API_RFQ, API_RFQ: API_RFQ,
email: { email: {
value: "", value: "",
error: null error: null
@ -186,18 +307,18 @@ const useSubmitForm = ((set, get) => ({
if (response.status !== 200) { if (response.status !== 200) {
throw Error("Response status is not OK: " + response.status + ".\n" + response); 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."}) get().finishSubmitForm({status: Validation.OK, message: DomainedRFQMessages.OK})
}, reason => { }, reason => {
console.error("Request rejected, reason:", reason) console.error("Request rejected, reason:", reason)
get().finishSubmitForm({ get().finishSubmitForm({
status: Validation.Invalid, 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" message: DomainedRFQMessages.ERROR
}) })
}).catch(err => { }).catch(err => {
console.error("Request failed, reason:", err) console.error("Request failed, reason:", err)
get().finishSubmitForm({ get().finishSubmitForm({
status: Validation.Invalid, 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" message: DomainedRFQMessages.ERROR
}) })
}) })
}, },
@ -238,19 +359,18 @@ const useHighlighted = ((set, get) => ({
const useCart = ((set, get) => ({ const useCart = ((set, get) => ({
crates: shared_data.columns.crates, crates: shared_data.columns.crates,
active_crate: "crate0", active_crate: "crate0",
_defaultCrates: Array.from(shared_data.columns.crates),
total_order_price: 0,
_newCrate: (crate_id) => set((state) => ({ _newCrate: (crate_id) => set((state) => ({
crates: state.crates.toSpliced(-1, 0, { crates: state.crates.toSpliced(-1, 0, {
...state._defaultCrates[0],
id: crate_id || "crate" + state.crates.length, id: crate_id || "crate" + state.crates.length,
crate_mode: "rack",
items: [],
warnings: [],
occupiedHP: 0
}), }),
active_crate: crate_id || "crate" + state.crates.length active_crate: crate_id || "crate" + state.crates.length
})), })),
delCrate: (id) => set(state => ({ _delCrate: (id) => set(state => ({
crates: state.crates.filter((crate => crate.id !== id)), 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, active_crate: state.active_crate === id ? null : state.active_crate,
})), })),
_setCrateMode: (id, mode) => set(state => ({ _setCrateMode: (id, mode) => set(state => ({
@ -264,7 +384,7 @@ const useCart = ((set, get) => ({
}) })
})), })),
setActiveCrate: (id) => set(state => ({active_crate: id})), setActiveCrate: (id) => set(state => ({active_crate: id})),
_addCardFromBacklog: (crate_to, index_from, index_to) => set(state => { _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 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; const dest = crate_to || state.active_crate;
if (!dest) return {}; if (!dest) return {};
@ -332,7 +452,7 @@ const useCart = ((set, get) => ({
}) })
})), })),
clearAll: () => set(state => ({ clearAll: () => set(state => ({
crates: [] crates: state._defaultCrates
})), })),
_updateOptions: (crate_id, index, new_options) => set(state => ({ _updateOptions: (crate_id, index, new_options) => set(state => ({
crates: state.crates.map((crate, _i) => { crates: state.crates.map((crate, _i) => {
@ -381,7 +501,7 @@ const useCart = ((set, get) => ({
itemsCopy = itemsCopy.map((item, index) => { itemsCopy = itemsCopy.map((item, index) => {
if (!item.options) return item; if (!item.options) return item;
if (!item.options_data) item.options_data = {}; if (!item.options_data) item.options_data = {};
item.options_data.ext_data = FillExtData(itemsCopy, index); item.options_data.ext_data = FillExtCardData(itemsCopy, index);
return item; return item;
}); });
return { return {
@ -393,16 +513,20 @@ const useCart = ((set, get) => ({
}) })
})), })),
totalOrderPrice: () => { _updateTotalOrderPrice: () => set(state => {
let sum = 0; let sum = 0;
get().crates.forEach( (crate, _i) => { get().crates.forEach( (crate, i) => {
sum += get().crate_modes[crate.crate_mode].price; sum += get().crate_modes[crate.crate_mode].price;
const crate_options = ProcessOptionsToData({options: get().crate_prices, data: crate.options_data || {}});
sum += crate_options ? crate_options.reduce((accumulator, currentValue) => accumulator+currentValue.price, 0) : 0;
crate.items.forEach((item, _) => { crate.items.forEach((item, _) => {
sum += item.price; sum += item.price;
}); });
}); });
return sum; 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: // Composite actions that require warnings recalculation:
@ -410,27 +534,41 @@ const useCart = ((set, get) => ({
const crate_id = "crate" + get().crates.length; const crate_id = "crate" + get().crates.length;
get()._newCrate(crate_id) get()._newCrate(crate_id)
get().fillExtData(crate_id); get().fillExtData(crate_id);
get().fillExtCrateData(crate_id);
get().fillOrderExtData();
get().fillWarnings(crate_id); get().fillWarnings(crate_id);
get()._updateTotalOrderPrice();
}, },
setCrateMode: (id, mode) => { setCrateMode: (id, mode) => {
get()._setCrateMode(id, mode) get()._setCrateMode(id, mode)
get().fillExtData(id); get().fillExtData(id);
get().fillExtCrateData(id);
get().fillOrderExtData();
get().fillWarnings(id); get().fillWarnings(id);
get().setActiveCrate(id); get().setActiveCrate(id);
get()._updateTotalOrderPrice();
}, },
addCardFromBacklog: (crate_to, index_from, index_to, just_mounted) => { delCrate: (id) => {
get()._delCrate(id);
get().fillOrderExtData();
get()._updateTotalOrderPrice();
},
addCardFromCatalog: (crate_to, index_from, index_to, just_mounted) => {
const dest = crate_to || get().active_crate; const dest = crate_to || get().active_crate;
if (!dest) { if (!dest) {
console.warn("No destination"); console.warn("No destination");
get().noDestinationWarning(); get().noDestinationWarning();
return {}; return {};
} }
get()._addCardFromBacklog(dest, index_from, index_to) get().showNotification(dest, index_to);
get()._addCardFromCatalog(dest, index_from, index_to)
get().fillExtData(dest); get().fillExtData(dest);
get().fillWarnings(dest); get().fillWarnings(dest);
get().setActiveCrate(dest); get().setActiveCrate(dest);
get()._updateTotalOrderPrice();
if (!just_mounted) { if (!just_mounted) {
get().cardAdded() get().cardAdded()
} }
@ -441,6 +579,7 @@ const useCart = ((set, get) => ({
get().fillExtData(crate_to); get().fillExtData(crate_to);
get().fillWarnings(crate_to); get().fillWarnings(crate_to);
get().setActiveCrate(crate_to); get().setActiveCrate(crate_to);
get()._updateTotalOrderPrice();
if (crate_from !== crate_to) { if (crate_from !== crate_to) {
get().fillExtData(crate_from); get().fillExtData(crate_from);
get().fillWarnings(crate_from); get().fillWarnings(crate_from);
@ -450,27 +589,42 @@ const useCart = ((set, get) => ({
get()._deleteCard(crate_id, index); get()._deleteCard(crate_id, index);
get().fillExtData(crate_id); get().fillExtData(crate_id);
get().fillWarnings(crate_id); get().fillWarnings(crate_id);
get()._updateTotalOrderPrice();
if (crate_id === get().highlighted.crate && index === get().highlighted.card) get().highlightReset() if (crate_id === get().highlighted.crate && index === get().highlighted.card) get().highlightReset()
}, },
clearCrate: (id) => { clearCrate: (id) => {
get()._clearCrate(id); get()._clearCrate(id);
get().fillWarnings(id); get().fillWarnings(id);
get()._updateTotalOrderPrice();
}, },
updateOptions: (crate_id, index, new_options) => { updateOptions: (crate_id, index, new_options) => {
get()._updateOptions(crate_id, index, new_options); get()._updateOptions(crate_id, index, new_options);
get().fillExtData(crate_id); get().fillExtData(crate_id);
get().fillWarnings(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) => ({ export const useShopStore = createWithEqualityFn((...params) => ({
...useBacklog(...params), ...useOptionsNotification(...params),
...useCatalog(...params),
...useSearch(...params),
...useCrateModes(...params), ...useCrateModes(...params),
...useCart(...params), ...useCart(...params),
...useSubmitForm(...params), ...useSubmitForm(...params),
...useLayout(...params), ...useLayout(...params),
...useHighlighted(...params), ...useHighlighted(...params),
...useImportJSON(...params), ...useImportJSON(...params),
...useCrateOptions(...params),
...useOrderOptions(...params),
})) }))

View File

@ -1,7 +1,9 @@
'use strict'; 'use strict';
export const data = window.shop_data; export const DATA = window.shop_data;
export const itemsUnfoldedList = Array.from(data.columns.backlog.categories.map(groupId => groupId.itemIds).flat()); export const DOMAIN = window.DOMAIN;
export const API_RFQ = window.API_RFQ;
export const itemsUnfoldedList = Array.from(DATA.columns.catalog.categories.map(groupId => groupId.itemIds).flat());
export const productStyle = (style, snapshot, removeAnim, hovered, selected, cart=false) => { export const productStyle = (style, snapshot, removeAnim, hovered, selected, cart=false) => {
const custom = { const custom = {
@ -9,7 +11,7 @@ export const productStyle = (style, snapshot, removeAnim, hovered, selected, car
backgroundColor: (hovered || selected) ? '#eae7f7' : 'initial', backgroundColor: (hovered || selected) ? '#eae7f7' : 'initial',
}; };
if (!cart && snapshot.draggingOver == null && // hack for backlog if (!cart && snapshot.draggingOver == null && // hack for catalog
((!snapshot.isDragging) // prevent next elements from animation ((!snapshot.isDragging) // prevent next elements from animation
|| (snapshot.isDragging && snapshot.isDropAnimating))) { // prevent dragged element from weird animation || (snapshot.isDragging && snapshot.isDropAnimating))) { // prevent dragged element from weird animation
style.transform = "none"; style.transform = "none";
@ -53,7 +55,7 @@ export function formatMoney(amount, decimalCount = 2, decimal = ".", thousands =
let i = parseInt(amount = Math.abs(Number(amount) || 0).toFixed(decimalCount)).toString(); let i = parseInt(amount = Math.abs(Number(amount) || 0).toFixed(decimalCount)).toString();
let j = (i.length > 3) ? i.length % 3 : 0; let j = (i.length > 3) ? i.length % 3 : 0;
return negativeSign + (j ? i.substr(0, j) + thousands : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + thousands) + (decimalCount ? decimal + Math.abs(amount - i).toFixed(decimalCount).slice(2) : ""); 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) { } catch (e) {
return amount; return amount;
} }
@ -91,4 +93,4 @@ export const compareArraysLevelOne = (a, b) =>
export function compareObjectsEmptiness(a, b) { export function compareObjectsEmptiness(a, b) {
return (!a && !b) || (!(!a !== !b) && Object.getPrototypeOf(a) === Object.getPrototypeOf(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)) (Object.getPrototypeOf(a) !== Object.getPrototypeOf([]) || !!Object.keys(a).length === !!Object.keys(b).length))
} }

View File

@ -8,8 +8,8 @@
import {item_occupied_counters, resource_counters} from "./count_resources"; import {item_occupied_counters, resource_counters} from "./count_resources";
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
const Levels = { export const Levels = {
"reminder": {priority: 1, icon: '/images/shop/icon-reminder.svg', color: "black"}, "reminder": {priority: 1, icon: '/images/shop/icon-reminder.svg', color: "#0d3547"},
"warning": {priority: 2, icon: '/images/shop/icon-warning.svg', color: "#c75e5e"}, "warning": {priority: 2, icon: '/images/shop/icon-warning.svg', color: "#c75e5e"},
} }
@ -71,7 +71,7 @@ const Types = {
"no_idc_source": { "no_idc_source": {
level: "warning", level: "warning",
trigger: no_source_trigger("idc"), trigger: no_source_trigger("idc"),
message: 'Should be after a Zotino or a HD68-IDC or with another IDC-BNC.' message: 'Should be after a Zotino or a HD68-IDC or with another IDC adapter.'
}, },
"clk_resource": { "clk_resource": {
level: "warning", level: "warning",

View File

@ -1,6 +1,4 @@
const shop_data = { const shop_data = {
API_RFQ: 'https://hooks.m-labs.hk/rfq',
currency: 'USD', currency: 'USD',
crateModes: { crateModes: {
@ -28,6 +26,142 @@ const shop_data = {
"rack", "desktop" "rack", "desktop"
], ],
crateOptions: {
options: [
{"if": [
{"==": [{"var": "ext_data.crate_mode"}, "rack",]},
{type: "Switch", args: {
title: "Add fan tray",
outvar: "fan_tray",
tip: "Add 1U 84hp fan tray (to be mounted under the crate) to improve cooling. " +
"Fans need 220VAC 50/60Hz power. 3 fans, 167m³/h air flow.",
fallback: false
}}
]},
],
prices: [{"if": [
{"and": [{"var": "fan_tray"}, {"==": [{"var": "ext_data.crate_mode"}, "rack",]}]},
{title: "Add fan tray", price: 470, disable_patch: {"fan_tray": false}, id: "fan_tray"}]}]
},
orderOptions: {
options: [
{"type": "Group", items:[
{type: "Switch", args: {
title: "Optional pre-installed NUC mini-computer",
outvar: "nuc",
tip: "Pre-installed NixOS desktop with ARTIQ and other scientific software. " +
"Hardware: ASUS® NUC 14 Pro Kit, Intel® i7-155H CPU, " +
"32GB RAM, 1TB NVMe. For other options contact us.",
fallback: true,
}},
{
"if": [
{"var": "nuc"},
[
{
type: "Radio",
args: {
title: "Desktop Environment:",
outvar: "nuc_desktop",
variants: ["Gnome", "KDE"],
fallback: 0,
classes: "form-check-inline ms-4"
}
},
{type: "Line", args: {title: "Additional software to be pre-installed", outvar: "software", fallback: "",
tip: "Most software from nixpkgs can be pre-installed."}},
{"if": [
{"var": "ext_data.has_crate"},
{type: "Switch", args: {
title: "Promotional USB stick",
outvar: "include_usb_stick_nuc",
tip: "Branded USB stick with device database and other relevant files. Files can also be emailed to you.",
fallback: false,
}}
]},
],
{"if": [
{"var": "ext_data.has_crate"},
{type: "Switch", args: {
title: "Include promotional USB stick",
outvar: "include_usb_stick",
tip: "Choose if you need a USB stick with device database and other relevant files. Alternative is to to receive them via other electronic means (e.g. email or cloud).",
fallback: true,
}}
]},
]
},
]},
{"type": "Group", items: [
{
type: "Radio",
args: {
title: "Shipping options:",
outvar: "shipping",
variants: [
"Incoterms 2020 FCA",
"Incoterms 2020 DAP",
"Prepay and add shipping (only available to credit customers)"
],
fallback: 0
}
},
{"if": [
{"==": [{"var": "shipping"}, "Incoterms 2020 FCA"]},
{type: "Line", args: {title: "Please provide your carrier account information and/or other shipping instructions",
outvar: "shipping_instructions", fallback: ""}}
]},
{"if": [
{"==": [{"var": "shipping"}, "Incoterms 2020 DAP"]},
{type: "Line", args: {title: "Please provide delivery address",
outvar: "shipping_instructions", fallback: "",
tip: "Additional customs fees may be charged to you by the carrier at the time of delivery."}}
]},
{"if": [
{"==": [{"var": "shipping"}, "Prepay and add shipping (only available to credit customers)"]},
[{type: "Radio", args: {title: "In case of additional customs fees:",
outvar: "prepay_fees_handling", fallback: 0,
variants: [
"Add to your final invoice",
"Carrier bills you directly"
]}},
{type: "Line", args: {title: "Please provide delivery address",
outvar: "shipping_instructions", fallback: "",}}],
]},
]}
],
prices: [{
"if": [{"var": "nuc"}, {title: "Pre-installed NUC mini-computer", price: 1300, disable_patch: {"nuc": false}, id: "nuc"}],
}],
shippingSummary: [
{type: "Label", args: {
content: ["Shipping method: ", {"var": "shipping"}]
}},
{"if": [
{"var": "shipping_instructions"},
{type: "Label", args: {
content: [
{"if": [
{"==": [{"var": "shipping"}, "Incoterms 2020 FCA"]},
"carrier account information and/or other shipping instructions: ",
"delivery address: "
]},
{"var": "shipping_instructions"}
]
}}]},
{type: "Label", args: {
content: [
{"if": [
{"==": [{"var": "shipping"}, "Prepay and add shipping (only available to credit customers)"]},
["In case of additional customs fees: ", {"lower": {"var": "prepay_fees_handling"}}],
]}
]
}},
]
},
items: { items: {
/* keys are also ids, avoid changing them */ /* keys are also ids, avoid changing them */
'kasli': { 'kasli': {
@ -45,7 +179,6 @@ const shop_data = {
'Price includes bitstream generation, flashing, testing, and firmware updates for 1 year (USD 1,400.00).', 'Price includes bitstream generation, flashing, testing, and firmware updates for 1 year (USD 1,400.00).',
], ],
size: 'big', size: 'big',
type: 'kasli',
options: [ options: [
{type: "Radio", args: {title: "DRTIO role", outvar: "drtio_role", variants: ["standalone", "master", "satellite"], tip: "Distributed Real Time Input/Output allows ARTIQ RTIO channels to be distributed among several satellite devices synchronized and controlled by a central core(master) device. Standalone option disables this feature."}}, {type: "Radio", args: {title: "DRTIO role", outvar: "drtio_role", variants: ["standalone", "master", "satellite"], tip: "Distributed Real Time Input/Output allows ARTIQ RTIO channels to be distributed among several satellite devices synchronized and controlled by a central core(master) device. Standalone option disables this feature."}},
{ {
@ -58,16 +191,21 @@ const shop_data = {
] ]
}, },
[ [
{type: "Line", args: {title: "IPv4", outvar: "ipv4", fallback: "192.168.1.75/24", tip: "Set up IPv4 address used by core device"}}, {type: "Line", args: {title: "IPv4", outvar: "ipv4", fallback: "192.168.1.75/24",
{type: "SwitchLine", args: {title: "IPv6", outvar: "ipv6"}}, tip: "Set up IPv4 address and mask used by core device", validator: {name: "ipv4"}}},
{type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", fallback: {text: "125 MHz", checked: false}, tip: "Use external clock reference: 10, 80 (beta), 100 or 125 MHz. Other variants may be provided if needed."}} {type: "SwitchLine", args: {title: "IPv6", outvar: "ipv6",
tip: "Set up IPv6 address and prefix used by core device",
validator: {name: "ipv6"}}},
{type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", fallback: {text: "125 MHz", checked: false},
validator: {name: "frequency", params: {min: 10e6, max: 1e9}},
tip: "Use external clock reference: 10, 80 (beta), 100 or 125 MHz. Other options may be provided if needed."}}
], ],
[ [
{type: "Switch", args: {title: "Optical fiber", outvar: "optics", tip: "Use optical fiber instead of direct attach copper cable"}}, {type: "Switch", args: {title: "Optical fiber", outvar: "optics", tip: "Use optical fiber instead of direct attach copper cable"}},
{"if": [ {"if": [
{"var": "optics"}, {"var": "optics"},
{type: "Radio", args: {title: "Fiber cable length", outvar: "cable_len", variants: ["1 M", "3 M", "5 M"], tip: "The desired length of the optical fiber cable", fallback: 1}}, {type: "Radio", args: {title: "Fiber cable length", outvar: "fiber_cable_len", variants: ["1 M", "3 M", "5 M"], tip: "The desired length of the optical fiber cable", fallback: 1}},
{type: "Radio", args: {title: "Copper cable length", outvar: "cable_len", variants: ["0.5 M", "1 M", "2 M"], tip: "The desired length of the direct attach copper cable", fallback: 0}}, {type: "Radio", args: {title: "Copper cable length", outvar: "copper_cable_len", variants: ["0.5 M", "1 M", "2 M"], tip: "The desired length of the direct attach copper cable", fallback: 0}},
]} ]}
] ]
] ]
@ -105,7 +243,6 @@ const shop_data = {
'4 MMCX clock outputs.', '4 MMCX clock outputs.',
], ],
size: 'big', size: 'big',
type: 'kasli',
hp: 8, hp: 8,
nbrSlotMin: 0, nbrSlotMin: 0,
nbrSlotMax: 12, nbrSlotMax: 12,
@ -126,16 +263,25 @@ const shop_data = {
] ]
}, },
[ [
{type: "Line", args: {title: "IPv4", outvar: "ipv4", fallback: "192.168.1.75/24", tip: "Set up IPv4 address used by core device"}}, {type: "Line", args: {title: "IPv4", outvar: "ipv4",
{type: "SwitchLine", args: {title: "IPv6", outvar: "ipv6"}}, validator: {name: "ipv4"},
{type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", fallback: {text: "125 MHz", checked: false}, tip: "Use external clock reference: 10, 80 (beta), 100 or 125 MHz. Other variants may be provided if needed."}} fallback: "192.168.1.75/24",
tip: "Set up IPv4 address used by core device"}},
{type: "SwitchLine", args: {title: "IPv6", outvar: "ipv6",
tip: "Set up IPv6 address and prefix used by core device",
validator: {name: "ipv6"}}},
{type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk",
validator: {name: "frequency", params: {min: 10e6, max: 1e9}},
fallback: {text: "125 MHz", checked: false},
tip: "Use external clock reference: 10, 80 (beta), 100 or 125 MHz. Other options may be provided if needed."}},
{type: "Switch", args: {title: "WRPLL", outvar: "wrpll", tip: "Enable WRPLL instead of Si5324"}},
], ],
[ [
{type: "Switch", args: {title: "Optical fiber", outvar: "optics", tip: "Use optical fiber instead of direct attach copper cable"}}, {type: "Switch", args: {title: "Optical fiber", outvar: "optics", tip: "Use optical fiber instead of direct attach copper cable"}},
{"if": [ {"if": [
{"var": "optics"}, {"var": "optics"},
{type: "Radio", args: {title: "Fiber cable length", outvar: "cable_len", variants: ["1 M", "3 M", "5 M"], tip: "The desired length of the optical fiber cable", fallback: 1}}, {type: "Radio", args: {title: "Fiber cable length", outvar: "fiber_cable_len", variants: ["1 M", "3 M", "5 M"], tip: "The desired length of the optical fiber cable", fallback: 1}},
{type: "Radio", args: {title: "Copper cable length", outvar: "cable_len", variants: ["0.5 M", "1 M", "2 M"], tip: "The desired length of the direct attach copper cable", fallback: 0}}, {type: "Radio", args: {title: "Copper cable length", outvar: "copper_cable_len", variants: ["0.5 M", "1 M", "2 M"], tip: "The desired length of the direct attach copper cable", fallback: 0}},
]} ]}
] ]
] ]
@ -168,7 +314,6 @@ const shop_data = {
'A pair of VHDCI carriers is a simple, low-latency and low-cost alternative to DRTIO for some applications.', 'A pair of VHDCI carriers is a simple, low-latency and low-cost alternative to DRTIO for some applications.',
], ],
size: 'big', size: 'big',
type: 'vhdcicarrier',
resources: [ resources: [
{name: "eem", max: 8}, {name: "eem", max: 8},
], ],
@ -199,7 +344,6 @@ const shop_data = {
datasheet_file: '/docs/sinara-datasheets/2118-2128.pdf', datasheet_file: '/docs/sinara-datasheets/2118-2128.pdf',
datasheet_name: '2118/2128 BNC/SMA-TTL datasheet', datasheet_name: '2118/2128 BNC/SMA-TTL datasheet',
size: 'big', size: 'big',
type: null,
options: [ options: [
{ {
"if": [ "if": [
@ -309,7 +453,6 @@ const shop_data = {
} }
], ],
size: 'small', size: 'small',
type: null,
warnings: [ warnings: [
"no_eem_source" "no_eem_source"
], ],
@ -401,7 +544,6 @@ const shop_data = {
} }
], ],
size: 'small', size: 'small',
type: null,
warnings: [ warnings: [
"no_eem_source" "no_eem_source"
], ],
@ -488,7 +630,6 @@ const shop_data = {
} }
], ],
size: 'small', size: 'small',
type: null,
warnings: [ warnings: [
"no_eem_source" "no_eem_source"
], ],
@ -512,7 +653,8 @@ const shop_data = {
'Digital step attenuator 0 to -31.5dB.', 'Digital step attenuator 0 to -31.5dB.',
'RF switch (1ns temporal resolution), 70dB isolation.', 'RF switch (1ns temporal resolution), 70dB isolation.',
'AD9910 and AD9912 cards can be used at the same time in the same crate.', 'AD9910 and AD9912 cards can be used at the same time in the same crate.',
'External 5W power amplifier is available separately, leave us a note if interested.' 'Power amplifier (e.g. AOM driver) options available, contact us for details.',
'Can be integrated and shipped with single- or double-pass fiber-coupled AOM at most wavelengths 400nm-1650nm, AOM driver, low-noise monitor PD and SU-Servo to reduce laser, AOM and fiber noise. Contact us for details.'
], ],
datasheet_file: '/docs/sinara-datasheets/4410-4412.pdf', datasheet_file: '/docs/sinara-datasheets/4410-4412.pdf',
datasheet_name: '4410/4412 Urukul datasheet', datasheet_name: '4410/4412 Urukul datasheet',
@ -522,7 +664,9 @@ const shop_data = {
"if": [ "if": [
{"var": "mono_eem"}, {"var": "mono_eem"},
[ [
{type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", fallback: {text: "125 MHz", checked: false}}}, {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk",
validator: {name: "frequency", params: {min: 10e6, max: 1e9}},
fallback: {text: "125 MHz", checked: false}}},
], ],
[ [
{type: "Switch", args: {title: "Synchronization", outvar: "sync", tip: "Synchronize phases across Urukuls"}}, {type: "Switch", args: {title: "Synchronization", outvar: "sync", tip: "Synchronize phases across Urukuls"}},
@ -531,7 +675,9 @@ const shop_data = {
{"var": "sync"}, {"var": "sync"},
null, null,
[ [
{type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", fallback: {text: "125 MHz", checked: false}}}, {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk",
validator: {name: "frequency", params: {min: 10e6, max: 1e9}},
fallback: {text: "125 MHz", checked: false}}},
{ {
"if": [ "if": [
{"var": "ext_data.has_sampler"}, {"var": "ext_data.has_sampler"},
@ -547,7 +693,6 @@ const shop_data = {
} }
], ],
size: 'small', size: 'small',
type: 'urukul',
warnings: [ warnings: [
"no_eem_source", "no_eem_source",
"no_clk_source" "no_clk_source"
@ -574,16 +719,18 @@ const shop_data = {
'RF switch (1ns temporal resolution), 70dB isolation.', 'RF switch (1ns temporal resolution), 70dB isolation.',
'AD9912 chip.', 'AD9912 chip.',
'AD9910 and AD9912 cards can be used at the same time in the same crate.', 'AD9910 and AD9912 cards can be used at the same time in the same crate.',
'External 5W power amplifier is available separately, leave us a note if interested.' 'Power amplifier (e.g. AOM driver) options available, contact us for details.',
'Can be integrated and shipped with single- or double-pass fiber-coupled AOM at most wavelengths 400nm-1650nm, and AOM driver. Contact us for details.'
], ],
datasheet_file: '/docs/sinara-datasheets/4410-4412.pdf', datasheet_file: '/docs/sinara-datasheets/4410-4412.pdf',
datasheet_name: '4410/4412 Urukul datasheet', datasheet_name: '4410/4412 Urukul datasheet',
options: [ options: [
{type: "Switch", args: {title: "Use 1 EEM", outvar: "mono_eem", tip: "Use one EEM port setup. RF switch and synchronization will be unavailable."}}, {type: "Switch", args: {title: "Use 1 EEM", outvar: "mono_eem", tip: "Use one EEM port setup. RF switch and synchronization will be unavailable."}},
{type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", fallback: {text: "125 MHz", checked: false}}} {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk",
validator: {name: "frequency", params: {min: 10e6, max: 1e9}},
fallback: {text: "125 MHz", checked: false}}}
], ],
size: 'small', size: 'small',
type: 'urukul',
warnings: [ warnings: [
"no_eem_source", "no_eem_source",
"no_clk_source" "no_clk_source"
@ -604,6 +751,7 @@ const shop_data = {
specs: [ specs: [
'2x 1.25 GS/s IQ upconverters.', '2x 1.25 GS/s IQ upconverters.',
'dual IQ mixer + 0.3 GHz to 4.8 GHz VCO + PLL.', 'dual IQ mixer + 0.3 GHz to 4.8 GHz VCO + PLL.',
'up to 16 dynamic tones using MIQRO gateware (available separately from QUARTIQ).',
'31.5 dB range digital step attenuator (similar to Urukul).', '31.5 dB range digital step attenuator (similar to Urukul).',
'2 channels of 5 MS/s ADC (similar to Sampler).', '2 channels of 5 MS/s ADC (similar to Sampler).',
'Artix-7 FPGA.', 'Artix-7 FPGA.',
@ -611,11 +759,12 @@ const shop_data = {
'The upconverter is optional, if you would like the baseband version please leave us a note.' 'The upconverter is optional, if you would like the baseband version please leave us a note.'
], ],
options: [ options: [
{type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", fallback: {text: "125 MHz", checked: false}}}, {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk",
validator: {name: "frequency", params: {min: 10e6, max: 1e9}},
fallback: {text: "125 MHz", checked: false}}},
{type: "Radio", args: {title: "Variant", outvar: "variant", variants: ["Baseband", "Upconverter"], fallback: 1}}, {type: "Radio", args: {title: "Variant", outvar: "variant", variants: ["Baseband", "Upconverter"], fallback: 1}},
], ],
size: 'small', size: 'small',
type: 'urukul',
warnings: [ warnings: [
"no_eem_source", "no_eem_source",
"no_clk_source" "no_clk_source"
@ -643,19 +792,29 @@ const shop_data = {
'DAC temperature can be stabilized using the Sinara 8451 Thermostat (sold separately).' 'DAC temperature can be stabilized using the Sinara 8451 Thermostat (sold separately).'
], ],
options: [ options: [
{type: "Switch", args: {title: "TEC chip", outvar: "tec_chip", tip: "Used for stabilizing temperature with Sinara 8451 Thermostat"}}, {
"if": [
{"var": "ext_data.has_thermostat"},
[
{type: "Switch", args: {title: "TEC", outvar: "tec", fallback: true, tip: "Used for stabilizing temperature with Sinara 8451 Thermostat"}}
],
[
{type: "Switch", args: {title: "TEC", outvar: "tec", tip: "Used for stabilizing temperature with Sinara 8451 Thermostat"}}
]
]
}
], ],
datasheet_file: '/docs/sinara-datasheets/5432.pdf', datasheet_file: '/docs/sinara-datasheets/5432.pdf',
datasheet_name: '5432 Zotino datasheet', datasheet_name: '5432 Zotino datasheet',
size: 'small', size: 'small',
type: 'zotino',
warnings: [ warnings: [
"no_eem_source", "no_eem_source",
"idc_resource" "idc_resource"
], ],
consumes: { consumes: {
hp: 4, hp: 4,
eem: 1 eem: 1,
tec: 1
}, },
resources: [ resources: [
{name: "idc", max: 4} {name: "idc", max: 4}
@ -677,7 +836,6 @@ const shop_data = {
'Channels can also be broken out to BNC or SMA using IDC-BNC, IDC-SMA or IDC-MCX cards.' 'Channels can also be broken out to BNC or SMA using IDC-BNC, IDC-SMA or IDC-MCX cards.'
], ],
size: 'small', size: 'small',
type: 'zotino',
warnings: [ warnings: [
"no_eem_source", "no_eem_source",
"idc_resource", "idc_resource",
@ -703,7 +861,6 @@ const shop_data = {
'Breaking out all 32 channels from a Zotino requires 4 IDC-BNC cards.' 'Breaking out all 32 channels from a Zotino requires 4 IDC-BNC cards.'
], ],
size: 'big', size: 'big',
type: 'idc-bnc',
warnings: [ warnings: [
"no_idc_source" "no_idc_source"
], ],
@ -712,6 +869,43 @@ const shop_data = {
idc: 1 idc: 1
} }
}, },
'hvamp32': {
id: 'hvamp32',
name: 'HV Amplifier',
name_number: '5633',
name_codename: '',
price: 1500,
image: '/images/shop/graphic-03_HVAMP32.svg',
specs: [
'Amplifier: LTC6090 or OPA462, 32 channels.',
'Amplification range up to ±25V and ±55V available.',
'Over-temperature protection.',
'External 12V power supply.',
'Slew rate 21V/us typical.',
'Bandwidth: 12MHz/gain, for ±25V range BW is 4.8MHz typical.',
'The same connector as in Zotino and Fastino.'
],
options: [
{type: "Radio",
args: {
title: "Amplification range",
outvar: "hvamp",
variants: ["±25V", "±55V"],
fallback: 0
}},
],
size: 'small',
warnings: [
"no_idc_source"
],
resources: [
{name: "idc", max: 4}
],
consumes: {
hp: 4,
idc: 4
}
},
'idc-sma-adapter': { 'idc-sma-adapter': {
id: 'idc-sma-adapter', id: 'idc-sma-adapter',
name: 'SMA-IDC', name: 'SMA-IDC',
@ -725,7 +919,6 @@ const shop_data = {
'Breaking out all 32 channels from a Zotino requires 4 SMA-IDC cards.' 'Breaking out all 32 channels from a Zotino requires 4 SMA-IDC cards.'
], ],
size: 'small', size: 'small',
type: 'idc-bnc',
warnings: [ warnings: [
"no_idc_source" "no_idc_source"
], ],
@ -739,12 +932,11 @@ const shop_data = {
name: 'MCX-IDC', name: 'MCX-IDC',
name_number: '5538', name_number: '5538',
name_codename: '', name_codename: '',
price: 160, price: 320,
image: '/images/shop/graphic-03_MCX-IDC.svg', image: '/images/shop/graphic-03_MCX-IDC32.svg',
specs: [ specs: [
'Breaks out analog signals from Zotino or HD68-IDC to MCX connectors.', 'Breaks out analog signals from Zotino or HD68-IDC to MCX connectors.',
'Each card provides 8 channels.', 'Each card provides 32 channels.',
'Breaking out all 32 channels from a Zotino requires 4 MCX-IDC cards.'
], ],
size: 'big', size: 'big',
type: 'idc-bnc', type: 'idc-bnc',
@ -753,7 +945,7 @@ const shop_data = {
], ],
consumes: { consumes: {
hp: 8, hp: 8,
idc: 1 idc: 4
} }
}, },
'hd68-idc-adapter': { 'hd68-idc-adapter': {
@ -767,9 +959,8 @@ const shop_data = {
'Connects an external HD68 cable to IDC-BNC, IDC-SMA or IDC-MCX cards.', 'Connects an external HD68 cable to IDC-BNC, IDC-SMA or IDC-MCX cards.',
], ],
size: 'small', size: 'small',
type: 'hd68',
options: [ options: [
{type: "Radio", args: {title: "Cable length", outvar: "cable_len", variants: ["1 M", "2 M", "3 M"], tip: "The desired length of the HD68 cable", fallback: 1}}, {type: "Radio", args: {title: "Cable length", outvar: "hd68_cable_len", variants: ["1 M", "2 M", "3 M"], tip: "The desired length of the HD68 cable", fallback: 1}},
], ],
options_class: "hd68-idc", options_class: "hd68-idc",
warnings: [ warnings: [
@ -824,7 +1015,6 @@ const shop_data = {
} }
], ],
size: 'big', size: 'big',
type: 'novo',
warnings: [ warnings: [
"no_eem_source" "no_eem_source"
], ],
@ -851,7 +1041,6 @@ const shop_data = {
{type: "Radio", args: {title: "Connectors", outvar: "n_eem", variants: ["1 EEM", "2 EEM", "3 EEM"], tip: "Number of EEM ports to use.", fallback: 1}}, {type: "Radio", args: {title: "Connectors", outvar: "n_eem", variants: ["1 EEM", "2 EEM", "3 EEM"], tip: "Number of EEM ports to use.", fallback: 1}},
], ],
size: 'small', size: 'small',
type: 'koster',
warnings: [ warnings: [
"no_eem_source" "no_eem_source"
], ],
@ -880,7 +1069,6 @@ const shop_data = {
], ],
options_class: "clocker", options_class: "clocker",
size: 'small', size: 'small',
type: 'clocker',
warnings: [ warnings: [
"no_eem_source", "no_eem_source",
"no_clk_source", "no_clk_source",
@ -911,14 +1099,50 @@ const shop_data = {
'Can be controlled by Kasli or work stand-alone with PoE supply.' 'Can be controlled by Kasli or work stand-alone with PoE supply.'
], ],
options: [ options: [
{type: "SwitchLine", args: {title: "IP", outvar: "ip", fallback: {text: "DHCP", checked: false}, tip: "Set up IP address used by the device"}}, {type: "SwitchLine", args: {title: "IP", outvar: "ip",
validator: {name: "ipv4or6"},
fallback: {text: "DHCP", checked: false},
tip: "Set up IP address used by the device"}},
{type: "Switch", args: {title: "Ext power", outvar: "ext_pwr", "tip": "Use external power supply in order to reduce number of used EEM connectors"}},
{type: "Switch", args: {title: "Term #0", outvar: "term_0", tip: "Enable termination on ADC channel #0"}},
{type: "Switch", args: {title: "Term #1", outvar: "term_1", tip: "Enable termination on ADC channel #1"}}
],
options_class: "stabilizer",
size: 'small',
warnings: [
"no_eem_source"
],
consumes: {
hp: 4,
eem: 1
},
},
'fast_servo': {
id: 'fast_servo',
name: 'Fast DSP',
name_number: '8462',
name_codename: 'Fast Servo',
price: 2900,
image: '/images/shop/graphic-03_Fast-Servo.svg',
specs: [
'High-speed, low-latency servo (Stabilizer-compatible) module.',
'Trenz TE0715-04 SoC module (XC7Z015).',
'2 channel 125MHz 16bit ADC (LTC2195).',
'2 channel 125MHz 14bit DAC (AD9117).',
'100Base-T Ethernet.',
'Can work stand-alone with PoE or DC supply.'
],
options: [
{type: "SwitchLine", args: {title: "IP", outvar: "ip",
validator: {name: "ipv4or6"},
fallback: {text: "DHCP", checked: false},
tip: "Set up IP address used by the device"}},
{type: "Switch", args: {title: "Ext power", outvar: "ext_pwr", "tip": "Use external power supply in order to reduce number of used EEM connectors"}}, {type: "Switch", args: {title: "Ext power", outvar: "ext_pwr", "tip": "Use external power supply in order to reduce number of used EEM connectors"}},
{type: "Switch", args: {title: "Term #0", outvar: "term_0", tip: "Enable termination on ADC channel #0"}}, {type: "Switch", args: {title: "Term #0", outvar: "term_0", tip: "Enable termination on ADC channel #0"}},
{type: "Switch", args: {title: "Term #1", outvar: "term_1", tip: "Enable termination on ADC channel #1"}} {type: "Switch", args: {title: "Term #1", outvar: "term_1", tip: "Enable termination on ADC channel #1"}}
], ],
options_class: "stabilizer", options_class: "stabilizer",
size: 'small', size: 'small',
type: null,
warnings: [ warnings: [
"no_eem_source" "no_eem_source"
], ],
@ -943,10 +1167,11 @@ const shop_data = {
'Large frequency changes take several milliseconds.', 'Large frequency changes take several milliseconds.',
], ],
options: [ options: [
{type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", fallback: {text: "125 MHz", checked: false}}} {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk",
validator: {name: "frequency", params: {min: 10e6, max: 600e6}},
fallback: {text: "125 MHz", checked: false}}}
], ],
size: 'small', size: 'small',
type: null,
warnings: [ warnings: [
"no_eem_source", "no_eem_source",
"no_clk_source" "no_clk_source"
@ -970,10 +1195,11 @@ const shop_data = {
'Each Almazny channel outputs twice the frequency of its corresponding Mirny channel.', 'Each Almazny channel outputs twice the frequency of its corresponding Mirny channel.',
], ],
options: [ options: [
{type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", fallback: {text: "125 MHz", checked: false}}} {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk",
validator: {name: "frequency", params: {min: 10e6, max: 600e6}},
fallback: {text: "125 MHz", checked: false}}}
], ],
size: 'big', size: 'big',
type: null,
warnings: [ warnings: [
"no_eem_source", "no_eem_source",
"no_clk_source" "no_clk_source"
@ -1003,7 +1229,6 @@ const shop_data = {
{type: "Switch", args: {title: "Ext power", outvar: "ext_pwr", "tip": "Use external power supply in order to reduce number of used EEM connectors"}} {type: "Switch", args: {title: "Ext power", outvar: "ext_pwr", "tip": "Use external power supply in order to reduce number of used EEM connectors"}}
], ],
size: 'small', size: 'small',
type: null,
warnings: [ warnings: [
"no_eem_source" "no_eem_source"
], ],
@ -1012,6 +1237,28 @@ const shop_data = {
eem: 1, eem: 1,
} }
}, },
'thermostat2ch': {
id: 'thermostat2ch',
name: 'Thermostat',
name_number: '8451',
name_codename: '',
price: 900,
image: '/images/shop/graphic-03_Thermostat2ch.svg',
specs: [
'2 TEC channels.',
'Parallel output 10 pin IDC 2.54mm and 5 pin 3.81mm connectors per channel.',
'Up to 8W (+/-2A with 4V compliance) heater/TEC drive from MAX1968 drivers.',
'100Base-T Ethernet with PoE.',
'Can stabilize temperature of Sinara 5432 DAC or external devices containing TEC and thermistor.'
],
size: 'small',
consumes: {
hp: 4
},
resources: [
{name: "tec", max: 2}
]
},
'shuttler': { 'shuttler': {
id: 'shuttler', id: 'shuttler',
name: 'DAC', name: 'DAC',
@ -1028,7 +1275,6 @@ const shop_data = {
'Included remote analog front-end (AFE) board converts differential signals to ±10V single-ended at the point of use, with additional gain and filtering.', 'Included remote analog front-end (AFE) board converts differential signals to ±10V single-ended at the point of use, with additional gain and filtering.',
], ],
size: 'big', size: 'big',
type: null,
warnings: [ warnings: [
"no_eem_source", "no_eem_source",
"no_clk_source" "no_clk_source"
@ -1052,20 +1298,23 @@ const shop_data = {
'AD9959 DDS (500MSPS, 10-bit).' 'AD9959 DDS (500MSPS, 10-bit).'
], ],
options: [ options: [
{type: "SwitchLine", args: {title: "IP", outvar: "ip", fallback: {text: "DHCP", checked: false}, tip: "Set up IP address used by the device"}}, {type: "SwitchLine", args: {title: "IP", outvar: "ip",
validator: {name: "ipv4or6"},
fallback: {text: "DHCP", checked: false},
tip: "Set up IP address used by the device"}},
{type: "Switch", args: {title: "Ext power", outvar: "ext_pwr", "tip": "Use external power supply in order to reduce number of used EEM connectors"}}, {type: "Switch", args: {title: "Ext power", outvar: "ext_pwr", "tip": "Use external power supply in order to reduce number of used EEM connectors"}},
{type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", fallback: {text: "125 MHz", checked: false}}}, {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk",
fallback: {text: "125 MHz", checked: false}, validator: {name: "frequency", params: {min: 10e6, max: 1e9}}}},
{type: "Switch", args: {title: "Termination #0", outvar: "term_0", tip: "Enable termination on ADC channel #0"}}, {type: "Switch", args: {title: "Termination #0", outvar: "term_0", tip: "Enable termination on ADC channel #0"}},
{type: "Switch", args: {title: "Termination #1", outvar: "term_1", tip: "Enable termination on ADC channel #1"}} {type: "Switch", args: {title: "Termination #1", outvar: "term_1", tip: "Enable termination on ADC channel #1"}}
], ],
size: 'big', size: 'big',
type: null,
warnings: [ warnings: [
"no_eem_source", "no_eem_source",
"no_clk_source" "no_clk_source"
], ],
consumes: { consumes: {
hp: 4, hp: 8,
eem: 1, eem: 1,
clk: 1 clk: 1
} }
@ -1089,22 +1338,52 @@ const shop_data = {
"Optional - external power brick will be shipped free of charge if removed." "Optional - external power brick will be shipped free of charge if removed."
], ],
size: 'big', size: 'big',
warnings: [],
consumes: {
hp: 8,
},
},
'kirdy': {
id: 'kirdy',
name: 'Laser diode driver',
name_number: '1550',
name_codename: 'Kirdy',
price: 1700,
image: '/images/shop/graphic-03_Kirdy.svg',
specs: [
"Precision LD driver: 300mA max output current, 20-bit resolution, 4V compliance voltage.",
"RMS noise 10Hz-1MHz: <200nA, density <300pA/√Hz.",
"Low-frequency modulation input, DC-18MHz 3dB bandwidth, suitable for laser locks and linewidth reduction.",
"High-frequency Bias-T modulation input, suitable for PDH sideband generation.",
"Monitor photodiode and LD protection.",
"Temperature controller: ±1A max current, sub-mK stability.",
"Full digital control and power over Ethernet.",
"Can ship with single-frequency narrow-linewidth laser pre-installed (1270-1610nm from USD +200.00, 633-1064nm from USD +4,000.00), fiber output on front panel.",
"Also suitable for commercial or homebuilt ECDL heads (with additional piezo driver), and injection-locked Fabry-Perot diodes."
],
options: [
{type: "Line", args: {title: "IP", outvar: "ip",
validator: {name: "ipv4"},
fallback: "192.168.1.128/24",
tip: "Set up IP address used by the device."}},
],
size: 'big',
type: null, type: null,
warnings: [], warnings: [],
consumes: { consumes: {
hp: 4, hp: 8,
}, },
}, },
}, },
columns: { columns: {
/*** /***
* backlog is the column containing all items on left aside, * catalog is the column containing all items on left aside,
* name should not change * name should not change
*/ */
'backlog': { 'catalog': {
id: 'backlog', id: 'catalog',
title: 'Backlog', title: 'Catalog',
/* itemIds define items order - change order to suit your need */ /* itemIds define items order - change order to suit your need */
categories: [ categories: [
{ name: 'Core', { name: 'Core',
@ -1130,6 +1409,7 @@ const shop_data = {
itemIds: [ itemIds: [
'zotino', 'zotino',
'fastino', 'fastino',
'hvamp32',
'novo', 'novo',
'shuttler']}, 'shuttler']},
{ name: 'Adapters', { name: 'Adapters',
@ -1139,13 +1419,18 @@ const shop_data = {
'idc-mcx-adapter', 'idc-mcx-adapter',
'hd68-idc-adapter', 'hd68-idc-adapter',
'vhdcicarrier']}, 'vhdcicarrier']},
{ name: 'Servos',
itemIds: [
'stabilizer',
'fast_servo',
'pounder',
'thermostat2ch',
'thermostat-eem']},
{ name: 'Misc', { name: 'Misc',
itemIds: [ itemIds: [
'koster', 'koster',
'stabilizer',
'pounder',
'thermostat-eem',
'eem_pwr_mod', 'eem_pwr_mod',
'kirdy',
]} ]}
], ],
}, },
@ -1153,6 +1438,7 @@ const shop_data = {
"crates": [{ "crates": [{
id: "crate0", id: "crate0",
crate_mode: "rack", crate_mode: "rack",
fan_tray: false,
items: [], items: [],
warnings: [], warnings: [],
occupiedHP: 0, occupiedHP: 0,

View File

@ -43,6 +43,7 @@
<link href="{{ get_url(path='favicon.ico', cachebust=true) }}" rel="icon" type="image/x-icon"> <link href="{{ get_url(path='favicon.ico', cachebust=true) }}" rel="icon" type="image/x-icon">
{% block styles %}{% endblock %} {% block styles %}{% endblock %}
</head> </head>
<body> <body>

View File

@ -5,7 +5,7 @@
<h5 class="pb-3">Sales inquiries</h5> <h5 class="pb-3">Sales inquiries</h5>
<p> <p>
To purchase ARTIQ Sinara hardware, software and gateware development services, or to enter a technical support agreement, write to <a href="mailto:sales@m-labs.hk">sales@m-labs.hk</a>. To purchase ARTIQ Sinara hardware, software and gateware development services, or to enter a technical support agreement, write to <a href="mailto:sales@{{get_env(name="DOMAINNAME", default="m-labs.hk")}}">sales@{{get_env(name="DOMAINNAME", default="m-labs.hk")}}</a>.
</p> </p>
</div> </div>
@ -15,7 +15,7 @@
<h5 class="pb-3">Commercial technical support</h5> <h5 class="pb-3">Commercial technical support</h5>
<p> <p>
If you need help setting up ARTIQ hardware purchased from us, or if you have a commercial technical support agreement with us, email us anytime at <a href="mailto:helpdesk@m-labs.hk">helpdesk@m-labs.hk</a>. If you need help setting up ARTIQ hardware purchased from us, or if you have a commercial technical support agreement with us, email us anytime at <a href="mailto:helpdesk@{{get_env(name="DOMAINNAME", default="m-labs.hk")}}">helpdesk@{{get_env(name="DOMAINNAME", default="m-labs.hk")}}</a>.
</p> </p>
<h5 class="pb-3">Community support</h5> <h5 class="pb-3">Community support</h5>

View File

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

View File

@ -46,6 +46,11 @@
<!-- Load Data --> <!-- Load Data -->
<script src="{{ get_url(path='js/shop_data.js', cachebust=true) }}"></script> <script src="{{ get_url(path='js/shop_data.js', cachebust=true) }}"></script>
<script>
window.DOMAIN = "{{get_env(name="DOMAINNAME", default="m-labs.hk")}}";
window.API_RFQ = `https://hooks.${window.DOMAIN}/rfq`;
</script>
<!-- Load our React component. --> <!-- Load our React component. -->
<!-- <script type="text/babel" src="{{ get_url(path='js/shop.jsx', cachebust=true) }}"></script> --> <!-- <script type="text/babel" src="{{ get_url(path='js/shop.jsx', cachebust=true) }}"></script> -->
<script src="{{ get_url(path='js/shop.bundle.js', cachebust=true) }}"></script> <script src="{{ get_url(path='js/shop.bundle.js', cachebust=true) }}"></script>

View File

@ -0,0 +1 @@
<a href="mailto:{{address}}@{{get_env(name="DOMAINNAME", default="m-labs.hk")}}">{{address}}@{{get_env(name="DOMAINNAME", default="m-labs.hk")}}</a>