630 lines
16 KiB
JavaScript
630 lines
16 KiB
JavaScript
const CONFIG = {
|
|
// API_BASE_URL: "http://localhost:4000/api/v1",
|
|
API_BASE_URL: "https://bids.apactech.io/api/v1",
|
|
};
|
|
|
|
let PREV_DATA = null;
|
|
|
|
function removeFalsyValues(obj, excludeKeys = []) {
|
|
return Object.entries(obj).reduce((acc, [key, value]) => {
|
|
if (value || excludeKeys.includes(key)) {
|
|
acc[key] = value;
|
|
}
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
function extractDomain(url) {
|
|
try {
|
|
const parsedUrl = new URL(url);
|
|
return parsedUrl.origin;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const webs = {
|
|
grays: "https://www.grays.com",
|
|
langtons: "https://www.langtons.com.au",
|
|
lawsons: "https://www.lawsons.com.au",
|
|
pickles: "https://www.pickles.com.au",
|
|
allbids: "https://www.allbids.com.au",
|
|
};
|
|
|
|
function extractModelId(url) {
|
|
switch (extractDomain(url)) {
|
|
case webs.grays: {
|
|
const match = url.match(/\/lot\/([\d-]+)\//);
|
|
return match ? match[1] : null;
|
|
}
|
|
case webs.langtons: {
|
|
const match = url.match(/auc-var-\d+/);
|
|
return match[0];
|
|
}
|
|
case webs.lawsons: {
|
|
const match = url.split("_");
|
|
return match ? match[1] : null;
|
|
}
|
|
case webs.pickles: {
|
|
const model = url.split("/").pop();
|
|
return model ? model : null;
|
|
}
|
|
case webs.allbids: {
|
|
const match = url.match(/-(\d+)(?:[\?#]|$)/);
|
|
return match ? match[1] : null;
|
|
}
|
|
}
|
|
}
|
|
|
|
const isValidUrl = (url) => {
|
|
return !!Object.keys(webs).find((item) => url.includes(webs[item]));
|
|
};
|
|
|
|
const showPage = async (pageLink = "pages/popup/popup.html") => {
|
|
const url = window.location.href; // sửa lỗi ở đây
|
|
|
|
if (!isValidUrl(url)) return;
|
|
try {
|
|
const res = await fetch(chrome.runtime.getURL(pageLink));
|
|
const html = await res.text();
|
|
|
|
const wrapper = document.createElement("div");
|
|
wrapper.innerHTML = html;
|
|
document.body.appendChild(wrapper);
|
|
} catch (err) {
|
|
console.error("Failed to load popup page:", err);
|
|
}
|
|
};
|
|
|
|
const getKey = () => {
|
|
return new Promise((resolve, reject) => {
|
|
chrome.storage.local.get("key", (result) => {
|
|
if (chrome.runtime.lastError) {
|
|
reject(chrome.runtime.lastError);
|
|
} else {
|
|
resolve(result.key || null);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
async function handleCreate(event, formElements) {
|
|
event.preventDefault();
|
|
|
|
const key = await getKey();
|
|
if (!key) {
|
|
showKey();
|
|
return;
|
|
}
|
|
|
|
const maxPrice = parseFloat(formElements.maxPrice.value);
|
|
const plusPrice = parseFloat(formElements.plusPrice.value);
|
|
const quantity = parseInt(formElements.quantity.value, 10);
|
|
|
|
const payload = {
|
|
url: formElements.url.value.trim(),
|
|
max_price: isNaN(maxPrice) ? null : maxPrice,
|
|
plus_price: isNaN(plusPrice) ? null : plusPrice,
|
|
quantity: isNaN(quantity) ? null : quantity,
|
|
};
|
|
|
|
// Validate required fields
|
|
if (!payload.url || payload.max_price === null) {
|
|
alert("Please fill out the URL and Max Price fields correctly.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${CONFIG.API_BASE_URL}/bids`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: key,
|
|
},
|
|
body: JSON.stringify(removeFalsyValues(payload)),
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
alert(result.message);
|
|
|
|
// showInfo
|
|
await showInfo(extractModelId(payload.url), formElements);
|
|
// handleChangeTitleButton
|
|
handleChangeTitleButton(true, formElements);
|
|
} catch (error) {
|
|
alert("Error: " + error.message);
|
|
console.error("API Error:", error);
|
|
}
|
|
}
|
|
|
|
async function handleUpdate(event, formElements, id) {
|
|
event.preventDefault();
|
|
|
|
const key = await getKey();
|
|
if (!key) {
|
|
showKey();
|
|
return;
|
|
}
|
|
|
|
const maxPrice = parseFloat(formElements.maxPrice.value);
|
|
const plusPrice = parseFloat(formElements.plusPrice.value);
|
|
const quantity = parseInt(formElements.quantity.value, 10);
|
|
|
|
const payload = {
|
|
max_price: isNaN(maxPrice) ? null : maxPrice,
|
|
plus_price: isNaN(plusPrice) ? null : plusPrice,
|
|
quantity: isNaN(quantity) ? null : quantity,
|
|
};
|
|
|
|
const keys = Object.values(formElements.form)
|
|
.filter((item) => {
|
|
return [
|
|
"mode_key",
|
|
"early_tracking_seconds",
|
|
"arrival_offset_seconds",
|
|
].includes(item?.id);
|
|
})
|
|
.reduce((prev, cur) => {
|
|
prev[cur.id] = cur.value;
|
|
return prev;
|
|
}, {});
|
|
|
|
const earlyTracking = parseInt(keys.early_tracking_seconds, 10);
|
|
const arrivalOffset = parseInt(keys.arrival_offset_seconds, 10);
|
|
|
|
if (earlyTracking < 600) {
|
|
alert("Early Tracking Seconds must be at least 600 seconds (10 minutes).");
|
|
return;
|
|
}
|
|
|
|
if (arrivalOffset < 60) {
|
|
alert("Arrival Offset Seconds must be at least 60 seconds (1 minute).");
|
|
return;
|
|
}
|
|
|
|
// Validate required fields
|
|
if (payload.max_price === null) {
|
|
alert("Please fill out the URL and Max Price fields correctly.");
|
|
return;
|
|
}
|
|
|
|
const localKeyName = keys["mode_key"];
|
|
|
|
const newKeys = Object.entries(keys).reduce((prev, [key, value]) => {
|
|
if (key === "mode_key") {
|
|
prev[key] = value;
|
|
} else {
|
|
prev[`${key}_${localKeyName}`] = Number(value);
|
|
}
|
|
return prev;
|
|
}, {});
|
|
|
|
let metadata = [];
|
|
|
|
if (
|
|
window.__result.metadata.length > 0 &&
|
|
window.__result.metadata.some((item) => item.key_name === "mode_key")
|
|
) {
|
|
metadata = window.__result.metadata.map((item) => {
|
|
if (Object.keys(newKeys).includes(item.key_name)) {
|
|
return {
|
|
...item,
|
|
value: newKeys[item.key_name],
|
|
};
|
|
}
|
|
return { ...item };
|
|
});
|
|
} else {
|
|
metadata = Object.entries(newKeys).map(([key, value]) => {
|
|
return {
|
|
key_name: key,
|
|
value,
|
|
};
|
|
});
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${CONFIG.API_BASE_URL}/bids/info/${id}`, {
|
|
method: "PUT",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: key,
|
|
},
|
|
body: JSON.stringify(removeFalsyValues({ ...payload, metadata })),
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
alert(result.message);
|
|
} catch (error) {
|
|
alert("Error: " + error.message);
|
|
console.error("API Error:", error);
|
|
} finally {
|
|
await showInfo();
|
|
}
|
|
}
|
|
|
|
const showBid = () => {
|
|
const formKey = document.getElementById("form-key");
|
|
const formBid = document.getElementById("form-bid");
|
|
|
|
formKey.style.display = "none";
|
|
formBid.style.display = "block";
|
|
};
|
|
|
|
const showKey = async () => {
|
|
chrome.storage.local.set({ key: null }, async () => {
|
|
console.log("abc");
|
|
});
|
|
|
|
const key = await getKey();
|
|
|
|
const formKey = document.getElementById("form-key");
|
|
const formBid = document.getElementById("form-bid");
|
|
|
|
const keyEl = document.querySelector("#form-key #key");
|
|
|
|
formBid.style.display = "none";
|
|
formKey.style.display = "block";
|
|
|
|
if (key && keyEl) {
|
|
keyEl.value = key;
|
|
}
|
|
};
|
|
|
|
const handleToogle = async () => {
|
|
const btn = document.getElementById("toggle-bid-extension");
|
|
const panel = document.getElementById("bid-extension");
|
|
|
|
if (btn && panel) {
|
|
btn.addEventListener("click", async () => {
|
|
const isHidden = panel.style.display === "none";
|
|
panel.style.display = isHidden ? "block" : "none";
|
|
|
|
if (isHidden) {
|
|
await handleShowForm();
|
|
}
|
|
});
|
|
} else {
|
|
console.error("Không tìm thấy nút hoặc panel!");
|
|
}
|
|
};
|
|
|
|
const handleShowForm = async () => {
|
|
const formBid = document.getElementById("form-bid");
|
|
const formKey = document.getElementById("form-key");
|
|
const keyBtn = document.getElementById("key-btn");
|
|
|
|
const currentKey = await getKey();
|
|
|
|
if (!currentKey) {
|
|
await showKey();
|
|
} else {
|
|
showBid();
|
|
}
|
|
|
|
keyBtn?.addEventListener("click", () => {
|
|
showKey();
|
|
});
|
|
};
|
|
const handleChangeTitleButton = (result, formElements) => {
|
|
if (result) {
|
|
formElements.createBtn.textContent = "Update";
|
|
} else {
|
|
formElements.createBtn.textContent = "Create";
|
|
}
|
|
};
|
|
|
|
const handleSaveKey = () => {
|
|
const form = document.querySelector("#form-key form");
|
|
if (!form) return;
|
|
|
|
form.addEventListener("submit", async (e) => {
|
|
e.preventDefault();
|
|
|
|
const inputKey = form.querySelector("#key");
|
|
if (!inputKey) return;
|
|
|
|
const keyValue = inputKey.value.trim();
|
|
if (!keyValue) {
|
|
alert("Please enter a key");
|
|
return;
|
|
}
|
|
|
|
// Lưu vào chrome.storage.local
|
|
chrome.storage.local.set({ key: keyValue }, async () => {
|
|
alert("Key saved successfully!");
|
|
showBid();
|
|
|
|
if (!isValidModel()) return;
|
|
|
|
await showInfo();
|
|
});
|
|
});
|
|
};
|
|
|
|
const isValidModel = () => {
|
|
const currentUrl = window.location.href;
|
|
|
|
const model = extractModelId(currentUrl);
|
|
|
|
return !!model;
|
|
};
|
|
|
|
const renderValueModeMetadata = (data, formElements, mode = null) => {
|
|
const early_tracking_seconds = getEarlyTrackingSeconds(data, mode);
|
|
const arrival_offset_seconds = getArrivalOffsetSeconds(data, mode);
|
|
|
|
formElements.metadataMode.arrival_offset_seconds.value =
|
|
arrival_offset_seconds;
|
|
|
|
formElements.metadataMode.early_tracking_seconds.value =
|
|
early_tracking_seconds;
|
|
|
|
return { early_tracking_seconds, arrival_offset_seconds };
|
|
};
|
|
|
|
function formatTimeFromMinutes(minutes) {
|
|
// Tính ngày, giờ, phút từ số phút
|
|
const days = Math.floor(minutes / (60 * 24));
|
|
const hours = Math.floor((minutes % (60 * 24)) / 60);
|
|
const mins = minutes % 60;
|
|
|
|
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`;
|
|
|
|
return result.trim();
|
|
}
|
|
|
|
function toReadableLabel(input) {
|
|
return input
|
|
.split("_") // Tách chuỗi theo dấu gạch dưới
|
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1)) // Viết hoa chữ cái đầu
|
|
.join(" "); // Nối lại bằng khoảng trắng
|
|
}
|
|
|
|
const setMinusLabel = (input, minutes = null) => {
|
|
const label = document.querySelector(`[for="${input?.id}"]`);
|
|
|
|
const text = toReadableLabel(input.id);
|
|
|
|
label.textContent = `${text} (${formatTimeFromMinutes(
|
|
minutes || Number(input.value) || 300 / 60
|
|
)})`;
|
|
};
|
|
|
|
const renderMetadaMode = (data, formElements, mode = null) => {
|
|
const mode_key = mode ? mode : getMode(data);
|
|
|
|
formElements.metadataModeBox.style.display = "flex";
|
|
|
|
renderValueModeMetadata(data, formElements, mode_key);
|
|
|
|
Object.values(formElements.form).forEach((item) => {
|
|
if (item?.id === "mode_key") {
|
|
item.value = mode_key;
|
|
|
|
item.addEventListener("change", (e) => {
|
|
renderValueModeMetadata(data, formElements, e.target.value);
|
|
|
|
Object.values(formElements.metadataMode).forEach((i) => {
|
|
setMinusLabel(i);
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
Object.values(formElements.metadataMode).forEach((item) => {
|
|
setMinusLabel(item);
|
|
|
|
item.addEventListener("input", (e) => {
|
|
setMinusLabel(item, Number(e.target.value));
|
|
});
|
|
});
|
|
};
|
|
|
|
const createInfoColumn = (data, formElements) => {
|
|
const inputsContainer = document.querySelector("#bid-extension .inputs");
|
|
const urlCol = document.querySelector("#url-col");
|
|
|
|
if (!inputsContainer || !urlCol) return;
|
|
|
|
// 1. Thêm ID và Name vào đầu inputsContainer
|
|
const otherEls = `
|
|
<div class="sub-col">
|
|
<div class="col">
|
|
<label>ID</label>
|
|
<input readonly value="${data?.id || "None"}" type="text" id="id" />
|
|
</div>
|
|
|
|
<div class="col">
|
|
<label for="mode_key">Mode</label>
|
|
<select id="mode_key" name="mode_key">
|
|
<option value="live">Live</option>
|
|
<option value="sandbox">Sandbox</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col">
|
|
<label>Name</label>
|
|
<textarea readonly id="maxPrice">${data?.name || "None"}</textarea>
|
|
</div>
|
|
`;
|
|
|
|
inputsContainer.insertAdjacentHTML("afterbegin", otherEls);
|
|
|
|
// 2. Tạo và chèn Current Price ngay sau #url-col
|
|
const currentPriceDiv = document.createElement("div");
|
|
currentPriceDiv.className = "col";
|
|
currentPriceDiv.innerHTML = `
|
|
<label>Current price</label>
|
|
<input readonly type="text" value="${
|
|
data?.current_price || "None"
|
|
}" id="currentPrice" />
|
|
`;
|
|
|
|
urlCol.parentNode.insertBefore(currentPriceDiv, urlCol.nextSibling);
|
|
|
|
formElements.quantity.value = data?.quantity || 1;
|
|
formElements.plusPrice.value = data?.plus_price || 0;
|
|
|
|
renderMetadaMode(data, formElements);
|
|
};
|
|
|
|
const showCompetitor = () => {
|
|
const script = document.createElement("script");
|
|
script.src = chrome.runtime.getURL("injected.js");
|
|
script.onload = function () {
|
|
this.remove();
|
|
};
|
|
(document.head || document.documentElement).appendChild(script);
|
|
|
|
// Nghe dữ liệu trả về
|
|
window.addEventListener("message", function (event) {
|
|
if (event.source !== window) return;
|
|
if (event.data && event.data.source === "my-extension") {
|
|
const bidHistory = event.data?.bidHistory || [];
|
|
const maxProxy = bidHistory.reduce((max, curr) => {
|
|
return curr.proxyamount > max.proxyamount ? curr : max;
|
|
}, bidHistory[0]);
|
|
|
|
console.log({ bidHistory });
|
|
|
|
const competitorEl = document.getElementById("competitor-max-bid-col");
|
|
const competitorInput = document.querySelector(
|
|
"#competitor-max-bid-col input"
|
|
);
|
|
|
|
competitorEl.style.display = "block";
|
|
|
|
competitorInput.value = maxProxy?.proxyamount || "None";
|
|
}
|
|
});
|
|
};
|
|
|
|
const getMode = (data) => {
|
|
return (
|
|
data.metadata.find((item) => item.key_name === "mode_key")?.value || "live"
|
|
);
|
|
};
|
|
|
|
const getEarlyTrackingSeconds = (data, outsiteMode = null) => {
|
|
const mode = outsiteMode ? outsiteMode : getMode(data);
|
|
|
|
return (
|
|
data.metadata.find(
|
|
(item) => item.key_name === `early_tracking_seconds_${mode}`
|
|
)?.value || data.web_bid.early_tracking_seconds
|
|
);
|
|
};
|
|
|
|
const getArrivalOffsetSeconds = (data, outsiteMode = null) => {
|
|
const mode = outsiteMode ? outsiteMode : getMode(data);
|
|
|
|
return (
|
|
data.metadata.find(
|
|
(item) => item.key_name === `arrival_offset_seconds_${mode}`
|
|
)?.value || data.web_bid.arrival_offset_seconds
|
|
);
|
|
};
|
|
|
|
const showInfo = async (model, formElements) => {
|
|
const key = await getKey();
|
|
if (!key) {
|
|
showKey();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${CONFIG.API_BASE_URL}/bids/${model}`, {
|
|
method: "GET",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: key,
|
|
},
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (!result || result?.status_code !== 200 || !result?.data) {
|
|
if (result.status_code !== 404) {
|
|
alert(result.message);
|
|
}
|
|
|
|
PREV_DATA = null;
|
|
return null;
|
|
}
|
|
|
|
window["__result"] = result.data;
|
|
|
|
formElements.maxPrice.value = result.data.max_price;
|
|
|
|
createInfoColumn(result.data, formElements);
|
|
|
|
PREV_DATA = result;
|
|
return result;
|
|
} catch (error) {
|
|
alert("Error: " + error.message);
|
|
console.error("API Error:", error);
|
|
}
|
|
};
|
|
|
|
(async () => {
|
|
await showPage();
|
|
handleToogle();
|
|
const formElements = {
|
|
url: document.querySelector("#form-bid #url"),
|
|
maxPrice: document.querySelector("#form-bid #maxPrice"),
|
|
plusPrice: document.querySelector("#form-bid #plusPrice"),
|
|
quantity: document.querySelector("#form-bid #quantity"),
|
|
createBtn: document.querySelector("#form-bid #createBtn"),
|
|
modeKey: document.querySelector("#form-bid #mode_key"),
|
|
metadataModeBox: document.querySelector("#form-bid #metadataMode"),
|
|
metadataMode: {
|
|
arrival_offset_seconds: document.querySelector(
|
|
"#form-bid #arrival_offset_seconds"
|
|
),
|
|
early_tracking_seconds: document.querySelector(
|
|
"#form-bid #early_tracking_seconds"
|
|
),
|
|
},
|
|
form: document.querySelector("#form-bid form"),
|
|
};
|
|
|
|
const style = document.createElement("link");
|
|
style.rel = "stylesheet";
|
|
style.href = chrome.runtime.getURL("assets/css/index.css");
|
|
document.head.appendChild(style);
|
|
|
|
handleSaveKey();
|
|
|
|
const currentUrl = window.location.href;
|
|
|
|
const model = extractModelId(currentUrl);
|
|
|
|
if (!model) return;
|
|
|
|
switch (extractDomain(currentUrl)) {
|
|
case webs.allbids: {
|
|
showCompetitor();
|
|
}
|
|
}
|
|
|
|
// set url on form
|
|
formElements.url.value = currentUrl;
|
|
|
|
await showInfo(model, formElements);
|
|
handleChangeTitleButton(!!PREV_DATA, formElements);
|
|
|
|
formElements.form.addEventListener("submit", (e) =>
|
|
PREV_DATA
|
|
? handleUpdate(e, formElements, PREV_DATA.data.id)
|
|
: handleCreate(e, formElements)
|
|
);
|
|
})();
|