update extension, grays flow, reset tool task
This commit is contained in:
parent
4d2c39c053
commit
42f0a881f4
|
|
@ -1 +1 @@
|
|||
{"createdAt":1747191706164}
|
||||
{"createdAt":1747292824357}
|
||||
|
|
@ -30,6 +30,7 @@
|
|||
"dayjs": "^1.11.13",
|
||||
"imap": "^0.8.19",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.30.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.13.0",
|
||||
"nestjs-paginate": "^11.1.0",
|
||||
|
|
@ -8660,9 +8661,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.8.3.tgz",
|
||||
"integrity": "sha512-BV9tebhfgcO0mT3desIQEM8vdFGOsqwfJ71a+mz/ECVa0uiokosBq3A3Vyk7XhyNUzzQECX/T/bpHQANemf5Vg==",
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
|
|
@ -10318,6 +10320,14 @@
|
|||
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/simple-imap/node_modules/moment": {
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.8.3.tgz",
|
||||
"integrity": "sha512-BV9tebhfgcO0mT3desIQEM8vdFGOsqwfJ71a+mz/ECVa0uiokosBq3A3Vyk7XhyNUzzQECX/T/bpHQANemf5Vg==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-imap/node_modules/readable-stream": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
"dayjs": "^1.11.13",
|
||||
"imap": "^0.8.19",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.30.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.13.0",
|
||||
"nestjs-paginate": "^11.1.0",
|
||||
|
|
|
|||
|
|
@ -5,14 +5,27 @@ import { BadRequestExceptionFilter } from './system/filters/bad-request-exceptio
|
|||
import { IoAdapter } from '@nestjs/platform-socket.io';
|
||||
import * as cookieParser from 'cookie-parser';
|
||||
import { useContainer } from 'class-validator';
|
||||
import { WebBidsService } from './modules/bids/services/web-bids.service';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
const prefix_version = '/api/v1';
|
||||
|
||||
const webBidService = app.get<WebBidsService>(WebBidsService);
|
||||
|
||||
const webs = await webBidService.webBidRepo.find();
|
||||
|
||||
const urls = webs.map((web) => web.origin_url);
|
||||
|
||||
const corsOrigins = process.env.CORS
|
||||
? process.env.CORS.split(',').map((origin) => origin.trim())
|
||||
: ['*']; // hoặc [] tùy ý bạn
|
||||
|
||||
|
||||
console.log('%csrc/main.ts:26 [...corsOrigins, ...urls]', 'color: #007acc;', [...corsOrigins, ...urls]);
|
||||
app.enableCors({
|
||||
origin: process.env.CORS.split(', ') || '*',
|
||||
origin: [...corsOrigins, ...urls],
|
||||
methods: ['GET', 'PUT', 'POST', 'DELETE', 'PATCH'],
|
||||
allowedHeaders: 'Content-Type, Authorization',
|
||||
credentials: true,
|
||||
|
|
|
|||
|
|
@ -103,4 +103,5 @@ export class GenerateKeysService {
|
|||
|
||||
return AppResponse.toResponse(result);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import { WebBidsService } from '../../services/web-bids.service';
|
|||
import { Event } from '../../utils/events';
|
||||
import AppResponse from '@/response/app-response';
|
||||
import { ClientUpdateLoginStatusDto } from '../../dto/bid/client-update-login-status.dto';
|
||||
import { UpdateBidDto } from '../../dto/bid/update-bid.dto';
|
||||
import { ClientInfoUpdateBidDto } from '../../dto/bid/client-info-update-bid.dto';
|
||||
|
||||
@Controller('bids')
|
||||
export class BidsController {
|
||||
|
|
@ -36,9 +38,14 @@ export class BidsController {
|
|||
return this.bidsService.clientIndex();
|
||||
}
|
||||
|
||||
@Get(':model')
|
||||
getByModel(@Param('model') model: Bid['model']) {
|
||||
return this.bidsService.getBidByModel(model);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() data: CreateBidDto) {
|
||||
return this.bidsService.create(data);
|
||||
return this.bidsService.create(data, (result) => !!result);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
|
|
@ -46,6 +53,11 @@ export class BidsController {
|
|||
return this.bidsService.clientUpdate(id, data);
|
||||
}
|
||||
|
||||
@Put('info/:id')
|
||||
updateInfo(@Param('id') id: Bid['id'], @Body() data: ClientInfoUpdateBidDto) {
|
||||
return this.bidsService.update(id, data);
|
||||
}
|
||||
|
||||
@Post('out-bid/:id')
|
||||
outBidStatus(@Param('id') id: Bid['id']) {
|
||||
return this.bidsService.outBid(id);
|
||||
|
|
@ -70,10 +82,8 @@ export class BidsController {
|
|||
}
|
||||
|
||||
@Post('update-login-status')
|
||||
async updateLoginStatus(
|
||||
@Body() data: ClientUpdateLoginStatusDto
|
||||
) {
|
||||
return await this.bidsService.emitLoginStatus(data)
|
||||
async updateLoginStatus(@Body() data: ClientUpdateLoginStatusDto) {
|
||||
return await this.bidsService.emitLoginStatus(data);
|
||||
}
|
||||
|
||||
@Post('test')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
import { IsNumber, IsOptional } from 'class-validator';
|
||||
|
||||
export class ClientInfoUpdateBidDto {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
max_price: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
quantity: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
plus_price: number;
|
||||
}
|
||||
|
|
@ -1,9 +1,16 @@
|
|||
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
} from 'typeorm';
|
||||
import { Timestamp } from './timestamp';
|
||||
import { Bid } from './bid.entity';
|
||||
|
||||
@Entity('send_message_histories')
|
||||
@Unique(['max_price', 'type', 'reserve_price'])
|
||||
@Unique(['max_price', 'type', 'reserve_price', 'bid'])
|
||||
export class SendMessageHistory extends Timestamp {
|
||||
@PrimaryGeneratedColumn('increment')
|
||||
id: number;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { NotificationService } from '@/modules/notification/notification.service';
|
||||
import {
|
||||
BadRequestException,
|
||||
HttpStatus,
|
||||
|
|
@ -21,6 +22,7 @@ import AppResponse from 'src/response/app-response';
|
|||
import { extractModelId, isTimeReached, subtractMinutes } from 'src/ultils';
|
||||
import { In, IsNull, Not, Repository } from 'typeorm';
|
||||
import { ClientUpdateBidDto } from '../dto/bid/client-update-bid.dto';
|
||||
import { ClientUpdateLoginStatusDto } from '../dto/bid/client-update-login-status.dto';
|
||||
import { CreateBidDto } from '../dto/bid/create-bid.dto';
|
||||
import { UpdateBidDto } from '../dto/bid/update-bid.dto';
|
||||
import { UpdateStatusByPriceDto } from '../dto/bid/update-status-by-price.dto';
|
||||
|
|
@ -28,11 +30,8 @@ import { BidHistory } from '../entities/bid-history.entity';
|
|||
import { Bid } from '../entities/bid.entity';
|
||||
import { ImageCompressionPipe } from '../pipes/image-compression-pipe';
|
||||
import { Constant } from '../utils/constant';
|
||||
import { WebBidsService } from './web-bids.service';
|
||||
import { NotificationService } from '@/modules/notification/notification.service';
|
||||
import { Event } from '../utils/events';
|
||||
import _ from 'lodash';
|
||||
import { ClientUpdateLoginStatusDto } from '../dto/bid/client-update-login-status.dto';
|
||||
import { WebBidsService } from './web-bids.service';
|
||||
|
||||
@Injectable()
|
||||
export class BidsService {
|
||||
|
|
@ -100,7 +99,7 @@ export class BidsService {
|
|||
await this.webBidsService.emitAllBidEvent();
|
||||
}
|
||||
|
||||
async create(data: CreateBidDto) {
|
||||
async create(data: CreateBidDto, response?: (result: Bid) => any) {
|
||||
const model = extractModelId(data.url);
|
||||
|
||||
const prev = await this.bidsRepo.findOne({ where: { model: model } });
|
||||
|
|
@ -121,7 +120,7 @@ export class BidsService {
|
|||
|
||||
await this.emitAllBidEvent();
|
||||
|
||||
return AppResponse.toResponse(plainToClass(Bid, result));
|
||||
return AppResponse.toResponse(response ? response(result) : plainToClass(Bid, result));
|
||||
}
|
||||
|
||||
async update(id: Bid['id'], data: UpdateBidDto) {
|
||||
|
|
@ -226,7 +225,7 @@ export class BidsService {
|
|||
);
|
||||
|
||||
// Nếu phiên đấu giá chưa có thời gian bắt đầu và kết thúc
|
||||
if (!bid.close_time && !bid.start_bid_time) {
|
||||
if (!bid.close_time && !bid.start_bid_time && data?.lot_id) {
|
||||
// Thiết lập thời gian bắt đầu là 5 phút trước khi đóng
|
||||
// bid.start_bid_time = new Date().toUTCString();
|
||||
bid.start_bid_time = subtractMinutes(
|
||||
|
|
@ -549,4 +548,14 @@ export class BidsService {
|
|||
|
||||
return nextBid;
|
||||
}
|
||||
|
||||
async getBidByModel(model: string) {
|
||||
|
||||
console.log('%csrc/modules/bids/services/bids.service.ts:554 model', 'color: #007acc;', model);
|
||||
const bid = await this.bidsRepo.findOne({ where: { model } });
|
||||
|
||||
if (!bid) return AppResponse.toResponse(null, {status_code: HttpStatus.NOT_FOUND});
|
||||
|
||||
return AppResponse.toResponse(plainToClass(Bid, bid));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ export class ConfigsService {
|
|||
return await this.configRepo.findOne({ where: { key_name } }) || null;
|
||||
}
|
||||
|
||||
async setConfig(key_name: keyof typeof ConfigsService.CONFIG_KEYS, value: string) {
|
||||
async setConfig(key_name: keyof typeof ConfigsService.CONFIG_KEYS, value: string, type: 'string' | 'number') {
|
||||
return await this.configRepo.upsert(
|
||||
{ key_name, value },
|
||||
{ key_name, value , type},
|
||||
['key_name']
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,16 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import { IsNull, Not } from 'typeorm';
|
||||
import { BidsService } from './bids.service';
|
||||
import { isTimeReached, subtractMinutes, subtractSeconds } from '@/ultils';
|
||||
import * as moment from 'moment';
|
||||
import {
|
||||
isTimeReached,
|
||||
shouldResetTool,
|
||||
subtractMinutes,
|
||||
subtractSeconds,
|
||||
} from '@/ultils';
|
||||
import { ConfigsService } from './configs.service';
|
||||
import { DashboardService } from './dashboard.service';
|
||||
import { Bid } from '../entities/bid.entity';
|
||||
@Injectable()
|
||||
export class TasksService {
|
||||
private readonly logger = new Logger(TasksService.name);
|
||||
|
|
@ -17,52 +24,57 @@ export class TasksService {
|
|||
|
||||
@Cron(CronExpression.EVERY_MINUTE)
|
||||
async handleCron() {
|
||||
const nextBid = await this.bidsService.getNextBid();
|
||||
const bids = await this.bidsService.bidsRepo.find({
|
||||
where: { status: 'biding' },
|
||||
select: ['close_time', 'created_at', 'start_bid_time', 'id', 'lot_id'],
|
||||
});
|
||||
|
||||
if (!nextBid) return;
|
||||
|
||||
const timeReset = subtractMinutes(nextBid.close_time, 20);
|
||||
|
||||
const timeToTracking = subtractSeconds(
|
||||
nextBid.close_time,
|
||||
nextBid.web_bid.early_tracking_seconds,
|
||||
);
|
||||
|
||||
if (!isTimeReached(timeReset) || isTimeReached(timeToTracking)) {
|
||||
console.log('Reset not allowed at this time');
|
||||
return;
|
||||
}
|
||||
|
||||
const lastestResetToolTime =
|
||||
const lastResetTimeRaw =
|
||||
await this.configsService.getConfig('REFRESH_TOOL_TIME');
|
||||
const lastResetTime = lastResetTimeRaw?.value
|
||||
? new Date(lastResetTimeRaw.value)
|
||||
: null;
|
||||
const now = new Date();
|
||||
|
||||
if (lastestResetToolTime?.value) {
|
||||
const lastReset = Date.parse(lastestResetToolTime.value);
|
||||
const now = Date.now();
|
||||
|
||||
const diffInMs = now - lastReset;
|
||||
const diffInHours = diffInMs / (1000 * 60 * 60);
|
||||
const minimumHours = 2;
|
||||
|
||||
if (diffInHours < minimumHours) {
|
||||
console.log(`Last reset was less than ${minimumHours} hours ago`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Proceeding to reset tool for next bid:', nextBid);
|
||||
|
||||
await this.dashboadService.resetTool();
|
||||
|
||||
|
||||
const time = new Date().toUTCString()
|
||||
|
||||
await this.configsService.setConfig(
|
||||
'REFRESH_TOOL_TIME',
|
||||
time,
|
||||
console.log('===== Reset Tool Check =====');
|
||||
console.log(
|
||||
'Last reset time:',
|
||||
lastResetTime
|
||||
? moment(lastResetTime).format('YYYY-MM-DD HH:mm:ss')
|
||||
: 'null',
|
||||
);
|
||||
console.log('Current time:', moment(now).format('YYYY-MM-DD HH:mm:ss'));
|
||||
|
||||
console.log('Reset successfully at: ' + time);
|
||||
const result = shouldResetTool(bids, lastResetTime, now);
|
||||
|
||||
if (result.shouldReset) {
|
||||
console.log('Should reset due to:', result.reason);
|
||||
if (result.bidId) {
|
||||
console.log('Related bid id:', result.bidId);
|
||||
|
||||
if (result.closeTime) {
|
||||
const closeTimeDate = new Date(result.closeTime);
|
||||
const expectedReset = moment(closeTimeDate)
|
||||
.subtract(20, 'minutes')
|
||||
.format('YYYY-MM-DD HH:mm:ss');
|
||||
console.log(
|
||||
'Expected reset time (close_time - 20 minutes):',
|
||||
expectedReset,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await this.dashboadService.resetTool();
|
||||
|
||||
await this.configsService.setConfig(
|
||||
'REFRESH_TOOL_TIME',
|
||||
now.toISOString(),
|
||||
'string',
|
||||
);
|
||||
|
||||
console.log('Tool reset at:', moment(now).format('YYYY-MM-DD HH:mm:ss'));
|
||||
} else {
|
||||
console.log('No reset needed at this time');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { Bid } from "@/modules/bids/entities/bid.entity";
|
||||
|
||||
export function extractModelId(url: string): string | null {
|
||||
switch (extractDomain(url)) {
|
||||
case 'https://www.grays.com': {
|
||||
|
|
@ -107,3 +109,72 @@ export function verifyCode(content: string) {
|
|||
// Nếu không tìm thấy mã xác minh, trả về null
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
export function shouldResetTool(
|
||||
bids: Bid[],
|
||||
lastResetTime: Date | null,
|
||||
now: Date = new Date(),
|
||||
) {
|
||||
const ONE_MINUTE = 60 * 1000;
|
||||
const ONE_HOUR = 60 * ONE_MINUTE;
|
||||
const TWO_HOURS = 2 * ONE_HOUR;
|
||||
const TWENTY_MINUTES = 20 * ONE_MINUTE;
|
||||
const FIVE_MINUTES = 5 * ONE_MINUTE;
|
||||
|
||||
// Nếu đã reset trong 1 giờ gần đây => không reset
|
||||
if (lastResetTime && now.getTime() - lastResetTime.getTime() < ONE_HOUR) {
|
||||
return {
|
||||
shouldReset: false,
|
||||
};
|
||||
}
|
||||
|
||||
// 1. Kiểm tra bid gần nhất có close_time trong vòng 20 phút tới
|
||||
const futureBids = bids
|
||||
.filter((b) => b.close_time)
|
||||
.map((b) => ({
|
||||
...b,
|
||||
closeTime: new Date(b.close_time!),
|
||||
}))
|
||||
.filter((b) => b.closeTime.getTime() > now.getTime())
|
||||
.sort((a, b) => a.closeTime.getTime() - b.closeTime.getTime());
|
||||
|
||||
const closest = futureBids[0];
|
||||
const hasBidCloseSoon =
|
||||
closest && closest.closeTime.getTime() - now.getTime() <= TWENTY_MINUTES;
|
||||
|
||||
if (hasBidCloseSoon) {
|
||||
return {
|
||||
shouldReset: true,
|
||||
reason: 'Bid close_time is within 20 minutes',
|
||||
bidId: closest.id,
|
||||
closeTime: closest.close_time
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Kiểm tra bid chưa có lot_id hoặc close_time, được tạo > 5 phút và cách reset trước > 2 tiếng
|
||||
// Đồng thời không có bid nào sắp close trong 20 phút tới
|
||||
if (!hasBidCloseSoon) {
|
||||
for (const bid of bids) {
|
||||
const createdAt = new Date(bid.created_at);
|
||||
if (
|
||||
(!bid.lot_id || !bid.close_time) &&
|
||||
now.getTime() - createdAt.getTime() > FIVE_MINUTES &&
|
||||
(!lastResetTime ||
|
||||
now.getTime() - lastResetTime.getTime() > TWO_HOURS)
|
||||
) {
|
||||
return {
|
||||
shouldReset: true,
|
||||
reason:
|
||||
'Bid is missing info and older than 5 mins, last reset > 2h, and no urgent bids',
|
||||
bidId: bid.id,
|
||||
closeTime: bid.close_time
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
shouldReset: false,
|
||||
};
|
||||
}
|
||||
|
|
@ -84,6 +84,9 @@ export class GrayApiBid extends ApiBid {
|
|||
configs.WEB_CONFIGS.GRAYS.AUTO_CALL_API_TO_TRACKING,
|
||||
this.id
|
||||
);
|
||||
|
||||
// extractLotsAndBids
|
||||
await this.extractLotsAndBids();
|
||||
} catch (error) {
|
||||
if (error.message.includes("Execution context was destroyed")) {
|
||||
console.log(
|
||||
|
|
@ -172,6 +175,32 @@ export class GrayApiBid extends ApiBid {
|
|||
return false;
|
||||
};
|
||||
|
||||
async extractLotsAndBids(url) {
|
||||
if (!this.page_context) return;
|
||||
|
||||
const data = await this.page_context.evaluate(() => {
|
||||
const rows = document.querySelectorAll("tr.current.open");
|
||||
|
||||
return Array.from(rows).map((row) => {
|
||||
const lotText =
|
||||
row.querySelector(".lot-number")?.textContent.trim() || "";
|
||||
const autobidMatch = row.textContent.match(
|
||||
/Your maximum autobid is AU\s*\$([\d,]+)/
|
||||
);
|
||||
const lotMatch = lotText.match(/Lot No:\s*(\S+)/);
|
||||
|
||||
return {
|
||||
model: lotMatch ? lotMatch[1] : null,
|
||||
max_price: autobidMatch
|
||||
? parseFloat(autobidMatch[1].replace(/,/g, ""))
|
||||
: null,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
global[`BIDED_DATA_${this.origin_url}`] = data || [];
|
||||
}
|
||||
|
||||
async handleLogin() {
|
||||
const page = this.page_context;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,13 @@
|
|||
import {
|
||||
outBid,
|
||||
pushPrice,
|
||||
updateBid,
|
||||
updateStatusByPrice,
|
||||
} from "../../system/apis/bid.js";
|
||||
import CONSTANTS from "../../system/constants.js";
|
||||
import {
|
||||
delay,
|
||||
extractNumber,
|
||||
isNumber,
|
||||
isTimeReached,
|
||||
removeFalsyValues,
|
||||
safeClosePage,
|
||||
takeSnapshot,
|
||||
} from "../../system/utils.js";
|
||||
import { ProductBid } from "../product-bid.js";
|
||||
|
||||
|
|
@ -65,6 +60,12 @@ export class GraysProductBid extends ProductBid {
|
|||
async isCloseProduct() {
|
||||
const close_time = await this.getCloseTime();
|
||||
|
||||
const currentUrl = await this.page_context.url();
|
||||
|
||||
if (currentUrl !== this.url) {
|
||||
return { result: false, close_time };
|
||||
}
|
||||
|
||||
if (!close_time) {
|
||||
const priceWasBid = await this.getPriceWasBid();
|
||||
|
||||
|
|
@ -130,6 +131,7 @@ export class GraysProductBid extends ProductBid {
|
|||
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.page_context.goto(this.url);
|
||||
console.log(`❌ [${this.id}] Error in placeBid: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -301,7 +303,6 @@ export class GraysProductBid extends ProductBid {
|
|||
);
|
||||
return;
|
||||
}
|
||||
const page = this.page_context;
|
||||
|
||||
if (global[`IS_PLACE_BID-${this.id}`]) {
|
||||
console.log(`⚠️ [${this.id}] Bid is already in progress, skipping.`);
|
||||
|
|
@ -316,17 +317,50 @@ export class GraysProductBid extends ProductBid {
|
|||
global.IS_CLEANING = false;
|
||||
|
||||
const isCloseProduct = await this.isCloseProduct();
|
||||
|
||||
if (isCloseProduct.result) {
|
||||
console.log(
|
||||
`⚠️ [${this.id}] Outbid detected, calling outBid function.`
|
||||
);
|
||||
|
||||
await outBid(this.id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const biddedData = this.getBidedData();
|
||||
|
||||
console.log({
|
||||
biddedData,
|
||||
});
|
||||
|
||||
const isBided = biddedData.find(
|
||||
(item) => item.model === this.model && item.max_price === this.max_price
|
||||
);
|
||||
|
||||
if (isBided) {
|
||||
console.log(`[${this.id}] This item bided. Skipping...`);
|
||||
global[`IS_PLACE_BID-${this.id}`] = false;
|
||||
global.IS_CLEANING = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.start_bid_time || !this.close_time) {
|
||||
console.log(
|
||||
`[${this.id}] Skipping processing: auction has started but not yet closed.`
|
||||
);
|
||||
|
||||
global.IS_CLEANING = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Kiểm tra nếu giá hiện tại lớn hơn giá tối đa cộng thêm giá cộng thêm
|
||||
if (this.current_price > this.max_price + this.plus_price) {
|
||||
console.log(`⚠️ [${this.id}] Outbid bid`); // Ghi log cảnh báo nếu giá hiện tại vượt quá mức tối đa cho phép
|
||||
|
||||
global.IS_CLEANING = true;
|
||||
|
||||
return; // Dừng hàm nếu giá đã vượt qua giới hạn
|
||||
}
|
||||
|
||||
|
|
@ -337,6 +371,8 @@ export class GraysProductBid extends ProductBid {
|
|||
this.name || "None"
|
||||
}`
|
||||
);
|
||||
global.IS_CLEANING = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -347,6 +383,9 @@ export class GraysProductBid extends ProductBid {
|
|||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
"-------------------------------------BIDING---------------------------------------"
|
||||
);
|
||||
const result = await this.placeBid();
|
||||
|
||||
global.IS_CLEANING = true;
|
||||
|
|
@ -358,6 +397,10 @@ export class GraysProductBid extends ProductBid {
|
|||
}
|
||||
}
|
||||
|
||||
getBidedData() {
|
||||
return global[`BIDED_DATA_${this.web_bid?.origin_url}`];
|
||||
}
|
||||
|
||||
action = async () => {
|
||||
try {
|
||||
const page = this.page_context;
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -0,0 +1,134 @@
|
|||
/* #bid-extension body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #121212;
|
||||
color: #e0e0e0;
|
||||
font-family: 'Segoe UI', Tahoma, sans-serif;
|
||||
width: 320px;
|
||||
} */
|
||||
|
||||
#bid-extension {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #121212;
|
||||
color: #e0e0e0;
|
||||
font-family: "Segoe UI", Tahoma, sans-serif;
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
#bid-extension .container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#bid-extension h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 12px;
|
||||
font-size: 22px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
#bid-extension label {
|
||||
font-size: 13px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
#bid-extension input,
|
||||
#bid-extension textarea {
|
||||
padding: 8px;
|
||||
background-color: #1e1e1e;
|
||||
color: #ffffff;
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#bid-extension input:focus,
|
||||
#bid-extension textarea:focus {
|
||||
border-color: #4a90e2;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#bid-extension .row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#bid-extension .col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#bid-extension .inputs .col {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
#bid-extension button {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background: linear-gradient(to right, #4a90e2, #357abd);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
#bid-extension button:hover {
|
||||
background: linear-gradient(to right, #3a78c2, #2d5faa);
|
||||
}
|
||||
|
||||
#bid-extension #errorMessage {
|
||||
margin-top: 2px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
#bid-extension .wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#bid-extension .key-container {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
#bid-extension .key-container a {
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
padding: 4px 10px;
|
||||
background: linear-gradient(to right, #4a90e2, #357abd);
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
#bid-extension .key-container a:hover {
|
||||
background: linear-gradient(to right, #3a78c2, #2d5faa);
|
||||
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
#bid-extension .inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#bid-extension svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
#toggle-bid-extension svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 305 B |
Binary file not shown.
|
After Width: | Height: | Size: 522 B |
|
|
@ -0,0 +1,4 @@
|
|||
chrome.action.onClicked.addListener((tab) => {
|
||||
// Lấy URL của tab hiện tại
|
||||
console.log("Current URL:", tab.url);
|
||||
});
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// config.js
|
||||
const CONFIG = {
|
||||
API_BASE_URL: "http://localhost:4000/api/v1",
|
||||
};
|
||||
|
||||
export default CONFIG;
|
||||
|
|
@ -0,0 +1,394 @@
|
|||
const CONFIG = {
|
||||
API_BASE_URL: "http://localhost:4000/api/v1",
|
||||
// API_BASE_URL: "https://bids.apactech.io/api/v1",
|
||||
};
|
||||
|
||||
let PREV_DATA = null;
|
||||
|
||||
function removeFalsyValues(obj, excludeKeys = []) {
|
||||
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||||
if (value || excludeKeys.includes(key)) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function extractDomain(url) {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
return parsedUrl.origin;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function extractModelId(url) {
|
||||
switch (extractDomain(url)) {
|
||||
case "https://www.grays.com": {
|
||||
const match = url.match(/\/lot\/([\d-]+)\//);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
case "https://www.langtons.com.au": {
|
||||
const match = url.match(/auc-var-\d+/);
|
||||
return match[0];
|
||||
}
|
||||
case "https://www.lawsons.com.au": {
|
||||
const match = url.split("_");
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
case "https://www.pickles.com.au": {
|
||||
const model = url.split("/").pop();
|
||||
return model ? model : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const showPage = async (pageLink = "pages/popup/popup.html") => {
|
||||
const res = await fetch(chrome.runtime.getURL(pageLink));
|
||||
const html = await res.text();
|
||||
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.innerHTML = html;
|
||||
document.body.appendChild(wrapper);
|
||||
};
|
||||
|
||||
const getKey = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.storage.local.get("key", (result) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(chrome.runtime.lastError);
|
||||
} else {
|
||||
resolve(result.key || null);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
async function handleCreate(event, formElements) {
|
||||
event.preventDefault();
|
||||
|
||||
const key = await getKey();
|
||||
if (!key) {
|
||||
showKey();
|
||||
return;
|
||||
}
|
||||
|
||||
const maxPrice = parseFloat(formElements.maxPrice.value);
|
||||
const plusPrice = parseFloat(formElements.plusPrice.value);
|
||||
const quantity = parseInt(formElements.quantity.value, 10);
|
||||
|
||||
const payload = {
|
||||
url: formElements.url.value.trim(),
|
||||
max_price: isNaN(maxPrice) ? null : maxPrice,
|
||||
plus_price: isNaN(plusPrice) ? null : plusPrice,
|
||||
quantity: isNaN(quantity) ? null : quantity,
|
||||
};
|
||||
|
||||
// Validate required fields
|
||||
if (!payload.url || payload.max_price === null) {
|
||||
alert("Please fill out the URL and Max Price fields correctly.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${CONFIG.API_BASE_URL}/bids`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: key,
|
||||
},
|
||||
body: JSON.stringify(removeFalsyValues(payload)),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
alert(result.message);
|
||||
|
||||
// showInfo
|
||||
await showInfo(extractModelId(payload.url), formElements);
|
||||
// handleChangeTitleButton
|
||||
handleChangeTitleButton(true, formElements);
|
||||
} catch (error) {
|
||||
alert("Error: " + error.message);
|
||||
console.error("API Error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdate(event, formElements, id) {
|
||||
event.preventDefault();
|
||||
|
||||
const key = await getKey();
|
||||
if (!key) {
|
||||
showKey();
|
||||
return;
|
||||
}
|
||||
|
||||
const maxPrice = parseFloat(formElements.maxPrice.value);
|
||||
const plusPrice = parseFloat(formElements.plusPrice.value);
|
||||
const quantity = parseInt(formElements.quantity.value, 10);
|
||||
|
||||
const payload = {
|
||||
max_price: isNaN(maxPrice) ? null : maxPrice,
|
||||
plus_price: isNaN(plusPrice) ? null : plusPrice,
|
||||
quantity: isNaN(quantity) ? null : quantity,
|
||||
};
|
||||
|
||||
// Validate required fields
|
||||
if (payload.max_price === null) {
|
||||
alert("Please fill out the URL and Max Price fields correctly.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${CONFIG.API_BASE_URL}/bids/info/${id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: key,
|
||||
},
|
||||
body: JSON.stringify(removeFalsyValues(payload)),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
alert(result.message);
|
||||
} catch (error) {
|
||||
alert("Error: " + error.message);
|
||||
console.error("API Error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const showBid = () => {
|
||||
const formKey = document.getElementById("form-key");
|
||||
const formBid = document.getElementById("form-bid");
|
||||
|
||||
formKey.style.display = "none";
|
||||
formBid.style.display = "block";
|
||||
};
|
||||
|
||||
const showKey = async () => {
|
||||
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");
|
||||
|
||||
// Kiểm tra xem nút và panel có tồn tại hay không
|
||||
if (btn && panel) {
|
||||
btn.addEventListener("click", async () => {
|
||||
panel.style.display = panel.style.display === "none" ? "block" : "none";
|
||||
await handleShowForm();
|
||||
});
|
||||
} else {
|
||||
console.error("Không tìm thấy nút hoặc panel!");
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowForm = async () => {
|
||||
const toggleBtn = document.getElementById("toggle-bid-extension");
|
||||
const formBid = document.getElementById("form-bid");
|
||||
const formKey = document.getElementById("form-key");
|
||||
const keyBtn = document.getElementById("key-btn");
|
||||
|
||||
const isVisible = (el) => el && el.style.display !== "none";
|
||||
|
||||
// Toggle hiển thị form hiện tại (bid hoặc key)
|
||||
toggleBtn?.addEventListener("click", async () => {
|
||||
if (isVisible(formBid)) {
|
||||
formBid.style.display = "none";
|
||||
} else if (isVisible(formKey)) {
|
||||
formKey.style.display = "none";
|
||||
} else {
|
||||
const currentKey = await getKey();
|
||||
if (!currentKey) {
|
||||
showKey();
|
||||
} else {
|
||||
showBid();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Nhấn vào icon key để chuyển sang form-key
|
||||
keyBtn?.addEventListener("click", () => {
|
||||
showKey();
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeTitleButton = (result, formElements) => {
|
||||
if (result) {
|
||||
formElements.createBtn.textContent = "Update";
|
||||
} else {
|
||||
formElements.createBtn.textContent = "Create";
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveKey = () => {
|
||||
const form = document.querySelector("#form-key form");
|
||||
if (!form) return;
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const inputKey = form.querySelector("#key");
|
||||
if (!inputKey) return;
|
||||
|
||||
const keyValue = inputKey.value.trim();
|
||||
if (!keyValue) {
|
||||
alert("Please enter a key");
|
||||
return;
|
||||
}
|
||||
|
||||
// Lưu vào chrome.storage.local
|
||||
chrome.storage.local.set({ key: keyValue }, async () => {
|
||||
alert("Key saved successfully!");
|
||||
showBid();
|
||||
|
||||
if (!isValidModel()) return;
|
||||
|
||||
await showInfo();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const isValidModel = () => {
|
||||
const currentUrl = window.location.href;
|
||||
|
||||
const model = extractModelId(currentUrl);
|
||||
|
||||
return !!model;
|
||||
};
|
||||
|
||||
const createInfoColumn = (data, formElements) => {
|
||||
const inputsContainer = document.querySelector("#bid-extension .inputs");
|
||||
const urlCol = document.querySelector("#url-col");
|
||||
|
||||
if (!inputsContainer || !urlCol) return;
|
||||
|
||||
// 1. Thêm ID và Name vào đầu inputsContainer
|
||||
const otherEls = `
|
||||
<div class="col">
|
||||
<label>ID</label>
|
||||
<input readonly value="${data?.id || "None"}" type="text" id="id" />
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<label>Name</label>
|
||||
<textarea readonly id="maxPrice">${data?.name || "None"}</textarea>
|
||||
</div>
|
||||
`;
|
||||
|
||||
inputsContainer.insertAdjacentHTML("afterbegin", otherEls);
|
||||
|
||||
// 2. Tạo và chèn Current Price ngay sau #url-col
|
||||
const currentPriceDiv = document.createElement("div");
|
||||
currentPriceDiv.className = "col";
|
||||
currentPriceDiv.innerHTML = `
|
||||
<label>Current price</label>
|
||||
<input readonly type="text" value="${
|
||||
data?.current_price || "None"
|
||||
}" id="currentPrice" />
|
||||
`;
|
||||
|
||||
urlCol.parentNode.insertBefore(currentPriceDiv, urlCol.nextSibling);
|
||||
|
||||
formElements.quantity.value = data?.quantity || 1;
|
||||
formElements.plusPrice.value = data?.plus_price || 0;
|
||||
};
|
||||
|
||||
const showInfo = async (model, formElements) => {
|
||||
const key = await getKey();
|
||||
if (!key) {
|
||||
showKey();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${CONFIG.API_BASE_URL}/bids/${model}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: key,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result || result?.status_code !== 200 || !result?.data) {
|
||||
if (result.status_code !== 404) {
|
||||
alert(result.message);
|
||||
}
|
||||
|
||||
PREV_DATA = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
formElements.maxPrice.value = result.data.max_price;
|
||||
|
||||
createInfoColumn(result.data, formElements);
|
||||
|
||||
PREV_DATA = result;
|
||||
return result;
|
||||
} catch (error) {
|
||||
alert("Error: " + error.message);
|
||||
console.error("API Error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
(async () => {
|
||||
await showPage();
|
||||
|
||||
const formElements = {
|
||||
url: document.querySelector("#form-bid #url"),
|
||||
maxPrice: document.querySelector("#form-bid #maxPrice"),
|
||||
plusPrice: document.querySelector("#form-bid #plusPrice"),
|
||||
quantity: document.querySelector("#form-bid #quantity"),
|
||||
createBtn: document.querySelector("#form-bid #createBtn"),
|
||||
form: document.querySelector("#form-bid form"),
|
||||
};
|
||||
|
||||
const style = document.createElement("link");
|
||||
style.rel = "stylesheet";
|
||||
style.href = chrome.runtime.getURL("assets/css/index.css");
|
||||
document.head.appendChild(style);
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.type = "module";
|
||||
script.src = chrome.runtime.getURL("pages/popup/popup.js");
|
||||
script.defer = true;
|
||||
|
||||
document.body.appendChild(script);
|
||||
|
||||
handleSaveKey();
|
||||
|
||||
const currentUrl = window.location.href;
|
||||
|
||||
const model = extractModelId(currentUrl);
|
||||
|
||||
if (!model) return;
|
||||
|
||||
// set url on form
|
||||
formElements.url.value = currentUrl;
|
||||
|
||||
await showInfo(model, formElements);
|
||||
handleChangeTitleButton(!!PREV_DATA, formElements);
|
||||
|
||||
formElements.form.addEventListener("submit", (e) =>
|
||||
PREV_DATA
|
||||
? handleUpdate(e, formElements, PREV_DATA.data.id)
|
||||
: handleCreate(e, formElements)
|
||||
);
|
||||
})();
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Bid Extension",
|
||||
"version": "1.0",
|
||||
"description": "Bid Extension",
|
||||
"action": {
|
||||
"default_popup": "pages/popup/popup.html",
|
||||
"default_icon": {
|
||||
"16": "assets/icons/16.png",
|
||||
"32": "assets/icons/32.png",
|
||||
"128": "assets/icons/128.png"
|
||||
}
|
||||
},
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"permissions": ["storage"],
|
||||
"host_permissions": ["http://*/*", "https://*/*"],
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["content.js"]
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": [
|
||||
"pages/popup/popup.html",
|
||||
"pages/popup/popup.js",
|
||||
"assets/css/index.css",
|
||||
"config.js",
|
||||
"assets/icons/*"
|
||||
],
|
||||
"matches": ["<all_urls>"]
|
||||
}
|
||||
],
|
||||
"icons": {
|
||||
"16": "assets/icons/16.png",
|
||||
"32": "assets/icons/32.png",
|
||||
"128": "assets/icons/128.png"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
<div
|
||||
id="bid-toggle-container"
|
||||
style="position: fixed; bottom: 20px; left: 20px; z-index: 9999"
|
||||
>
|
||||
<button
|
||||
id="toggle-bid-extension"
|
||||
style="
|
||||
padding: 12px 20px;
|
||||
background: #2c2f36;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 9999px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
||||
transition: background 0.3s ease, transform 0.2s ease;
|
||||
"
|
||||
onmouseover="this.style.background='#3a3d44'; this.style.transform='scale(1.05)'"
|
||||
onmouseout="this.style.background='#2c2f36'; this.style.transform='scale(1)'"
|
||||
>
|
||||
<svg
|
||||
fill="#ffffff"
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="20px"
|
||||
height="20px"
|
||||
viewBox="0 0 494.212 494.212"
|
||||
style="enable-background: new 0 0 494.212 494.212"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M483.627,401.147L379.99,297.511c-7.416-7.043-16.084-10.567-25.981-10.567c-10.088,0-19.222,4.093-27.401,12.278
|
||||
l-73.087-73.087l35.98-35.976c2.663-2.667,3.997-5.901,3.997-9.71c0-3.806-1.334-7.042-3.997-9.707
|
||||
c0.377,0.381,1.52,1.569,3.423,3.571c1.902,2,3.142,3.188,3.72,3.571c0.571,0.378,1.663,1.328,3.278,2.853
|
||||
c1.625,1.521,2.901,2.475,3.856,2.853c0.958,0.378,2.245,0.95,3.867,1.713c1.615,0.761,3.183,1.283,4.709,1.57
|
||||
c1.522,0.284,3.237,0.428,5.14,0.428c7.228,0,13.703-2.665,19.411-7.995c0.574-0.571,2.286-2.14,5.14-4.712
|
||||
c2.861-2.574,4.805-4.377,5.855-5.426c1.047-1.047,2.621-2.806,4.716-5.28c2.091-2.475,3.569-4.57,4.425-6.283
|
||||
c0.853-1.711,1.708-3.806,2.57-6.28c0.855-2.474,1.279-4.949,1.279-7.423c0-7.614-2.665-14.087-7.994-19.417L236.41,8.003
|
||||
c-5.33-5.33-11.802-7.994-19.413-7.994c-2.474,0-4.948,0.428-7.426,1.283c-2.475,0.854-4.567,1.713-6.28,2.568
|
||||
c-1.714,0.855-3.806,2.331-6.28,4.427c-2.474,2.094-4.233,3.665-5.282,4.712c-1.047,1.049-2.855,3-5.424,5.852
|
||||
c-2.572,2.856-4.143,4.57-4.712,5.142c-5.327,5.708-7.994,12.181-7.994,19.414c0,1.903,0.144,3.616,0.431,5.137
|
||||
c0.288,1.525,0.809,3.094,1.571,4.714c0.76,1.618,1.331,2.903,1.713,3.853c0.378,0.95,1.328,2.24,2.852,3.858
|
||||
c1.525,1.615,2.475,2.712,2.856,3.284c0.378,0.575,1.571,1.809,3.567,3.715c2,1.902,3.193,3.049,3.571,3.427
|
||||
c-2.664-2.667-5.901-3.999-9.707-3.999s-7.043,1.331-9.707,3.999l-99.371,99.357c-2.667,2.666-3.999,5.901-3.999,9.707
|
||||
c0,3.809,1.331,7.045,3.999,9.71c-0.381-0.381-1.524-1.574-3.427-3.571c-1.902-2-3.14-3.189-3.711-3.571
|
||||
c-0.571-0.378-1.665-1.328-3.283-2.852c-1.619-1.521-2.905-2.474-3.855-2.853c-0.95-0.378-2.235-0.95-3.854-1.714
|
||||
c-1.615-0.76-3.186-1.282-4.71-1.569c-1.521-0.284-3.234-0.428-5.137-0.428c-7.233,0-13.709,2.664-19.417,7.994
|
||||
c-0.568,0.57-2.284,2.144-5.138,4.712c-2.856,2.572-4.803,4.377-5.852,5.426c-1.047,1.047-2.615,2.806-4.709,5.281
|
||||
c-2.093,2.474-3.571,4.568-4.426,6.283c-0.856,1.709-1.709,3.806-2.568,6.28C0.432,212.061,0,214.535,0,217.01
|
||||
c0,7.614,2.665,14.082,7.994,19.414l116.485,116.481c5.33,5.328,11.803,7.991,19.414,7.991c2.474,0,4.948-0.422,7.426-1.277
|
||||
c2.475-0.855,4.567-1.714,6.28-2.569c1.713-0.855,3.806-2.327,6.28-4.425s4.233-3.665,5.28-4.716
|
||||
c1.049-1.051,2.856-2.995,5.426-5.855c2.572-2.851,4.141-4.565,4.712-5.14c5.327-5.709,7.994-12.184,7.994-19.411
|
||||
c0-1.902-0.144-3.617-0.431-5.14c-0.288-1.526-0.809-3.094-1.571-4.716c-0.76-1.615-1.331-2.902-1.713-3.854
|
||||
c-0.378-0.951-1.328-2.238-2.852-3.86c-1.525-1.615-2.475-2.71-2.856-3.285c-0.38-0.571-1.571-1.807-3.567-3.717
|
||||
c-2.002-1.902-3.193-3.045-3.571-3.429c2.663,2.669,5.902,4.001,9.707,4.001c3.806,0,7.043-1.332,9.707-4.001l35.976-35.974
|
||||
l73.086,73.087c-8.186,8.186-12.278,17.312-12.278,27.401c0,10.283,3.621,18.843,10.849,25.7L401.42,483.643
|
||||
c7.042,7.035,15.604,10.561,25.693,10.561c9.896,0,18.555-3.525,25.981-10.561l30.546-30.841
|
||||
c7.043-7.043,10.571-15.605,10.571-25.693C494.212,417.231,490.684,408.566,483.627,401.147z"
|
||||
/>
|
||||
</g>
|
||||
<g></g>
|
||||
<g></g>
|
||||
<g></g>
|
||||
<g></g>
|
||||
<g></g>
|
||||
<g></g>
|
||||
<g></g>
|
||||
<g></g>
|
||||
<g></g>
|
||||
<g></g>
|
||||
<g></g>
|
||||
<g></g>
|
||||
<g></g>
|
||||
<g></g>
|
||||
<g></g>
|
||||
</svg>
|
||||
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="bid-extension"
|
||||
class="wrapper"
|
||||
style="
|
||||
display: none;
|
||||
position: fixed;
|
||||
bottom: 90px;
|
||||
left: 20px;
|
||||
z-index: 9999;
|
||||
background-color: #1e1e1e;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
padding: 20px;
|
||||
width: 320px;
|
||||
"
|
||||
>
|
||||
<!-- Form bid -->
|
||||
<div style="display: none" id="form-bid">
|
||||
<form class="container">
|
||||
<h2>Bid</h2>
|
||||
<div class="inputs">
|
||||
<div id="url-col" class="col">
|
||||
<label>Url</label>
|
||||
<input readonly type="text" id="url" />
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<label>Max price</label>
|
||||
<input type="number" id="maxPrice" />
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<label>Plus price</label>
|
||||
<input type="number" id="plusPrice" />
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<label>Quantity</label>
|
||||
<input type="number" id="quantity" />
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" id="createBtn">Create</button>
|
||||
</form>
|
||||
|
||||
<div class="key-container">
|
||||
<span id="key-btn" class="key-btn">
|
||||
<svg
|
||||
fill="#ffffff"
|
||||
height="14px"
|
||||
width="14px"
|
||||
viewBox="0 0 367.578 367.578"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M281.541,97.751c0-53.9-43.851-97.751-97.751-97.751S86.038,43.851,86.038,97.751
|
||||
c0,44.799,30.294,82.652,71.472,94.159v144.668c0,4.026,1.977,9.1,4.701,12.065l14.514,15.798
|
||||
c1.832,1.993,4.406,3.136,7.065,3.136s5.233-1.143,7.065-3.136l14.514-15.798
|
||||
c2.724-2.965,4.701-8.039,4.701-12.065v-7.387l14.592-9.363
|
||||
c2.564-1.646,4.035-4.164,4.035-6.909c0-2.744-1.471-5.262-4.036-6.907l-14.591-9.363v-0.207
|
||||
l14.592-9.363c2.564-1.646,4.035-4.164,4.035-6.909c0-2.744-1.471-5.262-4.036-6.907l-14.591-9.363v-0.207
|
||||
l14.592-9.363c2.564-1.646,4.035-4.164,4.035-6.908c0-2.745-1.471-5.263-4.036-6.909l-14.591-9.363V191.91
|
||||
C251.246,180.403,281.541,142.551,281.541,97.751z
|
||||
M183.789,104.948c-20.985,0-37.996-17.012-37.996-37.996s17.012-37.996,37.996-37.996
|
||||
s37.996,17.012,37.996,37.996S204.774,104.948,183.789,104.948z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form key -->
|
||||
<div style="display: block" id="form-key">
|
||||
<form class="container">
|
||||
<h2>Key</h2>
|
||||
<div class="inputs">
|
||||
<div class="col">
|
||||
<label>Key</label>
|
||||
<input type="password" id="key" />
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" id="saveKeyBtn">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
const handleToogle = () => {
|
||||
const btn = document.getElementById("toggle-bid-extension");
|
||||
const panel = document.getElementById("bid-extension");
|
||||
|
||||
// Kiểm tra xem nút và panel có tồn tại hay không
|
||||
if (btn && panel) {
|
||||
btn.addEventListener("click", () => {
|
||||
panel.style.display = panel.style.display === "none" ? "block" : "none";
|
||||
});
|
||||
} else {
|
||||
console.error("Không tìm thấy nút hoặc panel!");
|
||||
}
|
||||
};
|
||||
|
||||
// init();
|
||||
|
||||
handleToogle();
|
||||
Loading…
Reference in New Issue