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>
|
</Box>
|
||||||
|
|
||||||
<Box className="absolute top-2.5 left-2.5 flex items-center gap-2">
|
<Box className="absolute top-0 left-0 py-2 px-4 flex items-center gap-2 justify-between w-full">
|
||||||
<Badge
|
<Box className="flex items-center gap-2">
|
||||||
color={payloadLoginStatus?.login_status ? "green" : "red"}
|
<Badge
|
||||||
size="xs"
|
color={payloadLoginStatus?.login_status ? "green" : "red"}
|
||||||
>
|
size="xs"
|
||||||
{statusLabel()}
|
>
|
||||||
</Badge>
|
{statusLabel()}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
<Badge
|
<Badge
|
||||||
color={stringToColor(
|
color={stringToColor(
|
||||||
isIBid(data)
|
isIBid(data)
|
||||||
|
? extractDomainSmart(data.web_bid.origin_url)
|
||||||
|
: extractDomainSmart(data.origin_url)
|
||||||
|
)}
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
{isIBid(data)
|
||||||
? extractDomainSmart(data.web_bid.origin_url)
|
? extractDomainSmart(data.web_bid.origin_url)
|
||||||
: extractDomainSmart(data.origin_url)
|
: extractDomainSmart(data.origin_url)}
|
||||||
)}
|
</Badge>
|
||||||
size="xs"
|
</Box>
|
||||||
>
|
|
||||||
{isIBid(data)
|
{isIBid(data) && moment(data.close_time).isSame(moment(), "day") && (
|
||||||
? extractDomainSmart(data.web_bid.origin_url)
|
<div className="w-[14px] h-[14px] rounded-full bg-green-600 animate-pulse"></div>
|
||||||
: extractDomainSmart(data.origin_url)}
|
)}
|
||||||
</Badge>
|
|
||||||
</Box>
|
</Box>
|
||||||
</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 */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import { Button, LoadingOverlay, Modal, ModalProps, PasswordInput, TextInput } from '@mantine/core';
|
import {
|
||||||
import { useForm, zodResolver } from '@mantine/form';
|
Button,
|
||||||
import _ from 'lodash';
|
LoadingOverlay,
|
||||||
import { useEffect, useRef, useState } from 'react';
|
Modal,
|
||||||
import { z } from 'zod';
|
ModalProps,
|
||||||
import { updateWebBid } from '../../apis/web-bid';
|
PasswordInput,
|
||||||
import { useConfirmStore } from '../../lib/zustand/use-confirm';
|
TextInput,
|
||||||
import { IWebBid } from '../../system/type';
|
} 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 {
|
export interface IWebBidModelProps extends ModalProps {
|
||||||
data: IWebBid | null;
|
data: IWebBid | null;
|
||||||
onUpdated?: () => void;
|
onUpdated?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
username: z.string().min(1, { message: 'Username is required' }),
|
username: z.string().min(1, { message: "Username is required" }),
|
||||||
password: z.string().min(6, { message: 'Password must be at least 6 characters long' }),
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(6, { message: "Password must be at least 6 characters long" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function WebAccountModal({ data, onUpdated, ...props }: IWebBidModelProps) {
|
export default function WebAccountModal({
|
||||||
const form = useForm({
|
data,
|
||||||
validate: zodResolver(schema),
|
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) => {
|
const handleSubmit = async (values: typeof form.values) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
setConfirm({
|
setConfirm({
|
||||||
title: 'Update ?',
|
title: "Update ?",
|
||||||
message: `This account will be update`,
|
message: `This account will be update`,
|
||||||
handleOk: async () => {
|
handleOk: async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const result = await updateWebBid(values);
|
const result = await updateWebBid(values);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
props.onClose();
|
props.onClose();
|
||||||
|
|
||||||
if (onUpdated) {
|
if (onUpdated) {
|
||||||
onUpdated();
|
onUpdated();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
okButton: {
|
okButton: {
|
||||||
color: 'blue',
|
color: "blue",
|
||||||
value: 'Update',
|
value: "Update",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const result = await updateWebBid(values);
|
const result = await updateWebBid(values);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
props.onClose();
|
props.onClose();
|
||||||
|
|
||||||
if (onUpdated) {
|
if (onUpdated) {
|
||||||
onUpdated();
|
onUpdated();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset();
|
form.reset();
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
form.setValues(data);
|
form.setValues(data);
|
||||||
|
|
||||||
prevData.current = data;
|
prevData.current = data;
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!props.opened) {
|
if (!props.opened) {
|
||||||
form.reset();
|
form.reset();
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [props.opened]);
|
}, [props.opened]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
className="relative"
|
className="relative"
|
||||||
classNames={{
|
classNames={{
|
||||||
header: '!flex !item-center !justify-center w-full',
|
header: "!flex !item-center !justify-center w-full",
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
size={'xl'}
|
size={"xl"}
|
||||||
title={<span className="text-xl font-bold">Account</span>}
|
title={<span className="text-xl font-bold">Account</span>}
|
||||||
centered
|
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">
|
{data ? "Update" : "Create"}
|
||||||
<TextInput className="col-span-2" size="sm" label="Username" {...form.getInputProps('username')} />
|
</Button>
|
||||||
<PasswordInput className="col-span-2" size="sm" label="Password" {...form.getInputProps('password')} />
|
</form>
|
||||||
|
|
||||||
<Button disabled={_.isEqual(form.getValues(), prevData.current)} className="col-span-2" type="submit" fullWidth size="sm" mt="md">
|
<LoadingOverlay
|
||||||
{data ? 'Update' : 'Create'}
|
visible={loading}
|
||||||
</Button>
|
zIndex={1000}
|
||||||
</form>
|
overlayProps={{ blur: 2 }}
|
||||||
|
/>
|
||||||
<LoadingOverlay visible={loading} zIndex={1000} overlayProps={{ blur: 2 }} />
|
</Modal>
|
||||||
</Modal>
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,14 @@ const schema = {
|
||||||
.number({ message: "Arrival offset seconds is required" })
|
.number({ message: "Arrival offset seconds is required" })
|
||||||
.refine((val) => val >= 60, {
|
.refine((val) => val >= 60, {
|
||||||
message: "Arrival offset seconds must be at least 60 seconds (1 minute)",
|
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" })
|
.number({ message: "Early login seconds is required" })
|
||||||
.refine((val) => val >= 600, {
|
.refine((val) => val >= 600, {
|
||||||
message: "Early login seconds must be at least 600 seconds (10 minute)",
|
message: "Early login seconds must be at least 600 seconds (10 minute)",
|
||||||
}).optional(),
|
})
|
||||||
|
.optional(),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function WebBidModal({
|
export default function WebBidModal({
|
||||||
|
|
@ -56,11 +58,7 @@ export default function WebBidModal({
|
||||||
message: `This web will be update`,
|
message: `This web will be update`,
|
||||||
handleOk: async () => {
|
handleOk: async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
console.log(
|
|
||||||
"%csrc/components/web-bid/web-bid-modal.tsx:54 values",
|
|
||||||
"color: #007acc;",
|
|
||||||
values
|
|
||||||
);
|
|
||||||
const result = await updateWebBid(values);
|
const result = await updateWebBid(values);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
|
|
@ -78,14 +76,19 @@ export default function WebBidModal({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} 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);
|
setLoading(true);
|
||||||
const result = await createWebBid({
|
const result = await createWebBid({
|
||||||
url,
|
url,
|
||||||
origin_url,
|
origin_url,
|
||||||
arrival_offset_seconds,
|
arrival_offset_seconds,
|
||||||
early_tracking_seconds
|
early_tracking_seconds,
|
||||||
} as IWebBid);
|
} as IWebBid);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
|
|
@ -157,9 +160,9 @@ export default function WebBidModal({
|
||||||
description="Note: that only integer minutes are accepted."
|
description="Note: that only integer minutes are accepted."
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
size="sm"
|
size="sm"
|
||||||
label={`Arrival offset seconds (${
|
label={`Arrival offset seconds (${formatTimeFromMinutes(
|
||||||
formatTimeFromMinutes(form.getValues()["arrival_offset_seconds"] / 60)
|
form.getValues()["arrival_offset_seconds"] / 60
|
||||||
})`}
|
)})`}
|
||||||
placeholder="msg: 300"
|
placeholder="msg: 300"
|
||||||
{...form.getInputProps("arrival_offset_seconds")}
|
{...form.getInputProps("arrival_offset_seconds")}
|
||||||
/>
|
/>
|
||||||
|
|
@ -167,9 +170,9 @@ export default function WebBidModal({
|
||||||
description="Note: that only integer minutes are accepted."
|
description="Note: that only integer minutes are accepted."
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
size="sm"
|
size="sm"
|
||||||
label={`Early tracking seconds (${
|
label={`Early tracking seconds (${formatTimeFromMinutes(
|
||||||
formatTimeFromMinutes(form.getValues()["early_tracking_seconds"] / 60)
|
form.getValues()["early_tracking_seconds"] / 60
|
||||||
})`}
|
)})`}
|
||||||
placeholder="msg: 600"
|
placeholder="msg: 600"
|
||||||
{...form.getInputProps("early_tracking_seconds")}
|
{...form.getInputProps("early_tracking_seconds")}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ export default function Bids() {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "close_time",
|
key: "close_time_ts",
|
||||||
title: "Close time",
|
title: "Close time",
|
||||||
typeFilter: "date",
|
typeFilter: "date",
|
||||||
renderRow(row) {
|
renderRow(row) {
|
||||||
|
|
|
||||||
|
|
@ -1,253 +1,306 @@
|
||||||
import { ActionIcon, Badge, Box, Menu, Text } from '@mantine/core';
|
import { ActionIcon, Badge, Box, Menu, Text } from "@mantine/core";
|
||||||
import { IconAd, IconAdOff, IconEdit, IconMenu, IconTrash, IconUserEdit } from '@tabler/icons-react';
|
import {
|
||||||
import { useMemo, useRef, useState } from 'react';
|
IconAd,
|
||||||
import { deletesWebBid, deleteWebBid, getWebBids, updateWebBid } from '../apis/web-bid';
|
IconAdOff,
|
||||||
import Table from '../lib/table/table';
|
IconEdit,
|
||||||
import { IColumn, TRefTableFn } from '../lib/table/type';
|
IconMenu,
|
||||||
import { useConfirmStore } from '../lib/zustand/use-confirm';
|
IconSettingsCode,
|
||||||
import { IWebBid } from '../system/type';
|
IconTrash,
|
||||||
import { formatTime } from '../utils';
|
IconUserEdit,
|
||||||
import { WebAccountModal, WebBidModal } from '../components/web-bid';
|
} from "@tabler/icons-react";
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
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() {
|
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 [webBidOpened, webBidModal] = useDisclosure(false);
|
||||||
const [webAccountOpened, webAccountModal] = useDisclosure(false);
|
const [webAccountOpened, webAccountModal] = useDisclosure(false);
|
||||||
|
const [scrapConfigOpened, scrapConfigModal] = useDisclosure(false);
|
||||||
|
|
||||||
const columns: IColumn<IWebBid>[] = [
|
const columns: IColumn<IWebBid>[] = [
|
||||||
{
|
{
|
||||||
key: 'id',
|
key: "id",
|
||||||
title: 'ID',
|
title: "ID",
|
||||||
typeFilter: 'number',
|
typeFilter: "number",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'origin_url',
|
key: "origin_url",
|
||||||
title: 'Domain',
|
title: "Domain",
|
||||||
typeFilter: 'text',
|
typeFilter: "text",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'url',
|
key: "url",
|
||||||
title: 'Tracking url',
|
title: "Tracking url",
|
||||||
typeFilter: 'text',
|
typeFilter: "text",
|
||||||
renderRow(row) {
|
renderRow(row) {
|
||||||
return <Text>{row.url || 'None'}</Text>;
|
return <Text>{row.url || "None"}</Text>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'active',
|
key: "active",
|
||||||
title: 'Status',
|
title: "Status",
|
||||||
typeFilter: 'text',
|
typeFilter: "text",
|
||||||
renderRow(row) {
|
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(() => {
|
|
||||||
return (
|
return (
|
||||||
<Table
|
<Box className="flex items-center justify-center">
|
||||||
actionsOptions={{
|
<Badge color={row.active ? "green" : "red"} size="sm">
|
||||||
actions: [
|
{row.active ? "Enable" : "Disable"}
|
||||||
{
|
</Badge>
|
||||||
key: 'add',
|
</Box>
|
||||||
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"
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
// 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 (
|
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
|
if (!result) return;
|
||||||
data={clickData}
|
if (refTableFn.current.fetchData) {
|
||||||
opened={webBidOpened}
|
refTableFn.current.fetchData();
|
||||||
onClose={() => {
|
|
||||||
webBidModal.close();
|
|
||||||
setClickData(null);
|
|
||||||
}}
|
|
||||||
onUpdated={() => {
|
|
||||||
setClickData(null);
|
|
||||||
|
|
||||||
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
|
<Menu.Dropdown>
|
||||||
data={clickData}
|
<Menu.Item
|
||||||
opened={webAccountOpened}
|
onClick={() => {
|
||||||
onClose={() => {
|
setClickData(row);
|
||||||
webAccountModal.close();
|
webBidModal.open();
|
||||||
setClickData(null);
|
}}
|
||||||
}}
|
leftSection={<IconEdit size={14} />}
|
||||||
onUpdated={() => {
|
>
|
||||||
setClickData(null);
|
Edit
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
onClick={() => {
|
||||||
|
setClickData(row);
|
||||||
|
webAccountModal.open();
|
||||||
|
}}
|
||||||
|
leftSection={<IconUserEdit size={14} />}
|
||||||
|
>
|
||||||
|
Account
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
if (refTableFn.current?.fetchData) {
|
<Menu.Item
|
||||||
refTableFn.current.fetchData();
|
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} />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}}
|
>
|
||||||
/>
|
{row.active ? "Disable" : "Enable"}
|
||||||
</Box>
|
</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 {
|
export interface IAdmin extends ITimestamp {
|
||||||
id: number;
|
id: number;
|
||||||
email: string;
|
email: string;
|
||||||
username: string;
|
username: string;
|
||||||
fullname: string;
|
fullname: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
is_system_account: boolean;
|
is_system_account: boolean;
|
||||||
permissions: [];
|
permissions: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IKey extends ITimestamp {
|
export interface IKey extends ITimestamp {
|
||||||
id: number;
|
id: number;
|
||||||
client_key: string;
|
client_key: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITimestamp {
|
export interface ITimestamp {
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface IHistory extends ITimestamp {
|
export interface IHistory extends ITimestamp {
|
||||||
id: number;
|
id: number;
|
||||||
price: number;
|
price: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IOutBidLog extends ITimestamp {
|
export interface IOutBidLog extends ITimestamp {
|
||||||
id: number;
|
id: number;
|
||||||
model: string;
|
model: string;
|
||||||
lot_id: string;
|
lot_id: string;
|
||||||
out_price: number;
|
out_price: number;
|
||||||
raw_data: string;
|
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 {
|
export interface IWebBid extends ITimestamp {
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
id: number;
|
id: number;
|
||||||
origin_url: string;
|
origin_url: string;
|
||||||
url: string | null;
|
url: string | null;
|
||||||
username: string | null;
|
username: string | null;
|
||||||
password: string | null;
|
password: string | null;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
arrival_offset_seconds: number;
|
arrival_offset_seconds: number;
|
||||||
early_tracking_seconds: number;
|
early_tracking_seconds: number;
|
||||||
snapshot_at: string | null
|
snapshot_at: string | null;
|
||||||
children: IBid[];
|
children: IBid[];
|
||||||
|
scrap_config: IScrapConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBid extends ITimestamp {
|
export interface IBid extends ITimestamp {
|
||||||
id: number;
|
id: number;
|
||||||
max_price: number;
|
max_price: number;
|
||||||
reserve_price: number;
|
reserve_price: number;
|
||||||
current_price: number;
|
current_price: number;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
url: string;
|
url: string;
|
||||||
model: string;
|
model: string;
|
||||||
lot_id: string;
|
lot_id: string;
|
||||||
plus_price: number;
|
plus_price: number;
|
||||||
close_time: string | null;
|
close_time: string | null;
|
||||||
start_bid_time: string | null;
|
close_time_ts: string | null;
|
||||||
first_bid: boolean;
|
start_bid_time: string | null;
|
||||||
status: 'biding' | 'out-bid' | 'win-bid';
|
first_bid: boolean;
|
||||||
histories: IHistory[];
|
status: "biding" | "out-bid" | "win-bid";
|
||||||
web_bid: IWebBid;
|
histories: IHistory[];
|
||||||
|
web_bid: IWebBid;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPermission extends ITimestamp {
|
export interface IPermission extends ITimestamp {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
export interface ISendMessageHistory extends ITimestamp {
|
export interface ISendMessageHistory extends ITimestamp {
|
||||||
id: number;
|
id: number;
|
||||||
message: string;
|
message: string;
|
||||||
bid: IBid;
|
bid: IBid;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"createdAt":1747292824357}
|
{"createdAt":1747701959077}
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"@nestjs/websockets": "^11.0.11",
|
"@nestjs/websockets": "^11.0.11",
|
||||||
"axios": "^1.8.3",
|
"axios": "^1.8.3",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
"cheerio": "^1.0.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
|
|
@ -4165,6 +4166,12 @@
|
||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||||
|
|
@ -4422,6 +4429,48 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/chokidar": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
|
|
@ -4923,6 +4972,34 @@
|
||||||
"node": ">= 8"
|
"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": {
|
"node_modules/dayjs": {
|
||||||
"version": "1.11.13",
|
"version": "1.11.13",
|
||||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||||
|
|
@ -5138,6 +5215,61 @@
|
||||||
"node": ">=6.0.0"
|
"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": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.4.7",
|
"version": "16.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||||
|
|
@ -5260,6 +5392,31 @@
|
||||||
"iconv-lite": "^0.6.2"
|
"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": {
|
"node_modules/encoding/node_modules/iconv-lite": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
|
@ -5341,6 +5498,18 @@
|
||||||
"node": ">=10.13.0"
|
"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": {
|
"node_modules/error-ex": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||||
|
|
@ -6773,6 +6942,25 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/http-errors": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||||
|
|
@ -8900,6 +9088,18 @@
|
||||||
"set-blocking": "^2.0.0"
|
"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": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
|
@ -9109,6 +9309,55 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
|
|
@ -11532,6 +11781,15 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.19.8",
|
"version": "6.19.8",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||||
|
|
@ -11842,6 +12100,39 @@
|
||||||
"url": "https://opencollective.com/webpack"
|
"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": {
|
"node_modules/whatwg-url": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
"@nestjs/websockets": "^11.0.11",
|
"@nestjs/websockets": "^11.0.11",
|
||||||
"axios": "^1.8.3",
|
"axios": "^1.8.3",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
"cheerio": "^1.0.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
import { AuthorizationMiddleware } from './modules/admins/middlewares/authorization.middleware';
|
import { AuthorizationMiddleware } from './modules/admins/middlewares/authorization.middleware';
|
||||||
import { ClientAuthenticationMiddleware } from './modules/auth/middlewares/client-authentication.middleware';
|
import { ClientAuthenticationMiddleware } from './modules/auth/middlewares/client-authentication.middleware';
|
||||||
import { NotificationModule } from './modules/notification/notification.module';
|
import { NotificationModule } from './modules/notification/notification.module';
|
||||||
|
import { ScrapsModule } from './modules/scraps/scraps.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -24,6 +25,7 @@ import { NotificationModule } from './modules/notification/notification.module';
|
||||||
AuthModule,
|
AuthModule,
|
||||||
AdminsModule,
|
AdminsModule,
|
||||||
NotificationModule,
|
NotificationModule,
|
||||||
|
ScrapsModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,9 @@ export class Bid extends Timestamp {
|
||||||
@Column({ default: null, nullable: true })
|
@Column({ default: null, nullable: true })
|
||||||
close_time: string;
|
close_time: string;
|
||||||
|
|
||||||
|
@Column({ default: null, nullable: true })
|
||||||
|
close_time_ts: Date | null;
|
||||||
|
|
||||||
@Column({ default: null, nullable: true })
|
@Column({ default: null, nullable: true })
|
||||||
start_bid_time: string;
|
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 { Timestamp } from './timestamp';
|
||||||
import { Bid } from './bid.entity';
|
import { Bid } from './bid.entity';
|
||||||
import { Exclude } from 'class-transformer';
|
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')
|
@Entity('web_bids')
|
||||||
export class WebBid extends Timestamp {
|
export class WebBid extends Timestamp {
|
||||||
|
|
@ -37,4 +45,7 @@ export class WebBid extends Timestamp {
|
||||||
cascade: true,
|
cascade: true,
|
||||||
})
|
})
|
||||||
children: Bid[];
|
children: Bid[];
|
||||||
|
|
||||||
|
@OneToOne(() => ScrapConfig, (scrap) => scrap.web_bid)
|
||||||
|
scrap_config: ScrapConfig;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ export class BidsService {
|
||||||
sortableColumns: [
|
sortableColumns: [
|
||||||
'id',
|
'id',
|
||||||
'close_time',
|
'close_time',
|
||||||
|
'close_time_ts',
|
||||||
'first_bid',
|
'first_bid',
|
||||||
'model',
|
'model',
|
||||||
'lot_id',
|
'lot_id',
|
||||||
|
|
@ -120,7 +121,9 @@ export class BidsService {
|
||||||
|
|
||||||
await this.emitAllBidEvent();
|
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) {
|
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()
|
new Date(close_time).getTime() > new Date(bid.close_time).getTime()
|
||||||
) {
|
) {
|
||||||
bid.close_time = close_time;
|
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
|
// 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) {
|
async getBidByModel(model: string) {
|
||||||
|
console.log(
|
||||||
console.log('%csrc/modules/bids/services/bids.service.ts:554 model', 'color: #007acc;', model);
|
'%csrc/modules/bids/services/bids.service.ts:554 model',
|
||||||
|
'color: #007acc;',
|
||||||
|
model,
|
||||||
|
);
|
||||||
const bid = await this.bidsRepo.findOne({ where: { 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));
|
return AppResponse.toResponse(plainToClass(Bid, bid));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export class TasksService {
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Cron(CronExpression.EVERY_MINUTE)
|
@Cron(CronExpression.EVERY_MINUTE)
|
||||||
async handleCron() {
|
async handleResetTool() {
|
||||||
const bids = await this.bidsService.bidsRepo.find({
|
const bids = await this.bidsService.bidsRepo.find({
|
||||||
where: { status: 'biding' },
|
where: { status: 'biding' },
|
||||||
select: ['close_time', 'created_at', 'start_bid_time', 'id', 'lot_id'],
|
select: ['close_time', 'created_at', 'start_bid_time', 'id', 'lot_id'],
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,9 @@ export class WebBidsService {
|
||||||
filterableColumns,
|
filterableColumns,
|
||||||
defaultSortBy: [['id', 'DESC']],
|
defaultSortBy: [['id', 'DESC']],
|
||||||
maxLimit: 100,
|
maxLimit: 100,
|
||||||
|
relations: {
|
||||||
|
scrap_config: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return AppResponse.toPagination<WebBid>(data, true, WebBid);
|
return AppResponse.toPagination<WebBid>(data, true, WebBid);
|
||||||
|
|
@ -66,6 +69,11 @@ export class WebBidsService {
|
||||||
children: { status: 'biding' },
|
children: { status: 'biding' },
|
||||||
},
|
},
|
||||||
relations: { children: { histories: true, web_bid: true } },
|
relations: { children: { histories: true, web_bid: true } },
|
||||||
|
order: {
|
||||||
|
children: {
|
||||||
|
close_time_ts: 'ASC',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
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 {
|
export function extractModelId(url: string): string | null {
|
||||||
switch (extractDomain(url)) {
|
switch (extractDomain(url)) {
|
||||||
|
|
@ -18,6 +18,10 @@ export function extractModelId(url: string): string | null {
|
||||||
const model = url.split('/').pop();
|
const model = url.split('/').pop();
|
||||||
return model ? model : null;
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function shouldResetTool(
|
export function shouldResetTool(
|
||||||
bids: Bid[],
|
bids: Bid[],
|
||||||
lastResetTime: Date | null,
|
lastResetTime: Date | null,
|
||||||
now: Date = new Date(),
|
now: Date = new Date(),
|
||||||
) {
|
) {
|
||||||
const ONE_MINUTE = 60 * 1000;
|
const ONE_MINUTE = 60 * 1000;
|
||||||
const ONE_HOUR = 60 * ONE_MINUTE;
|
const ONE_HOUR = 60 * ONE_MINUTE;
|
||||||
const TWO_HOURS = 2 * ONE_HOUR;
|
const TWO_HOURS = 2 * ONE_HOUR;
|
||||||
const TWENTY_MINUTES = 20 * ONE_MINUTE;
|
const TWENTY_MINUTES = 20 * ONE_MINUTE;
|
||||||
const FIVE_MINUTES = 5 * 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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Nếu đã reset trong 1 giờ gần đây => không reset
|
||||||
|
if (lastResetTime && now.getTime() - lastResetTime.getTime() < ONE_HOUR) {
|
||||||
return {
|
return {
|
||||||
shouldReset: false,
|
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 configs from "./system/config.js";
|
||||||
import {
|
import {
|
||||||
delay,
|
delay,
|
||||||
findNearestClosingChild,
|
extractModelId,
|
||||||
isTimeReached,
|
isTimeReached,
|
||||||
safeClosePage,
|
safeClosePage,
|
||||||
subtractSeconds,
|
subtractSeconds,
|
||||||
|
|
@ -274,21 +274,6 @@ const clearLazyTab = async () => {
|
||||||
// product tabs
|
// product tabs
|
||||||
const productTabs = _.flatMap(MANAGER_BIDS, "children");
|
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) {
|
for (const page of pages) {
|
||||||
try {
|
try {
|
||||||
if (page.isClosed()) continue; // Trang đã đóng thì bỏ qua
|
if (page.isClosed()) continue; // Trang đã đóng thì bỏ qua
|
||||||
|
|
@ -309,7 +294,7 @@ const clearLazyTab = async () => {
|
||||||
|
|
||||||
if (!isTimeReached(earlyTrackingTime)) {
|
if (!isTimeReached(earlyTrackingTime)) {
|
||||||
await safeClosePage(productTab);
|
await safeClosePage(productTab);
|
||||||
console.log(`🛑 Unused page detected: ${pageUrl}`);
|
console.log(`🛑 Unused page detectedd: ${pageUrl}`);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -317,6 +302,22 @@ const clearLazyTab = async () => {
|
||||||
continue;
|
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
|
// remove all listents
|
||||||
page.removeAllListeners();
|
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 (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...`);
|
console.log(`[${this.id}] This item bided. Skipping...`);
|
||||||
global[`IS_PLACE_BID-${this.id}`] = false;
|
global[`IS_PLACE_BID-${this.id}`] = false;
|
||||||
global.IS_CLEANING = true;
|
global.IS_CLEANING = true;
|
||||||
|
|
|
||||||
|
|
@ -3,48 +3,54 @@
|
||||||
// 1 : Apibids
|
// 1 : Apibids
|
||||||
// 2 : Producttab
|
// 2 : Producttab
|
||||||
|
|
||||||
const { default: puppeteer } = require('puppeteer');
|
const { default: puppeteer } = require("puppeteer");
|
||||||
|
|
||||||
Apibids = {
|
Apibids = {
|
||||||
type: 'Apibid',
|
type: "Apibid",
|
||||||
puppeteer_connect: 'puppeteer_connect',
|
puppeteer_connect: "puppeteer_connect",
|
||||||
url: 'https://www.grays.com/mygrays/auctions/biddingon.aspx',
|
url: "https://www.grays.com/mygrays/auctions/biddingon.aspx",
|
||||||
listentEvent: function () {
|
listentEvent: function () {
|
||||||
//action()
|
//action()
|
||||||
},
|
},
|
||||||
action: function () {},
|
action: function () {},
|
||||||
};
|
};
|
||||||
|
|
||||||
// n Producttab
|
// n Producttab
|
||||||
Producttab = {
|
Producttab = {
|
||||||
type: 'Producttab',
|
type: "Producttab",
|
||||||
url: 'https://www.grays.com/mygrays/auctions/biddingon.aspx',
|
url: "https://www.grays.com/mygrays/auctions/biddingon.aspx",
|
||||||
puppeteer_connect: 'puppeteer_connect',
|
puppeteer_connect: "puppeteer_connect",
|
||||||
max_price: '',
|
max_price: "",
|
||||||
model: 'model',
|
model: "model",
|
||||||
|
|
||||||
action: function () {},
|
action: function () {},
|
||||||
};
|
};
|
||||||
|
|
||||||
manage = {
|
manage = {
|
||||||
Apibids: { Apibids },
|
Apibids: { Apibids },
|
||||||
Producttabss: [{ Producttab }, { Producttab }],
|
Producttabss: [{ Producttab }, { Producttab }],
|
||||||
};
|
};
|
||||||
|
|
||||||
// 10 goi 1 lan
|
// 10 goi 1 lan
|
||||||
recheck = function name() {
|
recheck = function name() {
|
||||||
//check thay 2 record
|
//check thay 2 record
|
||||||
//tabApibids
|
//tabApibids
|
||||||
Apibids = new Apibids();
|
Apibids = new Apibids();
|
||||||
Apibids.puppeteer_connect();
|
Apibids.puppeteer_connect();
|
||||||
Apibids.listentEvent();
|
Apibids.listentEvent();
|
||||||
manage.Apibids = Apibids;
|
manage.Apibids = Apibids;
|
||||||
//n Producttab
|
//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
|
let data = null; const elements = document.querySelectorAll('.ng-scope');
|
||||||
- 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
|
for (let i = 0; i < elements.length; i++) {
|
||||||
- Lịch sử bid đang có 2 lần trùng
|
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 * as fs from "fs";
|
||||||
import path from "path";
|
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 { GrayApiBid } from "../models/grays.com/grays-api-bid.js";
|
||||||
import { GraysProductBid } from "../models/grays.com/grays-product-bid.js";
|
import { GraysProductBid } from "../models/grays.com/grays-product-bid.js";
|
||||||
import { LangtonsApiBid } from "../models/langtons.com.au/langtons-api-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: {
|
case configs.WEB_URLS.PICKLES: {
|
||||||
return new PicklesProductBid({ ...data });
|
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: {
|
case configs.WEB_URLS.PICKLES: {
|
||||||
return new PicklesApiBid({ ...web });
|
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`,
|
LANGTONS: `https://www.langtons.com.au`,
|
||||||
LAWSONS: `https://www.lawsons.com.au`,
|
LAWSONS: `https://www.lawsons.com.au`,
|
||||||
PICKLES: `https://www.pickles.com.au`,
|
PICKLES: `https://www.pickles.com.au`,
|
||||||
|
ALLBIDS: `https://www.allbids.com.au`,
|
||||||
},
|
},
|
||||||
WEB_CONFIGS: {
|
WEB_CONFIGS: {
|
||||||
GRAYS: {
|
GRAYS: {
|
||||||
|
|
@ -39,6 +40,10 @@ const configs = {
|
||||||
API_CHECKOUT:
|
API_CHECKOUT:
|
||||||
"https://www.pickles.com.au/delegate/secured/bidding/confirm",
|
"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;
|
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