update view admin
This commit is contained in:
parent
42f0a881f4
commit
d929366810
|
|
@ -0,0 +1,47 @@
|
|||
import { handleError, handleSuccess } from ".";
|
||||
import axios from "../lib/axios";
|
||||
import { IScrapConfig, IWebBid } from "../system/type";
|
||||
import { removeFalsyValues } from "../utils";
|
||||
|
||||
export const createScrapConfig = async (
|
||||
bid: Omit<
|
||||
IScrapConfig,
|
||||
"id" | "created_at" | "updated_at" | "scrap_items"
|
||||
> & { web_id: IWebBid["id"] }
|
||||
) => {
|
||||
const newData = removeFalsyValues(bid);
|
||||
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: "scrap-configs",
|
||||
withCredentials: true,
|
||||
method: "POST",
|
||||
data: newData,
|
||||
});
|
||||
|
||||
handleSuccess(data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateScrapConfig = async (scrapConfig: Partial<IScrapConfig>) => {
|
||||
const { search_url, keywords, id } = removeFalsyValues(scrapConfig);
|
||||
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: "scrap-configs/" + id,
|
||||
withCredentials: true,
|
||||
method: "PUT",
|
||||
data: { search_url, keywords },
|
||||
});
|
||||
|
||||
handleSuccess(data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
|
@ -217,26 +217,32 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
|
|||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box className="absolute top-2.5 left-2.5 flex items-center gap-2">
|
||||
<Badge
|
||||
color={payloadLoginStatus?.login_status ? "green" : "red"}
|
||||
size="xs"
|
||||
>
|
||||
{statusLabel()}
|
||||
</Badge>
|
||||
<Box className="absolute top-0 left-0 py-2 px-4 flex items-center gap-2 justify-between w-full">
|
||||
<Box className="flex items-center gap-2">
|
||||
<Badge
|
||||
color={payloadLoginStatus?.login_status ? "green" : "red"}
|
||||
size="xs"
|
||||
>
|
||||
{statusLabel()}
|
||||
</Badge>
|
||||
|
||||
<Badge
|
||||
color={stringToColor(
|
||||
isIBid(data)
|
||||
<Badge
|
||||
color={stringToColor(
|
||||
isIBid(data)
|
||||
? extractDomainSmart(data.web_bid.origin_url)
|
||||
: extractDomainSmart(data.origin_url)
|
||||
)}
|
||||
size="xs"
|
||||
>
|
||||
{isIBid(data)
|
||||
? extractDomainSmart(data.web_bid.origin_url)
|
||||
: extractDomainSmart(data.origin_url)
|
||||
)}
|
||||
size="xs"
|
||||
>
|
||||
{isIBid(data)
|
||||
? extractDomainSmart(data.web_bid.origin_url)
|
||||
: extractDomainSmart(data.origin_url)}
|
||||
</Badge>
|
||||
: extractDomainSmart(data.origin_url)}
|
||||
</Badge>
|
||||
</Box>
|
||||
|
||||
{isIBid(data) && moment(data.close_time).isSame(moment(), "day") && (
|
||||
<div className="w-[14px] h-[14px] rounded-full bg-green-600 animate-pulse"></div>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,163 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import {
|
||||
Button,
|
||||
LoadingOverlay,
|
||||
Modal,
|
||||
ModalProps,
|
||||
Textarea,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import _ from "lodash";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { z } from "zod";
|
||||
import { createScrapConfig, updateScrapConfig } from "../../apis/scrap";
|
||||
import { useConfirmStore } from "../../lib/zustand/use-confirm";
|
||||
import { IScrapConfig, IWebBid } from "../../system/type";
|
||||
export interface IScrapConfigModelProps extends ModalProps {
|
||||
data: IWebBid | null;
|
||||
onUpdated?: () => void;
|
||||
}
|
||||
|
||||
const schema = z.object({
|
||||
search_url: z
|
||||
.string()
|
||||
.url({ message: "Url is invalid" })
|
||||
.min(1, { message: "Url is required" }),
|
||||
keywords: z
|
||||
.string({ message: "Keyword is required" })
|
||||
.min(1, { message: "Keyword is required" })
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export default function ScrapConfigModal({
|
||||
data,
|
||||
onUpdated,
|
||||
...props
|
||||
}: IScrapConfigModelProps) {
|
||||
const form = useForm<IScrapConfig>({
|
||||
validate: zodResolver(schema),
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const prevData = useRef<IScrapConfig | null>(data?.scrap_config);
|
||||
|
||||
const { setConfirm } = useConfirmStore();
|
||||
|
||||
const handleSubmit = async (values: typeof form.values) => {
|
||||
if (data?.scrap_config) {
|
||||
setConfirm({
|
||||
title: "Update ?",
|
||||
message: `This config will be update`,
|
||||
handleOk: async () => {
|
||||
setLoading(true);
|
||||
const result = await updateScrapConfig({
|
||||
...values,
|
||||
id: data.scrap_config.id,
|
||||
});
|
||||
setLoading(false);
|
||||
|
||||
if (!result) return;
|
||||
|
||||
props.onClose();
|
||||
|
||||
if (onUpdated) {
|
||||
onUpdated();
|
||||
}
|
||||
},
|
||||
okButton: {
|
||||
color: "blue",
|
||||
value: "Update",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setLoading(true);
|
||||
const result = await createScrapConfig({
|
||||
...values,
|
||||
web_id: data?.id || 0,
|
||||
});
|
||||
setLoading(false);
|
||||
|
||||
if (!result) return;
|
||||
|
||||
props.onClose();
|
||||
|
||||
if (onUpdated) {
|
||||
onUpdated();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
if (!data) return;
|
||||
|
||||
form.setValues(data.scrap_config);
|
||||
|
||||
prevData.current = data.scrap_config;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.opened) {
|
||||
form.reset();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.opened]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="relative"
|
||||
classNames={{
|
||||
header: "!flex !item-center !justify-center w-full",
|
||||
}}
|
||||
{...props}
|
||||
size={"xl"}
|
||||
title={<span className="text-xl font-bold">Scrap config</span>}
|
||||
centered
|
||||
>
|
||||
<form
|
||||
onSubmit={form.onSubmit(handleSubmit)}
|
||||
className="grid grid-cols-2 gap-2.5"
|
||||
>
|
||||
<TextInput
|
||||
className="col-span-2"
|
||||
size="sm"
|
||||
label="Search url"
|
||||
withAsterisk
|
||||
description="Replace query keyword in url with phrase {{keyword}}"
|
||||
placeholder="https://www.abc.com/search?q={{keyword}}"
|
||||
{...form.getInputProps("search_url")}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
className="col-span-2"
|
||||
size="sm"
|
||||
label="Keywords"
|
||||
rows={4}
|
||||
placeholder="msg: Cisco"
|
||||
description={"Different keywords must be separated by commas."}
|
||||
{...form.getInputProps("keywords")}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={_.isEqual(form.getValues(), prevData.current)}
|
||||
className="col-span-2"
|
||||
type="submit"
|
||||
fullWidth
|
||||
size="sm"
|
||||
mt="md"
|
||||
>
|
||||
{data?.scrap_config ? "Update" : "Create"}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<LoadingOverlay
|
||||
visible={loading}
|
||||
zIndex={1000}
|
||||
overlayProps={{ blur: 2 }}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,109 +1,148 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Button, LoadingOverlay, Modal, ModalProps, PasswordInput, TextInput } from '@mantine/core';
|
||||
import { useForm, zodResolver } from '@mantine/form';
|
||||
import _ from 'lodash';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { updateWebBid } from '../../apis/web-bid';
|
||||
import { useConfirmStore } from '../../lib/zustand/use-confirm';
|
||||
import { IWebBid } from '../../system/type';
|
||||
import {
|
||||
Button,
|
||||
LoadingOverlay,
|
||||
Modal,
|
||||
ModalProps,
|
||||
PasswordInput,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import _ from "lodash";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { z } from "zod";
|
||||
import { updateWebBid } from "../../apis/web-bid";
|
||||
import { useConfirmStore } from "../../lib/zustand/use-confirm";
|
||||
import { IWebBid } from "../../system/type";
|
||||
export interface IWebBidModelProps extends ModalProps {
|
||||
data: IWebBid | null;
|
||||
onUpdated?: () => void;
|
||||
data: IWebBid | null;
|
||||
onUpdated?: () => void;
|
||||
}
|
||||
|
||||
const schema = z.object({
|
||||
username: z.string().min(1, { message: 'Username is required' }),
|
||||
password: z.string().min(6, { message: 'Password must be at least 6 characters long' }),
|
||||
username: z.string().min(1, { message: "Username is required" }),
|
||||
password: z
|
||||
.string()
|
||||
.min(6, { message: "Password must be at least 6 characters long" }),
|
||||
});
|
||||
|
||||
export default function WebAccountModal({ data, onUpdated, ...props }: IWebBidModelProps) {
|
||||
const form = useForm({
|
||||
validate: zodResolver(schema),
|
||||
});
|
||||
export default function WebAccountModal({
|
||||
data,
|
||||
onUpdated,
|
||||
...props
|
||||
}: IWebBidModelProps) {
|
||||
const form = useForm({
|
||||
validate: zodResolver(schema),
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const prevData = useRef<IWebBid | null>(data);
|
||||
const prevData = useRef<IWebBid | null>(data);
|
||||
|
||||
const { setConfirm } = useConfirmStore();
|
||||
const { setConfirm } = useConfirmStore();
|
||||
|
||||
const handleSubmit = async (values: typeof form.values) => {
|
||||
if (data) {
|
||||
setConfirm({
|
||||
title: 'Update ?',
|
||||
message: `This account will be update`,
|
||||
handleOk: async () => {
|
||||
setLoading(true);
|
||||
const result = await updateWebBid(values);
|
||||
setLoading(false);
|
||||
const handleSubmit = async (values: typeof form.values) => {
|
||||
if (data) {
|
||||
setConfirm({
|
||||
title: "Update ?",
|
||||
message: `This account will be update`,
|
||||
handleOk: async () => {
|
||||
setLoading(true);
|
||||
const result = await updateWebBid(values);
|
||||
setLoading(false);
|
||||
|
||||
if (!result) return;
|
||||
if (!result) return;
|
||||
|
||||
props.onClose();
|
||||
props.onClose();
|
||||
|
||||
if (onUpdated) {
|
||||
onUpdated();
|
||||
}
|
||||
},
|
||||
okButton: {
|
||||
color: 'blue',
|
||||
value: 'Update',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setLoading(true);
|
||||
const result = await updateWebBid(values);
|
||||
setLoading(false);
|
||||
if (onUpdated) {
|
||||
onUpdated();
|
||||
}
|
||||
},
|
||||
okButton: {
|
||||
color: "blue",
|
||||
value: "Update",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setLoading(true);
|
||||
const result = await updateWebBid(values);
|
||||
setLoading(false);
|
||||
|
||||
if (!result) return;
|
||||
if (!result) return;
|
||||
|
||||
props.onClose();
|
||||
props.onClose();
|
||||
|
||||
if (onUpdated) {
|
||||
onUpdated();
|
||||
}
|
||||
}
|
||||
};
|
||||
if (onUpdated) {
|
||||
onUpdated();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
if (!data) return;
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
if (!data) return;
|
||||
|
||||
form.setValues(data);
|
||||
form.setValues(data);
|
||||
|
||||
prevData.current = data;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data]);
|
||||
prevData.current = data;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.opened) {
|
||||
form.reset();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.opened]);
|
||||
useEffect(() => {
|
||||
if (!props.opened) {
|
||||
form.reset();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.opened]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="relative"
|
||||
classNames={{
|
||||
header: '!flex !item-center !justify-center w-full',
|
||||
}}
|
||||
{...props}
|
||||
size={'xl'}
|
||||
title={<span className="text-xl font-bold">Account</span>}
|
||||
centered
|
||||
return (
|
||||
<Modal
|
||||
className="relative"
|
||||
classNames={{
|
||||
header: "!flex !item-center !justify-center w-full",
|
||||
}}
|
||||
{...props}
|
||||
size={"xl"}
|
||||
title={<span className="text-xl font-bold">Account</span>}
|
||||
centered
|
||||
>
|
||||
<form
|
||||
onSubmit={form.onSubmit(handleSubmit)}
|
||||
className="grid grid-cols-2 gap-2.5"
|
||||
>
|
||||
<TextInput
|
||||
withAsterisk
|
||||
className="col-span-2"
|
||||
size="sm"
|
||||
label="Username"
|
||||
{...form.getInputProps("username")}
|
||||
/>
|
||||
<PasswordInput
|
||||
withAsterisk
|
||||
className="col-span-2"
|
||||
size="sm"
|
||||
label="Password"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={_.isEqual(form.getValues(), prevData.current)}
|
||||
className="col-span-2"
|
||||
type="submit"
|
||||
fullWidth
|
||||
size="sm"
|
||||
mt="md"
|
||||
>
|
||||
<form onSubmit={form.onSubmit(handleSubmit)} className="grid grid-cols-2 gap-2.5">
|
||||
<TextInput className="col-span-2" size="sm" label="Username" {...form.getInputProps('username')} />
|
||||
<PasswordInput className="col-span-2" size="sm" label="Password" {...form.getInputProps('password')} />
|
||||
{data ? "Update" : "Create"}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<Button disabled={_.isEqual(form.getValues(), prevData.current)} className="col-span-2" type="submit" fullWidth size="sm" mt="md">
|
||||
{data ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<LoadingOverlay visible={loading} zIndex={1000} overlayProps={{ blur: 2 }} />
|
||||
</Modal>
|
||||
);
|
||||
<LoadingOverlay
|
||||
visible={loading}
|
||||
zIndex={1000}
|
||||
overlayProps={{ blur: 2 }}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,12 +26,14 @@ const schema = {
|
|||
.number({ message: "Arrival offset seconds is required" })
|
||||
.refine((val) => val >= 60, {
|
||||
message: "Arrival offset seconds must be at least 60 seconds (1 minute)",
|
||||
}).optional(),
|
||||
early_tracking_seconds: z
|
||||
})
|
||||
.optional(),
|
||||
early_tracking_seconds: z
|
||||
.number({ message: "Early login seconds is required" })
|
||||
.refine((val) => val >= 600, {
|
||||
message: "Early login seconds must be at least 600 seconds (10 minute)",
|
||||
}).optional(),
|
||||
})
|
||||
.optional(),
|
||||
};
|
||||
|
||||
export default function WebBidModal({
|
||||
|
|
@ -56,11 +58,7 @@ export default function WebBidModal({
|
|||
message: `This web will be update`,
|
||||
handleOk: async () => {
|
||||
setLoading(true);
|
||||
console.log(
|
||||
"%csrc/components/web-bid/web-bid-modal.tsx:54 values",
|
||||
"color: #007acc;",
|
||||
values
|
||||
);
|
||||
|
||||
const result = await updateWebBid(values);
|
||||
setLoading(false);
|
||||
|
||||
|
|
@ -78,14 +76,19 @@ export default function WebBidModal({
|
|||
},
|
||||
});
|
||||
} else {
|
||||
const { url, origin_url, arrival_offset_seconds, early_tracking_seconds } = values;
|
||||
const {
|
||||
url,
|
||||
origin_url,
|
||||
arrival_offset_seconds,
|
||||
early_tracking_seconds,
|
||||
} = values;
|
||||
|
||||
setLoading(true);
|
||||
const result = await createWebBid({
|
||||
url,
|
||||
origin_url,
|
||||
arrival_offset_seconds,
|
||||
early_tracking_seconds
|
||||
early_tracking_seconds,
|
||||
} as IWebBid);
|
||||
setLoading(false);
|
||||
|
||||
|
|
@ -157,9 +160,9 @@ export default function WebBidModal({
|
|||
description="Note: that only integer minutes are accepted."
|
||||
className="col-span-2"
|
||||
size="sm"
|
||||
label={`Arrival offset seconds (${
|
||||
formatTimeFromMinutes(form.getValues()["arrival_offset_seconds"] / 60)
|
||||
})`}
|
||||
label={`Arrival offset seconds (${formatTimeFromMinutes(
|
||||
form.getValues()["arrival_offset_seconds"] / 60
|
||||
)})`}
|
||||
placeholder="msg: 300"
|
||||
{...form.getInputProps("arrival_offset_seconds")}
|
||||
/>
|
||||
|
|
@ -167,9 +170,9 @@ export default function WebBidModal({
|
|||
description="Note: that only integer minutes are accepted."
|
||||
className="col-span-2"
|
||||
size="sm"
|
||||
label={`Early tracking seconds (${
|
||||
formatTimeFromMinutes(form.getValues()["early_tracking_seconds"] / 60)
|
||||
})`}
|
||||
label={`Early tracking seconds (${formatTimeFromMinutes(
|
||||
form.getValues()["early_tracking_seconds"] / 60
|
||||
)})`}
|
||||
placeholder="msg: 600"
|
||||
{...form.getInputProps("early_tracking_seconds")}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ export default function Bids() {
|
|||
},
|
||||
},
|
||||
{
|
||||
key: "close_time",
|
||||
key: "close_time_ts",
|
||||
title: "Close time",
|
||||
typeFilter: "date",
|
||||
renderRow(row) {
|
||||
|
|
|
|||
|
|
@ -1,253 +1,306 @@
|
|||
import { ActionIcon, Badge, Box, Menu, Text } from '@mantine/core';
|
||||
import { IconAd, IconAdOff, IconEdit, IconMenu, IconTrash, IconUserEdit } from '@tabler/icons-react';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { deletesWebBid, deleteWebBid, getWebBids, updateWebBid } from '../apis/web-bid';
|
||||
import Table from '../lib/table/table';
|
||||
import { IColumn, TRefTableFn } from '../lib/table/type';
|
||||
import { useConfirmStore } from '../lib/zustand/use-confirm';
|
||||
import { IWebBid } from '../system/type';
|
||||
import { formatTime } from '../utils';
|
||||
import { WebAccountModal, WebBidModal } from '../components/web-bid';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { ActionIcon, Badge, Box, Menu, Text } from "@mantine/core";
|
||||
import {
|
||||
IconAd,
|
||||
IconAdOff,
|
||||
IconEdit,
|
||||
IconMenu,
|
||||
IconSettingsCode,
|
||||
IconTrash,
|
||||
IconUserEdit,
|
||||
} from "@tabler/icons-react";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
deletesWebBid,
|
||||
deleteWebBid,
|
||||
getWebBids,
|
||||
updateWebBid,
|
||||
} from "../apis/web-bid";
|
||||
import Table from "../lib/table/table";
|
||||
import { IColumn, TRefTableFn } from "../lib/table/type";
|
||||
import { useConfirmStore } from "../lib/zustand/use-confirm";
|
||||
import { IWebBid } from "../system/type";
|
||||
import { formatTime } from "../utils";
|
||||
import { WebAccountModal, WebBidModal } from "../components/web-bid";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import ScrapConfigModal from "../components/web-bid/scrap-config.modal";
|
||||
|
||||
export default function WebBids() {
|
||||
const refTableFn: TRefTableFn<IWebBid> = useRef({});
|
||||
const refTableFn: TRefTableFn<IWebBid> = useRef({});
|
||||
|
||||
const [clickData, setClickData] = useState<IWebBid | null>(null);
|
||||
const [clickData, setClickData] = useState<IWebBid | null>(null);
|
||||
|
||||
const { setConfirm } = useConfirmStore();
|
||||
const { setConfirm } = useConfirmStore();
|
||||
|
||||
const [webBidOpened, webBidModal] = useDisclosure(false);
|
||||
const [webAccountOpened, webAccountModal] = useDisclosure(false);
|
||||
const [webBidOpened, webBidModal] = useDisclosure(false);
|
||||
const [webAccountOpened, webAccountModal] = useDisclosure(false);
|
||||
const [scrapConfigOpened, scrapConfigModal] = useDisclosure(false);
|
||||
|
||||
const columns: IColumn<IWebBid>[] = [
|
||||
{
|
||||
key: 'id',
|
||||
title: 'ID',
|
||||
typeFilter: 'number',
|
||||
},
|
||||
{
|
||||
key: 'origin_url',
|
||||
title: 'Domain',
|
||||
typeFilter: 'text',
|
||||
},
|
||||
{
|
||||
key: 'url',
|
||||
title: 'Tracking url',
|
||||
typeFilter: 'text',
|
||||
renderRow(row) {
|
||||
return <Text>{row.url || 'None'}</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'active',
|
||||
title: 'Status',
|
||||
typeFilter: 'text',
|
||||
renderRow(row) {
|
||||
return (
|
||||
<Box className="flex items-center justify-center">
|
||||
<Badge color={row.active ? 'green' : 'red'} size="sm">
|
||||
{row.active ? 'Enable' : 'Disable'}
|
||||
</Badge>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
key: 'created_at',
|
||||
title: 'Created at',
|
||||
typeFilter: 'none',
|
||||
renderRow(row) {
|
||||
return <span>{formatTime(row.created_at)}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'updated_at',
|
||||
title: 'Update at',
|
||||
typeFilter: 'none',
|
||||
renderRow(row) {
|
||||
return <span>{formatTime(row.updated_at)}</span>;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const handleDelete = (data: IWebBid) => {
|
||||
setConfirm({
|
||||
title: 'Delete ?',
|
||||
message: 'This web will be delete',
|
||||
handleOk: async () => {
|
||||
await deleteWebBid(data);
|
||||
|
||||
if (refTableFn.current?.fetchData) {
|
||||
refTableFn.current.fetchData();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggle = async (data: IWebBid) => {
|
||||
setConfirm({
|
||||
title: (data.active ? 'Disable ' : 'Enable ') + 'ID: ' + data.id,
|
||||
message: 'This web will be ' + (data.active ? 'disable ' : 'enable '),
|
||||
handleOk: async () => {
|
||||
await updateWebBid({ ...data, active: !data.active || false });
|
||||
|
||||
if (refTableFn.current?.fetchData) {
|
||||
refTableFn.current.fetchData();
|
||||
}
|
||||
},
|
||||
okButton: {
|
||||
value: data.active ? 'Disable ' : 'Enable ',
|
||||
color: data.active ? 'red' : 'blue',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const table = useMemo(() => {
|
||||
const columns: IColumn<IWebBid>[] = [
|
||||
{
|
||||
key: "id",
|
||||
title: "ID",
|
||||
typeFilter: "number",
|
||||
},
|
||||
{
|
||||
key: "origin_url",
|
||||
title: "Domain",
|
||||
typeFilter: "text",
|
||||
},
|
||||
{
|
||||
key: "url",
|
||||
title: "Tracking url",
|
||||
typeFilter: "text",
|
||||
renderRow(row) {
|
||||
return <Text>{row.url || "None"}</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "active",
|
||||
title: "Status",
|
||||
typeFilter: "text",
|
||||
renderRow(row) {
|
||||
return (
|
||||
<Table
|
||||
actionsOptions={{
|
||||
actions: [
|
||||
{
|
||||
key: 'add',
|
||||
title: 'Add',
|
||||
callback: () => {
|
||||
webBidModal.open();
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
title: 'Delete',
|
||||
callback: (data) => {
|
||||
if (!data.length) return;
|
||||
setConfirm({
|
||||
title: 'Delete',
|
||||
message: `${data.length} will be delete`,
|
||||
handleOk: async () => {
|
||||
const result = await deletesWebBid(data);
|
||||
|
||||
if (!result) return;
|
||||
if (refTableFn.current.fetchData) {
|
||||
refTableFn.current.fetchData();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
disabled: (data) => data.length <= 0,
|
||||
},
|
||||
],
|
||||
}}
|
||||
refTableFn={refTableFn}
|
||||
striped
|
||||
showLoading={true}
|
||||
highlightOnHover
|
||||
styleDefaultHead={{
|
||||
justifyContent: 'flex-start',
|
||||
width: 'fit-content',
|
||||
}}
|
||||
options={{
|
||||
query: getWebBids,
|
||||
pathToData: 'data.data',
|
||||
keyOptions: {
|
||||
last_page: 'lastPage',
|
||||
per_page: 'perPage',
|
||||
from: 'from',
|
||||
to: 'to',
|
||||
total: 'total',
|
||||
},
|
||||
}}
|
||||
rows={[]}
|
||||
withColumnBorders
|
||||
showChooses={true}
|
||||
withTableBorder
|
||||
columns={columns}
|
||||
actions={{
|
||||
title: <Box className="w-full text-center">Action</Box>,
|
||||
body: (row) => {
|
||||
return (
|
||||
<Menu shadow="md" width={200}>
|
||||
<Menu.Target>
|
||||
<Box className="flex w-full items-center justify-center">
|
||||
<ActionIcon size="sm" variant="light">
|
||||
<IconMenu size={14} />
|
||||
</ActionIcon>
|
||||
</Box>
|
||||
</Menu.Target>
|
||||
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
setClickData(row);
|
||||
webBidModal.open();
|
||||
}}
|
||||
leftSection={<IconEdit size={14} />}
|
||||
>
|
||||
Edit
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
setClickData(row);
|
||||
webAccountModal.open();
|
||||
}}
|
||||
leftSection={<IconUserEdit size={14} />}
|
||||
>
|
||||
Account
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item onClick={() => handleToggle(row)} leftSection={row.active ? <IconAdOff size={14} /> : <IconAd size={14} />}>
|
||||
{row.active ? 'Disable' : 'Enable'}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item onClick={() => handleDelete(row)} leftSection={<IconTrash color="red" size={14} />}>
|
||||
Delete
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
},
|
||||
}}
|
||||
rowKey="id"
|
||||
/>
|
||||
<Box className="flex items-center justify-center">
|
||||
<Badge color={row.active ? "green" : "red"} size="sm">
|
||||
{row.active ? "Enable" : "Disable"}
|
||||
</Badge>
|
||||
</Box>
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
key: "created_at",
|
||||
title: "Created at",
|
||||
typeFilter: "none",
|
||||
renderRow(row) {
|
||||
return <span>{formatTime(row.created_at)}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "updated_at",
|
||||
title: "Update at",
|
||||
typeFilter: "none",
|
||||
renderRow(row) {
|
||||
return <span>{formatTime(row.updated_at)}</span>;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const handleDelete = (data: IWebBid) => {
|
||||
setConfirm({
|
||||
title: "Delete ?",
|
||||
message: "This web will be delete",
|
||||
handleOk: async () => {
|
||||
await deleteWebBid(data);
|
||||
|
||||
if (refTableFn.current?.fetchData) {
|
||||
refTableFn.current.fetchData();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggle = async (data: IWebBid) => {
|
||||
setConfirm({
|
||||
title: (data.active ? "Disable " : "Enable ") + "ID: " + data.id,
|
||||
message: "This web will be " + (data.active ? "disable " : "enable "),
|
||||
handleOk: async () => {
|
||||
await updateWebBid({ ...data, active: !data.active || false });
|
||||
|
||||
if (refTableFn.current?.fetchData) {
|
||||
refTableFn.current.fetchData();
|
||||
}
|
||||
},
|
||||
okButton: {
|
||||
value: data.active ? "Disable " : "Enable ",
|
||||
color: data.active ? "red" : "blue",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const table = useMemo(() => {
|
||||
return (
|
||||
<Box>
|
||||
{table}
|
||||
<Table
|
||||
actionsOptions={{
|
||||
actions: [
|
||||
{
|
||||
key: "add",
|
||||
title: "Add",
|
||||
callback: () => {
|
||||
webBidModal.open();
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
title: "Delete",
|
||||
callback: (data) => {
|
||||
if (!data.length) return;
|
||||
setConfirm({
|
||||
title: "Delete",
|
||||
message: `${data.length} will be delete`,
|
||||
handleOk: async () => {
|
||||
const result = await deletesWebBid(data);
|
||||
|
||||
<WebBidModal
|
||||
data={clickData}
|
||||
opened={webBidOpened}
|
||||
onClose={() => {
|
||||
webBidModal.close();
|
||||
setClickData(null);
|
||||
}}
|
||||
onUpdated={() => {
|
||||
setClickData(null);
|
||||
|
||||
if (refTableFn.current?.fetchData) {
|
||||
refTableFn.current.fetchData();
|
||||
if (!result) return;
|
||||
if (refTableFn.current.fetchData) {
|
||||
refTableFn.current.fetchData();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
},
|
||||
});
|
||||
},
|
||||
disabled: (data) => data.length <= 0,
|
||||
},
|
||||
],
|
||||
}}
|
||||
refTableFn={refTableFn}
|
||||
striped
|
||||
showLoading={true}
|
||||
highlightOnHover
|
||||
styleDefaultHead={{
|
||||
justifyContent: "flex-start",
|
||||
width: "fit-content",
|
||||
}}
|
||||
options={{
|
||||
query: getWebBids,
|
||||
pathToData: "data.data",
|
||||
keyOptions: {
|
||||
last_page: "lastPage",
|
||||
per_page: "perPage",
|
||||
from: "from",
|
||||
to: "to",
|
||||
total: "total",
|
||||
},
|
||||
}}
|
||||
rows={[]}
|
||||
withColumnBorders
|
||||
showChooses={true}
|
||||
withTableBorder
|
||||
columns={columns}
|
||||
actions={{
|
||||
title: <Box className="w-full text-center">Action</Box>,
|
||||
body: (row) => {
|
||||
return (
|
||||
<Menu shadow="md" width={200}>
|
||||
<Menu.Target>
|
||||
<Box className="flex w-full items-center justify-center">
|
||||
<ActionIcon size="sm" variant="light">
|
||||
<IconMenu size={14} />
|
||||
</ActionIcon>
|
||||
</Box>
|
||||
</Menu.Target>
|
||||
|
||||
<WebAccountModal
|
||||
data={clickData}
|
||||
opened={webAccountOpened}
|
||||
onClose={() => {
|
||||
webAccountModal.close();
|
||||
setClickData(null);
|
||||
}}
|
||||
onUpdated={() => {
|
||||
setClickData(null);
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
setClickData(row);
|
||||
webBidModal.open();
|
||||
}}
|
||||
leftSection={<IconEdit size={14} />}
|
||||
>
|
||||
Edit
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
setClickData(row);
|
||||
webAccountModal.open();
|
||||
}}
|
||||
leftSection={<IconUserEdit size={14} />}
|
||||
>
|
||||
Account
|
||||
</Menu.Item>
|
||||
|
||||
if (refTableFn.current?.fetchData) {
|
||||
refTableFn.current.fetchData();
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
setClickData(row);
|
||||
scrapConfigModal.open();
|
||||
}}
|
||||
leftSection={<IconSettingsCode size={14} />}
|
||||
>
|
||||
Scrap config
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
onClick={() => handleToggle(row)}
|
||||
leftSection={
|
||||
row.active ? (
|
||||
<IconAdOff size={14} />
|
||||
) : (
|
||||
<IconAd size={14} />
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
>
|
||||
{row.active ? "Disable" : "Enable"}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
onClick={() => handleDelete(row)}
|
||||
leftSection={<IconTrash color="red" size={14} />}
|
||||
>
|
||||
Delete
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
},
|
||||
}}
|
||||
rowKey="id"
|
||||
/>
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{table}
|
||||
|
||||
<WebBidModal
|
||||
data={clickData}
|
||||
opened={webBidOpened}
|
||||
onClose={() => {
|
||||
webBidModal.close();
|
||||
setClickData(null);
|
||||
}}
|
||||
onUpdated={() => {
|
||||
setClickData(null);
|
||||
|
||||
if (refTableFn.current?.fetchData) {
|
||||
refTableFn.current.fetchData();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<WebAccountModal
|
||||
data={clickData}
|
||||
opened={webAccountOpened}
|
||||
onClose={() => {
|
||||
webAccountModal.close();
|
||||
setClickData(null);
|
||||
}}
|
||||
onUpdated={() => {
|
||||
setClickData(null);
|
||||
|
||||
if (refTableFn.current?.fetchData) {
|
||||
refTableFn.current.fetchData();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<ScrapConfigModal
|
||||
data={clickData}
|
||||
opened={scrapConfigOpened}
|
||||
onClose={() => {
|
||||
scrapConfigModal.close();
|
||||
setClickData(null);
|
||||
}}
|
||||
onUpdated={() => {
|
||||
setClickData(null);
|
||||
|
||||
if (refTableFn.current?.fetchData) {
|
||||
refTableFn.current.fetchData();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,79 +1,94 @@
|
|||
export interface IAdmin extends ITimestamp {
|
||||
id: number;
|
||||
email: string;
|
||||
username: string;
|
||||
fullname: string;
|
||||
password?: string;
|
||||
is_system_account: boolean;
|
||||
permissions: [];
|
||||
id: number;
|
||||
email: string;
|
||||
username: string;
|
||||
fullname: string;
|
||||
password?: string;
|
||||
is_system_account: boolean;
|
||||
permissions: [];
|
||||
}
|
||||
|
||||
export interface IKey extends ITimestamp {
|
||||
id: number;
|
||||
client_key: string;
|
||||
id: number;
|
||||
client_key: string;
|
||||
}
|
||||
|
||||
export interface ITimestamp {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface IHistory extends ITimestamp {
|
||||
id: number;
|
||||
price: number;
|
||||
id: number;
|
||||
price: number;
|
||||
}
|
||||
|
||||
export interface IOutBidLog extends ITimestamp {
|
||||
id: number;
|
||||
model: string;
|
||||
lot_id: string;
|
||||
out_price: number;
|
||||
raw_data: string;
|
||||
id: number;
|
||||
model: string;
|
||||
lot_id: string;
|
||||
out_price: number;
|
||||
raw_data: string;
|
||||
}
|
||||
|
||||
export interface IScrapConfig extends ITimestamp {
|
||||
id: number;
|
||||
search_url: string;
|
||||
keywords: string;
|
||||
scrap_items: IScrapItem[];
|
||||
}
|
||||
|
||||
export interface IScrapItem extends ITimestamp {
|
||||
id: number;
|
||||
url: string;
|
||||
model: string;
|
||||
image_url: string | null;
|
||||
keyword: string;
|
||||
}
|
||||
|
||||
export interface IWebBid extends ITimestamp {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
id: number;
|
||||
origin_url: string;
|
||||
url: string | null;
|
||||
username: string | null;
|
||||
password: string | null;
|
||||
active: boolean;
|
||||
arrival_offset_seconds: number;
|
||||
early_tracking_seconds: number;
|
||||
snapshot_at: string | null
|
||||
children: IBid[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
id: number;
|
||||
origin_url: string;
|
||||
url: string | null;
|
||||
username: string | null;
|
||||
password: string | null;
|
||||
active: boolean;
|
||||
arrival_offset_seconds: number;
|
||||
early_tracking_seconds: number;
|
||||
snapshot_at: string | null;
|
||||
children: IBid[];
|
||||
scrap_config: IScrapConfig;
|
||||
}
|
||||
|
||||
export interface IBid extends ITimestamp {
|
||||
id: number;
|
||||
max_price: number;
|
||||
reserve_price: number;
|
||||
current_price: number;
|
||||
name: string | null;
|
||||
quantity: number;
|
||||
url: string;
|
||||
model: string;
|
||||
lot_id: string;
|
||||
plus_price: number;
|
||||
close_time: string | null;
|
||||
start_bid_time: string | null;
|
||||
first_bid: boolean;
|
||||
status: 'biding' | 'out-bid' | 'win-bid';
|
||||
histories: IHistory[];
|
||||
web_bid: IWebBid;
|
||||
id: number;
|
||||
max_price: number;
|
||||
reserve_price: number;
|
||||
current_price: number;
|
||||
name: string | null;
|
||||
quantity: number;
|
||||
url: string;
|
||||
model: string;
|
||||
lot_id: string;
|
||||
plus_price: number;
|
||||
close_time: string | null;
|
||||
close_time_ts: string | null;
|
||||
start_bid_time: string | null;
|
||||
first_bid: boolean;
|
||||
status: "biding" | "out-bid" | "win-bid";
|
||||
histories: IHistory[];
|
||||
web_bid: IWebBid;
|
||||
}
|
||||
|
||||
export interface IPermission extends ITimestamp {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
export interface ISendMessageHistory extends ITimestamp {
|
||||
id: number;
|
||||
message: string;
|
||||
bid: IBid;
|
||||
id: number;
|
||||
message: string;
|
||||
bid: IBid;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"createdAt":1747292824357}
|
||||
{"createdAt":1747701959077}
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
"@nestjs/websockets": "^11.0.11",
|
||||
"axios": "^1.8.3",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cheerio": "^1.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"cookie": "^1.0.2",
|
||||
|
|
@ -4165,6 +4166,12 @@
|
|||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
|
|
@ -4422,6 +4429,48 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cheerio": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz",
|
||||
"integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cheerio-select": "^2.1.0",
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.1.0",
|
||||
"encoding-sniffer": "^0.2.0",
|
||||
"htmlparser2": "^9.1.0",
|
||||
"parse5": "^7.1.2",
|
||||
"parse5-htmlparser2-tree-adapter": "^7.0.0",
|
||||
"parse5-parser-stream": "^7.1.2",
|
||||
"undici": "^6.19.5",
|
||||
"whatwg-mimetype": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio-select": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
|
||||
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-select": "^5.1.0",
|
||||
"css-what": "^6.1.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
|
|
@ -4923,6 +4972,34 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
||||
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-what": "^6.1.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"domutils": "^3.0.1",
|
||||
"nth-check": "^2.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/css-what": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
|
||||
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||
|
|
@ -5138,6 +5215,61 @@
|
|||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"entities": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.7",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||
|
|
@ -5260,6 +5392,31 @@
|
|||
"iconv-lite": "^0.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/encoding-sniffer": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz",
|
||||
"integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.6.3",
|
||||
"whatwg-encoding": "^3.1.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/encoding-sniffer/node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/encoding/node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
|
|
@ -5341,6 +5498,18 @@
|
|||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||
|
|
@ -6773,6 +6942,25 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
|
||||
"integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.1.0",
|
||||
"entities": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
|
|
@ -8900,6 +9088,18 @@
|
|||
"set-blocking": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nth-check": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
|
|
@ -9109,6 +9309,55 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5-htmlparser2-tree-adapter": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
|
||||
"integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domhandler": "^5.0.3",
|
||||
"parse5": "^7.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5-parser-stream": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
|
||||
"integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parse5": "^7.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5/node_modules/entities": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz",
|
||||
"integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
|
|
@ -11532,6 +11781,15 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.21.3",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
|
||||
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
|
|
@ -11842,6 +12100,39 @@
|
|||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iconv-lite": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-encoding/node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
"@nestjs/websockets": "^11.0.11",
|
||||
"axios": "^1.8.3",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cheerio": "^1.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"cookie": "^1.0.2",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
import { AuthorizationMiddleware } from './modules/admins/middlewares/authorization.middleware';
|
||||
import { ClientAuthenticationMiddleware } from './modules/auth/middlewares/client-authentication.middleware';
|
||||
import { NotificationModule } from './modules/notification/notification.module';
|
||||
import { ScrapsModule } from './modules/scraps/scraps.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -24,6 +25,7 @@ import { NotificationModule } from './modules/notification/notification.module';
|
|||
AuthModule,
|
||||
AdminsModule,
|
||||
NotificationModule,
|
||||
ScrapsModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [],
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ export class Bid extends Timestamp {
|
|||
@Column({ default: null, nullable: true })
|
||||
close_time: string;
|
||||
|
||||
@Column({ default: null, nullable: true })
|
||||
close_time_ts: Date | null;
|
||||
|
||||
@Column({ default: null, nullable: true })
|
||||
start_bid_time: string;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,15 @@
|
|||
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { Timestamp } from './timestamp';
|
||||
import { Bid } from './bid.entity';
|
||||
import { Exclude } from 'class-transformer';
|
||||
import { ScrapItem } from '@/modules/scraps/entities/scrap-item.entity';
|
||||
import { ScrapConfig } from '@/modules/scraps/entities/scrap-config.entity';
|
||||
|
||||
@Entity('web_bids')
|
||||
export class WebBid extends Timestamp {
|
||||
|
|
@ -37,4 +45,7 @@ export class WebBid extends Timestamp {
|
|||
cascade: true,
|
||||
})
|
||||
children: Bid[];
|
||||
|
||||
@OneToOne(() => ScrapConfig, (scrap) => scrap.web_bid)
|
||||
scrap_config: ScrapConfig;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ export class BidsService {
|
|||
sortableColumns: [
|
||||
'id',
|
||||
'close_time',
|
||||
'close_time_ts',
|
||||
'first_bid',
|
||||
'model',
|
||||
'lot_id',
|
||||
|
|
@ -120,7 +121,9 @@ export class BidsService {
|
|||
|
||||
await this.emitAllBidEvent();
|
||||
|
||||
return AppResponse.toResponse(response ? response(result) : plainToClass(Bid, result));
|
||||
return AppResponse.toResponse(
|
||||
response ? response(result) : plainToClass(Bid, result),
|
||||
);
|
||||
}
|
||||
|
||||
async update(id: Bid['id'], data: UpdateBidDto) {
|
||||
|
|
@ -266,6 +269,7 @@ export class BidsService {
|
|||
new Date(close_time).getTime() > new Date(bid.close_time).getTime()
|
||||
) {
|
||||
bid.close_time = close_time;
|
||||
bid.close_time_ts = new Date(close_time);
|
||||
}
|
||||
|
||||
// Nếu chưa có `model` nhưng dữ liệu mới có model, thì cập nhật model
|
||||
|
|
@ -550,11 +554,17 @@ export class BidsService {
|
|||
}
|
||||
|
||||
async getBidByModel(model: string) {
|
||||
|
||||
console.log('%csrc/modules/bids/services/bids.service.ts:554 model', 'color: #007acc;', model);
|
||||
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});
|
||||
if (!bid)
|
||||
return AppResponse.toResponse(null, {
|
||||
status_code: HttpStatus.NOT_FOUND,
|
||||
});
|
||||
|
||||
return AppResponse.toResponse(plainToClass(Bid, bid));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export class TasksService {
|
|||
) {}
|
||||
|
||||
@Cron(CronExpression.EVERY_MINUTE)
|
||||
async handleCron() {
|
||||
async handleResetTool() {
|
||||
const bids = await this.bidsService.bidsRepo.find({
|
||||
where: { status: 'biding' },
|
||||
select: ['close_time', 'created_at', 'start_bid_time', 'id', 'lot_id'],
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@ export class WebBidsService {
|
|||
filterableColumns,
|
||||
defaultSortBy: [['id', 'DESC']],
|
||||
maxLimit: 100,
|
||||
relations: {
|
||||
scrap_config: true,
|
||||
},
|
||||
});
|
||||
|
||||
return AppResponse.toPagination<WebBid>(data, true, WebBid);
|
||||
|
|
@ -66,6 +69,11 @@ export class WebBidsService {
|
|||
children: { status: 'biding' },
|
||||
},
|
||||
relations: { children: { histories: true, web_bid: true } },
|
||||
order: {
|
||||
children: {
|
||||
close_time_ts: 'ASC',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common';
|
||||
import { CreateScrapConfigDto } from '../dto/scrap-config/create-scrap-config';
|
||||
import { ScrapConfigsService } from '../services/scrap-config.service';
|
||||
import { UpdateScrapConfigDto } from '../dto/scrap-config/update-scrap-config';
|
||||
import { ScrapConfig } from '../entities/scrap-config.entity';
|
||||
import { IsNull, Not } from 'typeorm';
|
||||
import { ScrapItemsService } from '../services/scrap-item-config.service';
|
||||
|
||||
@Controller('admin/scrap-configs')
|
||||
export class ScrapConfigsController {
|
||||
constructor(
|
||||
private readonly scrapConfigsService: ScrapConfigsService,
|
||||
private readonly scrapItemsService: ScrapItemsService,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
async create(@Body() data: CreateScrapConfigDto) {
|
||||
return await this.scrapConfigsService.create(data);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
async update(
|
||||
@Param('id') id: ScrapConfig['id'],
|
||||
@Body() data: UpdateScrapConfigDto,
|
||||
) {
|
||||
return await this.scrapConfigsService.update(id, data);
|
||||
}
|
||||
|
||||
@Get()
|
||||
async test() {
|
||||
const scrapConfigs = await this.scrapConfigsService.scrapConfigRepo.find({
|
||||
where: {
|
||||
search_url: Not(IsNull()),
|
||||
keywords: Not(IsNull()),
|
||||
},
|
||||
relations: {
|
||||
web_bid: true,
|
||||
},
|
||||
});
|
||||
|
||||
const models = this.scrapConfigsService.scrapModels(scrapConfigs);
|
||||
|
||||
await Promise.allSettled(
|
||||
models.map(async (item) => {
|
||||
await item.action();
|
||||
|
||||
Object.keys(item.results).forEach(async (key) => {
|
||||
const data = item.results[key];
|
||||
|
||||
await this.scrapItemsService.scrapItemRepo.upsert(data, [
|
||||
'model',
|
||||
'scrap_config',
|
||||
]);
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return { a: 'abc' };
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { IsNumber, IsOptional, IsString, IsUrl } from 'class-validator';
|
||||
|
||||
export class CreateScrapConfigDto {
|
||||
@IsUrl()
|
||||
search_url: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
keywords: string;
|
||||
|
||||
@IsNumber()
|
||||
web_id: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateScrapConfigDto } from './create-scrap-config';
|
||||
|
||||
export class UpdateScrapConfigDto extends PartialType(CreateScrapConfigDto) {}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { WebBid } from '@/modules/bids/entities/wed-bid.entity';
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
} from 'typeorm';
|
||||
import { ScrapItem } from './scrap-item.entity';
|
||||
import { Timestamp } from './timestamp';
|
||||
|
||||
@Entity('scrap-configs')
|
||||
export class ScrapConfig extends Timestamp {
|
||||
@PrimaryGeneratedColumn('increment')
|
||||
id: number;
|
||||
|
||||
@Column({ unique: true })
|
||||
search_url: string;
|
||||
|
||||
@Column({ default: 'cisco' })
|
||||
keywords: string;
|
||||
|
||||
@OneToOne(() => WebBid, (web) => web.scrap_config, { onDelete: 'CASCADE' })
|
||||
@JoinColumn()
|
||||
web_bid: WebBid;
|
||||
|
||||
@OneToMany(() => ScrapItem, (web) => web.scrap_config, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
scrap_items: ScrapItem[];
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import {
|
||||
Column,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
} from 'typeorm';
|
||||
import { ScrapConfig } from './scrap-config.entity';
|
||||
import { Timestamp } from './timestamp';
|
||||
|
||||
@Entity('scrap-items')
|
||||
@Unique(['model', 'scrap_config'])
|
||||
export class ScrapItem extends Timestamp {
|
||||
@PrimaryGeneratedColumn('increment')
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column()
|
||||
url: string;
|
||||
|
||||
@Column()
|
||||
model: string;
|
||||
|
||||
@Column({ nullable: true, default: null })
|
||||
image_url: string | null;
|
||||
|
||||
@Column({ nullable: true, default: null })
|
||||
keyword: string;
|
||||
|
||||
@Column({ nullable: true, default: null })
|
||||
current_price: number;
|
||||
|
||||
@ManyToOne(() => ScrapConfig, (web) => web.scrap_items, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
scrap_config: ScrapConfig;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
export abstract class Timestamp {
|
||||
@CreateDateColumn({ type: 'timestamp', name: 'created_at' })
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamp', name: 'updated_at' })
|
||||
updated_at: Date;
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import axios from 'axios';
|
||||
import { ScrapInterface } from '../../scrap-interface';
|
||||
import { ScrapModel } from '../../scrap-model';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { Element } from 'domhandler';
|
||||
import { ScrapItem } from '@/modules/scraps/entities/scrap-item.entity';
|
||||
import { extractModelId, extractNumber } from '@/ultils';
|
||||
export class GraysScrapModel extends ScrapModel {
|
||||
action = async () => {
|
||||
const urls = this.extractUrls();
|
||||
|
||||
console.log({ urls });
|
||||
const results = await Promise.allSettled(
|
||||
urls.map(async (item) => ({
|
||||
html: (await axios.get(item.url)).data,
|
||||
keyword: item.keyword,
|
||||
})),
|
||||
);
|
||||
|
||||
const htmlsData = results
|
||||
.filter((res) => res.status === 'fulfilled')
|
||||
.map((res) => (res as PromiseFulfilledResult<any>).value);
|
||||
|
||||
await Promise.all(
|
||||
htmlsData.map(async (cur) => {
|
||||
try {
|
||||
const data = await this.getItemsInHtml(cur);
|
||||
const results = this.filterItemByKeyword(cur.keyword, data);
|
||||
|
||||
this.results[cur.keyword] = results; // hoặc push như gợi ý trên
|
||||
return results;
|
||||
} catch (err) {
|
||||
console.error(`❌ Error with keyword ${cur.keyword}:`, err);
|
||||
return []; // fallback
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
getItemsInHtml = async ({
|
||||
html,
|
||||
keyword,
|
||||
}: {
|
||||
html: string;
|
||||
keyword: string;
|
||||
}) => {
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const container = $('.sc-102aeaf3-1.eYPitT');
|
||||
|
||||
const items = container.children('div').toArray();
|
||||
|
||||
const results = items.map((el) => {
|
||||
const url = $(el).find('.sc-pKqro.sc-gFnajm.gqkMpZ.dzWUkJ').attr('href');
|
||||
|
||||
return {
|
||||
name: $(el).find('.sc-jlGgGc.dJRywx').text().trim(),
|
||||
image_url: $(el).find('img.sc-gtJxfw.jbgdlx').attr('src'),
|
||||
model: extractModelId(url),
|
||||
keyword,
|
||||
url,
|
||||
current_price: extractNumber(
|
||||
$(el).find('.sc-ijDOKB.sc-bStcSt.ikmQUw.eEycyP').text(),
|
||||
),
|
||||
scrap_config: { id: this.scrap_config_id },
|
||||
} as ScrapItem;
|
||||
});
|
||||
|
||||
return results;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { ScrapItem } from '../entities/scrap-item.entity';
|
||||
|
||||
export interface ScrapInterface {
|
||||
getItemsInHtml: (data: {
|
||||
html: string;
|
||||
keyword: string;
|
||||
}) => Promise<ScrapItem[]>;
|
||||
|
||||
action: () => Promise<void>;
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import { ScrapConfig } from '../entities/scrap-config.entity';
|
||||
import { ScrapItem } from '../entities/scrap-item.entity';
|
||||
import { ScrapInterface } from './scrap-interface';
|
||||
import { Element } from 'domhandler';
|
||||
export class ScrapModel implements ScrapInterface {
|
||||
protected keywords: string;
|
||||
protected search_url: string;
|
||||
protected scrap_config_id: ScrapConfig['id'];
|
||||
public results: Record<string, ScrapItem[]> = {};
|
||||
|
||||
constructor({
|
||||
keywords,
|
||||
search_url,
|
||||
scrap_config_id,
|
||||
}: {
|
||||
keywords: string;
|
||||
search_url: string;
|
||||
scrap_config_id: ScrapConfig['id'];
|
||||
}) {
|
||||
this.keywords = keywords;
|
||||
this.search_url = search_url;
|
||||
this.scrap_config_id = scrap_config_id;
|
||||
}
|
||||
|
||||
protected buildUrlWithKey(rawUrl: string, keyword: string) {
|
||||
return rawUrl.replaceAll('{{keyword}}', keyword);
|
||||
}
|
||||
|
||||
protected extractUrls(): { url: string; keyword: string }[] {
|
||||
// Tách từng key ra từ một chuỗi keywords
|
||||
const keywordList = this.keywords.split(', ');
|
||||
|
||||
// Dừng hàm nếu không có key nào
|
||||
if (keywordList.length <= 0) return [];
|
||||
|
||||
// Lập qua từng key để lấy url
|
||||
return keywordList.map((keyword) => {
|
||||
return {
|
||||
url: this.buildUrlWithKey(this.search_url, keyword),
|
||||
keyword,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
action: () => Promise<void>;
|
||||
|
||||
getInfoItems: (
|
||||
data: { name: string; el: Element }[],
|
||||
) => Record<string, string>[];
|
||||
|
||||
getItemsInHtml: (data: {
|
||||
html: string;
|
||||
keyword: string;
|
||||
}) => Promise<ScrapItem[]>;
|
||||
|
||||
protected filterItemByKeyword = (keyword: string, data: ScrapItem[]) => {
|
||||
return data.filter((item) =>
|
||||
item.name.toLowerCase().includes(keyword.toLowerCase()),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ScrapConfig } from './entities/scrap-config.entity';
|
||||
import { ScrapItem } from './entities/scrap-item.entity';
|
||||
import { ScrapConfigsService } from './services/scrap-config.service';
|
||||
import { ScrapConfigsController } from './controllers/scrap-config.controller';
|
||||
import { TasksService } from './services/tasks.service';
|
||||
import { ScrapItemsService } from './services/scrap-item-config.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([ScrapConfig, ScrapItem])],
|
||||
providers: [ScrapConfigsService, TasksService, ScrapItemsService],
|
||||
exports: [ScrapConfigsService, TasksService, ScrapItemsService],
|
||||
controllers: [ScrapConfigsController],
|
||||
})
|
||||
export class ScrapsModule {}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ScrapConfig } from '../entities/scrap-config.entity';
|
||||
import AppResponse from '@/response/app-response';
|
||||
import { CreateScrapConfigDto } from '../dto/scrap-config/create-scrap-config';
|
||||
import { UpdateScrapConfigDto } from '../dto/scrap-config/update-scrap-config';
|
||||
import axios from 'axios';
|
||||
import { WebBid } from '@/modules/bids/entities/wed-bid.entity';
|
||||
import { GraysScrapModel } from '../models/https:/www.grays.com/grays-scrap-model';
|
||||
|
||||
@Injectable()
|
||||
export class ScrapConfigsService {
|
||||
constructor(
|
||||
@InjectRepository(ScrapConfig)
|
||||
readonly scrapConfigRepo: Repository<ScrapConfig>,
|
||||
) {}
|
||||
|
||||
async create(data: CreateScrapConfigDto) {
|
||||
const result = await this.scrapConfigRepo.save({
|
||||
search_url: data.search_url,
|
||||
keywords: data.keywords,
|
||||
web_bid: { id: data.web_id },
|
||||
});
|
||||
|
||||
if (!result) return AppResponse.toResponse(false);
|
||||
|
||||
return AppResponse.toResponse(true);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: ScrapConfig['id'],
|
||||
{ web_id, ...data }: UpdateScrapConfigDto,
|
||||
) {
|
||||
const result = await this.scrapConfigRepo.update(id, { ...data });
|
||||
|
||||
if (!result.affected) return AppResponse.toResponse(false);
|
||||
|
||||
return AppResponse.toResponse(true);
|
||||
}
|
||||
|
||||
scrapModel(scrapConfig: ScrapConfig) {
|
||||
switch (scrapConfig.web_bid.origin_url) {
|
||||
case 'https://www.grays.com': {
|
||||
return new GraysScrapModel({
|
||||
...scrapConfig,
|
||||
scrap_config_id: scrapConfig.id,
|
||||
});
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scrapModels(data: ScrapConfig[]) {
|
||||
return data.map((item) => this.scrapModel(item)).filter((item) => !!item);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ScrapItem } from '../entities/scrap-item.entity';
|
||||
|
||||
@Injectable()
|
||||
export class ScrapItemsService {
|
||||
constructor(
|
||||
@InjectRepository(ScrapItem)
|
||||
readonly scrapItemRepo: Repository<ScrapItem>,
|
||||
) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import { IsNull, Not } from 'typeorm';
|
||||
import { ScrapConfigsService } from './scrap-config.service';
|
||||
@Injectable()
|
||||
export class TasksService {
|
||||
private readonly logger = new Logger(TasksService.name);
|
||||
|
||||
constructor(private readonly scrapConfigsService: ScrapConfigsService) {}
|
||||
|
||||
@Cron(CronExpression.EVERY_MINUTE)
|
||||
async handleScraps() {
|
||||
// const scrapConfigs = await this.scrapConfigsService.scrapConfigRepo.find({
|
||||
// where: {
|
||||
// search_url: Not(IsNull()),
|
||||
// keywords: Not(IsNull()),
|
||||
// },
|
||||
// relations: {
|
||||
// web_bid: true,
|
||||
// },
|
||||
// });
|
||||
// const models = this.scrapConfigsService.scrapModels(scrapConfigs);
|
||||
// await Promise.allSettled(models.map(async (item) => await item.action()));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Bid } from "@/modules/bids/entities/bid.entity";
|
||||
import { Bid } from '@/modules/bids/entities/bid.entity';
|
||||
|
||||
export function extractModelId(url: string): string | null {
|
||||
switch (extractDomain(url)) {
|
||||
|
|
@ -18,6 +18,10 @@ export function extractModelId(url: string): string | null {
|
|||
const model = url.split('/').pop();
|
||||
return model ? model : null;
|
||||
}
|
||||
case 'https://www.allbids.com.au': {
|
||||
const match = url.match(/-(\d+)(?:[\?#]|$)/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -110,71 +114,74 @@ export function verifyCode(content: string) {
|
|||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
export function extractNumber(str: string) {
|
||||
const match = str.match(/\d+(\.\d+)?/);
|
||||
return match ? parseFloat(match[0]) : null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import browser from "./system/browser.js";
|
|||
import configs from "./system/config.js";
|
||||
import {
|
||||
delay,
|
||||
findNearestClosingChild,
|
||||
extractModelId,
|
||||
isTimeReached,
|
||||
safeClosePage,
|
||||
subtractSeconds,
|
||||
|
|
@ -274,21 +274,6 @@ const clearLazyTab = async () => {
|
|||
// product tabs
|
||||
const productTabs = _.flatMap(MANAGER_BIDS, "children");
|
||||
|
||||
// for (const item of [...productTabs, ...MANAGER_BIDS]) {
|
||||
// if (!item.page_context) continue;
|
||||
|
||||
// try {
|
||||
// const avalableResult = await isPageAvailable(item.page_context);
|
||||
|
||||
// if (!avalableResult) {
|
||||
// await safeClosePage(item);
|
||||
// }
|
||||
// } catch (e) {
|
||||
// console.warn("⚠️ Error checking page_context.title()", e.message);
|
||||
// await safeClosePage(item);
|
||||
// }
|
||||
// }
|
||||
|
||||
for (const page of pages) {
|
||||
try {
|
||||
if (page.isClosed()) continue; // Trang đã đóng thì bỏ qua
|
||||
|
|
@ -309,7 +294,7 @@ const clearLazyTab = async () => {
|
|||
|
||||
if (!isTimeReached(earlyTrackingTime)) {
|
||||
await safeClosePage(productTab);
|
||||
console.log(`🛑 Unused page detected: ${pageUrl}`);
|
||||
console.log(`🛑 Unused page detectedd: ${pageUrl}`);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
|
@ -317,6 +302,22 @@ const clearLazyTab = async () => {
|
|||
continue;
|
||||
}
|
||||
|
||||
const modelProductTab = extractModelId(pageUrl);
|
||||
|
||||
if (modelProductTab) {
|
||||
const productWatingUpdate = productTabs.find(
|
||||
(item) =>
|
||||
item.model === modelProductTab &&
|
||||
isTimeReached(item.close_time) &&
|
||||
item.status === "biding"
|
||||
);
|
||||
|
||||
if (productWatingUpdate) {
|
||||
console.log("Waiting product update to close");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// remove all listents
|
||||
page.removeAllListeners();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,175 @@
|
|||
import fs from "fs";
|
||||
import configs from "../../system/config.js";
|
||||
import { getPathProfile, safeClosePage } from "../../system/utils.js";
|
||||
import { ApiBid } from "../api-bid.js";
|
||||
|
||||
export class AllbidsApiBid extends ApiBid {
|
||||
reloadInterval;
|
||||
constructor({ ...prev }) {
|
||||
super(prev);
|
||||
}
|
||||
|
||||
isLogin = async () => {
|
||||
if (!this.page_context) return false;
|
||||
|
||||
const filePath = getPathProfile(this.origin_url);
|
||||
|
||||
const currentUrl = await this.page_context.url();
|
||||
|
||||
console.log({
|
||||
filePath,
|
||||
currentUrl,
|
||||
a: currentUrl.includes(configs.WEB_URLS.ALLBIDS.LOGIN_URL),
|
||||
});
|
||||
|
||||
if (currentUrl.includes(configs.WEB_URLS.ALLBIDS.LOGIN_URL)) return false;
|
||||
|
||||
return (
|
||||
!(await this.page_context.$('input[name="Username"]')) &&
|
||||
fs.existsSync(filePath)
|
||||
);
|
||||
};
|
||||
|
||||
async handleLogin() {
|
||||
const page = this.page_context;
|
||||
|
||||
global.IS_CLEANING = false;
|
||||
|
||||
const filePath = getPathProfile(this.origin_url);
|
||||
|
||||
await page.waitForNavigation({ waitUntil: "domcontentloaded" });
|
||||
|
||||
// 🛠 Check if already logged in (login input should not be visible or profile exists)
|
||||
if (await this.isLogin()) {
|
||||
console.log(`✅ [${this.id}] Already logged in, skipping login process.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
console.log(`🗑 [${this.id}] Deleting existing file: ${filePath}`);
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
|
||||
const children = this.children.filter((item) => item.page_context);
|
||||
console.log(
|
||||
`🔍 [${this.id}] Found ${children.length} child pages to close.`
|
||||
);
|
||||
|
||||
if (children.length > 0) {
|
||||
console.log(`🛑 [${this.id}] Closing child pages...`);
|
||||
await Promise.all(
|
||||
children.map((item) => {
|
||||
console.log(
|
||||
`➡ [${this.id}] Closing child page with context: ${item.page_context}`
|
||||
);
|
||||
return safeClosePage(item);
|
||||
})
|
||||
);
|
||||
|
||||
console.log(
|
||||
`➡ [${this.id}] Closing main page context: ${this.page_context}`
|
||||
);
|
||||
await safeClosePage(this);
|
||||
|
||||
await this.onCloseLogin(this);
|
||||
}
|
||||
|
||||
console.log(`🔑 [${this.id}] Starting login process...`);
|
||||
|
||||
try {
|
||||
// ⌨ Enter email
|
||||
console.log(`✍ [${this.id}] Entering email:`, this.username);
|
||||
await page.type('input[name="Username"]', this.username, {
|
||||
delay: 100,
|
||||
});
|
||||
|
||||
// ⌨ Enter password
|
||||
console.log(`✍ [${this.id}] Entering password...`);
|
||||
await page.type('input[name="Password"]', this.password, {
|
||||
delay: 150,
|
||||
});
|
||||
|
||||
// 🚀 Click the login button
|
||||
console.log(`🔘 [${this.id}] Clicking the "Login" button`);
|
||||
await page.click("#btnSignIn", { delay: 92 });
|
||||
|
||||
// ⏳ Wait for navigation after login
|
||||
console.log(`⏳ [${this.id}] Waiting for navigation after login...`);
|
||||
await page.waitForNavigation({
|
||||
timeout: 8000,
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
|
||||
console.log(`🌍 [${this.id}] Current page after login:`, page.url());
|
||||
|
||||
await page.goto(this.url, { waitUntil: "networkidle2" });
|
||||
|
||||
// 📂 Save session context to avoid re-login
|
||||
await this.saveContext();
|
||||
console.log(`✅ [${this.id}] Login successful!`);
|
||||
|
||||
// await page.goto(this.url);
|
||||
console.log(`✅ [${this.id}] Navigation successful!`);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`❌ [${this.id}] Error during login process:`,
|
||||
error.message
|
||||
);
|
||||
} finally {
|
||||
global.IS_CLEANING = true;
|
||||
}
|
||||
}
|
||||
|
||||
action = async () => {
|
||||
try {
|
||||
const page = this.page_context;
|
||||
|
||||
page.on("response", async (response) => {
|
||||
const request = response.request();
|
||||
if (request.redirectChain().length > 0) {
|
||||
if (response.url().includes(configs.WEB_CONFIGS.ALLBIDS.LOGIN_URL)) {
|
||||
await this.handleLogin();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(this.url, { waitUntil: "networkidle2" });
|
||||
|
||||
await page.bringToFront();
|
||||
|
||||
// Set userAgent
|
||||
await page.setUserAgent(
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("Error [action]: ", error.message);
|
||||
}
|
||||
};
|
||||
|
||||
listen_events = async () => {
|
||||
if (this.page_context) return;
|
||||
|
||||
const results = await this.handlePrevListen();
|
||||
|
||||
if (!results) return;
|
||||
|
||||
this.reloadInterval = setInterval(async () => {
|
||||
try {
|
||||
if (this.page_context && !this.page_context.isClosed()) {
|
||||
console.log(`🔄 [${this.id}] Reloading page...`);
|
||||
await this.page_context.reload({ waitUntil: "networkidle2" });
|
||||
console.log(`✅ [${this.id}] Page reloaded successfully.`);
|
||||
|
||||
// this.handleUpdateWonItem();
|
||||
} else {
|
||||
console.log(
|
||||
`❌ [${this.id}] Page context is closed. Stopping reload.`
|
||||
);
|
||||
clearInterval(this.reloadInterval);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`🚨 [${this.id}] Error reloading page:`, error.message);
|
||||
}
|
||||
}, 60000); // 1p reload
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
import _ from "lodash";
|
||||
import { outBid, pushPrice, updateBid } from "../../system/apis/bid.js";
|
||||
import { sendMessage } from "../../system/apis/notification.js";
|
||||
import { createOutBidLog } from "../../system/apis/out-bid-log.js";
|
||||
import configs from "../../system/config.js";
|
||||
import CONSTANTS from "../../system/constants.js";
|
||||
import {
|
||||
convertAETtoUTC,
|
||||
isTimeReached,
|
||||
removeFalsyValues,
|
||||
takeSnapshot,
|
||||
} from "../../system/utils.js";
|
||||
import { ProductBid } from "../product-bid.js";
|
||||
|
||||
export class AllbidsProductBid extends ProductBid {
|
||||
constructor({ ...prev }) {
|
||||
super(prev);
|
||||
}
|
||||
|
||||
async waitForApiResponse() {
|
||||
if (!this.page_context) return;
|
||||
try {
|
||||
// Chờ cho Angular load (có thể tùy chỉnh thời gian nếu cần)
|
||||
await this.page_context.waitForFunction(
|
||||
() => window.angular !== undefined
|
||||
);
|
||||
|
||||
const auctionData = await this.page_context.evaluate(() => {
|
||||
let data = null;
|
||||
const elements = document.querySelectorAll(".ng-scope");
|
||||
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
try {
|
||||
const scope = angular.element(elements[i]).scope();
|
||||
if (scope?.auction) {
|
||||
data = scope.auction;
|
||||
break;
|
||||
}
|
||||
|
||||
// Thử lấy từ $parent nếu không thấy
|
||||
if (scope?.$parent?.auction) {
|
||||
data = scope.$parent.auction;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// Angular element có thể lỗi nếu phần tử không hợp lệ
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
return auctionData;
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`[${this.id}] Error in waitForApiResponse: ${error?.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async handleUpdateBid({
|
||||
lot_id,
|
||||
close_time,
|
||||
name,
|
||||
current_price,
|
||||
reserve_price,
|
||||
model,
|
||||
}) {
|
||||
const response = await updateBid(this.id, {
|
||||
lot_id,
|
||||
close_time,
|
||||
name,
|
||||
current_price,
|
||||
reserve_price: Number(reserve_price) || 0,
|
||||
model,
|
||||
});
|
||||
|
||||
if (response) {
|
||||
this.lot_id = response.lot_id;
|
||||
this.close_time = response.close_time;
|
||||
this.start_bid_time = response.start_bid_time;
|
||||
}
|
||||
}
|
||||
|
||||
async submitBid() {
|
||||
if (!this.page_context) return;
|
||||
|
||||
const response = await this.page_context.evaluate(
|
||||
async (aucID, bidAmount, submitUrl) => {
|
||||
try {
|
||||
const url = `${submitUrl}?aucID=${aucID}&bidAmount=${bidAmount}&bidType=maximum`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return { success: false, message: `HTTP error ${res.status}` };
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message || "Fetch failed" };
|
||||
}
|
||||
},
|
||||
this.model,
|
||||
this.max_price,
|
||||
configs.WEB_URLS.ALLBIDS.PLACE_BID
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
update = async () => {
|
||||
if (!this.page_context) return;
|
||||
|
||||
console.log(`🔄 [${this.id}] Call update for ID: ${this.id}`);
|
||||
|
||||
// 📌 Chờ phản hồi API từ trang, tối đa 10 giây
|
||||
const result = await this.waitForApiResponse();
|
||||
|
||||
// 📌 Nếu không có dữ liệu trả về thì dừng
|
||||
if (!result) {
|
||||
console.log(`⚠️ [${this.id}] No valid data received, skipping update.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 📌 Loại bỏ các giá trị không hợp lệ và bổ sung thông tin cần thiết
|
||||
const data = removeFalsyValues(
|
||||
{
|
||||
// model: result?.pid || null,
|
||||
lot_id: String(result?.aucCurrentBidID) || null,
|
||||
reserve_price: result?.aucBidIncrement || null,
|
||||
current_price: result.aucCurrentBid || null,
|
||||
close_time: result?.aucCloseUtc
|
||||
? new Date(result.aucCloseUtc).toUTCString()
|
||||
: null,
|
||||
// close_time: close_time && !this.close_time ? String(close_time) : null, // test
|
||||
name: result?.aucTitle || null,
|
||||
},
|
||||
["close_time"]
|
||||
);
|
||||
|
||||
console.log(`🚀 [${this.id}] Processed data ready for update`);
|
||||
|
||||
// 📌 Gửi dữ liệu cập nhật lên hệ thống
|
||||
await this.handleUpdateBid(data);
|
||||
|
||||
console.log("✅ Update successful!");
|
||||
|
||||
return { ...response, name: data.name, close_time: data.close_time };
|
||||
};
|
||||
|
||||
async handlePlaceBid() {
|
||||
if (!this.page_context) {
|
||||
console.log(
|
||||
`⚠️ [${this.id}] No page context found, aborting bid process.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const page = this.page_context;
|
||||
|
||||
if (global[`IS_PLACE_BID-${this.id}`]) {
|
||||
console.log(`⚠️ [${this.id}] Bid is already in progress, skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`🔄 [${this.id}] Starting bid process...`);
|
||||
global[`IS_PLACE_BID-${this.id}`] = true;
|
||||
|
||||
// Đợi phản hồi từ API
|
||||
const response = await this.waitForApiResponse();
|
||||
|
||||
if (
|
||||
!response ||
|
||||
isTimeReached(new Date(response.aucCloseUtc).toUTCString())
|
||||
) {
|
||||
console.log(
|
||||
`⚠️ [${this.id}] Outbid detected, calling outBid function.`
|
||||
);
|
||||
await outBid(this.id);
|
||||
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
|
||||
return; // Dừng hàm nếu giá đã vượt qua giới hạn
|
||||
}
|
||||
|
||||
// Kiểm tra thời gian bid
|
||||
if (this.start_bid_time && !isTimeReached(this.start_bid_time)) {
|
||||
console.log(
|
||||
`⏳ [${this.id}] Not yet time to bid. Skipping Product: ${
|
||||
this.name || "None"
|
||||
}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Kiểm tra nếu phản hồi không tồn tại hoặc nếu giá đấu của người dùng bằng với giá tối đa hiện tại
|
||||
if (
|
||||
!response ||
|
||||
(response?.aucUserMaxBid && response.aucUserMaxBid == this.max_price) ||
|
||||
response?.aucBidIncrement > this.max_price
|
||||
) {
|
||||
console.log(
|
||||
`⚠️ [${this.id}] No response or myBid equals max_price:`,
|
||||
response
|
||||
); // Ghi log nếu không có phản hồi hoặc giá đấu của người dùng bằng giá tối đa
|
||||
return; // Nếu không có phản hồi hoặc giá đấu bằng giá tối đa thì dừng hàm
|
||||
}
|
||||
|
||||
const bidHistoriesItem = _.maxBy(this.histories, "price");
|
||||
console.log(`📜 [${this.id}] Current bid history:`, this.histories);
|
||||
|
||||
if (
|
||||
bidHistoriesItem &&
|
||||
bidHistoriesItem?.price === this.current_price &&
|
||||
this.max_price == response?.aucUserMaxBid
|
||||
) {
|
||||
console.log(
|
||||
`🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem.price})`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("---------------------BIDDING--------------------");
|
||||
|
||||
const data = await this.submitBid();
|
||||
|
||||
console.log({ data });
|
||||
|
||||
await this.page_context.reload({ waitUntil: "networkidle0" });
|
||||
|
||||
const { aucUserMaxBid } = await this.waitForApiResponse();
|
||||
console.log(`📡 [${this.id}] API Response received:`, lotData);
|
||||
|
||||
// 📌 Kiểm tra trạng thái đấu giá từ API
|
||||
if (aucUserMaxBid == this.max_price) {
|
||||
console.log(`📸 [${this.id}] Taking bid success snapshot...`);
|
||||
await takeSnapshot(
|
||||
page,
|
||||
this,
|
||||
"bid-success",
|
||||
CONSTANTS.TYPE_IMAGE.SUCCESS
|
||||
);
|
||||
|
||||
sendMessage(this);
|
||||
|
||||
pushPrice({
|
||||
bid_id: this.id,
|
||||
price: aucUserMaxBid,
|
||||
});
|
||||
|
||||
console.log(`✅ [${this.id}] Bid placed successfully!`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`⚠️ [${this.id}] Bid action completed, but status is still "None".`
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(`🚨 [${this.id}] Error placing bid: ${error.message}`);
|
||||
} finally {
|
||||
console.log(`🔚 [${this.id}] Resetting bid flag.`);
|
||||
global[`IS_PLACE_BID-${this.id}`] = false;
|
||||
}
|
||||
}
|
||||
|
||||
action = async () => {
|
||||
try {
|
||||
const page = this.page_context;
|
||||
|
||||
// 📌 Kiểm tra nếu trang chưa tải đúng URL thì điều hướng đến URL mục tiêu
|
||||
if (!page.url() || !page.url().includes(this.url)) {
|
||||
console.log(`🔄 [${this.id}] Navigating to target URL: ${this.url}`);
|
||||
await this.gotoLink();
|
||||
}
|
||||
|
||||
await this.handlePlaceBid();
|
||||
} catch (error) {
|
||||
console.error(`🚨 [${this.id}] Error navigating the page: ${error}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -339,6 +339,13 @@ export class GraysProductBid extends ProductBid {
|
|||
);
|
||||
|
||||
if (isBided) {
|
||||
if (this.histories.length <= 0 && isTimeReached(this.start_bid_time)) {
|
||||
pushPrice({
|
||||
bid_id: this.id,
|
||||
price: this.max_price,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[${this.id}] This item bided. Skipping...`);
|
||||
global[`IS_PLACE_BID-${this.id}`] = false;
|
||||
global.IS_CLEANING = true;
|
||||
|
|
|
|||
|
|
@ -3,48 +3,54 @@
|
|||
// 1 : Apibids
|
||||
// 2 : Producttab
|
||||
|
||||
const { default: puppeteer } = require('puppeteer');
|
||||
const { default: puppeteer } = require("puppeteer");
|
||||
|
||||
Apibids = {
|
||||
type: 'Apibid',
|
||||
puppeteer_connect: 'puppeteer_connect',
|
||||
url: 'https://www.grays.com/mygrays/auctions/biddingon.aspx',
|
||||
listentEvent: function () {
|
||||
//action()
|
||||
},
|
||||
action: function () {},
|
||||
type: "Apibid",
|
||||
puppeteer_connect: "puppeteer_connect",
|
||||
url: "https://www.grays.com/mygrays/auctions/biddingon.aspx",
|
||||
listentEvent: function () {
|
||||
//action()
|
||||
},
|
||||
action: function () {},
|
||||
};
|
||||
|
||||
// n Producttab
|
||||
Producttab = {
|
||||
type: 'Producttab',
|
||||
url: 'https://www.grays.com/mygrays/auctions/biddingon.aspx',
|
||||
puppeteer_connect: 'puppeteer_connect',
|
||||
max_price: '',
|
||||
model: 'model',
|
||||
type: "Producttab",
|
||||
url: "https://www.grays.com/mygrays/auctions/biddingon.aspx",
|
||||
puppeteer_connect: "puppeteer_connect",
|
||||
max_price: "",
|
||||
model: "model",
|
||||
|
||||
action: function () {},
|
||||
action: function () {},
|
||||
};
|
||||
|
||||
manage = {
|
||||
Apibids: { Apibids },
|
||||
Producttabss: [{ Producttab }, { Producttab }],
|
||||
Apibids: { Apibids },
|
||||
Producttabss: [{ Producttab }, { Producttab }],
|
||||
};
|
||||
|
||||
// 10 goi 1 lan
|
||||
recheck = function name() {
|
||||
//check thay 2 record
|
||||
//tabApibids
|
||||
Apibids = new Apibids();
|
||||
Apibids.puppeteer_connect();
|
||||
Apibids.listentEvent();
|
||||
manage.Apibids = Apibids;
|
||||
//n Producttab
|
||||
//check thay 2 record
|
||||
//tabApibids
|
||||
Apibids = new Apibids();
|
||||
Apibids.puppeteer_connect();
|
||||
Apibids.listentEvent();
|
||||
manage.Apibids = Apibids;
|
||||
//n Producttab
|
||||
};
|
||||
```
|
||||
|
||||
\*\* Tắt polling trước khi demo
|
||||
<!-- all bids -->
|
||||
<!-- Lấy detail info của sản phẩm -->
|
||||
|
||||
- Trong thời gian đang bid nên mỡ tab lên -> hiện tại không mỡ tab lên khi start lại
|
||||
- Handle đăng nhập lại nếu không thành công -> hiện đang không đăng nhập lại nếu vì lí do nào đó không đăng nhập được
|
||||
- Lịch sử bid đang có 2 lần trùng
|
||||
let data = null; const elements = document.querySelectorAll('.ng-scope');
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const scope = angular.element(elements[i]).scope();
|
||||
if (scope && scope.auction) {
|
||||
console.log('Found at index:', i, 'Auction:', scope.auction); data = scope.auction;
|
||||
break; // dừng vòng lặp khi tìm thấy
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import * as fs from "fs";
|
||||
import path from "path";
|
||||
import { AllbidsApiBid } from "../models/allbids.com.au/allbids-api-bid.js";
|
||||
import { AllbidsProductBid } from "../models/allbids.com.au/allbids-product-bid.js";
|
||||
import { GrayApiBid } from "../models/grays.com/grays-api-bid.js";
|
||||
import { GraysProductBid } from "../models/grays.com/grays-product-bid.js";
|
||||
import { LangtonsApiBid } from "../models/langtons.com.au/langtons-api-bid.js";
|
||||
|
|
@ -39,6 +41,9 @@ export const createBidProduct = (web, data) => {
|
|||
case configs.WEB_URLS.PICKLES: {
|
||||
return new PicklesProductBid({ ...data });
|
||||
}
|
||||
case configs.WEB_URLS.ALLBIDS: {
|
||||
return new AllbidsProductBid({ ...data });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -56,6 +61,9 @@ export const createApiBid = (web) => {
|
|||
case configs.WEB_URLS.PICKLES: {
|
||||
return new PicklesApiBid({ ...web });
|
||||
}
|
||||
case configs.WEB_URLS.ALLBIDS: {
|
||||
return new AllbidsApiBid({ ...web });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ const configs = {
|
|||
LANGTONS: `https://www.langtons.com.au`,
|
||||
LAWSONS: `https://www.lawsons.com.au`,
|
||||
PICKLES: `https://www.pickles.com.au`,
|
||||
ALLBIDS: `https://www.allbids.com.au`,
|
||||
},
|
||||
WEB_CONFIGS: {
|
||||
GRAYS: {
|
||||
|
|
@ -39,6 +40,10 @@ const configs = {
|
|||
API_CHECKOUT:
|
||||
"https://www.pickles.com.au/delegate/secured/bidding/confirm",
|
||||
},
|
||||
ALLBIDS: {
|
||||
LOGIN_URL: "https://myaccount.allbids.com.au/account/login",
|
||||
PLACE_BID: "https://www.allbids.com.au/Bid/AjaxFinishBid",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -322,3 +322,43 @@ export function findNearestClosingChild(webBid) {
|
|||
|
||||
return nearestChild || null;
|
||||
}
|
||||
|
||||
export function extractModelId(url) {
|
||||
try {
|
||||
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;
|
||||
}
|
||||
case "https://www.allbids.com.au": {
|
||||
const match = url.match(/-(\d+)(?:[\?#]|$)/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function extractDomain(url) {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
return parsedUrl.origin;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -1,134 +0,0 @@
|
|||
/* #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.
|
Before Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 305 B |
Binary file not shown.
|
Before Width: | Height: | Size: 522 B |
|
|
@ -1,4 +0,0 @@
|
|||
chrome.action.onClicked.addListener((tab) => {
|
||||
// Lấy URL của tab hiện tại
|
||||
console.log("Current URL:", tab.url);
|
||||
});
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
// config.js
|
||||
const CONFIG = {
|
||||
API_BASE_URL: "http://localhost:4000/api/v1",
|
||||
};
|
||||
|
||||
export default CONFIG;
|
||||
|
|
@ -1,394 +0,0 @@
|
|||
const CONFIG = {
|
||||
API_BASE_URL: "http://localhost:4000/api/v1",
|
||||
// API_BASE_URL: "https://bids.apactech.io/api/v1",
|
||||
};
|
||||
|
||||
let PREV_DATA = null;
|
||||
|
||||
function removeFalsyValues(obj, excludeKeys = []) {
|
||||
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||||
if (value || excludeKeys.includes(key)) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function extractDomain(url) {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
return parsedUrl.origin;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
})();
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
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