Compare commits
1 Commits
main
...
zelda.gumt
| Author | SHA1 | Date |
|---|---|---|
|
|
326c9e71ad |
|
|
@ -1,13 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="public/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Bid System</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="public/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -29,8 +29,6 @@
|
|||
"react": "^19.0.0",
|
||||
"react-cropper": "^2.3.3",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-player": "^2.16.0",
|
||||
"react-router": "^6.29.0",
|
||||
"react-router-dom": "^6.29.0",
|
||||
"react-toastify": "^11.0.3",
|
||||
|
|
@ -2108,12 +2106,6 @@
|
|||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asap": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
|
|
@ -2141,12 +2133,6 @@
|
|||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/base16": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz",
|
||||
"integrity": "sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcp-47-match": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz",
|
||||
|
|
@ -2407,15 +2393,6 @@
|
|||
"integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-fetch": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz",
|
||||
"integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
|
@ -2698,15 +2675,6 @@
|
|||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
|
|
@ -3170,36 +3138,6 @@
|
|||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fbemitter": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fbemitter/-/fbemitter-3.0.0.tgz",
|
||||
"integrity": "sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"fbjs": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fbjs": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz",
|
||||
"integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-fetch": "^3.1.5",
|
||||
"fbjs-css-vars": "^1.0.0",
|
||||
"loose-envify": "^1.0.0",
|
||||
"object-assign": "^4.1.0",
|
||||
"promise": "^7.1.1",
|
||||
"setimmediate": "^1.0.5",
|
||||
"ua-parser-js": "^1.0.35"
|
||||
}
|
||||
},
|
||||
"node_modules/fbjs-css-vars": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz",
|
||||
"integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
|
|
@ -4139,30 +4077,12 @@
|
|||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/load-script": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
|
||||
"integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.curry": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz",
|
||||
"integrity": "sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.flow": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz",
|
||||
"integrity": "sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
|
|
@ -4493,12 +4413,6 @@
|
|||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
|
|
@ -5185,26 +5099,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.19",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
||||
|
|
@ -5515,15 +5409,6 @@
|
|||
"postcss": "^8.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/promise": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
|
||||
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asap": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
|
|
@ -5551,12 +5436,6 @@
|
|||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pure-color": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pure-color/-/pure-color-1.3.0.tgz",
|
||||
"integrity": "sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
|
|
@ -5587,18 +5466,6 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-base16-styling": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.6.0.tgz",
|
||||
"integrity": "sha512-yvh/7CArceR/jNATXOKDlvTnPKPmGZz7zsenQ3jUwLzHkNUR0CvY3yGYJbWJ/nnxsL8Sgmt5cO3/SILVuPO6TQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base16": "^1.0.0",
|
||||
"lodash.curry": "^4.0.1",
|
||||
"lodash.flow": "^3.3.0",
|
||||
"pure-color": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-cropper": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/react-cropper/-/react-cropper-2.3.3.tgz",
|
||||
|
|
@ -5638,53 +5505,12 @@
|
|||
"react": ">= 16.8 || 18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-fast-compare": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
|
||||
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-json-view": {
|
||||
"version": "1.21.3",
|
||||
"resolved": "https://registry.npmjs.org/react-json-view/-/react-json-view-1.21.3.tgz",
|
||||
"integrity": "sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"flux": "^4.0.1",
|
||||
"react-base16-styling": "^0.6.0",
|
||||
"react-lifecycles-compat": "^3.0.4",
|
||||
"react-textarea-autosize": "^8.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0 || ^16.3.0 || ^15.5.4",
|
||||
"react-dom": "^17.0.0 || ^16.3.0 || ^15.5.4"
|
||||
}
|
||||
},
|
||||
"node_modules/react-json-view/node_modules/flux": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/flux/-/flux-4.0.4.tgz",
|
||||
"integrity": "sha512-NCj3XlayA2UsapRpM7va6wU1+9rE5FIL7qoMcmxWHRzbp0yujihMBm9BBHZ1MDIk5h5o2Bl6eGiCe8rYELAmYw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"fbemitter": "^3.0.0",
|
||||
"fbjs": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^15.0.2 || ^16.0.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-markdown": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.3.tgz",
|
||||
|
|
@ -5721,22 +5547,6 @@
|
|||
"react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-player": {
|
||||
"version": "2.16.0",
|
||||
"resolved": "https://registry.npmjs.org/react-player/-/react-player-2.16.0.tgz",
|
||||
"integrity": "sha512-mAIPHfioD7yxO0GNYVFD1303QFtI3lyyQZLY229UEAp/a10cSW+hPcakg0Keq8uWJxT2OiT/4Gt+Lc9bD6bJmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"deepmerge": "^4.0.0",
|
||||
"load-script": "^1.0.0",
|
||||
"memoize-one": "^5.1.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-fast-compare": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
||||
|
|
@ -6329,12 +6139,6 @@
|
|||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||
|
|
@ -6507,12 +6311,6 @@
|
|||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/trim-lines": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
|
||||
|
|
@ -6588,32 +6386,6 @@
|
|||
"typescript": ">=4.8.4 <5.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ua-parser-js": {
|
||||
"version": "1.0.40",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz",
|
||||
"integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/ua-parser-js"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/faisalman"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/faisalman"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"ua-parser-js": "script/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/unified": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
|
||||
|
|
@ -7735,22 +7507,6 @@
|
|||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
|
|
|
|||
|
|
@ -31,8 +31,6 @@
|
|||
"react": "^19.0.0",
|
||||
"react-cropper": "^2.3.3",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-player": "^2.16.0",
|
||||
"react-router": "^6.29.0",
|
||||
"react-router-dom": "^6.29.0",
|
||||
"react-toastify": "^11.0.3",
|
||||
|
|
|
|||
|
|
@ -1,187 +1,120 @@
|
|||
import { generateNestParams, handleError, handleSuccess } from ".";
|
||||
import axios from "../lib/axios";
|
||||
import { IBid, IMetadata, IWebBid } from "../system/type";
|
||||
import { removeFalsyValues } from "../utils";
|
||||
import { generateNestParams, handleError, handleSuccess } from '.';
|
||||
import axios from '../lib/axios';
|
||||
import { IBid, IWebBid } from '../system/type';
|
||||
import { removeFalsyValues } from '../utils';
|
||||
|
||||
export const getBids = async (params: Record<string, string | number>) => {
|
||||
return await axios({
|
||||
url: "bids",
|
||||
params: generateNestParams(params),
|
||||
withCredentials: true,
|
||||
method: "GET",
|
||||
});
|
||||
return await axios({
|
||||
url: 'bids',
|
||||
params: generateNestParams(params),
|
||||
withCredentials: true,
|
||||
method: 'GET',
|
||||
});
|
||||
};
|
||||
|
||||
export const createBid = async (
|
||||
bid: Omit<IBid, "id" | "created_at" | "updated_at" | "is_system_account">
|
||||
) => {
|
||||
const newData = removeFalsyValues(bid);
|
||||
export const createBid = async (bid: Omit<IBid, 'id' | 'created_at' | 'updated_at' | 'is_system_account'>) => {
|
||||
const newData = removeFalsyValues(bid);
|
||||
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: "bids",
|
||||
withCredentials: true,
|
||||
method: "POST",
|
||||
data: newData,
|
||||
});
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: 'bids',
|
||||
withCredentials: true,
|
||||
method: 'POST',
|
||||
data: newData,
|
||||
});
|
||||
|
||||
handleSuccess(data);
|
||||
handleSuccess(data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateBid = async (bid: Partial<IBid>) => {
|
||||
const { plus_price, max_price, quantity, metadata } = removeFalsyValues(bid, [
|
||||
"plus_price",
|
||||
]);
|
||||
const { plus_price, max_price, quantity } = removeFalsyValues(bid, ['plus_price']);
|
||||
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: "bids/" + bid.id,
|
||||
withCredentials: true,
|
||||
method: "PUT",
|
||||
data: { plus_price, max_price, quantity, metadata },
|
||||
});
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: 'bids/' + bid.id,
|
||||
withCredentials: true,
|
||||
method: 'PUT',
|
||||
data: { plus_price, max_price, quantity },
|
||||
});
|
||||
|
||||
handleSuccess(data);
|
||||
handleSuccess(data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleBid = async (bid: Partial<IBid>) => {
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: "bids/toggle/" + bid.id,
|
||||
withCredentials: true,
|
||||
method: "POST",
|
||||
});
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: 'bids/toggle/' + bid.id,
|
||||
withCredentials: true,
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
handleSuccess(data);
|
||||
handleSuccess(data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteBid = async (bid: IBid) => {
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: "bids/" + bid.id,
|
||||
withCredentials: true,
|
||||
method: "DELETE",
|
||||
});
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: 'bids/' + bid.id,
|
||||
withCredentials: true,
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
handleSuccess(data);
|
||||
handleSuccess(data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const deletesBid = async (bids: IBid[]) => {
|
||||
const ids = bids.reduce((prev, cur) => {
|
||||
prev.push(cur.id);
|
||||
return prev;
|
||||
}, [] as number[]);
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: "bids/deletes",
|
||||
withCredentials: true,
|
||||
method: "POST",
|
||||
data: {
|
||||
ids,
|
||||
},
|
||||
});
|
||||
const ids = bids.reduce((prev, cur) => {
|
||||
prev.push(cur.id);
|
||||
return prev;
|
||||
}, [] as number[]);
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: 'bids/deletes',
|
||||
withCredentials: true,
|
||||
method: 'POST',
|
||||
data: {
|
||||
ids,
|
||||
},
|
||||
});
|
||||
|
||||
handleSuccess(data);
|
||||
handleSuccess(data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const getImagesWorking = async (
|
||||
values: (IBid | IWebBid) & { type: string }
|
||||
) => {
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: `bids/images-working/${values.type
|
||||
.toLocaleLowerCase()
|
||||
.replace("_", "-")}/${values.id}`,
|
||||
withCredentials: true,
|
||||
method: "GET",
|
||||
});
|
||||
export const getImagesWorking = async (values: (IBid | IWebBid) & { type: string }) => {
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: `bids/images-working/${values.type.toLocaleLowerCase().replace('_', '-')}/${values.id}`,
|
||||
withCredentials: true,
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.log("%csrc/apis/bid.ts:118 error", "color: #007acc;", error);
|
||||
}
|
||||
};
|
||||
|
||||
export const getRecords = async (values: IBid) => {
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: `bids/records/${values.id}`,
|
||||
withCredentials: true,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.log("%csrc/apis/bid.ts:118 error", "color: #007acc;", error);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteRecord = async (name: string) => {
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: `bids/record/${name}`,
|
||||
withCredentials: true,
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.log("%csrc/apis/bid.ts:154 error", "color: #007acc;", error);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteMetadata = async (id: IMetadata["id"]) => {
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: `bid-metadatas/${id}`,
|
||||
withCredentials: true,
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.log("%csrc/apis/bid.ts:154 error", "color: #007acc;", error);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateMetadata = async (id: IMetadata["id"], value: string) => {
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: `bid-metadatas/${id}`,
|
||||
withCredentials: true,
|
||||
method: "PUT",
|
||||
data: {
|
||||
value,
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.log("%csrc/apis/bid.ts:154 error", "color: #007acc;", error);
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.log('%csrc/apis/bid.ts:118 error', 'color: #007acc;', error);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,357 +1,117 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import {
|
||||
Button,
|
||||
LoadingOverlay,
|
||||
Modal,
|
||||
ModalProps,
|
||||
NumberInput,
|
||||
Select,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import _ from "lodash";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { z } from "zod";
|
||||
import { createBid, updateBid } from "../../apis/bid";
|
||||
import { useConfirmStore } from "../../lib/zustand/use-confirm";
|
||||
import { IBid, IMetadata } from "../../system/type";
|
||||
import { formatTimeFromMinutes } from "../../utils";
|
||||
import { Button, LoadingOverlay, Modal, ModalProps, NumberInput, TextInput } from '@mantine/core';
|
||||
import { useForm, zodResolver } from '@mantine/form';
|
||||
import _ from 'lodash';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { createBid, updateBid } from '../../apis/bid';
|
||||
import { useConfirmStore } from '../../lib/zustand/use-confirm';
|
||||
import { IBid } from '../../system/type';
|
||||
export interface IBidModelProps extends ModalProps {
|
||||
data: IBid | null;
|
||||
onUpdated?: () => void;
|
||||
data: IBid | null;
|
||||
onUpdated?: () => void;
|
||||
}
|
||||
|
||||
const schema = {
|
||||
url: z.string({ message: "Url is required" }).url("Invalid url format"),
|
||||
max_price: z
|
||||
.number({ message: "Max price is required" })
|
||||
.min(1, "Max price must be at least 1"),
|
||||
plus_price: z.number().min(0, "Plus price must be at least 1").optional(),
|
||||
quantity: z.number().min(1, "Quantity must be at least 1").optional(),
|
||||
arrival_offset_seconds_live: z
|
||||
.number({ message: "Arrival offset seconds is required" })
|
||||
.refine((val) => val >= 60, {
|
||||
message: "Arrival offset seconds must be at least 60 seconds (1 minute)",
|
||||
})
|
||||
.optional(),
|
||||
early_tracking_seconds_live: z
|
||||
.number({ message: "Early login seconds is required" })
|
||||
.refine((val) => val >= 600, {
|
||||
message: "Early login seconds must be at least 600 seconds (10 minute)",
|
||||
})
|
||||
.optional(),
|
||||
arrival_offset_seconds_sandbox: z
|
||||
.number({ message: "Arrival offset seconds is required" })
|
||||
.refine((val) => val >= 60, {
|
||||
message: "Arrival offset seconds must be at least 60 seconds (1 minute)",
|
||||
})
|
||||
.optional(),
|
||||
early_tracking_seconds_sandbox: z
|
||||
.number({ message: "Early login seconds is required" })
|
||||
.refine((val) => val >= 600, {
|
||||
message: "Early login seconds must be at least 600 seconds (10 minute)",
|
||||
})
|
||||
.optional(),
|
||||
url: z.string({ message: 'Url is required' }).url('Invalid url format'),
|
||||
max_price: z.number({ message: 'Max price is required' }).min(1, 'Max price must be at least 1'),
|
||||
plus_price: z.number().min(0, 'Plus price must be at least 1').optional(),
|
||||
quantity: z.number().min(1, 'Quantity must be at least 1').optional(),
|
||||
};
|
||||
|
||||
export default function BidModal({
|
||||
data,
|
||||
onUpdated,
|
||||
...props
|
||||
}: IBidModelProps) {
|
||||
const form = useForm({
|
||||
validate: zodResolver(z.object(schema)),
|
||||
});
|
||||
|
||||
const prevData = useRef<IBid | null>(data);
|
||||
|
||||
const { setConfirm } = useConfirmStore();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (values: typeof form.values) => {
|
||||
if (data) {
|
||||
setConfirm({
|
||||
title: "Update ?",
|
||||
message: `This product will be update`,
|
||||
handleOk: async () => {
|
||||
setLoading(true);
|
||||
|
||||
const metadata = valuesToMetadata(
|
||||
values as IBid & Record<string, any>
|
||||
);
|
||||
|
||||
const result = await updateBid({ ...values, metadata });
|
||||
setLoading(false);
|
||||
|
||||
if (!result) return;
|
||||
|
||||
props.onClose();
|
||||
|
||||
if (onUpdated) {
|
||||
onUpdated();
|
||||
}
|
||||
},
|
||||
okButton: {
|
||||
color: "blue",
|
||||
value: "Update",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const { url, max_price, plus_price } = values;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const metadata = valuesToMetadata(values as IBid & Record<string, any>);
|
||||
|
||||
const result = await createBid({
|
||||
url,
|
||||
max_price,
|
||||
plus_price,
|
||||
metadata,
|
||||
} as IBid);
|
||||
|
||||
setLoading(false);
|
||||
|
||||
if (!result) return;
|
||||
|
||||
props.onClose();
|
||||
|
||||
if (onUpdated) {
|
||||
onUpdated();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mappingValues = (ignore: string[] = []) => {
|
||||
if (!data) return {};
|
||||
let values: IBid & Record<string, any> = data;
|
||||
|
||||
const followKey = ["arrival_offset_seconds", "early_tracking_seconds"];
|
||||
|
||||
if (
|
||||
data.metadata.length &&
|
||||
data.metadata.some((item) => item.key_name === "mode_key")
|
||||
) {
|
||||
data.metadata.reduce((prev, cur) => {
|
||||
if (ignore.includes(cur.key_name)) {
|
||||
prev[cur.key_name as string] = form.values[cur.key_name];
|
||||
|
||||
return prev;
|
||||
}
|
||||
|
||||
if (cur.key_name === "mode_key") {
|
||||
prev[cur.key_name as string] = cur.value;
|
||||
return prev;
|
||||
}
|
||||
|
||||
prev[cur.key_name as string] = cur.value;
|
||||
|
||||
return prev;
|
||||
}, values);
|
||||
} else {
|
||||
const metadata = Object.assign(
|
||||
{ mode_key: "live" },
|
||||
...followKey.map((item) => ({
|
||||
[`${item}_live`]: data.web_bid[item as keyof typeof data.web_bid],
|
||||
[`${item}_sandbox`]: data.web_bid[item as keyof typeof data.web_bid],
|
||||
}))
|
||||
);
|
||||
|
||||
values = { ...values, ...metadata };
|
||||
}
|
||||
|
||||
return values;
|
||||
};
|
||||
|
||||
const valuesToMetadata = (values: IBid & Record<string, any>) => {
|
||||
const keys = [
|
||||
"mode_key",
|
||||
"arrival_offset_seconds_live",
|
||||
"arrival_offset_seconds_sandbox",
|
||||
"early_tracking_seconds_live",
|
||||
"early_tracking_seconds_sandbox",
|
||||
];
|
||||
|
||||
if (!values?.metadata) {
|
||||
const mode_key = values.mode_key;
|
||||
|
||||
if (!mode_key) return [];
|
||||
|
||||
const newValues = Object.entries(values)
|
||||
.map(([key, value]) => {
|
||||
if (keys.includes(key)) {
|
||||
return {
|
||||
key_name: key,
|
||||
value,
|
||||
} as IMetadata;
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((i) => i !== null);
|
||||
|
||||
return newValues;
|
||||
}
|
||||
|
||||
if (values.metadata.length <= 0) {
|
||||
return keys.map((item) => {
|
||||
return {
|
||||
key_name: item,
|
||||
value: values[item],
|
||||
} as IMetadata;
|
||||
});
|
||||
}
|
||||
|
||||
return values.metadata.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
value: values[item.key_name],
|
||||
};
|
||||
export default function BidModal({ data, onUpdated, ...props }: IBidModelProps) {
|
||||
const form = useForm({
|
||||
validate: zodResolver(z.object(schema)),
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
if (!data) return;
|
||||
const prevData = useRef<IBid | null>(data);
|
||||
|
||||
const values = mappingValues();
|
||||
const { setConfirm } = useConfirmStore();
|
||||
|
||||
form.setValues(values);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
prevData.current = data;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data]);
|
||||
const handleSubmit = async (values: typeof form.values) => {
|
||||
if (data) {
|
||||
setConfirm({
|
||||
title: 'Update ?',
|
||||
message: `This product will be update`,
|
||||
handleOk: async () => {
|
||||
setLoading(true);
|
||||
const result = await updateBid(values);
|
||||
setLoading(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.opened) {
|
||||
form.reset();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.opened]);
|
||||
if (!result) return;
|
||||
|
||||
useEffect(() => {
|
||||
if (!data && !form.values.mode_key) {
|
||||
form.setValues({ mode_key: "live" });
|
||||
} else {
|
||||
const values = mappingValues(["mode_key"]);
|
||||
form.setValues(values);
|
||||
}
|
||||
props.onClose();
|
||||
|
||||
prevData.current = data;
|
||||
if (onUpdated) {
|
||||
onUpdated();
|
||||
}
|
||||
},
|
||||
okButton: {
|
||||
color: 'blue',
|
||||
value: 'Update',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const { url, max_price, plus_price } = values;
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [form.values["mode_key"]]);
|
||||
setLoading(true);
|
||||
const result = await createBid({ url, max_price, plus_price } as IBid);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="relative"
|
||||
classNames={{
|
||||
header: "!flex !item-center !justify-center w-full",
|
||||
}}
|
||||
{...props}
|
||||
size={"xl"}
|
||||
title={<span className="text-xl font-bold">Bid</span>}
|
||||
centered
|
||||
>
|
||||
<form
|
||||
onSubmit={form.onSubmit(handleSubmit)}
|
||||
className="grid grid-cols-2 gap-2.5"
|
||||
>
|
||||
<Select
|
||||
className="col-span-2"
|
||||
label="Mode"
|
||||
data={[
|
||||
{ label: "Live", value: "live" },
|
||||
{ label: "Sandbox", value: "sandbox" },
|
||||
]}
|
||||
defaultValue="live"
|
||||
checkIconPosition="right"
|
||||
allowDeselect={false}
|
||||
{...form.getInputProps("mode_key")}
|
||||
/>
|
||||
setLoading(false);
|
||||
|
||||
{data && data.name && (
|
||||
<TextInput
|
||||
className="col-span-2"
|
||||
readOnly={!!data}
|
||||
size="sm"
|
||||
label="Name"
|
||||
value={data.name}
|
||||
/>
|
||||
)}
|
||||
<TextInput
|
||||
className="col-span-2"
|
||||
readOnly={!!data}
|
||||
size="sm"
|
||||
label="Url"
|
||||
{...form.getInputProps("url")}
|
||||
/>
|
||||
<NumberInput
|
||||
className="col-span-2"
|
||||
size="sm"
|
||||
label="Max price"
|
||||
{...form.getInputProps("max_price")}
|
||||
/>
|
||||
<NumberInput
|
||||
size="sm"
|
||||
label="Plus price"
|
||||
{...form.getInputProps("plus_price")}
|
||||
/>
|
||||
<NumberInput
|
||||
size="sm"
|
||||
label="Quantity"
|
||||
{...form.getInputProps("quantity")}
|
||||
/>
|
||||
if (!result) return;
|
||||
|
||||
<NumberInput
|
||||
description="Note: that only integer minutes are accepted."
|
||||
className="col-span-1"
|
||||
size="sm"
|
||||
label={`Arrival offset seconds (${formatTimeFromMinutes(
|
||||
form.getValues()[
|
||||
`arrival_offset_seconds_${form.getValues()["mode_key"]}`
|
||||
] / 60
|
||||
)})`}
|
||||
placeholder="msg: 300"
|
||||
{...form.getInputProps(
|
||||
`arrival_offset_seconds_${form.getValues()["mode_key"]}`
|
||||
)}
|
||||
/>
|
||||
props.onClose();
|
||||
|
||||
<NumberInput
|
||||
description="Note: that only integer minutes are accepted."
|
||||
className="col-span-1"
|
||||
size="sm"
|
||||
label={`Early tracking seconds (${formatTimeFromMinutes(
|
||||
form.getValues()[
|
||||
`early_tracking_seconds_${form.getValues()["mode_key"]}`
|
||||
] / 60
|
||||
)})`}
|
||||
placeholder="msg: 600"
|
||||
{...form.getInputProps(
|
||||
`early_tracking_seconds_${form.getValues()["mode_key"]}`
|
||||
)}
|
||||
/>
|
||||
if (onUpdated) {
|
||||
onUpdated();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
<Button
|
||||
// disabled={_.isEqual(form.values, prevData.current)}
|
||||
className="col-span-2"
|
||||
type="submit"
|
||||
fullWidth
|
||||
size="sm"
|
||||
mt="md"
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
if (!data) return;
|
||||
|
||||
form.setValues(data);
|
||||
|
||||
prevData.current = data;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.opened) {
|
||||
form.reset();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.opened]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="relative"
|
||||
classNames={{
|
||||
header: '!flex !item-center !justify-center w-full',
|
||||
}}
|
||||
{...props}
|
||||
size={'xl'}
|
||||
title={<span className="text-xl font-bold">Bid</span>}
|
||||
centered
|
||||
>
|
||||
{data ? "Update" : "Create"}
|
||||
</Button>
|
||||
</form>
|
||||
<form onSubmit={form.onSubmit(handleSubmit)} className="grid grid-cols-2 gap-2.5">
|
||||
{data && data.name && <TextInput className="col-span-2" readOnly={!!data} size="sm" label="Name" value={data.name} />}
|
||||
<TextInput className="col-span-2" readOnly={!!data} size="sm" label="Url" {...form.getInputProps('url')} />
|
||||
<NumberInput className="col-span-2" size="sm" label="Max price" {...form.getInputProps('max_price')} />
|
||||
<NumberInput size="sm" label="Plus price" {...form.getInputProps('plus_price')} />
|
||||
<NumberInput size="sm" label="Quantity" {...form.getInputProps('quantity')} />
|
||||
|
||||
<LoadingOverlay
|
||||
visible={loading}
|
||||
zIndex={1000}
|
||||
overlayProps={{ blur: 2 }}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
<Button disabled={_.isEqual(form.getValues(), prevData.current)} className="col-span-2" type="submit" fullWidth size="sm" mt="md">
|
||||
{data ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<LoadingOverlay visible={loading} zIndex={1000} overlayProps={{ blur: 2 }} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,2 @@
|
|||
export { default as ShowHistoriesModal } from "./show-histories-modal";
|
||||
export { default as BidModal } from "./bid-modal";
|
||||
export { default as RecordModal } from "./record-modal";
|
||||
export { default as ResponseDemoModal } from "./response-demo-modal";
|
||||
|
|
|
|||
|
|
@ -1,168 +0,0 @@
|
|||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Card,
|
||||
Group,
|
||||
Menu,
|
||||
Modal,
|
||||
ModalProps,
|
||||
ScrollArea,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconDots,
|
||||
IconPlayerRecordFilled,
|
||||
IconTrash,
|
||||
IconVideoFilled,
|
||||
} from "@tabler/icons-react";
|
||||
import moment from "moment";
|
||||
import { useEffect, useState } from "react";
|
||||
import ReactPlayer from "react-player";
|
||||
import { deleteRecord, getRecords } from "../../apis/bid";
|
||||
import { useConfirmStore } from "../../lib/zustand/use-confirm";
|
||||
import { IBid } from "../../system/type";
|
||||
|
||||
export interface IRecordModalProps extends ModalProps {
|
||||
data: IBid | null;
|
||||
onUpdated?: () => void;
|
||||
}
|
||||
|
||||
interface IRecord {
|
||||
bid_id: number;
|
||||
name: string;
|
||||
original_name: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export default function RecordModal({ data, ...props }: IRecordModalProps) {
|
||||
const [records, setRecords] = useState<IRecord[]>([]);
|
||||
const [selected, setSelected] = useState<IRecord | null>(null);
|
||||
|
||||
const { setConfirm } = useConfirmStore();
|
||||
|
||||
const fetch = async () => {
|
||||
if (!data) return;
|
||||
const response = await getRecords(data);
|
||||
if (response?.data) {
|
||||
setRecords(response.data);
|
||||
setSelected(response.data[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (item: IRecord) => {
|
||||
setConfirm({
|
||||
handleOk: async () => {
|
||||
const result = await deleteRecord(item.original_name);
|
||||
|
||||
if (!result?.data) return;
|
||||
|
||||
fetch();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
classNames={{
|
||||
header: "!flex !justify-center w-full",
|
||||
}}
|
||||
{...props}
|
||||
size={"xl"}
|
||||
title={<Title order={3}>Video Records</Title>}
|
||||
centered
|
||||
>
|
||||
<Box className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{/* Sidebar - Record list */}
|
||||
{records.length > 0 && (
|
||||
<ScrollArea className="md:col-span-1 h-[400px] pr-2">
|
||||
{records.map((item) => (
|
||||
<Card
|
||||
key={item.original_name}
|
||||
withBorder
|
||||
shadow="xs"
|
||||
radius="md"
|
||||
className={`mb-3 cursor-pointer transition-all ${
|
||||
selected?.original_name === item.original_name
|
||||
? "border-blue-500 bg-blue-50"
|
||||
: "hover:bg-gray-50"
|
||||
}`}
|
||||
onClick={() => setSelected(item)}
|
||||
>
|
||||
<Card.Section withBorder inheritPadding py="xs">
|
||||
<Group justify="space-between">
|
||||
<Group>
|
||||
<IconVideoFilled />
|
||||
|
||||
{selected &&
|
||||
selected.original_name === item.original_name && (
|
||||
<IconPlayerRecordFilled
|
||||
size={14}
|
||||
color="red"
|
||||
className="animate-pulse"
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
<Menu withinPortal position="bottom-end" shadow="sm">
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="subtle" color="gray">
|
||||
<IconDots size={16} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
onClick={() => handleDelete(item)}
|
||||
leftSection={<IconTrash size={14} />}
|
||||
color="red"
|
||||
>
|
||||
Delete
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
</Card.Section>
|
||||
<Text mt={"xs"} size="sm" fw={500}>
|
||||
{item.name}
|
||||
</Text>
|
||||
|
||||
<Text mt="xs" c="dimmed" size="xs">
|
||||
{moment(Number(item.timestamp)).fromNow()}
|
||||
</Text>
|
||||
</Card>
|
||||
))}
|
||||
</ScrollArea>
|
||||
)}
|
||||
|
||||
{/* Player - Main content */}
|
||||
{records.length > 0 && (
|
||||
<Box className="md:col-span-2">
|
||||
{selected && (
|
||||
<ReactPlayer
|
||||
url={`${import.meta.env.VITE_BASE_URL}bids/record/${
|
||||
selected.original_name
|
||||
}`}
|
||||
controls
|
||||
width="100%"
|
||||
height="auto"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{records.length <= 0 && (
|
||||
<Box className="col-span-3">
|
||||
<Text className="text-center" c="dimmed">
|
||||
No video
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,189 +0,0 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Card,
|
||||
Group,
|
||||
Menu,
|
||||
Modal,
|
||||
ModalProps,
|
||||
ScrollArea,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconCodeCircle,
|
||||
IconDots,
|
||||
IconPlayerRecordFilled,
|
||||
IconTrash,
|
||||
} from "@tabler/icons-react";
|
||||
import moment from "moment";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import ReactJson from "react-json-view";
|
||||
import { updateMetadata } from "../../apis/bid";
|
||||
import { useConfirmStore } from "../../lib/zustand/use-confirm";
|
||||
import { IBid, IMetadata } from "../../system/type";
|
||||
import { getResponseDemo } from "../../utils";
|
||||
export interface IResponseDemoProps extends ModalProps {
|
||||
data: IBid | null;
|
||||
onUpdated?: () => void;
|
||||
}
|
||||
|
||||
export default function ResponseDemoModal({
|
||||
data,
|
||||
...props
|
||||
}: IResponseDemoProps) {
|
||||
const { setConfirm } = useConfirmStore();
|
||||
const [selected, setSelected] = useState<Record<string, any> | null>(null);
|
||||
|
||||
const [responseDemo, setResponseDemo] = useState<null | IMetadata>(null);
|
||||
|
||||
const dataRef = useRef<IBid | null>(data);
|
||||
// const responseDemo = useMemo(() => {
|
||||
// if (!data) return null;
|
||||
|
||||
// const values = getResponseDemo(data);
|
||||
|
||||
// if (Array.isArray(values?.value) && values?.value[0]) {
|
||||
// setSelected(values?.value[0]);
|
||||
// }
|
||||
|
||||
// return values;
|
||||
// }, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
const value = getResponseDemo(data);
|
||||
|
||||
if (Array.isArray(value?.value) && value?.value[0]) {
|
||||
setSelected(value?.value[0]);
|
||||
}
|
||||
|
||||
setResponseDemo(value);
|
||||
dataRef.current = data;
|
||||
}, [data]);
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!responseDemo || !selected) return;
|
||||
console.log(selected);
|
||||
setConfirm({
|
||||
handleOk: async () => {
|
||||
const prevData = responseDemo.value as Record<string, any>[];
|
||||
|
||||
const newData = prevData.filter(
|
||||
(item) => item?.uuid !== selected?.uuid
|
||||
);
|
||||
|
||||
const result = await updateMetadata(
|
||||
responseDemo.id,
|
||||
JSON.stringify(newData)
|
||||
);
|
||||
|
||||
if (!result?.data) return;
|
||||
|
||||
props.onUpdated?.();
|
||||
|
||||
setResponseDemo(result.data);
|
||||
if (result?.data?.value[0]) {
|
||||
setSelected(result?.data?.value[0]);
|
||||
} else {
|
||||
setSelected(null);
|
||||
}
|
||||
// props.onClose();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
classNames={{
|
||||
header: "!flex !justify-center w-full",
|
||||
}}
|
||||
{...props}
|
||||
size={"xl"}
|
||||
title={<Title order={3}>Response Demo</Title>}
|
||||
centered
|
||||
>
|
||||
<Box className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{/* Sidebar - Record list */}
|
||||
{responseDemo && responseDemo?.value.length > 0 && (
|
||||
<ScrollArea className="md:col-span-1 h-[600px] pr-2">
|
||||
{Array.isArray(responseDemo.value) &&
|
||||
responseDemo.value.map((item) => {
|
||||
return (
|
||||
<Card
|
||||
onClick={() => setSelected(item)}
|
||||
key={item.uuid}
|
||||
withBorder
|
||||
shadow="xs"
|
||||
radius="md"
|
||||
className={`mb-3 cursor-pointer transition-all ${"border-blue-500 bg-blue-50"}`}
|
||||
>
|
||||
<Card.Section withBorder inheritPadding py="xs">
|
||||
<Group justify="space-between">
|
||||
<Group>
|
||||
<IconCodeCircle />
|
||||
|
||||
{selected && selected?.uuid === item?.uuid && (
|
||||
<IconPlayerRecordFilled
|
||||
size={14}
|
||||
color="red"
|
||||
className="animate-pulse"
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Menu withinPortal position="bottom-end" shadow="sm">
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="subtle" color="gray">
|
||||
<IconDots size={16} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
onClick={handleDelete}
|
||||
leftSection={<IconTrash size={14} />}
|
||||
color="red"
|
||||
>
|
||||
Delete
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
</Card.Section>
|
||||
<Text mt={"xs"} size="sm" fw={500}>
|
||||
{dataRef.current?.name}
|
||||
</Text>
|
||||
|
||||
<Text mt="xs" c="dimmed" size="xs">
|
||||
{item?.timestamp &&
|
||||
moment(Number(item?.timestamp)).fromNow()}
|
||||
</Text>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</ScrollArea>
|
||||
)}
|
||||
|
||||
{/* Player - Main content */}
|
||||
{responseDemo?.value && selected && (
|
||||
<ScrollArea h={600} className="md:col-span-2">
|
||||
<ReactJson theme={"bespin"} src={selected || {}} />
|
||||
</ScrollArea>
|
||||
)}
|
||||
|
||||
{(!responseDemo ||
|
||||
(Array.isArray(responseDemo.value) &&
|
||||
responseDemo.value.length <= 0)) && (
|
||||
<Box className="col-span-3">
|
||||
<Text className="text-center" c="dimmed">
|
||||
No response
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,37 +1,21 @@
|
|||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Image,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { upperFirst, useDisclosure } from "@mantine/hooks";
|
||||
import { Badge, Box, Button, Image, Text, Tooltip } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import moment from "moment";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Socket } from "socket.io-client";
|
||||
import { getImagesWorking, updateBid } from "../../apis/bid";
|
||||
import { getImagesWorking } from "../../apis/bid";
|
||||
import { useStatusToolStore } from "../../lib/zustand/use-status-tool-store";
|
||||
import { IBid, IWebBid } from "../../system/type";
|
||||
import {
|
||||
cn,
|
||||
extractDomainSmart,
|
||||
findNearestClosingChild,
|
||||
getEarlyTrackingSeconds,
|
||||
getMode,
|
||||
isTimeReached,
|
||||
stringToColor,
|
||||
subtractSeconds,
|
||||
} from "../../utils";
|
||||
import ShowImageModal from "./show-image-modal";
|
||||
import {
|
||||
IconExternalLink,
|
||||
IconImageInPicture,
|
||||
IconLivePhoto,
|
||||
IconTestPipe,
|
||||
} from "@tabler/icons-react";
|
||||
import { useConfirmStore } from "../../lib/zustand/use-confirm";
|
||||
import { IconExternalLink, IconImageInPicture } from "@tabler/icons-react";
|
||||
export interface IWorkingPageProps {
|
||||
data: (IBid | IWebBid) & { type: string };
|
||||
socket: Socket;
|
||||
|
|
@ -54,8 +38,6 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
|
|||
|
||||
const { statusTool } = useStatusToolStore();
|
||||
|
||||
const { setConfirm } = useConfirmStore();
|
||||
|
||||
function isIBid(obj: IBid | IWebBid): obj is IBid {
|
||||
return "name" in obj;
|
||||
}
|
||||
|
|
@ -84,33 +66,6 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
|
|||
return !statusTool || statusTool !== "online" ? "Unknown" : "logout";
|
||||
};
|
||||
|
||||
const handleToogleMode = async () => {
|
||||
if (!isIBid(data)) return;
|
||||
|
||||
setConfirm({
|
||||
title: "Confirm changing this product's mode?",
|
||||
okButton: {
|
||||
value: "Sure",
|
||||
color: "blue",
|
||||
},
|
||||
handleOk: async () => {
|
||||
const metadata = data.metadata;
|
||||
|
||||
const modeKey = metadata.find((item) => item.key_name === "mode_key");
|
||||
|
||||
if (modeKey && modeKey.value) {
|
||||
if (String(modeKey.value) === "live") {
|
||||
modeKey.value = "sandbox";
|
||||
} else {
|
||||
modeKey.value = "live";
|
||||
}
|
||||
}
|
||||
|
||||
await updateBid({ ...data, metadata });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const updateImage = ({
|
||||
type,
|
||||
|
|
@ -223,7 +178,7 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
|
|||
!isTimeReached(
|
||||
subtractSeconds(
|
||||
data.close_time,
|
||||
getEarlyTrackingSeconds(data) || 0
|
||||
data.web_bid?.early_tracking_seconds || 0
|
||||
)
|
||||
) && (
|
||||
<Tooltip label={"Time to tracking"}>
|
||||
|
|
@ -233,7 +188,7 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
|
|||
>{`TT: ${moment(
|
||||
subtractSeconds(
|
||||
data.close_time,
|
||||
getEarlyTrackingSeconds(data) || 0
|
||||
data.web_bid?.early_tracking_seconds || 0
|
||||
)
|
||||
).format("HH:mm:ss DD/MM/YYYY")}`}</Text>
|
||||
</Tooltip>
|
||||
|
|
@ -263,43 +218,26 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
|
|||
</Box>
|
||||
|
||||
<Box className="absolute top-0 left-0 py-2 px-4 flex items-center gap-2 justify-between w-full">
|
||||
<Box className="flex justify-between w-full">
|
||||
<Box className="flex items-center gap-2">
|
||||
<Badge
|
||||
color={payloadLoginStatus?.login_status ? "green" : "red"}
|
||||
size="xs"
|
||||
>
|
||||
{statusLabel()}
|
||||
</Badge>
|
||||
<Box className="flex items-center gap-2">
|
||||
<Badge
|
||||
color={payloadLoginStatus?.login_status ? "green" : "red"}
|
||||
size="xs"
|
||||
>
|
||||
{statusLabel()}
|
||||
</Badge>
|
||||
|
||||
<Badge
|
||||
color={stringToColor(
|
||||
isIBid(data)
|
||||
? extractDomainSmart(data.web_bid.origin_url)
|
||||
: extractDomainSmart(data.origin_url)
|
||||
)}
|
||||
size="xs"
|
||||
>
|
||||
{isIBid(data)
|
||||
<Badge
|
||||
color={stringToColor(
|
||||
isIBid(data)
|
||||
? extractDomainSmart(data.web_bid.origin_url)
|
||||
: extractDomainSmart(data.origin_url)}
|
||||
</Badge>
|
||||
</Box>
|
||||
{isIBid(data) && (
|
||||
<Tooltip label={upperFirst(getMode(data))}>
|
||||
<ActionIcon
|
||||
onClick={handleToogleMode}
|
||||
color={getMode(data) === "live" ? "teal" : "orange"}
|
||||
size="xs"
|
||||
>
|
||||
{getMode(data) === "live" ? (
|
||||
<IconLivePhoto size={14} />
|
||||
) : (
|
||||
<IconTestPipe size={14} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
: extractDomainSmart(data.origin_url)
|
||||
)}
|
||||
size="xs"
|
||||
>
|
||||
{isIBid(data)
|
||||
? extractDomainSmart(data.web_bid.origin_url)
|
||||
: extractDomainSmart(data.origin_url)}
|
||||
</Badge>
|
||||
</Box>
|
||||
|
||||
{isIBid(data) && moment(data.close_time).isSame(moment(), "day") && (
|
||||
|
|
|
|||
|
|
@ -116,3 +116,5 @@ export const removeFalsy = (data: { [key: string]: string | number }) => {
|
|||
return prev;
|
||||
}, {} as { [key: string]: string | number });
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,26 +12,18 @@ import { useDisclosure } from "@mantine/hooks";
|
|||
import {
|
||||
IconAd,
|
||||
IconAdOff,
|
||||
IconCode,
|
||||
IconEdit,
|
||||
IconHammer,
|
||||
IconHistory,
|
||||
IconMenu,
|
||||
IconPlayerRecord,
|
||||
IconPlus,
|
||||
IconTrash,
|
||||
} from "@tabler/icons-react";
|
||||
import _ from "lodash";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { deleteBid, getBids, toggleBid } from "../apis/bid";
|
||||
import {
|
||||
BidModal,
|
||||
RecordModal,
|
||||
ResponseDemoModal,
|
||||
ShowHistoriesModal,
|
||||
} from "../components/bid";
|
||||
import { BidModal, ShowHistoriesModal } from "../components/bid";
|
||||
import DeleteRowAction from "../components/bid/delete-row-action";
|
||||
import ShowHistoriesApiModal from "../components/bid/show-histories-api/show-histories-api-modal";
|
||||
import { haveHistories } from "../constant";
|
||||
import Table from "../lib/table/table";
|
||||
import { IColumn, TRefTableFn } from "../lib/table/type";
|
||||
|
|
@ -39,12 +31,8 @@ import { useChoosesStore } from "../lib/zustand/use-chooses-store";
|
|||
import { useConfirmStore } from "../lib/zustand/use-confirm";
|
||||
import { mappingStatusColors } from "../system/constants";
|
||||
import { IBid } from "../system/type";
|
||||
import {
|
||||
extractDomainSmart,
|
||||
formatTime,
|
||||
getMode,
|
||||
getResponseDemo,
|
||||
} from "../utils";
|
||||
import { extractDomainSmart, formatTime } from "../utils";
|
||||
import ShowHistoriesApiModal from "../components/bid/show-histories-api/show-histories-api-modal";
|
||||
|
||||
export default function Bids() {
|
||||
const refTableFn: TRefTableFn<IBid> = useRef({});
|
||||
|
|
@ -59,8 +47,6 @@ export default function Bids() {
|
|||
|
||||
const [openedHistoriesView, openedHistoriesViewModal] = useDisclosure(false);
|
||||
const [openedBid, bidModal] = useDisclosure(false);
|
||||
const [openedRecord, recordModal] = useDisclosure(false);
|
||||
const [openedResponseDemo, responseDemoModal] = useDisclosure(false);
|
||||
|
||||
const columns: IColumn<IBid>[] = [
|
||||
{
|
||||
|
|
@ -199,29 +185,6 @@ export default function Bids() {
|
|||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "metadata",
|
||||
title: "Mode",
|
||||
typeFilter: "none",
|
||||
style: {
|
||||
style: {
|
||||
minWidth: "100px",
|
||||
},
|
||||
},
|
||||
renderRow(row) {
|
||||
return (
|
||||
<Box className="flex items-center justify-center">
|
||||
<Badge
|
||||
color={getMode(row) === "live" ? "teal" : "orange"}
|
||||
size="sm"
|
||||
>
|
||||
{row.metadata.find((item) => item.key_name === "mode_key")
|
||||
?.value || "Live"}
|
||||
</Badge>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const handleDelete = (bid: IBid) => {
|
||||
|
|
@ -375,26 +338,12 @@ export default function Bids() {
|
|||
{row.status === "biding" ? "Disable" : "Enable"}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
setClickData(row);
|
||||
|
||||
if (getResponseDemo(row)) {
|
||||
responseDemoModal.open();
|
||||
} else {
|
||||
recordModal.open();
|
||||
}
|
||||
}}
|
||||
leftSection={
|
||||
getResponseDemo(row) ? (
|
||||
<IconCode size={14} />
|
||||
) : (
|
||||
<IconPlayerRecord size={14} />
|
||||
)
|
||||
}
|
||||
{/* <Menu.Item
|
||||
onClick={() => handleDelete(row)}
|
||||
leftSection={<IconTrash color="red" size={14} />}
|
||||
>
|
||||
{getResponseDemo(row) ? "Response demo" : "Record"}
|
||||
</Menu.Item>
|
||||
Delete
|
||||
</Menu.Item> */}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
|
|
@ -426,7 +375,6 @@ export default function Bids() {
|
|||
}}
|
||||
data={clickData}
|
||||
/>
|
||||
|
||||
<BidModal
|
||||
onUpdated={() => {
|
||||
if (refTableFn.current?.fetchData) {
|
||||
|
|
@ -444,40 +392,6 @@ export default function Bids() {
|
|||
data={clickData}
|
||||
/>
|
||||
|
||||
<RecordModal
|
||||
onUpdated={() => {
|
||||
if (refTableFn.current?.fetchData) {
|
||||
refTableFn.current.fetchData();
|
||||
}
|
||||
|
||||
setClickData(null);
|
||||
}}
|
||||
opened={openedRecord}
|
||||
onClose={() => {
|
||||
recordModal.close();
|
||||
|
||||
setClickData(null);
|
||||
}}
|
||||
data={clickData}
|
||||
/>
|
||||
|
||||
<ResponseDemoModal
|
||||
onUpdated={() => {
|
||||
if (refTableFn.current?.fetchData) {
|
||||
refTableFn.current.fetchData();
|
||||
}
|
||||
|
||||
setClickData(null);
|
||||
}}
|
||||
opened={openedResponseDemo}
|
||||
onClose={() => {
|
||||
responseDemoModal.close();
|
||||
|
||||
setClickData(null);
|
||||
}}
|
||||
data={clickData}
|
||||
/>
|
||||
|
||||
{openedHistoriesView && (
|
||||
<ShowHistoriesApiModal
|
||||
onUpdated={() => {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ export default function DashBoard() {
|
|||
const RETRY_CONNECT = useRef(2);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
socket.connect();
|
||||
|
||||
socket.on("connect", () => {
|
||||
|
|
@ -79,7 +78,6 @@ export default function DashBoard() {
|
|||
setWorkingData(newData);
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
return () => {
|
||||
console.log("🔌 Cleanup WebSocket listeners...");
|
||||
socket.off("adminBidsUpdated");
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export interface IAdmin extends ITimestamp {
|
||||
id: number;
|
||||
email: string;
|
||||
|
|
@ -64,12 +63,6 @@ export interface IWebBid extends ITimestamp {
|
|||
scrap_config: IScrapConfig;
|
||||
}
|
||||
|
||||
export interface IMetadata extends ITimestamp {
|
||||
id: number;
|
||||
key_name: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export interface IBid extends ITimestamp {
|
||||
id: number;
|
||||
max_price: number;
|
||||
|
|
@ -88,7 +81,6 @@ export interface IBid extends ITimestamp {
|
|||
status: "biding" | "out-bid" | "win-bid";
|
||||
histories: IHistory[];
|
||||
web_bid: IWebBid;
|
||||
metadata: IMetadata[];
|
||||
}
|
||||
|
||||
export interface IConfig extends ITimestamp {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import moment from "moment";
|
||||
import { IBid, IMetadata, IWebBid } from "../system/type";
|
||||
import _ from "lodash";
|
||||
import { IWebBid } from "../system/type";
|
||||
import _ from 'lodash'
|
||||
|
||||
export function cn(...args: ClassValue[]) {
|
||||
return twMerge(clsx(args));
|
||||
|
|
@ -204,6 +204,7 @@ export function extractDomainSmart(url: string) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
export function findNearestClosingChild(webBid: IWebBid) {
|
||||
const now = Date.now();
|
||||
|
||||
|
|
@ -254,37 +255,11 @@ export function formatTimeFromMinutes(minutes: number): string {
|
|||
const hours = Math.floor((minutes % (60 * 24)) / 60);
|
||||
const mins = minutes % 60;
|
||||
|
||||
let result = "";
|
||||
let result = '';
|
||||
|
||||
if (days > 0) result += `${days} ${days > 1 ? "days" : "day"} `;
|
||||
if (hours > 0) result += `${hours} ${hours > 1 ? "hours" : "hour"} `;
|
||||
if (mins > 0 || result === "") result += `${mins} minutes`;
|
||||
if (days > 0) result += `${days} ${days > 1? 'days' :'day'} `;
|
||||
if (hours > 0) result += `${hours} ${hours > 1 ? 'hours' : 'hour'} `;
|
||||
if (mins > 0 || result === '') result += `${mins} minutes`;
|
||||
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
export const getMode = (row: IBid) => {
|
||||
return (
|
||||
row.metadata.find((item) => item.key_name === "mode_key")?.value || "live"
|
||||
);
|
||||
};
|
||||
|
||||
export const getEarlyTrackingSeconds = (row: IBid) => {
|
||||
const mode = getMode(row);
|
||||
|
||||
return (
|
||||
row.metadata.find(
|
||||
(item) => item.key_name === `early_tracking_seconds_${mode}`
|
||||
)?.value || row.web_bid.early_tracking_seconds
|
||||
);
|
||||
};
|
||||
|
||||
export const getResponseDemo = (data: IBid) => {
|
||||
if (!data?.metadata) return null;
|
||||
|
||||
return (
|
||||
(data?.metadata.find(
|
||||
(item) => item.key_name === "demo_response"
|
||||
) as IMetadata) || null
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"createdAt":1752455341798}
|
||||
{"createdAt":1748825131474}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AdminsModule } from '../admins/admins.module';
|
||||
import { NotificationModule } from '../notification/notification.module';
|
||||
|
|
@ -34,8 +34,6 @@ import { AdminConfigsController } from './controllers/admin/admin-configs.contro
|
|||
import { BidMetadatasService } from './services/bid-metadatas.service';
|
||||
import { BidMetadata } from './entities/bid-metadata.entity';
|
||||
import { HotItemApi } from './apis/hot-item.api';
|
||||
import { MailsModule } from '../mails/mails.module';
|
||||
import { AdminBidMetadataController } from './controllers/admin/admin-bid-metadata.controller';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -63,7 +61,6 @@ import { AdminBidMetadataController } from './controllers/admin/admin-bid-metada
|
|||
AdminSendMessageHistoriesController,
|
||||
AdminDashboardController,
|
||||
AdminConfigsController,
|
||||
AdminBidMetadataController,
|
||||
],
|
||||
providers: [
|
||||
BidsService,
|
||||
|
|
@ -85,12 +82,10 @@ import { AdminBidMetadataController } from './controllers/admin/admin-bid-metada
|
|||
exports: [
|
||||
BotTelegramApi,
|
||||
SendMessageHistoriesService,
|
||||
BidMetadatasService,
|
||||
BidsService,
|
||||
ConfigsService,
|
||||
DashboardService,
|
||||
HotItemApi,
|
||||
WebBidsService,
|
||||
],
|
||||
})
|
||||
export class BidsModule {}
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
import { Body, Controller, Delete, Param, Post, Put } from '@nestjs/common';
|
||||
import { BidMetadata } from '../../entities/bid-metadata.entity';
|
||||
import { BidMetadatasService } from '../../services/bid-metadatas.service';
|
||||
|
||||
@Controller('admin/bid-metadatas')
|
||||
export class AdminBidMetadataController {
|
||||
constructor(private readonly bidMetadataService: BidMetadatasService) {}
|
||||
|
||||
@Delete(':id')
|
||||
async delete(@Param('id') id: BidMetadata['id']) {
|
||||
return await this.bidMetadataService.delete(id);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
async update(
|
||||
@Param('id') id: BidMetadata['id'],
|
||||
@Body('value') value: string,
|
||||
) {
|
||||
return await this.bidMetadataService.update(id, value);
|
||||
}
|
||||
}
|
||||
|
|
@ -67,19 +67,4 @@ export class AdminBidsController {
|
|||
) {
|
||||
return await this.bidsService.getImagesWorking(id, type);
|
||||
}
|
||||
|
||||
@Get('record/:name')
|
||||
async getRecord(@Param('name') name: string, @Res() res: Response) {
|
||||
return await this.bidsService.getRecord(name, res);
|
||||
}
|
||||
|
||||
@Get('records/:id')
|
||||
async getRecords(@Param('id') id: Bid['id']) {
|
||||
return await this.bidsService.getRecords(id);
|
||||
}
|
||||
|
||||
@Delete('record/:name')
|
||||
async deleteRecord(@Param('name') name: string) {
|
||||
return await this.bidsService.deleteRecord(name);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import AppResponse from '@/response/app-response';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
|
|
@ -6,7 +5,6 @@ import {
|
|||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UploadedFile,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
|
|
@ -14,9 +12,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
|||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { memoryStorage } from 'multer';
|
||||
import { ClientInfoUpdateBidDto } from '../../dto/bid/client-info-update-bid.dto';
|
||||
import { ClientUpdateBidDto } from '../../dto/bid/client-update-bid.dto';
|
||||
import { ClientUpdateLoginStatusDto } from '../../dto/bid/client-update-login-status.dto';
|
||||
import { CreateBidDto } from '../../dto/bid/create-bid.dto';
|
||||
import { UpdateStatusByPriceDto } from '../../dto/bid/update-status-by-price.dto';
|
||||
import { Bid } from '../../entities/bid.entity';
|
||||
|
|
@ -24,6 +20,10 @@ import { WebBid } from '../../entities/wed-bid.entity';
|
|||
import { BidsService } from '../../services/bids.service';
|
||||
import { WebBidsService } from '../../services/web-bids.service';
|
||||
import { Event } from '../../utils/events';
|
||||
import AppResponse from '@/response/app-response';
|
||||
import { ClientUpdateLoginStatusDto } from '../../dto/bid/client-update-login-status.dto';
|
||||
import { UpdateBidDto } from '../../dto/bid/update-bid.dto';
|
||||
import { ClientInfoUpdateBidDto } from '../../dto/bid/client-info-update-bid.dto';
|
||||
|
||||
@Controller('bids')
|
||||
export class BidsController {
|
||||
|
|
@ -81,15 +81,6 @@ export class BidsController {
|
|||
return this.bidsService.updateStatusWork(id, type, image);
|
||||
}
|
||||
|
||||
@Post('upload-record/:id')
|
||||
@UseInterceptors(FileInterceptor('video', { storage: memoryStorage() }))
|
||||
async uploadRecord(
|
||||
@Param('id') id: Bid['id'],
|
||||
@UploadedFile() video: Express.Multer.File,
|
||||
) {
|
||||
return this.bidsService.uploadRecord(id, video);
|
||||
}
|
||||
|
||||
@Post('update-login-status')
|
||||
async updateLoginStatus(@Body() data: ClientUpdateLoginStatusDto) {
|
||||
return await this.bidsService.emitLoginStatus(data);
|
||||
|
|
@ -111,13 +102,4 @@ export class BidsController {
|
|||
|
||||
return AppResponse.toResponse({ code });
|
||||
}
|
||||
|
||||
@Post('hook-action')
|
||||
async hookAction(
|
||||
@Body() data: any,
|
||||
@Query('id') id: Bid['id'],
|
||||
@Query('type') type: 'action' | 'api',
|
||||
) {
|
||||
return this.bidsService.hookAction({ id, type }, data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { IsArray, IsNumber, IsOptional } from 'class-validator';
|
||||
import { IsNumber, IsOptional } from 'class-validator';
|
||||
|
||||
export class ClientInfoUpdateBidDto {
|
||||
@IsNumber()
|
||||
|
|
@ -12,8 +12,4 @@ export class ClientInfoUpdateBidDto {
|
|||
@IsNumber()
|
||||
@IsOptional()
|
||||
plus_price: number;
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
metadata: Record<string, any>[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { Optional } from '@nestjs/common';
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
|
|
@ -22,8 +21,4 @@ export class CreateBidDto {
|
|||
@IsNumber()
|
||||
@IsOptional()
|
||||
plus_price: number;
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
metadata: Record<string, any>[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { IsArray, IsNumber, IsOptional } from 'class-validator';
|
||||
import { IsNumber, IsOptional } from 'class-validator';
|
||||
|
||||
export class UpdateBidDto {
|
||||
@IsNumber()
|
||||
|
|
@ -12,8 +12,4 @@ export class UpdateBidDto {
|
|||
@IsNumber()
|
||||
@IsOptional()
|
||||
plus_price: number;
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
metadata: Record<string, any>[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,52 +7,10 @@ import {
|
|||
} from 'typeorm';
|
||||
import { Bid } from './bid.entity';
|
||||
import { Timestamp } from './timestamp';
|
||||
import { WebBid } from './wed-bid.entity';
|
||||
|
||||
@Entity('bid_metadata')
|
||||
@Unique(['key_name', 'bid'])
|
||||
export class BidMetadata extends Timestamp {
|
||||
// value 'live' | 'sandbox'
|
||||
public static MODE_KEY = 'mode_key';
|
||||
|
||||
public static DEMO_RESPONSE = 'demo_response';
|
||||
public static LATEST_RECROD_LINK = 'lastest_record_link';
|
||||
|
||||
// value is minutes esg: arrival_offset_seconds of web bid parent
|
||||
public static ARRIAVAL_OFFSET_SECONDS_LIVE = 'arrival_offset_seconds_live';
|
||||
public static ARRIAVAL_OFFSET_SECONDS_SANDBOX =
|
||||
'arrival_offset_seconds_sandbox';
|
||||
|
||||
// value is minutes esg: early_tracking_seconds of web bid parent
|
||||
public static EARLY_TRACKING_SECONDS_LIVE = 'early_tracking_seconds_live';
|
||||
public static EARLY_TRACKING_SECONDS_SANDBOX =
|
||||
'early_tracking_seconds_sandbox';
|
||||
|
||||
public static DEFAULT_META_DATA = (webBid: WebBid) => {
|
||||
return [
|
||||
{
|
||||
key_name: BidMetadata.MODE_KEY,
|
||||
value: JSON.stringify('live'),
|
||||
},
|
||||
{
|
||||
key_name: BidMetadata.ARRIAVAL_OFFSET_SECONDS_LIVE,
|
||||
value: String(webBid.arrival_offset_seconds),
|
||||
},
|
||||
{
|
||||
key_name: BidMetadata.EARLY_TRACKING_SECONDS_LIVE,
|
||||
value: String(webBid.early_tracking_seconds),
|
||||
},
|
||||
{
|
||||
key_name: BidMetadata.ARRIAVAL_OFFSET_SECONDS_SANDBOX,
|
||||
value: String(webBid.arrival_offset_seconds),
|
||||
},
|
||||
{
|
||||
key_name: BidMetadata.EARLY_TRACKING_SECONDS_SANDBOX,
|
||||
value: String(webBid.early_tracking_seconds),
|
||||
},
|
||||
] as BidMetadata[];
|
||||
};
|
||||
|
||||
@PrimaryGeneratedColumn('increment')
|
||||
id: number;
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,6 @@ export class Bid extends Timestamp {
|
|||
@ManyToOne(() => WebBid, (web) => web.children, { onDelete: 'CASCADE' })
|
||||
web_bid: WebBid;
|
||||
|
||||
@OneToMany(() => BidMetadata, (metadata) => metadata.bid, { cascade: true })
|
||||
@OneToMany(() => BidMetadata, (metadata) => metadata.bid)
|
||||
metadata: BidMetadata[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
import { Timestamp } from './timestamp';
|
||||
import { Bid } from './bid.entity';
|
||||
import { Exclude } from 'class-transformer';
|
||||
import { ScrapItem } from '@/modules/scraps/entities/scrap-item.entity';
|
||||
import { ScrapConfig } from '@/modules/scraps/entities/scrap-config.entity';
|
||||
|
||||
@Entity('web_bids')
|
||||
|
|
|
|||
|
|
@ -21,14 +21,10 @@ export class ImageCompressionPipe implements PipeTransform {
|
|||
throw new BadRequestException('No file provided');
|
||||
}
|
||||
|
||||
const isVideo = file.mimetype === 'video/mp4';
|
||||
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
const isImage = ALLOWED_IMAGE_EXT.includes(ext);
|
||||
|
||||
if (!isImage && !isVideo) {
|
||||
if (!ALLOWED_IMAGE_EXT.includes(ext)) {
|
||||
throw new BadRequestException(
|
||||
`File "${file.originalname}" is not a valid image or video.`,
|
||||
`File "${file.originalname}" is not a valid image.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -40,37 +36,16 @@ export class ImageCompressionPipe implements PipeTransform {
|
|||
const files = readdirSync(uploadDir);
|
||||
files.forEach((file) => {
|
||||
const filePath = path.join(uploadDir, file);
|
||||
if (/\.(jpg|jpeg|png|webp|mp4)$/i.test(file)) {
|
||||
if (/\.(jpg|jpeg|png|webp)$/i.test(file)) {
|
||||
unlinkSync(filePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let filename = null;
|
||||
|
||||
if (isVideo) {
|
||||
filename = this.options.unique_name
|
||||
? `${Date.now()}-${file.originalname}`
|
||||
: file.originalname;
|
||||
|
||||
const filePath = path.join(uploadDir, filename);
|
||||
// Ghi trực tiếp buffer ra file
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const fs = require('fs');
|
||||
fs.writeFile(filePath, file.buffer, (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return { filename };
|
||||
}
|
||||
|
||||
filename = this.options.unique_name
|
||||
const filename = this.options.unique_name
|
||||
? `${Date.now()}-${this.resize.width || Constant.DEFAULT_RESIZE}x${this.resize.height || Constant.DEFAULT_RESIZE}-${file.originalname.replace(ext, '.webp')}`
|
||||
: file.originalname.replace(ext, '.webp');
|
||||
|
||||
const filePath = path.join(uploadDir, filename);
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ export class BidHistoriesService {
|
|||
|
||||
// Gửi thông tin bid đến bot telegram
|
||||
const botData = { ...bid, histories: response };
|
||||
// this.botTelegramApi.sendBidInfo(botData);
|
||||
this.botTelegramApi.sendBidInfo(botData);
|
||||
|
||||
// Send event thống place bid
|
||||
this.eventEmitter.emit(Event.BID_SUBMITED, botData);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,8 @@
|
|||
import {
|
||||
BadRequestException,
|
||||
HttpStatus,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { BidMetadata } from '../entities/bid-metadata.entity';
|
||||
import { Bid } from '../entities/bid.entity';
|
||||
import AppResponse from '@/response/app-response';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
|
||||
@Injectable()
|
||||
export class BidMetadatasService {
|
||||
|
|
@ -48,84 +41,4 @@ export class BidMetadatasService {
|
|||
await this.bidMetadataRepo.save(toSave);
|
||||
return true;
|
||||
}
|
||||
|
||||
async update(id: BidMetadata['id'], value: string) {
|
||||
const result = await this.bidMetadataRepo.update(id, { value: value });
|
||||
|
||||
if (!result) throw new BadRequestException(AppResponse.toResponse(null));
|
||||
|
||||
const data = await this.bidMetadataRepo.findOne({ where: { id } });
|
||||
|
||||
return AppResponse.toResponse(plainToClass(BidMetadata, data));
|
||||
}
|
||||
|
||||
getMode = (metadatas: BidMetadata[]) => {
|
||||
const result = metadatas.find(
|
||||
(item) => item.key_name === BidMetadata.MODE_KEY,
|
||||
);
|
||||
|
||||
return result ? result.value : null;
|
||||
};
|
||||
|
||||
getDemoResponse = (metadatas: BidMetadata[]) => {
|
||||
const result = metadatas.find(
|
||||
(item) => item.key_name === BidMetadata.DEMO_RESPONSE,
|
||||
);
|
||||
|
||||
return result ? result.value : null;
|
||||
};
|
||||
|
||||
getLatestRecordLink = (metadatas: BidMetadata[]) => {
|
||||
const result = metadatas.find(
|
||||
(item) => item.key_name === BidMetadata.LATEST_RECROD_LINK,
|
||||
);
|
||||
|
||||
return result ? result.value : null;
|
||||
};
|
||||
|
||||
getArrivalOffsetSecondsSandbox = (metadatas: BidMetadata[]) => {
|
||||
const result = metadatas.find(
|
||||
(item) => item.key_name === BidMetadata.ARRIAVAL_OFFSET_SECONDS_SANDBOX,
|
||||
);
|
||||
|
||||
return result ? Number(result.value) : null;
|
||||
};
|
||||
|
||||
getArrivalOffsetSecondsLive = (metadatas: BidMetadata[]) => {
|
||||
const result = metadatas.find(
|
||||
(item) => item.key_name === BidMetadata.ARRIAVAL_OFFSET_SECONDS_LIVE,
|
||||
);
|
||||
|
||||
return result ? Number(result.value) : null;
|
||||
};
|
||||
|
||||
getArrivalOffsetSecondsByMode = (metadatas: BidMetadata[]) => {
|
||||
const mode = this.getMode(metadatas) || 'live';
|
||||
|
||||
if (mode === 'live') {
|
||||
return this.getArrivalOffsetSecondsLive(metadatas);
|
||||
}
|
||||
|
||||
return this.getArrivalOffsetSecondsSandbox(metadatas);
|
||||
};
|
||||
|
||||
isSandbox(metadatas: BidMetadata[]) {
|
||||
return this.getMode(metadatas) !== 'live';
|
||||
}
|
||||
|
||||
async delete(id: Bid['id']) {
|
||||
const bid = await this.bidMetadataRepo.findOne({ where: { id } });
|
||||
|
||||
if (!bid)
|
||||
throw new NotFoundException(
|
||||
AppResponse.toResponse(false, {
|
||||
message: 'Meta is not found',
|
||||
status_code: HttpStatus.NOT_FOUND,
|
||||
}),
|
||||
);
|
||||
|
||||
await this.bidMetadataRepo.delete({ id: bid.id });
|
||||
|
||||
return AppResponse.toResponse(true, { message: 'Delete success !' });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
|||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { Response } from 'express';
|
||||
import * as fs from 'fs';
|
||||
import { existsSync, readdirSync } from 'fs';
|
||||
import * as _ from 'lodash';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import {
|
||||
FilterOperator,
|
||||
FilterSuffix,
|
||||
|
|
@ -22,13 +19,7 @@ import {
|
|||
import { Column } from 'nestjs-paginate/lib/helper';
|
||||
import { join } from 'path';
|
||||
import AppResponse from 'src/response/app-response';
|
||||
import {
|
||||
extractModelId,
|
||||
isTimePassedByMinutes,
|
||||
isTimeReached,
|
||||
parseVideoFileName,
|
||||
subtractMinutes,
|
||||
} from 'src/ultils';
|
||||
import { extractModelId, isTimeReached, subtractMinutes } from 'src/ultils';
|
||||
import { In, IsNull, Not, Repository } from 'typeorm';
|
||||
import { ClientUpdateBidDto } from '../dto/bid/client-update-bid.dto';
|
||||
import { ClientUpdateLoginStatusDto } from '../dto/bid/client-update-login-status.dto';
|
||||
|
|
@ -36,13 +27,12 @@ import { CreateBidDto } from '../dto/bid/create-bid.dto';
|
|||
import { UpdateBidDto } from '../dto/bid/update-bid.dto';
|
||||
import { UpdateStatusByPriceDto } from '../dto/bid/update-status-by-price.dto';
|
||||
import { BidHistory } from '../entities/bid-history.entity';
|
||||
import { BidMetadata } from '../entities/bid-metadata.entity';
|
||||
import { Bid } from '../entities/bid.entity';
|
||||
import { ImageCompressionPipe } from '../pipes/image-compression-pipe';
|
||||
import { Constant } from '../utils/constant';
|
||||
import { Event } from '../utils/events';
|
||||
import { BidMetadatasService } from './bid-metadatas.service';
|
||||
import { WebBidsService } from './web-bids.service';
|
||||
import { BidMetadatasService } from './bid-metadatas.service';
|
||||
|
||||
@Injectable()
|
||||
export class BidsService {
|
||||
|
|
@ -93,7 +83,6 @@ export class BidsService {
|
|||
relations: {
|
||||
histories: true,
|
||||
web_bid: true,
|
||||
metadata: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -126,78 +115,21 @@ export class BidsService {
|
|||
|
||||
const webBid = await this.webBidsService.createByUrl(data.url);
|
||||
|
||||
let metadata = BidMetadata.DEFAULT_META_DATA(webBid);
|
||||
|
||||
if (data.metadata) {
|
||||
metadata = metadata.map((item) => {
|
||||
const reqData = data.metadata.find(
|
||||
(i) => i?.key_name === item.key_name,
|
||||
);
|
||||
|
||||
if (!reqData) return { ...item };
|
||||
|
||||
return {
|
||||
...item,
|
||||
value:
|
||||
item.key_name === BidMetadata.MODE_KEY
|
||||
? JSON.stringify(reqData.value)
|
||||
: reqData.value <= 0
|
||||
? item.value
|
||||
: String(reqData.value),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const result = await this.bidsRepo.save({
|
||||
...data,
|
||||
model,
|
||||
web_bid: webBid,
|
||||
metadata,
|
||||
});
|
||||
|
||||
await this.emitAllBidEvent();
|
||||
|
||||
const warnings = [];
|
||||
|
||||
if (!webBid.username || !webBid.password) {
|
||||
// Add warning message
|
||||
warnings.push(
|
||||
`Account setup for ${webBid.origin_url} website is not yet complete.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!webBid.active) {
|
||||
// Add warning message
|
||||
warnings.push(
|
||||
`${webBid.origin_url} is disabled. Please enable to continue.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (warnings.length) {
|
||||
// Send event warning
|
||||
this.eventEmitter.emit(Event.SEND_WARNING, {
|
||||
title: `System Warning: Abnormal Data Detected`,
|
||||
messages: warnings,
|
||||
});
|
||||
}
|
||||
|
||||
// Send event success
|
||||
this.eventEmitter.emit(Event.SEND_SUCCESS, {
|
||||
title: 'Successfully Added Product',
|
||||
messages: [`Successfully added product to bid list: ${data.url}`],
|
||||
});
|
||||
|
||||
return AppResponse.toResponse(
|
||||
response ? response(result) : plainToClass(Bid, result),
|
||||
);
|
||||
}
|
||||
|
||||
async update(id: Bid['id'], { metadata = [], ...data }: UpdateBidDto) {
|
||||
const prev = await this.bidsRepo.findOne({
|
||||
where: { id },
|
||||
relations: { web_bid: true, metadata: true },
|
||||
});
|
||||
|
||||
async update(id: Bid['id'], data: UpdateBidDto) {
|
||||
const prev = await this.bidsRepo.findOne({ where: { id } });
|
||||
if (!prev) {
|
||||
throw new NotFoundException(
|
||||
AppResponse.toResponse(false, {
|
||||
|
|
@ -207,46 +139,13 @@ export class BidsService {
|
|||
);
|
||||
}
|
||||
|
||||
if (metadata) {
|
||||
await this.bidMetadatasService.bidMetadataRepo.upsert(
|
||||
metadata.map((item) => {
|
||||
return { ...item, bid: { id }, value: JSON.stringify(item.value) };
|
||||
}),
|
||||
['id', 'bid', 'key_name'],
|
||||
);
|
||||
}
|
||||
|
||||
let result = null;
|
||||
if (!prev.close_time) {
|
||||
// Trường hợp chưa có close_time => update đơn giản
|
||||
result = await this.bidsRepo.update(id, {
|
||||
...data,
|
||||
});
|
||||
} else {
|
||||
// Trường hợp đã có close_time => kiểm tra arrival_offset_seconds có thay đổi không
|
||||
const arrival_offset_seconds =
|
||||
this.bidMetadatasService.getArrivalOffsetSecondsByMode(
|
||||
metadata as BidMetadata[],
|
||||
);
|
||||
const prev_arrival_offset_seconds =
|
||||
this.bidMetadatasService.getArrivalOffsetSecondsByMode(prev.metadata);
|
||||
|
||||
if (arrival_offset_seconds !== prev_arrival_offset_seconds) {
|
||||
// Nếu offset thay đổi thì cần cập nhật lại start_bid_time
|
||||
const start_bid_time = arrival_offset_seconds
|
||||
? subtractMinutes(prev.close_time, arrival_offset_seconds / 60)
|
||||
: prev.start_bid_time;
|
||||
|
||||
result = await this.bidsRepo.update(id, {
|
||||
...data,
|
||||
start_bid_time,
|
||||
});
|
||||
} else {
|
||||
result = await this.bidsRepo.update(id, {
|
||||
...data,
|
||||
});
|
||||
}
|
||||
}
|
||||
const result = await this.bidsRepo.update(id, {
|
||||
...data,
|
||||
// status:
|
||||
// prev.max_price + prev.plus_price > data.max_price
|
||||
// ? 'out-bid'
|
||||
// : prev.status,
|
||||
});
|
||||
|
||||
if (!result) throw new BadRequestException(false);
|
||||
|
||||
|
|
@ -255,18 +154,6 @@ export class BidsService {
|
|||
return AppResponse.toResponse(true);
|
||||
}
|
||||
|
||||
async getBidForClientUpdate(id: Bid['id']) {
|
||||
return await this.bidsRepo.findOne({
|
||||
where: { id },
|
||||
relations: { histories: true, web_bid: true, metadata: true },
|
||||
order: {
|
||||
histories: {
|
||||
price: 'DESC',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async toggle(id: Bid['id']) {
|
||||
const bid = await this.bidsRepo.findOne({
|
||||
where: { id },
|
||||
|
|
@ -321,49 +208,20 @@ export class BidsService {
|
|||
return AppResponse.toResponse(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Workflow
|
||||
* START
|
||||
* |
|
||||
* |--> Tìm bid theo id --> Không có? --> Throw 404
|
||||
* |
|
||||
* |--> Nếu chưa có thời gian bắt đầu --> Tính offset --> Gán start_time
|
||||
* |
|
||||
* |--> Nếu đã hết giờ:
|
||||
* | |--> Nếu outbid --> Gán status 'out-bid'
|
||||
* | |--> Else --> Gán status 'win-bid'
|
||||
* |
|
||||
* |--> Nếu chưa hết giờ:
|
||||
* | |--> Nếu vượt giới hạn --> Gán status 'out-bid'
|
||||
* |
|
||||
* |--> Nếu close_time mới > cũ --> cập nhật
|
||||
* |--> Nếu có model mới và chưa có model --> gán
|
||||
* |
|
||||
* |--> Gọi `save(...)` để lưu lại DB
|
||||
* |--> Nếu có metadata --> gọi `upsert`
|
||||
* |
|
||||
* |--> Gửi sự kiện emitAllBidEvent
|
||||
* |--> Nếu status là out-bid hoặc win-bid --> gửi notification
|
||||
* |
|
||||
* * |--> Trả response
|
||||
* END
|
||||
*/
|
||||
async clientUpdate(
|
||||
id: Bid['id'],
|
||||
{ close_time, model, metadata, ...data }: ClientUpdateBidDto, // Nhận dữ liệu cập nhật
|
||||
) {
|
||||
// // Tìm kiếm phiên đấu giá trong database theo id
|
||||
// let bid = await this.bidsRepo.findOne({
|
||||
// where: { id },
|
||||
// relations: { histories: true, web_bid: true, metadata: true },
|
||||
// order: {
|
||||
// histories: {
|
||||
// price: 'DESC',
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
|
||||
let bid = await this.getBidForClientUpdate(id);
|
||||
// Tìm kiếm phiên đấu giá trong database theo id
|
||||
const bid = await this.bidsRepo.findOne({
|
||||
where: { id },
|
||||
relations: { histories: true, web_bid: true, metadata: true },
|
||||
order: {
|
||||
histories: {
|
||||
price: 'DESC',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Nếu không tìm thấy phiên đấu giá, trả về lỗi 404
|
||||
if (!bid)
|
||||
|
|
@ -376,40 +234,12 @@ export class BidsService {
|
|||
|
||||
// Nếu phiên đấu giá chưa có thời gian bắt đầu và kết thúc
|
||||
if (!bid.close_time && !bid.start_bid_time && data?.lot_id) {
|
||||
// Tách lấy arrival_offset_seconds trong product nếu không có lấy trong web bid.
|
||||
const arrival_offset_seconds =
|
||||
this.bidMetadatasService.getArrivalOffsetSecondsByMode(bid.metadata) ||
|
||||
bid.web_bid.arrival_offset_seconds;
|
||||
|
||||
// Thiết lập thời gian bắt đầu là 5 phút trước khi đóng
|
||||
// bid.start_bid_time = new Date().toUTCString();
|
||||
bid.start_bid_time = subtractMinutes(
|
||||
close_time,
|
||||
arrival_offset_seconds / 60,
|
||||
bid.web_bid.arrival_offset_seconds / 60,
|
||||
);
|
||||
|
||||
// update
|
||||
await this.bidsRepo.update(
|
||||
{ id },
|
||||
{ start_bid_time: bid.start_bid_time },
|
||||
);
|
||||
}
|
||||
|
||||
// Cập nhật thời gian kết thúc đấu giá nếu `close_time` mới lớn hơn `close_time` cũ
|
||||
if (
|
||||
close_time &&
|
||||
new Date(close_time).getTime() > new Date(bid.close_time).getTime()
|
||||
) {
|
||||
bid.close_time = close_time;
|
||||
bid.close_time_ts = new Date(close_time);
|
||||
|
||||
// update
|
||||
await this.bidsRepo.update(
|
||||
{ id },
|
||||
{ close_time: bid.close_time, close_time_ts: bid.close_time_ts },
|
||||
);
|
||||
|
||||
bid = await this.getBidForClientUpdate(id);
|
||||
}
|
||||
|
||||
// Kiểm tra nếu thời gian đóng bid đã đạt tới (tức phiên đấu giá đã kết thúc)
|
||||
|
|
@ -428,6 +258,7 @@ export class BidsService {
|
|||
}
|
||||
} else {
|
||||
// Nếu phiên đấu giá vẫn đang diễn ra và giá hiện tại vượt quá giới hạn đặt của người dùng
|
||||
|
||||
if (
|
||||
data.current_price > bid.max_price + bid.plus_price ||
|
||||
(!bid.histories.length &&
|
||||
|
|
@ -437,14 +268,14 @@ export class BidsService {
|
|||
}
|
||||
}
|
||||
|
||||
// // Cập nhật thời gian kết thúc đấu giá nếu `close_time` mới lớn hơn `close_time` cũ
|
||||
// if (
|
||||
// close_time &&
|
||||
// new Date(close_time).getTime() > new Date(bid.close_time).getTime()
|
||||
// ) {
|
||||
// bid.close_time = close_time;
|
||||
// bid.close_time_ts = new Date(close_time);
|
||||
// }
|
||||
// Cập nhật thời gian kết thúc đấu giá nếu `close_time` mới lớn hơn `close_time` cũ
|
||||
if (
|
||||
close_time &&
|
||||
new Date(close_time).getTime() > new Date(bid.close_time).getTime()
|
||||
) {
|
||||
bid.close_time = close_time;
|
||||
bid.close_time_ts = new Date(close_time);
|
||||
}
|
||||
|
||||
// Nếu chưa có `model` nhưng dữ liệu mới có model, thì cập nhật model
|
||||
if (model && !bid.model) {
|
||||
|
|
@ -472,30 +303,6 @@ export class BidsService {
|
|||
this.notificationService.emitBidStatus(result);
|
||||
}
|
||||
|
||||
// Send INFO
|
||||
if (bid.current_price < result.current_price && bid.histories.length) {
|
||||
this.eventEmitter.emit(Event.SEND_INFO, {
|
||||
title: 'New Higher Bid Detected',
|
||||
messages: [
|
||||
`Another user just placed a higher bid of ${result.current_price} on <a href="${bid.url}">${bid.name}</a>.`,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// Send error when bidding fail
|
||||
if (
|
||||
isTimePassedByMinutes(result.start_bid_time, 1) &&
|
||||
!bid.histories.length &&
|
||||
!this.bidMetadatasService.isSandbox(bid.metadata)
|
||||
) {
|
||||
this.eventEmitter.emit(Event.SEND_ERROR, {
|
||||
title: 'Bidding Error Detected',
|
||||
messages: [
|
||||
`An error occurred while placing a bid on <a href="${bid.url}">${bid.name}</a>. Please check the system.`,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// Trả về kết quả cập nhật dưới dạng response chuẩn
|
||||
return AppResponse.toResponse(plainToClass(Bid, result));
|
||||
}
|
||||
|
|
@ -652,34 +459,6 @@ export class BidsService {
|
|||
return AppResponse.toResponse(true);
|
||||
}
|
||||
|
||||
async uploadRecord(id: Bid['id'], video: Express.Multer.File) {
|
||||
if (!video) {
|
||||
throw new BadRequestException(
|
||||
AppResponse.toResponse(null, { message: 'File or Url is required' }),
|
||||
);
|
||||
}
|
||||
|
||||
const data: { filename: string } | undefined =
|
||||
await new ImageCompressionPipe(
|
||||
`${Constant.RECORD_FOLDER}`,
|
||||
{},
|
||||
{
|
||||
unique_image_folder: false,
|
||||
unique_name: false,
|
||||
},
|
||||
).transform(video);
|
||||
|
||||
if (!data)
|
||||
throw new BadRequestException(
|
||||
AppResponse.toResponse(null, {
|
||||
message: "Can't create media",
|
||||
status_code: HttpStatus.BAD_REQUEST,
|
||||
}),
|
||||
);
|
||||
|
||||
return AppResponse.toResponse(true);
|
||||
}
|
||||
|
||||
async getStatusWorkingImage(
|
||||
id: Bid['id'],
|
||||
type: string,
|
||||
|
|
@ -736,84 +515,6 @@ export class BidsService {
|
|||
return res.sendFile(filePath);
|
||||
}
|
||||
|
||||
async getRecord(name: string, res: Response) {
|
||||
const rootDir = process.cwd();
|
||||
const filePath = join(
|
||||
rootDir,
|
||||
`${Constant.MEDIA_PATH}/${Constant.RECORD_FOLDER}/${name}`,
|
||||
);
|
||||
|
||||
if (!existsSync(filePath)) {
|
||||
throw new NotFoundException(
|
||||
AppResponse.toResponse(null, {
|
||||
message: 'Folder not found',
|
||||
status_code: HttpStatus.NOT_FOUND,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const stat = fs.statSync(filePath);
|
||||
const fileSize = stat.size;
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'video/mp4',
|
||||
'Content-Length': fileSize,
|
||||
});
|
||||
|
||||
const readStream = fs.createReadStream(filePath);
|
||||
readStream.pipe(res);
|
||||
}
|
||||
|
||||
async getRecords(id: Bid['id']) {
|
||||
const rootDir = process.cwd();
|
||||
const folderPath = join(
|
||||
rootDir,
|
||||
`${Constant.MEDIA_PATH}/${Constant.RECORD_FOLDER}`,
|
||||
);
|
||||
|
||||
if (!existsSync(folderPath)) {
|
||||
throw new NotFoundException(
|
||||
AppResponse.toResponse(null, {
|
||||
message: 'Folder not found',
|
||||
status_code: HttpStatus.NOT_FOUND,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const files = await fs.promises.readdir(folderPath);
|
||||
|
||||
const data = files
|
||||
.map((item) => {
|
||||
return parseVideoFileName(item);
|
||||
})
|
||||
.filter((i) => i.bid_id == id);
|
||||
|
||||
const sorted = _.orderBy(data, ['timestamp'], ['desc']);
|
||||
|
||||
return AppResponse.toResponse(sorted);
|
||||
}
|
||||
|
||||
async deleteRecord(name: string) {
|
||||
const rootDir = process.cwd();
|
||||
const filePath = join(
|
||||
rootDir,
|
||||
`${Constant.MEDIA_PATH}/${Constant.RECORD_FOLDER}/${name}`,
|
||||
);
|
||||
|
||||
if (!existsSync(filePath)) {
|
||||
throw new NotFoundException(
|
||||
AppResponse.toResponse(null, {
|
||||
message: 'File not found',
|
||||
status_code: HttpStatus.NOT_FOUND,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await fs.promises.unlink(filePath);
|
||||
|
||||
return AppResponse.toResponse(true);
|
||||
}
|
||||
|
||||
async getImagesWorking(id: Bid['id'], type: string) {
|
||||
const rootDir = process.cwd();
|
||||
const folderPath = join(
|
||||
|
|
@ -868,13 +569,7 @@ export class BidsService {
|
|||
}
|
||||
|
||||
async getBidByModel(model: string) {
|
||||
const bid = await this.bidsRepo.findOne({
|
||||
where: { model },
|
||||
relations: { metadata: true, web_bid: true },
|
||||
select: {
|
||||
web_bid: { arrival_offset_seconds: true, early_tracking_seconds: true },
|
||||
},
|
||||
});
|
||||
const bid = await this.bidsRepo.findOne({ where: { model } });
|
||||
|
||||
if (!bid)
|
||||
return AppResponse.toResponse(null, {
|
||||
|
|
@ -883,59 +578,4 @@ export class BidsService {
|
|||
|
||||
return AppResponse.toResponse(plainToClass(Bid, bid));
|
||||
}
|
||||
|
||||
async hookAction(
|
||||
{ id, type }: { id: Bid['id']; type: 'action' | 'api' },
|
||||
data: any,
|
||||
) {
|
||||
const bid = await this.bidsRepo.findOne({
|
||||
where: { id },
|
||||
relations: { metadata: true },
|
||||
});
|
||||
|
||||
if (!bid)
|
||||
throw new NotFoundException(
|
||||
AppResponse.toResponse(null, { message: 'Not foud bid' }),
|
||||
);
|
||||
|
||||
if (type === 'api') {
|
||||
const result = JSON.parse(data?.data || {});
|
||||
|
||||
result['timestamp'] = new Date().getTime();
|
||||
result['uuid'] = uuid();
|
||||
|
||||
const prevDemoResponse = this.bidMetadatasService.getDemoResponse(
|
||||
bid.metadata,
|
||||
);
|
||||
|
||||
console.log({ prevDemoResponse, result });
|
||||
|
||||
await this.bidMetadatasService.upsert(
|
||||
{
|
||||
[BidMetadata.DEMO_RESPONSE]:
|
||||
JSON.stringify([result, ...(prevDemoResponse || [])]) ||
|
||||
JSON.stringify([]),
|
||||
},
|
||||
bid,
|
||||
);
|
||||
} else {
|
||||
const recordUrl = data?.record_url || '';
|
||||
|
||||
await this.bidMetadatasService.upsert(
|
||||
{
|
||||
[BidMetadata.LATEST_RECROD_LINK]: JSON.stringify(recordUrl),
|
||||
},
|
||||
bid,
|
||||
);
|
||||
}
|
||||
|
||||
const latestBidData = await this.bidsRepo.findOne({
|
||||
where: { id },
|
||||
relations: { metadata: true },
|
||||
});
|
||||
|
||||
this.eventEmitter.emit(Event.BID_DEMO, { bid: latestBidData, type });
|
||||
|
||||
return AppResponse.toResponse(data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ export class TasksService {
|
|||
private readonly dashboadService: DashboardService,
|
||||
) {}
|
||||
|
||||
// - Chạy kiểm tra và lấy thời gian close của product gần với hiện tại nhất
|
||||
// - Nếu thỏa điều kiện sẽ reset tool trước thời gian close 20 phút
|
||||
@Cron(CronExpression.EVERY_MINUTE)
|
||||
async handleResetTool() {
|
||||
const bids = await this.bidsService.bidsRepo.find({
|
||||
|
|
@ -38,8 +36,6 @@ export class TasksService {
|
|||
: null;
|
||||
const now = new Date();
|
||||
|
||||
const minutesReset = 20;
|
||||
|
||||
console.log('===== Reset Tool Check =====');
|
||||
console.log(
|
||||
'Last reset time:',
|
||||
|
|
@ -59,10 +55,10 @@ export class TasksService {
|
|||
if (result.closeTime) {
|
||||
const closeTimeDate = new Date(result.closeTime);
|
||||
const expectedReset = moment(closeTimeDate)
|
||||
.subtract(minutesReset, 'minutes')
|
||||
.subtract(20, 'minutes')
|
||||
.format('YYYY-MM-DD HH:mm:ss');
|
||||
console.log(
|
||||
`Expected reset time (close_time - ${minutesReset} minutes):`,
|
||||
'Expected reset time (close_time - 20 minutes):',
|
||||
expectedReset,
|
||||
);
|
||||
}
|
||||
|
|
@ -81,18 +77,4 @@ export class TasksService {
|
|||
console.log('No reset needed at this time');
|
||||
}
|
||||
}
|
||||
|
||||
// Reset lại server.
|
||||
// Nếu server quá lâu không được reset sẽ đẫn đến mất kết nối với imap (không nhận được mail)
|
||||
//Chạy vào 12 giờ đêm mỗi ngày
|
||||
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
|
||||
async handleResetServer() {
|
||||
const processName = 'auto-bid-server';
|
||||
|
||||
console.log('===== Reset Server Check =====');
|
||||
await this.dashboadService.resetProcessByName(processName);
|
||||
console.log(
|
||||
`Reset server at: ${moment(new Date()).format('YYYY-MM-DD HH:mm:ss')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,9 +68,7 @@ export class WebBidsService {
|
|||
url: Not(IsNull()),
|
||||
children: { status: 'biding' },
|
||||
},
|
||||
relations: {
|
||||
children: { histories: true, web_bid: true, metadata: true },
|
||||
},
|
||||
relations: { children: { histories: true, web_bid: true } },
|
||||
order: {
|
||||
children: {
|
||||
close_time_ts: 'ASC',
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ export class Constant {
|
|||
public static BOT_TELEGRAM_PATH = 'bot-data';
|
||||
|
||||
public static WORK_IMAGES_FOLDER = 'work-images';
|
||||
public static RECORD_FOLDER = 'records';
|
||||
public static TMP_FOLDER = 'tmp';
|
||||
|
||||
public static DEFAULT_RESIZE = 400;
|
||||
|
|
|
|||
|
|
@ -9,11 +9,6 @@ export class Event {
|
|||
public static LOGIN_STATUS = 'login-status';
|
||||
public static BID_SUBMITED = 'bid-submited';
|
||||
public static BID_STATUS = 'bid-status';
|
||||
public static BID_DEMO = 'bid-demo';
|
||||
public static SEND_WARNING = 'send-warning';
|
||||
public static SEND_SUCCESS = 'send-success';
|
||||
public static SEND_ERROR = 'send-error';
|
||||
public static SEND_INFO = 'send-info';
|
||||
|
||||
public static verifyCode(data: WebBid) {
|
||||
return `${this.VERIFY_CODE}.${data.origin_url}`;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { MailerModule } from '@nestjs-modules/mailer';
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MailsService } from './services/mails.service';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { BullModule } from '@nestjs/bull';
|
||||
import { MailProcessor } from './process/mail.processor';
|
||||
import { BidsModule } from '../bids/bids.module';
|
||||
@Module({
|
||||
imports: [
|
||||
forwardRef(() => BidsModule),
|
||||
MailerModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import { Bid } from '@/modules/bids/entities/bid.entity';
|
||||
import { BidMetadatasService } from '@/modules/bids/services/bid-metadatas.service';
|
||||
import { BidsService } from '@/modules/bids/services/bids.service';
|
||||
import { ScrapItem } from '@/modules/scraps/entities/scrap-item.entity';
|
||||
import {
|
||||
extractDomain,
|
||||
|
|
@ -21,8 +19,6 @@ export class MailsService {
|
|||
private readonly mailerService: MailerService,
|
||||
|
||||
@InjectQueue('mail-queue') private mailQueue: Queue,
|
||||
private readonly bidsService: BidsService,
|
||||
private readonly bidMetadataService: BidMetadatasService,
|
||||
) {}
|
||||
|
||||
async sendPlainText(to: string, subject: string, content: string) {
|
||||
|
|
@ -106,18 +102,11 @@ export class MailsService {
|
|||
});
|
||||
|
||||
// Separate hot and non-hot items, preserving original order
|
||||
const hotItemsList = decorated.filter((p) => p.isHotItem);
|
||||
const nonHotItems = decorated.filter((p) => !p.isHotItem);
|
||||
|
||||
// Đưa các item mới (isNew) lên đầu trong nhóm không phải hot item
|
||||
const sortedNonHotItems = [
|
||||
...nonHotItems.filter((p) => p.isNew),
|
||||
...nonHotItems.filter((p) => !p.isNew),
|
||||
const hotItemsFirst = [
|
||||
...decorated.filter((p) => p.isHotItem),
|
||||
...decorated.filter((p) => !p.isHotItem),
|
||||
];
|
||||
|
||||
// Separate hot and non-hot items, preserving original order
|
||||
const hotItemsFirst = [...hotItemsList, ...sortedNonHotItems];
|
||||
|
||||
const rows = hotItemsFirst
|
||||
.map((p) => {
|
||||
const isHotItem = isHotItemFn(p, hotItems);
|
||||
|
|
@ -136,7 +125,7 @@ export class MailsService {
|
|||
|
||||
return `
|
||||
<tr ${isHotItem ? 'style="background-color: #ff848496;margin-bottom: 4px;"' : 'margin-bottom: 4px;'}>
|
||||
<td style="display:flex;max-width: 60px;"><img src="${p.image_url}" alt="Product Image" style="height: 60px !important;width:60px !important; object-fit: contain; border-radius: 4px;" /></td>
|
||||
<td style="display:flex;max-width: 60px;"><img src="${p.image_url}" alt="Product Image" style="height: 60px !important; object-fit: contain; border-radius: 4px;" /></td>
|
||||
<td style="${nameStyle}">${namePrefix}${p.name}</td>
|
||||
<td style="font-weight: bold; color: #e03131;">${p.current_price ? '$' + p.current_price : 'None'}</td>
|
||||
<td><a href="${p.url}" target="_blank" style="color: #007bff;">View</a></td>
|
||||
|
|
@ -390,146 +379,4 @@ export class MailsService {
|
|||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
async getBidRecordEmailContent(bid: Bid, type: 'action' | 'api') {
|
||||
let recordData: {
|
||||
bid_id: number;
|
||||
name: string;
|
||||
original_name: string;
|
||||
timestamp: number;
|
||||
}[] = [];
|
||||
|
||||
try {
|
||||
const data = await this.bidsService.getRecords(bid.id);
|
||||
recordData = data.data || [];
|
||||
} catch (error) {
|
||||
recordData = [];
|
||||
}
|
||||
|
||||
const json = this.bidMetadataService.getDemoResponse(bid.metadata);
|
||||
|
||||
const videoUrl = this.bidMetadataService.getLatestRecordLink(bid.metadata);
|
||||
|
||||
const closeTime = bid.close_time
|
||||
? new Date(bid.close_time).toLocaleString('en-AU', {
|
||||
timeZone: 'Australia/Sydney',
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
})
|
||||
: 'Unknown';
|
||||
|
||||
let content = '';
|
||||
|
||||
if (type === 'action') {
|
||||
content = `
|
||||
<h3>📹 Record Preview</h3>
|
||||
<p><strong>Name:</strong> ${bid?.name}</p>
|
||||
<p><strong>Recorded:</strong> ${new Date().toLocaleString()}</p>
|
||||
<p>
|
||||
<a href="${videoUrl || ''}" target="_blank" style="
|
||||
display: inline-block;
|
||||
padding: 10px 16px;
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
margin-top: 12px;
|
||||
">
|
||||
▶️ Watch Video
|
||||
</a>
|
||||
</p>
|
||||
`;
|
||||
} else if (type === 'api') {
|
||||
content = `
|
||||
<h3>📦 Record JSON</h3>
|
||||
<pre style="
|
||||
background-color: #f4f4f4;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
overflow-x: auto;
|
||||
">${JSON.stringify(json, null, 2)}</pre>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div style="font-family: Arial, sans-serif; color: #333; max-width: 600px; margin: 0 auto;">
|
||||
<h2>📝 Bid Demo: ${bid?.name}</h2>
|
||||
<p><strong>⏰ Close Time:</strong> ${closeTime}</p>
|
||||
|
||||
${
|
||||
content
|
||||
? content
|
||||
: `<p style="color: red;">⚠️ No record available for this bid.</p>`
|
||||
}
|
||||
|
||||
<hr style="margin: 24px 0;">
|
||||
<p style="font-size: 12px; color: #888;">This is an automated message. Please do not reply.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
generateWarningEmailTemplate(title: string, messages: string[]): string {
|
||||
const timestamp = new Date().toLocaleString(); // hoặc new Date().toISOString()
|
||||
|
||||
return `
|
||||
<div style="font-family: Arial, sans-serif; padding: 20px; background-color: #fffbe6; color: #333;">
|
||||
<h2 style="color: #d9534f;">⚠️ ${title}</h2>
|
||||
<ul>
|
||||
${messages.map((msg) => `<li>${msg}</li>`).join('')}
|
||||
</ul>
|
||||
<p style="margin-top: 20px; font-size: 12px; color: #999;">
|
||||
Sent at: ${timestamp}
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
generateSuccessEmailTemplate(title: string, messages: string[]): string {
|
||||
const timestamp = new Date().toLocaleString(); // hoặc new Date().toISOString()
|
||||
|
||||
return `
|
||||
<div style="font-family: Arial, sans-serif; padding: 20px; background-color: #e6fff2; color: #333;">
|
||||
<h2 style="color: #28a745;">✅ ${title}</h2>
|
||||
<ul>
|
||||
${messages.map((msg) => `<li>${msg}</li>`).join('')}
|
||||
</ul>
|
||||
<p style="margin-top: 20px; font-size: 12px; color: #999;">
|
||||
Sent at: ${timestamp}
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
generateErrorEmailTemplate(title: string, messages: string[]): string {
|
||||
const timestamp = new Date().toLocaleString();
|
||||
|
||||
return `
|
||||
<div style="font-family: Arial, sans-serif; padding: 20px; background-color: #fdecea; color: #333;">
|
||||
<h2 style="color: #c82333;">❌ ${title}</h2>
|
||||
<ul>
|
||||
${messages.map((msg) => `<li>${msg}</li>`).join('')}
|
||||
</ul>
|
||||
<p style="margin-top: 20px; font-size: 12px; color: #999;">
|
||||
Sent at: ${timestamp}
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
generateInfoEmailTemplate(title: string, messages: string[]): string {
|
||||
const timestamp = new Date().toLocaleString();
|
||||
|
||||
return `
|
||||
<div style="font-family: Arial, sans-serif; padding: 20px; background-color: #e7f3fe; color: #333;">
|
||||
<h2 style="color: #31708f;">ℹ️ ${title}</h2>
|
||||
<ul>
|
||||
${messages.map((msg) => `<li>${msg}</li>`).join('')}
|
||||
</ul>
|
||||
<p style="margin-top: 20px; font-size: 12px; color: #999;">
|
||||
Sent at: ${timestamp}
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export class AdminNotificationListener {
|
|||
}) {
|
||||
if (JSON.parse(notification.send_to).length <= 0) return;
|
||||
|
||||
// this.botTelegramApi.sendMessage(notification.message);
|
||||
this.botTelegramApi.sendMessage(notification.message);
|
||||
|
||||
const mails =
|
||||
(await this.configsSerice.getConfig('MAIL_SCRAP_REPORT')).value || '';
|
||||
|
|
@ -53,114 +53,4 @@ export class AdminNotificationListener {
|
|||
moment(new Date()).format('YYYY-MM-DD HH:mm'),
|
||||
});
|
||||
}
|
||||
|
||||
@OnEvent(Event.BID_DEMO)
|
||||
async handleBidDemo({ bid, type }: { bid: Bid; type: 'api' | 'action' }) {
|
||||
const mails =
|
||||
(await this.configsSerice.getConfig('MAIL_SCRAP_REPORT')).value || '';
|
||||
|
||||
const html = await this.mailsService.getBidRecordEmailContent(bid, type);
|
||||
|
||||
this.mailsService.sendHtmlMailJob({
|
||||
to: mails,
|
||||
html: html,
|
||||
subject:
|
||||
'Demo Auto Auctions System ' +
|
||||
moment(new Date()).format('YYYY-MM-DD HH:mm'),
|
||||
});
|
||||
}
|
||||
|
||||
@OnEvent(Event.SEND_WARNING)
|
||||
async handleSendWarning({
|
||||
title,
|
||||
messages,
|
||||
}: {
|
||||
title: string;
|
||||
messages: string[];
|
||||
}) {
|
||||
const mails =
|
||||
(await this.configsSerice.getConfig('MAIL_SCRAP_REPORT')).value || '';
|
||||
|
||||
const html = this.mailsService.generateWarningEmailTemplate(
|
||||
title,
|
||||
messages,
|
||||
);
|
||||
|
||||
this.mailsService.sendHtmlMailJob({
|
||||
to: mails,
|
||||
html: html,
|
||||
subject:
|
||||
'[WARNING] Auto Auctions System ' +
|
||||
moment(new Date()).format('YYYY-MM-DD HH:mm'),
|
||||
});
|
||||
}
|
||||
|
||||
@OnEvent(Event.SEND_SUCCESS)
|
||||
async handleSendSuccess({
|
||||
title,
|
||||
messages,
|
||||
}: {
|
||||
title: string;
|
||||
messages: string[];
|
||||
}) {
|
||||
const mails =
|
||||
(await this.configsSerice.getConfig('MAIL_SCRAP_REPORT')).value || '';
|
||||
|
||||
const html = this.mailsService.generateSuccessEmailTemplate(
|
||||
title,
|
||||
messages,
|
||||
);
|
||||
|
||||
this.mailsService.sendHtmlMailJob({
|
||||
to: mails,
|
||||
html: html,
|
||||
subject:
|
||||
'[SUCCESS] Auto Auctions System ' +
|
||||
moment(new Date()).format('YYYY-MM-DD HH:mm'),
|
||||
});
|
||||
}
|
||||
|
||||
@OnEvent(Event.SEND_ERROR)
|
||||
async handleSendError({
|
||||
title,
|
||||
messages,
|
||||
}: {
|
||||
title: string;
|
||||
messages: string[];
|
||||
}) {
|
||||
const mails =
|
||||
(await this.configsSerice.getConfig('MAIL_SCRAP_REPORT')).value || '';
|
||||
|
||||
const html = this.mailsService.generateErrorEmailTemplate(title, messages);
|
||||
|
||||
this.mailsService.sendHtmlMailJob({
|
||||
to: mails,
|
||||
html: html,
|
||||
subject:
|
||||
'[ERROR] Auto Auctions System ' +
|
||||
moment(new Date()).format('YYYY-MM-DD HH:mm'),
|
||||
});
|
||||
}
|
||||
|
||||
@OnEvent(Event.SEND_INFO)
|
||||
async handleSendInfo({
|
||||
title,
|
||||
messages,
|
||||
}: {
|
||||
title: string;
|
||||
messages: string[];
|
||||
}) {
|
||||
const mails =
|
||||
(await this.configsSerice.getConfig('MAIL_SCRAP_REPORT')).value || '';
|
||||
|
||||
const html = this.mailsService.generateInfoEmailTemplate(title, messages);
|
||||
|
||||
this.mailsService.sendHtmlMailJob({
|
||||
to: mails,
|
||||
html: html,
|
||||
subject:
|
||||
'[INFO] Auto Auctions System ' +
|
||||
moment(new Date()).format('YYYY-MM-DD HH:mm'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { ScrapConfigsService } from '../../services/scrap-config.service';
|
|||
import { ScrapItemsService } from '../../services/scrap-item-config.service';
|
||||
import { UpsertScrapItemDto } from '../../dto/scrap-items/upsert-scrap-item.dto';
|
||||
import { ScrapItem } from '../../entities/scrap-item.entity';
|
||||
import { Paginate, PaginateQuery } from 'nestjs-paginate';
|
||||
|
||||
@Controller('scrap-items')
|
||||
export class ClientScrapItemsController {
|
||||
|
|
@ -15,9 +14,4 @@ export class ClientScrapItemsController {
|
|||
data as ScrapItem[],
|
||||
);
|
||||
}
|
||||
|
||||
@Get()
|
||||
async index(@Paginate() query: PaginateQuery) {
|
||||
return await this.scrapItemsService.index(query);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@ export class ScrapItem extends Timestamp {
|
|||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
@Column()
|
||||
url: string;
|
||||
|
||||
@Column()
|
||||
model: string;
|
||||
|
||||
@Column({ nullable: true, default: null, type: 'text' })
|
||||
@Column({ nullable: true, default: null })
|
||||
image_url: string | null;
|
||||
|
||||
@Column({ nullable: true, default: null })
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
import { WebBidsService } from '@/modules/bids/services/web-bids.service';
|
||||
import AppResponse from '@/response/app-response';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { paginate, PaginateQuery } from 'nestjs-paginate';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ScrapItem } from '../entities/scrap-item.entity';
|
||||
import AppResponse from '@/response/app-response';
|
||||
|
||||
@Injectable()
|
||||
export class ScrapItemsService {
|
||||
constructor(
|
||||
@InjectRepository(ScrapItem)
|
||||
readonly scrapItemRepo: Repository<ScrapItem>,
|
||||
private readonly webService: WebBidsService,
|
||||
) {}
|
||||
|
||||
async upsertScrapItems(items: ScrapItem[]) {
|
||||
|
|
@ -70,37 +67,4 @@ export class ScrapItemsService {
|
|||
|
||||
return AppResponse.toResponse(rs);
|
||||
}
|
||||
|
||||
async index(query: PaginateQuery) {
|
||||
const { data, meta } = await paginate(query, this.scrapItemRepo, {
|
||||
sortableColumns: ['id'],
|
||||
searchableColumns: ['id'],
|
||||
defaultLimit: 15,
|
||||
filterableColumns: {
|
||||
'scrap_config.web_bid.origin_url': true,
|
||||
},
|
||||
defaultSortBy: [['updated_at', 'DESC']],
|
||||
maxLimit: 100,
|
||||
relations: { scrap_config: { web_bid: true } },
|
||||
});
|
||||
|
||||
const sources = await this.webService.webBidRepo.find({
|
||||
select: { origin_url: true },
|
||||
});
|
||||
|
||||
const newData = data.map(({ scrap_config, ...item }) => {
|
||||
return {
|
||||
...item,
|
||||
source: scrap_config.web_bid.origin_url,
|
||||
};
|
||||
});
|
||||
|
||||
return AppResponse.toPaginationAny({
|
||||
data: newData,
|
||||
meta,
|
||||
bonus: {
|
||||
sources: sources.map((item) => item.origin_url),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ export class TasksService {
|
|||
updated_at: Between(startOfDay, endOfDay),
|
||||
},
|
||||
relations: { scrap_config: { web_bid: true } },
|
||||
order: { updated_at: 'DESC' },
|
||||
order: { updated_at: 'ASC' },
|
||||
});
|
||||
|
||||
const hotItems = await this.hotItemApi.listHotItem();
|
||||
|
|
@ -95,8 +95,8 @@ export class TasksService {
|
|||
}
|
||||
}
|
||||
|
||||
// Thực hiện gọi scrape vào lúc 8h59 (giờ ÚC) mỗi ngày
|
||||
@Cron('59 5 * * *')
|
||||
// @Cron('59 5 * * *')
|
||||
// @Cron(CronExpression.EVERY_MINUTE)
|
||||
async handleScraps() {
|
||||
const processName = 'scrape-data-keyword';
|
||||
await this.runProcessAndSendReport(processName);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export default class AppResponse {
|
|||
}
|
||||
|
||||
public static toPagination<M>(
|
||||
{ data, meta, bonus }: Paginated<M> & { bonus?: Record<string, any> },
|
||||
{ data, meta }: Paginated<M>,
|
||||
transform = true,
|
||||
dtoClass?: new () => M,
|
||||
) {
|
||||
|
|
@ -45,33 +45,6 @@ export default class AppResponse {
|
|||
last_page: meta.totalPages,
|
||||
per_page: meta.itemsPerPage,
|
||||
total: meta.totalItems,
|
||||
...bonus,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public static toPaginationAny(
|
||||
{ data, meta, bonus }: any,
|
||||
transform = true,
|
||||
dtoClass?: new () => any,
|
||||
) {
|
||||
const transformedData =
|
||||
transform && dtoClass
|
||||
? data.map((item) => plainToClass(dtoClass, item))
|
||||
: data;
|
||||
|
||||
return this.toResponse(transformedData, {
|
||||
bonus: {
|
||||
current_page: meta.currentPage,
|
||||
from: meta.currentPage * meta.itemsPerPage - (meta.itemsPerPage - 1),
|
||||
to:
|
||||
meta.currentPage * meta.itemsPerPage > meta.totalItems
|
||||
? meta.totalItems
|
||||
: meta.currentPage * meta.itemsPerPage,
|
||||
last_page: meta.totalPages,
|
||||
per_page: meta.itemsPerPage,
|
||||
total: meta.totalItems,
|
||||
...bonus,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,6 @@ export const excludeAuth = [
|
|||
path: 'admin/auth/forgot-password',
|
||||
method: RequestMethod.POST,
|
||||
},
|
||||
{
|
||||
path: '/admin/bids/record/(.*)',
|
||||
method: RequestMethod.GET,
|
||||
},
|
||||
] as (string | RouteInfo)[];
|
||||
|
||||
export const clientExcludeAuth = [
|
||||
|
|
@ -26,14 +22,6 @@ export const clientExcludeAuth = [
|
|||
path: 'extensions/policy',
|
||||
method: RequestMethod.GET,
|
||||
},
|
||||
{
|
||||
path: 'bids/hook-action',
|
||||
method: RequestMethod.POST,
|
||||
},
|
||||
{
|
||||
path: 'scrap-items',
|
||||
method: RequestMethod.GET,
|
||||
},
|
||||
] as (string | RouteInfo)[];
|
||||
|
||||
export const excludeAuthor = [
|
||||
|
|
@ -50,8 +38,4 @@ export const excludeAuthor = [
|
|||
path: 'admin/auth/me',
|
||||
method: RequestMethod.GET,
|
||||
},
|
||||
{
|
||||
path: '/admin/bids/record/(.*)',
|
||||
method: RequestMethod.GET,
|
||||
},
|
||||
] as (string | RouteInfo)[];
|
||||
|
|
|
|||
|
|
@ -65,19 +65,6 @@ export function isTimeReached(targetTime: string) {
|
|||
return now >= targetDate;
|
||||
}
|
||||
|
||||
export function isTimePassedByMinutes(
|
||||
targetTime: string,
|
||||
minutes: number,
|
||||
): boolean {
|
||||
const targetDate = new Date(targetTime);
|
||||
const now = new Date();
|
||||
|
||||
const diffMs = now.getTime() - targetDate.getTime(); // hiệu số milliseconds
|
||||
const diffMinutes = diffMs / (1000 * 60);
|
||||
|
||||
return diffMinutes >= minutes;
|
||||
}
|
||||
|
||||
export function extractDomain(url: string): string | null {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
|
|
@ -235,29 +222,3 @@ export const isHotItemFn = (item: ScrapItem, hotItems: any[]) => {
|
|||
item.name.toLowerCase().includes(obj.name.toLowerCase()),
|
||||
);
|
||||
};
|
||||
|
||||
export function parseVideoFileName(fileName: string) {
|
||||
// Loại bỏ phần mở rộng .mp4
|
||||
const nameWithoutExt = fileName.replace(/\.mp4$/, '');
|
||||
|
||||
// Tách phần ID đầu tiên
|
||||
const firstUnderscoreIndex = nameWithoutExt.indexOf('_');
|
||||
const id = nameWithoutExt.slice(0, firstUnderscoreIndex);
|
||||
|
||||
// Tách phần timestamp cuối cùng
|
||||
const lastUnderscoreIndex = nameWithoutExt.lastIndexOf('_');
|
||||
const timestamp = nameWithoutExt.slice(lastUnderscoreIndex + 1);
|
||||
|
||||
// Tên sản phẩm nằm giữa 2 phần còn lại
|
||||
const name = nameWithoutExt.slice(
|
||||
firstUnderscoreIndex + 1,
|
||||
lastUnderscoreIndex,
|
||||
);
|
||||
|
||||
return {
|
||||
bid_id: Number(id),
|
||||
name,
|
||||
original_name: fileName,
|
||||
timestamp: Number(timestamp),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,9 @@
|
|||
{
|
||||
"moduleFileExtensions": ["ts", "js", "json"],
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"preset": "ts-jest",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"testMatch": ["**/?(*.)+(spec|test).ts"],
|
||||
"moduleNameMapper": {
|
||||
"^@/(.*)$": "<rootDir>/src/$1"
|
||||
},
|
||||
"globals": {
|
||||
"ts-jest": {
|
||||
"tsconfig": "<rootDir>/tsconfig.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,11 +15,9 @@ import {
|
|||
delay,
|
||||
extractModelId,
|
||||
isTimeReached,
|
||||
randomDelayWithMeta,
|
||||
safeClosePage,
|
||||
subtractSeconds,
|
||||
} from "./system/utils.js";
|
||||
import CONSTANTS from "./system/constants.js";
|
||||
|
||||
global.IS_CLEANING = true;
|
||||
|
||||
|
|
@ -148,11 +146,6 @@ const tracking = async () => {
|
|||
Promise.allSettled(
|
||||
productTabs.map(async (productTab) => {
|
||||
console.log(`📌 Processing Product ID: ${productTab.id}`);
|
||||
console.log(
|
||||
`🔍 [${
|
||||
productTab.id
|
||||
}] Current URL: ${await productTab.page_context?.url?.()}`
|
||||
);
|
||||
|
||||
// Xác định parent context
|
||||
if (!productTab.parent_browser_context) {
|
||||
|
|
@ -169,7 +162,7 @@ const tracking = async () => {
|
|||
// Thời điểm tracking liên tục
|
||||
const earlyTrackingTime = subtractSeconds(
|
||||
productTab.close_time,
|
||||
productTab.getEarlyTrackingSeconds() || 0
|
||||
productTab?.web_bid?.early_tracking_seconds || 0
|
||||
);
|
||||
|
||||
// Check không mở tab nếu chưa đến giờ
|
||||
|
|
@ -228,6 +221,8 @@ const tracking = async () => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Thực thi hành động
|
||||
console.log(`🚀 Executing action for Product ID: ${productTab.id}`);
|
||||
await productTab.action();
|
||||
})
|
||||
);
|
||||
|
|
@ -294,7 +289,7 @@ const clearLazyTab = async () => {
|
|||
|
||||
const earlyTrackingTime = subtractSeconds(
|
||||
productTab.close_time,
|
||||
productTab.getEarlyTrackingSeconds() || 0
|
||||
productTab?.web_bid?.early_tracking_seconds || 0
|
||||
);
|
||||
|
||||
if (!isTimeReached(earlyTrackingTime)) {
|
||||
|
|
@ -328,11 +323,6 @@ const clearLazyTab = async () => {
|
|||
|
||||
console.log(`🛑 Unused page detected: ${pageUrl}`);
|
||||
|
||||
if (CONSTANTS.KEEP_LIVE_URLS.some((item) => pageUrl.includes(item))) {
|
||||
console.log("Url include keep live urls: " + pageUrl);
|
||||
continue;
|
||||
}
|
||||
|
||||
const bidData = MANAGER_BIDS.filter((item) => item.page_context)
|
||||
.map((i) => ({
|
||||
current_url: i.page_context.url(),
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ export class AllbidsApiBid extends ApiBid {
|
|||
|
||||
const currentUrl = await this.page_context.url();
|
||||
|
||||
console.log({
|
||||
filePath,
|
||||
currentUrl,
|
||||
a: currentUrl.includes(configs.WEB_URLS.ALLBIDS.LOGIN_URL),
|
||||
});
|
||||
|
||||
if (currentUrl.includes(configs.WEB_URLS.ALLBIDS.LOGIN_URL)) return false;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {
|
|||
takeSnapshot,
|
||||
} from "../../system/utils.js";
|
||||
import { ProductBid } from "../product-bid.js";
|
||||
import axios from "../../system/axios.js";
|
||||
|
||||
export class AllbidsProductBid extends ProductBid {
|
||||
constructor({ ...prev }) {
|
||||
|
|
@ -33,11 +32,7 @@ export class AllbidsProductBid extends ProductBid {
|
|||
for (let i = 0; i < elements.length; i++) {
|
||||
try {
|
||||
const scope = angular.element(elements[i]).scope();
|
||||
if (
|
||||
scope?.auction &&
|
||||
scope?.auction.aucID === this.model &&
|
||||
scope?.auction?.aucBidIncrement
|
||||
) {
|
||||
if (scope?.auction) {
|
||||
data = scope.auction;
|
||||
break;
|
||||
}
|
||||
|
|
@ -72,19 +67,14 @@ export class AllbidsProductBid extends ProductBid {
|
|||
() => window.angular !== undefined
|
||||
);
|
||||
|
||||
const historiesData = await this.page_context.evaluate((model) => {
|
||||
const historiesData = await this.page_context.evaluate(() => {
|
||||
let data = null;
|
||||
const elements = document.querySelectorAll(".ng-scope");
|
||||
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
try {
|
||||
const scope = angular.element(elements[i]).scope();
|
||||
if (
|
||||
scope &&
|
||||
scope.auction &&
|
||||
scope?.auction.aucID === model &&
|
||||
scope?.bidHistory
|
||||
) {
|
||||
if (scope?.bidHistory) {
|
||||
data = scope.bidHistory;
|
||||
break;
|
||||
}
|
||||
|
|
@ -101,7 +91,7 @@ export class AllbidsProductBid extends ProductBid {
|
|||
}
|
||||
|
||||
return data;
|
||||
}, this.model);
|
||||
});
|
||||
|
||||
return historiesData;
|
||||
} catch (error) {
|
||||
|
|
@ -137,13 +127,13 @@ export class AllbidsProductBid extends ProductBid {
|
|||
}
|
||||
}
|
||||
|
||||
async handlePlaceBidLive() {
|
||||
async submitBid() {
|
||||
if (!this.page_context) return;
|
||||
|
||||
const response = await this.page_context.evaluate(
|
||||
async (aucID, bidAmount, submitUrl) => {
|
||||
try {
|
||||
const url = `${submitUrl}?aucID=${aucID}&bidAmount=${bidAmount}`;
|
||||
const url = `${submitUrl}?aucID=${aucID}&bidAmount=${bidAmount}&bidType=maximum`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
|
|
@ -162,37 +152,7 @@ export class AllbidsProductBid extends ProductBid {
|
|||
},
|
||||
this.model,
|
||||
this.max_price,
|
||||
configs.WEB_CONFIGS.ALLBIDS.PLACE_BID
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async handlePlaceBidSanbox() {
|
||||
if (!this.page_context) return;
|
||||
|
||||
const response = await this.page_context.evaluate(
|
||||
async (aucID, submitUrl) => {
|
||||
try {
|
||||
const url = `${submitUrl}?aucID=${aucID}&bidAmount=${0}&bidType=maximum`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return { success: false, message: `HTTP error ${res.status}` };
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message || "Fetch failed" };
|
||||
}
|
||||
},
|
||||
this.model,
|
||||
configs.WEB_CONFIGS.ALLBIDS.PLACE_BID
|
||||
configs.WEB_URLS.ALLBIDS.PLACE_BID
|
||||
);
|
||||
|
||||
return response;
|
||||
|
|
@ -282,7 +242,7 @@ export class AllbidsProductBid extends ProductBid {
|
|||
}
|
||||
|
||||
// Kiểm tra thời gian bid
|
||||
if (!this.start_bid_time || !isTimeReached(this.start_bid_time)) {
|
||||
if (this.start_bid_time && !isTimeReached(this.start_bid_time)) {
|
||||
console.log(
|
||||
`⏳ [${this.id}] Not yet time to bid. Skipping Product: ${
|
||||
this.name || "None"
|
||||
|
|
@ -320,11 +280,37 @@ export class AllbidsProductBid extends ProductBid {
|
|||
|
||||
console.log("---------------------BIDDING--------------------");
|
||||
|
||||
if (this.isSandbox()) {
|
||||
await this.handleCallActionSanbox();
|
||||
} else {
|
||||
await this.handleCallActionLive();
|
||||
const data = await this.submitBid();
|
||||
|
||||
await this.page_context.reload({ waitUntil: "networkidle0" });
|
||||
|
||||
const { aucUserMaxBid } = await this.waitForApiResponse();
|
||||
console.log(`📡 [${this.id}] API Response received:`, { aucUserMaxBid });
|
||||
|
||||
// 📌 Kiểm tra trạng thái đấu giá từ API
|
||||
if (aucUserMaxBid == this.max_price) {
|
||||
console.log(`📸 [${this.id}] Taking bid success snapshot...`);
|
||||
await takeSnapshot(
|
||||
page,
|
||||
this,
|
||||
"bid-success",
|
||||
CONSTANTS.TYPE_IMAGE.SUCCESS
|
||||
);
|
||||
|
||||
sendMessage(this);
|
||||
|
||||
pushPrice({
|
||||
bid_id: this.id,
|
||||
price: aucUserMaxBid,
|
||||
});
|
||||
|
||||
console.log(`✅ [${this.id}] Bid placed successfully!`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`⚠️ [${this.id}] Bid action completed, but status is still "None".`
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(`🚨 [${this.id}] Error placing bid: ${error.message}`);
|
||||
} finally {
|
||||
|
|
@ -333,63 +319,6 @@ export class AllbidsProductBid extends ProductBid {
|
|||
}
|
||||
}
|
||||
|
||||
async handleCallActionLive() {
|
||||
const data = await this.handlePlaceBidLive();
|
||||
|
||||
await this.page_context.reload({ waitUntil: "networkidle0" });
|
||||
|
||||
// const { aucUserMaxBid } = await this.waitForApiResponse();
|
||||
console.log(`📡 [${this.id}] API Response received:`, {
|
||||
aucUserMaxBid: this.max_price,
|
||||
});
|
||||
|
||||
// 📌 Kiểm tra trạng thái đấu giá từ API
|
||||
if (
|
||||
data?.bidResult?.result ||
|
||||
data?.bidResult?.bidAmount == this.max_price
|
||||
) {
|
||||
console.log(`📸 [${this.id}] Taking bid success snapshot...`);
|
||||
|
||||
// sendMessage(this);
|
||||
|
||||
pushPrice({
|
||||
bid_id: this.id,
|
||||
price: this.max_price,
|
||||
});
|
||||
|
||||
await takeSnapshot(
|
||||
this.page_context,
|
||||
this,
|
||||
"bid-success",
|
||||
CONSTANTS.TYPE_IMAGE.SUCCESS
|
||||
);
|
||||
|
||||
console.log(`✅ [${this.id}] Bid placed successfully!`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`⚠️ [${this.id}] Bid action completed, but status is still "None".`
|
||||
);
|
||||
}
|
||||
|
||||
async handleCallActionSanbox() {
|
||||
const result = await this.handlePlaceBidSanbox();
|
||||
|
||||
await axios({
|
||||
url: this.ACTION_URL({ type: "api" }),
|
||||
data: {
|
||||
id: this.id,
|
||||
data: JSON.stringify(result),
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
await this.close();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
action = async () => {
|
||||
try {
|
||||
const page = this.page_context;
|
||||
|
|
|
|||
|
|
@ -98,11 +98,6 @@ export class ApiBid extends Bid {
|
|||
if (this.snapshot_at) {
|
||||
const nearestCloseTime = findNearestClosingChild(this);
|
||||
|
||||
// Nếu không có roi vào 2 trường hợp
|
||||
// 1 là không có item nào đang hợp lệ
|
||||
// 2 là có item chưa được craw dữ liệu
|
||||
|
||||
// Xử lí trường hợp 2
|
||||
if (!nearestCloseTime || this.children.some((item) => !item.close_time)) {
|
||||
console.log(`🔌 [${this.id}] Connecting to puppeteer...`);
|
||||
await this.puppeteer_connect();
|
||||
|
|
@ -114,18 +109,13 @@ export class ApiBid extends Bid {
|
|||
return true;
|
||||
}
|
||||
|
||||
// Xử lí trường hợp có item hợp lệ ( close time gần với thời gian hiện tại)
|
||||
const { close_time } = nearestCloseTime;
|
||||
console.log(`📅 [${this.id}] Nearest close_time: ${close_time}`);
|
||||
|
||||
const timeToTracking = subtractSeconds(
|
||||
close_time,
|
||||
nearestCloseTime.getEarlyTrackingSeconds() || 0
|
||||
this.early_tracking_seconds || 0
|
||||
);
|
||||
// const timeToTracking = subtractSeconds(
|
||||
// close_time,
|
||||
// this.early_tracking_seconds || 0
|
||||
// );
|
||||
|
||||
console.log(
|
||||
`🕰️ [${this.id}] Time to tracking: ${new Date(
|
||||
|
|
@ -165,7 +155,7 @@ export class ApiBid extends Bid {
|
|||
|
||||
const timeToTracking = subtractSeconds(
|
||||
close_time,
|
||||
nearestCloseTime.getEarlyTrackingSeconds() || 0
|
||||
this.early_tracking_seconds || 0
|
||||
);
|
||||
|
||||
// Nếu chưa đến giờ tracking => tab lazy
|
||||
|
|
|
|||
|
|
@ -209,10 +209,7 @@ export class GrayApiBid extends ApiBid {
|
|||
const filePath = getPathProfile(this.origin_url);
|
||||
|
||||
// 🔍 Check if already logged in (login input should not be visible)
|
||||
if (
|
||||
!(await page.$('input[name="username"]'))
|
||||
// || fs.existsSync(filePath)
|
||||
) {
|
||||
if (!(await page.$('input[name="username"]')) || fs.existsSync(filePath)) {
|
||||
console.log(`✅ [${this.id}] Already logged in, skipping login.`);
|
||||
|
||||
global.IS_CLEANING = true;
|
||||
|
|
@ -279,7 +276,7 @@ export class GrayApiBid extends ApiBid {
|
|||
try {
|
||||
const page = this.page_context;
|
||||
|
||||
await page.goto(this.url, { waitUntil: "networkidle2", timeout: 10000 });
|
||||
await page.goto(this.url, { waitUntil: "networkidle2" });
|
||||
console.log(`🌍 [${this.id}] Navigated to URL: ${this.url}`);
|
||||
|
||||
await page.bringToFront();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,322 @@
|
|||
import {
|
||||
outBid,
|
||||
pushPrice,
|
||||
updateBid,
|
||||
updateStatusByPrice,
|
||||
} from "../../system/apis/bid.js";
|
||||
import CONSTANTS from "../../system/constants.js";
|
||||
import {
|
||||
delay,
|
||||
extractNumber,
|
||||
isNumber,
|
||||
isTimeReached,
|
||||
removeFalsyValues,
|
||||
safeClosePage,
|
||||
takeSnapshot,
|
||||
} from "../../system/utils.js";
|
||||
import { ProductBid } from "../product-bid.js";
|
||||
|
||||
export class GraysProductBidBackup extends ProductBid {
|
||||
constructor({ ...prev }) {
|
||||
super(prev);
|
||||
}
|
||||
|
||||
async validate({ page, price_value }) {
|
||||
if (!this.start_bid_time || !isTimeReached(this.start_bid_time)) {
|
||||
console.log(`❌ [${this.id}] It's not time yet`);
|
||||
return { result: false, bid_price: 0 };
|
||||
}
|
||||
|
||||
if (!isNumber(price_value)) {
|
||||
console.log(`❌ [${this.id}] Can't get PRICE_VALUE`);
|
||||
await takeSnapshot(page, this, "price-value-null");
|
||||
|
||||
return { result: false, bid_price: 0 };
|
||||
}
|
||||
|
||||
const bid_price = this.plus_price + Number(price_value);
|
||||
|
||||
if (bid_price > this.max_price) {
|
||||
console.log(
|
||||
`❌ ${this.id} PRICE BID is more than MAX_VALUE => STOP BID THIS PRODUCT`
|
||||
);
|
||||
await takeSnapshot(page, this, "price-bid-more-than");
|
||||
|
||||
await outBid(this.id);
|
||||
|
||||
return { result: false, bid_price: 0 };
|
||||
}
|
||||
|
||||
const response = await pushPrice({
|
||||
bid_id: this.id,
|
||||
price: bid_price,
|
||||
});
|
||||
|
||||
if (!response.status) {
|
||||
return { result: false, bid_price: 0 };
|
||||
}
|
||||
|
||||
this.histories = response.data;
|
||||
|
||||
// RESET first bid
|
||||
if (this.histories.length > 0 && this.first_bid) {
|
||||
this.first_bid = false;
|
||||
}
|
||||
|
||||
return { result: true, bid_price };
|
||||
}
|
||||
|
||||
getCloseTime = async () => {
|
||||
try {
|
||||
if (!this.page_context) return null;
|
||||
|
||||
await this.page_context.waitForSelector("#lot-closing-datetime", {
|
||||
timeout: 3000,
|
||||
});
|
||||
|
||||
return await this.page_context.$eval(
|
||||
"#lot-closing-datetime",
|
||||
(el) => el.value
|
||||
);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
getPriceWasBid = async () => {
|
||||
try {
|
||||
if (!this.page_context) return null;
|
||||
|
||||
await this.page_context.waitForSelector(
|
||||
"#biddableLot form div div:nth-child(1) span span",
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
|
||||
const element = await this.page_context.$(
|
||||
"#biddableLot form div div:nth-child(1) span span"
|
||||
);
|
||||
|
||||
const textPrice = await this.page_context.evaluate(
|
||||
(el) => el.textContent,
|
||||
element
|
||||
);
|
||||
|
||||
return extractNumber(textPrice) || null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
async isCloseProduct() {
|
||||
const close_time = await this.getCloseTime();
|
||||
|
||||
if (!close_time) {
|
||||
const priceWasBid = await this.getPriceWasBid();
|
||||
|
||||
await updateStatusByPrice(this.id, priceWasBid);
|
||||
return { result: true, close_time: null };
|
||||
}
|
||||
|
||||
await delay(500);
|
||||
|
||||
if (!close_time || new Date(close_time).getTime() <= new Date().getTime()) {
|
||||
console.log(`❌ [${this.id}] Product is close ${close_time}`);
|
||||
return { result: true, close_time };
|
||||
}
|
||||
|
||||
return { result: false, close_time };
|
||||
}
|
||||
|
||||
async handleWritePrice(page, bid_price) {
|
||||
await page.type("#price", String(bid_price));
|
||||
await delay(500);
|
||||
}
|
||||
|
||||
async placeBid(page) {
|
||||
try {
|
||||
await page.click("#bid-type-standard");
|
||||
await delay(500);
|
||||
|
||||
await page.click("#btnSubmit");
|
||||
await delay(1000);
|
||||
|
||||
await page.waitForSelector("button", { timeout: 5000 });
|
||||
|
||||
await delay(500);
|
||||
|
||||
await page.click("button");
|
||||
|
||||
await page.waitForNavigation({ timeout: 5000 });
|
||||
|
||||
await takeSnapshot(
|
||||
page,
|
||||
this,
|
||||
"bid-success",
|
||||
CONSTANTS.TYPE_IMAGE.SUCCESS
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(`❌ [${this.id}] Timeout to loading`);
|
||||
await takeSnapshot(page, this, "timeout to loading");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async handleReturnProductPage(page) {
|
||||
await page.goto(this.url);
|
||||
await delay(1000);
|
||||
}
|
||||
|
||||
async handleUpdateBid({
|
||||
lot_id,
|
||||
close_time,
|
||||
name,
|
||||
current_price,
|
||||
reserve_price,
|
||||
}) {
|
||||
const response = await updateBid(this.id, {
|
||||
lot_id,
|
||||
close_time,
|
||||
name,
|
||||
current_price,
|
||||
reserve_price: Number(reserve_price) || 0,
|
||||
});
|
||||
|
||||
if (response) {
|
||||
this.lot_id = response.lot_id;
|
||||
this.close_time = response.close_time;
|
||||
this.start_bid_time = response.start_bid_time;
|
||||
}
|
||||
}
|
||||
|
||||
update = async () => {
|
||||
if (!this.page_context) return;
|
||||
|
||||
const page = this.page_context;
|
||||
|
||||
try {
|
||||
const close_time = await this.getCloseTime();
|
||||
|
||||
// Chờ phần tử xuất hiện trước khi lấy giá trị
|
||||
await page
|
||||
.waitForSelector("#priceValue", { timeout: 5000 })
|
||||
.catch(() => null);
|
||||
const price_value = await page
|
||||
.$eval("#priceValue", (el) => el.value)
|
||||
.catch(() => null);
|
||||
|
||||
await page.waitForSelector("#lotId", { timeout: 5000 }).catch(() => null);
|
||||
const lot_id = await page
|
||||
.$eval("#lotId", (el) => el.value)
|
||||
.catch(() => null);
|
||||
|
||||
await page
|
||||
.waitForSelector("#placebid-sticky > div:nth-child(2) > div > h3", {
|
||||
timeout: 5000,
|
||||
})
|
||||
.catch(() => null);
|
||||
const name = await page
|
||||
.$eval(".dls-heading-3.lotPageTitle", (el) => el.innerText)
|
||||
.catch(() => null);
|
||||
|
||||
await page
|
||||
.waitForSelector(
|
||||
"#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span",
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
.catch(() => null);
|
||||
const current_price = await page
|
||||
.$eval(
|
||||
"#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span",
|
||||
(el) => el.innerText
|
||||
)
|
||||
.catch(() => null);
|
||||
|
||||
console.log(
|
||||
`📌 [${this.id}] Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price: ${price_value}`
|
||||
);
|
||||
|
||||
const data = removeFalsyValues(
|
||||
{
|
||||
lot_id,
|
||||
reserve_price: price_value,
|
||||
close_time: close_time ? String(close_time) : null,
|
||||
name,
|
||||
current_price: current_price ? extractNumber(current_price) : null,
|
||||
},
|
||||
["close_time"]
|
||||
);
|
||||
|
||||
this.handleUpdateBid(data);
|
||||
|
||||
return { price_value, lot_id, name, current_price };
|
||||
} catch (error) {
|
||||
console.error(`🚨 Error updating product info: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
action = async () => {
|
||||
try {
|
||||
const page = this.page_context;
|
||||
|
||||
await this.gotoLink();
|
||||
console.log(`🌍 [${this.id}] Navigated to link.`);
|
||||
|
||||
await delay(1000);
|
||||
|
||||
const { close_time, ...isCloseProduct } = await this.isCloseProduct();
|
||||
if (isCloseProduct.result) {
|
||||
console.log(
|
||||
`❌ [${this.id}] The product is closed, cannot place a bid.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await delay(500);
|
||||
|
||||
const { price_value } = await this.update();
|
||||
if (!price_value) return;
|
||||
|
||||
const { result, bid_price } = await this.validate({ page, price_value });
|
||||
if (!result) {
|
||||
console.log(
|
||||
`❌ [${this.id}] Validation failed. Unable to proceed with bidding.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const bidHistoriesItem = _.maxBy(this.histories, "price");
|
||||
if (bidHistoriesItem && bidHistoriesItem.price === this.current_price) {
|
||||
console.log(
|
||||
`🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem.price})`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (price_value != bid_price) {
|
||||
console.log(
|
||||
`✍️ [${this.id}] Updating bid price from ${price_value} → ${bid_price}`
|
||||
);
|
||||
await this.handleWritePrice(page, bid_price);
|
||||
}
|
||||
|
||||
console.log(`🚀 [${this.id}] Placing the bid...`);
|
||||
const resultPlaceBid = await this.placeBid(page);
|
||||
if (!resultPlaceBid) {
|
||||
console.log(`❌ [${this.id}] Error occurred while placing the bid.`);
|
||||
await takeSnapshot(page, this, "place-bid-action");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✅ [${this.id}] Bid placed successfully! 🏆 Bid Price: ${bid_price}, Closing Time: ${close_time}`
|
||||
);
|
||||
await this.handleReturnProductPage(page);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`🚨 [${this.id}] Error navigating the page: ${error.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ import {
|
|||
updateBid,
|
||||
updateStatusByPrice,
|
||||
} from "../../system/apis/bid.js";
|
||||
import axios from "../../system/axios.js";
|
||||
import {
|
||||
delay,
|
||||
extractNumber,
|
||||
|
|
@ -89,10 +88,9 @@ export class GraysProductBid extends ProductBid {
|
|||
await this.page_context.evaluate(() => {
|
||||
document.querySelector("#price").value = "";
|
||||
});
|
||||
console.log("✅ Cleared price");
|
||||
|
||||
await this.page_context.type("#price", String(this.max_price));
|
||||
console.log("✅ Typed max price");
|
||||
|
||||
await delay(5000);
|
||||
|
||||
const currentValue = await this.page_context.$eval(
|
||||
|
|
@ -100,8 +98,6 @@ export class GraysProductBid extends ProductBid {
|
|||
(el) => el.value
|
||||
);
|
||||
|
||||
console.log("✅ Checked currentValue:", currentValue);
|
||||
|
||||
if (currentValue !== String(this.max_price)) {
|
||||
console.warn(
|
||||
`[${this.id}] Value not match #price: ${currentValue} !== ${this.max_price}`
|
||||
|
|
@ -110,60 +106,20 @@ export class GraysProductBid extends ProductBid {
|
|||
}
|
||||
|
||||
await this.page_context.click("#btnSubmit");
|
||||
console.log("✅ Clicked submit");
|
||||
await delay(1000);
|
||||
|
||||
await this.page_context.waitForSelector("button", { timeout: 5000 });
|
||||
|
||||
await delay(5000);
|
||||
|
||||
// await this.page_context.click("button");
|
||||
|
||||
// await this.page_context.waitForNavigation({ timeout: 5000 });
|
||||
|
||||
// await this.page_context.waitForFunction(
|
||||
// () => document.body.innerText.includes("Successfully"),
|
||||
// { timeout: 5000 } // hoặc lâu hơn nếu cần
|
||||
// );
|
||||
// console.log("✅ Found 'Successfully'");
|
||||
|
||||
// await pushPrice({
|
||||
// bid_id: this.id,
|
||||
// price: this.max_price,
|
||||
// });
|
||||
|
||||
// await this.handleReturnProductPage();
|
||||
|
||||
if (this.isSandbox()) {
|
||||
await this.handlePlaceBidSandbox();
|
||||
} else {
|
||||
await this.handlePlaceBidLive();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
await this.page_context.goto(this.url);
|
||||
console.log(error);
|
||||
console.log(`❌ [${this.id}] Error in placeBid: ${error.message}`);
|
||||
return false;
|
||||
} finally {
|
||||
global.IS_CLEANING = true;
|
||||
}
|
||||
}
|
||||
|
||||
async handlePlaceBidLive() {
|
||||
try {
|
||||
global.IS_CLEANING = false;
|
||||
await delay(500);
|
||||
|
||||
await this.page_context.click("button");
|
||||
|
||||
await this.page_context.waitForNavigation({ timeout: 5000 });
|
||||
|
||||
// await this.page_context.waitForFunction(
|
||||
// () => document.body.innerText.includes("Successfully"),
|
||||
// { timeout: 5000 } // hoặc lâu hơn nếu cần
|
||||
// );
|
||||
|
||||
await this.page_context.waitForFunction(
|
||||
() => document.body.innerText.includes("Successfully"),
|
||||
{ timeout: 5000 } // hoặc lâu hơn nếu cần
|
||||
);
|
||||
console.log("✅ Found 'Successfully'");
|
||||
|
||||
await pushPrice({
|
||||
|
|
@ -172,25 +128,15 @@ export class GraysProductBid extends ProductBid {
|
|||
});
|
||||
|
||||
await this.handleReturnProductPage();
|
||||
} finally {
|
||||
global.IS_CLEANING = true;
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.page_context.goto(this.url);
|
||||
console.log(`❌ [${this.id}] Error in placeBid: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async handlePlaceBidSandbox() {
|
||||
// call to trickger server send mail
|
||||
await axios({
|
||||
url: this.ACTION_URL(),
|
||||
method: "POST",
|
||||
data: {
|
||||
id: this.id,
|
||||
record_url: `${process.env.BASE_URL}admin/bids/record/${this.name_record}`,
|
||||
},
|
||||
});
|
||||
|
||||
await this.close();
|
||||
}
|
||||
|
||||
async handleReturnProductPage() {
|
||||
await this.page_context.goto(this.url);
|
||||
await delay(1000);
|
||||
|
|
@ -384,7 +330,11 @@ export class GraysProductBid extends ProductBid {
|
|||
|
||||
const biddedData = this.getBidedData();
|
||||
|
||||
const isBided = (biddedData || []).find(
|
||||
console.log({
|
||||
biddedData,
|
||||
});
|
||||
|
||||
const isBided = biddedData.find(
|
||||
(item) => item.model === this.model && item.max_price === this.max_price
|
||||
);
|
||||
|
||||
|
|
@ -440,22 +390,9 @@ export class GraysProductBid extends ProductBid {
|
|||
return;
|
||||
}
|
||||
|
||||
const bids = await this.getCurrentHistories();
|
||||
|
||||
const bidedData = this.bidedData(bids);
|
||||
|
||||
console.log({ bidedData });
|
||||
|
||||
if (bidedData && bidedData.Price >= this.max_price + this.plus_price) {
|
||||
console.log(`[${this.id}] Bidded with ${bidedData.Price}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
"-------------------------------------BIDING---------------------------------------"
|
||||
);
|
||||
|
||||
await this.startRecordSandbox();
|
||||
const result = await this.placeBid();
|
||||
|
||||
global.IS_CLEANING = true;
|
||||
|
|
@ -463,30 +400,10 @@ export class GraysProductBid extends ProductBid {
|
|||
} catch (error) {
|
||||
console.log(`🚨 [${this.id}] Error placing bid: ${error.message}`);
|
||||
} finally {
|
||||
await this.stopRecordSandbox();
|
||||
console.log(`🔚 [${this.id}] Resetting bid flag.`);
|
||||
}
|
||||
}
|
||||
|
||||
async getCurrentHistories() {
|
||||
const { data } = await axios({
|
||||
url: `https://www.grays.com/api/LotInfo/GetBiddingHistory?lotId=${this.lot_id}`,
|
||||
});
|
||||
|
||||
const bids = data?.Bids || [];
|
||||
|
||||
return bids;
|
||||
}
|
||||
|
||||
bidedData(bids) {
|
||||
// A.V - Lidcombe NSW
|
||||
const data = bids.find(
|
||||
(bid) =>
|
||||
`${bid?.UserInitials} - ${bid.UserShortAddress}` == "A.V - Lidcombe NSW"
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
getBidedData() {
|
||||
return global[`BIDED_DATA_${this.web_bid?.origin_url}`];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@ export class LangtonsApiBid extends ApiBid {
|
|||
global.socket.off(`verify-code.${this.origin_url}`); // Xóa listener tránh rò rỉ bộ nhớ
|
||||
rej(
|
||||
new Error(
|
||||
`[${this.id}] Timeout: No verification code received within 3 minute.`
|
||||
`[${this.id}] Timeout: No verification code received within 1 minute.`
|
||||
)
|
||||
);
|
||||
}, 180 * 1000); // 180 giây
|
||||
}, 120 * 1000); // 120 giây
|
||||
|
||||
global.socket.on(`verify-code.${this.origin_url}`, async (data) => {
|
||||
console.log(`📢 [${this.id}] VERIFY CODE:`, data);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import CONSTANTS from "../../system/constants.js";
|
|||
import {
|
||||
convertAETtoUTC,
|
||||
isTimeReached,
|
||||
randomDelayWithMeta,
|
||||
removeFalsyValues,
|
||||
takeSnapshot,
|
||||
} from "../../system/utils.js";
|
||||
|
|
@ -231,11 +230,6 @@ export class LangtonsProductBid extends ProductBid {
|
|||
console.log(`🔄 [${this.id}] Starting bid process...`);
|
||||
global[`IS_PLACE_BID-${this.id}`] = true;
|
||||
|
||||
await this.delayForAction();
|
||||
|
||||
// start record
|
||||
await this.startRecordSandbox();
|
||||
|
||||
const continueShopBtn = await this.getContinueShopButton();
|
||||
if (continueShopBtn) {
|
||||
console.log(
|
||||
|
|
@ -333,26 +327,6 @@ export class LangtonsProductBid extends ProductBid {
|
|||
}
|
||||
|
||||
// 📌 Nhấn nút "Place Bid"
|
||||
if (this.isSandbox()) {
|
||||
await this.handlePlaceBidSandbox();
|
||||
} else {
|
||||
await this.handlePlaceBidLive();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`🚨 [${this.id}] Error placing bid: ${error.message}`);
|
||||
} finally {
|
||||
console.log(`🔚 [${this.id}] Resetting bid flag.`);
|
||||
global[`IS_PLACE_BID-${this.id}`] = false;
|
||||
|
||||
// stop record
|
||||
this.stopRecordSandbox();
|
||||
}
|
||||
}
|
||||
|
||||
async handlePlaceBidLive() {
|
||||
const page = this.page_context;
|
||||
|
||||
try {
|
||||
await page.click(
|
||||
".place-bid-submit .btn.btn-primary.btn-block.place-bid-btn",
|
||||
{ delay: 5000 }
|
||||
|
|
@ -382,7 +356,7 @@ export class LangtonsProductBid extends ProductBid {
|
|||
CONSTANTS.TYPE_IMAGE.SUCCESS
|
||||
);
|
||||
|
||||
// sendMessage(this);
|
||||
sendMessage(this);
|
||||
|
||||
console.log(`✅ [${this.id}] Bid placed successfully!`);
|
||||
return;
|
||||
|
|
@ -392,64 +366,10 @@ export class LangtonsProductBid extends ProductBid {
|
|||
`⚠️ [${this.id}] Bid action completed, but status is still "None".`
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(`[${this.id}] Error handlePlaceBidLive: ${error}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async handlePlaceBidSandbox() {
|
||||
if (!this.page_context) return;
|
||||
console.log("🔧 Starting to update the form action for sandbox mode...");
|
||||
const result = await this.setFormAction();
|
||||
|
||||
if (!result) {
|
||||
console.error("❌ Failed to update the form action for sandbox mode.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
"✅ Form action successfully updated. Proceeding to place the bid..."
|
||||
);
|
||||
|
||||
await this.handlePlaceBidLive();
|
||||
|
||||
await this.close();
|
||||
}
|
||||
|
||||
async setFormAction(newActionUrl = this.ACTION_URL()) {
|
||||
try {
|
||||
// Thay đổi action của form
|
||||
await this.page_context.evaluate(
|
||||
(url, record_url) => {
|
||||
const form = document.querySelector('form[name="place-bid-form"]');
|
||||
if (form) {
|
||||
form.action = url;
|
||||
|
||||
const hiddenInput = document.createElement("input");
|
||||
hiddenInput.type = "hidden";
|
||||
hiddenInput.name = "record_url";
|
||||
hiddenInput.value = record_url;
|
||||
form.appendChild(hiddenInput);
|
||||
}
|
||||
},
|
||||
newActionUrl,
|
||||
`${process.env.BASE_URL}admin/bids/record/${this.name_record}`
|
||||
);
|
||||
|
||||
// Kiểm tra lại giá trị action sau khi đổi
|
||||
const actualAction = await this.page_context.evaluate(() => {
|
||||
const form = document.querySelector('form[name="place-bid-form"]');
|
||||
return form?.action || null;
|
||||
});
|
||||
|
||||
// Log kết quả
|
||||
if (actualAction === newActionUrl) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.log(`🚨 [${this.id}] Error placing bid: ${error.message}`);
|
||||
} finally {
|
||||
console.log(`🔚 [${this.id}] Resetting bid flag.`);
|
||||
global[`IS_PLACE_BID-${this.id}`] = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
removeFalsyValues,
|
||||
} from "../../system/utils.js";
|
||||
import { ProductBid } from "../product-bid.js";
|
||||
import axios from "../../system/axios.js";
|
||||
|
||||
export class LawsonsProductBid extends ProductBid {
|
||||
constructor({ ...prev }) {
|
||||
|
|
@ -113,7 +112,7 @@ export class LawsonsProductBid extends ProductBid {
|
|||
}, url);
|
||||
};
|
||||
|
||||
async handlePlaceBidLive() {
|
||||
submitBid() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (!this.page_context || !this.model) {
|
||||
console.log(`[${this.id}] Page context or model is missing.`);
|
||||
|
|
@ -164,54 +163,6 @@ export class LawsonsProductBid extends ProductBid {
|
|||
});
|
||||
}
|
||||
|
||||
async handlePlaceBidSanbox() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (!this.page_context || !this.model) {
|
||||
console.log(`[${this.id}] Page context or model is missing.`);
|
||||
reject(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`💰 [${this.id}] Prepared Bid Amount: ${this.max_price}`);
|
||||
|
||||
const result = await this.page_context.evaluate(
|
||||
async (lotRef, url) => {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
bidAmount: 0,
|
||||
lotRef,
|
||||
v2: true,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
},
|
||||
this.model,
|
||||
configs.WEB_CONFIGS.LAWSONS.API_CHECKOUT
|
||||
);
|
||||
|
||||
console.log("🧾 API Bid Result:", {
|
||||
bid_amount: this.max_price,
|
||||
result,
|
||||
});
|
||||
|
||||
resolve(result);
|
||||
} catch (err) {
|
||||
console.log(`[${this.id}] Failed to submit bid: ${err.message}`);
|
||||
reject(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async handlePlaceBid() {
|
||||
// Kiểm tra xem có page context không, nếu không có thì kết thúc quá trình đấu giá
|
||||
if (!this.page_context) {
|
||||
|
|
@ -259,7 +210,7 @@ export class LawsonsProductBid extends ProductBid {
|
|||
const shouldStop =
|
||||
!response ||
|
||||
response?.currentBidAmount > this.max_price + this.plus_price ||
|
||||
![true, null].includes(response.isOutBid) ||
|
||||
response.isOutBid != true ||
|
||||
!reservePrice ||
|
||||
reservePrice > this.max_price + this.plus_price;
|
||||
|
||||
|
|
@ -268,11 +219,6 @@ export class LawsonsProductBid extends ProductBid {
|
|||
reservePrice,
|
||||
currentBidAmount: response?.currentBidAmount,
|
||||
maxBidAmount: response?.maxBidAmount,
|
||||
a: response?.currentBidAmount > this.max_price + this.plus_price,
|
||||
b: [true, null].includes(response.isOutBid),
|
||||
c: !reservePrice,
|
||||
d: reservePrice > this.max_price + this.plus_price,
|
||||
isOutBid: response.isOutBid,
|
||||
});
|
||||
return; // Nếu gặp điều kiện dừng thì không thực hiện đấu giá
|
||||
}
|
||||
|
|
@ -309,14 +255,8 @@ export class LawsonsProductBid extends ProductBid {
|
|||
`💰 [${this.id}] Placing a bid with amount: ${this.max_price}`
|
||||
);
|
||||
|
||||
let result = null;
|
||||
|
||||
// Gửi bid qua API và nhận kết quả
|
||||
if (this.isSandbox()) {
|
||||
result = await this.handleCallActionSanbox();
|
||||
} else {
|
||||
result = await this.handlePlaceBidLive();
|
||||
}
|
||||
const result = await this.submitBid();
|
||||
|
||||
// Nếu không có kết quả (lỗi khi gửi bid) thì dừng lại
|
||||
if (!result) return;
|
||||
|
|
@ -324,7 +264,7 @@ export class LawsonsProductBid extends ProductBid {
|
|||
console.log({ result });
|
||||
|
||||
// Gửi thông báo đã đấu giá thành công
|
||||
// sendMessage(this);
|
||||
sendMessage(this);
|
||||
|
||||
await this.page_context.reload({ waitUntil: "networkidle0" });
|
||||
|
||||
|
|
@ -339,23 +279,6 @@ export class LawsonsProductBid extends ProductBid {
|
|||
}
|
||||
}
|
||||
|
||||
async handleCallActionSanbox() {
|
||||
const result = await this.handlePlaceBidSanbox();
|
||||
|
||||
await axios({
|
||||
url: this.ACTION_URL({ type: "api" }),
|
||||
data: {
|
||||
id: this.id,
|
||||
data: JSON.stringify(result),
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
await this.close();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async waitApiInfo() {
|
||||
if (!this.page_context) {
|
||||
console.error(`❌ [${this.id}] Error: page_context is undefined.`);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import configs from "../../system/config.js";
|
|||
import { delay, isTimeReached, removeFalsyValues } from "../../system/utils.js";
|
||||
import { ProductBid } from "../product-bid.js";
|
||||
import { sendMessage } from "../../system/apis/notification.js";
|
||||
import axios from "../../system/axios.js";
|
||||
|
||||
export class PicklesProductBid extends ProductBid {
|
||||
constructor({ ...prev }) {
|
||||
|
|
@ -118,7 +117,7 @@ export class PicklesProductBid extends ProductBid {
|
|||
}
|
||||
};
|
||||
|
||||
handlePlaceBidLive() {
|
||||
submitBid() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (!this.page_context || !this.lot_id) {
|
||||
console.log(`[${this.id}] Page context or model is missing.`);
|
||||
|
|
@ -286,15 +285,7 @@ export class PicklesProductBid extends ProductBid {
|
|||
);
|
||||
|
||||
// Gửi bid qua API và nhận kết quả
|
||||
|
||||
let result = null;
|
||||
if (this.isSandbox()) {
|
||||
result = await this.handleCallActionSanbox();
|
||||
} else {
|
||||
result = await this.handlePlaceBidLive();
|
||||
}
|
||||
|
||||
console.log({ result });
|
||||
const result = await this.submitBid();
|
||||
|
||||
// Nếu không có kết quả (lỗi khi gửi bid) thì dừng lại
|
||||
if (!result || !result?.confirmationRequest) {
|
||||
|
|
@ -306,6 +297,8 @@ export class PicklesProductBid extends ProductBid {
|
|||
return;
|
||||
}
|
||||
|
||||
console.log({ result });
|
||||
|
||||
// Gửi thông báo đã đấu giá thành công
|
||||
// sendMessage(this);
|
||||
|
||||
|
|
@ -319,7 +312,6 @@ export class PicklesProductBid extends ProductBid {
|
|||
console.log(`✅ [${this.id}] Bid placed successfully!`);
|
||||
} catch (error) {
|
||||
// Nếu có lỗi xảy ra trong quá trình đấu giá, log lại lỗi
|
||||
console.log(error);
|
||||
console.log(`🚨 [${this.id}] Error placing bid: ${error.message}`);
|
||||
} finally {
|
||||
// Đảm bảo luôn reset trạng thái đấu giá sau khi hoàn thành
|
||||
|
|
@ -328,66 +320,6 @@ export class PicklesProductBid extends ProductBid {
|
|||
}
|
||||
}
|
||||
|
||||
async handlePlaceBidSanbox() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (!this.page_context || !this.lot_id) {
|
||||
console.log(`[${this.id}] Page context or model is missing.`);
|
||||
reject("Context is not define");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`💰 [${this.id}] Prepared Bid Amount: ${this.max_price}`);
|
||||
|
||||
const result = await this.page_context.evaluate(
|
||||
async (lotRef, url) => {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
itemId: lotRef,
|
||||
bidValues: {
|
||||
activity: "BID",
|
||||
maxBid: 0, // giá trị tối đa của sản phẩm
|
||||
roundedMaxBid: 0, // giá trị tối đa của sản phẩm
|
||||
submittedBuyNowValue: null,
|
||||
},
|
||||
buyerFeeCalculated: false,
|
||||
buyerFees: null,
|
||||
dashboardRedirectUrl: null,
|
||||
itemTitle: null,
|
||||
productLine: null,
|
||||
registrationRequired: false,
|
||||
totalAmount: null,
|
||||
updateDetailsRequired: null,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
},
|
||||
this.lot_id,
|
||||
configs.WEB_CONFIGS.PICKLES.API_CHECKOUT
|
||||
);
|
||||
|
||||
console.log("🧾 API Bid Result:", {
|
||||
bid_amount: this.max_price + this.plus_price,
|
||||
result,
|
||||
});
|
||||
|
||||
resolve(result);
|
||||
} catch (err) {
|
||||
console.log(`[${this.id}] Failed to submit bid: ${err.message}`);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isOutBid = async () => {
|
||||
try {
|
||||
// Chờ tối đa 10s cho element xuất hiện
|
||||
|
|
@ -544,29 +476,4 @@ export class PicklesProductBid extends ProductBid {
|
|||
console.error(`🚨 [${this.id}] Error navigating the page: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
async handleCallActionSanbox() {
|
||||
try {
|
||||
const result = await this.handlePlaceBidSanbox();
|
||||
|
||||
await axios({
|
||||
url: this.ACTION_URL({ type: "api" }),
|
||||
data: {
|
||||
id: this.id,
|
||||
data: JSON.stringify(result),
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (global[`TRACKING_PROCRESS_${this.id}`]) {
|
||||
clearInterval(global[`TRACKING_PROCRESS_${this.id}`]);
|
||||
}
|
||||
|
||||
await this.close();
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.log("a:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,10 @@
|
|||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { PuppeteerScreenRecorder } from "puppeteer-screen-recorder";
|
||||
import { outBid, uploadRecord } from "../system/apis/bid.js";
|
||||
import BID_TYPE from "../system/bid-type.js";
|
||||
import browser from "../system/browser.js";
|
||||
import CONSTANTS from "../system/constants.js";
|
||||
import { getPathProfile, randomDelayWithMeta } from "../system/utils.js";
|
||||
import { getPathProfile } from "../system/utils.js";
|
||||
import { Bid } from "./bid.js";
|
||||
import { unlink } from "fs/promises";
|
||||
|
||||
export class ProductBid extends Bid {
|
||||
// value 'live' | 'sandbox'
|
||||
static MODE_KEY = "mode_key";
|
||||
|
||||
// value is minutes esg: arrival_offset_seconds of web bid parent
|
||||
static ARRIAVAL_OFFSET_SECONDS_LIVE = "arrival_offset_seconds_live";
|
||||
static ARRIAVAL_OFFSET_SECONDS_SANDBOX = "arrival_offset_seconds_sandbox";
|
||||
|
||||
// value is minutes esg: early_tracking_seconds of web bid parent
|
||||
static EARLY_TRACKING_SECONDS = "early_tracking_seconds";
|
||||
static EARLY_TRACKING_SECONDS_LIVE = `${this.EARLY_TRACKING_SECONDS}_live`;
|
||||
static EARLY_TRACKING_SECONDS_SANDBOX = `${this.EARLY_TRACKING_SECONDS}_sandbox`;
|
||||
|
||||
id;
|
||||
max_price;
|
||||
model;
|
||||
|
|
@ -39,9 +23,6 @@ export class ProductBid extends Bid {
|
|||
name;
|
||||
reserve_price;
|
||||
update;
|
||||
metadata;
|
||||
recorder;
|
||||
name_record;
|
||||
|
||||
constructor({
|
||||
url,
|
||||
|
|
@ -61,7 +42,6 @@ export class ProductBid extends Bid {
|
|||
current_price,
|
||||
reserve_price,
|
||||
name,
|
||||
metadata,
|
||||
}) {
|
||||
super(BID_TYPE.PRODUCT_TAB, url);
|
||||
this.max_price = max_price || 0;
|
||||
|
|
@ -80,7 +60,6 @@ export class ProductBid extends Bid {
|
|||
this.current_price = current_price;
|
||||
this.name = name;
|
||||
this.reserve_price = reserve_price;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
setNewData({
|
||||
|
|
@ -101,7 +80,6 @@ export class ProductBid extends Bid {
|
|||
current_price,
|
||||
reserve_price,
|
||||
name,
|
||||
metadata,
|
||||
}) {
|
||||
this.max_price = max_price || 0;
|
||||
this.model = model;
|
||||
|
|
@ -120,7 +98,6 @@ export class ProductBid extends Bid {
|
|||
this.current_price = current_price;
|
||||
this.name = name;
|
||||
this.reserve_price = reserve_price;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
puppeteer_connect = async () => {
|
||||
|
|
@ -182,85 +159,4 @@ export class ProductBid extends Bid {
|
|||
console.error("❌ Error during navigation:", error);
|
||||
}
|
||||
}
|
||||
|
||||
getMode = () => {
|
||||
return (
|
||||
this.metadata.find((item) => item.key_name === ProductBid.MODE_KEY)
|
||||
?.value || "live"
|
||||
);
|
||||
};
|
||||
|
||||
getEarlyTrackingSeconds = () => {
|
||||
const mode = this.getMode();
|
||||
|
||||
return (
|
||||
this.metadata.find(
|
||||
(item) =>
|
||||
item.key_name === `${ProductBid.EARLY_TRACKING_SECONDS}_${mode}`
|
||||
)?.value || this.web_bid.early_tracking_seconds
|
||||
);
|
||||
};
|
||||
|
||||
isSandbox() {
|
||||
return this.getMode() !== "live";
|
||||
}
|
||||
|
||||
async delayForAction() {
|
||||
// Thực thi hành động
|
||||
console.log(`[${this.id}] 🚀 Executing action`);
|
||||
const { wait, delay } = randomDelayWithMeta();
|
||||
|
||||
console.log(
|
||||
`[${this.id}] ⏳ Delay for action: ${(delay / 1000).toFixed(2)} seconds`
|
||||
);
|
||||
|
||||
await wait;
|
||||
|
||||
console.log(`[${this.id}] ✅ Finished delay`);
|
||||
}
|
||||
|
||||
async close() {
|
||||
await outBid(this.id);
|
||||
}
|
||||
|
||||
async startRecordSandbox() {
|
||||
if (
|
||||
!this.page_context ||
|
||||
!this.name ||
|
||||
this.recorder ||
|
||||
this.getMode() === "live"
|
||||
)
|
||||
return;
|
||||
|
||||
const dirPath = CONSTANTS.RECORD_VIDEO_PATH;
|
||||
|
||||
// 📁 Kiểm tra và tạo thư mục nếu chưa có
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true }); // recursive để tạo nested folder nếu cần
|
||||
console.log("📁 Created recording folder:", dirPath);
|
||||
}
|
||||
|
||||
this.name_record = `${this.id}_${this.name}_${new Date().getTime()}.mp4`;
|
||||
|
||||
const filePath = path.join(dirPath, this.name_record);
|
||||
|
||||
this.recorder = new PuppeteerScreenRecorder(this.page_context);
|
||||
await this.recorder.start(filePath);
|
||||
}
|
||||
|
||||
async stopRecordSandbox() {
|
||||
if (!this.recorder || this.getMode() === "live") return;
|
||||
|
||||
await this.recorder.stop();
|
||||
|
||||
const filePath = path.join(CONSTANTS.RECORD_VIDEO_PATH, this.name_record);
|
||||
|
||||
const result = await uploadRecord(this, filePath);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
ACTION_URL(options = { type: "action" }) {
|
||||
return `${process.env.BASE_URL}bids/hook-action?id=${this.id}&type=${options.type}`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
"puppeteer": "^24.4.0",
|
||||
"puppeteer-extra": "^3.3.6",
|
||||
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
||||
"puppeteer-screen-recorder": "^3.0.6",
|
||||
"socket.io-client": "^4.8.1"
|
||||
}
|
||||
},
|
||||
|
|
@ -43,133 +42,6 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ffmpeg-installer/darwin-arm64": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/darwin-arm64/-/darwin-arm64-4.1.5.tgz",
|
||||
"integrity": "sha512-hYqTiP63mXz7wSQfuqfFwfLOfwwFChUedeCVKkBtl/cliaTM7/ePI9bVzfZ2c+dWu3TqCwLDRWNSJ5pqZl8otA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"hasInstallScript": true,
|
||||
"license": "https://git.ffmpeg.org/gitweb/ffmpeg.git/blob_plain/HEAD:/LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@ffmpeg-installer/darwin-x64": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/darwin-x64/-/darwin-x64-4.1.0.tgz",
|
||||
"integrity": "sha512-Z4EyG3cIFjdhlY8wI9aLUXuH8nVt7E9SlMVZtWvSPnm2sm37/yC2CwjUzyCQbJbySnef1tQwGG2Sx+uWhd9IAw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"hasInstallScript": true,
|
||||
"license": "LGPL-2.1",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@ffmpeg-installer/ffmpeg": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/ffmpeg/-/ffmpeg-1.1.0.tgz",
|
||||
"integrity": "sha512-Uq4rmwkdGxIa9A6Bd/VqqYbT7zqh1GrT5/rFwCwKM70b42W5gIjWeVETq6SdcL0zXqDtY081Ws/iJWhr1+xvQg==",
|
||||
"license": "LGPL-2.1",
|
||||
"optional": true,
|
||||
"optionalDependencies": {
|
||||
"@ffmpeg-installer/darwin-arm64": "4.1.5",
|
||||
"@ffmpeg-installer/darwin-x64": "4.1.0",
|
||||
"@ffmpeg-installer/linux-arm": "4.1.3",
|
||||
"@ffmpeg-installer/linux-arm64": "4.1.4",
|
||||
"@ffmpeg-installer/linux-ia32": "4.1.0",
|
||||
"@ffmpeg-installer/linux-x64": "4.1.0",
|
||||
"@ffmpeg-installer/win32-ia32": "4.1.0",
|
||||
"@ffmpeg-installer/win32-x64": "4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ffmpeg-installer/linux-arm": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-arm/-/linux-arm-4.1.3.tgz",
|
||||
"integrity": "sha512-NDf5V6l8AfzZ8WzUGZ5mV8O/xMzRag2ETR6+TlGIsMHp81agx51cqpPItXPib/nAZYmo55Bl2L6/WOMI3A5YRg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"hasInstallScript": true,
|
||||
"license": "GPLv3",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@ffmpeg-installer/linux-arm64": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-arm64/-/linux-arm64-4.1.4.tgz",
|
||||
"integrity": "sha512-dljEqAOD0oIM6O6DxBW9US/FkvqvQwgJ2lGHOwHDDwu/pX8+V0YsDL1xqHbj1DMX/+nP9rxw7G7gcUvGspSoKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"hasInstallScript": true,
|
||||
"license": "GPLv3",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@ffmpeg-installer/linux-ia32": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-ia32/-/linux-ia32-4.1.0.tgz",
|
||||
"integrity": "sha512-0LWyFQnPf+Ij9GQGD034hS6A90URNu9HCtQ5cTqo5MxOEc7Rd8gLXrJvn++UmxhU0J5RyRE9KRYstdCVUjkNOQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"hasInstallScript": true,
|
||||
"license": "GPLv3",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@ffmpeg-installer/linux-x64": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-x64/-/linux-x64-4.1.0.tgz",
|
||||
"integrity": "sha512-Y5BWhGLU/WpQjOArNIgXD3z5mxxdV8c41C+U15nsE5yF8tVcdCGet5zPs5Zy3Ta6bU7haGpIzryutqCGQA/W8A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"hasInstallScript": true,
|
||||
"license": "GPLv3",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@ffmpeg-installer/win32-ia32": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/win32-ia32/-/win32-ia32-4.1.0.tgz",
|
||||
"integrity": "sha512-FV2D7RlaZv/lrtdhaQ4oETwoFUsUjlUiasiZLDxhEUPdNDWcH1OU9K1xTvqz+OXLdsmYelUDuBS/zkMOTtlUAw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "GPLv3",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@ffmpeg-installer/win32-x64": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/win32-x64/-/win32-x64-4.1.0.tgz",
|
||||
"integrity": "sha512-Drt5u2vzDnIONf4ZEkKtFlbvwj6rI3kxw1Ck9fpudmtgaZIHD4ucsWB2lCZBXRxJgXR+2IMSti+4rtM4C4rXgg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "GPLv3",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@puppeteer/browsers": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.8.0.tgz",
|
||||
|
|
@ -298,11 +170,6 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
|
||||
"integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
|
|
@ -875,20 +742,6 @@
|
|||
"pend": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fluent-ffmpeg": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz",
|
||||
"integrity": "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"async": "^0.2.9",
|
||||
"which": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
|
|
@ -1241,12 +1094,6 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/isobject": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
|
||||
|
|
@ -1764,42 +1611,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer-screen-recorder": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer-screen-recorder/-/puppeteer-screen-recorder-3.0.6.tgz",
|
||||
"integrity": "sha512-yzWlXWGi/FjsAe0fVr/zYlKFKsH1PGc6Pm7t58wlzVbs/jdLimjdO5VaTIqDJIkWuXFfD4WuDFptdU6AG3ls/Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/prasanaworld/dashboard/tiers?frequency=one-time"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://opencollective.com/puppeteer-screen-recorder"
|
||||
},
|
||||
{
|
||||
"type": "corporate",
|
||||
"url": "https://github.com/sponsors/prasanaworld/dashboard/tiers"
|
||||
},
|
||||
{
|
||||
"type": "corporate",
|
||||
"url": "https://opencollective.com/puppeteer-screen-recorder"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fluent-ffmpeg": "^2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@ffmpeg-installer/ffmpeg": "^1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"puppeteer": "19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
|
|
@ -2099,18 +1910,6 @@
|
|||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"which": "bin/which"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
"puppeteer": "^24.4.0",
|
||||
"puppeteer-extra": "^3.3.6",
|
||||
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
||||
"puppeteer-screen-recorder": "^3.0.6",
|
||||
"socket.io-client": "^4.8.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB |
|
|
@ -124,27 +124,6 @@ export const updateStatusWork = async (item, filePath) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const uploadRecord = async (item, filePath) => {
|
||||
try {
|
||||
const response = await axios({
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
url: `bids/upload-record/${item.id}`,
|
||||
data: {
|
||||
video: fs.createReadStream(filePath),
|
||||
},
|
||||
});
|
||||
fs.unlinkSync(filePath);
|
||||
|
||||
return response.data?.data;
|
||||
} catch (error) {
|
||||
console.error("❌ Upload failed:", error.response?.data || error.message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateLoginStatus = async (data) => {
|
||||
try {
|
||||
const response = await axios({
|
||||
|
|
|
|||
|
|
@ -8,13 +8,11 @@ const CONSTANTS = {
|
|||
PROFILE_PATH: path.join(__dirname, "profiles"),
|
||||
LOCAL_DATA_PATH: path.join(__dirname, "local-data"),
|
||||
ERROR_IMAGES_PATH: path.join(__dirname, "error-images"),
|
||||
RECORD_VIDEO_PATH: path.join(__dirname, "videos"),
|
||||
TYPE_IMAGE: {
|
||||
ERRORS: "errors",
|
||||
SUCCESS: "success",
|
||||
WORK: "work",
|
||||
},
|
||||
KEEP_LIVE_URLS: ["https://auction.grays.com/confirmbid/index.html"],
|
||||
};
|
||||
|
||||
export default CONSTANTS;
|
||||
|
|
|
|||
|
|
@ -305,31 +305,19 @@ export async function isPageAvailable(page) {
|
|||
}
|
||||
}
|
||||
|
||||
// Lấy product có close time gần với hiện tại nhất
|
||||
export function findNearestClosingChild(webBid) {
|
||||
const now = Date.now();
|
||||
|
||||
const validChildren = webBid.children.filter((child) => {
|
||||
return (
|
||||
child.close_time &&
|
||||
!isNaN(new Date(child.close_time).getTime()) &&
|
||||
typeof child.getEarlyTrackingSeconds === "function"
|
||||
);
|
||||
});
|
||||
const validChildren = webBid.children.filter(
|
||||
(child) => child.close_time && !isNaN(new Date(child.close_time).getTime())
|
||||
);
|
||||
|
||||
if (validChildren.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 🎯 Tìm con gần nhất với hiện tại, ưu tiên early_tracking lớn nhất
|
||||
const nearestChild = _.minBy(validChildren, (child) => {
|
||||
const closeTime = new Date(child.close_time).getTime();
|
||||
const timeDiff = Math.abs(closeTime - now);
|
||||
const earlyTracking = child.getEarlyTrackingSeconds() || 0;
|
||||
|
||||
// ❗ Giảm ảnh hưởng của earlyTracking bằng cách trừ vào timeDiff
|
||||
// => càng nhiều earlyTracking thì càng tốt (timeDiff - earlyTracking * trọng số)
|
||||
return timeDiff - earlyTracking * 1000; // dùng 1000 để ưu tiên rõ rệt
|
||||
return Math.abs(new Date(child.close_time).getTime() - now);
|
||||
});
|
||||
|
||||
return nearestChild || null;
|
||||
|
|
@ -374,11 +362,3 @@ export function extractDomain(url) {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function randomDelayWithMeta(min = 1000, max = 10000) {
|
||||
const delay = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
return {
|
||||
delay,
|
||||
wait: new Promise((resolve) => setTimeout(resolve, delay)),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ const init = async () => {
|
|||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"
|
||||
);
|
||||
|
||||
await page.evaluateOnNewDocument(() => {
|
||||
Object.defineProperty(navigator, "webdriver", { get: () => false });
|
||||
});
|
||||
|
||||
const models = ScrapConfigsService.scrapModels(scrapConfigs, page);
|
||||
console.log(`Loaded ${models.length} scrape models`);
|
||||
|
||||
|
|
|
|||
|
|
@ -38,8 +38,7 @@ export class GraysScrapModel extends ScrapModel {
|
|||
};
|
||||
|
||||
getItemsInHtml = async (data) => {
|
||||
const elements = await this.page.$$('[style="display:block"] > div > div');
|
||||
|
||||
const elements = await this.page.$$(".sc-102aeaf3-1.eYPitT > div");
|
||||
const results = [];
|
||||
|
||||
for (const el of elements) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,257 @@
|
|||
import { access, constants, mkdir, readFile, writeFile } from "fs/promises";
|
||||
import * as path from "path";
|
||||
import { dirname } from "path";
|
||||
import browser from "../system/browser.js";
|
||||
import CONSTANTS from "../system/constants.js";
|
||||
import {
|
||||
extractDomain,
|
||||
extractModelId,
|
||||
extractNumber,
|
||||
} from "../system/ultils.js";
|
||||
import { ScrapModel } from "./scrap-model.js";
|
||||
|
||||
export class GumtreeScrapModel extends ScrapModel {
|
||||
COOKIE_FILE_PATH = () => {
|
||||
const filePath = path.join(
|
||||
CONSTANTS.PROFILE_PATH,
|
||||
`${extractDomain(this.web_bid.origin_url)}.json`
|
||||
);
|
||||
const dir = dirname(filePath);
|
||||
|
||||
// Đảm bảo thư mục chứa cookie tồn tại
|
||||
access(dir, constants.F_OK).catch(() => mkdir(dir, { recursive: true }));
|
||||
|
||||
return filePath;
|
||||
};
|
||||
action = async () => {
|
||||
const urlsData = this.extractUrls();
|
||||
|
||||
for (let item of urlsData) {
|
||||
const data = await this.scrapOnMultiplePage(item);
|
||||
const results = this.filterItemByKeyword(item.keyword, data);
|
||||
this.results[item.keyword] = results;
|
||||
console.log({ results: this.results });
|
||||
}
|
||||
};
|
||||
|
||||
async loadCookiesIfExist(page) {
|
||||
try {
|
||||
// Đọc file cookie
|
||||
const cookieJson = await readFile(this.COOKIE_FILE_PATH(), "utf-8");
|
||||
const cookies = JSON.parse(cookieJson);
|
||||
|
||||
if (cookies && cookies.length > 0) {
|
||||
// Set cookie lên page
|
||||
await page.setCookie(...cookies);
|
||||
console.log("Cookies loaded to page");
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT") {
|
||||
console.log("No cookie file found, continue without loading cookies");
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildPaginatedUrl(item, current_page = 1) {
|
||||
if (current_page <= 1) {
|
||||
return item.url;
|
||||
}
|
||||
|
||||
const url = new URL(item.url);
|
||||
|
||||
// Lấy pathname và tách các phần
|
||||
const parts = url.pathname.split("/").filter(Boolean); // loại bỏ phần rỗng do dấu /
|
||||
const keywordIndex = parts.findIndex((p) => p === item.keyword);
|
||||
|
||||
if (keywordIndex === -1) {
|
||||
// fallback nếu không tìm thấy keyword trong url
|
||||
return item.url;
|
||||
}
|
||||
|
||||
// Chèn "page-N" sau keyword
|
||||
parts.splice(keywordIndex + 1, 0, `page-${current_page}`);
|
||||
|
||||
// Gán lại pathname và trả về
|
||||
url.pathname = "/" + parts.join("/");
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
async saveCookies(page) {
|
||||
const cookies = await page.cookies();
|
||||
|
||||
try {
|
||||
let existingCookies = [];
|
||||
try {
|
||||
const data = await readFile(this.COOKIE_FILE_PATH(), "utf-8");
|
||||
existingCookies = JSON.parse(data);
|
||||
} catch (err) {
|
||||
// Nếu chưa có file cookies thì bỏ qua
|
||||
}
|
||||
|
||||
// Gộp cookie theo tên + domain
|
||||
const merged = [...existingCookies, ...cookies];
|
||||
const uniqueCookies = Object.values(
|
||||
merged.reduce((acc, c) => {
|
||||
acc[`${c.name}|${c.domain}`] = c;
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
|
||||
await writeFile(
|
||||
this.COOKIE_FILE_PATH(),
|
||||
JSON.stringify(uniqueCookies, null, 2)
|
||||
);
|
||||
console.log(`✅ Saved ${uniqueCookies.length} cookies.`);
|
||||
} catch (err) {
|
||||
console.error("❌ Failed to save cookies:", err);
|
||||
}
|
||||
}
|
||||
|
||||
scrapOnMultiplePage = async (item, current_page = 1) => {
|
||||
const context = await browser.createBrowserContext();
|
||||
|
||||
const newPage = await context.newPage();
|
||||
await newPage.emulateTimezone("Australia/Sydney");
|
||||
|
||||
await newPage.evaluateOnNewDocument(() => {
|
||||
Object.defineProperty(navigator, "webdriver", {
|
||||
get: () => false,
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
const url = this.buildPaginatedUrl(item, current_page);
|
||||
|
||||
await newPage.setExtraHTTPHeaders({
|
||||
Accept:
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||
Referer: item.url,
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
Connection: "keep-alive",
|
||||
"Device-Memory": "8",
|
||||
Dpr: "1",
|
||||
Priority: "u=0, i",
|
||||
Downlink: "1.7",
|
||||
Rtt: "150",
|
||||
Ect: "4g",
|
||||
Cookie: `machId=Ooi7FqYBXo3ARpf8UgUnyaqDXb8gu6gWi5RBb6AYKA5uU27aumFtkY31jSNqB-rWbMi4_YrC66nS_6ixprMIf0ZVwQv6r_llGU0q; libtg=a; bs=%7B%22st%22%3A%7B%7D%7D; afx_csid_hs=ec41291cab664d00a9a715f651fafebf8dda253152a003f; _gcl_au=1.1.442579364.1748923314; _sharedID=3bf469ac-7844-43cd-8f97-578eabfcedde; _sharedID_cst=zix7LPQsHA%3D%3D; _ga=GA1.1.1525016630.1748923314; _fbp=fb.2.1748923314432.133704941733557798; __gsas=ID=3feeb077fda5501c:T=1748923189:RT=1748923189:S=ALNI_Mbsr7RafzbystD4r52ksjRS4S23zQ; s_ecid=MCMID%7C37798711041403725812860372477407048703; AMCVS_50BE5F5858D2477A0A495C7F%40AdobeOrg=1; _lr_geo_location_state=SG; _lr_geo_location=VN; _cc_id=7e5f418613248c588934fb2345dd9bac; panoramaId_expiry=1749527989238; panoramaId=1f8fc411f0404b0c540182b8fcd04945a7023f3b737fa7dcdac189479761ed2b; panoramaIdType=panoIndiv; optimizelyEndUserId=oeu1748923315172r0.2636919564518334; DM_SitId1958=1; DM_SitId1958SecId15098=1; afx_ptpce=1; _ga_TC0NXL1S6B=deleted; _lr_env_src_ats=false; uuid=410C8105-3511-44FC-998F-499E8D02C682; aam_tnt=aamsegid%3D6797281%2Caamsegid%3D6880889; aam_uuid=38041763738279131192814186141414595589; __utmc=160852194; __utmz=160852194.1748931762.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); DM_SitId1958SecId15099=1; afx_profile_da=1; afx_syndr=1748997579162; afx_profile_hs=%7B%22acid%22%3A%22da141af1ddce2856532f636d429ee1aaf7709fbd%22%2C%22prid%22%3A%22CBmqA1KYADpw7phCiqki9LCJKtakXP1T9daPv46II%2BCD8OxOEvqXyJN%2BLRXlRVmg2WbQMWDpvSljMflz8UN15cgfZ2SO2UMG%2FoGVoX%2BMZDZlgoJq7E4YAJOovVXxybLQd%2Bv9aeSYfoTB2wYIsD1TQJGLFeebO%2BDxTeffm2o4qhXLHj3Lj02TGn21W5GMWBiknNuosNKK8lEooKTNsCeCRw%3D%3D%22%2C%22pridsd%22%3A%222025-06-04T00%3A37%3A32.3575981%2B00%3A00%22%2C%22consent%22%3A%7B%22dnt%22%3Afalse%7D%2C%22pv%22%3A%222025.6.2.1%22%7D; afx_profile=%7B%22acid%22%3A%22da141af1ddce2856532f636d429ee1aaf7709fbd%22%2C%22prid%22%3A%22CBmqA1KYADpw7phCiqki9LCJKtakXP1T9daPv46II%2BCD8OxOEvqXyJN%2BLRXlRVmg2WbQMWDpvSljMflz8UN15cgfZ2SO2UMG%2FoGVoX%2BMZDZlgoJq7E4YAJOovVXxybLQd%2Bv9aeSYfoTB2wYIsD1TQJGLFeebO%2BDxTeffm2o4qhXLHj3Lj02TGn21W5GMWBiknNuosNKK8lEooKTNsCeCRw%3D%3D%22%2C%22pridsd%22%3A%222025-06-04T00%3A37%3A32.3575981%2B00%3A00%22%2C%22consent%22%3A%7B%22dnt%22%3Afalse%7D%2C%22pv%22%3A%222025.6.2.1%22%7D; __utma=160852194.1525016630.1748923314.1748931762.1748999624.2; optimizelySession=0; aam_dfp=aamsegid%3D6797281%2C6880889%2C7220740%2C7333813%2C8458228%2C8458232%2C9320660%2C9448391; _lr_sampling_rate=100; kndctr_50BE5F5858D2477A0A495C7F_AdobeOrg_identity=CiYzNzc5ODcxMTA0MTQwMzcyNTgxMjg2MDM3MjQ3NzQwNzA0ODcwM1IRCOeLyJ_zMhgBKgRTR1AzMAHwAaW2t8zzMg==; PEAKHOUR_VISIT=683fff8f780a5460000003137b91a2b1; __rp_ch=683fff8f780a5460000003137b91a30f:smXduwBNl3z822Ybp00oZcPPLV3__dJEOFcljzXBmjLgXU2-xUTbqQbLyq7sX0AdFq816gpMpa1-88AVag; AMCV_50BE5F5858D2477A0A495C7F%40AdobeOrg=-408604571%7CMCMID%7C37798711041403725812860372477407048703%7CMCIDTS%7C20243%7CMCAID%7CNONE%7CMCOPTOUT-1749031987s%7CNONE%7CMCAAMLH-1749629587%7C3%7CMCAAMB-1749629587%7Cj8Odv6LonN4r3an7LhD3WZrU1bUpAkFkkiY1ncBR96t2PTI%7CvVersion%7C4.6.0%7CMCCIDH%7C-1081851458; kndctr_50BE5F5858D2477A0A495C7F_AdobeOrg_cluster=sgp3; _lr_retry_request=true; up=%7B%22upData%22%3A%22eJy1kcEOgjAQRL9m7%2B22CB4VLibe9AdKBSUibVgw8e%2FdKomYaEKMXNqXzWZmOoV4DYh1fuIT1CowqEwALs585zdviAAjFLVzvmh5bnkuEy2WzO2wS1eG7Wa3D1wyH0xXMFc2bJSmpgLidKxsK7KO2TPjoIpSSjGoyqmyHBvTEJs%2BP4GNnk4PByF0NN2ha%2FvZcr%2FLzlc0mQv1zXEUVf1e8beG%2F%2FuVjX8Z6SRgnN0BgF606w%3D%3D%22%7D; afx_dcr=; afx_orref=; __gads=ID=dd76a5311e79db68:T=1748923190:RT=1749025285:S=ALNI_MaQdPDjdG0-l54Mw-5zkEZq3ruzwA; __eoi=ID=34a32a91b325f0d8:T=1748923190:RT=1749025285:S=AA-AfjZsqCgmvwIEsjsGZWkO7Mjv; __rtbh.lid=%7B%22eventType%22%3A%22lid%22%2C%22id%22%3A%22XCQntaL7xSSUJ3wNfloA%22%2C%22expiryDate%22%3A%222026-06-04T08%3A23%3A48.727Z%22%7D; __rtbh.uid=%7B%22eventType%22%3A%22uid%22%2C%22id%22%3A%22undefined%22%2C%22expiryDate%22%3A%222026-06-04T08%3A23%3A48.779Z%22%7D; _uetsid=7caa7710402f11f08d410f2f6e2194d2; _uetvid=7caabae0402f11f0ad477f10ec12ba4d; nol_fpid=wxumrg5hye2hhnzso9vqwynhtcfrc1748923314|1748923314929|1749025428972|1749025429412; cto_bundle=C4hywl9rRTJJVXZPJTJGMVMlMkZWYjJYS1Z3OTlmY3BwZWklMkJVRDMzUGQxZk12OGhKU2JTbzZlSG9MU2J4JTJCVG5iNndUOXhtMjJmTXpzQjhKV2RjVzNBeGJINGQ1RWxOazBUazQxQXFEek8wSGFIYUpUZzRhNWlpVmlGVTElMkZYeTZicyUyRmpkU0hFS1BQaFJ6YmgwSzM5SXo1bVdaa1lBc0FOM2NpU28xR1A3STlnVmhuR1Nlbjk4ZVBvT2ZrdUhCZVJuNzdlajNpZzg4bGxxZWlZQkNoVmpLRjBhU25wJTJGVnclM0QlM0Q; cto_bidid=bTR-CV9JMmNVbnVJOTVEVWl1YWdiNEsybEFlYzR5Q2JLQnVLdjBkcU1VU1d0JTJGVkdqVSUyRnQ0QzZaem5IcVBRcUY4Vm9EVFBLJTJCa2dPU2lyWWVPbUk4YVQ1TG5tQzJGTjQ0dEp2d1pUSjZOcXNkNEltVDBvb3RmOFZJUktkVXZUS0dGQTV3Zw; _ga_TC0NXL1S6B=GS2.1.s1749024783$o6$g1$t1749025457$j60$l0$h0`,
|
||||
});
|
||||
|
||||
await this.loadCookiesIfExist(newPage);
|
||||
|
||||
await newPage.goto(url);
|
||||
|
||||
await this.saveCookies(newPage);
|
||||
|
||||
const result = await this.getItemsInHtml({ ...item, url }, newPage);
|
||||
|
||||
console.log({ current_page });
|
||||
// Kiểm tra nếu là trang cuối hoặc không còn kết quả
|
||||
const lastBtn = await newPage.$(
|
||||
".page-number-navigation__link.page-number-navigation__link-last"
|
||||
);
|
||||
const lastPageNumber = lastBtn
|
||||
? this.extractPageNumber(
|
||||
await lastBtn.evaluate((a) => a.getAttribute("href"))
|
||||
)
|
||||
: current_page;
|
||||
|
||||
await this.closePageAndContext(newPage, context);
|
||||
|
||||
if (!result.length || current_page >= lastPageNumber) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Đệ quy lấy trang tiếp theo
|
||||
const nextResults = await this.scrapOnMultiplePage(
|
||||
item,
|
||||
current_page + 1
|
||||
);
|
||||
return [...result, ...nextResults];
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
async closePageAndContext(newPage, context) {
|
||||
// try {
|
||||
// if (newPage && !newPage.isClosed?.()) {
|
||||
// await newPage.close();
|
||||
// }
|
||||
// } catch (err) {
|
||||
// console.warn("newPage close error:", err.message);
|
||||
// }
|
||||
// try {
|
||||
// await context?.close();
|
||||
// } catch (err) {
|
||||
// console.warn("context close error:", err.message);
|
||||
// }
|
||||
}
|
||||
|
||||
extractPageNumber(url) {
|
||||
const match = url.match(/page-(\d+)/);
|
||||
return match ? parseInt(match[1], 10) : 1; // Trả về 1 nếu không tìm thấy số trang
|
||||
}
|
||||
|
||||
getItemsInHtml = async (data, page) => {
|
||||
const elements = await page.$$(
|
||||
"#react-root > div > div:nth-child(2) > div > div:nth-child(2) > main > section > div:first-child > div a"
|
||||
);
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const el of elements) {
|
||||
// Scroll tới phần tử trước khi thao tác
|
||||
await el.evaluate((node) =>
|
||||
node.scrollIntoView({ behavior: "smooth", block: "center" })
|
||||
);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 800));
|
||||
|
||||
const url = await el
|
||||
.evaluate((a) => a.getAttribute("href"))
|
||||
.catch(() => null);
|
||||
|
||||
const { imageUrl, className } = await el
|
||||
.$eval("img.user-ad-image__thumbnail", (img) => {
|
||||
const src = img.getAttribute("src") || img.getAttribute("data-src");
|
||||
return {
|
||||
imageUrl: src && src.trim() !== "" ? src : null,
|
||||
className: img.className, // Lấy class name đầy đủ của thẻ img
|
||||
};
|
||||
})
|
||||
.catch(() => ({ imageUrl: null, className: null }));
|
||||
|
||||
const name = await el
|
||||
.$eval(".user-ad-row-new-design__title-span", (el) =>
|
||||
el.textContent.trim()
|
||||
)
|
||||
.catch(() => null);
|
||||
|
||||
const current_price = await el
|
||||
.$eval(".user-ad-price-new-design__price", (el) =>
|
||||
el.textContent.trim()
|
||||
)
|
||||
.catch(() => null);
|
||||
|
||||
if (url) {
|
||||
results.push({
|
||||
url: `${this.web_bid.origin_url}${url}`,
|
||||
image_url: imageUrl,
|
||||
name,
|
||||
keyword: data.keyword,
|
||||
model: extractModelId(`${this.web_bid.origin_url}${url}`),
|
||||
current_price: extractNumber(current_price),
|
||||
scrap_config_id: this.scrap_config_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { ScrapModel } from "./scrap-model.js";
|
||||
import axios from "axios";
|
||||
|
||||
export class GumtreeScrapModel extends ScrapModel {
|
||||
action = async () => {
|
||||
const urlsData = this.extractUrls();
|
||||
|
||||
const { data } = await axios({
|
||||
method: "POST",
|
||||
baseURL: process.env.WVM_API,
|
||||
url: "gumtree-scrap",
|
||||
timeout: 300000,
|
||||
data: {
|
||||
data: urlsData,
|
||||
scrap_config_id: this.scrap_config_id,
|
||||
},
|
||||
});
|
||||
|
||||
this.results = data;
|
||||
};
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ export class ScrapModel {
|
|||
|
||||
filterItemByKeyword(keyword, data) {
|
||||
return data.filter((item) =>
|
||||
item.name?.toLowerCase().includes(keyword.toLowerCase())
|
||||
item.name.toLowerCase().includes(keyword.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,11 @@
|
|||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.2",
|
||||
"cheerio": "^1.0.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"lodash": "^4.17.21",
|
||||
"node-ssh": "^13.2.1",
|
||||
"playwright": "^1.52.0",
|
||||
"puppeteer": "^24.4.0",
|
||||
"puppeteer-extra": "^3.3.6",
|
||||
"puppeteer-extra-plugin-stealth": "^2.11.2"
|
||||
|
|
@ -150,6 +153,15 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asn1": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-types": {
|
||||
"version": "0.13.4",
|
||||
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
|
||||
|
|
@ -272,6 +284,21 @@
|
|||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
|
|
@ -291,6 +318,15 @@
|
|||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/buildcheck": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
|
||||
"integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
|
|
@ -313,6 +349,48 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz",
|
||||
"integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cheerio-select": "^2.1.0",
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.1.0",
|
||||
"encoding-sniffer": "^0.2.0",
|
||||
"htmlparser2": "^9.1.0",
|
||||
"parse5": "^7.1.2",
|
||||
"parse5-htmlparser2-tree-adapter": "^7.0.0",
|
||||
"parse5-parser-stream": "^7.1.2",
|
||||
"undici": "^6.19.5",
|
||||
"whatwg-mimetype": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio-select": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
|
||||
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-select": "^5.1.0",
|
||||
"css-what": "^6.1.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/chromium-bidi": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz",
|
||||
|
|
@ -418,6 +496,48 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cpu-features": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
|
||||
"integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"buildcheck": "~0.0.6",
|
||||
"nan": "^2.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
||||
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-what": "^6.1.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"domutils": "^3.0.1",
|
||||
"nth-check": "^2.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/css-what": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
|
||||
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
|
||||
|
|
@ -482,6 +602,61 @@
|
|||
"integrity": "sha512-jJF48UdryzKiWhJ1bLKr7BFWUQCEIT5uCNbDLqkQJBtkFxYzILJH44WN0PDKMIlGDN7Utb8vyUY85C3w4R/t2g==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"entities": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.5.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
|
||||
|
|
@ -514,6 +689,19 @@
|
|||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encoding-sniffer": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz",
|
||||
"integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.6.3",
|
||||
"whatwg-encoding": "^3.1.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
|
|
@ -523,6 +711,18 @@
|
|||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/env-paths": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
||||
|
|
@ -758,6 +958,20 @@
|
|||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
|
|
@ -920,6 +1134,25 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
|
||||
"integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.1.0",
|
||||
"entities": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||
|
|
@ -946,6 +1179,18 @@
|
|||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
|
|
@ -1034,6 +1279,18 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/isobject": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
|
||||
|
|
@ -1127,6 +1384,30 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir/node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
|
@ -1217,6 +1498,13 @@
|
|||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nan": {
|
||||
"version": "2.22.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz",
|
||||
"integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/netmask": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
|
||||
|
|
@ -1226,6 +1514,35 @@
|
|||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-ssh": {
|
||||
"version": "13.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-ssh/-/node-ssh-13.2.1.tgz",
|
||||
"integrity": "sha512-rfl4GWMygQfzlExPkQ2LWyya5n2jOBm5vhEnup+4mdw7tQhNpJWbP5ldr09Jfj93k5SfY5lxcn8od5qrQ/6mBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-stream": "^2.0.0",
|
||||
"make-dir": "^3.1.0",
|
||||
"sb-promise-queue": "^2.1.0",
|
||||
"sb-scandir": "^3.1.0",
|
||||
"shell-escape": "^0.2.0",
|
||||
"ssh2": "^1.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/nth-check": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
|
|
@ -1297,6 +1614,55 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5-htmlparser2-tree-adapter": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
|
||||
"integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domhandler": "^5.0.3",
|
||||
"parse5": "^7.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5-parser-stream": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
|
||||
"integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parse5": "^7.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5/node_modules/entities": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz",
|
||||
"integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
|
|
@ -1318,6 +1684,36 @@
|
|||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz",
|
||||
"integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.52.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz",
|
||||
"integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
|
|
@ -1570,6 +1966,33 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sb-promise-queue": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/sb-promise-queue/-/sb-promise-queue-2.1.1.tgz",
|
||||
"integrity": "sha512-qXfdcJQMxMljxmPprn4Q4hl3pJmoljSCzUvvEBa9Kscewnv56n0KqrO6yWSrGLOL9E021wcGdPa39CHGKA6G0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/sb-scandir": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/sb-scandir/-/sb-scandir-3.1.1.tgz",
|
||||
"integrity": "sha512-Q5xiQMtoragW9z8YsVYTAZcew+cRzdVBefPbb9theaIKw6cBo34WonP9qOCTKgyAmn/Ch5gmtAxT/krUgMILpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sb-promise-queue": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
|
|
@ -1618,6 +2041,12 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-escape": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/shell-escape/-/shell-escape-0.2.0.tgz",
|
||||
"integrity": "sha512-uRRBT2MfEOyxuECseCZd28jC1AJ8hmqqneWQ4VWUTgCAFvb3wKU1jLqj6egC4Exrr88ogg3dp+zroH4wJuaXzw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||
|
|
@ -1672,6 +2101,23 @@
|
|||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ssh2": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz",
|
||||
"integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"asn1": "^0.2.6",
|
||||
"bcrypt-pbkdf": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"cpu-features": "~0.0.10",
|
||||
"nan": "^2.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/streamx": {
|
||||
"version": "2.22.0",
|
||||
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz",
|
||||
|
|
@ -1751,12 +2197,27 @@
|
|||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/typed-query-selector": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz",
|
||||
"integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.21.3",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
|
||||
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
|
|
@ -1773,6 +2234,27 @@
|
|||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iconv-lite": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -13,8 +13,11 @@
|
|||
"type": "module",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.2",
|
||||
"cheerio": "^1.0.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"lodash": "^4.17.21",
|
||||
"node-ssh": "^13.2.1",
|
||||
"playwright": "^1.52.0",
|
||||
"puppeteer": "^24.4.0",
|
||||
"puppeteer-extra": "^3.3.6",
|
||||
"puppeteer-extra-plugin-stealth": "^2.11.2"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { AllbidsScrapModel } from "../models/allbids-scrap-model.js";
|
||||
import { GraysScrapModel } from "../models/grays-scrap-model.js";
|
||||
import { GumtreeScrapModel } from "../models/gumtree-scrap-model.js";
|
||||
import { LangtonsScrapModel } from "../models/langtons-scrap-model.js";
|
||||
import { LawsonsScrapModel } from "../models/lawsons-scrap-model.js";
|
||||
import { PicklesScrapModel } from "../models/pickles-scrap-model.js";
|
||||
|
|
@ -42,6 +43,13 @@ export class ScrapConfigsService {
|
|||
page: page,
|
||||
});
|
||||
}
|
||||
case "https://www.gumtree.com.au": {
|
||||
return new GumtreeScrapModel({
|
||||
...scrapConfig,
|
||||
scrap_config_id: scrapConfig.id,
|
||||
page: page,
|
||||
});
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
// import puppeteer from 'puppeteer';
|
||||
import puppeteer from "puppeteer-extra";
|
||||
import StealthPlugin from "puppeteer-extra-plugin-stealth";
|
||||
import CONSTANTS from "./constants.js";
|
||||
|
||||
puppeteer.use(StealthPlugin());
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: process.env.ENVIRONMENT === "prod" ? "new" : false,
|
||||
headless: process.env.ENVIRONMENT === "prod" ? true : false,
|
||||
// userDataDir: CONSTANTS.PROFILE_PATH, // Thư mục lưu profile
|
||||
timeout: 60000,
|
||||
args: [
|
||||
|
|
@ -29,7 +31,7 @@ const browser = await puppeteer.launch({
|
|||
"--disable-threaded-animation", // Giảm animation chạy trên nhiều thread
|
||||
"--disable-threaded-scrolling", // Tắt cuộn trang đa luồng
|
||||
"--disable-logging", // Tắt log debug
|
||||
"--blink-settings=imagesEnabled=false", // Không tải hình ảnh,
|
||||
// "--blink-settings=imagesEnabled=false", // Không tải hình ảnh,
|
||||
"--disable-background-timer-throttling", // Tránh việc throttling các timer khi chạy nền.
|
||||
"--disable-webrtc",
|
||||
"--disable-ipc-flooding-protection", // Nếu có extension cần IPC, cái này giúp tối ưu.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
import * as path from "path";
|
||||
import { fileURLToPath } from "url"; // ✅ Cần import từ 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const CONSTANTS = {
|
||||
PROFILE_PATH: path.join(__dirname, "profiles"),
|
||||
};
|
||||
|
||||
export default CONSTANTS;
|
||||
|
|
@ -29,6 +29,10 @@ export function extractModelId(url) {
|
|||
const match = url.match(/-(\d+)(?:[\?#]|$)/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
case "https://www.gumtree.com.au": {
|
||||
const match = url.match(/\/(\d+)(?:\/)?$/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue