update
This commit is contained in:
parent
d12c6fbbfc
commit
3a9a4d092d
File diff suppressed because one or more lines are too long
|
|
@ -233,28 +233,6 @@ const clickPublist = async () => {
|
||||||
thiefService.clickByPoint(btn);
|
thiefService.clickByPoint(btn);
|
||||||
};
|
};
|
||||||
|
|
||||||
function extractListings(productsEl: HTMLElement) {
|
|
||||||
const children = Array.from(productsEl.children);
|
|
||||||
|
|
||||||
return children.map((child) => {
|
|
||||||
// Lấy title (thường là span hoặc div có dir="auto")
|
|
||||||
const titleEl = child.querySelector('span[dir="auto"], div[dir="auto"]');
|
|
||||||
const title = titleEl?.textContent?.trim() || "";
|
|
||||||
|
|
||||||
// Lấy giá (span có attribute dir="auto")
|
|
||||||
const priceEl = Array.from(child.querySelectorAll('span[dir="auto"]')).find(
|
|
||||||
(el) =>
|
|
||||||
/\d/.test(el.textContent || "") && /[AU$]/.test(el.textContent || "")
|
|
||||||
);
|
|
||||||
|
|
||||||
// Tách lấy số, ví dụ: "AU$20" -> "20"
|
|
||||||
const priceMatch = priceEl?.textContent?.match(/\d+(?:\.\d+)?/);
|
|
||||||
const price = priceMatch ? parseFloat(priceMatch[0]) : 0;
|
|
||||||
|
|
||||||
return { title, price, el: productsEl };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* B1. Upload images
|
* B1. Upload images
|
||||||
* B2. Write title
|
* B2. Write title
|
||||||
|
|
@ -333,19 +311,79 @@ const handle = async (item: IItem) => {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// function extractListings(productsEl: HTMLElement) {
|
||||||
|
// const children = Array.from(productsEl.children);
|
||||||
|
|
||||||
|
// return children.map((child) => {
|
||||||
|
// // Lấy title (thường là span hoặc div có dir="auto")
|
||||||
|
// const titleEl = child.querySelector('span[dir="auto"], div[dir="auto"]');
|
||||||
|
// const title = titleEl?.textContent?.trim() || "";
|
||||||
|
|
||||||
|
// // Lấy giá (span có attribute dir="auto")
|
||||||
|
// const priceEl = Array.from(child.querySelectorAll('span[dir="auto"]')).find(
|
||||||
|
// (el) =>
|
||||||
|
// /\d/.test(el.textContent || "") && /[AU$]/.test(el.textContent || "")
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // Tách lấy số, ví dụ: "AU$20" -> "20"
|
||||||
|
// const priceMatch = priceEl?.textContent?.match(/\d+(?:\.\d+)?/);
|
||||||
|
// const price = priceMatch ? parseFloat(priceMatch[0]) : 0;
|
||||||
|
|
||||||
|
// return { title, price, el: productsEl };
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const getProducts = async () => {
|
||||||
|
// let products = await thiefService.getElementByXPath(selectors.products);
|
||||||
|
// if (!products) {
|
||||||
|
// products = await thiefService.getElementByXPath(
|
||||||
|
// selectors.products_fallback
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!products) return [];
|
||||||
|
|
||||||
|
// return extractListings(products) as ISyncItem[];
|
||||||
|
// };
|
||||||
|
|
||||||
const getProducts = async () => {
|
const getProducts = async () => {
|
||||||
let products = await thiefService.getElementByXPath(selectors.products);
|
const products1 = await thiefService.getElementByXPath(selectors.products);
|
||||||
if (!products) {
|
const products2 = await thiefService.getElementByXPath(
|
||||||
products = await thiefService.getElementByXPath(
|
selectors.products_fallback
|
||||||
selectors.products_fallback
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!products) return [];
|
// Gom 2 cái vào một mảng, bỏ null
|
||||||
|
const allProductsEls = [products1, products2].filter(
|
||||||
|
Boolean
|
||||||
|
) as HTMLElement[];
|
||||||
|
|
||||||
return extractListings(products) as ISyncItem[];
|
if (allProductsEls.length === 0) return [];
|
||||||
|
|
||||||
|
// Nối tất cả kết quả extractListings từ mỗi element
|
||||||
|
return allProductsEls.flatMap((el) => extractListings(el)) as ISyncItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function extractListings(productsEl: HTMLElement) {
|
||||||
|
const children = Array.from(productsEl.children);
|
||||||
|
|
||||||
|
return children.map((child) => {
|
||||||
|
// Lấy title
|
||||||
|
const titleEl = child.querySelector('span[dir="auto"], div[dir="auto"]');
|
||||||
|
const title = titleEl?.textContent?.trim() || "";
|
||||||
|
|
||||||
|
// Lấy giá
|
||||||
|
const priceEl = Array.from(child.querySelectorAll('span[dir="auto"]')).find(
|
||||||
|
(el) =>
|
||||||
|
/\d/.test(el.textContent || "") && /[AU$]/.test(el.textContent || "")
|
||||||
|
);
|
||||||
|
|
||||||
|
const priceMatch = priceEl?.textContent?.match(/\d+(?:\.\d+)?/);
|
||||||
|
const price = priceMatch ? parseFloat(priceMatch[0]) : 0;
|
||||||
|
|
||||||
|
return { title, price, el: productsEl };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const syncListing = async () => {
|
const syncListing = async () => {
|
||||||
const url = window.location.href;
|
const url = window.location.href;
|
||||||
if (!url.includes("https://www.facebook.com/marketplace/you/selling")) return;
|
if (!url.includes("https://www.facebook.com/marketplace/you/selling")) return;
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ export interface Column<T> {
|
||||||
// Thêm interface cho custom actions
|
// Thêm interface cho custom actions
|
||||||
interface CustomAction<T> {
|
interface CustomAction<T> {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string | ((row: T) => ReactNode);
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
variant?: "default" | "secondary" | "destructive" | "outline";
|
variant?: "default" | "secondary" | "destructive" | "outline";
|
||||||
action: (row: T) => void;
|
action: (row: T) => void;
|
||||||
|
|
@ -1470,7 +1470,9 @@ export function DataTable<T extends Record<string, any>>({
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{action.icon && <span className="mr-2">{action.icon}</span>}
|
{action.icon && <span className="mr-2">{action.icon}</span>}
|
||||||
{action.label}
|
{typeof action.label === "string"
|
||||||
|
? action.label
|
||||||
|
: action.label(row)}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import {
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
Trash2,
|
Trash2,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
|
@ -12,7 +12,7 @@ import {
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "~/components/ui/dropdown-menu"
|
} from "~/components/ui/dropdown-menu";
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupLabel,
|
SidebarGroupLabel,
|
||||||
|
|
@ -21,18 +21,18 @@ import {
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from "~/components/ui/sidebar"
|
} from "~/components/ui/sidebar";
|
||||||
|
|
||||||
export function NavProjects({
|
export function NavProjects({
|
||||||
projects,
|
projects,
|
||||||
}: {
|
}: {
|
||||||
projects: {
|
projects: {
|
||||||
name: string
|
name: string;
|
||||||
url: string
|
url: string;
|
||||||
icon: LucideIcon
|
icon: LucideIcon;
|
||||||
}[]
|
}[];
|
||||||
}) {
|
}) {
|
||||||
const { isMobile } = useSidebar()
|
const { isMobile } = useSidebar();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||||
|
|
@ -83,5 +83,5 @@ export function NavProjects({
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
import { Outlet } from "react-router";
|
import { Outlet } from "react-router";
|
||||||
|
|
||||||
import { AppSidebar } from "~/components/app-sidebar";
|
import { AppSidebar } from "~/components/app-sidebar";
|
||||||
|
|
@ -14,6 +15,7 @@ import {
|
||||||
SidebarInset,
|
SidebarInset,
|
||||||
SidebarProvider,
|
SidebarProvider,
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
|
useSidebar,
|
||||||
} from "~/components/ui/sidebar";
|
} from "~/components/ui/sidebar";
|
||||||
|
|
||||||
export default function PrivateLayout() {
|
export default function PrivateLayout() {
|
||||||
|
|
@ -45,8 +47,9 @@ export default function PrivateLayout() {
|
||||||
// <Loader showLabel={true} size="size-10" />
|
// <Loader showLabel={true} size="size-10" />
|
||||||
// </div>
|
// </div>
|
||||||
// );
|
// );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<SidebarProvider open={false}>
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
<SidebarInset className="h-screen overflow-x-hidden">
|
<SidebarInset className="h-screen overflow-x-hidden">
|
||||||
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 sticky top-0 bg-white z-50">
|
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 sticky top-0 bg-white z-50">
|
||||||
|
|
|
||||||
|
|
@ -208,8 +208,10 @@ export default function List() {
|
||||||
// 🎯 Custom Actions - Đa dạng và có điều kiện
|
// 🎯 Custom Actions - Đa dạng và có điều kiện
|
||||||
const customActions = [
|
const customActions = [
|
||||||
{
|
{
|
||||||
key: "publist",
|
key: "listing",
|
||||||
label: "Publist",
|
label: (row: IProduct) => {
|
||||||
|
return row?.status ? "Unlist" : "Publist";
|
||||||
|
},
|
||||||
icon: <UploadCloud className="h-4 w-4" />,
|
icon: <UploadCloud className="h-4 w-4" />,
|
||||||
action: (data: IProduct) => {
|
action: (data: IProduct) => {
|
||||||
handlePublist(data);
|
handlePublist(data);
|
||||||
|
|
@ -259,11 +261,15 @@ export default function List() {
|
||||||
enabled: !!states,
|
enabled: !!states,
|
||||||
});
|
});
|
||||||
|
|
||||||
const publistMutation = useMutation({
|
const listingMutation = useMutation({
|
||||||
mutationFn: async (data: Partial<IProduct>) => {
|
mutationFn: async (data: Partial<IProduct>) => {
|
||||||
await delay(300);
|
await delay(300);
|
||||||
|
|
||||||
return productApi.customAction(data.id || 0, "publist", data);
|
if (data.status) {
|
||||||
|
return productApi.customAction(data.id || 0, "unlist", data);
|
||||||
|
} else {
|
||||||
|
return productApi.customAction(data.id || 0, "publist", data);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
console.log({ data });
|
console.log({ data });
|
||||||
|
|
@ -273,7 +279,7 @@ export default function List() {
|
||||||
|
|
||||||
const handlePublist = async (data: Partial<IProduct>) => {
|
const handlePublist = async (data: Partial<IProduct>) => {
|
||||||
toast.promise(
|
toast.promise(
|
||||||
publistMutation.mutateAsync(data), // gọi function để trả về promise
|
listingMutation.mutateAsync(data), // gọi function để trả về promise
|
||||||
{
|
{
|
||||||
loading: "Loading...",
|
loading: "Loading...",
|
||||||
success: (result) => `${data?.title} toast has been added`,
|
success: (result) => `${data?.title} toast has been added`,
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,11 @@ export class ProductsController extends CoreController<
|
||||||
return this.service.publist(id);
|
return this.service.publist(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('unlist/:id')
|
||||||
|
async unlist(@Param('id') id: Product['id']): Promise<any> {
|
||||||
|
return this.service.unlist(id);
|
||||||
|
}
|
||||||
|
|
||||||
@Post('publist-finish/:id')
|
@Post('publist-finish/:id')
|
||||||
async publistFinish(
|
async publistFinish(
|
||||||
@Param('id') id: Product['id'],
|
@Param('id') id: Product['id'],
|
||||||
|
|
|
||||||
|
|
@ -74,28 +74,28 @@ export class ProductsService extends CoreService<Product> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async beforeDelete(
|
// protected async beforeDelete(
|
||||||
id: number,
|
// id: number,
|
||||||
data: Partial<Product>,
|
// data: Partial<Product>,
|
||||||
req: Request,
|
// req: Request,
|
||||||
): Promise<boolean> {
|
// ): Promise<boolean> {
|
||||||
// Emit sự kiện để bắt đầu publish
|
// // Emit sự kiện để bắt đầu publish
|
||||||
this.eventService.sendEvent(ProductsService.EVENTS.SEND_DELETE, {
|
// this.eventService.sendEvent(ProductsService.EVENTS.SEND_DELETE, {
|
||||||
...data,
|
// ...data,
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Đợi phản hồi từ client
|
// // Đợi phản hồi từ client
|
||||||
const result = await this.waitForDeleteResult({ ...data, id });
|
// const result = await this.waitForDeleteResult({ ...data, id });
|
||||||
|
|
||||||
if (!result)
|
// if (!result)
|
||||||
throw new BadRequestException(
|
// throw new BadRequestException(
|
||||||
AppResponse.toResponse(false, {
|
// AppResponse.toResponse(false, {
|
||||||
message: SystemLang.getText('messages', 'try_again'),
|
// message: SystemLang.getText('messages', 'try_again'),
|
||||||
}),
|
// }),
|
||||||
);
|
// );
|
||||||
|
|
||||||
return true;
|
// return true;
|
||||||
}
|
// }
|
||||||
|
|
||||||
async create(data: CreateProductDto): Promise<any> {
|
async create(data: CreateProductDto): Promise<any> {
|
||||||
const product = this.repo.create(data);
|
const product = this.repo.create(data);
|
||||||
|
|
@ -197,6 +197,32 @@ export class ProductsService extends CoreService<Product> {
|
||||||
return AppResponse.toResponse(plainData);
|
return AppResponse.toResponse(plainData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async unlist(id: Product['id']) {
|
||||||
|
const product = await this.repo.findOne({ where: { id } });
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
AppResponse.toResponse(null, {
|
||||||
|
message: SystemLang.getText('messages', 'not_found'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const plainData = plainToClass(Product, product);
|
||||||
|
|
||||||
|
// Emit sự kiện để bắt đầu publish
|
||||||
|
this.eventService.sendEvent(ProductsService.EVENTS.SEND_DELETE, {
|
||||||
|
...plainData,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Đợi phản hồi từ client
|
||||||
|
const publistResult = await this.waitForDeleteResult(plainData);
|
||||||
|
|
||||||
|
await this.repo.update(plainData.id, { status: false });
|
||||||
|
|
||||||
|
return AppResponse.toResponse(plainData);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Chờ event PUBLIST_FINISH và bắt lỗi nếu quá thời gian hoặc lỗi bất ngờ
|
* Chờ event PUBLIST_FINISH và bắt lỗi nếu quá thời gian hoặc lỗi bất ngờ
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue