first commit

This commit is contained in:
Admin 2025-08-21 15:37:34 +07:00
parent a31a77d934
commit 276abd49f8
25 changed files with 474 additions and 158 deletions

View File

@ -2,6 +2,5 @@ VITE_API_URL=""
VITE_API_TOKEN="" VITE_API_TOKEN=""
VITE_API_SYNC_URL=""

3
.gitignore vendored
View File

@ -11,7 +11,8 @@ node_modules
dist dist
dist-content dist-content
dist-ssr dist-ssr
auto-post-facebook-extensions /auto-post-facebook-extensions*
auto-post-facebook-extensions_1.0/
*.local *.local
# Editor directories and files # Editor directories and files

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -7,9 +7,9 @@
<title>Vite + React + TS</title> <title>Vite + React + TS</title>
<script type="module" crossorigin src="/assets/popup.js"></script> <script type="module" crossorigin src="/assets/popup.js"></script>
<link rel="modulepreload" crossorigin href="/assets/_commonjsHelpers-CqkleIqs.js"> <link rel="modulepreload" crossorigin href="/assets/_commonjsHelpers-CqkleIqs.js">
<link rel="modulepreload" crossorigin href="/assets/product-api.service-BCmn_jbQ.js"> <link rel="modulepreload" crossorigin href="/assets/product-api.service-Dn0sYl6J.js">
<link rel="modulepreload" crossorigin href="/assets/message.service-DcR3euAR.js"> <link rel="modulepreload" crossorigin href="/assets/message.service-DcR3euAR.js">
<link rel="stylesheet" crossorigin href="/assets/popup-DfztxwQY.css"> <link rel="stylesheet" crossorigin href="/assets/popup-DxX4E5UP.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

15
package-lock.json generated
View File

@ -27,6 +27,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"fs-extra": "^11.3.1", "fs-extra": "^11.3.1",
"lodash": "^4.17.21",
"lucide-react": "^0.540.0", "lucide-react": "^0.540.0",
"p-queue": "^8.1.0", "p-queue": "^8.1.0",
"react": "^19.1.1", "react": "^19.1.1",
@ -43,6 +44,7 @@
"@eslint/js": "^9.33.0", "@eslint/js": "^9.33.0",
"@types/chrome": "^0.1.4", "@types/chrome": "^0.1.4",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/lodash": "^4.17.20",
"@types/node": "^24.3.0", "@types/node": "^24.3.0",
"@types/react": "^19.1.10", "@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7", "@types/react-dom": "^19.1.7",
@ -2575,6 +2577,13 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.3.0", "version": "24.3.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
@ -5089,6 +5098,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"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.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",

View File

@ -35,6 +35,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"fs-extra": "^11.3.1", "fs-extra": "^11.3.1",
"lodash": "^4.17.21",
"lucide-react": "^0.540.0", "lucide-react": "^0.540.0",
"p-queue": "^8.1.0", "p-queue": "^8.1.0",
"react": "^19.1.1", "react": "^19.1.1",
@ -51,6 +52,7 @@
"@eslint/js": "^9.33.0", "@eslint/js": "^9.33.0",
"@types/chrome": "^0.1.4", "@types/chrome": "^0.1.4",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/lodash": "^4.17.20",
"@types/node": "^24.3.0", "@types/node": "^24.3.0",
"@types/react": "^19.1.10", "@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7", "@types/react-dom": "^19.1.7",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -7,9 +7,9 @@
<title>Vite + React + TS</title> <title>Vite + React + TS</title>
<script type="module" crossorigin src="/assets/popup.js"></script> <script type="module" crossorigin src="/assets/popup.js"></script>
<link rel="modulepreload" crossorigin href="/assets/_commonjsHelpers-CqkleIqs.js"> <link rel="modulepreload" crossorigin href="/assets/_commonjsHelpers-CqkleIqs.js">
<link rel="modulepreload" crossorigin href="/assets/product-api.service-BCmn_jbQ.js"> <link rel="modulepreload" crossorigin href="/assets/product-api.service-Dn0sYl6J.js">
<link rel="modulepreload" crossorigin href="/assets/message.service-DcR3euAR.js"> <link rel="modulepreload" crossorigin href="/assets/message.service-DcR3euAR.js">
<link rel="stylesheet" crossorigin href="/assets/popup-DfztxwQY.css"> <link rel="stylesheet" crossorigin href="/assets/popup-DxX4E5UP.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

1
server/.env.example Normal file
View File

@ -0,0 +1 @@
BASE_URL = 'https://int.ipsupply.com.au/api/'

3
server/.gitignore vendored
View File

@ -25,3 +25,6 @@ auto-post-facebook-extensions
*.sln *.sln
*.sw? *.sw?
.env .env
product-cache*
meta.json
data.json

View File

@ -1 +1,20 @@
[] [
{
"title": "2-Port Analog Telephone Adapter - ATA191-K9",
"price": 220,
"el": {},
"date": "2025-08-21T08:34:00.451Z"
},
{
"title": "Cisco 2911 Voice Bundle, Pvdm3-16, Uc License Pak, Fl- Cube10 - CISCO2911-V/K9",
"price": 125,
"el": {},
"date": "2025-08-21T08:34:00.451Z"
},
{
"title": "Air-ct7510-k9 - AIR-CT7510-K9",
"price": 8562,
"el": {},
"date": "2025-08-21T08:34:00.451Z"
}
]

154
server/package-lock.json generated
View File

@ -9,8 +9,11 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"axios": "^1.11.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^5.1.0" "dotenv": "^17.2.1",
"express": "^5.1.0",
"lodash": "^4.17.21"
} }
}, },
"node_modules/accepts": { "node_modules/accepts": {
@ -26,6 +29,23 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
@ -84,6 +104,18 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
@ -153,6 +185,15 @@
} }
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -162,6 +203,18 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/dotenv": {
"version": "17.2.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz",
"integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -221,6 +274,21 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": { "node_modules/escape-html": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@ -295,6 +363,63 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -383,6 +508,21 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@ -453,6 +593,12 @@
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT" "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/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -592,6 +738,12 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/qs": { "node_modules/qs": {
"version": "6.14.0", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",

View File

@ -12,7 +12,10 @@
"license": "ISC", "license": "ISC",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"axios": "^1.11.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^5.1.0" "dotenv": "^17.2.1",
"express": "^5.1.0",
"lodash": "^4.17.21"
} }
} }

View File

@ -2,7 +2,17 @@ import express from "express";
import fs from "fs"; import fs from "fs";
import bodyParser from "body-parser"; import bodyParser from "body-parser";
import cors from "cors"; import cors from "cors";
import axios from "axios";
import path from "path";
import _ from "lodash";
import dotenv from "dotenv";
dotenv.config();
const metaPath = path.join(process.cwd(), "meta.json");
const dataFile = "data.json";
const cachePrefix = "product-cache-";
const app = express(); const app = express();
// Cho phép tất cả origin gọi API // Cho phép tất cả origin gọi API
@ -19,14 +29,41 @@ app.post("/sync", (req, res) => {
return res.status(400).json({ error: "Dữ liệu phải là array" }); return res.status(400).json({ error: "Dữ liệu phải là array" });
} }
// Thêm field date cho mỗi item // Chuẩn hoá dữ liệu (có thêm date)
const mapped = data.map((item) => ({ const mapped = data.map((item) => ({
...item, ...item,
date: new Date().toISOString(), date: new Date().toISOString(),
})); }));
// Ghi xuống file // Đọc dữ liệu cũ
fs.writeFileSync("data.json", JSON.stringify(mapped, null, 2)); let oldData = [];
try {
oldData = JSON.parse(fs.readFileSync(dataFile, "utf-8"));
} catch (e) {
oldData = [];
}
// So sánh (bỏ qua field date vì nó luôn khác)
const stripDate = (arr) => arr.map(({ date, ...rest }) => rest);
const oldStripped = stripDate(oldData);
const newStripped = stripDate(mapped);
if (!_.isEqual(oldStripped, newStripped)) {
// Nếu khác → xoá cache
const files = fs.readdirSync(".");
files.forEach((file) => {
if (file.startsWith(cachePrefix)) {
fs.unlinkSync(path.join(".", file));
}
});
console.log("Dữ liệu thay đổi → xoá cache");
} else {
console.log("Dữ liệu không đổi → giữ cache");
}
// Ghi dữ liệu mới
fs.writeFileSync(dataFile, JSON.stringify(mapped, null, 2));
res.json({ message: "Đã lưu dữ liệu thành công", saved: mapped.length }); res.json({ message: "Đã lưu dữ liệu thành công", saved: mapped.length });
}); });
@ -40,6 +77,91 @@ app.get("/data", (req, res) => {
res.json(JSON.parse(content)); res.json(JSON.parse(content));
}); });
app.post("/", async (req, res) => {
try {
// ===== Helpers =====
const readJSON = (path, fallback = null) => {
try {
return JSON.parse(fs.readFileSync(path, "utf-8"));
} catch {
return fallback;
}
};
const writeJSON = (path, data) => {
try {
fs.writeFileSync(path, JSON.stringify(data, null, 2));
} catch (e) {
console.warn("Không ghi được file:", e.message);
}
};
// ===== Meta =====
let meta = readJSON(metaPath, { total: 4000 });
const filter = req.body?.filter || {};
const originalFilter = { ...filter };
const status = filter?.where?.status_listing;
if (["listed", "unlisted"].includes(status)) {
filter.skip = 0;
}
// ===== Cache check =====
const cacheFile = `${cachePrefix}${JSON.stringify(originalFilter)}.json`;
if (fs.existsSync(cacheFile)) {
return res.json(readJSON(cacheFile, {}));
}
// ===== Load publisted data nếu cần =====
let publistedData = [];
if (status && meta.total) {
filter.limit = meta.total;
publistedData = readJSON("data.json", []);
}
// ===== Call API gốc =====
const { data } = await axios({
headers: { Authorization: req.headers.authorization },
url: "transferGetData",
baseURL: process.env.BASE_URL,
method: "POST",
data: { ...req.body, filter },
});
// Update meta
if (typeof data.total === "number") {
writeJSON(metaPath, { total: data.total });
}
// ===== Xử lý listed/unlisted =====
if (status === "listed" || status === "unlisted") {
const skip = originalFilter.skip || 0;
const limit = originalFilter.limit || data.data.length;
const listedCodes = new Set(
publistedData.map((i) => (i.title + i.price || "").trim().toLowerCase())
);
let filteredData = data.data.filter((item) => {
const key = `${item.name} - ${item.code}${item.price}`.toLowerCase();
return status === "listed"
? listedCodes.has(key)
: !listedCodes.has(key);
});
data.total = filteredData.length;
data.data = filteredData.slice(skip, skip + limit);
data.filter = { ...originalFilter, skip, limit };
writeJSON(cacheFile, data);
}
res.json(data);
} catch (err) {
console.error("API error:", err.message);
res.status(500).json({ error: err.message });
}
});
app.listen(3000, () => { app.listen(3000, () => {
console.log("Server chạy tại http://localhost:3000"); console.log("Server chạy tại http://localhost:3000");
}); });

View File

@ -21,8 +21,6 @@ class ProductApiService {
}; };
return axios({ return axios({
url: "transferGetData",
method: "POST", method: "POST",
data: { data: {
urlAPI: "/api/ebay-listing/listing-get-list", urlAPI: "/api/ebay-listing/listing-get-list",
@ -40,8 +38,6 @@ class ProductApiService {
async get(data: IPost) { async get(data: IPost) {
return axios({ return axios({
url: "transferGetData",
method: "POST", method: "POST",
data: { data: {
urlAPI: "/api/product-model/get-info/" + data.id, urlAPI: "/api/product-model/get-info/" + data.id,
@ -52,7 +48,6 @@ class ProductApiService {
async sync(data: ISyncItem[]) { async sync(data: ISyncItem[]) {
return axios({ return axios({
baseURL: import.meta.env.VITE_API_SYNC_URL,
url: "sync", url: "sync",
method: "POST", method: "POST",
data, data,
@ -61,7 +56,6 @@ class ProductApiService {
async getPublistedProducts() { async getPublistedProducts() {
return axios({ return axios({
baseURL: import.meta.env.VITE_API_SYNC_URL,
url: "data", url: "data",
}); });
} }

View File

@ -30,7 +30,7 @@ export function ConfirmAlert({
return ( return (
<AlertDialog open={open} onOpenChange={setOpen}> <AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger> <AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent aria-describedby={undefined}>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle> <AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription> <AlertDialogDescription>{description}</AlertDialogDescription>

View File

@ -20,7 +20,15 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Select, SelectTrigger, SelectValue } from "@/components/ui/select"; import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { import {
Table, Table,
TableBody, TableBody,
@ -50,17 +58,16 @@ export function PostTable() {
const msg = new MessageService("popup"); const msg = new MessageService("popup");
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [categoryFilter, setCategoryFilter] = useState("all"); const [filter, setFilter] = useState<Record<string, any>>({});
const [conditionFilter, setConditionFilter] = useState("all"); const [statusFilter, setStatusFilter] = useState("all");
const [brandFilter, setBrandFilter] = useState("all");
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false); const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
const [search] = useDebounce(searchTerm, 400); const [search] = useDebounce(searchTerm, 400);
const queryKey = useMemo( const queryKey = useMemo(
() => ["products", { currentPage, search }], () => ["products", { currentPage, search, filter }],
[currentPage, search] [currentPage, search, filter]
); );
// --- React Query fetch --- // --- React Query fetch ---
@ -72,6 +79,8 @@ export function PostTable() {
skip: (currentPage - 1) * productApi.item_per_page, skip: (currentPage - 1) * productApi.item_per_page,
where: { where: {
productModelCode: searchTerm, productModelCode: searchTerm,
status_listing:
filter?.statusFilter === "all" ? undefined : filter.statusFilter,
}, },
}) })
); );
@ -102,18 +111,15 @@ export function PostTable() {
const clearFilters = () => { const clearFilters = () => {
setSearchTerm(""); setSearchTerm("");
setCategoryFilter("all"); setStatusFilter("all");
setConditionFilter("all"); setFilter({});
setBrandFilter("all");
setCurrentPage(1); setCurrentPage(1);
setIsFilterModalOpen(false);
}; };
const activeFiltersCount = [ const activeFiltersCount = [statusFilter !== "all", searchTerm !== ""].filter(
categoryFilter !== "all", Boolean
conditionFilter !== "all", ).length;
brandFilter !== "all",
searchTerm !== "",
].filter(Boolean).length;
const totalPages = useMemo(() => { const totalPages = useMemo(() => {
if (!rawProducts?.total) return 0; if (!rawProducts?.total) return 0;
@ -149,9 +155,13 @@ export function PostTable() {
data: data, data: data,
}); });
} else { } else {
console.log({
a: { ...data, ...mapToIPost({ ...res.data?.data }) },
b: data,
});
msg.send("content", "popup-to-content", { msg.send("content", "popup-to-content", {
type: "publist", type: "publist",
data: mapToIPost({ ...data, ...res.data?.data }), data: { ...data, images: mapToIPost({ ...res.data?.data }).images },
}); });
} }
}; };
@ -199,27 +209,26 @@ export function PostTable() {
)} )}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-md"> <DialogContent aria-describedby={undefined} className="sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle>Product Filters</DialogTitle> <DialogTitle>Product Filters</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<Select <Select
value={conditionFilter} value={statusFilter === "all" ? "" : statusFilter}
onValueChange={setConditionFilter} onValueChange={setStatusFilter}
> >
<SelectTrigger> <SelectTrigger className="w-[180px]">
<SelectValue placeholder="Condition" /> <SelectValue placeholder="Select a status" />
</SelectTrigger> </SelectTrigger>
<SelectContent aria-describedby={undefined}>
<SelectGroup>
<SelectItem value="listed">Listed</SelectItem>
<SelectItem value="unlisted">Unlisted</SelectItem>
</SelectGroup>
</SelectContent>
</Select> </Select>
<Select value={brandFilter} onValueChange={setBrandFilter}>
<SelectTrigger>
<SelectValue placeholder="Brand" />
</SelectTrigger>
</Select>
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
variant="outline" variant="outline"
@ -229,7 +238,10 @@ export function PostTable() {
Clear Filters Clear Filters
</Button> </Button>
<Button <Button
onClick={() => setIsFilterModalOpen(false)} onClick={() => {
setIsFilterModalOpen(false);
setFilter({ statusFilter });
}}
className="flex-1" className="flex-1"
> >
Apply Apply

View File

@ -110,9 +110,9 @@ export default function ProductModal({
const watchedTags = form.watch("tags"); const watchedTags = form.watch("tags");
const conditions = ["New", "Used - like new", "Used - good", "Used - fair"]; const conditions = ["New", "Used - like new", "Used - good", "Used - fair"];
const categories = ["Tools"]; const categories = ["Tools", "Electronics & computers"];
const { isLoading, refetch, ...query } = usePost(data); const { ...query } = usePost(data);
// const delImageMutation = useMutation({ // const delImageMutation = useMutation({
// mutationFn: async (imageUrl: string) => { // mutationFn: async (imageUrl: string) => {
@ -307,7 +307,10 @@ export default function ProductModal({
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{children}</DialogTrigger> <DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="md:max-w-4xl h-[95vh] flex flex-col p-0"> <DialogContent
aria-describedby={undefined}
className="md:max-w-4xl h-[95vh] flex flex-col p-0"
>
{/* Header */} {/* Header */}
<DialogHeader className="p-6 border-b"> <DialogHeader className="p-6 border-b">
<DialogTitle className="text-2xl font-bold"> <DialogTitle className="text-2xl font-bold">
@ -566,7 +569,7 @@ export default function ProductModal({
</FormItem> </FormItem>
)} )}
/> />
<FormField {/* <FormField
control={form.control} control={form.control}
name="publist" name="publist"
render={({ field }) => { render={({ field }) => {
@ -600,7 +603,7 @@ export default function ProductModal({
</FormItem> </FormItem>
); );
}} }}
/> /> */}
</CardContent> </CardContent>
</Card> </Card>
</form> </form>
@ -610,9 +613,9 @@ export default function ProductModal({
{/* Footer */} {/* Footer */}
<div className="p-6 border-t bg-white flex justify-end gap-3"> <div className="p-6 border-t bg-white flex justify-end gap-3">
<Button type="button" variant="outline" onClick={handleClose}> <Button type="button" variant="outline" onClick={handleClose}>
Cancel Close
</Button> </Button>
<Button {/* <Button
type="submit" type="submit"
onClick={form.handleSubmit(onSubmit)} onClick={form.handleSubmit(onSubmit)}
disabled={form.formState.isSubmitting} disabled={form.formState.isSubmitting}
@ -624,7 +627,7 @@ export default function ProductModal({
) : ( ) : (
"Create" "Create"
)} )}
</Button> </Button> */}
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -15,7 +15,7 @@ msgService.on<{ data: IPost }>(
try { try {
console.log("[publish] Content nhận:", payload); console.log("[publish] Content nhận:", payload);
await delayRD(1000, 2000); await delayRD(800, 1000);
await facebookService.handlePublist(payload.data); await facebookService.handlePublist(payload.data);
} catch (error) { } catch (error) {
@ -36,19 +36,19 @@ msgService.on<{ data: IPost }>(
try { try {
console.log("[unlist] Content nhận:", payload); console.log("[unlist] Content nhận:", payload);
await delayRD(1000, 2000); await delayRD(800, 1000);
await facebookService.gotoSell(); await facebookService.gotoSell();
await delayRD(1000, 2000); await delayRD(800, 1000);
await facebookService.handleDelete(payload.data); await facebookService.handleDelete(payload.data);
await delayRD(1000, 2000); await delayRD(800, 1000);
const products = await facebookService.getProducts(); const products = await facebookService.getProducts();
await delayRD(1000, 2000); await delayRD(800, 1000);
productApi.sync(products); productApi.sync(products);
} catch (error) { } catch (error) {
@ -69,11 +69,11 @@ msgService.on<{ data: IPost }>(
try { try {
console.log("[re-publist] Content nhận:", payload); console.log("[re-publist] Content nhận:", payload);
await delayRD(1000, 2000); await delayRD(800, 1000);
await facebookService.gotoSell(); await facebookService.gotoSell();
await delayRD(1000, 2000); await delayRD(800, 1000);
await facebookService.handleRePublist(payload.data); await facebookService.handleRePublist(payload.data);
} catch (error) { } catch (error) {
@ -88,7 +88,7 @@ msgService.on<{ data: IPost }>(
); );
(async () => { (async () => {
await delayRD(1000, 2000); await delayRD(800, 1000);
// Sử dụng // Sử dụng
thiefService thiefService
@ -101,7 +101,7 @@ msgService.on<{ data: IPost }>(
'[aria-label="Collection of your Marketplace items"]' '[aria-label="Collection of your Marketplace items"]'
); );
await delayRD(1000, 2000); await delayRD(800, 1000);
const products = await facebookService.getProducts(); const products = await facebookService.getProducts();
console.log({ products }); console.log({ products });

View File

@ -3,7 +3,6 @@ import ax from "axios";
const axios = ax.create({ const axios = ax.create({
// Dev // Dev
baseURL: import.meta.env.VITE_API_URL, baseURL: import.meta.env.VITE_API_URL,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + import.meta.env.VITE_API_TOKEN, Authorization: "Bearer " + import.meta.env.VITE_API_TOKEN,

View File

@ -35,9 +35,7 @@ export function mapToIPost(raw: any): IPost {
.map((img: any) => .map((img: any) =>
typeof img === "string" typeof img === "string"
? img ? img
: `${(import.meta.env.VITE_API_URL as string).replaceAll("api", "")}${ : `${import.meta.env.VITE_BASE_IMG_URL as string}${img.url}`
img.url
}`
) )
.slice(0, 10); // chỉ lấy tối đa 10 ảnh .slice(0, 10); // chỉ lấy tối đa 10 ảnh
} }