refactor code
|
|
@ -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?
|
||||
|
|
@ -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...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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:()=>{}})})});
|
||||
|
|
@ -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();
|
||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 305 B After Width: | Height: | Size: 305 B |
|
Before Width: | Height: | Size: 522 B After Width: | Height: | Size: 522 B |
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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"
|
||||
}
|
||||
674
content.js
|
|
@ -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)
|
||||
);
|
||||
})();
|
||||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
@ -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>
|
||||
16
injected.js
|
|
@ -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 }, "*");
|
||||
}
|
||||
})();
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 305 B |
|
After Width: | Height: | Size: 522 B |
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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: () => {},
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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();
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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",
|
||||
},
|
||||
"*"
|
||||
);
|
||||
};
|
||||
|
|
@ -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",
|
||||
},
|
||||
"*"
|
||||
);
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 />;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"target": "es2020",
|
||||
"module": "es2020",
|
||||
"moduleResolution": "bundler" // hoặc "node" nếu bạn dùng tsc không bundler
|
||||
}
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||