auto-post-marketplace-facebook/src/popup/popup.tsx

420 lines
15 KiB
TypeScript

import { productApi } from "@/api/product-api.service";
import { ImprovedToggleFilter } from "@/components/improved-toggle-filter";
import Loader from "@/components/loader";
import ProductModal from "@/components/product-modal";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { removeFalsyValues } from "@/features/app";
import { mapToIPost, type IPost } from "@/lib/utils";
import { useMutation, useQuery } from "@tanstack/react-query";
import {
ChevronLeft,
ChevronRight,
DoorOpenIcon,
Edit,
Eye,
EyeOff,
MoreHorizontal,
RefreshCcwIcon,
Search,
} from "lucide-react";
import { useMemo, useState } from "react";
import { useDebounce } from "use-debounce";
export default function Popup() {
const [searchTerm, setSearchTerm] = useState("");
const [filter, setFilter] = useState<Record<string, any>>({});
const [currentPage, setCurrentPage] = useState(1);
const [search] = useDebounce(searchTerm, 400);
const queryKey = useMemo(
() => ["products", { currentPage, search, filter }],
[currentPage, search, filter]
);
// --- React Query fetch ---
const {
data: rawProducts,
isFetching,
...dataQuery
} = useQuery({
queryKey,
queryFn: async () => {
const data = await productApi.apiRequest(
"index",
removeFalsyValues({
skip: (currentPage - 1) * productApi.item_per_page,
where: {
productModelCode: searchTerm,
status_listing:
filter?.statusFilter === "all" ? undefined : filter.statusFilter,
},
})
);
return data;
},
});
const { data: publistedProducts, ...publistQuery } = useQuery({
queryKey: ["publised-products"],
queryFn: async () => {
const data = await productApi.apiRequest("getPublistedProducts", {});
return data ?? [];
},
staleTime: 0, // luôn coi là stale -> gọi lại API mỗi lần mount
refetchOnMount: "always",
});
const actionMutation = useMutation({
mutationKey: ["action-mutaions"],
mutationFn: async (data: IPost) => {
if (data.status) {
return productApi.apiRequest("unlist", data);
}
const res = await productApi.apiRequest("get", data);
if (!res || !(res as any)?.data) return;
return productApi.apiRequest("publist", {
...data,
images: mapToIPost({ ...(res as any)?.data }).images,
});
},
});
const data: IPost[] = useMemo(() => {
if (!rawProducts || !(rawProducts as any)?.data) return [];
return (rawProducts as any)?.data.map((item: any) => mapToIPost(item));
}, [rawProducts]);
const formatPrice = (price: number) =>
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(price);
const clearFilters = () => {
setSearchTerm("");
setFilter({ statusFilter: "" });
setCurrentPage(1);
};
const activeFiltersCount = [filter.statusFilter, searchTerm !== ""].filter(
Boolean
).length;
const totalPages = useMemo(() => {
if (!(rawProducts as any)?.total) return 0;
return Math.ceil((rawProducts as any).total / productApi.item_per_page);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [(rawProducts as any)?.total, productApi.item_per_page]);
// --- reset page when filter changes ---
// eslint-disable-next-line react-hooks/exhaustive-deps
const from = useMemo(() => {
return (currentPage - 1) * productApi.item_per_page + 1;
}, [currentPage]);
const to = useMemo(() => {
return Math.min(
currentPage * productApi.item_per_page,
(rawProducts as any)?.total ?? 0
);
}, [currentPage, rawProducts]);
const handleActionListing = async (data: IPost) => {
console.log({ post: data });
actionMutation.mutate(data);
};
return (
<Popover>
<PopoverTrigger asChild>
<Button size={"icon"}>
<DoorOpenIcon />
</Button>
</PopoverTrigger>
<PopoverContent side="top" align="end" className="w-auto">
<div className="space-y-4 min-h-[722px] min-w-[638px]">
<div className="flex items-center justify-between gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<ImprovedToggleFilter
filter={filter as any}
setFilter={setFilter}
activeFiltersCount={activeFiltersCount}
clearFilters={clearFilters}
/>
</div>
<div className="flex items-center justify-between w-full gap-8">
<span>
Showing {from}-{to} of {(rawProducts as any)?.total ?? 0} products
</span>
{totalPages > 1 && (
<div className="flex items-center justify-end gap-2">
<Button
variant="outline"
size="xs"
onClick={() =>
setCurrentPage((prev) => Math.max(1, prev - 1))
}
disabled={currentPage === 1}
>
<ChevronLeft />
Previous
</Button>
<div className="flex items-center gap-1">
{totalPages <= 7 ? (
// Show all pages if 7 or fewer
Array.from({ length: totalPages }, (_, i) => (
<Button
key={i + 1}
variant={currentPage === i + 1 ? "default" : "outline"}
size="xs"
onClick={() => setCurrentPage(i + 1)}
>
{i + 1}
</Button>
))
) : (
// Show pages with ellipsis for more than 7 pages
<>
{/* First page */}
<Button
variant={currentPage === 1 ? "default" : "outline"}
size="xs"
onClick={() => setCurrentPage(1)}
>
1
</Button>
{/* Left ellipsis */}
{currentPage > 4 && (
<span className="px-2 text-muted-foreground">...</span>
)}
{/* Middle pages */}
{Array.from({ length: 3 }, (_, i) => {
let pageNum;
if (currentPage <= 4) {
pageNum = i + 2;
} else if (currentPage >= totalPages - 3) {
pageNum = totalPages - 4 + i;
} else {
pageNum = currentPage - 1 + i;
}
if (pageNum > 1 && pageNum < totalPages) {
return (
<Button
key={pageNum}
variant={
currentPage === pageNum ? "default" : "outline"
}
size="xs"
onClick={() => setCurrentPage(pageNum)}
>
{pageNum}
</Button>
);
}
return null;
}).filter(Boolean)}
{/* Right ellipsis */}
{currentPage < totalPages - 3 && (
<span className="px-2 text-muted-foreground">...</span>
)}
{/* Last page */}
<Button
variant={
currentPage === totalPages ? "default" : "outline"
}
size="xs"
onClick={() => setCurrentPage(totalPages)}
>
{totalPages}
</Button>
</>
)}
</div>
<Button
variant="outline"
size="xs"
onClick={() =>
setCurrentPage((prev) => Math.min(totalPages, prev + 1))
}
disabled={currentPage === totalPages}
>
Next
<ChevronRight />
</Button>
</div>
)}
</div>
<div className="rounded-md border overflow-x-auto relative">
<Table className="min-w-[500px]">
<TableHeader>
<TableRow>
<TableHead className="min-w-[200px]">Name</TableHead>
<TableHead className="w-[80px]">Price</TableHead>
<TableHead className="w-[80px]">Status</TableHead>
<TableHead className="w-[80px] text-center sticky right-0 bg-background border-l shadow-[-4px_0_8px_rgba(0,0,0,0.1)]">
Actions
</TableHead>
</TableRow>
</TableHeader>
<TableBody className="relative">
{isFetching && (
<TableRow>
<TableCell
colSpan={10}
className="text-center py-8 text-muted-foreground "
>
<div className="h-full flex items-center justify-center w-full">
<Loader />
</div>
</TableCell>
</TableRow>
)}
{data.length === 0 && !isFetching ? (
<TableRow>
<TableCell
colSpan={10}
className="text-center py-8 text-muted-foreground"
>
<div className="h-full flex items-center justify-center w-full">
<span> No products found</span>
</div>
</TableCell>
</TableRow>
) : (
data.map((post) => {
const status = (publistedProducts as any)?.some(
(item: any) => item.title.includes(post.sku)
);
post.status = status;
return (
<TableRow key={post.id}>
<TableCell className="font-medium">
<div
className="truncate max-w-[340px] w-fit"
title={post.title}
>
{post.title}
</div>
<div
className="text-sm text-muted-foreground truncate max-w-[340px]"
title={post.description}
>
{post.description}
</div>
</TableCell>
<TableCell className="font-semibold">
{formatPrice(post.price)}
</TableCell>
<TableCell>
<Badge variant="secondary">
{post?.status ? "Listed" : "Unlisted"}
</Badge>
</TableCell>
<TableCell className="text-center sticky right-0 bg-background border-l shadow-[-4px_0_8px_rgba(0,0,0,0.1)]">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="w-full flex items-center justify-center">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 mx-auto"
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<ProductModal data={post}>
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault(); // Ngăn dropdown đóng lại
e.stopPropagation();
}}
>
<Edit className="h-4 w-4 mr-2" /> Edit
</DropdownMenuItem>
</ProductModal>
<DropdownMenuItem
onClick={() => handleActionListing(post)}
>
{post.status ? (
<EyeOff className="h-4 w-4 mr-2" />
) : (
<Eye className="h-4 w-4 mr-2" />
)}
{post.status ? "Unlist" : "List"}
{/* {actionMutation.isPending && <Loader />} */}
</DropdownMenuItem>
{/* <DropdownMenuItem onClick={() => handleUnListing(post)}>
unList
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleRePublist(post)}>
re publist
</DropdownMenuItem> */}
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</div>
</div>
</PopoverContent>
</Popover>
);
}