From 42f0a881f4c2905eaa299a1812a96f4f7b547885 Mon Sep 17 00:00:00 2001 From: Admin Date: Fri, 16 May 2025 09:56:39 +0700 Subject: [PATCH] update extension, grays flow, reset tool task --- .DS_Store | Bin 6148 -> 8196 bytes auto-bid-server/bot-data/metadata.json | 2 +- auto-bid-server/package-lock.json | 16 +- auto-bid-server/package.json | 1 + auto-bid-server/src/main.ts | 15 +- .../admins/services/generate-key.service.ts | 1 + .../controllers/client/bids.controller.ts | 20 +- .../dto/bid/client-info-update-bid.dto.ts | 15 + .../entities/send-message-histories.entity.ts | 11 +- .../src/modules/bids/services/bids.service.ts | 23 +- .../modules/bids/services/configs.service.ts | 4 +- .../modules/bids/services/tasks.servise.ts | 96 +++-- auto-bid-server/src/ultils/index.ts | 71 ++++ .../models/grays.com/grays-api-bid.js | 29 ++ .../models/grays.com/grays-product-bid.js | 55 ++- bid-extension/.DS_Store | Bin 0 -> 6148 bytes bid-extension/assets/css/index.css | 134 ++++++ bid-extension/assets/icons/128.png | Bin 0 -> 4131 bytes bid-extension/assets/icons/16.png | Bin 0 -> 305 bytes bid-extension/assets/icons/32.png | Bin 0 -> 522 bytes bid-extension/background.js | 4 + bid-extension/config.js | 6 + bid-extension/content.js | 394 ++++++++++++++++++ bid-extension/manifest.json | 42 ++ bid-extension/pages/popup/popup.html | 170 ++++++++ bid-extension/pages/popup/popup.js | 17 + 26 files changed, 1057 insertions(+), 69 deletions(-) create mode 100644 auto-bid-server/src/modules/bids/dto/bid/client-info-update-bid.dto.ts create mode 100644 bid-extension/.DS_Store create mode 100644 bid-extension/assets/css/index.css create mode 100644 bid-extension/assets/icons/128.png create mode 100644 bid-extension/assets/icons/16.png create mode 100644 bid-extension/assets/icons/32.png create mode 100644 bid-extension/background.js create mode 100644 bid-extension/config.js create mode 100644 bid-extension/content.js create mode 100644 bid-extension/manifest.json create mode 100644 bid-extension/pages/popup/popup.html create mode 100644 bid-extension/pages/popup/popup.js diff --git a/.DS_Store b/.DS_Store index cb763e0f78ac2f94794a85c27c16bba086f6bf36..dfa9dfe442ac880f96c65cb8fd8ddea32d0a7839 100644 GIT binary patch delta 401 zcmZoMXmOBWU|?W$DortDU;r^WfEYvza8E20o2aMA$g?qEH}hr%jz7$c**Q2SHn1@A zOy*%pXWTKFhrLi;qPp75z)(lQ*utzHM+K07BjFTZOt zH>*5jH&CC#W^dLmMpmE&4EmdIv57Gz@-iebWHO{M=rW`-R4|kP$vlQ)ph!MLo@Y*e za#Buy6396~M-~EU-Tz(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, diff --git a/auto-bid-server/src/modules/admins/services/generate-key.service.ts b/auto-bid-server/src/modules/admins/services/generate-key.service.ts index 596525b..8f2558d 100644 --- a/auto-bid-server/src/modules/admins/services/generate-key.service.ts +++ b/auto-bid-server/src/modules/admins/services/generate-key.service.ts @@ -103,4 +103,5 @@ export class GenerateKeysService { return AppResponse.toResponse(result); } + } diff --git a/auto-bid-server/src/modules/bids/controllers/client/bids.controller.ts b/auto-bid-server/src/modules/bids/controllers/client/bids.controller.ts index 1b2b433..9d2622b 100644 --- a/auto-bid-server/src/modules/bids/controllers/client/bids.controller.ts +++ b/auto-bid-server/src/modules/bids/controllers/client/bids.controller.ts @@ -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') diff --git a/auto-bid-server/src/modules/bids/dto/bid/client-info-update-bid.dto.ts b/auto-bid-server/src/modules/bids/dto/bid/client-info-update-bid.dto.ts new file mode 100644 index 0000000..d3ddd47 --- /dev/null +++ b/auto-bid-server/src/modules/bids/dto/bid/client-info-update-bid.dto.ts @@ -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; +} diff --git a/auto-bid-server/src/modules/bids/entities/send-message-histories.entity.ts b/auto-bid-server/src/modules/bids/entities/send-message-histories.entity.ts index 5d34dd7..a1317d5 100644 --- a/auto-bid-server/src/modules/bids/entities/send-message-histories.entity.ts +++ b/auto-bid-server/src/modules/bids/entities/send-message-histories.entity.ts @@ -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; diff --git a/auto-bid-server/src/modules/bids/services/bids.service.ts b/auto-bid-server/src/modules/bids/services/bids.service.ts index beae3f8..b6e5501 100644 --- a/auto-bid-server/src/modules/bids/services/bids.service.ts +++ b/auto-bid-server/src/modules/bids/services/bids.service.ts @@ -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)); + } } diff --git a/auto-bid-server/src/modules/bids/services/configs.service.ts b/auto-bid-server/src/modules/bids/services/configs.service.ts index 9a7f3f3..a913378 100644 --- a/auto-bid-server/src/modules/bids/services/configs.service.ts +++ b/auto-bid-server/src/modules/bids/services/configs.service.ts @@ -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'] ); } diff --git a/auto-bid-server/src/modules/bids/services/tasks.servise.ts b/auto-bid-server/src/modules/bids/services/tasks.servise.ts index 3af672b..6f27b34 100644 --- a/auto-bid-server/src/modules/bids/services/tasks.servise.ts +++ b/auto-bid-server/src/modules/bids/services/tasks.servise.ts @@ -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'); + } } } diff --git a/auto-bid-server/src/ultils/index.ts b/auto-bid-server/src/ultils/index.ts index 03eb11d..88a86f4 100644 --- a/auto-bid-server/src/ultils/index.ts +++ b/auto-bid-server/src/ultils/index.ts @@ -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, + }; + } \ No newline at end of file diff --git a/auto-bid-tool/models/grays.com/grays-api-bid.js b/auto-bid-tool/models/grays.com/grays-api-bid.js index 97c3f4f..f2b0c37 100644 --- a/auto-bid-tool/models/grays.com/grays-api-bid.js +++ b/auto-bid-tool/models/grays.com/grays-api-bid.js @@ -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; diff --git a/auto-bid-tool/models/grays.com/grays-product-bid.js b/auto-bid-tool/models/grays.com/grays-product-bid.js index 7cc2cb2..fa55857 100644 --- a/auto-bid-tool/models/grays.com/grays-product-bid.js +++ b/auto-bid-tool/models/grays.com/grays-product-bid.js @@ -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; diff --git a/bid-extension/.DS_Store b/bid-extension/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..ff39f15aebacf413641dd4a66220018f776c10f5 GIT binary patch literal 6148 zcmeHKO;3YB5PgGc5 zlgz$lna5-w8-@WO^X>8!m;s3CiqV+E9@G2k1B=4SAyM2jR(L{4!77nJLy}R=CG4Zm_{K9E)e5>N0WGV~)`< zs^M8_$SKI}<-%K!anxblG1nSpH;bI~IB|zF*pIyS;wO0FUi9?VoTb>G<-Nz^rI{&U z3YY?aN&(hvwRqyuMpM8PFa>rB==UL^D`o*(kM7gK!L#zaTrTF^QmCzi&1tPo=vQ<#jcYybD4e)b%vdeO$8=|0Pjo`e0=6Ex QLbD$MCxZ>9z>g~M2_eE_MF0Q* literal 0 HcmV?d00001 diff --git a/bid-extension/assets/css/index.css b/bid-extension/assets/css/index.css new file mode 100644 index 0000000..68bea98 --- /dev/null +++ b/bid-extension/assets/css/index.css @@ -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; +} diff --git a/bid-extension/assets/icons/128.png b/bid-extension/assets/icons/128.png new file mode 100644 index 0000000000000000000000000000000000000000..c7de32f12f0ec8ede8ecbf967a13ef62710ae7a5 GIT binary patch literal 4131 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 literal 0 HcmV?d00001 diff --git a/bid-extension/assets/icons/16.png b/bid-extension/assets/icons/16.png new file mode 100644 index 0000000000000000000000000000000000000000..46986c5cb4fd5f183d149b1c8f628084040b3474 GIT binary patch literal 305 zcmV-10nYx3P)Px#>q$gGR5(wS(mhH8K@ps0w%Laaov5y77Xf_ezST1e#? zEIda@BatT#4C~D9I^FJ@_szU{Z=k*gxWf)+IKis9JvNnA{C~uD77Sz`BMk9`uM$v7 zPEH^Xc*8~9tOQ))tfjPstmCBwyknyUh)^nLRRT8ginTsS?Gd-)dOeWa8W4x~L6$Y( z9-sK=xXGjhM4JwNhvv8idB$!NNXhB_;`^vj&QEe2k^{};2@bPPM0YMR$9Wp-1au`= zI~u?>#@UYM$*toMz#{ASzmh`^;0Ak`;{)E2)$DuJSIcUyB00000NkvXXu0mjf Db_amO literal 0 HcmV?d00001 diff --git a/bid-extension/assets/icons/32.png b/bid-extension/assets/icons/32.png new file mode 100644 index 0000000000000000000000000000000000000000..b82bd113285a98419f8f633ee98e00603214b260 GIT binary patch literal 522 zcmV+l0`>igP)Px$#7RU!R9HvFmpv~9K@>pGLkI;4y#zmpMktVwkch$`pwK8(@*0gONJK;-6zGIf z;3oH~GFQgF+1;7lS?_h5cX!U&JNM28^dzJNnu19%S^vJnz&#r6T` zz*Oq}3t%opqKrTmTwPoe!s!A&(;plvPy}~LNS00kc~|7tfENu65%7hlCFHA*Os&X& zXTW@@Kqx#<;td$k>F~)w4{+%yIN$o!n$S#414E{B@DW%A4t!}Dakj)Ha1}BI(%;J! z1>|*Si~3|>D`}za2m($CAAw=u5tyh$!bd>3qrg2d9$Ug!pf-sRfjT6bp#rfbp34Zt zlxT$rbelE(HYvvANQJrQD)9eay;}XoF`7l!a?nMB*;BcU%LrGUr8S!m4l` zUpBCHU<`OL?fc={N9-Di1rOS { + // Lấy URL của tab hiện tại + console.log("Current URL:", tab.url); +}); diff --git a/bid-extension/config.js b/bid-extension/config.js new file mode 100644 index 0000000..6d7a601 --- /dev/null +++ b/bid-extension/config.js @@ -0,0 +1,6 @@ +// config.js +const CONFIG = { + API_BASE_URL: "http://localhost:4000/api/v1", +}; + +export default CONFIG; diff --git a/bid-extension/content.js b/bid-extension/content.js new file mode 100644 index 0000000..a799987 --- /dev/null +++ b/bid-extension/content.js @@ -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 = ` +
+ + +
+ +
+ + +
+ `; + + 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 = ` + + + `; + + 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) + ); +})(); diff --git a/bid-extension/manifest.json b/bid-extension/manifest.json new file mode 100644 index 0000000..e59b97a --- /dev/null +++ b/bid-extension/manifest.json @@ -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": [""], + "js": ["content.js"] + } + ], + "web_accessible_resources": [ + { + "resources": [ + "pages/popup/popup.html", + "pages/popup/popup.js", + "assets/css/index.css", + "config.js", + "assets/icons/*" + ], + "matches": [""] + } + ], + "icons": { + "16": "assets/icons/16.png", + "32": "assets/icons/32.png", + "128": "assets/icons/128.png" + } +} diff --git a/bid-extension/pages/popup/popup.html b/bid-extension/pages/popup/popup.html new file mode 100644 index 0000000..68ead10 --- /dev/null +++ b/bid-extension/pages/popup/popup.html @@ -0,0 +1,170 @@ +
+ +
+ +
+ + + + +
+
+

Key

+
+
+ + +
+
+ +
+
+
diff --git a/bid-extension/pages/popup/popup.js b/bid-extension/pages/popup/popup.js new file mode 100644 index 0000000..34f60f0 --- /dev/null +++ b/bid-extension/pages/popup/popup.js @@ -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();