bid-extension/content.js

675 lines
17 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,
};
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.url || 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 = Object.entries(newKeys).map(([key, value]) => {
return {
key_name: key,
value,
};
});
console.log({ newKeys, payload, metadata });
try {
const response = await fetch(`${CONFIG.API_BASE_URL}/bids`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: key,
},
body: JSON.stringify(removeFalsyValues({ ...payload, metadata })),
});
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} ${
Number(input.value)
? `(${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);
});
});
}
});
};
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="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;
[formElements.id, formElements.name].forEach((item) => {
console.log({ item });
item.value = data[item.id] || "None";
item.parentElement.style.display = "block";
});
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);
}
};
const renderLableMetadataMode = (formElements) => {
Object.values(formElements.metadataMode).forEach((item) => {
setMinusLabel(item);
item.addEventListener("input", (e) => {
setMinusLabel(item, Number(e.target.value));
});
});
};
(async () => {
await showPage();
handleToogle();
const formElements = {
url: document.querySelector("#form-bid #url"),
id: document.querySelector("#form-bid #id"),
name: document.querySelector("#form-bid #name"),
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);
renderLableMetadataMode(formElements);
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)
);
})();