Compare commits

..

42 Commits

Author SHA1 Message Date
e7cb6619e3 Update variants -> drtio role and add a tip
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-28 14:41:10 +08:00
b815c4dd4e Move first and last cards' options popup so that it will be accessible
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-28 12:16:16 +08:00
38541f8928 Combine warning and options iconed buttons in one line
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-27 17:12:22 +08:00
b7087711aa Replace axios with built-in Fetch API
Decreases size of the bundle from 375 to 347 KB

Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-21 16:48:21 +08:00
f11ebc1152 Update options
* Separate Urukul 4412
* Add options for LVDS TTL
* Remove usb flash option
* Add cable lengths for fiber and copper options
* Add IP option for Stabilizer and Pounder
* Added note that most options can be changed after shipment

Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-21 15:30:45 +08:00
a4ff9f7057 Rebase and fix warning in development mode
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
281f0a26f0 Add termination choices to stabilizer
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
159d4b631a Add phaser baseband/upconverter variants
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
e3a50f1b9a Add options to zotino, thermostat, sampler and pounder
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
72c1b1747c Add edge counter for the options to TTL
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
291332eaf4 Add HD68 cable options and fix first small cards options
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
2480b9ad54 Leave opt-out option only to the first kasli
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
cd780b83e4 Change ttl groups to banks and remove green leaf
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
bd15b8d19d Slightly promote new functionality and consider mono_eem modes on sampler/urukul for suservo
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
acdaeda699 Fix cards state being not updated on touchables
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
1a0b613044 Fix not-disappearing warnings and clocker clock slots
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
58bc01cf06 Small duplication removal
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
5559a04403 Remove redundant notifications about connectors and AD9910
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
ce31357802 Integrate clock/slots configuration into the calculator
Also adjust sizes

Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
79e4253e0f Fixed initial display of empty options in the options summary
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
840b0223a7 Still fixing the bug
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
dfbf9cbfe7 Fix scroll issue and found another bug
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
c55687e05a Add groups of options and some fixes for long lists of options
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
f67aa7fc51 Add external data for use per every card. Apply it for TTL and Suservo
Signed-off-by: esavkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
b245655c41 Fix tooltip showed out when options overlay was closed
Signed-off-by: esavkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
a3075a5691 Use tips on more cards
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
8db77cf8c7 Add tooltips with hints for the user
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
788653abaa Add icons to options titles
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
0df69499b8 Remove MAC from options
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
8bdfedc4b8 Reposition summary popup relative to icon respective to window width, hide on scroll
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
0fe665c0ba Hide popups on clicking outside of them
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
894c1d2e14 Update bundle file
Add custom options to more cards

Minor design adjustments: add margin-left to icon and optimize the icon

Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
23c7508358 Add popover for cart summary with options data
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
288a0cc74c Make overlay appear on button click
And fix options absense for non-carrier cards

Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
0be655f83a Make the design more compact
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
d72a295aa5 Adjust styles
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
4d5dd505ae Fix add of useless options to JSON
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
ac87fede4b Add switchline compact design
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
ff8a6d54bc Add switch and line
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
f93cf142f0 Fix JSON shop load
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
1a7d44c121 Make it update state
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
d543e54326 Add basic demo example
Kinda works, but buggy

Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-11-20 15:40:44 +08:00
126 changed files with 5178 additions and 6922 deletions

View File

@ -12,29 +12,14 @@ Clone the project:
Install Zola. Install Zola.
Start with targeting `m-labs.hk` domain: Start:
``` ```
zola serve zola serve
``` ```
Environmental variable `DOMAINNAME` can be specified to [substitute links on the website](#domain-handling). To update the .bundle.js and .jsx file:
To build the .bundle.js from .jsx files:
``` ```
nix-shell -p nodejs nix-shell -p nodejs --run "npm run build"
npm install
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

@ -1,8 +1,9 @@
base_url = "https://m-labs.hk" base_url = "/"
title = "M-Labs" title = "M-Labs"
description = "Open tools for open physics." description = "Open tools for open physics."
compile_sass = true compile_sass = true
insert_anchor_links = true
build_search_index = false build_search_index = false
[markdown] [markdown]

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: {{ email(address="sb") }} Contact: sb@m-****.hk

View File

@ -1,116 +0,0 @@
+++
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 650.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.
##### I have overpaid or double-paid an invoice. What should I do?
Contact sb@m-labs.hk and we will return the excess funds within a few business days. You shall pay all banking and credit card charges incurred.
##### We are a distributor. Can we have exclusive rights to your products?
Exclusivity is subject to contractual minimum order volumes determined at our discretion, but generally exceeding USD 300,000.00 per year for most countries.
##### 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.com/video/watch/shenzhen-the-silicon-valley-of-hardware-full-documentary-future-cities-wired" rel="noopener noreferrer" target="_blank">a bustling tech scene</a>, and where many of the world's electronic gadgets are designed and manufactured. Our main office is located in the center of Hong Kong, a cosmopolitan city with world-class infrastructure, many cultural and social events, and <a href="https://www.discoverhongkong.com/eng/see-do/great-outdoors/hikes/index.jsp" rel="noopener noreferrer" target="_blank">beautiful natural scenery</a>. It has a separate system from mainland China, where, for example, communications are unrestricted, taxes are low, and customs tariffs virtually inexistent. Hong Kong is located next to Shenzhen, a city with <a href="https://www.wired.co.uk/video/shenzhen-full-documentary" rel="noopener noreferrer" target="_blank">a bustling tech scene</a>, and where many of the world's electronic gadgets are designed and manufactured.
We also have a location in Manila, Philippines. 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 {{ email(address="jobs") }} or [jobs@m-labs.ph](mailto:jobs@m-labs.ph)! ##### Contact us at jobs@m-\*\*\*\*.hk or jobs@m-\*\*\*\*.ph!
{% end %} {% end %}

View File

@ -37,7 +37,7 @@ The <a href="https://github.com/quartiq/stabilizer" target="_blank" rel="noopene
<div class="row d-flex align-items-top mt-5 mb-5"> {% layout_html(css='row d-flex align-items-top mt-5 mb-5') %}
<div class="col-12 col-md-3"> <div class="col-12 col-md-3">
<ul> <ul>
@ -62,11 +62,11 @@ 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 {{ 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. To purchase this controller, email sales@m-***s.hk. 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>
</div> {% end %}

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 ({{ email(address="sales") }}) if you also want to move ARTIQ forward! We acknowledge support from our partners below. Please get in touch (sales@m-\*\*\*.hk) if you also want to move ARTIQ forward!
{% end %} {% end %}

View File

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

View File

@ -13,25 +13,25 @@ template = "page.html"
{% layout_card(title="ARTIQ manual", sameheight=100) %} {% layout_card(title="ARTIQ manual", sameheight=100) %}
<small>Stable version</small> <small>Stable version</small>
<a href="https://m-labs.hk/artiq/manual.pdf" target="_blank" itemprop="url">PDF</a> | <a href="https://m-labs.hk/artiq/manual/" target="_blank" rel="noopener noreferrer" itemprop="url">HTML</a> <a href="https://m-labs.hk/artiq/manual.pdf" target="_blank">PDF</a> | <a href="https://m-labs.hk/artiq/manual/" target="_blank" rel="noopener noreferrer">HTML</a>
{% end %} {% end %}
{% layout_card(title="SiPyCo manual", sameheight=100) %} {% layout_card(title="SiPyCo manual", sameheight=100) %}
<small>Starting with ARTIQ-5, this library replaces <tt>artiq.protocols</tt>.</small> <small>Starting with ARTIQ-5, this library replaces <tt>artiq.protocols</tt>.</small>
<a href="https://m-labs.hk/artiq/sipyco-manual.pdf" target="_blank" itemprop="url">PDF</a> | <a href="https://m-labs.hk/artiq/sipyco-manual/" target="_blank" rel="noopener noreferrer" itemprop="url">HTML</a> <a href="https://m-labs.hk/artiq/sipyco-manual.pdf" target="_blank">PDF</a> | <a href="https://m-labs.hk/artiq/sipyco-manual/" target="_blank" rel="noopener noreferrer">HTML</a>
{% end %} {% end %}
{% layout_card(title="ARTIQ manual", sameheight=100) %} {% layout_card(title="ARTIQ manual", sameheight=100) %}
<small>Beta (development) version</small> <small>Beta (development) version</small>
<a href="https://m-labs.hk/artiq/manual-beta.pdf" target="_blank" itemprop="url">PDF</a> | <a href="https://m-labs.hk/artiq/manual-beta/" target="_blank" rel="noopener noreferrer" itemprop="url">HTML</a> <a href="https://m-labs.hk/artiq/manual-beta.pdf" target="_blank">PDF</a> | <a href="https://m-labs.hk/artiq/manual-beta/" target="_blank" rel="noopener noreferrer">HTML</a>
{% end %} {% end %}
{% layout_card(title="ARTIQ manual", sameheight=100) %} {% layout_card(title="ARTIQ manual", sameheight=100) %}
<small>Legacy version</small> <small>Legacy version</small>
<a href="https://m-labs.hk/artiq/manual-legacy.pdf" target="_blank" itemprop="url">PDF</a> | <a href="https://m-labs.hk/artiq/manual-legacy/" target="_blank" rel="noopener noreferrer" itemprop="url">HTML</a> <a href="https://m-labs.hk/artiq/manual-legacy.pdf" target="_blank">PDF</a> | <a href="https://m-labs.hk/artiq/manual-legacy/" target="_blank" rel="noopener noreferrer">HTML</a>
{% end %} {% end %}
@ -39,14 +39,14 @@ template = "page.html"
{% layout_card(title="Slideshow", sameheight=100) %} {% layout_card(title="Slideshow", sameheight=100) %}
<small>The ARTIQ experiment control system</small> <small>The ARTIQ experiment control system</small>
<a href="/docs/artiq/artiq_overview.pdf" target="_blank" rel="noopener noreferrer" itemprop="url">View slides</a> <a href="/docs/artiq/artiq_overview.pdf" target="_blank" rel="noopener noreferrer">View slides</a>
{% end %} {% end %}
{% layout_card(title="Slideshow", sameheight=100) %} {% layout_card(title="Slideshow", sameheight=100) %}
<small>Timing control in ARTIQ</small> <small>Timing control in ARTIQ</small>
<a href="/docs/artiq/slides_timing.pdf" target="_blank" rel="noopener noreferrer" itemprop="url">View slides</a> <a href="/docs/artiq/slides_timing.pdf" target="_blank" rel="noopener noreferrer">View slides</a>
{% end %} {% end %}
</div> </div>
@ -64,40 +64,40 @@ template = "page.html"
<div class="row mt-5"> <div class="row mt-5">
{% layout_card(src="images/mattermost@2x.png", css="col-12 col-md-4 text-center") %} {% layout_card(src="images/mattermost@2x.png", css="col-12 col-md-4 text-center") %}
<a href="http://chat.m-labs.hk/" target="_blank" rel="noopener noreferrer" itemprop="url">Mattermost live chat</a> <a href="http://chat.m-labs.hk/" target="_blank" rel="noopener noreferrer">Mattermost live chat</a>
<small>(bridged to IRC)</small> <small>(bridged to IRC)</small>
{% end %} {% end %}
{% layout_card(src="images/forum@2x.png", css="col-12 col-md-4 text-center") %} {% layout_card(src="images/forum@2x.png", css="col-12 col-md-4 text-center") %}
<a href="https://forum.m-labs.hk/" target="_blank" rel="noopener noreferrer" itemprop="url">Forum</a> <a href="https://forum.m-labs.hk/" target="_blank" rel="noopener noreferrer">Forum</a>
<small>&nbsp;</small> <small>&nbsp;</small>
{% end %} {% end %}
{% layout_card(src="images/irc@2x.png", css="col-12 col-md-4 text-center") %} {% layout_card(src="images/irc@2x.png", css="col-12 col-md-4 text-center") %}
<a href="https://webchat.oftc.net/" target="_blank" rel="noopener noreferrer" itemprop="url">IRC: #m-labs</a> <a href="https://webchat.oftc.net/" target="_blank" rel="noopener noreferrer">IRC: #m-labs</a>
<small>on OFTC</small> <small>on OFTC</small>
{% end %} {% end %}
{% layout_card(src="images/press@2x.png", css="col-12 col-md-4 text-center") %} {% layout_card(src="images/press@2x.png", css="col-12 col-md-4 text-center") %}
<a href="https://www.nist.gov/news-events/news/2015/01/open-source-software-quantum-information" target="_blank" rel="noopener noreferrer" itemprop="url">NIST ARTIQ press release</a> <a href="https://www.nist.gov/news-events/news/2015/01/open-source-software-quantum-information" target="_blank" rel="noopener noreferrer">NIST ARTIQ press release</a>
<small>&nbsp;</small> <small>&nbsp;</small>
{% end %} {% end %}
{% layout_card(src="images/git@2x.png", css="col-12 col-md-4 text-center") %} {% layout_card(src="images/git@2x.png", css="col-12 col-md-4 text-center") %}
<a href="https://github.com/m-labs/artiq" target="_blank" rel="noopener noreferrer" itemprop="url">ARTIQ source code repository</a> <a href="https://github.com/m-labs/artiq" target="_blank" rel="noopener noreferrer">ARTIQ source code repository</a>
<small>on GitHub</small> <small>on GitHub</small>
{% end %} {% end %}
{% layout_card(src="images/git@2x.png", css="col-12 col-md-4 text-center") %} {% layout_card(src="images/git@2x.png", css="col-12 col-md-4 text-center") %}
<a href="https://git.m-labs.hk" target="_blank" rel="noopener noreferrer" itemprop="url">Gitea</a> <a href="https://git.m-labs.hk" target="_blank" rel="noopener noreferrer">Gitea</a>
<small>&nbsp;</small> <small>&nbsp;</small>
{% end %} {% end %}
@ -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 {{ email(address="sales") }} contact sales@m-\*\*\*s.hk
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)
@ -129,117 +129,108 @@ We welcome inquiries from research groups of all sizes.<br>[See what has been fu
{% layout_card(title="Entangler core", sameheight=120) %} {% layout_card(title="Entangler core", sameheight=120) %}
<small>A FPGA core written in Migen with ARTIQ interface, for controlling remote quantum entanglement of trapped ions.</small> <small>A FPGA core written in Migen with ARTIQ interface, for controlling remote quantum entanglement of trapped ions.</small>
<a href="https://github.com/OxfordIonTrapGroup/entangler-core" target="_blank" rel="noopener noreferrer" itemprop="url">Repository</a> | <a href="https://arxiv.org/abs/1911.10841" target="_blank" rel="noopener noreferrer" itemprop="url">Paper</a> <a href="https://github.com/OxfordIonTrapGroup/entangler-core" target="_blank" rel="noopener noreferrer">Repository</a> | <a href="https://arxiv.org/abs/1911.10841" target="_blank" rel="noopener noreferrer">Paper</a>
{% end %} {% end %}
{% layout_card(title="ndscan", sameheight=120) %} {% layout_card(title="ndscan", sameheight=120) %}
<small>N-dimensional scans for ARTIQ</small> <small>N-dimensional scans for ARTIQ</small>
<a href="https://github.com/OxfordIonTrapGroup/ndscan" target="_blank" rel="noopener noreferrer" itemprop="url">Repository</a> <a href="https://github.com/OxfordIonTrapGroup/ndscan" target="_blank" rel="noopener noreferrer">Repository</a>
{% end %} {% end %}
{% layout_card(title="DAX - Duke ARTIQ extensions", sameheight=120) %}
<small>A library to provide tools for system organization/abstraction and to improve usability by automating common functionality.</small>
<a href="https://gitlab.com/duke-artiq" target="_blank" rel="noopener noreferrer" itemprop="url">Repositories</a>
{% end %}
{% layout_card(title="flake8-artiq", sameheight=120) %}
<small>A Flake8 plugin for checking ARTIQ code</small>
<a href="https://gitlab.com/duke-artiq/flake8-artiq" target="_blank" rel="noopener noreferrer" itemprop="url">Repository</a>
{% end %}
{% layout_card(title="Oxford routines", sameheight=120) %} {% layout_card(title="Oxford routines", sameheight=120) %}
<small>Oxford Ion-Trap Group routines</small> <small>Oxford Ion-Trap Group routines</small>
<a href="https://github.com/OxfordIonTrapGroup/oitg" target="_blank" rel="noopener noreferrer" itemprop="url">Repository</a> <a href="https://github.com/OxfordIonTrapGroup/oitg" target="_blank" rel="noopener noreferrer">Repository</a>
{% end %}
{% layout_card(title="ATOMIQ", sameheight=120) %}
<small>An abstraction layer to move hardware specific information into a configuration layer and enables working with generic software objects representing the actual hardware in the lab.</small>
<a href="https://thequantumlaend.de/2024/03/07/atomiq-our-convenience-layer-for-artiq-now-available/" target="_blank" rel="noopener noreferrer" itemprop="url">Announcement</a> | <a href="https://gitlab.com/atomiq-project/atomiq" target="_blank" rel="noopener noreferrer" itemprop="url">Repository</a>
{% end %} {% end %}
{% layout_card(title="UCLA routines", sameheight=120) %} {% layout_card(title="UCLA routines", sameheight=120) %}
<small>ARTIQ experiments in use at UCLA AMO</small> <small>ARTIQ experiments in use at UCLA AMO</small>
<a href="https://github.com/EGGS-Experiment/LAX_exp" target="_blank" rel="noopener noreferrer" itemprop="url">Repository</a> <a href="https://github.com/EGGS-Experiment/LAX_exp" target="_blank" rel="noopener noreferrer">Repository</a>
{% end %} {% end %}
{% layout_card(title="nvOS", sameheight=120) %} {% layout_card(title="nvOS", sameheight=120) %}
<small>A quantum operating system built around ARTIQ and NV centers in diamond.</small> <small>A quantum operating system built around ARTIQ and NV centers in diamond.</small>
<a href="https://github.com/vontell/nvOS" target="_blank" rel="noopener noreferrer" itemprop="url">Repository</a> <a href="https://github.com/vontell/nvOS" target="_blank" rel="noopener noreferrer">Repository</a>
{% end %} {% end %}
{% layout_card(title="Haeffner Lab routines", sameheight=120) %} {% layout_card(title="Haeffner Lab routines", sameheight=120) %}
<small>Haeffner Lab (Berkeley) routines</small> <small>Haeffner Lab (Berkeley) routines</small>
<a href="https://github.com/HaeffnerLab/artiq-work-lattice" target="_blank" rel="noopener noreferrer" itemprop="url">Repository</a> <a href="https://github.com/HaeffnerLab/artiq-work-lattice" target="_blank" rel="noopener noreferrer">Repository</a>
{% end %} {% end %}
{% layout_card(title="Birmingham examples", sameheight=120) %} {% layout_card(title="Birmingham examples", sameheight=120) %}
<small>A repository of simple examples of ARTIQ code</small> <small>A repository of simple examples of ARTIQ code</small>
<a href="https://github.com/cnourshargh/Bham-ARTIQ-examples" target="_blank" rel="noopener noreferrer" itemprop="url">Repository</a> <a href="https://github.com/cnourshargh/Bham-ARTIQ-examples" target="_blank" rel="noopener noreferrer">Repository</a>
{% 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" itemprop="url">Repository</a> {% 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) %}
<small>A library to provide tools for system organization/abstraction and to improve usability by automating common functionality.</small>
<a href="https://gitlab.com/duke-artiq" target="_blank" rel="noopener noreferrer">Repositories</a>
{% end %}
{% layout_card(title="flake8-artiq", sameheight=120) %}
<small>A Flake8 plugin for checking ARTIQ code</small>
<a href="https://gitlab.com/duke-artiq/flake8-artiq" target="_blank" rel="noopener noreferrer">Repository</a>
{% end %} {% end %}
{% layout_card(title="GenericSCPIDriver", sameheight=120) %} {% layout_card(title="GenericSCPIDriver", sameheight=120) %}
<small>A generic Python driver for SCPI devices driven over serial connections. Compatible with ARTIQ.</small> <small>A generic Python driver for SCPI devices driven over serial connections. Compatible with ARTIQ.</small>
<a href="https://github.com/charlesbaynham/GenericSCPIDriver" target="_blank" rel="noopener noreferrer" itemprop="url">Repository</a> <a href="https://github.com/charlesbaynham/GenericSCPIDriver" target="_blank" rel="noopener noreferrer">Repository</a>
{% end %} {% end %}
{% layout_card(title="artiq_influx_generic", sameheight=120) %} {% layout_card(title="artiq_influx_generic", sameheight=120) %}
<small>RPC interface to InfluxDB with support for generic data logging.</small> <small>RPC interface to InfluxDB with support for generic data logging.</small>
<a href="https://gitlab.com/charlesbaynham/artiq_influx_generic" target="_blank" rel="noopener noreferrer" itemprop="url">Repository</a> <a href="https://gitlab.com/charlesbaynham/artiq_influx_generic" target="_blank" rel="noopener noreferrer">Repository</a>
{% end %} {% end %}
{% layout_card(title="DP700", sameheight=120) %} {% layout_card(title="DP700", sameheight=120) %}
<small>ARTIQ NDSP for RIGOL DP700 Series power supplies.</small> <small>ARTIQ NDSP for RIGOL DP700 Series power supplies.</small>
<a href="https://github.com/OregonIons/DP700" target="_blank" rel="noopener noreferrer" itemprop="url">Repository</a> <a href="https://github.com/OregonIons/DP700" target="_blank" rel="noopener noreferrer">Repository</a>
{% end %} {% end %}
{% layout_card(title="AGUC8", sameheight=120) %} {% layout_card(title="AGUC8", sameheight=120) %}
<small>ARTIQ NDSP for Newport AG-UC8 piezo motor controller.</small> <small>ARTIQ NDSP for Newport AG-UC8 piezo motor controller.</small>
<a href="https://github.com/OregonIons/AGUC8" target="_blank" rel="noopener noreferrer" itemprop="url">Repository</a> <a href="https://github.com/OregonIons/AGUC8" target="_blank" rel="noopener noreferrer">Repository</a>
{% end %} {% end %}
{% layout_card(title="TSICam", sameheight=120) %} {% layout_card(title="TSICam", sameheight=120) %}
<small>ARTIQ NDSP for Thorlabs Scientific Imaging cameras.</small> <small>ARTIQ NDSP for Thorlabs Scientific Imaging cameras.</small>
<a href="https://github.com/OregonIons/TSICam" target="_blank" rel="noopener noreferrer" itemprop="url">Repository</a> <a href="https://github.com/OregonIons/TSICam" target="_blank" rel="noopener noreferrer">Repository</a>
{% end %} {% end %}
{% layout_card(title="U Toronto examples", sameheight=120) %} {% layout_card(title="U Toronto examples", sameheight=120) %}
<small>A repository of examples of ARTIQ code and instructions</small> <small>A repository of examples of ARTIQ code and instructions</small>
<a href="https://github.com/vuthalab/artiq" target="_blank" rel="noopener noreferrer" itemprop="url">Repository</a> <a href="https://github.com/vuthalab/artiq" target="_blank" rel="noopener noreferrer">Repository</a>
{% 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" itemprop="url">Repository</a>
{% end %} {% end %}
</div> </div>
{% layout_div(css="col-12 text-center") %} {% layout_div(css="col-12 text-center") %}
Want your project listed here? Write to {{ email(address="sb") }}. Want your project listed here? Write to sb@m-l\*\*\*.hk.
{% 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="https://www.ise.pw.edu.pl/" target="_blank" rel="noopener noreferrer">Institute for Electronics Systems</a> at the Warsaw University of Technology. The Sinara hardware is in active development, and the latest information is available <a href="https://github.com/sinara-hw" target="_blank" rel="noopener noreferrer">on the wiki of each project's page</a>. Most of the hardware engineering is done at the <a href="http://www.ise.pw.edu.pl/" target="_blank" rel="noopener noreferrer">Institute for Electronics Systems</a> at the Warsaw University of Technology.
Kasli and EEMs can be ordered now. We can deliver a rack-mountable crate that contains all the cards, is fully tested, and is ready to be connected to your experiment and computer network. Use our [web-based configuration and ordering tool](../place-order), or contact {{ email(address="sales") }} with your requirements, and we will establish a quote. Kasli and EEMs can be ordered now. We can deliver a rack-mountable crate that contains all the cards, is fully tested, and is ready to be connected to your experiment and computer network. Contact sales@m-\*\*\*s.hk with your requirements and we will establish a quote.
{% end %} {% end %}
@ -28,10 +28,6 @@ One of the main devices in the Sinara family is the 1124 Carrier (codenamed Kasl
<a href="https://github.com/sinara-hw/Kasli/wiki" target="_blank" rel="noopener noreferrer">More information</a> <a href="https://github.com/sinara-hw/Kasli/wiki" target="_blank" rel="noopener noreferrer">More information</a>
<p style="padding-top: 8px">
<span class='doc-icon'></span> <a href="/docs/sinara-datasheets/1124.pdf" target="_blank" rel="noopener noreferrer">1124 Carrier Kasli 2.0 datasheet</a>
</p>
{% end %} {% end %}
{% layout_text_img(src="images/kasli-soc@2x.png", popup="images/origin/kasli-soc.jpg", alt="", textleft=true, shadow=false) %} {% layout_text_img(src="images/kasli-soc@2x.png", popup="images/origin/kasli-soc.jpg", alt="", textleft=true, shadow=false) %}
@ -44,10 +40,6 @@ See our paper <a href="https://arxiv.org/abs/2111.15290" target="_blank" rel="no
<a href="https://github.com/sinara-hw/Kasli-SoC/wiki" target="_blank" rel="noopener noreferrer">More information</a> <a href="https://github.com/sinara-hw/Kasli-SoC/wiki" target="_blank" rel="noopener noreferrer">More information</a>
<p style="padding-top: 8px">
<span class='doc-icon'></span> <a href="/docs/sinara-datasheets/1125.pdf" target="_blank" rel="noopener noreferrer">1125 Carrier Kasli SoC datasheet</a>
</p>
{% end %} {% end %}
{% layout_text_img(src="images/isolated-ttl@2x.png", popup="images/origin/dio.jpg", alt="", shadow=false) %} {% layout_text_img(src="images/isolated-ttl@2x.png", popup="images/origin/dio.jpg", alt="", shadow=false) %}
@ -175,11 +167,6 @@ Comparing Mirny to Urukul:
<a href="https://github.com/sinara-hw/mirny/wiki" target="_blank" rel="noopener noreferrer">More information</a> <a href="https://github.com/sinara-hw/mirny/wiki" target="_blank" rel="noopener noreferrer">More information</a>
<p style="padding-top: 8px">
<span class='doc-icon'></span> <a href="/docs/sinara-datasheets/4456.pdf" target="_blank" rel="noopener noreferrer">4456 Synthesizer Mirny datasheet</a>
</p>
{% end %} {% end %}
@ -246,11 +233,6 @@ In SU-Servo mode, the 5108 Sampler can be used in combination with the 4410 DDS
Note that update rate specification on this page is for the hardware only; ARTIQ kernel and RTIO overhead make the effective sample rate lower. Typically, only with gateware (e.g. SU-Servo) can the maximum bandwidth be achieved. SU-Servo is part of the regular ARTIQ firmware; development of other gateware can be purchased separately. Note that update rate specification on this page is for the hardware only; ARTIQ kernel and RTIO overhead make the effective sample rate lower. Typically, only with gateware (e.g. SU-Servo) can the maximum bandwidth be achieved. SU-Servo is part of the regular ARTIQ firmware; development of other gateware can be purchased separately.
<p style="padding-top: 8px">
<span class='doc-icon'></span> <a href="/docs/sinara-datasheets/5108.pdf" target="_blank" rel="noopener noreferrer">5108 ADC Sampler datasheet</a>
</p>
{% end %} {% end %}
@ -277,11 +259,6 @@ The Sinara 7210 is a low-noise clock distribution module that can be used to dis
<a href="https://github.com/sinara-hw/Clocker/wiki" target="_blank" rel="noopener noreferrer">More information</a> <a href="https://github.com/sinara-hw/Clocker/wiki" target="_blank" rel="noopener noreferrer">More information</a>
<p style="padding-top: 8px">
<span class='doc-icon'></span> <a href="/docs/sinara-datasheets/7210.pdf" target="_blank" rel="noopener noreferrer">7210 Clocker datasheet</a>
</p>
{% end %} {% end %}
@ -293,7 +270,6 @@ 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.
@ -317,11 +293,11 @@ The 4624 AWG "Phaser" is a quad channel 1.25 GS/s RF generator card with dual IQ
{% layout_centered_content(min_width=true, css="row d-flex align-items-center mt-5") %} {% layout_centered_content(min_width=true, css="row d-flex align-items-center mt-5") %}
##### Ordering from M-Labs is easy and quick ##### Kasli and EEMs can be ordered now
We can deliver a rack-mountable crate that contains all the cards, is fully tested, and is ready to be connected to your experiment and computer network. The lead time can be as short as a few working days and we will provide assistance to help you set up your new equipment with ARTIQ via the online helpdesk. Using our AFWS tool, you can keep the firmware of your M-Labs devices up-to-date easily, and benefit from the new features we continuously develop into ARTIQ. We can deliver a rack-mountable crate that contains all the cards, is fully tested, and is ready to be connected to your experiment and computer network.
Use our [web-based configuration and ordering tool](../place-order), or contact {{ email(address="sales") }} with your requirements, and we will establish a quote. Contact sales@m-\*\*\*s.hk 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="https://web.archive.org/web/20130825023759/http://www.milkdrop.co.uk/guide.htm" target="_blank" rel="noopener noreferrer">MilkDrop preset format</a>. The Milkymist One device comes with a built-in editor. Creating new patches can be done with a simple programming language based on the <a href="http://www.milkdrop.co.uk/guide.htm" target="_blank" rel="noopener noreferrer">MilkDrop preset format</a>. The Milkymist One device comes with a built-in editor.
Under the hood, the Milkymist One is like a mini-computer running our Flickernoise video synthesis software. 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="https://web.archive.org/web/20140226181735/http://www.mbnet.fi/artikkeli/lehti/avointa_vj_rautaa_3_2012">Milkymist One -visualisaattori: Avointa vj-rautaa</a> [FI] - MikroBitti (03/2012): <a href="http://www.mbnet.fi/artikkeli/lehti/avointa_vj_rautaa_3_2012">Milkymist One -visualisaattori: Avointa vj-rautaa</a> [FI]
- Make Magazine (30/09/2011): <a href="http://blog.makezine.com/2011/09/30/milkymist-one-an-open-source-vj-console-goes-on-sale/">Milkymist One, an Open Source VJ Console, Goes on Sale</a> - 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="https://web.archive.org/web/20160409033154/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="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="https://www.geisswerks.com/milkdrop/">MilkDrop</a> 1.x, the popular audio visualization plug-in for Winamp. Flickernoise is the video synthesis application developed for the Milkymist One and the Milkymist SoC. It renders hardware-accelerated visual effects comparable (and, to some extent, compatible) with those of <a href="http://www.nullsoft.com/free/milkdrop">MilkDrop</a> 1.x, the popular audio visualization plug-in for Winamp.
Flickernoise allows the creation of visual patches and their connection with all the interfaces that the Milkymist One provides thanks to a built-in graphical user interface. Let the visuals react to sound and MIDI events, connect a camera and create live phantasmagoric images of yourself... Flickernoise 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="https://web.archive.org/web/20151025081338/http://www.yaffs.org/">YAFFS</a> flash filesystem (modified version <a href="http://www.github.com/m-labs/rtems-yaffs2">here</a>). - The <a href="http://www.yaffs.org">YAFFS</a> flash filesystem (modified version <a href="http://www.github.com/m-labs/rtems-yaffs2">here</a>).
- The <a href="http://www.libpng.org">libpng</a>, <a href="http://www.ijg.org">libjpeg</a>, <a href="http://www.ijg.org">openjpeg</a> and <a href="http://jbig2dec.sourceforge.net/">jbig2dec</a> image decompression libraries. - The <a href="http://www.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="https://www.mupdf.com">MuPDF</a> library for the online help system. - The <a href="http://www.mupdf.com">MuPDF</a> library for the online help system.
- <a href="http://www.github.com/m-labs/mtk">MTK</a>, a modified version of the <a href="http://www.genode-labs.com/products/fpga-graphics">Genode FX</a> embedded GUI toolkit, which provides all the elements for common user interaction (windows, buttons, etc.). - <a href="http://www.github.com/m-labs/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="https://web.archive.org/web/20210416131215/http://blog.lambdaconcept.com/doku.php?id=migen:tutorial" target="_blank" rel="noopener noreferrer">Migen Step by Step Tutorial</a> by LambdaConcept - <a href="http://blog.lambdaconcept.com/doku.php?id=migen:tutorial" target="_blank" rel="noopener noreferrer">Migen Step by Step Tutorial</a> by LambdaConcept
{% end %} {% 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="https://web.archive.org/web/20210518042504/http://blog.lambdaconcept.com/doku.php?id=nmigen:tutorial" target="_blank" rel="noopener noreferrer">tutorial by LambdaConcept</a>, and another <a href="https://github.com/RobertBaruch/nmigen-tutorial" target="_blank" rel="noopener noreferrer">tutorial by Robert Baruch</a>, as well as his video series on building a 6800 CPU on an FPGA with nMigen: <a href="https://www.youtube.com/watch?v=85ZCTuekjGA" target="_blank" rel="noopener noreferrer">part 1</a> <a href="https://www.youtube.com/watch?v=AQOXoKQhG3I" target="_blank" rel="noopener noreferrer">part 2</a> <a href="https://www.youtube.com/watch?v=aLQqOxnVMOQ" target="_blank" rel="noopener noreferrer">part 3</a> <a href="https://www.youtube.com/watch?v=xqMtyCu4lME" target="_blank" rel="noopener noreferrer">part 4</a>. Documentation for nMigen and its external components is rather limited at the moment. However, you can follow the <a href="http://blog.lambdaconcept.com/doku.php?id=nmigen:tutorial" target="_blank" rel="noopener noreferrer">tutorial by LambdaConcept</a>, and another <a href="https://github.com/RobertBaruch/nmigen-tutorial" target="_blank" rel="noopener noreferrer">tutorial by Robert Baruch</a>, as well as his video series on building a 6800 CPU on an FPGA with nMigen: <a href="https://www.youtube.com/watch?v=85ZCTuekjGA" target="_blank" rel="noopener noreferrer">part 1</a> <a href="https://www.youtube.com/watch?v=AQOXoKQhG3I" target="_blank" rel="noopener noreferrer">part 2</a> <a href="https://www.youtube.com/watch?v=aLQqOxnVMOQ" target="_blank" rel="noopener noreferrer">part 3</a> <a href="https://www.youtube.com/watch?v=xqMtyCu4lME" target="_blank" rel="noopener noreferrer">part 4</a>.
{% end %} {% 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 {{ email(address="sb") }} 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 sb at m-labs.hk 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 {{ email(address="sales") }}. **Commercial support for smoltcp is available.** Email sales@m-l\*\*\*s.hk.
{% end %} {% end %}

2944
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,57 +1,40 @@
{ {
"name": "m-labs-zola", "name": "m-labs-zola",
"sideEffects": false,
"version": "1.0.0", "version": "1.0.0",
"description": "These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. For deployment, see the nix-scripts repository. Commits to https://git.m-labs.hk/M-Labs/web2019.git are automatically deployed to m-labs.hk through Hydra.", "description": "These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. For deployment, see the nix-scripts repository. Commits to https://git.m-labs.hk/M-Labs/web2019.git are automatically deployed to m-labs.hk through Hydra.",
"scripts": { "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",
"optimize-bootstrap": "zola build -o tmp_out && npx purgecss --css static/css/bootstrap-5.3.0.min.css --content 'tmp_out/**/*.html' -o static/css/bootstrap-5.3.0.opt.css --safelist modal-backdrop && rm -rf tmp_out"
}, },
"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.24.8", "@babel/cli": "^7.23.0",
"@babel/core": "^7.25.2", "@babel/core": "^7.23.2",
"@babel/preset-env": "^7.25.3", "@babel/preset-env": "^7.23.2",
"@babel/preset-react": "^7.24.7", "@babel/preset-react": "^7.22.15",
"@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",
"webpack": "^5.93.0", "bootstrap": "^5.3.0",
"jquery": "^3.7.0",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-bootstrap": "^2.9.1",
"react-beautiful-dnd": "^13.1.1",
"react-dom": "^18.2.0",
"uuid": "^9.0.1",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4", "webpack-cli": "^5.1.4",
"webpack-preprocessor-loader": "^1.3.0", "json-logic-js": "^2.0.2",
"purgecss": "^7.0.2" "@uidotdev/usehooks": "^2.4.1"
}, },
"babel": { "babel": {
"presets": [ "presets": [
"@babel/preset-react", "@babel/preset-env",
[ "@babel/preset-react"
"@babel/preset-env",
{
"targets": {
"browsers": [
">0.25%",
"not dead",
"not op_mini all"
]
}
}
]
] ]
} }
} }

View File

@ -90,53 +90,29 @@ a {
} }
} }
.navbar { .navbar-light .navbar-nav .nav-link,
.navbar-light .navbar-nav .nav-link, .dropdown-item {
.dropdown-item { outline: none;
outline: none; color: $color-primary;
text-decoration: none;
&:visited {
color: $color-primary; color: $color-primary;
text-decoration: none;
&:visited {
color: $color-primary;
}
&:hover {
color: $color-secondary;
}
} }
.navbar-light .navbar-nav .active>.nav-link, &:hover {
.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;
} }
.dropdown-menu {
border: none;
}
.dropdown-item {
&:hover,
&:active {
background-color: transparent;
}
}
.dropdown-item.active {
color: $color-secondary;
background-color: transparent;
}
.navbar-toggler {
outline: none;
&:focus,
&:hover {
outline: none;
}
}
} }
.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;
}
/** /**
* Custom * Custom
@ -151,23 +127,45 @@ 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 {
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;
@ -201,63 +199,6 @@ 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

@ -56,13 +56,6 @@
background-size: cover; background-size: cover;
} }
.card-featured {
background: #fff url("../images/fireworks-phone@2x.png") no-repeat top center;
background-size: contain;
}
.card-featured > div {
padding-top: 66.64%;
}
.card-artiq { .card-artiq {
background: #fff url("../images/artiq-phone@2x.png") no-repeat top center; background: #fff url("../images/artiq-phone@2x.png") no-repeat top center;
@ -114,14 +107,10 @@ h3, h2, h1 {
padding: .4rem; } padding: .4rem; }
p ~ h5, div.desc-wrapper ~ h5 { p ~ h5 {
margin-top: 3rem; margin-top: 3rem;
} }
div.desc-wrapper {
margin-bottom: 1rem;
}
img.kf25 { img.kf25 {
width: 310px; width: 310px;
} }
@ -129,10 +118,6 @@ img.kf25 {
// Small devices (landscape phones, 576px and up) // Small devices (landscape phones, 576px and up)
@media (min-width: 576px) { @media (min-width: 576px) {
.card-featured > div {
padding-top: 0;
}
.card-artiq > div { .card-artiq > div {
padding-top: 0; padding-top: 0;
} }
@ -224,11 +209,6 @@ img.kf25 {
box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important; box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important;
} }
.card-featured {
background: #fff url("../images/fireworks@2x.png") no-repeat top right;
}
.card-artiq { .card-artiq {
background: #fff url("../images/artiq@2x.png") no-repeat top right; background: #fff url("../images/artiq@2x.png") no-repeat top right;
} }

View File

@ -8,62 +8,51 @@ button {
outline: none!important; outline: none!important;
} }
.rfqFeedback {
/* -webkit-box-shadow: 0px 0px 33px -7px rgba(0,0,0,0.75);
-moz-box-shadow: 0px 0px 33px -7px rgba(0,0,0,0.75);
box-shadow: 0px 0px 33px -7px rgba(0,0,0,0.75);*/
border: 1px solid $brand-color;
.modal-body, .modal-content, .modal {
border-radius: 0;
}
.form-group {
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
text-align: center;
/*position: absolute;
width: 350px;*/
background: white;
/*left: calc(100%/2 - 350px/2);*/
/*top: calc(50% - 50px);*/
font-size: .9rem;
}
button {
background-color: inherit;
align-self: center;
border: 0;
position: absolute;
right: 10px;
top: 10px;
img {
width: 15px;
}
}
.btn.btn-primary.disabled {
background-color: gray;
}
.btn-outline-primary {
color: $btn-primary-2;
border-color: $btn-primary-2;
background-color: inherit;
&:hover {
background-color: inherit;
color: $btn-secondary-2;
border-color: $btn-secondary-2;
}
}
}
#root-shop { #root-shop {
.layout { .layout {
.rfqFeedback {
display: flex;
align-items: center;
padding: 2rem 3rem;
text-align: center;
/*position: absolute;
width: 350px;*/
background: white;
/*left: calc(100%/2 - 350px/2);*/
-webkit-box-shadow: 0px 0px 33px -7px rgba(0,0,0,0.75);
-moz-box-shadow: 0px 0px 33px -7px rgba(0,0,0,0.75);
box-shadow: 0px 0px 33px -7px rgba(0,0,0,0.75);
/*top: calc(50% - 50px);*/
border: 1px solid $brand-color;
font-size: .9rem;
button {
background-color: inherit;
align-self: center;
border: 0;
position: absolute;
right: 10px;
top: 10px;
img {
width: 15px;
}
}
.btn.btn-primary.disabled {
background-color: gray;
}
.btn-outline-primary,
.btn-outline-primary:hover {
color: $btn-primary-2;
border-color: $btn-primary-2;
background-color: inherit;
}
}
display: flex; display: flex;
> aside.aside { > aside.aside {
@ -72,32 +61,26 @@ 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;*/
}
.gradient-bottom { > aside.aside:after {
position: sticky; position: fixed;
bottom: 0; bottom: 0;
height: 100px; height: 100px;
//width: max(1/4 * 100%, 310px); width: calc(2 / 6 * 100%);
width: inherit; content: "";
content: ""; background: linear-gradient(
background: linear-gradient( to top,
to top, rgba(13, 53, 71, 1),
rgba(13, 53, 71, 1), rgba(13, 53, 71, 0)
rgba(13, 53, 71, 0) );
); pointer-events: none;
pointer-events: none;
}
} }
> section.main { > section.main {
flex: 4; flex: 4;
width: calc(3/4 * 100%); max-width: calc(4 / 6 * 100%);
overflow-y: scroll; overflow-y: scroll;
} }
} }
@ -106,7 +89,7 @@ button {
display: flex; display: flex;
color: white; color: white;
padding: 1rem 0rem 1rem 1.5rem; padding: 3rem 2rem 1rem;
.content { .content {
flex: 1; flex: 1;
@ -125,7 +108,6 @@ button {
h3 { h3 {
color: white; color: white;
font-size: 1.5rem;
} }
button { button {
@ -183,57 +165,8 @@ button {
} }
} }
.catalog-container { .backlog-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 {
@ -253,25 +186,37 @@ button {
padding-bottom: .5rem; padding-bottom: .5rem;
} }
.btn-outline-primary { .btn-outline-primary,
.btn-outline-primary:hover {
color: $btn-primary-2; color: $btn-primary-2;
border-color: $btn-primary-2; border-color: $btn-primary-2;
background-color: inherit; background-color: inherit;
&:hover {
background-color: inherit;
color: $btn-secondary-2;
border-color: $btn-secondary-2;
}
} }
.control { .control {
display: flex; display: flex;
font-size: .8rem; font-size: .8rem;
> .description { > p {
width: 80%; width: 50%;
padding-right: 30px; padding-right: 30px;
} }
.crate-mode {
text-align: right;
width: 50%;
a {
cursor: pointer;
margin-right: 1rem;
color: inherit;
text-decoration: none;
padding-bottom: 5px;
}
a.active {
font-weight: 700;
border-bottom: 3px solid $btn-primary-2;
}
}
} }
.summary { .summary {
@ -297,9 +242,6 @@ button {
.item-card-name { .item-card-name {
font-weight: 700; font-weight: 700;
&.tabbed {
padding-left: 16px;
}
} }
.price { .price {
@ -374,19 +316,13 @@ button {
button { button {
background-color: inherit; background-color: inherit;
border: 0; border: 0;
margin-left: 16px; margin-left: 20px;
img { img {
width: 20px; width: 20px;
height: auto; height: auto;
} }
} }
span {
width: 28px;
}
.span-with-margin {
margin-left: 16px;
}
} }
> .summary-form { > .summary-form {
@ -411,7 +347,7 @@ button {
outline: none; outline: none;
} }
.order-form-submit, input[type="submit"],
.btn-cla { .btn-cla {
/*background-color: $btn-primary-2;*/ /*background-color: $btn-primary-2;*/
font-weight: 700; font-weight: 700;
@ -430,39 +366,16 @@ button {
border: 1px solid #e53e3e !important; border: 1px solid #e53e3e !important;
} }
.btn-outline-primary { .btn-outline-primary,
.btn-outline-primary:hover {
color: $btn-primary-2; color: $btn-primary-2;
border-color: $btn-primary-2; border-color: $btn-primary-2;
background-color: inherit; background-color: inherit;
&:hover {
background-color: inherit;
color: $btn-secondary-2;
}
} }
} }
} }
} }
.order-bar {
width: 90%;
font-size: 0.9rem;
padding: 0;
input[type="text"] {
padding: 0;
font-size: 0.9rem;
line-height: 1.1;
}
.options-group {
margin-bottom: 1rem;
padding: 0.5rem;
}
.shop-radio-label {
font-weight: bold;
}
}
.crate { .crate {
position: relative; position: relative;
@ -642,43 +555,6 @@ button {
.crate-info { .crate-info {
padding: 1rem 0 0; padding: 1rem 0 0;
} }
.crate-bar {
width: 100%;
font-size: 0.9rem;
.crate-mode {
text-align: left;
width: 75%;
display: inline;
a {
cursor: pointer;
margin-right: 1rem;
color: inherit;
text-decoration: none;
padding-bottom: 5px;
display: inline-block;
}
a.active {
font-weight: 700;
border-bottom: 3px solid $btn-primary-2;
}
}
.delete-crate {
text-decoration: none;
cursor: pointer;
text-align: right;
display: inline-flex;
width: 25%;
color: inherit;
img {
margin-left: 1rem;
align-self: center;
}
}
}
} }
} }
@ -711,37 +587,6 @@ button {
margin-left: -10px; margin-left: -10px;
} }
#accordion_crates {
background-color: inherit;
.accordion_crates_item {
.accordion-header {
padding-bottom: 0;
}
.accordion-button {
background-color: inherit;
font-weight: bold;
&:hover {
background-color: $color-highlight;
}
}
}
#accordion_crates_add {
.accordion-header {
padding-bottom: 0;
}
.accordion-button {
font-weight: bold;
&:hover {
background-color: $color-highlight;
}
}
.accordion-button:after {
background-image: url("/images/shop/icon-add.svg");
}
}
}
} }
.k-popup-connectors, .k-popup-connectors,
@ -750,13 +595,17 @@ button {
color: white; color: white;
font-weight: 700; font-weight: 700;
font-size: .6rem; font-size: .6rem;
padding: .5rem .8rem; padding: .8rem 1rem;
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15); box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);
text-align: left; text-align: left;
p { p {
margin-bottom: 0; margin-bottom: 0;
} }
p + p {
padding-bottom: 8px;
}
} }
.k-popup-connectors { .k-popup-connectors {

View File

@ -24,8 +24,7 @@ $btn-secondary-2: #a88cfd !default;
$link-primary-dark: #c2affd !default; $link-primary-dark: #c2affd !default;
$link-secondary-dark: #cec2ea !default; $link-secondary-dark: #cec2ea !default;
$color-hover: #eae7f7 !default; $color-hover: #eae7f7 !default;
$color-highlight: #dfe9ff !default;
// Import partials. // Import partials.
@import @import

View File

@ -10,18 +10,10 @@ from flask import request
from flask_mail import Mail from flask_mail import Mail
from flask_mail import Message from flask_mail import Message
from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.middleware.proxy_fix import ProxyFix
from jinja2.utils import htmlsafe_json_dumps
load_dotenv() load_dotenv()
mail_password_file = getenv("FLASK_MAIL_PASSWORD_FILE")
if mail_password_file is not None:
with open(mail_password_file, "r") as f:
mail_password = f.read().strip()
else:
mail_password = None
app = Flask(__name__) app = Flask(__name__)
app.config.update( app.config.update(
DEBUG=getenv("FLASK_DEBUG") == "True", DEBUG=getenv("FLASK_DEBUG") == "True",
@ -30,7 +22,7 @@ app.config.update(
MAIL_USE_SSL=getenv("FLASK_MAIL_USE_SSL"), MAIL_USE_SSL=getenv("FLASK_MAIL_USE_SSL"),
MAIL_DEBUG=False, MAIL_DEBUG=False,
MAIL_USERNAME=getenv("FLASK_MAIL_USERNAME"), MAIL_USERNAME=getenv("FLASK_MAIL_USERNAME"),
MAIL_PASSWORD=mail_password, MAIL_PASSWORD=getenv("FLASK_MAIL_PASSWORD"),
MAIL_RECIPIENT=getenv("FLASK_MAIL_RECIPIENT"), MAIL_RECIPIENT=getenv("FLASK_MAIL_RECIPIENT"),
MAIL_SENDER=getenv("FLASK_MAIL_SENDER") MAIL_SENDER=getenv("FLASK_MAIL_SENDER")
) )
@ -49,7 +41,7 @@ def after(response):
@app.route("/rfq", methods=["POST"]) @app.route("/rfq", methods=["POST"])
def send_rfq(): def send_rfq():
payload = request.json payload = request.json
payload = json.loads(htmlsafe_json_dumps(payload)) payload = json.loads(json.htmlsafe_dumps(payload))
if payload is None: if payload is None:
resp = jsonify(error="invalid data") resp = jsonify(error="invalid data")

File diff suppressed because one or more lines are too long

View File

@ -11,18 +11,6 @@
.feedback-add-success { .feedback-add-success {
display: none; display: none;
} }
.feedback-add-failure {
background-color: #c75e5e;
display: block;
position: fixed;
top: 20px;
right: 20px;
padding: 1em;
z-index: 100000;
color: white;
border-radius: 10px;
box-shadow: 0 0 5px 3px;
}
#accordion_categories, #accordion_categories,
#accordion_categories .accordion-header, #accordion_categories .accordion-header,
@ -55,7 +43,7 @@
color: white; color: white;
font-weight: bold; font-weight: bold;
font-size: 1.75rem; font-size: 1.75rem;
padding: .75rem 1.5rem; padding: .75rem 2rem;
} }
#accordion_categories .accordion-body { #accordion_categories .accordion-body {
@ -73,10 +61,6 @@
text-decoration: none; text-decoration: none;
} }
.options-invalid {
box-shadow: 0 0 0 .25rem rgba(229, 62, 62, 0.25)!important;
--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important;
}
/* /*
##Device = Tablets, Ipads (portrait) ##Device = Tablets, Ipads (portrait)
##Screen = B/w 768px to 1024px ##Screen = B/w 768px to 1024px
@ -98,14 +82,6 @@
height: calc(100vh - 10px - 2.5rem); /* .navbar vertical padding + line height (.navbar-brand.font-size.rem * body.font-size * body.line-height)*/ 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;
} }
@ -123,7 +99,7 @@
} }
#root-shop .productItem .content ul { #root-shop .productItem .content ul {
font-size: .75rem; font-size: .6rem;
} }
#root-shop .panel .control { #root-shop .panel .control {
@ -132,11 +108,11 @@
} }
#root-shop .panel .control > .description, #root-shop .panel .control > .description,
#root-shop .crate-mode { #root-shop .panel .control > .crate-mode {
width: 100%; width: 100%;
} }
#root-shop .crate-mode { #root-shop .panel .control > .crate-mode {
text-align: left; text-align: left;
} }
@ -145,11 +121,10 @@
} }
#root-shop .panel .summary>.summary-price table { #root-shop .panel .summary>.summary-price table {
font-size: 0.8rem; font-size: 1rem;
} }
#root-shop .panel .summary>.summary-form form, #root-shop .panel .summary>.summary-form form {
#root-shop .panel .summary>.summary-form .order-bar {
width: 100%; width: 100%;
} }
@ -175,7 +150,7 @@
} }
#root-shop table tr { #root-shop table tr {
padding: .2em 0; padding: .8em 0;
display: flex !important; display: flex !important;
justify-content: space-between; justify-content: space-between;
} }
@ -204,7 +179,7 @@
} }
body { body {
font-size: .8rem; font-size: .7rem;
} }
#root-shop, #root-shop>div { #root-shop, #root-shop>div {
@ -229,7 +204,7 @@
} }
#root-shop table tr { #root-shop table tr {
padding: .2em 0; padding: .8em 0;
display: flex !important; display: flex !important;
justify-content: space-between; justify-content: space-between;
} }
@ -239,16 +214,15 @@
} }
#root-shop .panel .summary>.summary-price table { #root-shop .panel .summary>.summary-price table {
font-size: .8rem; font-size: .7rem;
} }
#root-shop .panel .summary>.summary-form form, #root-shop .panel .summary>.summary-form 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: 1.0rem; font-size: .85rem;
} }
/*#root-shop .panel .summary>.summary-form form input[type="submit"] { /*#root-shop .panel .summary>.summary-form form input[type="submit"] {
@ -269,7 +243,7 @@
##Screen = B/w 481px to 767px ##Screen = B/w 481px to 767px
*/ */
@media (min-width: 481px) and (max-width: 767px) { @media (min-width: 481px) and (max-width: 767px) {
.feedback-add-success, .feedback-add-failure { .feedback-add-success {
background-color: green; background-color: green;
display: block; display: block;
position: fixed; position: fixed;
@ -281,9 +255,6 @@
border-radius: 10px; border-radius: 10px;
box-shadow: 0 0 5px 3px; box-shadow: 0 0 5px 3px;
} }
.feedback-add-failure {
background-color: #c75e5e;
}
.dropdown-item { .dropdown-item {
font-size: .75em; font-size: .75em;
@ -300,7 +271,7 @@
} }
body { body {
font-size: .8rem; font-size: .7rem;
} }
#root-shop, #root-shop>div { #root-shop, #root-shop>div {
@ -320,11 +291,11 @@
} }
#root-shop .productItem .content h3 { #root-shop .productItem .content h3 {
font-size: 1.25rem; font-size: 1rem;
} }
#root-shop .productItem .content ul { #root-shop .productItem .content ul {
font-size: .75rem; font-size: .5rem;
} }
#root-shop .panel { #root-shop .panel {
@ -336,16 +307,13 @@
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
#root-shop .panel .control > .description { #root-shop .panel .control > .description,
#root-shop .panel .control > .crate-mode {
width: 100%; width: 100%;
} }
#root-shop .panel .crate .crate-bar .crate-mode { #root-shop .panel .control > .crate-mode {
text-align: left; text-align: left;
width: 50%;
}
#root-shop .panel .crate .crate-bar .crate-mode a {
display: block;
} }
#root-shop .panel .summary { #root-shop .panel .summary {
@ -357,16 +325,15 @@
} }
#root-shop .panel .summary>.summary-price table { #root-shop .panel .summary>.summary-price table {
font-size: .8rem; font-size: .7rem;
} }
#root-shop .panel .summary>.summary-form form, #root-shop .panel .summary>.summary-form 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: 1rem; font-size: .85rem;
} }
/*#root-shop .panel .summary>.summary-form form input[type="submit"] { /*#root-shop .panel .summary>.summary-form form input[type="submit"] {
@ -379,6 +346,7 @@
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;
@ -397,7 +365,7 @@
} }
#root-shop table tr { #root-shop table tr {
padding: .2em 0; padding: .8em 0;
display: flex !important; display: flex !important;
justify-content: space-between; justify-content: space-between;
} }
@ -405,22 +373,21 @@
#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: min(310px, 60vw); width: 310px;
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: min(310px, 60vw); left: 310px;
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: fixed; position: absolute;
height: 100%; height: 100%;
width: 100%; width: 100%;
background-color: rgba(0, 0, 0, .3); background-color: rgba(0, 0, 0, .3);
@ -434,13 +401,13 @@
transition: left .3s; transition: left .3s;
position: fixed; position: fixed;
z-index: 1; z-index: 1;
left: max(-310px, -60vw); left: -310px;
width: min(310px, 60vw); width: 310px;
height: 100%; height: 100%;
} }
#root-shop .layout>aside.aside .gradient-bottom { #root-shop .layout>aside.aside:after {
display: none; width: 0;
} }
#root-shop .layout>aside.aside + section.main { #root-shop .layout>aside.aside + section.main {
@ -462,7 +429,7 @@
overflow: initial; overflow: initial;
} }
#root-shop .layout>aside.aside.menu-opened > .catalog-container { #root-shop .layout>aside.aside.menu-opened > .backlog-container {
overflow-y: scroll; overflow-y: scroll;
height: 100%; height: 100%;
} }
@ -472,7 +439,7 @@
} }
#accordion_categories button { #accordion_categories button {
font-size: 1.5rem; font-size: 1rem;
padding: .5rem 0.5rem; padding: .5rem 0.5rem;
} }
} }
@ -481,7 +448,7 @@
##Screen = B/w 320px to 479px ##Screen = B/w 320px to 479px
*/ */
@media (min-width: 320px) and (max-width: 480px) { @media (min-width: 320px) and (max-width: 480px) {
.feedback-add-success, .feedback-add-failure { .feedback-add-success {
background-color: green; background-color: green;
display: block; display: block;
position: fixed; position: fixed;
@ -493,9 +460,6 @@
border-radius: 10px; border-radius: 10px;
box-shadow: 0 0 5px 3px; box-shadow: 0 0 5px 3px;
} }
.feedback-add-failure {
background-color: #c75e5e;
}
.dropdown-item { .dropdown-item {
font-size: .75em; font-size: .75em;
@ -512,7 +476,7 @@
} }
body { body {
font-size: .8rem; font-size: .7rem;
} }
#root-shop, #root-shop>div { #root-shop, #root-shop>div {
@ -528,21 +492,20 @@
} }
#root-shop .productItem .content h3 { #root-shop .productItem .content h3 {
font-size: 1.25rem; font-size: 1rem;
} }
#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: min(310px, 90vw); width: 310px;
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: min(310px, 90vw); left: 310px;
position: relative; position: relative;
z-index: 0; z-index: 0;
} }
@ -562,13 +525,13 @@
transition: left .3s; transition: left .3s;
position: fixed; position: fixed;
z-index: 1; z-index: 1;
left: max(-310px, -90vw); left: -310px;
width: min(310px, 90vw); width: 310px;
height: 100%; height: 100%;
} }
#root-shop .layout>aside.aside .gradient-bottom { #root-shop .layout>aside.aside:after {
display: none; width: 0;
} }
#root-shop .layout>aside.aside + section.main { #root-shop .layout>aside.aside + section.main {
@ -607,16 +570,13 @@
font-size: 1.5rem; font-size: 1.5rem;
} }
#root-shop .panel .control > .description { #root-shop .panel .control > .description,
#root-shop .panel .control > .crate-mode {
width: 100%; width: 100%;
} }
#root-shop .panel .crate .crate-bar .crate-mode { #root-shop .panel .control > .crate-mode {
text-align: left; text-align: left;
width: 50%;
}
#root-shop .panel .crate .crate-bar .crate-mode a {
display: block;
} }
#root-shop .panel .summary { #root-shop .panel .summary {
@ -624,16 +584,15 @@
} }
#root-shop .panel .summary>.summary-price table { #root-shop .panel .summary>.summary-price table {
font-size: .8rem; font-size: .7rem;
} }
#root-shop .panel .summary>.summary-form form, #root-shop .panel .summary>.summary-form 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: 1rem; font-size: .85rem;
} }
/*#root-shop .panel .summary>.summary-form form input[type="submit"] { /*#root-shop .panel .summary>.summary-form form input[type="submit"] {
@ -646,6 +605,7 @@
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;
} }
@ -662,7 +622,7 @@
} }
#root-shop table tr { #root-shop table tr {
padding: .2em 0; padding: .8em 0;
display: flex !important; display: flex !important;
justify-content: space-between; justify-content: space-between;
} }
@ -671,7 +631,7 @@
overflow: initial; overflow: initial;
} }
#root-shop .layout>aside.aside.menu-opened > .catalog-container { #root-shop .layout>aside.aside.menu-opened > .backlog-container {
overflow-y: scroll; overflow-y: scroll;
height: 100%; height: 100%;
} }
@ -681,7 +641,7 @@
} }
#accordion_categories button { #accordion_categories button {
font-size: 1.5rem; font-size: 1rem;
padding: .5rem 0.5rem; padding: .5rem 0.5rem;
} }
} }

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

View File

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

Before

Width:  |  Height:  |  Size: 490 B

View File

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

Before

Width:  |  Height:  |  Size: 392 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 46 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 33 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 37 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 117 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 35 KiB

View File

@ -1 +0,0 @@
<svg width="232" height="232" version="1.1" viewBox="0 0 232 232" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-4.0001 -12)" fill="none" stroke="#715ec7" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"><path d="m61.7 204.1-45.7-76.1 45.7-76.1a7.9 7.9 0 0 1 6.8-3.9h147.5a8 8 0 0 1 8 8v144a8 8 0 0 1-8 8h-147.5a7.9 7.9 0 0 1-6.8-3.9z"/><line x1="160" x2="112" y1="104" y2="152"/><line x1="160" x2="112" y1="152" y2="104"/></g></svg>

Before

Width:  |  Height:  |  Size: 463 B

View File

@ -1 +0,0 @@
<svg width="32" height="32" version="1.1" viewBox="0 0 8.4667 8.4667" xmlns="http://www.w3.org/2000/svg"><circle cx="4.2333" cy="4.2333" r="3.9077" fill="#d0423f" stroke="#d0423f" stroke-linecap="round" stroke-width=".65136"/></svg>

Before

Width:  |  Height:  |  Size: 233 B

View File

@ -1,39 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="512"
height="512"
version="1.1"
viewBox="0 0 512 512"
id="svg1"
sodipodi:docname="icon-search.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.59375"
inkscape:cx="255.68627"
inkscape:cy="256"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M456.69,421.39,362.6,327.3a173.81,173.81,0,0,0,34.84-104.58C397.44,126.38,319.06,48,222.72,48S48,126.38,48,222.72s78.38,174.72,174.72,174.72A173.81,173.81,0,0,0,327.3,362.6l94.09,94.09a25,25,0,0,0,35.3-35.3ZM97.92,222.72a124.8,124.8,0,1,1,124.8,124.8A124.95,124.95,0,0,1,97.92,222.72Z"
fill="#fff"
id="path1"
style="fill:#ffffff;fill-opacity:0.5" />
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

32
static/js/as.js Normal file
View File

@ -0,0 +1,32 @@
(function () {
var swRegistration;
var newWorker;
var isRefreshing = false;
var pathname = window.location.pathname;
var deferredPrompt;
if ('serviceWorker' in navigator) {
console.info('Service Worker is supported');
window.addEventListener('load', function () {
navigator.serviceWorker.register('/sw.js', {
updateViaCache: 'all',
}).then(function (registration) {
swRegistration = registration;
console.log('[SW] registration successful with scope: ', swRegistration.scope);
console.log('[SW] is waiting: ', swRegistration.waiting);
navigator.serviceWorker.addEventListener('controllerchange', function () {
if (isRefreshing) {
return;
}
window.location.reload();
isRefreshing = true;
});
}, function (err) {
console.log('[SW] registration failed: ', err);
});
});
}
})();

2
static/js/jquery-3.7.0.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2
static/js/jquery-3.7.0.slim.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,70 +0,0 @@
import React from 'react'
import {Droppable} from "@hello-pangea/dnd";
import {cartStyle, compareArraysWithIds} from "./utils";
import {ProductCartItem} from "./ProductCartItem";
import {FakePlaceholder} from "./FakePlaceholder";
import {hp_to_slots} from "./count_resources";
import {useShopStore} from "./shop_store";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
/**
* Component that displays a list of <ProductCartItem>
*/
export function Cart({crate_index}) {
// #!render_count
const renderCount = useRenderCount();
const crate = useShopStore((state) => state.crates[crate_index], (a, b) => {
return compareArraysWithIds(a.items, b.items) && a.occupiedHP === b.occupiedHP && a.crate_mode === b.crate_mode
});
const crateParams = useShopStore((state) => state.crateParams);
// #!render_count
console.log("Cart renders: ", renderCount)
const nbrOccupied = hp_to_slots(crate.occupiedHP);
const nbrSlots = hp_to_slots(crateParams(crate.crate_mode).hp);
const products = crate.items.map((item, index) => {
return (
<ProductCartItem
card_index={index}
crate_index={crate_index}
first={index === 0}
last={index === crate.items.length - 1 && nbrOccupied >= nbrSlots}
key={item.id}/>
);
});
return (
<Droppable droppableId={crate.id} direction="horizontal">
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
style={cartStyle(
provided.droppableProps.style,
snapshot,
)}
className="items-cart-list">
{products}
{provided.placeholder && (
<div style={{display: 'none'}}>
{provided.placeholder}
</div>
)}
<FakePlaceholder
nToDraw={nbrSlots - nbrOccupied}
isDraggingOver={snapshot.isDraggingOver}/>
</div>
)}
</Droppable>
);
}

View File

@ -1,69 +0,0 @@
import React from 'react';
import {Droppable} from "@hello-pangea/dnd";
import {useShopStore} from "./shop_store";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
import {CatalogGroups} from "./CatalogGroups";
import {SearchBar} from "./SearchBar";
import {CatalogSearchResult} from "./CatalogSearchResult";
import {GradientBottom} from "./GradientBottom";
/**
* Component that renders the catalog in the aside
*/
export function Catalog() {
// #!render_count
const renderCount = useRenderCount();
const data = useShopStore((state) => state.groups);
const items = useShopStore((state) => state.cards);
const onClickToggleMobileSideMenu = useShopStore((state) => state.switchSideMenu);
const isMobile = useShopStore((state) => state.isMobile);
const showSearch = useShopStore((state) => state.listed_cards.length > 0 && 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,44 +0,0 @@
import {ProductItem} from "./ProductItem";
import React from "react";
import {useShopStore} from "./shop_store";
export function CatalogGroups() {
const data = useShopStore((state) => state.groups);
const items = useShopStore((state) => state.cards);
const ordered_groups = data.categories.map(groupItem => ({
name: groupItem.name,
items: groupItem.itemIds.map(itemId => items[itemId])
}));
let item_index = -1;
const groups = ordered_groups.map((group, g_index) => {
return (
<div className="accordion-item" key={`${group.name}`}>
<h2 className="accordion-header">
<button className="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target={`#collapse${g_index}`} aria-expanded="true"
aria-controls={`collapse${g_index}`}>
{group.name}
</button>
</h2>
<div id={`collapse${g_index}`} className="accordion-collapse collapse" aria-labelledby="headingOne"
data-bs-parent="#accordion_categories">
<div className="accordion-body">
{group.items.map(item => {
item_index++;
return (
<ProductItem card_index={item_index} key={item.id} />
)
})}
</div>
</div>
</div>
);
}
);
return (
<div className="accordion accordion-flush" id="accordion_categories">
{groups}
</div>
)
}

View File

@ -1,16 +0,0 @@
import React from 'react';
import {useShopStore} from "./shop_store";
import {ProductItem} from "./ProductItem";
export function CatalogSearchResult() {
const cards_to_display = useShopStore((state) => state.listed_cards);
return ( <>
{cards_to_display.map((item, _) => {
return (
<ProductItem card_index={item} key={"searched_" + item} />
)
})}
</>
)
}

View File

@ -1,53 +0,0 @@
import React from 'react';
import {Cart} from "./Cart";
import {CrateMode} from "./CrateMode";
import {CrateWarnings} from "./CrateWarnings";
import {useShopStore} from "./shop_store";
import {CrateOptions} from "./CrateOptions";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
/**
* Component that displays the main crate with reminder rules.
* It includes <Cart> and rules
*/
export function Crate({crate_index}) {
// #!render_count
const renderCount = useRenderCount();
const crate = useShopStore((state) => state.crates[crate_index],
(a, b) => a.length === b.length && a.id === b.id);
const modes_order = useShopStore((state) => state.modes_order);
const onDeleteCrate = useShopStore((state) => state.delCrate);
// #!render_count
console.log("Crate renders: ", renderCount)
return (
<div className="crate">
{
modes_order.includes(crate.crate_mode) ? (
<div className="crate-bar d-inline-flex justify-content-between">
<CrateMode crate_index={crate_index}/>
<div className="delete-crate align-self-start align-content-start justify-content-end" onClick={() => onDeleteCrate(crate.id)}>
Delete crate <img src="/images/shop/icon-remove.svg" alt="remove"/>
</div>
</div>
) : <></>
}
<div className="crate-products">
<Cart crate_index={crate_index}/>
<CrateWarnings crate_index={crate_index} />
<CrateOptions crate_index={crate_index}/>
</div>
</div>
);
}

View File

@ -1,45 +0,0 @@
import React from 'react'
import {Accordion} from "react-bootstrap";
import {Crate} from "./Crate";
import {useShopStore} from "./shop_store";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
export function CrateList() {
// #!render_count
const renderCount = useRenderCount();
const crates = useShopStore((state) => state.crates,
(a, b) => a.length === b.length);
const active_crate = useShopStore((state) => state.active_crate);
const onAddCrate = useShopStore((state) => state.newCrate);
const setActiveCrate = useShopStore((state) => state.setActiveCrate);
const onSelectHandler = (e) => {
// if e === null, that means that an accordion item was collapsed rather than expanded. e will be non-null when an item is expanded
if (e !== null)
setActiveCrate(e);
else
setActiveCrate("")
};
// #!render_count
console.log("CrateList renders: ", renderCount)
return (
<Accordion id="accordion_crates" flush activeKey={active_crate} onSelect={onSelectHandler}>
{crates.map((crate, index) =>
<Accordion.Item eventKey={crate.id} key={"accordion"+crate.id} className="accordion_crates_item">
<Accordion.Header>{crate.name ? crate.name : <>Crate #{`${index}`}</>} </Accordion.Header>
<Accordion.Body>
<Crate crate_index={index}/>
</Accordion.Body>
</Accordion.Item>
)}
<Accordion.Item eventKey="last" id="accordion_crates_add">
<Accordion.Header onClick={onAddCrate}>
Add new crate
</Accordion.Header>
</Accordion.Item>
</Accordion>)
}

View File

@ -1,35 +0,0 @@
import React from 'react';
import {useShopStore} from "./shop_store";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
/**
* Component that displays crate modes
*/
export function CrateMode({crate_index}) {
// #!render_count
const renderCount = useRenderCount();
const modes_order = useShopStore((state) => state.modes_order);
const crate_modes = useShopStore((state) => state.crate_modes);
const crate = useShopStore((state) => state.crates[crate_index],
(a, b) => a.id === b.id && a.crate_mode === b.crate_mode);
const setMode = useShopStore((state) => state.setCrateMode);
// #!render_count
console.log("CrateMode renders: ", renderCount)
return (
<div className="crate-mode">
{modes_order.map((mode_name, _) => (
<a
key={mode_name}
className={(crate.crate_mode === mode_name ? 'active' : '')}
onClick={() => setMode(crate.id, mode_name)}
href="#"
role="button">{crate_modes[mode_name].name}</a>
))}
</div>
);
}

View File

@ -1,39 +0,0 @@
import React from 'react';
import {useShopStore} from "./shop_store";
import {ProcessOptions} from "./options/Options";
export function CrateOptions({crate_index}) {
const crate_id = useShopStore((state) => state.crates[crate_index].id);
const optionsLogic = useShopStore((state) => state.crate_options);
const updateOptions = useShopStore((state) => state.updateCrateOptions);
const options_data = useShopStore((state) => state.crates[crate_index].options_data || {});
const options = ProcessOptions({
options: optionsLogic,
data: options_data,
id: "crate_options" + crate_id,
target: {
construct: ((outvar, value) => {
// #!options_log
console.log("construct", outvar, value, options_data);
options_data[outvar] = value;
}),
update: ((outvar, value) => {
// #!options_log
console.log("update", outvar, value, options_data);
if (outvar in options_data) options_data[outvar] = value;
updateOptions(crate_id, {[outvar]: value});
})
}
});
return (
<div className="crate-bar">
{options}
</div>
)
}

View File

@ -1,27 +0,0 @@
import React from "react";
import {LevelUI} from "./warnings";
import {useShopStore} from "./shop_store";
import {compareArraysWithIds} from "./utils";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
export function CrateWarnings({crate_index}) {
// #!render_count
const renderCount = useRenderCount();
const crate_warnings = useShopStore(state => (state.crates[crate_index].warnings), compareArraysWithIds)
// #!render_count
console.log("CrateWarnings renders: ", renderCount)
return (
<div className="crate-info">
{crate_warnings.map((rule, index) => (
<p key={index} className="rule" style={{'color': LevelUI(rule.level).color}}>
<img src={LevelUI(rule.level).icon} /> <i>{rule.message}</i>
</p>
))}
</div>
)
}

View File

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

@ -1,27 +0,0 @@
import React from 'react';
/**
* Component that displays a placeholder inside crate.
* Allows to display how it remains space for the current crate.
*/
export function FakePlaceholder({isDraggingOver, nToDraw}) {
const fakePlaceholder = [];
for (let i = nToDraw; i > 0; i--) {
fakePlaceholder.push(
<div key={i} style={{
display: isDraggingOver ? 'none' : 'block',
border: '1px dashed #ccc',
width: '45px',
marginBottom: '5px',
}}></div>
);
}
return (
<React.Fragment>
{fakePlaceholder}
</React.Fragment>
);
}

View File

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

View File

@ -1,104 +0,0 @@
import {useShopStore} from "./shop_store";
import {useClickAway} from "./options/useClickAway";
import {Modal} from "react-bootstrap";
import React from "react";
import {Validation} from "./validate";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
const JSONExample = JSON.stringify({
"crates": [
{
"items": [
{
"pn": "1124",
"options": null
},
{
"pn": "2128",
"options": null
},
{
"pn": "2128",
"options": null
},
{
"pn": "2128",
"options": null
}
],
"type": "rack",
"options": {}
},
{
"items": [],
"type": "no_crate",
"options": {}
}
],
"options": {}
});
export function ImportJSON() {
// #!render_count
const renderCount = useRenderCount();
const shouldShow = useShopStore((state) => state.importShouldOpen);
const data = useShopStore((state) => state.importValue);
const loadDescription = useShopStore((state) => state.loadDescription);
const updateImportDescription = useShopStore((state) => state.updateImportDescription);
const closeImport = useShopStore((state) => state.closeImport);
const showImport = useShopStore((state) => state.openImport);
// #!render_count
console.log("ImportJSON renders: ", renderCount)
const ref = useClickAway((e) => {
if (e.type === "mousedown") // ignore touchstart
closeImport()
}
);
return (<>
<button
className="btn btn-sm btn-outline-primary m-0 mb-2"
style={{'cursor': 'pointer'}}
onClick={showImport}>Import JSON
</button>
<Modal show={shouldShow} animation={true} centered className="rfqFeedback">
<Modal.Body ref={ref}>
<div className="form-group">
<p className="small">
Input the JSON description below. Should be something like:
<br/>
{JSONExample}
</p>
</div>
<div className="form-group w-100">
<textarea
onChange={(event) => {
updateImportDescription(event.target.value)
}}
value={data.value}
className="form-control w-100"
rows="5"
placeholder="Input JSON description here."/>
</div>
{data.error !== Validation.OK ? (
<div className="form-group">
<p className="text-danger">{data.error === Validation.Empty ? "Empty input" : "Invalid JSON"}</p>
</div>
) : null}
<div className="d-flex flex-column flex-sm-row justify-content-end">
<a type="button" onClick={closeImport}
className="btn btn-sm btn-outline-primary m-0 mb-2 mt-2 mb-sm-0 me-sm-2">Close</a>
<a type="button" onClick={loadDescription}
className={`btn btn-sm btn-primary m-0 ms-sm-2 mt-2 ${data.error ? 'disabled' : ''}`}>Load
configuration</a>
</div>
</Modal.Body>
</Modal>
</>)
}

View File

@ -1,46 +0,0 @@
import React from 'react';
import {useShopStore} from "./shop_store";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
/**
* Component that provides a base layout (aside/main) for the page.
*/
export function Layout({aside, main}) {
// #!render_count
const renderCount = useRenderCount();
const mobileSideMenuShouldOpen = useShopStore(state => state.sideMenuIsOpen);
const onClickToggleMobileSideMenu = useShopStore(state => state.switchSideMenu);
const showCardAddedFeedback = useShopStore(state => state.showCardAddedFeedback);
const showNoDestination = useShopStore(state => state.showNoDestination);
// #!render_count
console.log("Layout renders: ", renderCount)
return (
<div className="layout">
<aside className={'aside ' + (mobileSideMenuShouldOpen ? 'menu-opened' : '')}>{aside}</aside>
{mobileSideMenuShouldOpen ? (
<section className="main" onClick={onClickToggleMobileSideMenu}>{main}</section>
) : (
<section className="main">{main}</section>
)}
{showCardAddedFeedback ? (
!showNoDestination ?
(<div className="feedback-add-success">
added
</div>)
: (<div className="feedback-add-failure">
No cards added: all crates are closed
</div>)
) : null}
</div>
);
}

View File

@ -1,63 +0,0 @@
import {DialogPopup} from "./options/DialogPopup";
import React from "react";
import {useShopStore} from "./shop_store";
import {SummaryPopup} from "./options/SummaryPopup";
export function OptionsDialogWrapper({crate_index, card_index, first, last}) {
const crate_id = useShopStore((state) => state.crates[crate_index].id);
const options = useShopStore((state) => state.crates[crate_index].items[card_index].options);
const options_data = useShopStore((state) => state.crates[crate_index].items[card_index].options_data);
const card_size = useShopStore((state) => state.crates[crate_index].items[card_index].size);
const card_id = useShopStore((state) => state.crates[crate_index].items[card_index].id);
const options_class = useShopStore((state) => state.crates[crate_index].items[card_index].options_class);
const sideMenuIsOpen = useShopStore((state) => state.sideMenuIsOpen);
const _notificationTimer = useShopStore((state) => state.notificationTimer);
const hideNotification = useShopStore((state) => state.hideNotification);
const displayNotification = useShopStore((state) =>
state.notificationCrateId === crate_id &&
(state.notificationCardIndex === card_index || (state.crates[crate_index].items.length + (state.notificationCardIndex || -1)) === card_index));
const onOptionsUpdate = useShopStore((state) => state.updateOptions);
return (
<DialogPopup
options={options}
data={options_data}
options_class={options_class}
key={"popover" + crate_id +card_id}
id={"popover"+ crate_id + card_id}
big={card_size === "big"}
first={first}
last={last}
sideMenuIsOpen={sideMenuIsOpen}
onHideNotification={hideNotification}
displayNotification={displayNotification}
target={{
construct: ((outvar, value) => {
// #!options_log
console.log("construct", outvar, value, options_data);
options_data[outvar] = value;
}),
update: ((outvar, value) => {
// #!options_log
console.log("update", outvar, value, options_data);
if (outvar in options_data) options_data[outvar] = value;
onOptionsUpdate(crate_id, card_index, {[outvar]: value});
})
}}
/>
)
}
export function OptionsSummaryWrapper({crate_index, card_index}) {
const card_id = useShopStore((state) => state.crates[crate_index].items[card_index].id);
const options = useShopStore((state) => state.crates[crate_index].items[card_index].options);
const options_data = useShopStore((state) => state.crates[crate_index].items[card_index].options_data);
return (
<SummaryPopup id={card_id + "options"} options={options}
data={options_data}/>
)
}

View File

@ -1,78 +0,0 @@
import React from 'react'
import {Validation} from "./validate.js";
import {useShopStore} from "./shop_store";
import {ShowJSON} from "./ShowJSON";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
import {OrderOptions} from "./OrderOptions";
/**
* Components that renders the form to request quote.
*/
export function OrderForm() {
// #!render_count
const renderCount = useRenderCount();
const email = useShopStore((state) => state.email);
const note = useShopStore((state) => state.note);
const isProcessing = useShopStore((state) => state.isProcessing);
const updateEmail = useShopStore((state) => state.updateEmail);
const updateNote = useShopStore((state) => state.updateNote);
const submitForm = useShopStore((state) => state.submitForm);
const submitDisabled = useShopStore((state) => state.submitDisabled);
const resetEmailValidation = useShopStore((state) => state.resetEmailValidation);
// #!render_count
console.log("OrderForm renders: ", renderCount)
return (
<div className="summary-form">
<OrderOptions/>
<form onSubmit={submitForm} noValidate>
<input
className={`${email.error > 0 ? 'errorField' : ''}`}
type="email"
placeholder="Email"
onFocus={resetEmailValidation}
onChange={(event) => updateEmail(event.target.value)}
onBlur={(event) => updateEmail(event.target.value)}
value={email.value}/>
{email.error === Validation.Empty ? (
<div className="error">
<small>Required</small>
</div>
) : null}
{email.error === Validation.Invalid ? (
<div className="error">
<small>Your email is incomplete</small>
</div>
) : null}
<textarea
onChange={(event) => updateNote(event.target.value)}
defaultValue={note.value}
rows="5"
placeholder="Additional notes"/>
<div className="d-flex flex-column flex-sm-row justify-content-between">
<ShowJSON/>
<input className="btn btn-primary w-100 m-0 ms-sm-2 order-form-submit" type="button"
disabled={submitDisabled()}
onClick={submitForm}
value={`${isProcessing ? 'Processing ...' : 'Request quote'}`}/>
</div>
{/*This will open an email window. Send the email to make your request.*/}
</form>
</div>
);
}

View File

@ -1,38 +0,0 @@
import {useShopStore} from "./shop_store";
import {ProcessOptions} from "./options/Options";
import React from "react";
export function OrderOptions() {
const optionsLogic = useShopStore((state) => state.order_options);
const updateOptions = useShopStore((state) => state.updateOrderOptions);
const options_data = useShopStore((state) => state.order_options_data || {});
const options = ProcessOptions({
options: optionsLogic,
data: options_data,
id: "order_options",
target: {
construct: ((outvar, value) => {
// #!options_log
console.log("construct", outvar, value, options_data);
options_data[outvar] = value;
updateOptions({[outvar]: value});
}),
update: ((outvar, value) => {
// #!options_log
console.log("update", outvar, value, options_data);
if (outvar in options_data) options_data[outvar] = value;
updateOptions({[outvar]: value});
})
}
});
return (
<div className="order-bar">
{options}
</div>
)
}

View File

@ -1,56 +0,0 @@
import React from 'react'
import {SummaryOrder} from "./SummaryOrder";
import {OrderForm} from "./OrderForm";
import {CrateList} from "./CrateList";
import {useShopStore} from "./shop_store";
import {ImportJSON} from "./ImportJSON";
import {RFQFeedback} from "./RFQFeedback";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
import {OrderOptions} from "./OrderOptions";
/**
* Component that renders all things for order.
* It acts like-a layout, this component do nothing more.
*/
export function OrderPanel({title, description}) {
// #!render_count
const renderCount = useRenderCount();
const isMobile = useShopStore((state) => state.isMobile);
const onClickToggleMobileSideMenu = useShopStore((state) => state.switchSideMenu);
// #!render_count
console.log("OrderPanel renders: ", renderCount)
return (<section className="panel">
<h2>{title}</h2>
<div className="control justify-content-between">
{description}
</div>
<div>
<ImportJSON/>
</div>
<RFQFeedback/>
{isMobile ? (
<div className="mobileBtnDisplaySideMenu">
<button onClick={onClickToggleMobileSideMenu}>
<img src="/images/shop/icon-add.svg" alt="add"/>
</button>
</div>
) : null}
<CrateList/>
<section className="summary">
<SummaryOrder/>
<OrderForm/>
</section>
</section>);
}

View File

@ -1,115 +0,0 @@
import React from 'react'
import {Draggable} from "@hello-pangea/dnd";
import {compareObjectsEmptiness, productStyle} from "./utils";
import {Resources} from "./Resources";
import {CardWarnings} from "./CardWarnings";
import {useShopStore} from "./shop_store";
import {OptionsDialogWrapper} from "./OptionsWrapper";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
/**
* Component that renders a product.
* Used in the crate
*/
export function ProductCartItem({card_index, crate_index, first, last}) {
// #!render_count
const renderCount = useRenderCount();
const card = useShopStore((state) => state.crates[crate_index].items[card_index],
(a, b) => a.id === b.id);
const card_show_warnings = useShopStore(state => state.crates[crate_index].items[card_index].show_warnings, compareObjectsEmptiness);
const card_counted_resources = useShopStore(state => state.crates[crate_index].items[card_index].counted_resources, compareObjectsEmptiness);
const highlighted = useShopStore((state) => state.crates[crate_index].id === state.highlighted.crate && card_index === state.highlighted.card);
const options_disabled = useShopStore((state) => !!state.crateParams(state.crates[crate_index].crate_mode).warnings_disabled);
const crate_id = useShopStore((state) => state.crates[crate_index].id);
const setHighlight = useShopStore((state) => state.highlightCard);
const removeHighlight = useShopStore((state) => state.highlightReset);
const onCardRemove = useShopStore((state) => state.deleteCard);
// #!render_count
console.log("ProductCartItem renders: ", renderCount)
const options = !options_disabled && card && card.options && card.options.length > 0;
const warnings = !options_disabled && card_show_warnings && card_show_warnings.length > 0;
const resources = !options_disabled && card_counted_resources && card_counted_resources.length > 0;
return (
<Draggable draggableId={card.id} index={card_index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
...productStyle(
provided.draggableProps.style,
snapshot,
true,
!!highlighted,
false,
true
)
}}
onMouseEnter={() => setHighlight(crate_id, card_index)}
onMouseLeave={removeHighlight}
>
{/* warning container */}
<div className="progress-container warning d-flex justify-content-evenly">
{warnings &&
(<CardWarnings crate_index={crate_index} card_index={card_index} />)
}
{options && (
<OptionsDialogWrapper
crate_index={crate_index}
card_index={card_index}
first={first}
last={last}
/>
)}
</div>
<h6>{card.name_number}</h6>
<div
onMouseEnter={() => setHighlight(crate_id, card_index)}
onClick={() => setHighlight(crate_id, card_index)}
>
<img
className='item-cart'
src={card.image}/>
</div>
{/* remove container */}
<div
style={{'display': highlighted ? 'flex' : 'none'}}
className="overlayRemove"
onClick={() => onCardRemove(crate_id, card_index)}>
<img src="/images/shop/icon-remove.svg" alt="rm"/>
<p>Remove</p>
</div>
{/* progression container */}
{resources && (
<Resources crate_index={crate_index} card_index={card_index} />
)}
</div>
)}
</Draggable>
);
}

View File

@ -1,91 +0,0 @@
import React from 'react';
import {Draggable} from "@hello-pangea/dnd";
import {formatMoney, productStyle} from "./utils";
import {useShopStore} from "./shop_store";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
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.
* Used in the aside (e.g catalog of products)
*/
export function ProductItem({card_index}) {
// #!render_count
const renderCount = useRenderCount();
const getCardDescription = useShopStore((state) => state.getCardDescription);
const currency = useShopStore((state) => state.currency);
const addCardFromCatalog = useShopStore((state) => state.addCardFromCatalog);
const card = getCardDescription(card_index);
// #!render_count
console.log("ProductItem renders: ", renderCount)
return (
<section className="productItem">
<div className="content">
<h3 style={{'marginBottom': card.name_codename ? '5px' : '20px'}}>{card.name_number} {card.name}</h3>
{card.name_codename ? (
<p>{card.name_codename}</p>
) : null}
<div className="price">{`${currency} ${formatMoney(card.price)}`}</div>
<CardSpecs specs={card.specs}/>
<DatasheetLink datasheet_file={card.datasheet_file} datasheet_name={card.datasheet_name}/>
</div>
<div className="content">
<AddButton onAdd={() => addCardFromCatalog(null, card_index, null)} />
<Draggable draggableId={card.id + card_index} index={card_index}>
{(provided, snapshot) => (
<React.Fragment>
<img
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={productStyle(
provided.draggableProps.style,
snapshot,
true, // hack: remove weird animation after a drop
)}
src={card.image}/>
{/* Allows to simulate a clone */}
{snapshot.isDragging && (
<img className="simclone" src={card.image}/>
)}
</React.Fragment>
)}
</Draggable>
</div>
</section>
);
}

View File

@ -1,42 +0,0 @@
import {useShopStore} from "./shop_store";
import {useClickAway} from "./options/useClickAway";
import {Modal} from "react-bootstrap";
import {Validation} from "./validate";
import React from "react";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
export function RFQFeedback() {
// #!render_count
const renderCount = useRenderCount();
const closeRFQ = useShopStore((state) => state.closeRFQFeedback);
const shouldShow = useShopStore((state) => state.shouldShowRFQFeedback);
const status = useShopStore((state) => state.processingResult);
// #!render_count
console.log("RFQFeedback renders: ", renderCount)
const ref = useClickAway((e) => {
if (e.type === "mousedown") // ignore touchstart
closeRFQ()
});
return (
<Modal show={shouldShow} animation={true} centered>
<Modal.Body ref={ref} className="rfqFeedback">
<div className="d-flex">
<div>
{status.status === Validation.OK ?
<img width="30px" src="/images/shop/icon-done.svg" alt="close"/>
: <img width="30px" src="/images/shop/icon-warning.svg" alt="close"/>
}
</div>
<div style={{'padding': '0 .5em'}}>
{status.message}
</div>
</div>
</Modal.Body>
</Modal>
)
}

View File

@ -1,105 +0,0 @@
import {OverlayTrigger} from "react-bootstrap";
import React from "react";
import {v4 as uuidv4} from "uuid";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
import {useShopStore} from "./shop_store";
import {compareArraysLevelOne} from "./utils";
const resourcesWidthStyle = (occupied, max) => {
return {
width: `${Math.min(occupied * 100 / max, 100)}%`,
}
};
function EEMRenderer({occupied, max}) {
return (
<div className="nbr-connectors" key={uuidv4()}>
<div style={{...resourcesWidthStyle(occupied, max)}}></div>
</div>
)
}
function ClockRenderer({occupied, max}) {
return (
<div className="nbr-clocks" key={uuidv4()}>
<div style={{...resourcesWidthStyle(occupied, max)}}></div>
</div>
)
}
const resource_progress_renderers = {
"eem": EEMRenderer,
"clk": ClockRenderer,
"idc": EEMRenderer,
"tec": EEMRenderer
}
function EEMTipRender({occupied, max}) {
return (<p key={uuidv4()}>{`${occupied}/${max} EEM connectors used`}</p>);
}
function IDCTipRender({occupied, max}) {
return (<p key={uuidv4()}>{`${occupied}/${max} IDC connectors used`}</p>);
}
function TECTipRender({occupied, max}) {
return (<p key={uuidv4()}>{`${occupied}/${max} TEC connectors used`}</p>);
}
function ClockTipRender({occupied, max}) {
return (<p key={uuidv4()}>{`${occupied}/${max} clock connectors used`}</p>);
}
const resource_tip = {
"eem": EEMTipRender,
"clk": ClockTipRender,
"idc": IDCTipRender,
"tec": TECTipRender
}
function RenderResources({resources, library}) {
if (!resources) return null;
let result = [];
resources.forEach((value, _) => {
if (library[value.name]) result.push(library[value.name](value));
});
return result;
}
export function Resources({crate_index, card_index}) {
// #!render_count
const renderCount = useRenderCount();
const resources = useShopStore(state => state.crates[crate_index].items[card_index].counted_resources, compareArraysLevelOne);
// #!render_count
console.log("Resources renders: ", renderCount)
return (
<OverlayTrigger
placement="top"
trigger={['click', 'hover', 'focus']}
overlay={({arrowProps, hasDoneInitialMeasure, show, ...props}) => (
<div className="k-popup-connectors" {...props}>
<RenderResources
resources={resources}
library={resource_tip}
/>
</div>
)}
rootClose
>
<div className="progress-container">
<RenderResources
resources={resources}
library={resource_progress_renderers}
/>
</div>
</OverlayTrigger>
)
}

View File

@ -1,18 +0,0 @@
import React from 'react';
import {useShopStore} from "./shop_store";
export function SearchBar() {
const search_bar_value = useShopStore((state) => state.search_bar_value);
const updateSearchBar = useShopStore((state) => state.updateSearchBar);
return (
<div className="search-catalog form-outline">
<input type="search"
id="search_bar"
className="search-catalog-input form-control"
placeholder="Search"
value={search_bar_value}
onChange={event => updateSearchBar(event.target.value)}
aria-label="Search"/>
</div>
)
}

View File

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

View File

@ -1,99 +0,0 @@
import React, {useState} from "react";
import {Modal} from "react-bootstrap";
import {useShopStore} from "./shop_store";
import {useClickAway} from "./options/useClickAway";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
import {Validation} from "./validate";
const copyButtonStates = {
[Validation.OK]: {
style: "btn-outline-success",
content: "✓ copied"
},
[Validation.Empty]: {
style: "btn-outline-primary",
content: "Copy"
},
[Validation.Invalid]: {
style: "btn-outline-danger",
content: "Error"
},
};
export function ShowJSON() {
// #!render_count
const renderCount = useRenderCount();
const shouldShow = useShopStore((state) => state.shouldShowDescription);
const description = useShopStore((state) => state.description);
const closeDescription = useShopStore((state) => state.closeDescription);
const showDescription = useShopStore((state) => state.showDescription);
const [copiedState, setCopiedState] = useState(Validation.Empty);
// #!render_count
console.log("ShowJSON renders: ", renderCount)
const ref = useClickAway((e) => {
if (e.type === "mousedown") // ignore touchstart
closeDescription()
}
);
const copyToClipboard = (text) => {
try {
navigator.clipboard.writeText(text)
.then((_value) => { // success
setCopiedState(Validation.OK);
setTimeout(() => {setCopiedState(Validation.Empty)}, 1500);
}, (reason) => { // error
setCopiedState(Validation.Invalid);
setTimeout(() => {setCopiedState(Validation.Empty)}, 3000);
console.warn("Copy to clipboard rejected: ", reason)
});
} catch (e) {
setCopiedState(Validation.Invalid);
setTimeout(() => {setCopiedState(Validation.Empty)}, 3000);
console.warn("Copy to clipboard error: ", e)
}
};
return (<>
<input
className="btn btn-outline-primary w-100 m-0 mb-2 mb-sm-0 me-sm-2"
style={{'cursor': 'pointer', 'fontWeight': '700'}}
defaultValue="Show JSON"
onClick={showDescription}
readOnly={true}/>
<Modal show={shouldShow} animation={true} className="rfqFeedback" centered>
<Modal.Body ref={ref}>
<textarea
value={description}
className="form-control w-100"
rows={10}
readOnly={true}
placeholder="There should be description of the crate"/>
<div className="d-flex flex-column flex-sm-row justify-content-end">
{window.isSecureContext && (
<a type="button"
onClick={() => {
copyToClipboard(description)
}}
className={"btn btn-sm m-0 mb-1 mt-2 mb-sm-0 me-sm-2 " + copyButtonStates[copiedState].style}
>
{copyButtonStates[copiedState].content}
</a>
)}
<a type="button" onClick={closeDescription}
className="btn btn-sm btn-outline-primary m-0 mb-1 mt-2 mb-sm-0 me-sm-2">Close</a>
</div>
</Modal.Body>
</Modal>
</>)
}

View File

@ -1,34 +0,0 @@
import {range} from "./utils";
import React from "react";
import {useShopStore} from "./shop_store";
import {SummaryCrateHeader} from "./SummaryCrateHeader";
import {SummaryCrateCard} from "./SummaryCrateCard";
import {SummaryCratePricedOptions} from "./SummaryCratePricedOptions";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
export function SummaryCrate({crate_index}) {
// #!render_count
const renderCount = useRenderCount();
const crate_id = useShopStore((state) => state.crates[crate_index].id);
const crate_len = useShopStore((state) => state.crates[crate_index].items.length);
// #!render_count
console.log("SummaryCrate renders: ", renderCount)
return (
<tbody key={"summary_crate_body" + crate_id}>
<SummaryCrateHeader crate_index={crate_index}/>
{range(0, crate_len).map((index, _i) =>
<SummaryCrateCard crate_index={crate_index} card_index={index} key={"summary_crate_" + crate_id + "_" +index} />
)}
<SummaryCratePricedOptions crate_index={crate_index}/>
</tbody>
)
}

View File

@ -1,78 +0,0 @@
import {compareObjectsEmptiness, formatMoney} from "./utils";
import {WarningIndicator} from "./CardWarnings";
import React from "react";
import {useShopStore} from "./shop_store";
import {OptionsSummaryWrapper} from "./OptionsWrapper";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
export function SummaryCrateCard({crate_index, card_index}) {
// #!render_count
const renderCount = useRenderCount();
const currency = useShopStore((state) => state.currency);
const deleteCard = useShopStore((state) => state.deleteCard);
const setHighlight = useShopStore((state) => state.highlightCard);
const resetHighlight = useShopStore((state) => state.highlightReset);
const highlighted = useShopStore((state) => state.crates[crate_index].id === state.highlighted.crate && card_index === state.highlighted.card);
const crate_id = useShopStore((state) => state.crates[crate_index].id);
const card = useShopStore((state) => state.crates[crate_index].items[card_index],
(a, b) => a.id === b.id);
const card_show_warnings = useShopStore(state => state.crates[crate_index].items[card_index].show_warnings, compareObjectsEmptiness);
const card_options_data = useShopStore(state => state.crates[crate_index].items[card_index].options_data, compareObjectsEmptiness);
const options_disabled = useShopStore((state) => !!state.crateParams(state.crates[crate_index].crate_mode).warnings_disabled);
// #!render_count
console.log("SummaryCrateCard renders: ", renderCount)
const options = !options_disabled && card && card.options && card.options.length > 0;
const options_data = !options_disabled && card_options_data && Object.keys(card_options_data).length > 0;
const warnings = !options_disabled && card_show_warnings && card_show_warnings.length > 0;
return (
<tr
key={"summary_crate_" + crate_id + "_" + card_index}
className={`hoverable ${highlighted ? 'selected' : ''}`}
onClick={() => setHighlight(crate_id, card_index)}
onMouseEnter={() => setHighlight(crate_id, card_index)}
onMouseLeave={() => resetHighlight()}>
<td className="item-card-name tabbed">
<div>{`${card.name_number} ${card.name} ${card.name_codename}`}</div>
</td>
<td className="price">
<div className="d-inline-flex align-content-center">
{`${currency} ${formatMoney(card.price)}`}
<button onClick={() => deleteCard(crate_id, card_index)}>
<img src="/images/shop/icon-remove.svg" className="d-block"/>
</button>
<div style={{'width': '45px', 'height': '20px'}}
className="d-inline-flex align-content-center align-self-center justify-content-evenly">
{(warnings ? (
<WarningIndicator crate_index={crate_index} card_index={card_index}/>
) : (
<span style={{
'display': 'inline-block',
'minWidth': '20px',
}}>&nbsp;</span>
))}
{((options && options_data) ? (
<OptionsSummaryWrapper crate_index={crate_index} card_index={card_index}/>
) : (
<span style={{
'display': 'inline-block',
'width': '20px',
}}>&nbsp;</span>
))}
</div>
</div>
</td>
</tr>);
}

View File

@ -1,53 +0,0 @@
import {formatMoney} from "./utils";
import React from "react";
import {useShopStore} from "./shop_store";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
export function SummaryCrateHeader({crate_index}) {
// #!render_count
const renderCount = useRenderCount();
const currency = useShopStore((state) => state.currency);
const crateParams = useShopStore((state) => state.crateParams);
const clearCrate = useShopStore((state) => state.clearCrate);
const delCrate = useShopStore((state) => state.delCrate);
const crate_mode = useShopStore((state) => state.crates[crate_index].crate_mode);
const crate_id = useShopStore((state) => state.crates[crate_index].id);
const crate_name = useShopStore((state) => state.crates[crate_index].name);
const modes_order = useShopStore((state) => state.modes_order);
const crate_mode_displayed = modes_order.includes(crate_mode);
// #!render_count
console.log("SummaryCrateHeader renders: ", renderCount)
let crate_type = crateParams(crate_mode);
return (
<tr key={"summary_crate_" + crate_id}>
<td className="item-card-name">{!!crate_name ? crate_name : crate_type.name + " #" + crate_index}</td>
<td className="price">
<div className="d-inline-flex">
{crate_mode_displayed && `${currency} ${formatMoney(crate_type.price)}`}
<button onClick={() => clearCrate(crate_id)}>
<img src="/images/shop/icon-clear.svg" alt="empty crate"/>
</button>
{
crate_mode_displayed ? (
<button onClick={() => delCrate(crate_id)}>
<img src="/images/shop/icon-remove.svg" alt="remove crate"/>
</button>
) : <span className="span-with-margin"></span>
}
</div>
</td>
</tr>
)
}

View File

@ -1,43 +0,0 @@
import {formatMoney} from "./utils";
import React from "react";
import {useShopStore} from "./shop_store";
import {ProcessOptionsToData} from "./options/Options";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
export function SummaryCratePricedOptions({crate_index}) {
// #!render_count
const renderCount = useRenderCount();
const currency = useShopStore((state) => state.currency);
const crate_id = useShopStore((state) => state.crates[crate_index].id);
const optionsPrices = useShopStore((state) => state.crate_prices);
const updateOptions = useShopStore((state) => state.updateCrateOptions);
const options_data = useShopStore((state) => state.crates[crate_index].options_data || {});
const options = ProcessOptionsToData({options: optionsPrices, data: options_data});
// #!render_count
console.log("SummaryCratePricedOptions renders: ", renderCount)
return options.map((option, _i) => (
<tr key={"summary_crate_" + crate_id +"option_" + option.id}>
<td className="item-card-name tabbed">
<div>{option.title}</div>
</td>
<td className="price">
<div className="d-inline-flex align-content-center">
{`${currency} ${formatMoney(option.price)}`}
<button onClick={() => updateOptions(crate_id, option.disable_patch)}>
<img src="/images/shop/icon-remove.svg" className="d-block"/>
</button>
<div style={{'width': '45px', 'height': '20px'}} className="d-inline"></div>
</div>
</td>
</tr>
));
}

View File

@ -1,29 +0,0 @@
import {range} from "./utils";
import React from "react";
import {useShopStore} from "./shop_store";
import {SummaryCrate} from "./SummaryCrate";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
import {SummaryOrderPricedOptions} from "./SummaryOrderPricedOptions";
import {SummaryOrderShipping} from "./SummaryOrderShipping";
export function SummaryCrates() {
// #!render_count
const renderCount = useRenderCount();
const crates_l = useShopStore((state) => state.crates.length);
// #!render_count
console.log("SummaryCrates renders: ", renderCount)
return (
<>
{range(0, crates_l).map((index, _i) => {
return <SummaryCrate crate_index={index} key={"summary_crate_body_" + index} />
})}
<SummaryOrderPricedOptions/>
<SummaryOrderShipping/>
</>
)
}

View File

@ -1,44 +0,0 @@
import React from 'react';
import {SummaryCrates} from "./SummaryCrates";
import {SummaryTotalPrice} from "./SummaryTotalPrice";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
/**
* Components that displays the list of card that are used in the crate.
* It is a summary of purchase
*/
export function SummaryOrder() {
// #!render_count
const renderCount = useRenderCount();
// #!render_count
console.log("SummaryOrder renders: ", renderCount)
return (
<div className="summary-price">
<table>
<SummaryCrates/>
<tfoot>
<tr>
<td className="item-card-name">Price estimate</td>
<td className="price">
<SummaryTotalPrice/>
<span style={{
'display': 'inline-block',
'width': '30px',
}}>&nbsp;</span>
</td>
</tr>
</tfoot>
</table>
</div>
);
}

View File

@ -1,43 +0,0 @@
import {formatMoney} from "./utils";
import React from "react";
import {useShopStore} from "./shop_store";
import {ProcessOptionsToData} from "./options/Options";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
export function SummaryOrderPricedOptions() {
// #!render_count
const renderCount = useRenderCount();
const currency = useShopStore((state) => state.currency);
const optionsPrices = useShopStore((state) => state.order_prices);
const updateOptions = useShopStore((state) => state.updateOrderOptions);
const options_data = useShopStore((state) => state.order_options_data);
const options = ProcessOptionsToData({options: optionsPrices, data: options_data});
// #!render_count
console.log("SummaryOrderPricedOptions renders: ", renderCount)
return <tbody key={"summary_order_body"}>
{options.map((option, _i) => (
<tr key={"summary_order" + "option_" + option.id}>
<td className="item-card-name">
<div>{option.title}</div>
</td>
<td className="price">
<div className="d-inline-flex align-content-center">
{`${currency} ${formatMoney(option.price)}`}
<button onClick={() => updateOptions(option.disable_patch)}>
<img src="/images/shop/icon-remove.svg" className="d-block"/>
</button>
<div style={{'width': '45px', 'height': '20px'}} className="d-inline"></div>
</div>
</td>
</tr>
))}
</tbody>;
}

View File

@ -1,35 +0,0 @@
import {formatMoney} from "./utils";
import React from "react";
import {useShopStore} from "./shop_store";
import {ProcessOptions, ProcessOptionsToData} from "./options/Options";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
export function SummaryOrderShipping() {
// #!render_count
const renderCount = useRenderCount();
const shipping_summary = useShopStore((state) => state.shipping_summary);
const options_data = useShopStore((state) => state.order_options_data);
// #!render_count
console.log("SummaryOrderShipping renders: ", renderCount)
const options = ProcessOptions({
options: shipping_summary,
data: options_data,
id: "shipping_options",
target: null,
});
return <tbody key="summary_shipping_order_body">
{options.map((option, i) => (
<tr key={"summary_shipping_order_option_" + i} id={"summary_shipping_order_option_" + i}>
<td className="item-card-name" key={"summary_shipping_order_key_option_" + i} id={"summary_shipping_order_key_option_" + i}>
{option}
</td>
</tr>
))}
</tbody>;
}

View File

@ -1,17 +0,0 @@
import {useShopStore} from "./shop_store";
import {formatMoney} from "./utils";
import React from "react";
export function SummaryTotalPrice() {
const currency = useShopStore((state) => state.currency);
const total_price = useShopStore((state) => state.total_order_price);
return (
<div>
{currency} {formatMoney(total_price)}
<button style={{'opacity': '0', 'cursor': 'initial'}}>
<img src="/images/shop/icon-remove.svg" alt="icon remove"/>
</button>
</div>
)
}

View File

@ -1,82 +0,0 @@
const count_item_occupied_eem = (item) => {
if (!item.options_data
|| item.options_data.ext_pwr === false
|| item.options_data.mono_eem === false
)
return (item.consumes && item.consumes.eem) || 0;
else if (item.options_data.ext_pwr === true)
return 0;
else if (item.options_data.mono_eem === true || item.options_data.n_eem === "1 EEM")
return 1;
else if (item.options_data.n_eem === "3 EEM")
return 3;
return (item.consumes && item.consumes.eem) || 0;
}
const count_item_occupied_clock = (item) => {
return (item.options_data && (item.options_data.ext_clk === true || (item.options_data.ext_clk && item.options_data.ext_clk.checked === true)) ) ? 0 : ((item.consumes && item.consumes.clk) || 0);
}
const count_item_occupied_idc = (item) => {
return (item.consumes && item.consumes.idc) || 0;
}
const count_item_occupied_hp = (item) => {
return (item.consumes && item.consumes.hp) || 0;
}
const count_item_occupied_tec = (item) => {
return (item.options_data && item.options_data.tec === false) ? 0 : ((item.consumes && item.consumes.tec) || 0);
}
export const item_occupied_counters = {
"eem": count_item_occupied_eem,
"clk": count_item_occupied_clock,
"idc": count_item_occupied_idc,
"tec": count_item_occupied_tec,
"hp": count_item_occupied_hp,
}
function CounterFactory(name) {
return (data, index) => {
let count = 0;
for (let i = index + 1; i < data.length; i++) {
count += item_occupied_counters[name](data[i]);
if (data[i].resources && !!data[i].resources.find((value, _i) => value.name === name)) break;
}
return count;
}
}
export const resource_counters = {
"eem": CounterFactory("eem"),
"clk": CounterFactory("clk"),
"idc": CounterFactory("idc"),
"hp": CounterFactory("hp"),
"tec": CounterFactory("tec"),
}
export function CountResources(data, index) {
if (!data[index].resources) return null;
let result = [];
data[index].resources.forEach((item, _) => {
if (resource_counters[item.name]) result.push({
name: item.name,
occupied: resource_counters[item.name](data, index),
max: item.max
});
});
return result;
}
export function FillResources(data, disabled) {
return data.map((element, index) => {
element.counted_resources = disabled ? [] : CountResources(data, index);
return element;
})
}
export function hp_to_slots(hp) {
return Math.trunc(hp / 4);
}

View File

@ -1,80 +0,0 @@
import {useShopStore} from "./shop_store";
import {FilterOptions} from "./options/utils";
import {v4 as uuidv4} from "uuid";
export function validateJSON(description) {
let crates_raw;
let order_options;
try {
const parsed = JSON.parse(description);
// here we can check additional fields
crates_raw = parsed.crates;
order_options = parsed.options;
} catch (e) {
return false;
}
if (!order_options) return false;
const crate_modes = useShopStore.getState().crate_modes;
const modes_order = useShopStore.getState().modes_order;
const pn_to_card = useShopStore.getState().pn_to_cards;
try {
for (const crate of crates_raw) {
if (!crate.type || !crate.items || !crate.options || !(crate.type in crate_modes)) return false;
for (const card of crate.items) {
if (!(card.pn in pn_to_card) || card.options === undefined) return false;
}
}
} catch (e) {
return false;
}
// only last one should be spare cards
return crates_raw.filter((crate) => !modes_order.includes(crate.type)).length === 1 && crates_raw[crates_raw.length - 1].type === "no_crate";
}
// no validation in this function
export function JSONToCrates(description) {
const parsed = JSON.parse(description);
const crates_raw = parsed.crates;
const pn_to_card = useShopStore.getState().getCardDescriptionByPn;
const crates = Array.from(crates_raw.map((crate, c_i) => ({
id: crate.type === "no_crate" ? "spare" : "crate" + c_i,
name: crate.type === "no_crate" ? "Spare cards" : undefined,
options_data: crate.options,
crate_mode: crate.type,
items: Array.from(crate.items.map((card, _i) => ({
...pn_to_card(card.pn),
id: uuidv4(),
options_data: card.options || {}
}))),
warnings: [],
occupiedHP: 0,
})));
return {
// some additional fields go here
order_options_data: parsed.options,
crates: crates
};
}
export function CratesToJSON(crates) {
const crateOptions = useShopStore.getState().crate_options;
const orderOptions = useShopStore.getState().order_options;
const orderOptionsData = useShopStore.getState().order_options_data;
return JSON.stringify({
// additional fields can go here
crates: Array.from(crates.map((crate, _i) => ({
items: Array.from(crate.items.map((card, _) => ({
pn: card.name_number,
options: (card.options_data && card.options) ? FilterOptions(card.options, card.options_data) : null
}))),
type: crate.crate_mode,
options: FilterOptions(crateOptions, crate.options_data)
}))),
options: FilterOptions(orderOptions, orderOptionsData)
}, null, 2)
}

View File

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

View File

@ -1,21 +0,0 @@
import {OverlayTrigger, Tooltip} from "react-bootstrap";
import React from "react";
export function Notification({id, tip, content, sideMenuIsOpen, show, onHide}) {
return (
<OverlayTrigger
placement="top"
trigger={["click", "hover"]}
style={{display: 'inline'}}
show={show}
overlay={props => <Tooltip id={id} {...props}>{tip}</Tooltip>}
rootClose={!sideMenuIsOpen}
onToggle={onHide}
popperConfig={{
strategy: 'fixed'
}}
>
{content}
</OverlayTrigger>
)
}

View File

@ -1,63 +0,0 @@
import React from "react";
import {apply as json_logic_apply} from "json-logic-js";
import {componentsList} from "./components/components";
import {true_type_of} from "./utils";
export function ProcessOptions({options, data, target, id}) {
let options_t = true_type_of(options);
if (options_t === "array") {
return Array.from(
options.map((option_item, i) => ProcessOptions({
options: option_item,
data: data,
target: target,
id: id + i
}))
);
} else if (options_t === "object") {
if (
true_type_of(options.type) === "string" &&
(true_type_of(options.args) === "object" || true_type_of(options.items) === "array")
) {
if (options.type in componentsList) {
return componentsList[options.type](target, id + options.type, data, options.args);
} else if (options.type === "Group") {
return (
<div className="border rounded options-group" key={id + "group"}>
{ProcessOptions({
options: json_logic_apply(options.items, data),
data: data,
target: target,
id: id
})}
</div>);
} else {
return componentsList["Default"](options.type, id + "missing");
}
} else {
return ProcessOptions({options: json_logic_apply(options, data), data: data, target: target, id: id});
}
}
}
export function ProcessOptionsToData({options, data}) {
let options_t = true_type_of(options);
if (options_t === "array") {
return Array.from(
options.map((option_item, _i) => ProcessOptionsToData({
options: option_item,
data: data,
}))
).filter((item, _i) => !!item).flat();
} else if (options_t === "object") {
if (true_type_of(options.title) === "string") {
return options;
} else {
return ProcessOptionsToData({options: json_logic_apply(options, data), data: data});
}
} else {
//throw Error("Incompatible type for the option: " + options_t)
return null;
}
}

View File

@ -1,95 +0,0 @@
import React, {useEffect, useState} from "react";
import {useClickAway} from "./useClickAway";
import {FilterOptions, true_type_of} from "./utils";
export function SummaryPopup({id, options, data}) {
const [show, setShow] = useState(false);
const [position, setPosition] = useState({x: 0, y: 0});
const [size, setSize] = useState({w: 0, h: 0});
let display_options = FilterOptions(options, data);
const close = () => {
setShow(false);
document.removeEventListener("scroll", handleScroll, true);
}
const ref = useClickAway(close);
const reposition = () => {
let popup_button = document.getElementById(id + "img");
if (!popup_button) {
document.removeEventListener("scroll", handleScroll, true);
return;
}
let rect = popup_button.getBoundingClientRect()
let pos_x = (rect.left + rect.right) / 2;
let pos_y = (rect.top + rect.bottom) / 2;
if (pos_x + size.w > window.innerWidth) {
setPosition({x: pos_x - size.w - 20, y: pos_y - size.h / 2});
} else {
setPosition({x: pos_x - size.w / 2, y: pos_y - size.h - 20});
}
}
const handleScroll = (e) => {
if (e.target !== document.getElementById(id)) {
close();
}
}
useEffect(() => {
if (show) {
let popup = document.getElementById(id);
let width = popup.offsetWidth;
let height = popup.offsetHeight;
setSize({w: width, h: height});
reposition()
}
}, [show])
useEffect(() => {
if (show) {
reposition();
}
}, [show, size])
const handleClick = (_event) => {
setShow(!show);
if (!show) {
document.addEventListener("scroll", handleScroll, true);
}
};
const stringify = (value) => {
let value_type = true_type_of(value);
if (value_type === "string") {
return value;
} else if (value_type === "object") {
if (value.checked === false) {
return "off";
} else if (value.checked === true && value.text) {
return value.text;
}
}
return JSON.stringify(value);
}
return (
<div ref={ref}>
<img className="alert-info d-block" src={show ? "/images/shop/icon-close.svg" : "/images/shop/icon-customize.svg"}
id={id + "img"}
onClick={handleClick}/>
<div style={{'display': show ? 'flex' : 'none', 'top': position.y, 'left': position.x}}
className="overlayVariant card border rounded"
id={id}>
<div className="card-body">
{Array.from(Object.entries(display_options)
.filter(([key, value], _) => key !== "ext_data")
.map(([key, value], _) => {
return (<p className="card-text" key={id + key}><i>{key}</i>: {stringify(value)}</p>);
}))}
</div>
</div>
</div>
);
}

View File

@ -1,17 +0,0 @@
import React from "react";
import {apply as json_logic_apply, add_operation as json_add_operation} from "json-logic-js";
json_add_operation("lower", (some_str) => some_str && some_str.toLowerCase(some_str));
json_add_operation("upper", (some_str) => some_str && some_str.toUpperCase(some_str));
json_add_operation("capitalize", (some_str) => some_str && some_str.capitalizeFirstLetter(some_str));
export function Label(target, id, data, {content}) {
const resulting_string = json_logic_apply(content, data).flat().join("");
return (
<div id={id+"label"} key={id+"label"} className="options-label">
{resulting_string}
</div>
)
}

View File

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

View File

@ -1,71 +0,0 @@
import React, {Component} from "react";
import {Tip} from "./Tip";
class Radio extends Component {
constructor(props) {
super(props);
// Initialize the state object with the initial values from the props
this.state = {
variant: props.outvar in props.data ? props.data[props.outvar] : props.variants[props.fallback ? props.fallback : 0],
};
// Bind the event handler to this
this.handleClick = this.handleClick.bind(this);
this.props.target.construct(this.props.outvar, this.state.variant);
}
handleClick(variant) {
// Update the state object with the new value for outvar
this.setState({
...this.state,
variant: variant
});
this.props.target.update(this.props.outvar, variant);
}
static getDerivedStateFromProps(props, current_state) {
if (current_state.variant !== props.data[props.outvar]) {
return {
variant: props.data[props.outvar]
}
}
return null
}
render() {
let key = this.props.id + this.props.outvar;
return (
<div className="shop-radio" key={this.props.id}>
<div style={{"display": "inline"}} className="shop-radio-label">
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
{this.props.title}
</div>
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
<div className="d-block">
{this.props.variants.map((variant, _) => (
<div className={`form-check shop-radio-variant ${this.props.classes}`} key={key + variant}>
<input
className="form-check-input"
type="radio"
name={key}
id={key + variant}
checked={this.state.variant === variant}
onClick={() => this.handleClick(variant)}
onChange={() => this.handleClick(variant)}
/>
<label className="form-check-label" htmlFor={key + variant}>
{variant}
</label>
</div>
))}
</div>
</div>
);
}
}
export function RadioWrapper(target, id, data, {title, variants, outvar, fallback, icon, tip, classes}) {
return <Radio target={target} title={title} variants={variants} outvar={outvar} icon={icon} tip={tip} key={id}
fallback={fallback} classes={classes}
id={id} data={data}/>;
}

View File

@ -1,63 +0,0 @@
import React, {Component} from "react";
import {Tip} from "./Tip";
class Switch extends Component {
constructor(props) {
super(props);
// Initialize the state object with the initial values from the props
this.state = {
checked: props.outvar in props.data ? !!(props.data[props.outvar]) : !!(props.fallback)
};
// Bind the event handler to this
this.handleClick = this.handleClick.bind(this);
this.props.target.construct(this.props.outvar, this.state.checked);
}
handleClick() {
// Update the state object with the new value for outvar
let new_checked = !this.state.checked;
this.setState({
checked: new_checked
});
this.props.target.update(this.props.outvar, new_checked);
}
static getDerivedStateFromProps(props, current_state) {
if (current_state.checked !== props.data[props.outvar]) {
return {
checked: props.data[props.outvar]
}
}
return null
}
render() {
let key = this.props.id + this.props.outvar;
return (
<div className="shop-switch" key={this.props.id}>
<div className="form-check form-switch" key={key}>
<input
className="form-check-input"
type="checkbox"
role="switch"
id={key}
checked={this.state.checked}
onClick={this.handleClick}
onChange={this.handleClick}
/>
<label className="form-check-label" htmlFor={key} style={{"display": "inline"}}>
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
{this.props.title}
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
</label>
</div>
</div>
);
}
}
export function SwitchWrapper(target, id, data, {title, fallback, outvar, icon, tip, classes}) {
return <Switch target={target} title={title} fallback={fallback} outvar={outvar} icon={icon} tip={tip} key={id}
id={id} data={data} classes={classes}/>;
}

Some files were not shown because too many files have changed in this diff Show More