Refactor, multiple crates and warnings fixes #113

Merged
sb10q merged 42 commits from esavkin/web2019:96-refactor into master 2024-01-23 16:48:54 +08:00
58 changed files with 3561 additions and 3681 deletions

View File

@ -18,8 +18,10 @@ Start:
zola serve
```
To update the .bundle.js and .jsx file:
To build the .bundle.js from .jsx files:
```
nix-shell -p nodejs --run "npm run build"
nix-shell -p nodejs
npm install
npm run build
```

295
package-lock.json generated
View File

@ -12,8 +12,8 @@
"@babel/core": "^7.23.2",
"@babel/preset-env": "^7.23.2",
"@babel/preset-react": "^7.22.15",
"@hello-pangea/dnd": "^16.5.0",
"@uidotdev/usehooks": "^2.4.1",
"axios": "^1.6.0",
"babel-loader": "^9.1.3",
"babel-preset-minify": "^0.5.2",
"bootstrap": "^5.3.0",
@ -21,12 +21,13 @@
"json-logic-js": "^2.0.2",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-bootstrap": "^2.9.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",
"zustand": "^4.4.7"
}
},
"node_modules/@ampproject/remapping": {
@ -1785,12 +1786,12 @@
"dev": true
},
"node_modules/@babel/runtime": {
"version": "7.22.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.6.tgz",
"integrity": "sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==",
"version": "7.23.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz",
"integrity": "sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==",
"dev": true,
"dependencies": {
"regenerator-runtime": "^0.13.11"
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
@ -1854,6 +1855,76 @@
"node": ">=10.0.0"
}
},
"node_modules/@hello-pangea/dnd": {
"version": "16.5.0",
"resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-16.5.0.tgz",
"integrity": "sha512-n+am6O32jo/CFXciCysz83lPM3I3F58FJw4uS44TceieymcyxQSfzK5OhzPAKrVBZktmuOI6Zim9WABTMtXv4A==",
"dev": true,
"dependencies": {
"@babel/runtime": "^7.23.2",
"css-box-model": "^1.2.1",
"memoize-one": "^6.0.0",
"raf-schd": "^4.0.3",
"react-redux": "^8.1.3",
"redux": "^4.2.1",
"use-memo-one": "^1.1.3"
},
"peerDependencies": {
"react": "^16.8.5 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@hello-pangea/dnd/node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"dev": true
},
"node_modules/@hello-pangea/dnd/node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
"dev": true
},
"node_modules/@hello-pangea/dnd/node_modules/react-redux": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz",
"integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==",
"dev": true,
"dependencies": {
"@babel/runtime": "^7.12.1",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/use-sync-external-store": "^0.0.3",
"hoist-non-react-statics": "^3.3.2",
"react-is": "^18.0.0",
"use-sync-external-store": "^1.0.0"
},
"peerDependencies": {
"@types/react": "^16.8 || ^17.0 || ^18.0",
"@types/react-dom": "^16.8 || ^17.0 || ^18.0",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0",
"react-native": ">=0.59",
"redux": "^4 || ^5.0.0-beta.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
},
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
@ -2075,18 +2146,6 @@
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-redux": {
"version": "7.1.25",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz",
"integrity": "sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==",
"dev": true,
"dependencies": {
"@types/hoist-non-react-statics": "^3.3.0",
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0",
"redux": "^4.0.0"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.6",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz",
@ -2102,6 +2161,12 @@
"integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==",
"dev": true
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
"integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==",
"dev": true
},
"node_modules/@types/warning": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
@ -2415,23 +2480,6 @@
"node": ">= 8"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
},
"node_modules/axios": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
"integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-helper-evaluate-path": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/babel-helper-evaluate-path/-/babel-helper-evaluate-path-0.5.0.tgz",
@ -2944,18 +2992,6 @@
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@ -3042,15 +3078,6 @@
}
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@ -3252,40 +3279,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fs-readdir-recursive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
@ -3799,12 +3792,6 @@
"node": ">=6"
}
},
"node_modules/memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
"dev": true
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@ -4041,12 +4028,6 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true
},
"node_modules/punycode": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
@ -4083,25 +4064,6 @@
"node": ">=0.10.0"
}
},
"node_modules/react-beautiful-dnd": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz",
"integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==",
"dev": true,
"dependencies": {
"@babel/runtime": "^7.9.2",
"css-box-model": "^1.2.0",
"memoize-one": "^5.1.1",
"raf-schd": "^4.0.2",
"react-redux": "^7.2.0",
"redux": "^4.0.4",
"use-memo-one": "^1.1.1"
},
"peerDependencies": {
"react": "^16.8.5 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-bootstrap": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.9.1.tgz",
@ -4145,43 +4107,12 @@
"react": "^18.2.0"
}
},
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true
},
"node_modules/react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
"dev": true
},
"node_modules/react-redux": {
"version": "7.2.9",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
"integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
"dev": true,
"dependencies": {
"@babel/runtime": "^7.15.4",
"@types/react-redux": "^7.1.20",
"hoist-non-react-statics": "^3.3.2",
"loose-envify": "^1.4.0",
"prop-types": "^15.7.2",
"react-is": "^17.0.2"
},
"peerDependencies": {
"react": "^16.8.3 || ^17 || ^18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
@ -4251,9 +4182,9 @@
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"dev": true
},
"node_modules/regenerator-transform": {
@ -4385,9 +4316,9 @@
}
},
"node_modules/semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true,
"bin": {
"semver": "bin/semver"
@ -4740,6 +4671,15 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"dev": true,
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
@ -4889,6 +4829,15 @@
"node": ">=10.0.0"
}
},
"node_modules/webpack-preprocessor-loader": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/webpack-preprocessor-loader/-/webpack-preprocessor-loader-1.3.0.tgz",
"integrity": "sha512-wvHkDvgU9lhKQ1OWIJsawPBT/0wr+J7dwC7DHy0KtmXR/thGOAWbKEErGeJ2aXGSpwgqQTolIRoETlwMzocK1g==",
"dev": true,
"engines": {
"node": ">=6.11.5"
}
},
"node_modules/webpack-sources": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
@ -4991,6 +4940,34 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zustand": {
"version": "4.4.7",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.7.tgz",
"integrity": "sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==",
"dev": true,
"dependencies": {
"use-sync-external-store": "1.2.0"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
}
}
}

View File

@ -1,5 +1,6 @@
{
"name": "m-labs-zola",
"sideEffects": false,
"version": "1.0.0",
"description": "These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. For deployment, see the nix-scripts repository. Commits to https://git.m-labs.hk/M-Labs/web2019.git are automatically deployed to m-labs.hk through Hydra.",
"scripts": {
@ -23,13 +24,15 @@
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-bootstrap": "^2.9.1",
"react-beautiful-dnd": "^13.1.1",
"@hello-pangea/dnd": "^16.5.0",
"react-dom": "^18.2.0",
"uuid": "^9.0.1",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4",
"json-logic-js": "^2.0.2",
"@uidotdev/usehooks": "^2.4.1"
"zustand": "^4.4.7",
"@uidotdev/usehooks":"^2.4.1",
"webpack-preprocessor-loader": "^1.3.0"
},
"babel": {
"presets": [

View File

@ -149,7 +149,7 @@ a {
border: 1px solid $btn-primary-2 !important;
text-decoration: none;
&:hover {
&:hover, &:disabled {
background-color: $btn-secondary-2;
border: 1px solid $btn-secondary-2 !important;
}

View File

@ -8,50 +8,61 @@ button {
outline: none!important;
}
#root-shop {
.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;
.layout {
/*position: absolute;
width: 350px;*/
background: white;
/*left: calc(100%/2 - 350px/2);*/
.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;
/*top: calc(50% - 50px);*/
button {
background-color: inherit;
align-self: center;
border: 0;
position: absolute;
right: 10px;
top: 10px;
font-size: .9rem;
}
img {
width: 15px;
}
}
button {
background-color: inherit;
align-self: center;
border: 0;
position: absolute;
right: 10px;
top: 10px;
.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;
}
img {
width: 15px;
}
}
.btn.btn-primary.disabled {
background-color: gray;
}
.btn-outline-primary {
color: $btn-primary-2;
border-color: $btn-primary-2;
background-color: inherit;
&:hover {
background-color: inherit;
color: $btn-secondary-2;
border-color: $btn-secondary-2;
}
}
}
#root-shop {
.layout {
display: flex;
@ -186,37 +197,25 @@ button {
padding-bottom: .5rem;
}
.btn-outline-primary,
.btn-outline-primary:hover {
.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;
}
}
.control {
display: flex;
font-size: .8rem;
> p {
width: 50%;
> .description {
width: 80%;
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 {
@ -316,13 +315,19 @@ button {
button {
background-color: inherit;
border: 0;
margin-left: 20px;
margin-left: 16px;
img {
width: 20px;
height: auto;
}
}
span {
width: 28px;
}
.span-with-margin {
margin-left: 16px;
}
}
> .summary-form {
@ -347,7 +352,7 @@ button {
outline: none;
}
input[type="submit"],
.order-form-submit,
.btn-cla {
/*background-color: $btn-primary-2;*/
font-weight: 700;
@ -366,11 +371,14 @@ button {
border: 1px solid #e53e3e !important;
}
.btn-outline-primary,
.btn-outline-primary:hover {
.btn-outline-primary {
color: $btn-primary-2;
border-color: $btn-primary-2;
background-color: inherit;
&:hover {
background-color: inherit;
color: $btn-secondary-2;
}
}
}
}
@ -555,6 +563,43 @@ button {
.crate-info {
padding: 1rem 0 0;
}
.crate-bar {
width: 100%;
font-size: 0.9rem;
.crate-mode {
text-align: left;
width: 75%;
display: inline;
a {
cursor: pointer;
margin-right: 1rem;
color: inherit;
text-decoration: none;
padding-bottom: 5px;
display: inline-block;
}
a.active {
font-weight: 700;
border-bottom: 3px solid $btn-primary-2;
}
}
.delete-crate {
text-decoration: none;
cursor: pointer;
text-align: right;
display: inline-flex;
width: 25%;
color: inherit;
img {
margin-left: 1rem;
align-self: center;
}
}
}
}
}
@ -587,6 +632,37 @@ button {
margin-left: -10px;
}
#accordion_crates {
background-color: inherit;
.accordion_crates_item {
.accordion-header {
padding-bottom: 0;
}
.accordion-button {
background-color: inherit;
font-weight: bold;
&:hover {
background-color: $color-highlight;
}
}
}
#accordion_crates_add {
.accordion-header {
padding-bottom: 0;
}
.accordion-button {
font-weight: bold;
&:hover {
background-color: $color-highlight;
}
}
.accordion-button:after {
background-image: url("/images/shop/icon-add.svg");
}
}
}
}
.k-popup-connectors,
@ -595,17 +671,13 @@ button {
color: white;
font-weight: 700;
font-size: .6rem;
padding: .8rem 1rem;
padding: .5rem .8rem;
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);
text-align: left;
p {
margin-bottom: 0;
}
p + p {
padding-bottom: 8px;
}
}
.k-popup-connectors {

View File

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

View File

@ -11,6 +11,18 @@
.feedback-add-success {
display: none;
}
.feedback-add-failure {
background-color: #c75e5e;
display: block;
position: fixed;
top: 20px;
right: 20px;
padding: 1em;
z-index: 100000;
color: white;
border-radius: 10px;
box-shadow: 0 0 5px 3px;
}
#accordion_categories,
#accordion_categories .accordion-header,
@ -108,11 +120,11 @@
}
#root-shop .panel .control > .description,
#root-shop .panel .control > .crate-mode {
#root-shop .crate-mode {
width: 100%;
}
#root-shop .panel .control > .crate-mode {
#root-shop .crate-mode {
text-align: left;
}
@ -243,7 +255,7 @@
##Screen = B/w 481px to 767px
*/
@media (min-width: 481px) and (max-width: 767px) {
.feedback-add-success {
.feedback-add-success, .feedback-add-failure {
background-color: green;
display: block;
position: fixed;
@ -255,6 +267,9 @@
border-radius: 10px;
box-shadow: 0 0 5px 3px;
}
.feedback-add-failure {
background-color: #c75e5e;
}
.dropdown-item {
font-size: .75em;
@ -307,13 +322,16 @@
margin-bottom: 1.5rem;
}
#root-shop .panel .control > .description,
#root-shop .panel .control > .crate-mode {
#root-shop .panel .control > .description {
width: 100%;
}
#root-shop .panel .control > .crate-mode {
#root-shop .panel .crate .crate-bar .crate-mode {
text-align: left;
width: 50%;
}
#root-shop .panel .crate .crate-bar .crate-mode a {
display: block;
}
#root-shop .panel .summary {
@ -448,7 +466,7 @@
##Screen = B/w 320px to 479px
*/
@media (min-width: 320px) and (max-width: 480px) {
.feedback-add-success {
.feedback-add-success, .feedback-add-failure {
background-color: green;
display: block;
position: fixed;
@ -460,6 +478,9 @@
border-radius: 10px;
box-shadow: 0 0 5px 3px;
}
.feedback-add-failure {
background-color: #c75e5e;
}
.dropdown-item {
font-size: .75em;
@ -570,13 +591,16 @@
font-size: 1.5rem;
}
#root-shop .panel .control > .description,
#root-shop .panel .control > .crate-mode {
#root-shop .panel .control > .description {
width: 100%;
}
#root-shop .panel .control > .crate-mode {
#root-shop .panel .crate .crate-bar .crate-mode {
text-align: left;
width: 50%;
}
#root-shop .panel .crate .crate-bar .crate-mode a {
display: block;
}
#root-shop .panel .summary {

View File

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

After

Width:  |  Height:  |  Size: 463 B

View File

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

After

Width:  |  Height:  |  Size: 233 B

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

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

View File

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

73
static/js/shop/Cart.jsx Normal file
View File

@ -0,0 +1,73 @@
import React from 'react'
import {Droppable} from "@hello-pangea/dnd";
import {cartStyle, compareArraysWithIds} from "./utils";
import {ProductCartItem} from "./ProductCartItem";
import {FakePlaceholder} from "./FakePlaceholder";
import {FillExtData} from "./options/utils";
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) => {
const ext_data = FillExtData(crate.items, index);
return (
<ProductCartItem
card_index={index}
crate_index={crate_index}
ext_data={ext_data}
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>
);
}

50
static/js/shop/Crate.jsx Normal file
View File

@ -0,0 +1,50 @@
import React from 'react';
import {Cart} from "./Cart";
import {CrateMode} from "./CrateMode";
import {CrateWarnings} from "./CrateWarnings";
import {useShopStore} from "./shop_store";
// #!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} />
</div>
</div>
);
}

View File

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

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

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

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

@ -0,0 +1,101 @@
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"
},
{
"items": [],
"type": "no_crate"
}
]
});
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>
</>)
}

46
static/js/shop/Layout.jsx Normal file
View File

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

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

@ -0,0 +1,75 @@
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";
/**
* 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">
<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

@ -0,0 +1,56 @@
import React from 'react'
import {OrderSummary} from "./OrderSummary";
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";
/**
* 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">
<OrderSummary/>
<OrderForm/>
</section>
</section>);
}

View File

@ -0,0 +1,45 @@
import React from 'react';
import {useShopStore} from "./shop_store";
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 OrderSummary() {
// #!render_count
const renderCount = useRenderCount();
// #!render_count
console.log("OrderSummary 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

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

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

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

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

71
static/js/shop/Shop.jsx Normal file
View File

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

View File

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

@ -0,0 +1,30 @@
import {range} from "./utils";
import React from "react";
import {useShopStore} from "./shop_store";
import {SummaryCrateHeader} from "./SummaryCrateHeader";
import {SummaryCrateCard} from "./SummaryCrateCard";
// #!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} />
)}
</tbody>
)
}

View File

@ -0,0 +1,82 @@
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">
<span style={{
'display': 'inline-block',
'width': '16px',
}}>&nbsp;</span>
<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

@ -0,0 +1,54 @@
import {formatMoney} from "./utils";
import React from "react";
import {useShopStore} from "./shop_store";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
import {CrateMode} from "./CrateMode";
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

@ -0,0 +1,25 @@
import {range} from "./utils";
import React from "react";
import {useShopStore} from "./shop_store";
import {SummaryCrate} from "./SummaryCrate";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
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} />
})}
</>
)
}

View File

@ -0,0 +1,17 @@
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.totalOrderPrice());
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

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

View File

@ -0,0 +1,70 @@
import {useShopStore} from "./shop_store";
import {FilterOptions} from "./options/utils";
import {v4 as uuidv4} from "uuid";
export function validateJSON(description) {
let crates_raw;
try {
const parsed = JSON.parse(description);
// here we can check additional fields
crates_raw = parsed.crates;
} catch (e) {
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.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,
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
crates: crates
};
}
export function CratesToJSON(crates) {
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
})))
}, null, 2)
}

View File

@ -0,0 +1,42 @@
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}) {
const [show, setShow] = useState(false);
const ref = useClickAway((e) => {
if (e.type === "mousedown") // ignore touchstart
setShow(false)
}
);
let div_classes = `overlayVariant border rounded ${big ? "overlay-bigcard" : "overlay-smallcard"} ${(!big && first) ? "overlay-first" : ""} ${(!big && last) ? "overlay-last" : ""} ${options_class || ""}`;
const handleClick = (_event) => {
setShow(!show);
};
return (
<div ref={ref}>
<Notification
id={"processed_options_notification" + id}
tip="Customization options available"
sideMenuIsOpen={sideMenuIsOpen}
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

@ -0,0 +1,30 @@
import {OverlayTrigger, Tooltip} from "react-bootstrap";
import React, {useEffect, useState} from "react";
export function Notification({id, tip, content, sideMenuIsOpen}) {
const [show, setShow] = useState(false);
useEffect(() => {
setTimeout(() => {
setShow(true)
}, 100);
setTimeout(() => {
setShow(false)
}, 5000);
}, []);
return (
<OverlayTrigger
placement="top"
trigger={["click", "hover"]}
style={{display: 'inline'}}
show={show}
onToggle={() => setShow(false)}
overlay={<Tooltip id={id}>{tip}</Tooltip>}
rootClose={!sideMenuIsOpen}
>
{content}
</OverlayTrigger>
)
}

View File

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

View File

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

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

View File

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

View File

@ -0,0 +1,54 @@
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);
}
render() {
let key = this.props.id + this.props.outvar;
return (
<div className="shop-switch" key={this.props.id}>
<div className="form-check form-switch" key={key}>
<input
className="form-check-input"
type="checkbox"
role="switch"
id={key}
checked={this.state.checked}
onClick={this.handleClick}
onChange={this.handleClick}
/>
<label className="form-check-label" htmlFor={key} style={{"display": "inline"}}>
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
{this.props.title}
</label>
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
</div>
</div>
);
}
}
export function SwitchWrapper(target, id, data, {title, fallback, outvar, icon, tip}) {
return <Switch target={target} title={title} fallback={fallback} outvar={outvar} icon={icon} tip={tip} key={id}
id={id} data={data}/>;
}

View File

@ -0,0 +1,67 @@
import React, {Component} from "react";
import {Tip} from "./Tip";
class SwitchLine extends Component {
constructor(props) {
super(props);
// Initialize the state object with the initial values from the props
this.state = {
text: props.outvar in props.data ? props.data[props.outvar].text : (props.fallback ? props.fallback.text : ""),
checked: props.outvar in props.data ? props.data[props.outvar].checked : (props.fallback ? props.fallback.checked : false)
};
// Bind the event handler to this
this.handleText = this.handleText.bind(this);
this.handleCheck = this.handleCheck.bind(this);
this.props.target.construct(this.props.outvar, this.state);
}
handleText(element) {
let new_state = {
...this.state,
text: element.target.value
}
this.setState(new_state);
this.props.target.update(this.props.outvar, new_state);
}
handleCheck() {
// Update the state object with the new value for outvar
let new_state = {
...this.state,
checked: !this.state.checked
}
this.setState(new_state);
this.props.target.update(this.props.outvar, new_state);
}
render() {
let key = this.props.id + this.props.outvar;
return (
<div className="shop-switch-line" key={this.props.id}>
<div className="form-check form-switch" key={key}>
<input
className="form-check-input"
type="checkbox"
role="switch"
id={key + "switch"}
checked={this.state.checked}
onClick={this.handleCheck}
onChange={this.handleCheck}
/>
<label className="form-check-label" htmlFor={key + "switch"}>
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
{this.props.title}
</label>
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
</div>
<input type="text" className="form-control form-control-sm" id={key + "line"} onChange={this.handleText}
value={this.state.text} disabled={!this.state.checked}/>
</div>
);
}
}
export function SwitchLineWrapper(target, id, data, {title, fallback, outvar, icon, tip}) {
return <SwitchLine target={target} title={title} fallback={fallback} outvar={outvar} icon={icon} tip={tip} key={id}
id={id} data={data}/>;
}

View File

@ -0,0 +1,16 @@
import React from "react";
import {OverlayTrigger, Tooltip} from "react-bootstrap";
export function Tip({id, tip}) {
return (
<OverlayTrigger
placement="auto"
trigger={['click', 'hover', 'focus']}
style={{display: 'inline'}}
overlay={<Tooltip id={id}>{tip}</Tooltip>}
rootClose
>
<img src={`/images/shop/icon-reminder.svg`} className="options-icon"/>
</OverlayTrigger>
);
}

View File

@ -0,0 +1,6 @@
import React from "react";
export function UnimplementedComponent(type, id) {
//console.error("Missing component with type:", type)
return <div key={type + id} style={{background: "red"}}>UNIMPLEMENTED</div>
}

View File

@ -0,0 +1,17 @@
'use strict'
import {LineWrapper} from "./Line";
import {RadioWrapper} from "./Radio";
import {SwitchWrapper} from "./Switch";
import {SwitchLineWrapper} from "./SwitchLine";
import {UnimplementedComponent} from "./UnimplementedComponent";
// Class components are used because we cannot use hooks for updating the state
export const componentsList = {
"Radio": RadioWrapper,
"Switch": SwitchWrapper,
"Line": LineWrapper,
"SwitchLine": SwitchLineWrapper,
"Default": UnimplementedComponent,
};

View File

@ -0,0 +1,31 @@
import React from "react";
// copy from "@uidotdev/usehooks"
export function useClickAway(cb) {
const ref = React.useRef(null);
const refCb = React.useRef(cb);
React.useLayoutEffect(() => {
refCb.current = cb;
});
React.useEffect(() => {
const handler = (e) => {
const element = ref.current;
if (element && !element.contains(e.target)) {
refCb.current(e);
}
};
document.addEventListener("mousedown", handler);
document.addEventListener("touchstart", handler);
return () => {
document.removeEventListener("mousedown", handler);
document.removeEventListener("touchstart", handler);
};
}, []);
return ref;
}

View File

@ -0,0 +1,38 @@
import {apply as json_logic_apply} from "json-logic-js";
import {componentsList} from "./components/components";
// https://stackoverflow.com/a/70511311
export const true_type_of = (obj) => Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
export function FillExtData(data, index) {
return {
has_other_dio: data.filter((value, item_index) => index !== item_index && value.name &&value.name.endsWith("-TTL")).length > 0,
has_dds: data.filter(((value, _) => value.name === "DDS" && value.name_number === "4410" && (!value.options_data || !value.options_data.mono_eem))).length > 0,
has_sampler: data.filter(((value, _) => value.name === "Sampler" && (!value.options_data || !value.options_data.mono_eem))).length > 0,
}
}
export function FilterOptions(options, data) {
let options_t = true_type_of(options);
let target = {};
if (options_t === "array") {
options.map((option_item, _) => {
Object.assign(target, FilterOptions(option_item, data))
});
} else if (options_t === "object") {
if (
true_type_of(options.type) === "string" &&
(true_type_of(options.args) === "object" || true_type_of(options.items) === "array")
) {
if (options.type in componentsList) {
target[options.args.outvar] = data[options.args.outvar];
} else if (options.type === "Group") {
Object.assign(target, FilterOptions(json_logic_apply(options.items, data), data))
}
} else {
Object.assign(target, FilterOptions(json_logic_apply(options, data), data))
}
}
return target
}

View File

@ -0,0 +1,474 @@
'use strict';
import {createWithEqualityFn} from "zustand/traditional";
import {data as shared_data, itemsUnfoldedList} from "./utils";
import {true_type_of} from "./options/utils";
import {v4 as uuidv4} from "uuid";
import {FillResources} from "./count_resources";
import {FillExtData} from "./options/utils";
import {TriggerCrateWarnings, TriggerWarnings} from "./warnings";
import {Validation, validateEmail, validateNote, validateJSONInput} from "./validate";
import {CratesToJSON, JSONToCrates} from "./json_porter";
const cards_to_pn_map = (cards) => {
let result = {};
Object.entries(cards).forEach(([key, card], _i) => { result[card.name_number] = key})
return result;
};
const useBacklog = ((set, get) => ({
cards: shared_data.items,
groups: shared_data.columns.backlog,
cards_list: itemsUnfoldedList,
currency: shared_data.currency,
pn_to_cards: cards_to_pn_map(shared_data.items),
getCardDescription: index => get().cards[get().cards_list[index]],
getCardDescriptionByPn: pn => get().cards[get().pn_to_cards[pn]],
cardIndexById: card_id => get().cards_list.findIndex((element) => (card_id === element))
}));
const useCrateModes = ((set, get) => ({
crate_modes: shared_data.crateModes,
modes_order: shared_data.crateModeOrder,
crateParams: mode => get().crate_modes[mode],
}));
const useLayout = ((set, get) => ({
isTouch: window.isTouchEnabled(),
isMobile: window.deviceIsMobile(),
sideMenuIsOpen: false,
showCardAddedFeedback: false,
showNoDestination: false,
timerAdded: null,
switchSideMenu: () => set(state => ({
sideMenuIsOpen: !state.sideMenuIsOpen
})),
cardAdded: () => set(state => ({
showCardAddedFeedback: true,
showNoDestination: false,
timerAdded: (!!state.timerAdded ? clearTimeout(state.timerAdded) : null) || (state.isMobile && setTimeout(() => {
get()._endCardAdded()
}, 2000))
})),
noDestinationWarning: () => set(state => ({
showCardAddedFeedback: true,
showNoDestination: true,
timerAdded: (!!state.timerAdded ? clearTimeout(state.timerAdded) : null) || (setTimeout(() => {
get()._endCardAdded()
}, 2000))
})),
_endCardAdded: () => set(state => ({
showCardAddedFeedback: false,
timerAdded: !!state.timerAdded ? clearTimeout(state.timerAdded) : null
}))
}));
const useImportJSON = ((set, get) => ({
importShouldOpen: false,
importValue: {
value: "",
error: Validation.OK
},
openImport: () => set(state => ({
importShouldOpen: true
})),
closeImport: () => set(state => ({
importShouldOpen: false
})),
_loadDescription: () => set(state => {
const parsed = JSONToCrates(state.importValue.value);
// if (parsed.crates[-1].crate_mode !== "")
return {
importShouldOpen: false,
// additional fields go here
crates: parsed.crates
}}),
loadDescription: () => {
get()._loadDescription()
get().crates.forEach((crate, _i) => {
get().fillExtData(crate.id)
get().fillWarnings(crate.id)
})
},
updateImportDescription: (new_description) => set(state => ({
importValue: {
value: new_description,
error: validateJSONInput(new_description)
}
}))
}));
const useSubmitForm = ((set, get) => ({
isProcessing: false,
shouldShowRFQFeedback: false,
processingResult: {
status: Validation.OK,
message: ""
},
API_RFQ: shared_data.API_RFQ,
email: {
value: "",
error: null
},
note: {
value: "",
error: Validation.OK
},
description: "",
shouldShowDescription: false,
updateEmail: (new_email) => set(state => ({
email: {
value: new_email,
error: validateEmail(new_email)
}
})),
updateNote: (new_notes) => set(state => ({
note: {
value: new_notes,
error: validateNote(new_notes)
}
})),
resetEmailValidation: () => set(state => ({
email: {
value: state.email.value,
error: Validation.OK
}
})),
_revalidateForm: () => set(state => ({
email: {
value: state.email.value,
error: validateEmail(state.email.value)
},
note: {
value: state.note.value,
error: validateEmail(state.note.value)
},
})),
updateDescription: () => set(state => ({
description: CratesToJSON(state.crates)
})),
showDescription: () => set(state => ({
description: CratesToJSON(state.crates),
shouldShowDescription: true
})),
closeDescription: () => set(state => ({
shouldShowDescription: false
})),
_submitForm: () => set(state => ({isProcessing: true})),
finishSubmitForm: (result) => set(state => ({
isProcessing: false,
shouldShowRFQFeedback: true,
processingResult: result})),
submitDisabled: () => (get().email.error !== Validation.OK),
submitForm: () => {
get().updateDescription();
get()._revalidateForm();
get()._submitForm();
if (get().submitDisabled()) return;
fetch(get().API_RFQ, {
method: "POST",
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
email: get().email.value,
note: get().note.value,
configuration: get().description
})
}).then(response => {
if (response.status !== 200) {
throw Error("Response status is not OK: " + response.status + ".\n" + response);
}
get().finishSubmitForm({status: Validation.OK, message: "We've received your request and will be in contact soon."})
}, reason => {
console.error("Request rejected, reason:", reason)
get().finishSubmitForm({
status: Validation.Invalid,
message: "We cannot receive your request. Try using the export by coping the configuration and send it to us at sales@m-labs.hk"
})
}).catch(err => {
console.error("Request failed, reason:", err)
get().finishSubmitForm({
status: Validation.Invalid,
message: "We cannot receive your request. Try using the export by coping the configuration and send it to us at sales@m-labs.hk"
})
})
},
closeRFQFeedback: () => set(state => ({shouldShowRFQFeedback: false}))
}));
const useHighlighted = ((set, get) => ({
highlighted: {
crate: "",
card: 0
},
highlightedTimer: null,
// #!if disable_card_highlight === false
highlightCard: (crate_id, index) => set(state => ({
highlighted: {
crate: crate_id,
card: index
},
highlightedTimer: (!!state.highlightedTimer ? clearTimeout(state.highlightedTimer) : null) || (state.isTouch && setTimeout(() => {
get().highlightReset()
}, 2000))
})),
highlightReset: () => set(state => ({
highlighted: {
crate: "",
card: 0
},
highlightedTimer: !!state.highlightedTimer ? clearTimeout(state.highlightedTimer) : null
})),
// #!else
highlightCard: () => {return null;},
highlightReset: () => {return null;},
// #!endif
}));
const useCart = ((set, get) => ({
crates: shared_data.columns.crates,
active_crate: "crate0",
_defaultCrates: Array.from(shared_data.columns.crates),
_newCrate: (crate_id) => set((state) => ({
crates: state.crates.toSpliced(-1, 0, {
...state._defaultCrates[0],
id: crate_id || "crate" + state.crates.length,
}),
active_crate: crate_id || "crate" + state.crates.length
})),
delCrate: (id) => set(state => ({
crates: state.crates.filter((crate => crate.id !== id || !state.modes_order.includes(crate.crate_mode))),
active_crate: state.active_crate === id ? null : state.active_crate,
})),
_setCrateMode: (id, mode) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate.id === id) {
return {
...crate,
crate_mode: mode
}
} else return crate;
})
})),
setActiveCrate: (id) => set(state => ({active_crate: id})),
_addCardFromBacklog: (crate_to, index_from, index_to) => set(state => {
const take_from = (true_type_of(index_from) === "array" ? index_from : [index_from]).map((item, _i) => (state.cards_list[item]));
const dest = crate_to || state.active_crate;
if (!dest) return {};
return {
crates: state.crates.map((crate, _i) => {
if (dest === crate.id) {
index_to = index_to != null ? index_to : crate.items.length;
return {
...crate,
items: crate.items.toSpliced(index_to, 0, ...take_from.map((card_name, _) => {
return {...state.cards[card_name], id: uuidv4()}
}))
}
} else return crate;
})
}
}),
_moveCard: (crate_from, index_from, crate_to, index_to) => set(state => {
const the_card = state.crates.find((crate, _) => crate_from === crate.id ).items[index_from];
return {
crates: state.crates.map((crate, _i) => {
if (crate_to === crate_from && crate_to === crate.id) {
let items_copy = Array.from(crate.items);
let item = items_copy.splice(index_from, 1)[0]
items_copy.splice(index_to, 0, item).filter((item, _) => !!item)
return {
...crate,
items: items_copy
}
} else if (crate_to === crate.id) {
return {
...crate,
items: crate.items.toSpliced(index_to, 0, the_card)
}
} else if (crate_from === crate.id) {
return {
...crate,
items: crate.items.toSpliced(index_to, 1)
}
}
else return crate;
})
}
}),
_deleteCard: (crate_id, index) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate_id === crate.id) {
return {
...crate,
items: crate.items.toSpliced(index, 1)
}
}
else return crate;
})
})),
_clearCrate: (id) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (id === crate.id) {
return {
...crate,
items: []
}
}
else return crate;
})
})),
clearAll: () => set(state => ({
crates: state._defaultCrates
})),
_updateOptions: (crate_id, index, new_options) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate_id === crate.id) {
let itemsCopy = Array.from(crate.items);
itemsCopy[index] = {
...itemsCopy[index],
options_data: {
...itemsCopy[index].options_data,
...new_options
}};
return {
...crate,
items: itemsCopy
}
}
else return crate;
})
})),
fillWarnings: (crate_id) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate_id === crate.id) {
//console.log("--- CHECK ALERTS ---")
let itemsCopy = Array.from(crate.items);
const disabled = !!get().crateParams(crate.crate_mode).warnings_disabled;
itemsCopy = FillResources(itemsCopy, disabled);
itemsCopy = TriggerWarnings(itemsCopy, disabled);
const [crate_warnings, occupied] = TriggerCrateWarnings(crate);
return {
...crate,
items: itemsCopy,
warnings: crate_warnings,
occupiedHP: occupied
}
}
else return crate;
})
})),
fillExtData: (crate_id) => set(state => ({
crates: state.crates.map((crate, _i) => {
if (crate_id === crate.id) {
let itemsCopy = Array.from(crate.items);
itemsCopy = itemsCopy.map((item, index) => {
if (!item.options) return item;
if (!item.options_data) item.options_data = {};
item.options_data.ext_data = FillExtData(itemsCopy, index);
return item;
});
return {
...crate,
items: Array.from(itemsCopy)
}
}
else return crate;
})
})),
totalOrderPrice: () => {
let sum = 0;
get().crates.forEach( (crate, _i) => {
sum += get().crate_modes[crate.crate_mode].price;
crate.items.forEach((item, _) => {
sum += item.price;
});
});
return sum;
},
// Composite actions that require warnings recalculation:
newCrate: () => {
const crate_id = "crate" + get().crates.length;
get()._newCrate(crate_id)
get().fillExtData(crate_id);
get().fillWarnings(crate_id);
},
setCrateMode: (id, mode) => {
get()._setCrateMode(id, mode)
get().fillExtData(id);
get().fillWarnings(id);
get().setActiveCrate(id);
},
addCardFromBacklog: (crate_to, index_from, index_to, just_mounted) => {
const dest = crate_to || get().active_crate;
if (!dest) {
console.warn("No destination");
get().noDestinationWarning();
return {};
}
get()._addCardFromBacklog(dest, index_from, index_to)
get().fillExtData(dest);
get().fillWarnings(dest);
get().setActiveCrate(dest);
if (!just_mounted) {
get().cardAdded()
}
},
moveCard: (crate_from, index_from, crate_to, index_to) => {
get()._moveCard(crate_from, index_from, crate_to, index_to);
get().fillExtData(crate_to);
get().fillWarnings(crate_to);
get().setActiveCrate(crate_to);
if (crate_from !== crate_to) {
get().fillExtData(crate_from);
get().fillWarnings(crate_from);
}
},
deleteCard: (crate_id, index) => {
get()._deleteCard(crate_id, index);
get().fillExtData(crate_id);
get().fillWarnings(crate_id);
if (crate_id === get().highlighted.crate && index === get().highlighted.card) get().highlightReset()
},
clearCrate: (id) => {
get()._clearCrate(id);
get().fillWarnings(id);
},
updateOptions: (crate_id, index, new_options) => {
get()._updateOptions(crate_id, index, new_options);
get().fillExtData(crate_id);
get().fillWarnings(crate_id);
}
}))
export const useShopStore = createWithEqualityFn((...params) => ({
...useBacklog(...params),
...useCrateModes(...params),
...useCart(...params),
...useSubmitForm(...params),
...useLayout(...params),
...useHighlighted(...params),
...useImportJSON(...params),
}))

94
static/js/shop/utils.js Normal file
View File

@ -0,0 +1,94 @@
'use strict';
export const data = window.shop_data;
export const itemsUnfoldedList = Array.from(data.columns.backlog.categories.map(groupId => groupId.itemIds).flat());
export const productStyle = (style, snapshot, removeAnim, hovered, selected, cart=false) => {
const custom = {
opacity: snapshot.isDragging ? .7 : 1,
backgroundColor: (hovered || selected) ? '#eae7f7' : 'initial',
};
if (!cart && snapshot.draggingOver == null && // hack for backlog
((!snapshot.isDragging) // prevent next elements from animation
|| (snapshot.isDragging && snapshot.isDropAnimating))) { // prevent dragged element from weird animation
style.transform = "none";
}
if (!snapshot.isDropAnimating) {
return { ...style, ...custom};
}
if (removeAnim) {
// cannot be 0, but make it super tiny
custom.transitionDuration = '0.001s';
}
return {
...style,
...custom,
};
}
export const cartStyle = (style, snapshot) => {
const isDraggingOver = snapshot.isDraggingOver;
return {
...style,
...{
backgroundColor: isDraggingOver ? '#f2f2f2' : '#f9f9f9',
border: isDraggingOver ? '1px dashed #ccc' : '0',
},
};
}
export function formatMoney(amount, decimalCount = 2, decimal = ".", thousands = ",") {
// https://stackoverflow.com/questions/149055/how-can-i-format-numbers-as-currency-string-in-javascript
// changes: return amount if error in order to avoid empty value
try {
decimalCount = Math.abs(decimalCount);
decimalCount = isNaN(decimalCount) ? 2 : decimalCount;
const negativeSign = amount < 0 ? "-" : "";
let i = parseInt(amount = Math.abs(Number(amount) || 0).toFixed(decimalCount)).toString();
let j = (i.length > 3) ? i.length % 3 : 0;
return negativeSign + (j ? i.substr(0, j) + thousands : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + thousands) + (decimalCount ? decimal + Math.abs(amount - i).toFixed(decimalCount).slice(2) : "");
} catch (e) {
return amount;
}
}
export const range = (start, end) => {
const length = end - start;
return Array.from({ length }, (_, i) => start + i);
}
export const move = (source, destination, droppableSource, droppableDestination) => {
console.log('==> move', source, destination);
const sourceClone = Array.from(source);
const destClone = Array.from(destination);
const [removed] = sourceClone.splice(droppableSource.index, 1);
destClone.splice(droppableDestination.index, 0, removed);
const result = {columns: {}};
result.columns[droppableSource.droppableId] = sourceClone;
result.columns[droppableDestination.droppableId] = destClone;
return result;
};
export const compareArraysWithIds = (a, b) =>
a.length === b.length &&
a.every((element, index) => element.id === b[index].id);
export const compareArraysLevelOne = (a, b) =>
a.length === b.length &&
a.every((element, index) => element === b[index]);
export function compareObjectsEmptiness(a, b) {
return (!a && !b) || (!(!a !== !b) && Object.getPrototypeOf(a) === Object.getPrototypeOf(b) &&
(Object.getPrototypeOf(a) !== Object.getPrototypeOf([]) || !!Object.keys(a).length === !!Object.keys(b).length))
}

View File

@ -0,0 +1,34 @@
import {validateJSON} from "./json_porter";
export const Validation = {
OK: 0,
Empty: 1,
Invalid: 2,
};
export function validateEmail(value) {
if (!value || value.trim() === '') {
return Validation.Empty
} else if (value && !value.match(/^\w+([\+\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/)) {
return Validation.Invalid;
}
return Validation.OK;
}
export function validateNote(value) {
if (!value || value.trim() === '') {
return Validation.Empty
}
return Validation.OK;
}
export function validateJSONInput(value) {
if (!value || value.trim() === '') {
return Validation.Empty
} else if (value && !(validateJSON(value))) {
return Validation.Invalid;
}
return Validation.OK;
}

156
static/js/shop/warnings.js Normal file
View File

@ -0,0 +1,156 @@
/**
* This module should contain warnings subsystem.
* First idea - there should be either definitive list of available warnings,
* or defined as some json logic leading to the warning message(s), similar way how FillExtData works.
* Second - resources indicator should be separate component
*/
import {item_occupied_counters, resource_counters} from "./count_resources";
import {useShopStore} from "./shop_store";
export const Levels = {
"reminder": {priority: 1, icon: '/images/shop/icon-reminder.svg', color: "#0d3547"},
"warning": {priority: 2, icon: '/images/shop/icon-warning.svg', color: "#c75e5e"},
}
const find_in_counters = (counters, name) => {
return counters.find((element) => element.name === name)
}
const find_previous_source = (data, index, source) => {
return data.slice(0, index).find((element) => {
return element.resources && find_in_counters(element.resources, source)
})
}
const find_next_source_index = (data, index, source) => {
return data.slice(index + 1).findIndex((element) => {
return element.resources && find_in_counters(element.resources, source)
}) + index + 1
}
const not_enough_resource_trigger = (name) => {
return (_data, _index, counters) => {
const resource = find_in_counters(counters, name);
return resource.occupied > resource.max;
}
}
const no_source_trigger = (name) => {
return (data, index, _counters) => {
const occupied = item_occupied_counters[name](data[index]);
if (occupied > 0)
return !find_previous_source(data, index, name);
return false;
}
}
const wiring_constraint = (name) => {
return (data, index, _counters) => {
const next = find_next_source_index(data, index, name);
return next - index === 1;
}
}
const Types = {
"eem_resource": {
level: "warning",
trigger: not_enough_resource_trigger("eem"),
message: "Insufficient EEM connectors"
},
"no_eem_source": {
level: "warning",
trigger: no_source_trigger("eem"),
message: 'This card needs a card that provides a EEM connector (e.g. Kasli) at its left.'
},
"idc_resource": {
level: "warning",
trigger: not_enough_resource_trigger("idc"),
message: "Insufficient IDC connectors."
},
"no_idc_source": {
level: "warning",
trigger: no_source_trigger("idc"),
message: 'Should be after a Zotino or a HD68-IDC or with another IDC-BNC.'
},
"clk_resource": {
level: "warning",
trigger: not_enough_resource_trigger("clk"),
message: "Insufficient clock connectors."
},
"no_clk_source": {
level: "warning",
trigger: no_source_trigger("clk"),
message: 'This card needs either a card that provides a clock source (e.g. Kasli or Clocker) at its left or use an external clock source.'
},
"eem_wiring_constraint": {
level: "reminder",
trigger: wiring_constraint("eem"),
message: "Due to wiring constraints, the carrier can only connect to EEM cards immediately at its right, without crossing another carrier."
},
"default": {
level: "warning",
trigger: (_a, _b, _c) => {
return true;
},
message: 'This item has unimplemented warning'
}
}
export function TriggerWarnings(data, disabled) {
return data.map((element, index) => {
if (!element.warnings) return element;
element.show_warnings = disabled ? [] :element.warnings
.map((warning, _) => {
if (!!Types[warning])
return Types[warning].trigger(data, index, element.counted_resources) ? {trigger: undefined, name: warning, ...Types[warning]} : null;
else
return Types.default;
})
.filter((warning, _) => {
return !!warning
});
return element;
})
}
export function MaxLevel(warnings) {
let mx = {priority: 0, icon: null};
for (const warning of warnings) {
if (Levels[warning.level].priority > mx.priority) mx = Levels[warning.level];
}
return mx;
}
export function LevelUI(warning_level) {
const warning_t = Levels[warning_level];
return {icon: warning_t.icon, color: warning_t.color};
}
const crate_warnings = {
"overfit": {
message: "You have reached the maximum number of slots allowed for this crate. Consider removing cards.",
level: "warning",
trigger: (crate, occupied) => {
const nbrHP = useShopStore.getState().crateParams(crate.crate_mode).hp;
return occupied > nbrHP && nbrHP > 0;
}
},
"underfit_rack": {
message: "The selected cards fit in a 42hp desktop crate, consider switching to it for a more compact system",
level: "reminder",
trigger: (crate, occupied) => {
const nbrHPDesktop = useShopStore.getState().crate_modes.desktop.hp;
return crate.crate_mode === useShopStore.getState().crate_modes.rack.id && occupied < nbrHPDesktop;
}
}
}
export function TriggerCrateWarnings(crate) {
const nbrOccupied = resource_counters.hp(crate.items, -1);
let warnings = [];
Object.entries(crate_warnings).forEach(([id, warning], _) => {
if (warning.trigger(crate, nbrOccupied)) warnings.push({...warning, id: id, trigger: undefined});
})
return [warnings, nbrOccupied];
}

View File

@ -1,450 +0,0 @@
'use strict';
import React, {Component} from "react";
import jsonLogic from 'json-logic-js';
import {useState, useEffect} from 'react';
import {useClickAway} from "@uidotdev/usehooks";
import {OverlayTrigger, Tooltip} from "react-bootstrap";
// https://stackoverflow.com/a/70511311
const true_type_of = (obj) => Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
function Tip({id, tip}) {
return (
<OverlayTrigger
placement="auto"
trigger={['click', 'hover', 'focus']}
style={{display: 'inline'}}
overlay={<Tooltip id={id}>{tip}</Tooltip>}
>
<img src={`/images/shop/icon-reminder.svg`} className="options-icon"/>
</OverlayTrigger>
);
}
class Radio extends Component {
constructor(props) {
super(props);
// Initialize the state object with the initial values from the props
this.state = {
variant: props.outvar in props.data ? props.data[props.outvar] : props.variants[props.fallback ? props.fallback : 0],
};
// Bind the event handler to this
this.handleClick = this.handleClick.bind(this);
this.props.target.construct(this.props.outvar, this.state.variant);
}
handleClick(variant) {
// Update the state object with the new value for outvar
this.setState({
...this.state,
variant: variant
});
this.props.target.update(this.props.outvar, variant);
}
render() {
let key = this.props.id + this.props.outvar;
return (
<div className="shop-radio" key={this.props.id}>
<div style={{"display": "inline"}}>
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
{this.props.title}
</div>
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
{this.props.variants.map((variant, _) => (
<div className="form-check" key={key + variant}>
<input
className="form-check-input"
type="radio"
name={key}
id={key + variant}
checked={this.state.variant === variant}
onClick={() => this.handleClick(variant)}
onChange={() => this.handleClick(variant)}
/>
<label className="form-check-label" htmlFor={key + variant}>
{variant}
</label>
</div>
))}
</div>
);
}
}
function RadioWrapper(target, id, data, {title, variants, outvar, fallback, icon, tip}) {
return <Radio target={target} title={title} variants={variants} outvar={outvar} icon={icon} tip={tip} key={id}
fallback={fallback}
id={id} data={data}/>;
}
class Switch extends Component {
constructor(props) {
super(props);
// Initialize the state object with the initial values from the props
this.state = {
checked: props.outvar in props.data ? !!(props.data[props.outvar]) : !!(props.fallback)
};
// Bind the event handler to this
this.handleClick = this.handleClick.bind(this);
this.props.target.construct(this.props.outvar, this.state.checked);
}
handleClick() {
// Update the state object with the new value for outvar
let new_checked = !this.state.checked;
this.setState({
checked: new_checked
});
this.props.target.update(this.props.outvar, new_checked);
}
render() {
let key = this.props.id + this.props.outvar;
return (
<div className="shop-switch" key={this.props.id}>
<div className="form-check form-switch" key={key}>
<input
className="form-check-input"
type="checkbox"
role="switch"
id={key}
checked={this.state.checked}
onClick={this.handleClick}
onChange={this.handleClick}
/>
<label className="form-check-label" htmlFor={key} style={{"display": "inline"}}>
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
{this.props.title}
</label>
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
</div>
</div>
);
}
}
function SwitchWrapper(target, id, data, {title, fallback, outvar, icon, tip}) {
return <Switch target={target} title={title} fallback={fallback} outvar={outvar} icon={icon} tip={tip} key={id}
id={id} data={data}/>;
}
class Line extends Component {
constructor(props) {
super(props);
// Initialize the state object with the initial values from the props
this.state = {
text: props.outvar in props.data ? props.data[props.outvar] : (props.fallback ? props.fallback : "")
};
// Bind the event handler to this
this.handleClick = this.handleClick.bind(this);
this.props.target.construct(this.props.outvar, this.state.text);
}
handleClick(element) {
let text = element.target.value;
this.setState({
text: text
});
this.props.target.update(this.props.outvar, text);
}
render() {
let key = this.props.id + this.props.outvar;
return (
<div className="shop-line" key={this.props.id}>
<label htmlFor={key} className="form-label">
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
{this.props.title}:
</label>
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
<input type="text" className="form-control form-control-sm" id={key} onChange={this.handleClick}
value={this.state.text}/>
</div>
);
}
}
function LineWrapper(target, id, data, {title, fallback, outvar, icon, tip}) {
return <Line target={target} title={title} fallback={fallback} outvar={outvar} icon={icon} tip={tip} key={id}
id={id} data={data}/>;
}
class SwitchLine extends Component {
constructor(props) {
super(props);
// Initialize the state object with the initial values from the props
this.state = {
text: props.outvar in props.data ? props.data[props.outvar].text : (props.fallback ? props.fallback.text : ""),
checked: props.outvar in props.data ? props.data[props.outvar].checked : (props.fallback ? props.fallback.checked : false)
};
// Bind the event handler to this
this.handleText = this.handleText.bind(this);
this.handleCheck = this.handleCheck.bind(this);
this.props.target.construct(this.props.outvar, this.state);
}
handleText(element) {
let new_state = {
...this.state,
text: element.target.value
}
this.setState(new_state);
this.props.target.update(this.props.outvar, new_state);
}
handleCheck() {
// Update the state object with the new value for outvar
let new_state = {
...this.state,
checked: !this.state.checked
}
this.setState(new_state);
this.props.target.update(this.props.outvar, new_state);
}
render() {
let key = this.props.id + this.props.outvar;
return (
<div className="shop-switch-line" key={this.props.id}>
<div className="form-check form-switch" key={key}>
<input
className="form-check-input"
type="checkbox"
role="switch"
id={key + "switch"}
checked={this.state.checked}
onClick={this.handleCheck}
onChange={this.handleCheck}
/>
<label className="form-check-label" htmlFor={key + "switch"}>
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
{this.props.title}
</label>
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
</div>
<input type="text" className="form-control form-control-sm" id={key + "line"} onChange={this.handleText}
value={this.state.text} disabled={!this.state.checked}/>
</div>
);
}
}
function SwitchLineWrapper(target, id, data, {title, fallback, outvar, icon, tip}) {
return <SwitchLine target={target} title={title} fallback={fallback} outvar={outvar} icon={icon} tip={tip} key={id}
id={id} data={data}/>;
}
function UnimplementedComponent(type, id) {
//console.error("Missing component with type:", type)
return <div key={type + id} style={{background: "red"}}>UNIMPLEMENTED</div>
}
const componentsList = {
"Radio": RadioWrapper,
"Switch": SwitchWrapper,
"Line": LineWrapper,
"SwitchLine": SwitchLineWrapper,
"Default": UnimplementedComponent,
};
export function ProcessOptions({options, data, target, id}) {
let options_t = true_type_of(options);
if (options_t === "array") {
return Array.from(
options.map((option_item, i) => ProcessOptions({
options: option_item,
data: data,
target: target,
id: id + i
}))
);
} else if (options_t === "object") {
if (
true_type_of(options.type) === "string" &&
(true_type_of(options.args) === "object" || true_type_of(options.items) === "array")
) {
if (options.type in componentsList) {
return componentsList[options.type](target, id + options.type, data, options.args);
} else if (options.type === "Group") {
return (
<div className="border rounded" key={id + "group"}>
{ProcessOptions({
options: jsonLogic.apply(options.items, data),
data: data,
target: target,
id: id
})}
</div>);
} else {
return componentsList["Default"](options.type, id + "missing");
}
} else {
return ProcessOptions({options: jsonLogic.apply(options, data), data: data, target: target, id: id});
}
}
}
export function FilterOptions(options, data) {
let options_t = true_type_of(options);
let target = {};
if (options_t === "array") {
options.map((option_item, _) => {
Object.assign(target, FilterOptions(option_item, data))
});
} else if (options_t === "object") {
if (
true_type_of(options.type) === "string" &&
(true_type_of(options.args) === "object" || true_type_of(options.items) === "array")
) {
if (options.type in componentsList) {
target[options.args.outvar] = data[options.args.outvar];
} else if (options.type === "Group") {
Object.assign(target, FilterOptions(jsonLogic.apply(options.items, data), data))
}
} else {
Object.assign(target, FilterOptions(jsonLogic.apply(options, data), data))
}
}
return target
}
export function OptionsDialogPopup({options, data, target, id, big, first, last, options_class}) {
const [show, setShow] = useState(false);
const ref = useClickAway((e) => {
if (e.type === "mousedown") // ignore touchstart
setShow(false)
}
);
let div_classes = `overlayVariant border rounded ${big ? "overlay-bigcard" : "overlay-smallcard"} ${(!big && first) ? "overlay-first" : ""} ${(!big && last) ? "overlay-last" : ""} ${options_class || ""}`;
const handleClick = (event) => {
setShow(!show);
};
return (
<div ref={ref}>
<img className="alert-info" src={show ? "/images/shop/icon-close.svg" : "/images/shop/icon-customize.svg"}
onClick={handleClick}/>
<div style={{'display': show ? 'flex' : 'none'}} className={div_classes}>
<ProcessOptions
options={options}
data={data}
key={"processed_options_" + id}
id={"processed_options_" + id}
target={target}
/>
</div>
</div>
);
}
export function OptionsSummaryPopup({id, options, data}) {
const [show, setShow] = useState(false);
const [position, setPosition] = useState({x: 0, y: 0});
const [size, setSize] = useState({w: 0, h: 0});
let display_options = FilterOptions(options, data);
const close = () => {
setShow(false);
document.removeEventListener("scroll", handleScroll, true);
}
const ref = useClickAway(close);
const reposition = () => {
let popup_button = document.getElementById(id + "img");
if (!popup_button) {
document.removeEventListener("scroll", handleScroll, true);
return;
}
let rect = popup_button.getBoundingClientRect()
let pos_x = (rect.left + rect.right) / 2;
let pos_y = (rect.top + rect.bottom) / 2;
if (pos_x + size.w > window.innerWidth) {
setPosition({x: pos_x - size.w - 20, y: pos_y - size.h / 2});
} else {
setPosition({x: pos_x - size.w / 2, y: pos_y - size.h - 20});
}
}
const handleScroll = (e) => {
if (e.target !== document.getElementById(id)) {
close();
}
}
useEffect(() => {
if (show) {
let popup = document.getElementById(id);
let width = popup.offsetWidth;
let height = popup.offsetHeight;
setSize({w: width, h: height});
reposition()
}
}, [show])
useEffect(() => {
if (show) {
reposition();
}
}, [show, size])
const handleClick = (event) => {
setShow(!show);
if (!show) {
document.addEventListener("scroll", handleScroll, true);
}
};
const stringify = (value) => {
let value_type = true_type_of(value);
if (value_type === "string") {
return value;
} else if (value_type === "object") {
if (value.checked === false) {
return "off";
} else if (value.checked === true && value.text) {
return value.text;
}
}
return JSON.stringify(value);
}
return (
<div ref={ref}>
<img className="alert-info" src={show ? "/images/shop/icon-close.svg" : "/images/shop/icon-customize.svg"}
id={id + "img"}
onClick={handleClick}/>
<div style={{'display': show ? 'flex' : 'none', 'top': position.y, 'left': position.x}}
className="overlayVariant card border rounded"
id={id}>
<div className="card-body">
{Array.from(Object.entries(display_options)
.filter(([key, value], _) => key !== "ext_data")
.map(([key, value], _) => {
return (<p className="card-text" key={id + key}><i>{key}</i>: {stringify(value)}</p>);
}))}
</div>
</div>
</div>
);
}
export function FillExtData(data, index) {
return {
has_other_dio: data.filter((value, item_index) => index !== item_index && value.name &&value.name.endsWith("-TTL")).length > 0,
has_dds: data.filter(((value, _) => value.name === "DDS" && value.name_number === "4410" && (!value.options_data || !value.options_data.mono_eem))).length > 0,
has_sampler: data.filter(((value, _) => value.name === "Sampler" && (!value.options_data || !value.options_data.mono_eem))).length > 0,
}
}

File diff suppressed because it is too large Load Diff

View File

@ -13,10 +13,30 @@ module.exports = {
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: 'babel-loader'
use: [
'babel-loader',
{
loader: "webpack-preprocessor-loader",
options: {
debug: false,
directives: {
render_count: false,
options_log: false
},
params: {
ENV: process.env.NODE_ENV,
disable_card_highlight: false
},
verbose: false,
}
}
],
}
]
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.jsx'],
},
//devtool: "inline-source-map",
//mode: "development"
devtool: false,