first commit

This commit is contained in:
Admin 2025-05-16 15:02:12 +07:00
commit ccdabd9350
9 changed files with 738 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

134
assets/css/index.css Normal file
View File

@ -0,0 +1,134 @@
/* #bid-extension body {
margin: 0;
padding: 0;
background-color: #121212;
color: #e0e0e0;
font-family: 'Segoe UI', Tahoma, sans-serif;
width: 320px;
} */
#bid-extension {
margin: 0;
padding: 0;
background-color: #121212;
color: #e0e0e0;
font-family: "Segoe UI", Tahoma, sans-serif;
width: 320px;
}
#bid-extension .container {
display: flex;
flex-direction: column;
gap: 10px;
}
#bid-extension h2 {
text-align: center;
margin-bottom: 12px;
font-size: 22px;
color: #ffffff;
}
#bid-extension label {
font-size: 13px;
margin-bottom: 2px;
}
#bid-extension input,
#bid-extension textarea {
padding: 8px;
background-color: #1e1e1e;
color: #ffffff;
border: 1px solid #333;
border-radius: 6px;
font-size: 14px;
width: 100%;
box-sizing: border-box;
}
#bid-extension input:focus,
#bid-extension textarea:focus {
border-color: #4a90e2;
outline: none;
}
#bid-extension .row {
display: flex;
gap: 10px;
}
#bid-extension .col {
flex: 1;
display: flex;
flex-direction: column;
}
#bid-extension .inputs .col {
padding-left: 0px;
padding-right: 0px;
}
#bid-extension button {
margin-top: 10px;
padding: 10px;
background: linear-gradient(to right, #4a90e2, #357abd);
color: white;
border: none;
border-radius: 6px;
font-size: 15px;
cursor: pointer;
transition: background 0.3s ease;
}
#bid-extension button:hover {
background: linear-gradient(to right, #3a78c2, #2d5faa);
}
#bid-extension #errorMessage {
margin-top: 2px;
font-size: 11px;
}
#bid-extension .wrapper {
position: relative;
}
#bid-extension .key-container {
position: absolute;
top: 20px;
left: 20px;
}
#bid-extension .key-container a {
color: #ffffff;
font-size: 14px;
padding: 4px 10px;
background: linear-gradient(to right, #4a90e2, #357abd);
border-radius: 6px;
text-decoration: none;
display: inline-block;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
}
#bid-extension .key-container a:hover {
background: linear-gradient(to right, #3a78c2, #2d5faa);
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.3);
transform: translateY(-2px);
}
#bid-extension .inputs {
display: flex;
flex-direction: column;
gap: 8px;
}
#bid-extension svg {
width: 14px;
height: 14px;
}
#toggle-bid-extension svg {
width: 20px;
height: 20px;
}

BIN
assets/icons/128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
assets/icons/16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

BIN
assets/icons/32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

4
background.js Normal file
View File

@ -0,0 +1,4 @@
chrome.action.onClicked.addListener((tab) => {
// Lấy URL của tab hiện tại
console.log("Current URL:", tab.url);
});

389
content.js Normal file
View File

@ -0,0 +1,389 @@
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;
}
}
function extractModelId(url) {
switch (extractDomain(url)) {
case "https://www.grays.com": {
const match = url.match(/\/lot\/([\d-]+)\//);
return match ? match[1] : null;
}
case "https://www.langtons.com.au": {
const match = url.match(/auc-var-\d+/);
return match[0];
}
case "https://www.lawsons.com.au": {
const match = url.split("_");
return match ? match[1] : null;
}
case "https://www.pickles.com.au": {
const model = url.split("/").pop();
return model ? model : null;
}
}
}
const showPage = async (pageLink = "pages/popup/popup.html") => {
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);
};
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,
};
// Validate required fields
if (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/info/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: key,
},
body: JSON.stringify(removeFalsyValues(payload)),
});
const result = await response.json();
alert(result.message);
} catch (error) {
alert("Error: " + error.message);
console.error("API Error:", error);
}
}
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 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>ID</label>
<input readonly value="${data?.id || "None"}" type="text" id="id" />
</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;
};
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;
}
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"),
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);
const script = document.createElement("script");
script.type = "module";
script.src = chrome.runtime.getURL("pages/popup/popup.js");
script.defer = true;
document.body.appendChild(script);
handleSaveKey();
const currentUrl = window.location.href;
const model = extractModelId(currentUrl);
if (!model) return;
// 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)
);
})();

41
manifest.json Normal file
View File

@ -0,0 +1,41 @@
{
"manifest_version": 3,
"name": "Bid Extension",
"version": "1.0",
"description": "Bid Extension",
"action": {
"default_popup": "pages/popup/popup.html",
"default_icon": {
"16": "assets/icons/16.png",
"32": "assets/icons/32.png",
"128": "assets/icons/128.png"
}
},
"background": {
"service_worker": "background.js"
},
"permissions": ["storage"],
"host_permissions": ["http://*/*", "https://*/*"],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
],
"web_accessible_resources": [
{
"resources": [
"pages/popup/popup.html",
"assets/css/index.css",
"config.js",
"assets/icons/*"
],
"matches": ["<all_urls>"]
}
],
"icons": {
"16": "assets/icons/16.png",
"32": "assets/icons/32.png",
"128": "assets/icons/128.png"
}
}

170
pages/popup/popup.html Normal file
View File

@ -0,0 +1,170 @@
<div
id="bid-toggle-container"
style="position: fixed; bottom: 20px; left: 20px; z-index: 9999"
>
<button
id="toggle-bid-extension"
style="
padding: 12px 20px;
background: #2c2f36;
color: #ffffff;
border: none;
border-radius: 9999px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
transition: background 0.3s ease, transform 0.2s ease;
"
onmouseover="this.style.background='#3a3d44'; this.style.transform='scale(1.05)'"
onmouseout="this.style.background='#2c2f36'; this.style.transform='scale(1)'"
>
<svg
fill="#ffffff"
version="1.1"
id="Capa_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
width="20px"
height="20px"
viewBox="0 0 494.212 494.212"
style="enable-background: new 0 0 494.212 494.212"
xml:space="preserve"
>
<g>
<path
d="M483.627,401.147L379.99,297.511c-7.416-7.043-16.084-10.567-25.981-10.567c-10.088,0-19.222,4.093-27.401,12.278
l-73.087-73.087l35.98-35.976c2.663-2.667,3.997-5.901,3.997-9.71c0-3.806-1.334-7.042-3.997-9.707
c0.377,0.381,1.52,1.569,3.423,3.571c1.902,2,3.142,3.188,3.72,3.571c0.571,0.378,1.663,1.328,3.278,2.853
c1.625,1.521,2.901,2.475,3.856,2.853c0.958,0.378,2.245,0.95,3.867,1.713c1.615,0.761,3.183,1.283,4.709,1.57
c1.522,0.284,3.237,0.428,5.14,0.428c7.228,0,13.703-2.665,19.411-7.995c0.574-0.571,2.286-2.14,5.14-4.712
c2.861-2.574,4.805-4.377,5.855-5.426c1.047-1.047,2.621-2.806,4.716-5.28c2.091-2.475,3.569-4.57,4.425-6.283
c0.853-1.711,1.708-3.806,2.57-6.28c0.855-2.474,1.279-4.949,1.279-7.423c0-7.614-2.665-14.087-7.994-19.417L236.41,8.003
c-5.33-5.33-11.802-7.994-19.413-7.994c-2.474,0-4.948,0.428-7.426,1.283c-2.475,0.854-4.567,1.713-6.28,2.568
c-1.714,0.855-3.806,2.331-6.28,4.427c-2.474,2.094-4.233,3.665-5.282,4.712c-1.047,1.049-2.855,3-5.424,5.852
c-2.572,2.856-4.143,4.57-4.712,5.142c-5.327,5.708-7.994,12.181-7.994,19.414c0,1.903,0.144,3.616,0.431,5.137
c0.288,1.525,0.809,3.094,1.571,4.714c0.76,1.618,1.331,2.903,1.713,3.853c0.378,0.95,1.328,2.24,2.852,3.858
c1.525,1.615,2.475,2.712,2.856,3.284c0.378,0.575,1.571,1.809,3.567,3.715c2,1.902,3.193,3.049,3.571,3.427
c-2.664-2.667-5.901-3.999-9.707-3.999s-7.043,1.331-9.707,3.999l-99.371,99.357c-2.667,2.666-3.999,5.901-3.999,9.707
c0,3.809,1.331,7.045,3.999,9.71c-0.381-0.381-1.524-1.574-3.427-3.571c-1.902-2-3.14-3.189-3.711-3.571
c-0.571-0.378-1.665-1.328-3.283-2.852c-1.619-1.521-2.905-2.474-3.855-2.853c-0.95-0.378-2.235-0.95-3.854-1.714
c-1.615-0.76-3.186-1.282-4.71-1.569c-1.521-0.284-3.234-0.428-5.137-0.428c-7.233,0-13.709,2.664-19.417,7.994
c-0.568,0.57-2.284,2.144-5.138,4.712c-2.856,2.572-4.803,4.377-5.852,5.426c-1.047,1.047-2.615,2.806-4.709,5.281
c-2.093,2.474-3.571,4.568-4.426,6.283c-0.856,1.709-1.709,3.806-2.568,6.28C0.432,212.061,0,214.535,0,217.01
c0,7.614,2.665,14.082,7.994,19.414l116.485,116.481c5.33,5.328,11.803,7.991,19.414,7.991c2.474,0,4.948-0.422,7.426-1.277
c2.475-0.855,4.567-1.714,6.28-2.569c1.713-0.855,3.806-2.327,6.28-4.425s4.233-3.665,5.28-4.716
c1.049-1.051,2.856-2.995,5.426-5.855c2.572-2.851,4.141-4.565,4.712-5.14c5.327-5.709,7.994-12.184,7.994-19.411
c0-1.902-0.144-3.617-0.431-5.14c-0.288-1.526-0.809-3.094-1.571-4.716c-0.76-1.615-1.331-2.902-1.713-3.854
c-0.378-0.951-1.328-2.238-2.852-3.86c-1.525-1.615-2.475-2.71-2.856-3.285c-0.38-0.571-1.571-1.807-3.567-3.717
c-2.002-1.902-3.193-3.045-3.571-3.429c2.663,2.669,5.902,4.001,9.707,4.001c3.806,0,7.043-1.332,9.707-4.001l35.976-35.974
l73.086,73.087c-8.186,8.186-12.278,17.312-12.278,27.401c0,10.283,3.621,18.843,10.849,25.7L401.42,483.643
c7.042,7.035,15.604,10.561,25.693,10.561c9.896,0,18.555-3.525,25.981-10.561l30.546-30.841
c7.043-7.043,10.571-15.605,10.571-25.693C494.212,417.231,490.684,408.566,483.627,401.147z"
/>
</g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
</svg>
</button>
</div>
<div
id="bid-extension"
class="wrapper"
style="
display: none;
position: fixed;
bottom: 90px;
left: 20px;
z-index: 9999;
background-color: #1e1e1e;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
padding: 20px;
width: 320px;
"
>
<!-- Form bid -->
<div style="display: none" id="form-bid">
<form class="container">
<h2>Bid</h2>
<div class="inputs">
<div id="url-col" class="col">
<label>Url</label>
<input readonly type="text" id="url" />
</div>
<div class="col">
<label>Max price</label>
<input type="number" id="maxPrice" />
</div>
<div class="col">
<label>Plus price</label>
<input type="number" id="plusPrice" />
</div>
<div class="col">
<label>Quantity</label>
<input type="number" id="quantity" />
</div>
</div>
<button type="submit" id="createBtn">Create</button>
</form>
<div class="key-container">
<span id="key-btn" class="key-btn">
<svg
fill="#ffffff"
height="14px"
width="14px"
viewBox="0 0 367.578 367.578"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M281.541,97.751c0-53.9-43.851-97.751-97.751-97.751S86.038,43.851,86.038,97.751
c0,44.799,30.294,82.652,71.472,94.159v144.668c0,4.026,1.977,9.1,4.701,12.065l14.514,15.798
c1.832,1.993,4.406,3.136,7.065,3.136s5.233-1.143,7.065-3.136l14.514-15.798
c2.724-2.965,4.701-8.039,4.701-12.065v-7.387l14.592-9.363
c2.564-1.646,4.035-4.164,4.035-6.909c0-2.744-1.471-5.262-4.036-6.907l-14.591-9.363v-0.207
l14.592-9.363c2.564-1.646,4.035-4.164,4.035-6.909c0-2.744-1.471-5.262-4.036-6.907l-14.591-9.363v-0.207
l14.592-9.363c2.564-1.646,4.035-4.164,4.035-6.908c0-2.745-1.471-5.263-4.036-6.909l-14.591-9.363V191.91
C251.246,180.403,281.541,142.551,281.541,97.751z
M183.789,104.948c-20.985,0-37.996-17.012-37.996-37.996s17.012-37.996,37.996-37.996
s37.996,17.012,37.996,37.996S204.774,104.948,183.789,104.948z"
/>
</svg>
</span>
</div>
</div>
<!-- Form key -->
<div style="display: block" id="form-key">
<form class="container">
<h2>Key</h2>
<div class="inputs">
<div class="col">
<label>Key</label>
<input type="password" id="key" />
</div>
</div>
<button type="submit" id="saveKeyBtn">Save</button>
</form>
</div>
</div>