diff --git a/.DS_Store b/.DS_Store index cb763e0..dfa9dfe 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/auto-bid-server/bot-data/metadata.json b/auto-bid-server/bot-data/metadata.json index 2ecec79..b8429b3 100644 --- a/auto-bid-server/bot-data/metadata.json +++ b/auto-bid-server/bot-data/metadata.json @@ -1 +1 @@ -{"createdAt":1747191706164} \ No newline at end of file +{"createdAt":1747292824357} \ No newline at end of file diff --git a/auto-bid-server/package-lock.json b/auto-bid-server/package-lock.json index e5feefe..6081c11 100644 --- a/auto-bid-server/package-lock.json +++ b/auto-bid-server/package-lock.json @@ -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", diff --git a/auto-bid-server/package.json b/auto-bid-server/package.json index e0dd519..5f2107d 100644 --- a/auto-bid-server/package.json +++ b/auto-bid-server/package.json @@ -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", diff --git a/auto-bid-server/src/main.ts b/auto-bid-server/src/main.ts index fe8e9c6..a7623e6 100644 --- a/auto-bid-server/src/main.ts +++ b/auto-bid-server/src/main.ts @@ -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); + + 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 0000000..ff39f15 Binary files /dev/null and b/bid-extension/.DS_Store differ 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 0000000..c7de32f Binary files /dev/null and b/bid-extension/assets/icons/128.png differ diff --git a/bid-extension/assets/icons/16.png b/bid-extension/assets/icons/16.png new file mode 100644 index 0000000..46986c5 Binary files /dev/null and b/bid-extension/assets/icons/16.png differ diff --git a/bid-extension/assets/icons/32.png b/bid-extension/assets/icons/32.png new file mode 100644 index 0000000..b82bd11 Binary files /dev/null and b/bid-extension/assets/icons/32.png differ diff --git a/bid-extension/background.js b/bid-extension/background.js new file mode 100644 index 0000000..9e69979 --- /dev/null +++ b/bid-extension/background.js @@ -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); +}); 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();