Refactor, multiple crates and warnings fixes #113
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 |
|
@ -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
2484
static/js/shop.jsx
2484
static/js/shop.jsx
File diff suppressed because it is too large
Load Diff
|
@ -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>
|
||||
);
|
||||
|
||||
}
|
|
@ -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>);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
||||
}
|
|
@ -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>)
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
||||
}
|
|
@ -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>
|
||||
</>)
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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}/>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
||||
}
|
|
@ -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>);
|
||||
}
|
|
@ -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',
|
||||
}}> </span>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
||||
}
|
|
@ -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>
|
||||
</>)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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',
|
||||
}}> </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',
|
||||
}}> </span>
|
||||
))}
|
||||
{((options && options_data) ? (
|
||||
<OptionsSummaryWrapper crate_index={crate_index} card_index={card_index}/>
|
||||
) : (
|
||||
<span style={{
|
||||
'display': 'inline-block',
|
||||
'width': '20px',
|
||||
}}> </span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>);
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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} />
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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}/>;
|
||||
}
|
|
@ -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}/>;
|
||||
}
|
|
@ -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}/>;
|
||||
}
|
|
@ -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}/>;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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),
|
||||
}))
|
|
@ -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))
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -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
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue