refactor code

This commit is contained in:
Admin 2025-08-13 11:15:18 +07:00
parent e3c3ce1f95
commit b2fc9cb629
70 changed files with 9345 additions and 1088 deletions

BIN
.DS_Store vendored

Binary file not shown.

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

69
README.md Normal file
View File

@ -0,0 +1,69 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

BIN
assets/.DS_Store vendored

Binary file not shown.

View File

@ -1,135 +0,0 @@
#bid-extension {
margin: 0;
padding: 0;
background-color: #121212;
color: #e0e0e0;
font-family: "Segoe UI", Tahoma, sans-serif;
width: 500px;
}
#bid-extension .container {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
}
#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 select,
#bid-extension textarea {
padding: 4px 8px;
background-color: #1e1e1e;
color: #ffffff;
border: 1px solid #333;
border-radius: 6px;
font-size: 14px;
width: 100%;
box-sizing: border-box;
height: 32px;
}
#bid-extension input:focus,
#bid-extension select: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;
background: linear-gradient(to right, #4a90e2, #357abd);
color: white;
border: none;
border-radius: 6px;
font-size: 15px;
height: 36px;
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;
}
#bid-extension .sub-col {
display: flex;
align-items: center;
gap: 10px;
}
#toggle-bid-extension svg {
width: 20px;
height: 20px;
}

View File

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

View File

@ -0,0 +1 @@
const r="https://esearch.danielvu.com";async function i(){(await chrome.tabs.query({})).some(t=>t.url&&t.url.startsWith(r))||chrome.tabs.create({url:r,active:!1})}chrome.runtime.onInstalled.addListener(()=>{chrome.alarms.create("checkTab",{periodInMinutes:.25})});chrome.alarms.onAlarm.addListener(e=>{e.name==="checkTab"&&i()});let a="";chrome.runtime.onMessage.addListener((e,c,t)=>{e.type==="HIGHLIGHT_TEXT"&&(a=e.text),e.type==="GET_HIGHLIGHT"&&t({text:a}),console.log(e),e.type==="SEARCH"&&chrome.tabs.create({url:e.url},n=>{chrome.scripting.executeScript({target:{tabId:n.id},func:()=>{}})})});

27
bid-extension/content.js Normal file
View File

@ -0,0 +1,27 @@
let t=null,d="";const a=()=>{const e="bid-extensions";if(document.getElementById(e))return;const o=document.createElement("div");o.id=e,document.body.appendChild(o);const s=document.createElement("script");s.src=chrome.runtime.getURL("inject-ui.js"),s.type="module",document.body.appendChild(s),window.addEventListener("message",i=>{if(i.source!==window)return;const n=i.data;n?.direction==="to-content"&&(n.type==="SAVE_KEY"&&chrome.storage.local.set({key:n.payload},()=>{console.log("✅ Key saved:",n.payload)}),n.type==="GET_KEY"&&chrome.storage.local.get(["key"],r=>{window.postMessage({direction:"from-content",type:"GET_KEY_RESULT",value:r.key},"*")}))})};document.addEventListener("selectionchange",()=>{d=window?.getSelection()?.toString().trim()});document.addEventListener("mousedown",e=>{t&&!t.contains(e.target)&&(t.style.display="none")});document.addEventListener("mouseup",e=>{d&&setTimeout(()=>{l(e.pageX,e.pageY)},10)});document.addEventListener("keydown",function(e){e.ctrlKey&&e.code==="Space"&&(e.preventDefault(),window.scrollTo({top:0,behavior:"smooth"}))});function p(){t=document.createElement("div"),t.innerHTML=`
<div style="display: flex; flex-direction: column; gap: 8px;">
<button id="esearch-btn" style="
padding: 3px 3px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
width: 80px;
font-weight: bold;
">🔍 ESearch</button>
<button id="erp-btn" style="
padding: 3px 3px;
background-color: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
width: 80px;
font-weight: bold;
">🔍 ERP VPN</button>
</div>
`,Object.assign(t.style,{position:"absolute",display:"none",zIndex:999999,backgroundColor:"#fff",color:"#000",border:"1px solid #ccc",borderRadius:"5px",padding:"8px",boxShadow:"0 2px 6px rgba(0,0,0,0.2)",fontSize:"14px"}),document.body.appendChild(t),document.getElementById("esearch-btn")?.addEventListener("click",()=>{c(`https://esearch.danielvu.com?keyword=${d}`)}),document.getElementById("erp-btn")?.addEventListener("click",()=>{c(`https://int.ipsupply.com.au/erptools/001_search-vpn?search=${d}`)})}function l(e,o){t.style.top=`${o+10}px`,t.style.left=`${e+10}px`,t.style.display="block"}function c(e){chrome.runtime.sendMessage({type:"SEARCH",url:e}),t.style.display="none"}const u=()=>{const e=window.location.href;(Object.values({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"}).some(i=>e.includes(i))||e.includes("https://members.brokerbin.com/"))&&a(),p()};u();

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 305 B

After

Width:  |  Height:  |  Size: 305 B

View File

Before

Width:  |  Height:  |  Size: 522 B

After

Width:  |  Height:  |  Size: 522 B

175
bid-extension/inject-ui.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,38 @@
{
"manifest_version": 3,
"name": "Bid Extension",
"version": "4.3",
"description": "Bid Extension",
"action": {
"default_icon": {
"16": "icons/16.png",
"32": "icons/32.png",
"128": "icons/128.png"
}
},
"permissions": ["storage", "tabs", "alarms"],
"host_permissions": ["http://*/*", "https://*/*"],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_idle",
"type": "module"
}
],
"web_accessible_resources": [
{
"resources": ["inject-ui.js"],
"matches": ["<all_urls>"]
}
],
"background": {
"service_worker": "background.js"
},
"icons": {
"16": "icons/16.png",
"32": "icons/32.png",
"128": "icons/128.png"
}
}

1
bid-extension/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

21
components.json Normal file
View File

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@ -1,674 +0,0 @@
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)
);
})();

23
eslint.config.js Normal file
View File

@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { globalIgnores } from 'eslint/config'
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<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>

View File

@ -1,16 +0,0 @@
(function () {
let data = null;
const elements = document.querySelectorAll(".ng-scope");
for (let i = 0; i < elements.length; i++) {
const scope = window.angular?.element(elements[i]).scope();
if (scope && scope.auction) {
console.log("Found at index:", i, "Auction:", scope.bidHistory);
data = scope.bidHistory;
break;
}
}
if (data) {
window.postMessage({ source: "my-extension", bidHistory: data }, "*");
}
})();

View File

@ -1,42 +0,0 @@
{
"manifest_version": 3,
"name": "Bid Extension",
"version": "2.1",
"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/*",
"injected.js"
],
"matches": ["<all_urls>"]
}
],
"icons": {
"16": "assets/icons/16.png",
"32": "assets/icons/32.png",
"128": "assets/icons/128.png"
}
}

5969
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

58
package.json Normal file
View File

@ -0,0 +1,58 @@
{
"name": "re-make-bid-extension",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"dev:build": "vite build --watch"
},
"dependencies": {
"@hookform/resolvers": "^5.1.1",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tooltip": "^1.2.7",
"@tailwindcss/vite": "^4.1.11",
"axios": "^1.10.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.525.0",
"next-themes": "^0.4.6",
"react": "^19.1.0",
"react-day-picker": "^9.8.0",
"react-dom": "^19.1.0",
"react-hook-form": "^7.60.0",
"sonner": "^2.0.6",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.11",
"zod": "^4.0.5",
"zustand": "^5.0.6"
},
"devDependencies": {
"@eslint/js": "^9.30.1",
"@tailwindcss/cli": "^4.1.11",
"@types/chrome": "^0.1.0",
"@types/node": "^24.0.13",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
"eslint": "^9.30.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"tw-animate-css": "^1.3.5",
"typescript": "~5.8.3",
"typescript-eslint": "^8.35.1",
"vite": "^7.0.4",
"vite-plugin-css-injected-by-js": "^3.5.2",
"vite-plugin-static-copy": "^3.1.1"
}
}

BIN
pages/.DS_Store vendored

Binary file not shown.

View File

@ -1,217 +0,0 @@
<div
id="bid-toggle-container"
style="position: fixed; bottom: 100px; right: 20px; z-index: 9999"
>
<button
id="toggle-bid-extension"
style="
padding: 12px 20px;
max-height: 44px;
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: 170px;
right: 20px;
z-index: 9999;
background-color: #1e1e1e;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
padding: 20px;
width: 500px;
"
>
<!-- Form bid -->
<div style="display: none" id="form-bid">
<form class="container">
<h2>Bid</h2>
<div style="max-height: 560px; overflow: auto" class="inputs">
<div class="sub-col">
<div style="display: none" class="col">
<label>ID</label>
<input readonly 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 style="display: none" class="col">
<label>Name</label>
<textarea rows="2" readonly id="name"></textarea>
</div>
<div style="display: none" id="competitor-max-bid-col" class="col">
<label>Competior max bid</label>
<input readonly type="text" id="competitor-max-bid" />
</div>
<div id="url-col" class="col">
<label>Url</label>
<input readonly type="text" id="url" />
</div>
<div id="max-price-col" class="col">
<label>Max price</label>
<input type="number" id="maxPrice" />
</div>
<div class="sub-col">
<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>
<div id="metadataMode" class="sub-col">
<div class="col">
<label for="arrival_offset_seconds">Arrival offset seconds</label>
<input
type="number"
id="arrival_offset_seconds"
name="arrival_offset_seconds"
/>
</div>
<div class="col">
<label for="early_tracking_seconds">Early tracking seconds</label>
<input
type="number"
id="early_tracking_seconds"
name="early_tracking_seconds"
/>
</div>
</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>

BIN
public/icons/128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
public/icons/16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

BIN
public/icons/32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

38
public/manifest.json Normal file
View File

@ -0,0 +1,38 @@
{
"manifest_version": 3,
"name": "Bid Extension",
"version": "4.3",
"description": "Bid Extension",
"action": {
"default_icon": {
"16": "icons/16.png",
"32": "icons/32.png",
"128": "icons/128.png"
}
},
"permissions": ["storage", "tabs", "alarms"],
"host_permissions": ["http://*/*", "https://*/*"],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_idle",
"type": "module"
}
],
"web_accessible_resources": [
{
"resources": ["inject-ui.js"],
"matches": ["<all_urls>"]
}
],
"background": {
"service_worker": "background.js"
},
"icons": {
"16": "icons/16.png",
"32": "icons/32.png",
"128": "icons/128.png"
}
}

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

73
src/api/product.ts Normal file
View File

@ -0,0 +1,73 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { removeFalsyValues } from "@/features/app";
import axios from "@/lib/axios";
export const getInfo = async ({
key,
model,
}: {
key?: string;
model: string;
}) => {
const response = await axios({
url: `bids/${model}`,
...(key
? {
headers: {
Authorization: key,
},
}
: {}),
});
return response.data;
};
export const createProduct = async ({
key,
data,
}: {
key?: string;
data: Record<string, any>;
}) => {
const response = await axios({
url: `bids`,
...(key
? {
headers: {
Authorization: key,
},
}
: {}),
data: data,
method: "POST",
});
return response.data;
};
export const updateProduct = async ({
key,
data,
}: {
key: string;
data: Partial<IBid>;
}) => {
const { max_price, metadata } = removeFalsyValues(data);
const res = await axios({
url: "bids/info/" + data.id,
withCredentials: true,
method: "PUT",
data: { max_price, metadata },
...(key
? {
headers: {
Authorization: key,
},
}
: {}),
});
return res.data;
};

27
src/app.tsx Normal file
View File

@ -0,0 +1,27 @@
// app.tsx
import { useEffect } from "react";
import AutionLayout from "./layout/aution-layout";
import BrokerbinLayout from "./layout/brokerbin-layout";
import { webs } from "./features/app";
function App() {
useEffect(() => {
const currentURL = window.location.href;
const isTargetWeb =
Object.values(webs).some((url) => currentURL.includes(url)) ||
currentURL.includes("https://members.brokerbin.com/");
if (isTargetWeb) {
document.documentElement.classList.add("custom-html-size");
}
}, []);
if (window.location.href.includes("https://members.brokerbin.com/")) {
return <BrokerbinLayout />;
}
return <AutionLayout />;
}
export default App;

View File

@ -0,0 +1,61 @@
<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>

After

Width:  |  Height:  |  Size: 3.4 KiB

1
src/assets/react.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

48
src/background.ts Normal file
View File

@ -0,0 +1,48 @@
const targetUrl = "https://esearch.danielvu.com"; // URL cần mở
// Hàm kiểm tra tab đã mở chưa
async function checkAndOpenTab() {
const tabs = await chrome.tabs.query({});
const alreadyOpened = tabs.some(
(tab) => tab.url && tab.url.startsWith(targetUrl)
);
if (!alreadyOpened) {
chrome.tabs.create({ url: targetUrl, active: false });
}
}
// Lập lịch báo thức cứ mỗi 15 giây
chrome.runtime.onInstalled.addListener(() => {
chrome.alarms.create("checkTab", { periodInMinutes: 0.25 }); // ~15 giây
});
// Gọi hàm khi báo thức kích hoạt
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === "checkTab") {
checkAndOpenTab();
}
});
// HIGHTLIGHT
let latestHighlight = "";
chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
if (message.type === "HIGHLIGHT_TEXT") {
latestHighlight = message.text;
}
if (message.type === "GET_HIGHLIGHT") {
sendResponse({ text: latestHighlight });
}
console.log(message);
if (message.type === "SEARCH") {
chrome.tabs.create({ url: message.url }, (tab) => {
chrome.scripting.executeScript({
target: { tabId: tab.id as number },
func: () => {},
});
});
}
});

View File

@ -0,0 +1,87 @@
"use client";
import * as React from "react";
import { ChevronDownIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
interface DatetimePickerProps {
onChange?: (datetime: { date: Date | undefined; time: string }) => void;
initialValue?: { date: Date | undefined; time: string }; // Thêm prop initialValue
}
export function DatetimePicker({
onChange,
initialValue,
}: DatetimePickerProps) {
const [open, setOpen] = React.useState(false);
const [date, setDate] = React.useState<Date | undefined>(initialValue?.date); // Sử dụng initialValue.date
const [time, setTime] = React.useState<string>(
initialValue?.time || "00:00:00"
); // Sử dụng initialValue.time hoặc mặc định "00:00:00"
// Handle date change
const handleDateChange = (selectedDate: Date | undefined) => {
setDate(selectedDate);
onChange?.({ date: selectedDate, time });
setOpen(false);
};
// Handle time change
const handleTimeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newTime = event.target.value;
setTime(newTime);
onChange?.({ date, time: newTime });
};
return (
<div className="flex gap-4 items-center justify-between">
<div className="flex flex-col gap-1 flex-1">
<Label htmlFor="date-picker" className="p-0 !m-0">
Date
</Label>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
id="date-picker"
className="w-full justify-between font-normal !text-black"
>
{date ? date.toLocaleDateString() : "Select date"}
<ChevronDownIcon />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
<Calendar
mode="single"
selected={date}
captionLayout="dropdown"
onSelect={handleDateChange}
/>
</PopoverContent>
</Popover>
</div>
<div className="flex flex-col gap-1 flex-1">
<Label htmlFor="time-picker" className="p-0 !m-0">
Time
</Label>
<Input
type="time"
id="time-picker"
step="1"
value={time}
onChange={handleTimeChange}
className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
/>
</div>
</div>
);
}

View File

@ -0,0 +1,23 @@
export default function Loader() {
return (
<div role="status">
<svg
aria-hidden="true"
className="inline w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-gray-600 dark:fill-gray-300"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
<span className="sr-only">Loading...</span>
</div>
);
}

View File

@ -0,0 +1,155 @@
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
)
}
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
)
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
)
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
)
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
)
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
)
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@ -0,0 +1,59 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"!inline-flex !items-center !justify-center !gap-2 !whitespace-nowrap !rounded-md !text-sm !font-medium !transition-all disabled:!pointer-events-none disabled:!opacity-50 [&_svg]:!pointer-events-none [&_svg:not([class*='size-'])]:!size-4 !shrink-0 [&_svg]:!shrink-0 !outline-none !focus-visible:!border-ring !focus-visible:!ring-ring/50 !focus-visible:!ring-[3px] aria-invalid:!ring-destructive/20 dark:aria-invalid:!ring-destructive/40 aria-invalid:!border-destructive",
{
variants: {
variant: {
default:
"!bg-primary !text-primary-foreground !shadow-xs !hover:bg-primary/90",
destructive:
"!bg-destructive !text-white !shadow-xs !hover:bg-destructive/90 !focus-visible:!ring-destructive/20 !dark:focus-visible:!ring-destructive/40 !dark:!bg-destructive/60",
outline:
"!border !bg-background !shadow-xs !hover:bg-accent !hover:text-accent-foreground !dark:bg-input/30 !dark:border-input !dark:hover:bg-input/50",
secondary:
"!bg-secondary !text-secondary-foreground !shadow-xs !hover:bg-secondary/80",
ghost:
"!hover:bg-accent !hover:text-accent-foreground !dark:hover:bg-accent/50",
link: "!text-primary !underline-offset-4 !hover:underline",
},
size: {
default: "!h-9 !px-4 !py-2 has-[>svg]:!px-3",
sm: "!h-8 !rounded-md !gap-1.5 !px-3 has-[>svg]:!px-2.5",
lg: "!h-10 !rounded-md !px-6 has-[>svg]:!px-4",
icon: "!size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };

View File

@ -0,0 +1,211 @@
import * as React from "react";
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react";
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
import { cn } from "@/lib/utils";
import { Button, buttonVariants } from "@/components/ui/button";
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"];
}) {
const defaultClassNames = getDefaultClassNames();
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"flex gap-4 flex-col md:flex-row relative",
defaultClassNames.months
),
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
nav: cn(
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none !bg-white !text-black btn-svg",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none !bg-white !text-black btn-svg",
defaultClassNames.button_next
),
month_caption: cn(
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
defaultClassNames.month_caption
),
dropdowns: cn(
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
defaultClassNames.dropdown_root
),
dropdown: cn(
"absolute bg-popover inset-0 opacity-0",
defaultClassNames.dropdown
),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
defaultClassNames.caption_label
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
defaultClassNames.weekday
),
week: cn("flex w-full mt-2", defaultClassNames.week),
week_number_header: cn(
"select-none w-(--cell-size)",
defaultClassNames.week_number_header
),
week_number: cn(
"text-[0.8rem] select-none text-muted-foreground",
defaultClassNames.week_number
),
day: cn(
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
defaultClassNames.day
),
range_start: cn(
"rounded-l-md bg-accent",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
);
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
);
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("size-4", className)}
{...props}
/>
);
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
);
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center">
{children}
</div>
</td>
);
},
...components,
}}
{...props}
/>
);
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames();
const ref = React.useRef<HTMLButtonElement>(null);
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus();
}, [modifiers.focused]);
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:!bg-black data-[selected-single=true]:!text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:!text-primary-foreground data-[range-end=true]:!bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:!border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:!text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70 !bg-white !text-black",
defaultClassNames.day,
className
)}
{...props}
/>
);
}
export { Calendar, CalendarDayButton };

View File

@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@ -0,0 +1,255 @@
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
);
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
);
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"!bg-popover !text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto !rounded-md !border !p-1 !shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
);
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
);
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground !relative !flex !cursor-default !items-center !gap-2 rounded-sm !py-1.5 !pr-2 !pl-8 !text-sm !outline-hidden !select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="!pointer-events-none !absolute !left-2 !flex !size-3.5 !items-center !justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
);
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
);
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
);
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
);
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
);
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};

View File

@ -0,0 +1,25 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"!file:text-foreground !placeholder:text-muted-foreground !selection:bg-primary !selection:text-primary-foreground " +
"!dark:bg-input/30 !border-input !flex !h-9 !w-full !min-w-0 !rounded-md !border !bg-transparent !px-3 !py-1 !text-base !shadow-xs " +
"!transition-[color,box-shadow] !outline-none " +
"!file:inline-flex !file:h-7 !file:border-0 !file:bg-transparent !file:text-sm !file:font-medium " +
"disabled:!pointer-events-none disabled:!cursor-not-allowed disabled:!opacity-50 " +
"md:!text-sm " +
"!focus-visible:border-ring !focus-visible:ring-ring/50 !focus-visible:ring-[3px] " +
"aria-invalid:!ring-destructive/20 dark:aria-invalid:!ring-destructive/40 aria-invalid:!border-destructive",
className
)}
{...props}
/>
);
}
export { Input };

View File

@ -0,0 +1,24 @@
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cn } from "@/lib/utils";
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"!flex !items-center !gap-2 !text-sm !leading-none !font-medium !select-none " +
"group-data-[disabled=true]:!pointer-events-none group-data-[disabled=true]:!opacity-50 " +
"peer-disabled:!cursor-not-allowed peer-disabled:!opacity-50",
className
)}
{...props}
/>
);
}
export { Label };

View File

@ -0,0 +1,46 @@
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils";
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"!bg-popover !text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) !rounded-md !border !p-4 !shadow-md !outline-hidden",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
);
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

View File

@ -0,0 +1,23 @@
import { useTheme } from "next-themes";
import { Toaster as Sonner, type ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
);
};
export { Toaster };

View File

@ -0,0 +1,35 @@
import * as React from "react";
import * as SwitchPrimitive from "@radix-ui/react-switch";
import { cn } from "@/lib/utils";
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"!p-0 data-[state=checked]:!bg-gray-700 data-[state=unchecked]:!bg-gray-300 " +
"focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:!bg-gray-600 " +
"!inline-flex h-[1.15rem] !w-8 !shrink-0 !items-center !rounded-full " +
"!border-transparent !shadow-xs !transition-all !outline-none focus-visible:ring-[3px] " +
"disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"!bg-white dark:!bg-gray-100 " +
"!pointer-events-none !block !size-4 !rounded-full !ring-0 transition-transform " +
"data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
);
}
export { Switch };

View File

@ -0,0 +1,63 @@
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
);
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
);
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"!bg-primary !text-primary-foreground !animate-in !fade-in-0 !zoom-in-95 " +
"data-[state=closed]:!animate-out data-[state=closed]:!fade-out-0 data-[state=closed]:!zoom-out-95 " +
"data-[side=bottom]:!slide-in-from-top-2 data-[side=left]:!slide-in-from-right-2 " +
"data-[side=right]:!slide-in-from-left-2 data-[side=top]:!slide-in-from-bottom-2 " +
"!z-50 !w-fit !rounded-md !px-3 !py-1.5 !text-xs !text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="!bg-primary !fill-primary !z-50 !size-2.5 !translate-y-[calc(-50%_-_2px)] !rotate-45 !rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

178
src/content.ts Normal file
View File

@ -0,0 +1,178 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
let popupDiv: any = null;
let selectedText: string | undefined = "";
const injectApp = () => {
const id = "bid-extensions";
if (document.getElementById(id)) return;
const root = document.createElement("div");
root.id = id;
document.body.appendChild(root);
const script = document.createElement("script");
script.src = chrome.runtime.getURL("inject-ui.js");
script.type = "module";
document.body.appendChild(script);
// handling
// Nghe message từ App (inject vào trang web)
window.addEventListener("message", (event) => {
// Chỉ xử lý message từ chính window
if (event.source !== window) return;
const message = event.data;
// Không đúng định dạng → bỏ qua
if (message?.direction !== "to-content") return;
// Ghi key vào chrome.storage
if (message.type === "SAVE_KEY") {
chrome.storage.local.set({ key: message.payload }, () => {
console.log("✅ Key saved:", message.payload);
});
}
// Lấy key từ chrome.storage
if (message.type === "GET_KEY") {
chrome.storage.local.get(["key"], (result) => {
// Gửi kết quả về lại React App
window.postMessage(
{
direction: "from-content",
type: "GET_KEY_RESULT",
value: result.key,
},
"*"
);
});
}
});
};
// Theo dõi thay đổi selection
document.addEventListener("selectionchange", () => {
selectedText = window?.getSelection()?.toString().trim();
});
// Click ra ngoài thì ẩn popup
document.addEventListener("mousedown", (e) => {
if (popupDiv && !popupDiv.contains(e.target)) {
popupDiv.style.display = "none";
}
});
// Khi mouseup mà có selectedText thì hiển thị popup
document.addEventListener("mouseup", (e) => {
if (selectedText) {
setTimeout(() => {
showPopupAt(e.pageX, e.pageY);
}, 10);
}
});
// Host key scroll to top
document.addEventListener("keydown", function (event) {
if (event.ctrlKey && event.code === "Space") {
event.preventDefault();
window.scrollTo({ top: 0, behavior: "smooth" });
}
});
// Tạo popup chỉ 1 lần
function initPopup() {
popupDiv = document.createElement("div");
popupDiv.innerHTML = `
<div style="display: flex; flex-direction: column; gap: 8px;">
<button id="esearch-btn" style="
padding: 3px 3px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
width: 80px;
font-weight: bold;
">🔍 ESearch</button>
<button id="erp-btn" style="
padding: 3px 3px;
background-color: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
width: 80px;
font-weight: bold;
">🔍 ERP VPN</button>
</div>
`;
Object.assign(popupDiv.style, {
position: "absolute",
display: "none", // ẩn ban đầu
zIndex: 999999,
backgroundColor: "#fff",
color: "#000",
border: "1px solid #ccc",
borderRadius: "5px",
padding: "8px",
boxShadow: "0 2px 6px rgba(0,0,0,0.2)",
fontSize: "14px",
});
document.body.appendChild(popupDiv);
document.getElementById("esearch-btn")?.addEventListener("click", () => {
openSearchTab(`https://esearch.danielvu.com?keyword=${selectedText}`);
});
document.getElementById("erp-btn")?.addEventListener("click", () => {
openSearchTab(
`https://int.ipsupply.com.au/erptools/001_search-vpn?search=${selectedText}`
);
});
}
// Hiển thị popup tại vị trí con trỏ
function showPopupAt(x: number, y: number) {
popupDiv.style.top = `${y + 10}px`;
popupDiv.style.left = `${x + 10}px`;
popupDiv.style.display = "block";
}
// Gửi message tới background script
function openSearchTab(url: string) {
chrome.runtime.sendMessage({
type: "SEARCH",
url,
});
popupDiv.style.display = "none";
}
const init = () => {
const currentURL = window.location.href;
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",
};
const isTargetWeb =
Object.values(webs).some((url) => currentURL.includes(url)) ||
currentURL.includes("https://members.brokerbin.com/");
if (isTargetWeb) {
injectApp();
}
initPopup();
};
init();

161
src/features/app.ts Normal file
View File

@ -0,0 +1,161 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unused-vars */
export const EXTENTION_ROOT_ID = "bid-extensions";
export function extractModelId(url: string) {
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] || null;
}
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: {
// eslint-disable-next-line no-useless-escape
const match = url.match(/-(\d+)(?:[\?#]|$)/);
return match ? match[1] : null;
}
}
}
export function extractDomain(url: string) {
try {
const parsedUrl = new URL(url);
return parsedUrl.origin;
} catch (error: any) {
return null;
}
}
export 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",
};
export const getMode = (data: { metadata: any[] }) => {
return (
data.metadata.find((item) => item.key_name === "mode_key")?.value || "live"
);
};
export const getEarlyTrackingSeconds = (
data: { metadata: any; web_bid?: any },
outsiteMode = null
) => {
const mode = outsiteMode ? outsiteMode : getMode(data);
return (
data.metadata.find(
(item: { key_name: string }) =>
item.key_name === `early_tracking_seconds_${mode}`
)?.value || data.web_bid.early_tracking_seconds
);
};
export const getArrivalOffsetSeconds = (
data: { metadata: any; web_bid?: any },
outsiteMode = null
) => {
const mode = outsiteMode ? outsiteMode : getMode(data);
return (
data.metadata.find(
(item: { key_name: string }) =>
item.key_name === `arrival_offset_seconds_${mode}`
)?.value || data.web_bid.arrival_offset_seconds
);
};
export function removeFalsyValues<T extends Record<string, any>>(
obj: T,
excludeKeys: (keyof T)[] = []
): Partial<T> {
return Object.entries(obj).reduce((acc, [key, value]) => {
if (value || excludeKeys.includes(key as keyof T)) {
acc[key as keyof T] = value;
}
return acc;
}, {} as Partial<T>);
}
export function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function getSecondsFromNow(datetime: {
date: Date | undefined;
time: string;
}): number | null {
if (!datetime.date) {
return null; // Trả về null nếu date không được chọn
}
// Tách giờ, phút, giây từ time string (HH:mm:ss)
const [hours, minutes, seconds] = datetime.time.split(":").map(Number);
// Tạo Date object mới từ date và time
const targetDate = new Date(datetime.date);
targetDate.setHours(hours, minutes, seconds, 0);
// Lấy thời điểm hiện tại
const now = new Date();
// Tính khoảng cách thời gian (mili giây)
const diffInMs = targetDate.getTime() - now.getTime();
// Chuyển sang giây (làm tròn xuống)
const diffInSeconds = Math.floor(diffInMs / 1000);
return diffInSeconds;
}
export function formatTimeFromMinutes(minutes: number): string {
// 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();
}
export function getDatetimeFromSeconds(seconds: number): {
date: Date | undefined;
time: string;
} {
// Lấy thời điểm hiện tại
const now = new Date();
// Tính thời điểm tương lai bằng cách cộng số giây vào hiện tại
const targetDate = new Date(now.getTime() + seconds * 1000);
// Lấy giờ, phút, giây và định dạng thành chuỗi "HH:mm:ss"
const hours = String(targetDate.getHours()).padStart(2, "0");
const minutes = String(targetDate.getMinutes()).padStart(2, "0");
const secondsStr = String(targetDate.getSeconds()).padStart(2, "0");
const time = `${hours}:${minutes}:${secondsStr}`;
return {
date: targetDate,
time,
};
}

View File

@ -0,0 +1,21 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export const saveKey = (payload: any) => {
window.postMessage(
{
direction: "to-content",
type: "SAVE_KEY",
payload: payload,
},
"*"
);
};
export const getKey = () => {
window.postMessage(
{
direction: "to-content",
type: "GET_KEY",
},
"*"
);
};

View File

@ -0,0 +1,21 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export const saveStateLogin = (payload: any) => {
window.postMessage(
{
direction: "to-content",
type: "SAVE_STATE_LOGIN",
payload: payload,
},
"*"
);
};
export const getStateLogin = () => {
window.postMessage(
{
direction: "to-content",
type: "GET_STATE_LOGIN",
},
"*"
);
};

130
src/index.css Normal file
View File

@ -0,0 +1,130 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
.custom-html-size {
@apply !text-[16px];
}
}
.btn-svg > svg {
width: 20px;
height: 20px;
color: black;
}

16
src/inject-ui.tsx Normal file
View File

@ -0,0 +1,16 @@
import ReactDOM from "react-dom/client";
import App from "./app";
import "./index.css";
import { Toaster } from "./components/ui/sonner";
import { EXTENTION_ROOT_ID } from "./features/app";
const container = document.getElementById(EXTENTION_ROOT_ID);
if (container) {
ReactDOM.createRoot(container).render(
<>
<App />
<Toaster position="top-right" />
</>
);
}

100
src/interfate.d.ts vendored Normal file
View File

@ -0,0 +1,100 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
interface IKey extends ITimestamp {
id: number;
client_key: string;
}
interface ITimestamp {
created_at: string;
updated_at: string;
}
interface IHistory extends ITimestamp {
id: number;
price: number;
}
interface IOutBidLog extends ITimestamp {
id: number;
model: string;
lot_id: string;
out_price: number;
raw_data: string;
}
interface IScrapConfig extends ITimestamp {
id: number;
search_url: string;
keywords: string;
enable: boolean | "0" | "1";
scrap_items: IScrapItem[];
}
interface IScrapItem extends ITimestamp {
id: number;
url: string;
model: string;
image_url: string | null;
keyword: string;
}
interface IWebBid extends ITimestamp {
created_at: string;
updated_at: string;
id: number;
origin_url: string;
url: string | null;
username: string | null;
password: string | null;
active: boolean;
arrival_offset_seconds: number;
early_tracking_seconds: number;
snapshot_at: string | null;
children: IBid[];
scrap_config: IScrapConfig;
}
interface IMetadata extends ITimestamp {
id: number;
key_name: string;
value: any;
}
interface IBid extends ITimestamp {
id: number;
max_price: number;
reserve_price: number;
current_price: number;
name: string | null;
quantity: number;
url: string;
model: string;
lot_id: string;
plus_price: number;
close_time: string | null;
close_time_ts: string | null;
start_bid_time: string | null;
first_bid: boolean;
status: "biding" | "out-bid" | "win-bid";
histories: IHistory[];
web_bid: IWebBid;
metadata: IMetadata[];
}
interface IConfig extends ITimestamp {
id: number;
key_name: string;
value: string;
type: "string" | "number";
}
interface IPermission extends ITimestamp {
id: number;
name: string;
description: string;
}
interface ISendMessageHistory extends ITimestamp {
id: number;
message: string;
bid: IBid;
}

View File

@ -0,0 +1,70 @@
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { extractModelId } from "@/features/app";
import { getKey } from "@/features/key-manager";
import useAppStore from "@/lib/zustand/app";
import FormPage from "@/pages/form";
import KeyPage from "@/pages/key";
import { Hammer } from "lucide-react";
import { useEffect, useState } from "react";
export default function AutionLayout() {
const { setKey, page } = useAppStore();
const [isShow, setIsShow] = useState(false);
useEffect(() => {
// Call to get key
getKey();
// Handle listent and add key to state
const handleResponse = (event: MessageEvent) => {
if (
event.data?.direction === "from-content" &&
event.data?.type === "GET_KEY_RESULT"
) {
setKey(event.data.value);
}
};
window.addEventListener("message", handleResponse);
return () => window.removeEventListener("message", handleResponse);
}, [setKey]);
useEffect(() => {
const currentUrl = window.location.href;
const model = extractModelId(currentUrl);
if (!model) return;
setIsShow(true);
}, []);
return (
<div className="fixed bottom-[100px] right-[20px] z-50">
{isShow && (
<Popover>
<PopoverTrigger asChild>
<Button className="!bg-black" size={"icon"}>
<Hammer />
</Button>
</PopoverTrigger>
<PopoverContent side="top" align="end" className="!w-fit">
{(() => {
switch (page) {
case "key":
return <KeyPage />;
case "form":
return <FormPage />;
default:
return <KeyPage />;
}
})()}
</PopoverContent>
</Popover>
)}
</div>
);
}

View File

@ -0,0 +1,14 @@
import CheckAllList from "@/pages/check-all-list";
import FormFillQuantities from "@/pages/form-fill-quanties";
export default function BrokerbinLayout() {
// Render button check all list
if (window.location.href.includes("https://members.brokerbin.com/partkey")) {
return <CheckAllList />;
}
// Render forn quantities
if (window.location.href.includes("https://members.brokerbin.com/rfq")) {
return <FormFillQuantities />;
}
}

View File

@ -0,0 +1,76 @@
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { saveKey } from "@/features/key-manager";
import { saveStateLogin } from "@/features/login-state-manager";
import useAppStore from "@/lib/zustand/app";
import { Menu } from "lucide-react";
import type { ReactNode } from "react";
export interface IChildrenAutionProps {
title: string;
children: ReactNode;
}
export default function ChildrenAutionLayout({
title,
children,
}: IChildrenAutionProps) {
const { page, logout } = useAppStore();
const handleLogout = () => {
logout();
saveKey("");
saveStateLogin(false);
};
const handleScrollToTop = () => {
window.scrollTo({ top: 0, behavior: "smooth" });
};
return (
<div className="min-w-[300px] w-[480px]">
<div className="space-y-3">
<div className="w-full flex items-center justify-between">
<h4 className="leading-none font-semibold !text-lg">{title}</h4>
{page !== "key" && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant={"outline"} size={"icon"}>
<Menu color="#000" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="!z-[999999]"
side="top"
align="end"
>
<DropdownMenuItem onClick={handleScrollToTop}>
Scroll to top
<DropdownMenuShortcut>Ctrl+Space</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={handleLogout}
className="text-destructive"
>
Logout
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
<div>{children}</div>
</div>
</div>
);
}

31
src/lib/axios.ts Normal file
View File

@ -0,0 +1,31 @@
import ax from "axios";
import useAppStore from "@/lib/zustand/app"; // Giả sử có biến page
import { toast } from "sonner";
const axios = ax.create({
// Production
baseURL: "https://bids.apactech.io/api/v1",
// Dev
// baseURL: "http://localhost:4000/api/v1",
headers: {
Authorization: useAppStore.getState().key,
"Content-Type": "application/json",
},
});
// Intercept response
axios.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Gọi setPage("key") từ Zustand
useAppStore.getState().logout?.();
toast("Unauthenticated !");
}
return Promise.reject(error);
}
);
export default axios;

6
src/lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

20
src/lib/zustand/app.ts Normal file
View File

@ -0,0 +1,20 @@
import { create } from "zustand";
interface AppState {
key: string;
page: "key" | "form";
setKey: (newKey: string) => void;
setPage: (newPage: "form" | "key") => void;
logout: () => void;
}
const useAppStore = create<AppState>((set) => ({
key: "",
page: "form",
isLogin: false,
setKey: (newKey) => set({ key: newKey }),
logout: () => set({ key: "", page: "key" }),
setPage: (newPage: "form" | "key") => set({ page: newPage }),
}));
export default useAppStore;

View File

@ -0,0 +1,33 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { CheckCheck } from "lucide-react";
export default function CheckAllList() {
const handleSelectAll = () => {
document
.querySelectorAll('input[type="checkbox"][name="partcart[]"]')
.forEach((checkbox: any) => {
checkbox.checked = true;
});
};
return (
<div className="fixed bottom-[100px] right-[20px] z-50">
<Tooltip>
<TooltipTrigger>
<Button onClick={handleSelectAll} size="icon" className="size-8">
<CheckCheck />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Click to select all</p>
</TooltipContent>
</Tooltip>
</div>
);
}

View File

@ -0,0 +1,120 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { FormInput } from "lucide-react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
// ✅ Zod Schema
const schema = z.object({
subject: z.string().optional(),
quantity: z
.number({ error: "Please enter a valid number" })
.min(1, "Quantity must be at least 1"),
});
type FormValues = z.infer<typeof schema>;
export default function FormFillQuantities() {
const {
setValue,
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
subject: "",
quantity: 1,
},
});
const onSubmit = (data: FormValues) => {
const inputs = document.querySelectorAll<HTMLInputElement>(
'input[name="rfq[parts][qty][]"]'
);
inputs.forEach((input) => {
input.value = data.quantity.toString();
});
const subject: any = document.getElementById("rfqsubject");
if (!subject || !data.subject) return;
subject.value = data.subject;
};
useEffect(() => {
const firstItem: HTMLInputElement | null = document.querySelector(
"#allparts tbody tr:nth-child(3) td input"
);
if (!firstItem) return;
setValue("subject", `WTB: ${firstItem.value}`);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className="fixed bottom-[100px] right-[20px] z-50">
<Popover defaultOpen={true}>
<PopoverTrigger asChild>
<Button className="!bg-black flex items-center gap-2">
<FormInput size={16} />
Fill Quantity
</Button>
</PopoverTrigger>
<PopoverContent side="top" align="end" className="w-64">
<div className="space-y-3">
<div className="text-center">
<h4 className="text-lg font-semibold">RFQ</h4>
</div>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="grid gap-2">
<Label htmlFor="subject">Subject</Label>
<Input
id="subject"
type="text"
placeholder="E.g., Bulk request"
{...register("subject")}
/>
{errors.subject && (
<span className="text-sm text-red-500">
{errors.subject.message}
</span>
)}
</div>
<div className="grid gap-2">
<Label htmlFor="quantity">Quantity</Label>
<Input
id="quantity"
type="number"
{...register("quantity", { valueAsNumber: true })}
/>
{errors.quantity && (
<span className="text-sm text-red-500">
{errors.quantity.message}
</span>
)}
</div>
<Button type="submit" className="w-full">
Apply
</Button>
</form>
</div>
</PopoverContent>
</Popover>
</div>
);
}

414
src/pages/form.tsx Normal file
View File

@ -0,0 +1,414 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { createProduct, getInfo, updateProduct } from "@/api/product";
import { DatetimePicker } from "@/components/app/datetime-picker";
import Loader from "@/components/app/loader";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
delay,
extractModelId,
formatTimeFromMinutes,
getArrivalOffsetSeconds,
getDatetimeFromSeconds,
getEarlyTrackingSeconds,
getMode,
getSecondsFromNow,
} from "@/features/app";
import useAppStore from "@/lib/zustand/app";
import { AxiosError } from "axios";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import ChildrenAutionLayout from "@/layout/children-aution-layout";
const schema = z.object({
mode_key: z.boolean(),
max_price: z
.number({ message: "Max price is required" })
.min(1, "Max price must be at least 1"),
arrival_offset_seconds: z
.number({ message: "Bid lead time is required" })
.min(60, "Must be at least 60 seconds (1 minute)"),
early_tracking_seconds: z
.number({ message: "Tracking start time is required" })
.min(600, "Must be at least 600 seconds (10 minutes)"),
});
export default function FormPage() {
const { key } = useAppStore();
const [model, setModel] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [data, setData] = useState<IBid | null>(null);
const [competitor, setCompetitor] = useState<any[] | null>(null);
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
defaultValues: {
mode_key: true,
max_price: 1,
arrival_offset_seconds: 300,
early_tracking_seconds: 600,
},
});
const onSubmit = async (formData: z.infer<typeof schema>) => {
if (!model) return toast("Missing model ID");
setLoading(true);
const mode = formData.mode_key ? "live" : "sandbox";
const metadata = [
{ key_name: "mode_key", value: mode },
{
key_name: `arrival_offset_seconds_${mode}`,
value: formData.arrival_offset_seconds,
},
{
key_name: `early_tracking_seconds_${mode}`,
value: formData.early_tracking_seconds,
},
] as IMetadata[];
// TODO: Submit to API here
if (data) {
try {
const newMetadata = data.metadata.map((item) => {
const metaItem = metadata.find((i) => i.key_name === item.key_name);
if (metaItem) {
return {
...item,
value: metaItem.value,
};
}
return item;
});
const res = await updateProduct({
key,
data: {
...formData,
metadata: newMetadata,
id: data.id,
},
});
if (res?.data) {
await delay(400);
toast.success("Bid Updated !");
await handleShowInfo();
}
} catch (error) {
toast.error("Something wen't wrong !");
} finally {
setLoading(false);
}
} else {
try {
const res = await createProduct({
key,
data: {
metadata,
url: window.location.href,
max_price: formData.max_price,
},
});
if (res?.data) {
await delay(400);
toast.success("Bid Created !");
await handleShowInfo();
}
} catch (error) {
toast.error(
(error as AxiosError)?.message || "Something wen't wrong !"
);
} finally {
setLoading(false);
}
}
};
const handleShowInfo = useCallback(async () => {
const modelId = extractModelId(window.location.href);
if (!modelId) return;
setModel(modelId);
getInfo({ model: modelId, key })
.then((res) => {
if (!res?.data) return;
const mode = getMode(res.data);
const early_tracking_seconds = getEarlyTrackingSeconds(res.data);
const arrival_offset_seconds = getArrivalOffsetSeconds(res.data);
form.reset({
max_price: res.data.max_price,
arrival_offset_seconds,
early_tracking_seconds,
mode_key: mode === "live",
});
// Set data to state
setData(res.data);
})
.catch((err) => toast((err as AxiosError)?.message));
}, [key, form]);
const listentCompetitor = () => {
let data = null;
const elements = document.querySelectorAll(".ng-scope");
for (let i = 0; i < elements.length; i++) {
const scope = (window as any)["angular"]?.element(elements[i]).scope();
if (scope && scope.auction) {
data = scope.bidHistory;
break;
}
}
if (data) {
setCompetitor(data);
}
};
useEffect(() => {
handleShowInfo();
}, [handleShowInfo]);
useEffect(() => {
listentCompetitor();
}, []);
useEffect(() => {
const values = form.getValues();
if (!values) return;
if (values.mode_key) return;
if (
form.formState.errors.arrival_offset_seconds ||
form.formState.errors.arrival_offset_seconds
) {
console.log("jghwjkgjew");
const message = "Please select a time 10 minutes greater than current";
form.setError("arrival_offset_seconds", {
message,
});
form.setError("early_tracking_seconds", {
message,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [form.formState.errors]);
return (
<ChildrenAutionLayout title="">
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<div className="flex items-center gap-2">
<Label className="!p-0 !m-0">Sandbox</Label>
<Switch
checked={form.watch("mode_key")}
onCheckedChange={(v) => {
form.setValue("mode_key", v);
if (!data) return;
// Biding value when change Switch
const mode = v ? "live" : "sandbox";
const newMetadata = data.metadata.map((item) => {
if (item.key_name === "mode_key") {
return {
...item,
value: mode,
};
}
return item;
});
const early_tracking_seconds = getEarlyTrackingSeconds({
...data,
metadata: newMetadata,
});
const arrival_offset_seconds = getArrivalOffsetSeconds({
...data,
metadata: newMetadata,
});
form.setValue("arrival_offset_seconds", arrival_offset_seconds);
form.setValue("early_tracking_seconds", early_tracking_seconds);
}}
/>
<Label className="!p-0 !m-0">Live</Label>
</div>
<div className="flex flex-col gap-1">
<Label
className="!text-lg !m-0 !uppercase font-semibold"
htmlFor="max_price"
>
Your Bid
</Label>
<Input
className="!h-11"
type="number"
{...form.register("max_price", { valueAsNumber: true })}
/>
{form.formState.errors.max_price && (
<p className="text-red-500 text-xs">
{form.formState.errors.max_price.message}
</p>
)}
</div>
<div className="flex items-center w-full gap-4">
{data && data.current_price >= 0 && (
<div className="flex flex-col gap-1 flex-1">
<Label>Current price</Label>
<Input
value={data.current_price}
className="placeholder:!text-xs"
readOnly
/>
</div>
)}
{competitor && competitor?.[0].proxyamount && (
<div className="flex flex-col gap-1">
<Label className="text-destructive">Competitor max bid</Label>
<Input
readOnly
type="number"
className="placeholder:!text-xs"
value={competitor?.[0].proxyamount}
/>
</div>
)}
</div>
{form.watch("mode_key") && (
<div className="flex items-center justify-between gap-2">
<Tooltip delayDuration={400}>
<TooltipTrigger asChild>
<div className="flex flex-col gap-1 flex-1">
<Label htmlFor="arrival_offset_seconds">
Bid lead time{" "}
{form.watch("arrival_offset_seconds") &&
`(${formatTimeFromMinutes(
form.watch("arrival_offset_seconds") / 60
)})`}
</Label>
<Input
type="number"
{...form.register("arrival_offset_seconds", {
valueAsNumber: true,
})}
className="placeholder:!text-xs w-full"
placeholder="msg:300"
/>
{form.formState.errors.arrival_offset_seconds && (
<p className="text-red-500 text-xs">
{form.formState.errors.arrival_offset_seconds.message}
</p>
)}
</div>
</TooltipTrigger>
<TooltipContent className="!bg-black">
Send bid this many seconds before auction ends.
</TooltipContent>
</Tooltip>
<Tooltip delayDuration={400}>
<TooltipTrigger asChild>
<div className="flex flex-col gap-1 flex-1">
<Label htmlFor="early_tracking_seconds">
Tracking start time{" "}
{form.watch("early_tracking_seconds") &&
`(${formatTimeFromMinutes(
form.watch("early_tracking_seconds") / 60
)})`}
</Label>
<Input
type="number"
{...form.register("early_tracking_seconds", {
valueAsNumber: true,
})}
className="placeholder:!text-xs w-full"
placeholder="msg:600"
/>
{form.formState.errors.early_tracking_seconds && (
<p className="text-red-500 text-xs">
{form.formState.errors.early_tracking_seconds.message}
</p>
)}
</div>
</TooltipTrigger>
<TooltipContent className="!bg-black">
Start monitoring this many seconds before auction ends.
</TooltipContent>
</Tooltip>
</div>
)}
{!form.watch("mode_key") && (
<div className="flex flex-col gap-1 w-full">
<Label htmlFor="arrival_offset_seconds">Start at:</Label>
<DatetimePicker
initialValue={(() => {
if (!data) return undefined;
const arrival_offset_seconds = getArrivalOffsetSeconds(data);
return getDatetimeFromSeconds(arrival_offset_seconds);
})()}
onChange={(date) => {
const seconds = getSecondsFromNow(date);
if (!seconds) return;
form.setValue("arrival_offset_seconds", seconds);
form.setValue("early_tracking_seconds", seconds);
}}
/>
{/* Hiển thị lỗi ngay dưới DatetimePicker */}
{(form.formState.errors.arrival_offset_seconds ||
form.formState.errors.early_tracking_seconds) && (
<div className="text-red-500 text-xs mt-1">
{form.formState.errors.arrival_offset_seconds?.message ||
form.formState.errors.early_tracking_seconds?.message}
</div>
)}
</div>
)}
<Button
type="submit"
className="w-full mt-2 !bg-black"
disabled={loading}
>
{data ? "Update" : "Create"}
{loading && <Loader />}
</Button>
</form>
</ChildrenAutionLayout>
);
}

67
src/pages/key.tsx Normal file
View File

@ -0,0 +1,67 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import useAppStore from "@/lib/zustand/app";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { saveKey } from "@/features/key-manager";
import { toast } from "sonner";
import ChildrenAutionLayout from "@/layout/children-aution-layout";
// Schema validation bằng zod
const schema = z.object({
key: z.string().min(3, "Key phải có ít nhất 3 ký tự"),
});
type FormData = z.infer<typeof schema>;
export default function KeyPage() {
const { key, setKey, setPage } = useAppStore();
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: { key },
});
const onSubmit = (data: FormData) => {
// Call to save key event
saveKey(data.key);
// Set state key global
setKey(data.key);
// Change show page
setPage("form");
toast("Key save success !");
};
return (
<ChildrenAutionLayout title="Key">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col gap-6">
<div className="grid gap-1">
<Input
type="password"
placeholder="Enter your key..."
{...register("key")}
/>
{errors.key && (
<p className="text-sm text-red-500">{errors.key.message}</p>
)}
</div>
<div className="flex flex-col gap-3">
<Button type="submit" className="w-full">
Submit
</Button>
</div>
</div>
</form>
</ChildrenAutionLayout>
);
}

1
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

31
tsconfig.app.json Normal file
View File

@ -0,0 +1,31 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}

16
tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"target": "es2020",
"module": "es2020",
"moduleResolution": "bundler" // hoc "node" nếu bn dùng tsc không bundler
}
}

25
tsconfig.node.json Normal file
View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

BIN
uploads/2025:08:13_4.3.zip Normal file

Binary file not shown.

28
vite.config.ts Normal file
View File

@ -0,0 +1,28 @@
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import path from "path";
import { defineConfig } from "vite";
import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js";
export default defineConfig({
plugins: [react(), tailwindcss(), cssInjectedByJsPlugin()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
build: {
outDir: "bid-extension",
cssCodeSplit: true,
rollupOptions: {
input: {
content: "src/content.ts",
background: "src/background.ts",
"inject-ui": "src/inject-ui.tsx",
},
output: {
entryFileNames: "[name].js",
},
},
emptyOutDir: false,
},
});